Skip to content

feat(agents): run Claude and Codex in-container off the home volume - #86

Merged
ExtraToast merged 1 commit into
mainfrom
feat/agent-login-home-volume
Sep 18, 2026
Merged

ExtraToast merged 1 commit into
mainfrom
feat/agent-login-home-volume

Conversation

@ExtraToast

Copy link
Copy Markdown
Contributor

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 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.

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, RunnerCredentialSecretManager with the Secret it stamped into every runner Pod, and the three credential-worker config keys. V28 drops agent_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 (the SO_PEERCRED pair, which needs Linux)
  • detekt (main, test, integrationTest) + ktlint — clean
  • Both suites run with --rerun-tasks. A green UP-TO-DATE run appeared partway through and is not a result
  • Spec regenerated and committed: 216 lines removed, five endpoints gone, /api/v1/agent-logins added. Read back from the file, not inferred
  • oasdiff breaking --fail-on WARN against the deprecation commit — no breaking changes. Against origin/main the same diff reports five errors, which is why this stacks on that PR rather than standing alone

Notes

  • The routing test is encoded in three places and all three now agree: RunnerSessionBindingRouter, AgentGatewayClientRouter and AttachPreconditionChecker. The last one is the one that matters and the easiest to miss — it required session.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 with workspace has no gateway endpoint. The tmux process would start, the API would return Bound, 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.
  • A Workspace binds in-container when it is Scratch and has no runner Pod. A Scratch Workspace created before Tracer bullet: run a Shell Agent Session in a Scratch Workspace inside the API container #62 can still hold a Pod-bound Claude or Codex session, and those rows survive this deploy; routing one in-container would strand it, since restart() throws and ensureBound() answers with this container's directory instead of the Pod's /workspace.
  • A Repo-backed Claude session has no Agent Login during the transition. It still routes to a runner Pod, and this removes the Secret injection that used to supply one. Routing it in-container instead would start the session in an empty directory — the in-container binder does not clone. Remove runner Pod orchestration from agents-api #67 closes the gap by moving the whole path across. /api/v1/agent-logins documents that it does not answer for a Repo-backed Workspace, and agents-ui must show the sign-in hint there regardless of what it reports.
  • V28 is 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 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.
  • Run Claude and Codex Agent Sessions with Agent Login on the home volume #64 stays open: its third and fifth criteria are agents-ui's, and a fleet-infra follow-up drops the now-unread CREDENTIAL_WORKER_URL, INTERNAL_TOKEN and CREDENTIAL_INGEST_BEARER wiring plus the agents-login worker manifests.

refs #64

@ExtraToast ExtraToast added type: feature New user-facing or operator-facing capability. area: agents Agent runtime, agent APIs, tools, prompts, or UI. component: api HTTP API, OpenAPI contract, or API client concern. priority: P1 High; important and should be handled in the current iteration. labels Sep 18, 2026
@ExtraToast ExtraToast self-assigned this Sep 18, 2026
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
ExtraToast force-pushed the feat/agent-login-home-volume branch from e79d530 to 1df8dfb Compare September 18, 2026 12:58
@ExtraToast
ExtraToast changed the base branch from deprecate/credential-endpoints to main September 18, 2026 12:58
@ExtraToast ExtraToast closed this Sep 18, 2026
@ExtraToast ExtraToast reopened this Sep 18, 2026
@ExtraToast
ExtraToast merged commit 27c1e25 into main Sep 18, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: agents Agent runtime, agent APIs, tools, prompts, or UI. component: api HTTP API, OpenAPI contract, or API client concern. priority: P1 High; important and should be handled in the current iteration. type: feature New user-facing or operator-facing capability.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant