Repository navigation
feat: scope jobs to the token that submitted them - #83
Merged
Merged
Conversation
The worker takes its tokens from BRIDGE_TOKEN (named default), BRIDGE_TOKENS or BRIDGE_TOKENS_FILE, at most 64, each checked at start with a message that names the variable and never a token, and both token variables leave the environment before the first child starts. A job belongs to the token that submitted it: another token reads and cancels it as 404, exactly like an unknown job, and idempotency keys are per token. The owner name is kept in jobs.sqlite (an older file gains the column with its rows owned by default) and is not in the job snapshot; the three metrics counters gain an owner label. A two-token compose peer carries the PHP and Python integration checks; the PHP client and the MCP server are unchanged.
Merged
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.
Fixes #69, as decided in its comment of 2026-10-21.
What
Per-client tokens on the worker, with every job scoped to the token that submitted it.
BRIDGE_TOKEN(the single token, nameddefault, as before),BRIDGE_TOKENS=name=token,..., orBRIDGE_TOKENS_FILE(onename=tokenper line, blank and#lines ignored, nothing trimmed, a regular file of at most 65536 bytes with mode 0600 or stricter, read once). Names are 1 to 64 characters of[A-Za-z0-9_.-], tokens keep today's rule, at most 64 of them, no duplicate name or value. Any violation, an unreadable or group-readable file, both lists together, orBRIDGE_TOKENbeside either, stops the worker with a message naming the variable and never a token.BRIDGE_TOKENandBRIDGE_TOKENSleave the environment before the first child starts; the file's content never enters it. An empty variable counts as unset, so compose's${X:-}forms keep working.hmac.compare_digest, with no early exit, and yields the token's name as the request's owner.GET /v1/jobs/{id}andPOST /v1/jobs/{id}/cancelwith another owner's token answer the same 404 as an unknown job, byte for byte. Idempotency keys are per owner (the unique index becomes(owner, idempotency_key)). The owner's name, never the value, is kept injobs.sqlite; a file from before this change gains the column on open, its rows owned bydefault, and the old key-only index is replaced. The job snapshot is unchanged: noownerfield.GET /healthzandGET /metricsanswer any configured token. Capacity, concurrency and retention stay shared.ownerlabel onbridge_jobs_total,bridge_jobs_submitted_totalandbridge_submissions_rejected_total, with every configured name's series present at zero; the other families are unchanged.Tests
tests/python/test_server.py:TokenSourceTestsdrivemain()through every source and every refusal (short or whitespace token, bad name or entry, duplicate name or value, 65 entries in the list and in the file, a 65537-byte file, a FIFO, group- or other-readable modes, a missing path, a directory, an unreadable file, a non-UTF-8 file, the three exclusivity cases, empty variables counting as unset), asserting the variable is named and no token or token prefix appears;OwnerHttpTestson a two-token server with a state directory: another token's GET and cancel equal the unknown-job 404, the owner's work, the snapshot's key set is exactly the public fields, a shared key is two jobs and each repeat returns its own, health and metrics for every token and 401 for wrong ones, the owner series at zero and moving for the right owner after a job and a 429, and after a restart the raw bytes ofjobs.sqlite*hold the name but neither token while the restored job is still 404 for the other token.tests/python/test_jobs.py:OwnerTestsat store level, an old-schema file created in the test and migrated (checked again after a second open), the index swap, and the child-environment test extended toBRIDGE_TOKENSand the file's content with a positive control.tests/php/integration.phpandtests/integration.pyagainst the new two-token compose peertokens-worker(two fixed test-only literals, independent of the run's token): 404 for the other token's read and cancel, success for the owner, two jobs for a shared key, 401 for a wrong token; the Python side also checks the owner counters moved.Files
worker/ai_bridge/server.py(token resolution and validation,_authorizedreturning the owner, the owner passed to the store),worker/ai_bridge/jobs.py(Settings.owners,_owner, the column and its migration,(owner, key)scoping,_owned, owner-keyed counters),worker/ai_bridge/metrics.py(the label),worker/ai_bridge/tasks.py(the test-onlytest.inspecttask reportsBRIDGE_TOKENSand a needle in the child's environment).tests/python/test_server.py,tests/python/test_jobs.py,tests/php/integration.php,tests/integration.py.docker/compose.yaml(tokens-worker, the two clients' variables),.github/workflows/tests.yml,AGENTS.md,.claude/skills/check/SKILL.md,.claude/settings.json(Bash and PowerShell twins),README.md(the test layer line and the limits),docs/testing.md(counts and the peer).docs/protocol.md(Tokens section, key scope, old-file rule, the label),docs/security.md(the "every job" sentence replaced by the scope rule),docs/mcp.md(the MCP server's own token name on a shared worker),CHANGELOG.md(Unreleased, Added).docs/integration.mdsaid a token authorizes every job;.claude/rules/php-client.mdand.claude/rules/worker.mdlist the peers and the limits.Checks
The full command block of
AGENTS.md, with the peer added, run as one command on this head (d73d7d8: the slice commit 6a2a0b9 merged withmainat 04cb9d2) in a worktree, compose's default image PHP 8.5:--profile test config: okpython-tests: Ran 240 tests, OKphp-tests: PASS: 148 PHP contract checksphp -loversrc,examples,tests/php: no syntax errors (25 files)up -d worker frankenphp fault-worker hostile tokens-worker,php-integration: PASS: 192 total PHP checks including real HTTP failure casesintegration: PASS: 12 concurrent isolated jobs through a reused FrankenPHP worker; top_k narrowing; embedding; redaction; invalid and missing-job cases; worker metrics; jobs scoped to their tokenmcp-fetcher,mcp-tests: Ran 125 tests, OKcomposer-install: ok (archive mode left to CI)php-fetcher,php-frameworks-fetcher,php-analysis: [OK] No errorsphp-psr18-tests: PASS: 178 PHP contract checkslaravel-tests: Laravel 13.38.0: 94 checks passed;symfony-tests: Symfony 8.0.15: 96 checks passedtls-check: PASS: 7 TLS checks with the example CA; PASS: 3 TLS checks with no CA configuredPHP 8.2, 8.3 and 8.4 are left to CI;
model-smoke.ymlruns on this PR since worker files are on itspaths:list.Review
Two independent reviews of the diff, both read-only. Codex (another model family, on the inlined diff because its shell was unavailable on this machine) found that the compose peer built its token list from the run's
BRIDGE_TOKEN, which a legal token containing a comma would break, and a two-line comment; both fixed (fixed test-only literals for the peer, one-line comment). The delivery reviewer found four defects, all fixed (a collapsed line in a test, the protocol's wording on comment lines in the token file, the worker rule not naming the new limits, a migration re-open test that asserted nothing) and five missing tests, all added (file over the size cap, a FIFO, more than 64 entries in the file form, empty variables as unset, the re-open assertions). Security: nothing open in either review; token values are checked against every response, the metrics body, the SQLite bytes, the child environment and every start-up message.Considered, not done: Docker secrets mount as 0444 by default and would be refused as group-readable; an operator using them needs
mode: 0400on the secret. Not verified here: PHP 8.2 to 8.4 (CI), two real hosts.