diff --git a/docs/adr/0005-result-envelope-and-errors.md b/docs/adr/0005-result-envelope-and-errors.md index 086dbbc..e6b2894 100644 --- a/docs/adr/0005-result-envelope-and-errors.md +++ b/docs/adr/0005-result-envelope-and-errors.md @@ -27,3 +27,72 @@ Every successful tool call returns one text content item with JSON: Messages are truncated and stripped of tokens and the Authorization header. - Argument validation failures become `is_error=True` with the jsonschema path. - Unknown tool names raise `MCPError(INVALID_PARAMS)`. + +## Amendments + +### Secret handling in results (implemented in `gitea/executor.py`, `gitea/encoding.py`) + +Two layers run on every result, success and error, before the size limit: + +1. Exact redaction (always on, independent of `SECRET_DETECTION_MODE`). The + following values are replaced with `[REDACTED]` wherever they occur in a + string or dict key of the payload: the token of the calling identity, + `GITEA_TOKEN`, `MCP_BEARER_TOKEN`, `GITEA_OAUTH_CLIENT_SECRET`, the + `extra_secrets` given to the executor, and secret values submitted in the + request body of the same call. +2. Pattern masking (`SECRET_DETECTION_MODE`). `off` skips it. `mask` replaces + each match of the secret patterns (provider keys, JWTs, private key headers, + generic `api_key`/`token` assignments) with a shortened form. `block` + withholds the whole result and returns an `is_error` result of kind + `secret_blocked`. + +Request-body secret fields. Values of these fields, when at least 4 characters +long, are collected from the submitted body (nested objects and lists up to a +depth of 8) and redacted from the result of that call, because Gitea echoes +some of them back (a created webhook returns its `authorization_header`): +`password`, `secret`, `client_secret`, `token`, `access_token`, `auth_password`, +`auth_token`, `private_key`, `authorization_header`, `remote_password`, +`mirror_password`, `mirror_token`, `aws_secret_access_key`, `aws_access_key_id`. +For Action secret writes the `data` value is redacted at any length. Only the +value sent in the same call is known to the executor, so a later read of an +object that stores such a value is not covered by this rule (pattern masking +still applies). + +Credential-issuing operations. The successful results of +`CREDENTIAL_ISSUING_OPERATIONS` skip pattern masking and block mode, because +their whole purpose is to hand the caller a freshly minted credential that +masking would make unusable (and block mode would refuse to deliver): + +- `userCreateToken` (access token) +- `adminCreateRunnerRegistrationToken`, `orgCreateRunnerRegistrationToken`, + `repoCreateRunnerRegistrationToken`, `userCreateRunnerRegistrationToken` + (Actions runner registration tokens, delivered in a response header) +- `userCreateOAuth2Application`, `userUpdateOAuth2Application` (OAuth2 + `client_secret`) +- `repoCreateDeployToken` (deploy token) + +Exact redaction still applies to these results, so the caller's own tokens are +never echoed, and their errors are masked like every other error. Operators who +do not want any credential to pass through the model context should disable +these tools with `DISABLED_TOOLS`. + +Hardening of the result path: JSON from Gitea nested deeper than 100 levels is +returned as text instead of being parsed (the recursive redaction and masking +passes must not hit the interpreter recursion limit), and a base64 file upload +whose decoded size would exceed `MAX_BINARY_BYTES` is rejected before it is +decoded with kind `payload_too_large`. + +### Path encoding and the raw tool + +Path values are percent-encoded with `/` escaped as `%2F`, except for the +parameters that Gitea 28.1.0 serves through wildcard routes (branch, tag, file +path, ref and compare names on `branches`, `tags`, `contents`, `contents-ext`, +`raw`, `media`, `compare`, `git/refs`, `commits/{ref}/status(es)` and +`pulls/{base}/{head}`). The decision is keyed on the route and the parameter, +not the parameter name: `releases/tags/{tag}` and `editorconfig/{filepath}` are +single-segment routes that only resolve with `%2F`. + +The raw tool `gitea_api_request` rejects `%` in paths, so it cannot send +`%2F`. A route with a single-segment parameter whose value contains a slash +(a release tag such as `rel/1.0`) must be called through the generated tool for +that operation.