Serve the agent CLI only to agents #118

Merged
kleb merged 3 commits from feature/authenticated-cli-downloads into main 2026-08-26 08:01:22 +02:00
Collaborator

The agent CLI download endpoints were unauthenticated. Anyone who could reach the server could pull an 84 MB binary, and that anonymous conveyance was the only reason this repository carried third-party notices: the compiled binaries embed Bun, which statically links JavaScriptCore and tinycc under the LGPL. LGPL duties attach on conveying to others, not on private use.

Gate

  • Agents send a bearer token. handlePublicAgentRoute runs before authenticate, so it validates via resolveBearerToken itself.
  • A machine still bootstrapping sends the install grant as ?install=<otp>, threaded into the release URLs by publicAgentCliManifest. The grant is inspected, never consumed, so downloading does not cost the caller the exchange the same link still owes. Once exchanged it stops opening downloads.
  • Rate-limited callers get 429, not 401, so a good credential is never reported as bad.
  • Responses became private with Vary: Authorization.
  • The artifact query allowlist is exactly sha256, plus install when present.

The updater would otherwise have broken silently: downloadRelease used a bare fetch with no credentials and now goes through the client's authenticated rawRequest. A skill-only update carries a stub that throws, pinning the invariant that it must never reach for a release.

Removal

With no anonymous conveyance left, the notices, relinking guide, relinkable bundles, and the generator have nothing to serve. This also retires notices:check, whose committed output (245 packages, rolldown 1.2.1) had drifted from the lockfile (212, rolldown 1.2.5) and failed on any clean checkout.

Second commit

The bootstrap smoke asserted that reusing a spent grant fails with 410, but the assertion could never match: --json writes the error document to stdout while the helper built its rejection from stderr alone. The failure predates this branch; d82ca9f fails it identically, verified by checking that commit out and reproducing.

Verification

typecheck, lint, knip, format:check clean. agent-access, build-config, agent-config-distribution, agent-integration-update, validator-schemas pass. build:agent-cli produces a valid manifest and smoke:agent-bootstrap passes end to end, exercising the gated bootstrap download with a real compiled binary.

Deploy ordering, not optional

The installed CLI at d82ca9f downloads anonymously. The moment this server is live that CLI cannot self-update, including to the build that fixes it. Order must be: build the new CLI, deploy the server, then replace the local binary once by hand. Agents installed after that update normally.

The agent CLI download endpoints were unauthenticated. Anyone who could reach the server could pull an 84 MB binary, and that anonymous conveyance was the only reason this repository carried third-party notices: the compiled binaries embed Bun, which statically links JavaScriptCore and tinycc under the LGPL. LGPL duties attach on conveying to others, not on private use. ## Gate - Agents send a bearer token. `handlePublicAgentRoute` runs before `authenticate`, so it validates via `resolveBearerToken` itself. - A machine still bootstrapping sends the install grant as `?install=<otp>`, threaded into the release URLs by `publicAgentCliManifest`. The grant is inspected, never consumed, so downloading does not cost the caller the exchange the same link still owes. Once exchanged it stops opening downloads. - Rate-limited callers get 429, not 401, so a good credential is never reported as bad. - Responses became `private` with `Vary: Authorization`. - The artifact query allowlist is exactly `sha256`, plus `install` when present. The updater would otherwise have broken silently: `downloadRelease` used a bare `fetch` with no credentials and now goes through the client's authenticated `rawRequest`. A skill-only update carries a stub that throws, pinning the invariant that it must never reach for a release. ## Removal With no anonymous conveyance left, the notices, relinking guide, relinkable bundles, and the generator have nothing to serve. This also retires `notices:check`, whose committed output (245 packages, rolldown 1.2.1) had drifted from the lockfile (212, rolldown 1.2.5) and failed on any clean checkout. ## Second commit The bootstrap smoke asserted that reusing a spent grant fails with 410, but the assertion could never match: `--json` writes the error document to stdout while the helper built its rejection from stderr alone. The failure predates this branch; `d82ca9f` fails it identically, verified by checking that commit out and reproducing. ## Verification typecheck, lint, knip, format:check clean. agent-access, build-config, agent-config-distribution, agent-integration-update, validator-schemas pass. `build:agent-cli` produces a valid manifest and `smoke:agent-bootstrap` passes end to end, exercising the gated bootstrap download with a real compiled binary. ## Deploy ordering, not optional The installed CLI at `d82ca9f` downloads anonymously. The moment this server is live that CLI cannot self-update, including to the build that fixes it. Order must be: build the new CLI, deploy the server, then replace the local binary once by hand. Agents installed after that update normally.
The CLI download endpoints were unauthenticated, so anyone who could reach
the server could pull an 84 MB binary. That is conveyance to third parties,
and it is the sole reason the repository carried third-party notices: the
compiled binaries embed Bun, which statically links JavaScriptCore and tinycc
under the LGPL.

Every artifact request now proves it belongs to somebody. An agent presents
its bearer token. A machine still being bootstrapped presents the install
grant it is about to exchange, threaded into the URLs the install document
hands it; the grant is inspected rather than consumed, so downloading never
costs the caller the exchange the same link still owes them. Responses turn
private and vary on Authorization so no cache serves them onward.

With no anonymous conveyance left, the notices, the relinking guide, the
relinkable bundles, and the generator that produced them have nothing to
serve, and go. This also retires notices:check, whose committed output had
drifted from the lockfile and failed on any clean checkout.
The bootstrap smoke asserted that reusing a spent install grant fails with
410, but the assertion could never have matched. In --json mode the CLI
writes its error document to stdout and leaves stderr empty, while the helper
built its rejection from stderr alone, so every non-zero exit collapsed to
"command exited 4".

The failure predates this branch: d82ca9f fails the same assertion the same
way. Falling back to stdout restores the check, and throwResponse already
carries the status into the document, so the 410 is there to match.
Collaborator

kReview review

Verdict: no findings

No findings to address in the reviewed diff.

No findings to address across the reviewed diff.

Reviewed by kReview at a156789835. This comment is conservative and based only on the PR diff, metadata, and supplied repository context.

Est. cost ~$6.34 total (2.7M in / 10.0k out) · this run ~$2.84 (1.0M in / 4.8k out) / gpt-5.6-sol.

<!-- codex-forgejo-review --> <!-- codex-forgejo-review-head:a1567898356cc8beea84ee2a1d08d2973c4a91a3 --> ## kReview review **Verdict:** no findings No findings to address in the reviewed diff. No findings to address across the reviewed diff. _Reviewed by kReview at `a156789835`. This comment is conservative and based only on the PR diff, metadata, and supplied repository context._ _Est. cost ~$6.34 total (2.7M in / 10.0k out) · this run ~$2.84 (1.0M in / 4.8k out) / gpt-5.6-sol._ <!-- codex-forgejo-review-state:eyJoZWFkU2hhIjoiYTE1Njc4OTgzNTZjYzhiZWVhODRlZTJhMWQwOGQyOTczYzRhOTFhMyIsInN1bW1hcnkiOiJObyBmaW5kaW5ncyB0byBhZGRyZXNzIGFjcm9zcyB0aGUgcmV2aWV3ZWQgZGlmZi4iLCJmaW5kaW5ncyI6W10sImN1bXVsYXRpdmVDb3N0Ijp7InVzZCI6Ni4zMzYwNzYsImlucHV0VG9rZW5zIjoyNjk2NjQ5LCJvdXRwdXRUb2tlbnMiOjk5NjgsIm1vZGVsIjoiZ3B0LTUuNi1zb2wifX0= -->
The gate accepted the grant as an install query parameter and publicAgentCliManifest
threaded it into every release URL, but bootstrap.manifestUrl was built separately
and stayed bare. A machine-readable consumer that followed the advertised manifest
URL therefore got 401 before it had a token, even though the inline release URLs
worked. Found in review of the first commit.

The regression test now fetches every URL the document advertises before the
exchange, not just the release ones.
kleb merged commit ece1b01a55 into main 2026-08-26 08:01:22 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
knopersikcuo/Planner!118
No description provided.