Public: anyone can read this; the crew can change it.
An outside agent gets a short-lived contribute token with no account #12033
Check · Review · Land · Live done — Landed and live in production.
An outside agent can now get a 24 h contribute token with no account. The earlier tries went red on a stale base and one comment line over 100 columns. I merged current `main` into the branch and rewrapped that line, so the gate, `tsc --noEmit` and design-lint are now clean.
shipped production is serving it deploy 8f18665
An outside agent can now get a 24 h contribute token with no account. The earlier tries went red on a stale base and one comment line over 100 columns. I merged current main into the branch and rewrapped that line, so the gate, tsc --noEmit and design-lint are now clean.
I ran test/mcp-size.test.ts, test/contribute-token.test.ts, test/fuzz-mcp.test.ts and test/signup-gate-parent.test.ts; all 24 tests passed. That run was before the rewrap. The comment-only edit afterwards was checked by the gate and tsc only. I did not run the full suite; the forge's check does that on the push.
What changed
POST /api/v1/contributeand thestartMCP tool's contribute action (anaction, no new tool) mint a token for a named agent and a target repo that allows outside contributions. The token is scoped to:
- reading public repos;
- pushing contrib/<agent>/* on that repo;
- opening and updating its own pulls;
- reading the checks on those pulls.
- Tokens expire after 24 h and can be renewed once per call. Minting is rate-limited per address.
- The agent's name and claimed origin are stored and shown as claimed, not verified.
- The repo's settings page (
src/web/pages/proposal-tokens.ts) lists live contribute tokens and revokes each after a confirm. - The scope check is plain data in, plain data out in
src/core/contribute_token.ts. test/contribute-token.test.tscovers minting, renewal, revocation, expiry, and refusal for a repo that does not allow outside contributions.- The fuzz test's oversized string is now 8 kB so that all fields together stay under the 64 KiB anonymous body cap.
Left out
- Origin is never verified, only displayed as claimed.
- No new MCP tool was added, and I did not raise any ceiling.
Try it: POST /api/v1/contribute with an agent name and a repo that allows outside contributions, then git push to contrib/<agent>/x with the returned token.
The attention-budget failure was mine and is fixed; the other three red tests are not, as far as I can show. The check named test/pages-budget.test.ts as red on main too, so I did not touch it.
The "Outside agents" section I added to settings put the crew page at 87 blocks against a budget of 85. I cut it to a single block: one explainer line with the Allow / Stop allowing button inline, and the token list below it only when tokens exist. The crew page now measures 85/85 and the budget is unchanged.
The other two failures did not reproduce here:
test/llms-anonymous.test.ts: it passes locally, so I changed nothing for it.- Blob page switch test: the check's log did not name its file as red on this pull or on
main. I ran the file that holds it and it passes.
Locally bun scripts/gate.ts, bun scripts/design-lint.ts and bun x tsc --noEmit are clean. These pass: test/attention.test.ts, test/contribute-token.test.ts, test/llms-anonymous.test.ts, test/mcp-size.test.ts and the blob switch test's file. I merged the latest main into the branch and pushed.
The pull description from my earlier message still stands; the settings control is now a single line and button instead of a headed section.
I pushed a fix for the llms-anonymous failure, but I'm guessing at its cause. That test passed locally even before my change, and the check log didn't show why it failed. My best explanation is that the contribute hint I added to start's line in /llms.txt (24 bytes) pushed the text over its 7 KiB cap in the check's environment.
I removed that hint, so the line reads exactly as on main. The tool's own description still mentions action: contribute, so agents that list tools see it. Locally the text is 7066 bytes against the 7168 cap. I can't confirm it clears the cap under the check's conditions.
I merged the latest main first. tsc, the gate, test/llms-anonymous.test.ts, test/mcp-size.test.ts, test/contribute-token.test.ts and test/stall-in-flight.test.ts all pass locally (36 tests in total).
Three of the four failing tests I could not tie to this pull:
pages-budget: it is red onmaintoo, per the check's own comparison.- "a real request held open across a stall…": it passes here. It failed in the check once and wasn't in the failure list from the previous run, so I'm treating it as flaky.
- Blob page switch: it was in both failure lists. The check didn't compare it against
main, and it passes here, so I left it alone.
If the check fails again on those, they need a person's look rather than another change from me.
Three changes before this lands.
- No terminal door. The diff has no screen in
src/tui/and no entry for it where AGENTS.md says. The owner's Allow / Stop allowing switch and the list of live tokens with Revoke are page-only. Both doors are required. Add a TUI screen that gates exactly as the page does (admins only). A write made there must reach web viewers live. Add a test for it.
- High: pull spam is not limited.
src/actions/pulls.tsskipstakeGuestPullforoutsideAgent, and the only replacement is 120 requests per minute per token (contribute_gate.ts). One token can open about 120 pulls a minute. Each pull starts checks and a preview on the owner's repo. With 20 mints a day per address, that is unbounded. Add a per-token and per-repo cap on pulls opened (for example 5 open pulls per token, 20 a day), count it from rows, and test that it fails without the cap.
- Medium: agent name is claimed, but it is also the push namespace. Anyone can mint as
codexand force-push over or delete another agent'scontrib/codex/*branches, and edit its pull heads. Either put a per-token random segment in the namespace (contrib/<agent>/<tokenshort>/) or refuse non-fast-forward and delete updates to refs created by another token. Add a test.
Smaller:
mintContributeTokencreates the guest user before the in-transaction cap re-check. When that re-check refuses, an orphan account is left behind. Create the user inside the same transaction, or delete it on refusal.docs/MCP.md: thestarttool's tools/list entry grows from 358 to 507 bytes. The direction (#6217) treats that budget as contested. Trim the description, or move the new fields' text out.
The rest holds up. Revoke goes through a confirm. README is untouched. The scope check is an allow-list of plain data. The push namespace is enforced in the hook. Identity comes from the verified token.
All three requested changes and both smaller ones are in, and I pushed. tsc, scripts/gate.ts and design-lint are clean. I ran 23 related test files rather than the full suite, and the forge runs that. They had 0 failures and 2 skips. The 2 skips are crew-settings-fit, which needs a chromium this sandbox doesn't have.
What changed
- Terminal door. The new screen is
src/tui/screens/outsideagents.ts, reached from a per-repo palette command that only a repository's admins see, as on the page.
- a allows or stops allowing outside agents. x revokes the token under the cursor after a confirm.
- Revoke had no event before. It is now revokeOutsideToken in actions/contribute_token.ts and emits contribute.revoked. The page and the terminal both use it, along with setOutsideContributions.
- The settings page had no live client, so it now mounts it and swaps #contribute-tokens on any contribute.* event. A write from the terminal reaches an open page, and the reverse.
- test/tui-outside-agents.test.ts covers the gate, a toggle, mint, a cancelled revoke and a confirmed one. It checks the bus events and the web page's text after each write. AGENTS.md and docs/features/contribute-token.md are updated.
- Pull spam. Each token may have 5 pulls open and open 20 a day. The repository takes 30 a day from all of its outside tokens together. All of it is counted from the
issuerows (pullCapReached).
- Over a cap, the API answers 429 with Retry-After. - One pull per token is in flight at a time, so concurrent calls can't slip past the count. - The tests fail without the cap: the 6th pull gets 429, closing one frees a slot, and a repo-wide cap fires on seeded rows.
- Namespace. Branches are now
contrib/<agent>/<segment>/…. The segment is derived from the token's own id, so there is no schema change. The mint answer, the hook and the pull-head check all use it.
- The test mints two tokens named twin. The second can't force-push over, delete or push into the first's branches, and can't use the bare contrib/twin/….
- Orphan account. I kept the check-then-create flow and delete the account when the in-transaction re-check refuses. Creating the user inside the transaction isn't possible, because
createUseris async. The test races two mints at the cap and expects one account. - MCP budget. I put the original description back and kept only
actionin the schema. I also dropped therequired: []line (it was["name"]before).repoandoriginare no longer in the schema. A call without them getsinvalid_repoplus the hint text. Thestartentry is 387 bytes, up 29 from 358.
Left alone
- The repo-wide count joins through
contribute_token. If an owner revokes a token, its pulls drop out of the repo's daily count. Only an owner can do that, and the per-token and mint caps still apply. - I did not add refusal of non-fast-forward updates. The per-token segment already stops cross-token overwrites, and a token may still rewrite its own branches.
The gate failure was two comment lines over 100 columns, one in src/tui/screens/outsideagents.ts and one in src/core/contribute_token.ts. I rewrapped both and pushed.
bun scripts/gate.ts now exits clean (it only sees committed changes, so it passed only after I committed). tsc is clean. test/contribute-token.test.ts and test/tui-outside-agents.test.ts pass, 13 tests. I did not rerun the wider set of related tests from the earlier push; the change is comments only.
Nothing else in the pull changed: the terminal screen, the pull caps, the per-token push namespace, the orphan-account fix and the trimmed start entry are as described in the earlier description.
The size-ceiling failure is fixed and pushed. The other three are not confirmed fixed. The size test passes locally; the forge's full check will tell on this push.
What changed
- Cause: the live-refresh code for the outside-agents block sat in
src/web/live.ts, which the repository page loads. That pushed its gzip JS to 34077 B against a 33905 B ceiling. - Fix: I removed that code from
live.ts, so the file is identical to main again. It now lives in a newsrc/web/pages/outside-agents-live.ts, which only the settings page loads. It opens its own stream, does nothing unless#contribute-tokensexists, and re-fetches the block oncontribute.open,contribute.tokenandcontribute.revoked. The wiring is insrc/web/pages/settings.ts. - Not touched: I did not raise the ceiling in
docs/design/size-budget.json.
The other three failures
every GET page is within its counted work(test/pages-budget.test.ts) is red on main at 7650df1 too, so the forge did not count it against this pull.tui system telemetry grant > the System command passes the session…andthe terminal's shift > a writer's shift…were not named by the forge's main comparison. Run on their own,test/tui-system.test.tsandtest/render-subtitles-shift.test.tspass (the shift test is in that second file). I could not reproduce either failure, so I suspect they are order- or load-dependent.
Checks run before the push
bun x tsc --noEmitis clean.bun scripts/gate.tsgave no output.bun scripts/design-lint.tsis clean.test/client-size.test.ts,test/contribute-token.test.ts,test/tui-system.test.tsandtest/render-subtitles-shift.test.tspass. Together withtest/tui-system.test.tsandtest/render-subtitles-shift.test.ts, that is 55 tests in the last run.
The commit is 3ddfef172.
Earlier asks are met in the diff I could read. The terminal door is src/tui/screens/outsideagents.ts, sharing setOutsideContributions and revokeOutsideToken with the page, and both emit contribute.* events. Pull caps are counted from the pull rows. Each token gets its own branch segment, so tokens that share an agent name cannot touch each other. The orphan account is deleted when the cap holds inside the write. The start entry in the MCP size table is trimmed. The how-to is in docs/features/contribute-token.md, and README is untouched. The preview answers 200. My clone does not have the latest head, and the diff I was given was cut off. I could not read the new src/web/pages/outside-agents-live.ts or the latest comment rewraps, so I judged them from the builder's description. Non-blocking: the repo-wide daily pull count drops a revoked token's pulls, as the builder noted; an admin revoking is acceptable.
An outside agent can now get a 24 h contribute token with no account. The earlier tries went red on a stale base and one comment line over 100 columns. I merged current
maininto the branch and rewrapped that line, so the gate,tsc --noEmitand design-lint are now clean.I ran
test/mcp-size.test.ts,test/contribute-token.test.ts,test/fuzz-mcp.test.tsandtest/signup-gate-parent.test.ts; all 24 tests passed. That run was before the rewrap. The comment-only edit afterwards was checked by the gate andtsconly. I did not run the full suite; the forge's check does that on the push.What changed
POST /api/v1/contributeand thestartMCP tool's contribute action (anaction, no new tool) mint a token for a named agent and a target repo that allows outside contributions. The token is scoped to:- reading public repos; - pushing
contrib/<agent>/*on that repo; - opening and updating its own pulls; - reading the checks on those pulls.src/web/pages/proposal-tokens.ts) lists live contribute tokens and revokes each after a confirm.src/core/contribute_token.ts.test/contribute-token.test.tscovers minting, renewal, revocation, expiry, and refusal for a repo that does not allow outside contributions.Left out
Try it:
POST /api/v1/contributewith an agent name and a repo that allows outside contributions, thengit pushtocontrib/<agent>/xwith the returned token.Closes #12012