feat(agents): run Claude and Codex in-container off the home volume - #86
Merged
Merged
Conversation
ADR 0002: an Agent Login is a property of the user, not of a Workspace. The user signs in once from a terminal in an Agent Session, the CLI writes its own login files under $HOME, and the home volume (fleet-infra#326) keeps them across restarts. Everything agents-api did to capture, store, validate and inject a credential is therefore dead weight, and it goes here. Claude and Codex join Shell in-container. The gateway spawns bare `claude` or `codex` in tmux as `agent` through run-as-agent, which sets HOME=/home/agent, so the CLI finds its own login with nothing injected. Bare, not `claude -p` / `codex exec`: the interactive TUI is what a user can sign in from, and it is the only place a login is ever created. The headless forms belong to #66. `GET /api/v1/agent-logins` replaces `/api/v1/credentials/status`. The old endpoint reported what agents-api had captured and stored for a user; this reports whether a provider's CLI has written a login, read straight off the volume. Presence only -- nothing derived from the login reaches the wire. No `X-User-Id` either: credentials were per-user rows, but a home volume is one per container, so a user header would imply an answer this cannot give. A missing login is not an error and does not block a session. The CLI prompts for sign-in itself; agents-ui adds the hint alongside it (#64's third criterion, the agents-ui half still to come). Removed: the browser credential proxy and its five endpoints, the @hidden internal ingest endpoint and the filter guarding it, HttpCredentialWorkerClient, CredentialValidator, AgentOauthCredential and its repository, and RunnerCredentialSecretManager with the Secret it stamped into every runner Pod. V28 drops `agent_oauth_credentials`. Forward-only and deliberately so: the rows are OAuth tokens nothing reads, and keeping them would leave live credentials in the database purely so a rollback could use a mechanism this release removes. The recovery for a bad release is to sign in again from an Agent Session, which is the point of putting the login on a volume. `destroy()` still deletes `agent-runner-credentials-<short>`. It no longer creates one, but the Secrets an earlier release wrote hold OAuth tokens and nothing else reaps them. Two scope decisions, neither of them free: - Claude and Codex bind in-container for a **Scratch** Workspace only. A Repo-backed one still needs the Pod entrypoint's clone, so routing it here would start an Agent Session in an empty directory. The cost is that a Repo-backed Claude session has no Agent Login during the transition, because this removes the Secret injection that used to supply one. #67 closes it by moving the whole path in-container. - The Agent Kind no longer selects a binder. A Workspace binds in-container when it is Scratch **and** has no runner Pod: a Scratch Workspace created before #62 can still hold a Pod-bound Claude or Codex session, and those rows survive this deploy. Routing one in-container would strand it -- restart() throws, and ensureBound() answers with this container's directory instead of the Pod's /workspace. Three places encode that same test and all three now agree: RunnerSessionBindingRouter, AgentGatewayClientRouter, and AttachPreconditionChecker. The last one mattered most and was the easiest to miss: it still required `session.kind == SHELL`, so a Claude session bound in-container was resolved as remote, found no gateway endpoint -- a Scratch Workspace never provisions a Pod -- and the WebSocket attach was rejected with "workspace has no gateway endpoint". The tmux process started, the API returned Bound, and the terminal never opened, which removes the only place an Agent Login can be created. Covered now by a test proven to fail against the old gate. Verified: :api:test 617 passed / 2 skipped, :api:integrationTest 106 passed / 2 skipped, both with --rerun-tasks (a green UP-TO-DATE run is not a result). detekt and ktlint clean on main/test/integrationTest. The exported spec has no `/api/v1/credentials/**` path left and does have `/api/v1/agent-logins`, read back from the file rather than inferred. `oasdiff breaking --fail-on WARN` was run against both possible bases. Against origin/main it reports 5 x `api-path-removed-without-deprecation`; against the deprecation commit it reports none. That is the whole reason this is a second PR and not one -- api-contract-checks has no waiver. Part of #64
ExtraToast
force-pushed
the
feat/agent-login-home-volume
branch
from
September 18, 2026 12:58
e79d530 to
1df8dfb
Compare
ExtraToast
changed the base branch from
deprecate/credential-endpoints
to
main
September 18, 2026 12:58
5 tasks
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
Second of two steps for #64, and the one with the behaviour in it. ADR 0002 says an Agent Login is a property of the user, not of a Workspace: the user signs in once from a terminal in an Agent Session, the CLI writes its own login files under
$HOME, and the home volume (fleet-infra#326, now live) keeps them across restarts. Everything agents-api did to capture, store, validate and inject a credential is therefore dead weight, and it goes here.Claude and Codex join Shell in-container. The gateway spawns bare
claudeorcodexin tmux asagentthroughrun-as-agent, which setsHOME=/home/agent, so the CLI finds its own login with nothing injected. Bare, notclaude -p/codex exec: the interactive TUI is what a user can sign in from, and it is the only place a login is ever created. The headless forms belong to #66.GET /api/v1/agent-loginsreplaces/api/v1/credentials/status. The old endpoint reported what agents-api had captured and stored for a user; this reports whether a provider's CLI has written a login, read straight off the volume. Presence only — nothing derived from the login reaches the wire. NoX-User-Ideither: credentials were per-user rows, but a home volume is one per container, so a user header would imply an answer this cannot give.Removed: the browser credential proxy and its five endpoints, the
@Hiddeninternal ingest endpoint and the filter guarding it,HttpCredentialWorkerClient,CredentialValidator,AgentOauthCredentialand its repository,RunnerCredentialSecretManagerwith the Secret it stamped into every runner Pod, and the three credential-worker config keys.V28dropsagent_oauth_credentials.Validation
:api:test— 619 tests, 617 passed, 0 failed, 2 pre-existing skips:api:integrationTest— 108 tests, 106 passed, 0 failed, 2 skipped (theSO_PEERCREDpair, which needs Linux)--rerun-tasks. A greenUP-TO-DATErun appeared partway through and is not a result/api/v1/agent-loginsadded. Read back from the file, not inferredoasdiff breaking --fail-on WARNagainst the deprecation commit — no breaking changes. Againstorigin/mainthe same diff reports five errors, which is why this stacks on that PR rather than standing aloneNotes
RunnerSessionBindingRouter,AgentGatewayClientRouterandAttachPreconditionChecker. The last one is the one that matters and the easiest to miss — it requiredsession.kind == SHELL, so a Claude session bound in-container resolved as remote, found no gateway endpoint (a Scratch Workspace never provisions a Pod) and the WebSocket attach was rejected withworkspace has no gateway endpoint. The tmux process would start, the API would returnBound, and the terminal would never open — which removes the only place an Agent Login can be created. Covered by a test proven to fail against the old gate.restart()throws andensureBound()answers with this container's directory instead of the Pod's/workspace./api/v1/agent-loginsdocuments that it does not answer for a Repo-backed Workspace, andagents-uimust show the sign-in hint there regardless of what it reports.V28is forward-only. The rows are OAuth tokens nothing reads, and keeping them would leave live credentials in the database purely so a rollback could use a mechanism this release removes. The recovery for a bad release is to sign in again from an Agent Session, which is the point of putting the login on a volume.destroy()still deletesagent-runner-credentials-<short>. It no longer creates one, but the Secrets an earlier release wrote hold OAuth tokens and nothing else reaps them.agents-ui's, and afleet-infrafollow-up drops the now-unreadCREDENTIAL_WORKER_URL,INTERNAL_TOKENandCREDENTIAL_INGEST_BEARERwiring plus theagents-loginworker manifests.refs #64