feat(mcp): add not_implemented CredError for stub arms (Greptile P2)

Stub arms returned of_misconfigured, which is documented as an operator config error (500) -
so an operator configuring a not-yet-built mode would get a misleading 'misconfigured' 500.
Add a dedicated not_implemented variant (-> 501 in http_status) and switch _todo to it; the
exhaustiveness gate forced the new arm in summary and http_status. Tests assert the stub arms
now signal not_implemented and that it maps to 501.
This commit is contained in:
Tin Chi Lo 2026-06-17 17:51:30 -07:00
parent 0d6b9ab86f
commit 109ba26397
4 changed files with 18 additions and 4 deletions

View file

@ -63,6 +63,8 @@ def http_status(err: CredError) -> int:
return 500
case "precondition_required":
return 412
case "not_implemented":
return 501
assert_never(err.tag)

View file

@ -205,5 +205,5 @@ def _bearer(token: StoredToken) -> StaticHeaderAuth:
def _todo(kind: AuthSpecKind) -> Result[httpx.Auth, CredError]:
return Error(
CredError.of_misconfigured(f"{kind.value}: resolver arm not implemented yet")
CredError.of_not_implemented(f"{kind.value}: resolver arm not implemented yet")
)

View file

@ -68,6 +68,7 @@ class CredError:
"upstream_unavailable",
"unsupported_mode",
"precondition_required",
"not_implemented",
] = tag()
unauthorized: str = (
@ -85,6 +86,9 @@ class CredError:
precondition_required: str = (
case()
) # a required per-user value (e.g. an env var) has not been provided -> 412
not_implemented: str = (
case()
) # the declared mode's resolver arm is not built yet -> 501 (not operator error)
@staticmethod
def of_unauthorized(detail: str) -> CredError:
@ -106,6 +110,10 @@ class CredError:
def of_precondition_required(detail: str) -> CredError:
return CredError(precondition_required=detail)
@staticmethod
def of_not_implemented(detail: str) -> CredError:
return CredError(not_implemented=detail)
@property
def summary(self) -> str:
# Exhaustiveness: every Literal tag has an arm; the trailing assert_never typechecks
@ -121,6 +129,8 @@ class CredError:
return self.unsupported_mode
case "precondition_required":
return f"precondition required: {self.precondition_required}"
case "not_implemented":
return f"not implemented: {self.not_implemented}"
assert_never(self.tag)

View file

@ -192,10 +192,11 @@ async def test_api_key_per_user_isolated_by_subject():
assert result.error.tag == "unauthorized"
def test_missing_status_maps_byok_401_distinct_from_env_var_412():
# The two per-user sources surface different HTTP statuses at the edge.
def test_crederror_maps_to_distinct_http_statuses():
# Each failure class surfaces its own HTTP status at the edge.
assert http_status(CredError.of_unauthorized("byok missing")) == 401
assert http_status(CredError.of_precondition_required("env var missing")) == 412
assert http_status(CredError.of_not_implemented("stub arm")) == 501
async def test_passthrough_forwards_the_inbound_token():
@ -242,9 +243,10 @@ async def test_self_contained_arms_never_read_the_inbound_token():
],
)
async def test_unimplemented_arms_fail_closed(config: dict):
# Stub arms signal not_implemented (-> 501), not misconfigured (-> 500 operator error).
result = await PROVIDER.resolve(SUBJECT, _spec(config))
assert isinstance(result, Error)
assert result.error.tag == "misconfigured"
assert result.error.tag == "not_implemented"
async def test_authorization_code_returns_a_valid_stored_token():