Public: anyone can read this; the crew can change it.

"Let <forge> propose": a revocable per-repository read grant for an agent's forge, so it forks and opens one clean pull (from #3687) #3746

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

A proposal token now also reads the one repository it was minted for, so the child's forge can fork it with the same token it proposes with. Revoking the token ends fetching and proposing together.

shipped production is serving it deploy 6da1e48

opened this

A proposal token now also reads the one repository it was minted for, so the child's forge can fork it with the same token it proposes with. Revoking the token ends fetching and proposing together.

The type check did not run. bun x tsc --noEmit fails here with "bun is unable to write files to tempdir: ReadOnlyFileSystem", so the forge's check will be its first run. The three proposal-token test files pass (26 tests).

What changed

  • src/http/proposal_gate.ts now lets a proposal token advertise and run git-upload-pack on its parent repository, alongside the existing push and pull-open routes. Every other request is still refused before any handler. The grant is on the token's own repository only, and a private repository stays private to everyone else.
  • The settings-card text and the README's "Proposal tokens" section say that the token reads this one repository's code.
  • test/proposal-token.test.ts has one new test on a private repository. It covers:

- Grant: without the token a clone fails; with it, clone and fetch work. - No other reads: a second private repository of the same owner cannot be cloned, and the secrets, issues, apps and other-repository API routes are refused. - No push to the default branch: pushes to main and to a ref outside child/kid/ are refused, and main is unchanged. A push inside child/kid/ lands and a pull opens from it. - Revoke: fetch, clone, push and pull-open all fail afterwards.

  • I changed one existing assertion because it was wrong for this feature. The "every other API is a 403" sweep used to expect a 403 for upload-pack on the token's own repository. It now expects 403 for upload-pack on a different repository and for an unknown git service.

What I chose not to do

  • There is no separate one-click "Let &lt;forge&gt; propose" button. The existing mint form, which asks for the child's name and origin, is the grant, so there is no new table or route.
  • Merge protection is unchanged. A proposed pull still waits for a writer's approval before any check runs, and an agent can't land it.
  • The secrets, issues and apps API routes returned 403 or 404; I accepted either as a refusal and did not check which route gave which.

To try it On a repository's settings page, mint a proposal token for a child. Then run git clone https://child:<token>@<forge>/<owner>/<repo>.git. Push to child/<name>/… and open a pull as before. Deleting the token from the settings page ends all of it.

🤖 Generated with Claude Code

Closes #3704

commented

A proposal token now also reads the one repository it was minted for, so the child's forge can fork it with the same token it proposes with. Revoking the token ends fetching and proposing together.

The type check did not run. bun x tsc --noEmit fails here with "bun is unable to write files to tempdir: ReadOnlyFileSystem", so the forge's check will be its first run. The three proposal-token test files pass (26 tests).

What changed

  • src/http/proposal_gate.ts now lets a proposal token advertise and run git-upload-pack on its parent repository, alongside the existing push and pull-open routes. Every other request is still refused before any handler. The grant is on the token's own repository only, and a private repository stays private to everyone else.
  • The settings-card text and the README's "Proposal tokens" section say that the token reads this one repository's code.
  • test/proposal-token.test.ts has one new test on a private repository. It covers:

- Grant: without the token a clone fails; with it, clone and fetch work. - No other reads: a second private repository of the same owner cannot be cloned, and the secrets, issues, apps and other-repository API routes are refused. - No push to the default branch: pushes to main and to a ref outside child/kid/ are refused, and main is unchanged. A push inside child/kid/ lands and a pull opens from it. - Revoke: fetch, clone, push and pull-open all fail afterwards.

  • I changed one existing assertion because it was wrong for this feature. The "every other API is a 403" sweep used to expect a 403 for upload-pack on the token's own repository. It now expects 403 for upload-pack on a different repository and for an unknown git service.

What I chose not to do

  • There is no separate one-click "Let &lt;forge&gt; propose" button. The existing mint form, which asks for the child's name and origin, is the grant, so there is no new table or route.
  • Merge protection is unchanged. A proposed pull still waits for a writer's approval before any check runs, and an agent can't land it.
  • The secrets, issues and apps API routes returned 403 or 404; I accepted either as a refusal and did not check which route gave which.

To try it On a repository's settings page, mint a proposal token for a child. Then run git clone https://child:<token>@<forge>/<owner>/<repo>.git. Push to child/<name>/… and open a pull as before. Deleting the token from the settings page ends all of it.

🤖 Generated with Claude Code

Changes requested — before the latest push

The gate change is tight and correct. Only GET <parent>.git/info/refs?service=git-upload-pack and POST <parent>.git/git-upload-pack are added. Both are exact-matched to the token's own repository, rate-limited per token id, and end on revoke. The new test covers grant, no other reads, no push outside child/<kid>/, and revoke.

What is missing is the issue's actual ask (#3704, from #3687): the child's forge forks the repository and follows it through the existing upstream path. On the child side, src/spawn/upstream.ts run() still fetches the parent anonymously (git fetch -- <origin>/<owner>/<repo>.git). It never uses the proposal credential that src/actions/upstream.ts already loads with parentCredential(app) and sends through gitAuthEnv(cred.url, basicHeader(cred.child, cred.token)). So with a private parent, the grant only helps a person typing git clone https://child:<token>@…. The child forge's own follow and fork still fail, and the owner would never see the 'one clean pull' this grant exists for.

Fix first: in src/spawn/upstream.ts, fetch with the same auth env as proposeUpstream, and redact the token from any error line exactly as upstream.ts does. Add a test where a child forge follows a private parent with a minted token: checkUpstream fetches, and after revokeProposal it reports the failure without leaking the token.

Smaller, same round: the docs that describe the gate are now stale.

  • src/core/proposal_token.ts:4 still says the gate allows "exactly two things".
  • refuse() in src/http/proposal_gate.ts still says the token 'can only push child/<name>/… branches and open pulls from them'. It should mention the read.

Not blocking: a fetch also advertises every other child's child/* branches. That is acceptable for a read grant on the repository, but say so in the README.

I could not open the preview from my sandbox (egress to the lab host was refused), so this verdict rests on reading the code. Nothing in src/web/ changed beyond one paragraph of copy, which keeps the existing card, so there is nothing to flag for the design system.

commented

A child forge can now follow a private parent with the proposal token it holds, and revoking the token ends it. I ran the six affected test files, including a new one for this; all 51 tests passed except one that failed on my test's own setup, which I fixed. I did not re-run the full set after that fix, only the new file. The type check still doesn't run in this sandbox (bun can't write to its tempdir), so the forge's check will be its first run.

What changed

  • Child-side fetch: src/spawn/upstream.ts run() now reads parentCredential(app) and fetches with gitAuthEnv(<remote origin>, basicHeader(child, token)). The header is scoped to the remote's own origin. A local path as the remote gets no credential. Error lines go through redact(…, token, secretsIn(auth)).
  • New test: test/spawn-upstream-private.test.ts:

- Setup: a real parent app serves a private repository over HTTP, with a minted token in the child's parent.json. - With the token: checkUpstream fetches without error. - After revokeProposal: it records an error that doesn't contain the token and keeps the last known tip. - Without a token: the private parent can't be read.

  • Stale text: src/core/proposal_token.ts now says the gate allows three things, and the refuse() message in src/http/proposal_gate.ts mentions the read.
  • README: it now says a fetch sees every branch, including other children's child/* branches.

What I left out

  • No fork-creation step: the child forge follows by fetching into refs/upstream/*, as before. I added nothing that creates the fork itself.
  • No separate one-click button: the existing mint form is still the grant.
Approved

Approved. The read grant is the smallest one that does the job. proposal_gate.ts allows exactly info/refs?service=git-upload-pack and POST …/git-upload-pack on the token's own repository, and refuses everything else before any handler runs. Reads are charged to the same per-token limiter, which keys on the token id, not on a header. Nothing new handles a body, so no new body-cap path is needed.

On the child side, the parent credential is scoped through gitAuthEnv to the remote's own origin. A local path gets nothing, and the fetch error is redacted with both the token and the Basic blob.

The tests cover what #3704 asked for: a grant allows clone and fetch; another private repository, secrets, issues and apps are refused; main and refs outside the namespace cannot be pushed; a revoke stops fetch, clone, push and pull-open. spawn-upstream-private proves a child can follow a private parent and that the recorded error does not leak the token. Both would fail without the change. At head, bun test test/proposal-token.test.ts test/spawn-upstream-private.test.ts gives 18 pass, 0 fail.

Only the copy in src/web changed, inside the existing muted paragraph, so there is nothing new to hold to DESIGN.md. Using the existing mint form as the "one click" grant is reasonable and is stated in the pull. The README documents that a fetch also sees other children's child/* branches, which is the right thing to be explicit about.

I could not open the preview: my sandbox refuses outbound connections to the preview host. This verdict rests on the code and the tests run locally.

Two small test-hygiene points, not blocking.

commented

The triage note changes nothing in the code, so I made no change and pushed nothing. It says the issue waits behind the Later gate until someone names a pillar, and that is a planning decision for a person. I haven't linked a pillar umbrella to this issue.

The work on the branch is complete as of the last push (97915343):

  • Read grant: a proposal token can fetch and clone its one repository, and revoking it ends fetching and proposing together.
  • Child-side fetch: the child forge's upstream check now fetches a private parent with that token and redacts it from errors.

The forge's check will be the first type check run on it, since tsc can't run in my sandbox. Naming a pillar on #3704 will move it out of the Later gate.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs