Skip to content

feat(auth): authenticate from DEPOT_API_TOKEN_URL in a Dispatch sandbox - #689

Merged
robstolarz merged 6 commits into
mainfrom
rob/dep-8526-cli-token-url
Nov 15, 2026
Merged

robstolarz merged 6 commits into
mainfrom
rob/dep-8526-cli-token-url

Conversation

@robstolarz

@robstolarz robstolarz commented Nov 15, 2026 •

Copy link
Copy Markdown
Contributor

DEP-8526

In a Dispatch sandbox depot had no credentials, so depot agent list failed with "missing API token". The sandbox does carry DEPOT_API_TOKEN_URL, an address that answers with a short-lived token acting as the running turn's sender. The depot agent session calls the API accepts that token on now send it.

  • Which calls: the credential is picked per API call. CreateSession, GetSession, GetSessionMessage and ListDepotAgentSessions take the turn token, so agent create, list and logs use it. WatchSession, InterruptSession, ArchiveSession, SendInput and Launch do not, so those calls, and every other command (depot build, cache tools, configure-docker), send exactly what they did before. agent watch makes both kinds of call and picks for each. create --watch sends every call as before, so it never creates a session with one token that it is then refused a watch on with another.
  • Where it sits, for the calls that take it: --token, then DEPOT_TOKEN, then a saved login, then DEPOT_RUN_TOKEN_URL, then DEPOT_API_TOKEN_URL, then the sandbox's cache token. Nothing explicit is ever overridden.
  • Order of the two URLs: DEPOT_RUN_TOKEN_URL first. It is set for this CLI on purpose and its token reaches every org API; the turn token is ambient in the sandbox and narrower. A sandbox normally has only one of them, and managed runs keep today's behaviour.
  • One path: same token source, per-request refresh and client wrapper as DEPOT_RUN_TOKEN_URL (pkg/runtoken), one instance per variable.
  • Never stored or shown: fetched per invocation, held in memory only, nothing written to disk. Errors name the variable and never include the URL, the response body or transport error text.
  • On failure: error: could not fetch a token from DEPOT_API_TOKEN_URL: the endpoint answered HTTP 503, then the usual missing-token error.

The URL is requested exactly as given: the endpoint answers for the first audience parameter only.

Agent context, proof and test plan

What the address expects (depot/api at 7ff33e65c0)

  • src/domains/depot-agent-identity/runtime/block.ts:28,174 sets DEPOT_API_TOKEN_URL to the sandbox metadata address plus /token?audience=depot-api.
  • src/utils/workloadIdentity.ts:9: the audience is depot-api. src/domains/identity/service/oidc-mint.ts:59-71 and src/authn/oidc.ts:235-257 require it to be the only audience.
  • The answer is {"value":"<token>"}, the same shape DEPOT_RUN_TOKEN_URL answers with, so the CLI does a plain GET of the variable's value and adds nothing to it.
  • src/domains/depot-agent-identity/README.md:64-76: the token acts as the turn's sender and is accepted by a short list of services, session listing among them.

Which API call takes the turn token (depot/api at 7ff33e65c0)

Command API call API takes the turn token CLI sends
agent create CreateSession yes, for a turn a person typed (src/domains/depot-agent/runtime/depot.agent.v1.DepotAgentService.ts:78, :84) turn token; as on main with --watch
agent list ListDepotAgentSessions yes (:230) turn token
agent logs, watch GetSession yes (:204) turn token
agent logs, watch GetSessionMessage yes (:220) turn token
agent watch, create --watch WatchSession no (:244) as on main
agent stop InterruptSession no (:141) as on main
agent stop --archive ArchiveSession no (:151) as on main
agent send SendInput no (:119) as on main
agent launch Launch no (src/domains/depot-agent-launch/runtime/launch.ts:14) as on main

A call the API does not take the token on would answer "This Depot API call can't be made as a Depot agent turn's sender yet" (src/authn/resolveToken.ts:195,210), so the CLI never sends it there.

  • "As on main" is --token, DEPOT_TOKEN, a saved login, DEPOT_RUN_TOKEN_URL, then the sandbox's cache token.
  • create --watch uses one token for CreateSession and WatchSession. In a sandbox that is the cache token, which the session API rejects, so the command fails at CreateSession as it does on main and no session is left running unwatched. With no token at all it fails with the missing-token error before it sends anything.
  • agent stop makes no call that takes the turn token and never reads the address.
  • send --watch, launch --watch and create --watch keep one token for all their calls, as on main.
  • pkg/cmd/agent/credential_test.go runs every command with only the token address and a cache token set and asserts the credential each call arrived with.

Proof: a real Dispatch sandbox, binary built there from this branch at e2ed4ee

Commits after this run narrow which calls send the turn token (the table above). agent list, the command these runs exercise, resolves its credential the same way.

The sandbox agent cloned this branch, built it (Go 1.26.8, linux/amd64) and ran it. The sandbox environment had DEPOT_API_TOKEN_URL set and no DEPOT_TOKEN.

e2ed4ee08ba82f3e505e7bdfd17e2f810c89234b
build exit=0
1 no extra env
exit=0 lines=21
ID                                    STATUS    UPDATED

2 bad address
exit=1 lines=0
error: could not fetch a token from DEPOT_API_TOKEN_URL: the endpoint answered HTTP 404
unauthenticated: Invalid token

3 nothing listening
exit=1 lines=0
error: could not fetch a token from DEPOT_API_TOKEN_URL: could not connect
unauthenticated: Invalid token

4 address removed
exit=1 lines=0
missing API token; pass --token or DEPOT_TOKEN, or run `depot login`

5 other command, nothing listening
exit=1 lines=0
unauthenticated: Invalid token

6 files
0

In 2 and 3 the second line comes from the sandbox's build-cache token, which the CLI falls back to as before and which the session API rejects. Run 5 is a command outside the session API: it never asks the token address, so no error names the variable and it gets the same answer as on the previous release. "6 files" counts files under ~/.config/depot after the runs.

Proof: this branch's binary on a laptop against the real API, empty config directory, token address served on localhost

  • Good address: exit 0, 21 lines, and the server saw exactly /token?audience=depot-api.
  • Address answering 503: the error above, then the missing-token error, exit 1.
  • --token given with the address pointing at the local server: the server saw 0 requests.
  • depot list projects with only the address set: "missing API token" as before, the server saw 0 requests.
  • The config directory held 0 files afterwards.

Not covered here

  • The depot already installed in a sandbox is an older release, so depot agent list with no setup works there only once a release with this change is installed.
  • A saved login outranks both token addresses, as it already did for DEPOT_RUN_TOKEN_URL. A sandbox has no saved login.
  • agent watch, create --watch and stop from inside a sandbox need the API to take the turn token on WatchSession, InterruptSession and ArchiveSession. When it does, each call is a one-word switch from authed to asTurn.
  • Only agent list was run with a real turn token. create and logs were not; the per-call choice is covered by unit tests against a local server.

Post-deploy test plan

Nothing deploys: the change ships in the next CLI release. Once a release containing this commit is the depot on a Dispatch sandbox's PATH:

  1. depot agent create "Run: depot agent list; echo exit=\$? and reply with the output" --watch on any org. Expected: the reply holds a session table and exit=0.
  2. Same session, DEPOT_API_TOKEN_URL=http://169.254.169.253/nope depot agent list. Expected: stderr starts error: could not fetch a token from DEPOT_API_TOKEN_URL, exit 1.
  3. Same session, DEPOT_API_TOKEN_URL=http://127.0.0.1:9/token depot list projects. Expected: no line naming DEPOT_API_TOKEN_URL; the command behaves as it does on the previous release.
  4. On a laptop with a login, DEPOT_API_TOKEN_URL=http://127.0.0.1:1/ depot agent list. Expected: works as before, the URL is not contacted.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RQk3K7GpvQg7ySXyCrirEe


Note

Medium Risk
Changes authentication precedence and which token is sent per agent API call; mistakes could break sandbox agent commands or leak overly broad credentials to the wrong RPCs.

Overview
Enables depot agent (and related session RPCs) inside Dispatch sandboxes by minting credentials from DEPOT_API_TOKEN_URL, using the same fetch/refresh path as DEPOT_RUN_TOKEN_URL.

runtoken now keeps a separate source per env var, wraps HTTP clients to refresh both run and turn tokens, blocks redirects, and surfaces fetch failures as variable-named errors that omit URLs and response bodies.

ResolveAgentAuth returns an org-scoped token and a narrower turn token; agent commands pick per RPC via asTurn for CreateSession, GetSession, GetSessionMessage, and ListDepotAgentSessions, and the existing org token for watch/stop/send/launch. Commands that need only turn-token RPCs can succeed with just the turn URL; mixed commands fail fast with missing API token when no cache/org credential exists.

Integration tests assert the Bearer header each agent subcommand sends matches what the API accepts.

Reviewed by Cursor Bugbot for commit 619d73a. Bugbot is set up for automated code reviews on this repo. Configure here.

robstolarz and others added 2 commits November 14, 2026 19:58
A Dispatch sandbox sets DEPOT_API_TOKEN_URL, an address that answers with a
short-lived token acting as the running turn's sender. The CLI now reads it
through the same path as DEPOT_RUN_TOKEN_URL when no --token, DEPOT_TOKEN or
saved login is present, so `depot agent list` works there with no setup.

DEPOT_RUN_TOKEN_URL is preferred when both are set. A failed fetch prints an
error naming the variable; errors never carry the URL, the response body or
a token.

DEP-8526

Co-Authored-By: Claude Mythos 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RQk3K7GpvQg7ySXyCrirEe
A transport error can quote the request URL or a response header, so a
failed fetch now reports a category (timed out, could not connect, failed)
instead of the error's text, and redirects are not followed.

DEP-8526

Co-Authored-By: Claude Mythos 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RQk3K7GpvQg7ySXyCrirEe
@linear-code

linear-code Bot commented Nov 15, 2026

Copy link
Copy Markdown

DEP-8526

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Risk: high. Cursor Bugbot completed successfully with no findings that need human review, so this automation is approving. Auth token fetching from DEPOT_API_TOKEN_URL is high risk, which is within the configured approval threshold.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Comment thread pkg/runtoken/runtoken.go Resolved
Comment thread pkg/runtoken/runtoken.go Resolved
Comment thread pkg/cmd/org/credits_test.go Resolved
The turn token from DEPOT_API_TOKEN_URL is accepted by the session API
alone, so resolving it for every command replaced a sandbox's working
build and cache credentials. It now has its own source, consulted only by
`depot agent` commands, ahead of the cache token. Every other command
resolves exactly what it did before.

Fetch errors keep cancellation and timeout in the error chain without the
transport text, and a token source follows its variable instead of being
read once per process.

DEP-8526

Co-Authored-By: Claude Mythos 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RQk3K7GpvQg7ySXyCrirEe

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No new blocking issues. 1 optional suggestion (a nit or a note on pre-existing code) was found and not posted.

The API takes a turn's token for GetSession, GetSessionMessage,
ListDepotAgentSessions and CreateSession only. Launch and SendInput do
not take it, so `depot agent launch` and `depot agent send` resolve
credentials as they do on main.

Co-Authored-By: Claude Mythos 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RQk3K7GpvQg7ySXyCrirEe

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

A Dispatch turn's own token is accepted by CreateSession, GetSession,
GetSessionMessage and ListDepotAgentSessions only. Those calls send it;
WatchSession, InterruptSession and ArchiveSession send the token every
other command resolves, as before. A command that makes both kinds of
call (create --watch, watch) picks per call, and fails before sending
anything when it has no token for one of them. stop no longer reads the
turn token at all.

Co-Authored-By: Claude Mythos 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RQk3K7GpvQg7ySXyCrirEe
Comment thread pkg/cmd/agent/session.go Resolved
A session created with a turn's token and then refused its watch would
be left running, and each retry would add another. create --watch now
resolves its token as it does on main and uses it for every call.

Co-Authored-By: Claude Mythos 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RQk3K7GpvQg7ySXyCrirEe

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found no new bugs in this version. My earlier create --watch finding is addressed. It changes how credentials are picked for each call, so a human should still look before merging.

What was reviewed:

  • create --watch now resolves through orgCaller, so CreateSession and WatchSession go out with the same token. create without --watch, plus list, logs and watch, send the turn token only on the four RPCs named in asTurn.
  • DEPOT_API_TOKEN_URL is read only in ResolveAgentAuth, so ResolveProjectAuth and ResolveStaticOrgAuth (and with them depot build) rank credentials the same way as before.
  • fetchError keeps context.Canceled and DeadlineExceeded in the error chain without the URL or the response body. fromEnv now reads the env var on every call, so a later t.Setenv in a test takes effect.
Extended reasoning...

The change adds a second sandbox token source (DEPOT_API_TOKEN_URL) in pkg/runtoken and uses it for a subset of the depot agent session RPCs, through ResolveAgentAuth and the new caller/orgCaller split. It touches authentication: which token goes on which API call, what order credentials are tried in, and keeping token endpoint errors from leaking the URL or response body. The latest commit fixes the create --watch issue raised earlier, and the earlier precedence, error-chain and test-caching points also hold in the current code. I deferred rather than approved because this is auth code, and the per-RPC choice depends on server-side behaviour that cannot be checked from this repo.

@robstolarz

Copy link
Copy Markdown
Contributor Author

From Claude: merge gate proof (delegate approval).

  • Head: 619d73a4e75cb1da44232370e0effacd50071c5b
  • Base: origin/main at b16b8d0219633a351b4953989ce214158b96dade
  • Command: codex -s read-only -c mcp_servers.glass_house.enabled=false -C <worktree> review --base origin/main
  • Verdict: "No actionable defects were identified in the diff. Tests could not be run because the read-only sandbox prevented Go from creating its build directory." (tests ran green in CI on this head)
  • Earlier gate finding on 945063b0 ([P1] turn token sent to calls that refuse it) is fixed in this head: list, logs and create use the turn token; watch and stop use the ordinary credential.
  • Bugbot: approved, 0 open threads.
  • Date: 2026-11-15

@robstolarz
robstolarz merged commit ecac218 into main Nov 15, 2026
13 checks passed
@robstolarz
robstolarz deleted the rob/dep-8526-cli-token-url branch November 15, 2026 05:29
@robstolarz

Copy link
Copy Markdown
Contributor Author

Correction to the merge record: the bot approval on this PR was given on 7d1e159, four commits before the merged head 619d73a. At the merged head the review bot commented that it found no new bugs and that its earlier create --watch finding was addressed, but it did not approve, because the per-RPC token choice depends on server behaviour it cannot check from this repo. The local codex review on 619d73a was clean and there were 0 open threads. So this merged without an approval on its final head. The change is on main but not in a published release.

From Claude.

@robstolarz

Copy link
Copy Markdown
Contributor Author

Follow-up to the correction above, after checking the check runs on 619d73a: both Cursor Bugbot and Cursor Approval Agent: Pull Request Router and Approver completed with success on that head. The bot does not post a new approving review on each push, which is why the latest approving review points at an older commit. So the merged head did pass the Cursor review; the second review bot's deferral stands as written.

From Claude.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant