Skip to content

Mint a per-session agent token with auth token --session-id - #856

Draft
jeremy wants to merge 8 commits into
mainfrom
auth-token-session
Draft

jeremy wants to merge 8 commits into
mainfrom
auth-token-session

Conversation

@jeremy

@jeremy jeremy commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

Why

An agent profile's shared token is cached per profile and served to every process on the host. Run a dozen sandboxed boxes and the connector on one computer, and Basecamp sees one token: every request reads as the agent, with nothing saying which launch made it.

What

basecamp auth token --stored --session-id <id> [--session-label <label>] mints a fresh self-token for that one session. The id and label ride the client_credentials request as launch_id and launch_label. bc3 stores them on the token and logs the id beside the agent. bc3 can't use "session" for this, because there it means web sign-in. The CLI keeps "session", the word its callers use.

  • Validated locally, before anything is sent, against the server's rules. The id is 32 lowercase hex characters (128 random bits). The label is optional: at most 100 characters, not blank, with no control, format (bidi), private-use, surrogate or line/paragraph-separator characters (\p{Cc}\p{Cf}\p{Co}\p{Cs}\p{Zl}\p{Zp}, the same set bc3 refuses). Unassigned code points are accepted on both sides, because a host's Unicode tables can be newer than the CLI's or the server's, and a refused label costs the launch its whole token. A label without an id, or an id without --stored, is a usage error. A session token never comes from BASECAMP_TOKEN.
  • Agent profiles only. A person's login has no session to attribute, and it isn't handed out in place of one.
  • Without the flags, nothing changes. It's the same path, output and JSON shape. With them, --json adds session_id and session_label beside token.

Decisions worth a look

Not cached. Each call mints. The shared cached token is neither served nor written. The caller is the per-launch cache: the sandbox's nono cmd:// capture holds each token for 240 s inside the launch's own proxy, which dies with the launch. Caching here would mean one keyring entry per launch that nothing ever cleans up. It would also make every launch contend on the profile's one credential lock for a token only that launch may use. A capture every 240 s is about 15 mints an hour per box, well inside the token endpoint's per-client budget.

Fail closed: a token bound to the session, or none.

  • Refused (any 4xx): the command exits non-zero, and the message names the session and the OAuth error code. Following this package's existing rule (oauthErrorCodes), it doesn't repeat error_description, because the request carried the client secret.
  • Ignored: a server that predates session attribution answers 200 with an ordinary token and doesn't echo launch_id (or echoes different values). The command exits non-zero with "this Basecamp predates agent session attribution", and the minted token is discarded: not printed, not cached.
  • It never retries without the session.
  • This is the contract the later steps depend on. Once Basecamp can stop a session, a token not bound to it is one that stopping it wouldn't reach. And a silently unattributed token is exactly the failure that reads as working.
  • The cost falls only on callers that pass the flags. Today that's the sandbox, which ships after bc3.

Holds. A session mint presents the same client secret the shared mint does. So it honors a stored hold before sending anything, and it remembers a refusal of the secret (invalid_client, invalid_grant, a bare 401/403, a 429), so a dozen launches don't keep presenting a dead secret. It takes the credential key's lock only to write a hold; nothing is written on success.

A refusal of the request itself, such as invalid_request for a malformed launch parameter, holds nothing, so one bad launch can't stop the others. Per-(credential, session) holds, for a future "session stopped" refusal, belong with that step and aren't here.

Tests

  • internal/auth/agent_launch_test.go, red then green. It covers:
    • fresh mint each call, with the params sent and the stored credential untouched;
    • an id alone, with no empty label sent;
    • an ignoring server (no echo, another id, another label, a missing label), where the token is discarded and nothing is held;
    • an invalid_request refusal, which names the session and holds nothing;
    • an invalid_client refusal, which is held, so the next session mint sends nothing;
    • a non-agent profile, which is a usage error;
    • local validation, including the bidi-override, line-separator and invalid UTF-8 refusals, and acceptance of a character newer than the build's Unicode tables (U+1FAE9).
  • internal/commands/auth_token_session_test.go: the command's JSON shape with and without a session, and the flag-combination usage errors.
  • .surface updated for the two flags.
  • An empty --session-id "" (a launcher's unset variable) is a usage error, not a fall-back to the shared token. Session mode is decided by whether the flag was given, not by its value. Red then green in TestAuthTokenSessionFlagUsage.
  • bin/ci: green.

Review round

Copilot couldn't review (quota). My own adversarial pass found one real issue, fixed in the second commit: --session-id "$ID" with ID unset fell back to the shared token. It also turned up two points I'm leaving as they are:

  • The label echo is matched exactly, so bc3 has to store and echo the label verbatim. It does (validated, never normalized). A future server that normalizes labels would fail closed, not open.
  • A token minted by a server that ignores the session is discarded here but stays valid server-side for its hour. Nothing can revoke it without client-authenticated revocation, and it is no wider than the shared token the same host already holds.

Overlap with open PRs

Related

Step 1 of the multi-session proposal; draft for discussion with the agents team.

Copilot AI balanced review requested due to automatic review settings October 6, 2026 22:41
@github-actions github-actions Bot added commands CLI command implementations tests Tests (unit and e2e) auth OAuth authentication labels Oct 6, 2026

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 6 files

Reply with feedback, questions, or to request a fix.

Turn on auto-fix | Re-trigger cubic

Comment thread internal/commands/auth.go Outdated
Comment thread internal/auth/agent_launch_test.go Outdated
Copilot AI balanced review requested due to automatic review settings October 6, 2026 22:46

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI balanced review requested due to automatic review settings October 6, 2026 22:52

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

jeremy added 5 commits October 6, 2026 21:05
An agent profile's shared token is cached and served to every process on
the host, so Basecamp can't tell one sandboxed launch from another. With
--session-id (and optionally --session-label), auth token --stored mints a
fresh self-token for that session, sending launch_id and launch_label on
the client_credentials request, and prints it without caching it or
touching the shared one.

The token is bound to the session or not handed out: a server that
refuses the parameters, or answers without echoing them back because it
predates session attribution, fails the command and the unbound token is
discarded. Holds on the client secret are honored and written as the
shared mint's are; a refusal of the session's own parameters holds
nothing.
A launcher that passes --session-id "$ID" with ID unset got the profile's
shared, unattributed token, because session mode was decided by the flag's
value. It is now decided by whether the flag was given, so an empty id is a
usage error like any other malformed one.
bc3 refuses exactly control, format, private-use, surrogate and line or
paragraph separator characters in a launch label, and no longer refuses
unassigned code points: a host's Unicode tables can be newer than the
server's, and a refused label costs the launch its whole token. The local
check now refuses the same set, named category by category, since Go's
unicode.C table also covers code points it doesn't know.
The mock echoes the launch id back whatever the request sent, so a second
mint that dropped it would have passed. Each recorded mint is now checked.
#854 renamed MintHold and its helpers to RenewalHold after this branch
was cut, so the session mint and its tests named things that no longer
exist.
@jeremy
jeremy force-pushed the auth-token-session branch from 2b91776 to c56bff3 Compare October 7, 2026 04:15
Copilot AI balanced review requested due to automatic review settings October 7, 2026 04:15
@jeremy

jeremy commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

@codex review

@jeremy

jeremy commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review

@jeremy I have started the AI code review. It will take a few minutes to complete.

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-22T20:49:48.837439Z 5f3dfda Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

Reviewed commit: c56bff331b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 6 files

Reply with feedback, questions, or to request a fix.

Turn on auto-fix | Re-trigger cubic

Comment thread internal/auth/agent_launch.go Outdated
…s sessions

A server that echoed this session's id but a different label, or none,
was still told it predates agent session attribution, which its own echo
contradicts. Only a missing id says that now; an id for another session
and a label that came back wrong or not at all each say so.
Copilot AI balanced review requested due to automatic review settings October 7, 2026 05:26
@jeremy

jeremy commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

@codex review

@jeremy

jeremy commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review

@jeremy I have started the AI code review. It will take a few minutes to complete.

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

Reviewed commit: 40a798b79a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 6 files

Reply with feedback, questions, or to request a fix.

Turn on auto-fix | Re-trigger cubic

Comment thread internal/auth/agent_launch.go
@jeremy

jeremy commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

Rebased onto main (943dbb5). Git found no textual conflicts, but the branch no longer built: #854 renamed MintHold, rememberMintHold, mintHoldRefused and errMintHeld to their RenewalHold forms after this branch was cut. c56bff3 follows the rename. The PR stays a draft.

Review threads: 3 resolved (2 fixed, 1 declined with the reasoning in its thread).

Declined:

CI is green on 40a798b. Codex reported on 40a798b with no major issues. cubic's review of that head raised only the declined finding. Copilot can't review because the requester's quota is exhausted.

@jeremy

jeremy commented Oct 14, 2026

Copy link
Copy Markdown
Member Author

Parked with bc3 #13710, behind Rob's agent-connections plan, which defers per-launch attribution (card https://app.basecamp.com/2914079/buckets/48699913/card_tables/cards/10311028084).

If revived, release this only after bc3 #13710 is deployed. This PR refuses to mint unless Basecamp records the session, and agent-sandbox-nono's check of the CLI's flags can't tell a server without #13710, so an early release would cut off its data-tier boxes again.

Basecamp stores the launch id and label on the token, and its request
lines carry only the id beside the agent (bc3 #13710 filters the label
from logged parameters). The comment said the label went into the logs.
Copilot AI balanced review requested due to automatic review settings October 17, 2026 04:58

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

🟡 Changes recommended

Concurrent session refusals can weaken stored credential holds and allow a known-dead secret to be sent again.

1 open issue

Review effort: Balanced

Comment thread internal/auth/agent_launch.go Outdated
A session mint holds no credential lock across its round trip, so another
launch's verdict on the same secret can be written while it is in flight.
The last write won: an invalid_client refusal, followed by the 429 the
abuse tracker answers once those refusals trip it, left a one-minute hold
in place of a permanent one, and the dead secret went out again after it.
A shorter 429 could likewise cut short a longer one.

rememberRenewalHold now keeps a stored hold that still holds and outlasts
the new one, and returns it, so the session mint answers with the verdict
that stood and tells its caller the wait the next mint will meet.
Copilot AI balanced review requested due to automatic review settings October 22, 2026 18:39

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

🔵 Needs a closer look

Security-sensitive token attribution and concurrent hold handling warrant human confirmation against the server-side contract, which was unavailable to reviewers.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

@jeremy

jeremy commented Oct 22, 2026

Copy link
Copy Markdown
Member Author

Review threads: 1 resolved (1 fixed, 0 declined).

Not reported on the head:

  • Copilot: requested on 5f3dfda and still pending after 38 minutes.
  • Codex: last reported on 40a798b. It was already re-requested on this PR, so I didn't ask again.

The branch merges cleanly into main at e2d5b2a (v0.17.0), so I didn't rebase it. make check passed on that merge result, and CI is green on 5f3dfda.

I checked the contract against bc3 #13710 as merged (5d78ae75). bc3 accepts launch_id as 32 lowercase hex characters. It accepts launch_label at 1 to 100 characters: not all whitespace, no Cc, Cf, Co, Cs, Zl or Zp characters, and only alongside an id. It echoes both on the token response, and a malformed value gets invalid_request. --session-id and --session-label validate to the same rules and send those field names, so nothing needed to change. The PR stays a draft.

@jeremy

jeremy commented Oct 22, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: 5f3dfda0a2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

This branch has not been deployed

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

Labels

auth OAuth authentication commands CLI command implementations tests Tests (unit and e2e)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants