From c124ecdb397e02552761f32ea30eed4559ca97fd Mon Sep 17 00:00:00 2001 From: Joshua Gilman Date: Mon, 24 Aug 2026 10:37:34 -0700 Subject: [PATCH] docs(security): define exact error boundary --- docs/docs/explanation/security-model.md | 17 ++++++++--------- docs/docs/reference/mcp-tools.md | 16 ++++++++-------- docs/docs/reference/public-api.md | 4 +++- mcpserver/server.go | 2 +- server.go | 3 ++- 5 files changed, 22 insertions(+), 20 deletions(-) diff --git a/docs/docs/explanation/security-model.md b/docs/docs/explanation/security-model.md index 9ad9f90..8b88f5a 100644 --- a/docs/docs/explanation/security-model.md +++ b/docs/docs/explanation/security-model.md @@ -68,18 +68,17 @@ Static filtering is useful for deployment-wide availability, but it is not dynam ## Client errors are intentionally coarse -The root API exposes stable sentinel classifications. The MCP adapter projects failures to short text such as `permission denied`, `capability failed`, `resource limit exceeded`, and `internal failure`. +Detailed causes exist only on the trusted side of the public boundary. Internal packages and host authorizers, resolvers, and handlers can hold or log those causes. A direct call to `authz/rego.Authorize` can return an ordinary error that identifies an undefined or non-Boolean decision, or carries an OPA evaluation or builtin failure. -This projection prevents trusted diagnostic detail from becoming model-visible. In particular, the MCP response does not forward: +`codemode.Server.Execute` removes those trusted causes. It returns the documented public sentinel for execution, policy, handler, resource, and internal failures. Request cancellation returns `context.Canceled`. A deadline returns `ErrResourceLimit` and preserves `context.DeadlineExceeded` for `errors.Is`; no other execution cause remains wrapped at the root API. -- resolver and authentication diagnostics -- policy reasons -- handler error text -- panic values or stack details -- credentials -- source or argument values copied into wrapped errors +The MCP adapter narrows the boundary again. It emits only the fixed error texts in the [MCP tool reference](../reference/mcp-tools.md#errors). Resolver and custom-service details and recovered panic values become coarse responses. SDK input-schema errors are different: they occur before trusted subject resolution and can identify malformed client-owned fields or values. -The host can log trusted details on its side of the boundary if its authorizer, resolver, and handlers implement that logging. CodeMode's client response remains coarse. Unknown service errors and recovered adapter panics become `internal failure`. A nil `Server.Execute` context is a caller-contract violation and is currently classified as `ErrInternal`. +This projection prevents trusted diagnostic detail from becoming model-visible. In particular, MCP responses do not expose budget values, filtered capability identities, unknown requested names, argument names or values, source locations or text, Rego decision paths or rule names, handler messages, credentials, panic values, or stack details. + +If a host needs detailed diagnostics, its trusted authorizer, resolver, or handler must record them before returning. CodeMode cannot recover a discarded cause after the root or MCP projection. Apply the host's normal access controls and redaction rules to those logs. + +An allowed or denied call proves only the result for that subject, capability, and canonical argument set. It does not show whether the policy is default-open, default-deny, complete, or incomplete. Treat an unexpected denial or policy failure as a reason to contact the host, not as evidence about the policy's rules or defaults. ## Execution state does not cross calls diff --git a/docs/docs/reference/mcp-tools.md b/docs/docs/reference/mcp-tools.md index d30878b..c67ab16 100644 --- a/docs/docs/reference/mcp-tools.md +++ b/docs/docs/reference/mcp-tools.md @@ -205,19 +205,19 @@ Only the final converted value crosses the execution boundary. `print` output is ## Authoring and recovery -The listed descriptions above are the model-facing contract. Recovery uses the same fixed coarse errors on this page. Error payloads stay non-disclosing; they do not gain diagnostic detail, aliases, or suggested alternate names. +The listed descriptions above are the model-facing contract. Recovery uses the same fixed coarse errors on this page. When recording or reporting a failed call, keep the coarse text and the recovery action; do not echo the failed source, arguments, credentials, or unknown requested name. - Search with a short literal substring over enabled names and summaries. If the result is empty, retry with a shorter term. -- Pass `describe_api` the exact `name` returned by `search_api`, without whitespace or case changes. -- Compare native call arguments with the `describe_api` field shapes before `execute`. +- After `capability not found`, search again and pass `describe_api` an exact returned `name`, without whitespace or case changes. +- After `invalid capability arguments`, compare the call with the published `signature` and `input` field shapes. +- After `invalid program`, use a zero-argument `def main():`, call only names confirmed through search and description inside `main`, and return the final value. - Use `describe_api.output` as the result contract. Do not parse a type name from `signature`. -- Define `def main():` with zero arguments. Call only names confirmed through `search_api` and `describe_api` inside `main`. Return `main`'s final result. -- After `resource limit exceeded`, reduce program or result complexity and retry. -- `permission denied` and `authorization policy failure` are outcomes for that call only. They do not disclose policy rules. +- After `resource limit exceeded`, reduce source or result complexity and retry. +- After `permission denied` or `authorization policy failure`, contact the host if the access was expected. One allowed or denied input cannot establish whether the policy is default-open, default-deny, complete, or incomplete. ## Errors -After a well-formed call reaches the adapter, a resolver or service failure becomes a successful MCP protocol response with `isError` set and one coarse text item. The adapter does not expose wrapped policy diagnostics, handler errors, panic values, stack details, credentials, source, or arguments. +After a well-formed call reaches the adapter, a resolver or service failure becomes a successful MCP protocol response with `isError` set and one of the eleven fixed text values below. The adapter removes resolver and custom-service details and recovered panic values. It does not expose budget values, filtered capability identities, unknown requested names, argument names or values, source locations or text, Rego decision paths or rule names, handler messages, credentials, panic values, or stack details. | Text | Meaning | | --- | --- | @@ -233,6 +233,6 @@ After a well-formed call reaches the adapter, a resolver or service failure beco | `context deadline exceeded` | A service returned a bare deadline error. Root CodeMode execution deadlines are normally projected as `resource limit exceeded`. | | `internal failure` | Any unknown service error or recovered adapter failure. | -Malformed MCP arguments are rejected by the SDK's input-schema validation and do not call the invocation resolver. For well-formed inputs, resolver failure stops the request before search, description, or execution. +Malformed MCP arguments are rejected by the SDK's input-schema validation and do not call the invocation resolver. These errors can identify malformed client-owned fields or values because validation occurs before trusted resolution. For well-formed inputs, resolver failure stops the request before search, description, or execution and becomes `unauthenticated` without resolver detail. See [Public API reference](public-api.md) for Go contracts and [Security model](../explanation/security-model.md) for the resolver and execution trust boundaries. diff --git a/docs/docs/reference/public-api.md b/docs/docs/reference/public-api.md index 7442da1..056dd9e 100644 --- a/docs/docs/reference/public-api.md +++ b/docs/docs/reference/public-api.md @@ -194,7 +194,7 @@ Printed text, globals, and intermediate values do not cross the execution bounda ### Error classifications -Use `errors.Is` to inspect these exported sentinel errors. Client adapters can replace wrapped diagnostic detail with the sentinel text. +Use `errors.Is` to inspect these exported sentinel errors. `Server.Execute` removes trusted wrapped causes and returns only the classifications below. Client adapters must not depend on diagnostic text or an unwrapped internal, policy, or handler error. | Error | Classification | | --- | --- | @@ -211,6 +211,8 @@ Use `errors.Is` to inspect these exported sentinel errors. Client adapters can r Request cancellation returns `context.Canceled`. A deadline is classified as `ErrResourceLimit` and also wraps `context.DeadlineExceeded` at the root API. +Internal packages and trusted authorizer and handler implementations can hold detailed causes before this projection. A direct `authz/rego.Authorize` call can return an ordinary error that names an undefined or non-Boolean decision or wraps an OPA evaluation failure. `Server.Execute` maps that error to `ErrPolicyFailure` without retaining the cause. If the host needs the detail, record it inside the trusted implementation before returning. + ## `authz` ### Subjects diff --git a/mcpserver/server.go b/mcpserver/server.go index d51a68f..15df710 100644 --- a/mcpserver/server.go +++ b/mcpserver/server.go @@ -198,7 +198,7 @@ func resolveSubject(ctx context.Context, resolver InvocationResolver) (authz.Sub return subject, nil } -// projectToolError maps a service failure to stable coarse tool-safe text. +// projectToolError removes wrapped service detail and maps failures to fixed client-visible sentinels. func projectToolError(err error) error { switch { case errors.Is(err, codemode.ErrResourceLimit): diff --git a/server.go b/server.go index a586227..f5b74c0 100644 --- a/server.go +++ b/server.go @@ -111,7 +111,8 @@ func (server *Server) Execute(ctx context.Context, subject authz.Subject, progra return result, nil } -// projectExecutionError converts trusted internal execution failures to the public safe taxonomy. +// projectExecutionError removes trusted execution causes at the root boundary. +// It preserves only safe sentinels and documented context cancellation and deadline wrapping. func projectExecutionError(err error) error { switch { case errors.Is(err, execution.ErrInvalidProgram):