Repository navigation
feat: authenticate service-to-API calls (run token + service keys) - #1558
Closed
Josh Duffney (duffney) wants to merge 8 commits into
Closed
Josh Duffney (duffney) wants to merge 8 commits into
Josh Duffney (duffney) wants to merge 8 commits into
Conversation
Add an opaque per-run bearer credential used by workers when calling the API: - `scope_rt_<requestId>_<secret><checksum>` with 32 random bytes of secret and a base62 CRC32 checksum so malformed tokens are rejected without a DB read. - Only a hex SHA-256 digest is meant to be persisted; digests are compared in constant time. - `BearerProvider`, `bearerHeaders` and `fetchWithBearer` let API clients add `Authorization: Bearer` only when a token is available, leaving existing no-token calls unchanged. Refs #1557 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Recognize two non-user credentials before IdP verification and attach them as a separate `req.principal`, leaving `req.auth`/`req.user` semantics unchanged: - `Authorization: Bearer scope_rt_…` is verified with a point read on the request: hash must match `run.tokenHash`, the run must be `processing`, and the claim must be younger than RUN_TOKEN_MAX_AGE_MS (default 24h). Forged, inactive or expired tokens return 401; a DB failure returns 503. The IdP is never called for run tokens. - `X-Service-Name` + `X-Internal-Key` are verified against per-service keys (INTERNAL_API_KEY_REGISTER_AGENTS, INTERNAL_API_KEY_MODEL_SCANNER). Missing or short keys disable that service (fail closed). Scopes are recorded on the principal only; enforcement is left to the authorization layer. - Sending both service headers and a bearer returns 400 ambiguous_credentials. - When auth is disabled, all credentials are ignored as before. Also adds `getPrincipal(req)`, server-only `RunState.tokenHash` (kept out of the zod schema) and a metadata-only `runToken` route flag that emits `x-scope-run-token` in OpenAPI. Requests without credentials behave exactly as before. Refs #1557 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…nHash - Flag the 14 GET routes workers and the judge call during a run with `runToken: true` (MCP server, skill revision by ref + archive, extension, resource + revisions, task prompt + content, codebase revision + archive, criteria list + by id). An inventory test pins the exact list. - Never expose the run-token digest: strip `run.tokenHash` from request get/list, attempt list/detail, history snapshots and run archives. Refs #1557 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ents The queue processor mints an opaque run token when it atomically claims a run, persists only its SHA-256 digest in the claim ($set run.tokenHash), and keeps the raw token in memory for the life of the handling. MCP, skill, extension, resource, prompt, codebase, gate-prompt and judge clients send it as a bearer via an optional BearerProvider. The digest is unset on the terminal success and error writes, and the in-memory token is dropped when handling ends. Clients without a bearer behave exactly as before. Refs #1557 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The judge extracts a well-formed run token from the evaluate request's Authorization header and, when present, builds a per-evaluation REST criteria provider that sends it as a bearer. Requests without a run token keep using the cached per-project provider. RestApiCriteriaProvider and createCriteriaProvider accept an optional BearerProvider. Refs #1557 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
register-agent.sh sends X-Service-Name: register-agents and X-Internal-Key from a private header file when SCOPE_INTERNAL_API_KEY is set, so the key never appears on the curl command line. Model-scanner fetch/upsert/sync calls send the model-scanner principal headers the same way. Without a key both callers behave exactly as before. Compose passes INTERNAL_API_KEY_REGISTER_AGENTS and INTERNAL_API_KEY_MODEL_SCANNER to the API and the matching key to each caller; .env.example documents the variables. A missing key on the API side disables that service principal (fail closed). Scopes are identified only; enforcement lands with RBAC. Refs #1557 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adds unit tests for the run-token verifier (valid claim, malformed, forged, deleted/missing digest, not processing, expired, store unavailable), the per-service key verifier (per-service keys and scopes, wrong/foreign/unknown keys, fail closed when unset or too short), and the auth middleware (run and service callers attached and logged without secrets, IdP and user access skipped, 401/400/503 mappings, no-token requests still anonymous, credentials ignored while auth is disabled). Refs #1557 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
auth-rbac.md §6 now describes the implemented run token (mint at claim, digest at rest, verification rules, failure codes, redaction, runToken route metadata) and per-service keys (headers, fail-closed rules, service table), and notes that enforcement arrives with route-level authorization. ENV_VARIABLES.md documents RUN_TOKEN_MAX_AGE_MS, INTERNAL_API_KEY_REGISTER_AGENTS, INTERNAL_API_KEY_MODEL_SCANNER and SCOPE_INTERNAL_API_KEY. Refs #1557 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This was referenced Oct 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Authenticates service-to-API calls. Authentication only: route-level authorization (RBAC v2) will be stacked on top of this PR.
Refs #1557
Run tokens
scope_rt_<requestId>_<secret><checksum>token when it claims a run. The secret is 256 bits; the checksum is CRC-32.run.tokenHash. The worker's terminal writes$unsetit.processing;RUN_TOKEN_MAX_AGE_MS.401.runToken: true(exposed asx-scope-run-tokenin OpenAPI).run.tokenHashis removed from all run responses and history.Per-service keys
register-agentsand the model scanners sendX-Service-NameandX-Internal-Key, but only whenSCOPE_INTERNAL_API_KEYis set.INTERNAL_API_KEY_REGISTER_AGENTSandINTERNAL_API_KEY_MODEL_SCANNER. Keys must be at least 32 characters and are compared in constant time.401).Caller identity
req.principal(user|run|service).req.authandreq.userkeep their existing semantics. Run and service callers get the anonymous user and noreq.auth.Implementation choices
Authorizationreturn400 ambiguous_credentials.finallyblock.agents:read,agents:register,models:sync.Demos
N/A: no Portal or CLI changes.
Before
N/A
After
N/A
Testing
pnpm --filter "./packages/**" build: passedpnpm exec tsc --noEmit -p .inapps/api,apps/judge,packages/shared,packages/model-scanning,apps/workers/coder-acp-copilot,apps/workers/coder-acp-claude-code,apps/model-scanners/copilotandapps/model-scanners/anthropic: passedpnpm exec vitest run apps/api packages/shared apps/judge packages/model-scanning scripts/register-agent.test.ts apps/workers: passed (133 files, 2216 tests)docker compose config -q: passedpnpm generate:openapi(inapps/api): regeneratedwebsite/src/openapi/scope-openapi.json; the snapshot test passeslintscripts were not run:eslintis not installed in the workspace (eslint: command not found).Documentation and compatibility
docs/architecture/auth-rbac.md§6 covers run tokens and per-service keys, with the authentication flow, failure codes and the service table.ENV_VARIABLES.mdaddsRUN_TOKEN_MAX_AGE_MS,INTERNAL_API_KEY_REGISTER_AGENTS,INTERNAL_API_KEY_MODEL_SCANNERandSCOPE_INTERNAL_API_KEY..env.exampleis updated.SCOPE_INTERNAL_API_KEYon each caller. A caller without a key stays anonymous, as before. A caller that sends a key the API doesn't have configured gets401.Checklist
up()/down()and keep it CosmosDB-compatible. (run.tokenHashis optional and transient.)NOTICE/NOTICE-REVIEW.txtwithpnpm noticeas needed.