Skip to content

box runs: re-land the per-run token file (#3574) together with its senders and readers #3581

Open

Description

@hsinatfootprintai

Problem

#3574 (per-run token file for agent runs; stop staging the box principal) was reverted in the v0.132.0 release PR (#3576) because nothing yet sends StartBoxRunRequest.run_token or reads CONTAINARIUM_JWT_TOKEN_FILE. The change also stops copying the box principal into the run user's home, so a run started without a token would have no platform credential. The release checklist's "paired changes" rule applies: the first half must not ship without the second.

Current state in v0.132.0 / v0.132.1: the run_token field (10) stays in the proto (the FILE-level breaking gate forbids deleting it), the daemon accepts and ignores it, and it does not advertise the box_run_token capability.

Task

Re-land #3574's behavior (by reverting the revert of the daemon code) only together with the senders and readers:

  • every client that starts runs sends run_token after checking the box_run_token capability,
  • every consumer reads CONTAINARIUM_JWT_TOKEN_FILE,
  • an upgrade note names both, since an older client against a new daemon loses the staged credential.

Restore the field's doc comment at the same time.

Acceptance

  • An end-to-end run with a token reaches the platform with the per-run credential and no staged principal.
  • A run from a client that does not send a token is refused with a clear error, or documented as unsupported.
  • Changelog has an "Upgrading" entry.

Blocked by: #3609, #3610
Sprint: #3603
Part of #3603

Estimate: build 1d · CI+review 1d · deploy+verify wait 1d → ships 2026-11-17 (P50) / 2026-11-19 (P80)
Waiting on: #3609, #3610 (dependencies); this is link 3 of 3: daemon re-land, pinning test, upgrade note

Verify on dev: start a run with a token and one without: the first reaches the platform with the per-run credential and no principal staged in the run user's home; the second is refused with a clear error; the box_run_token capability is advertised

Activity

  1. added
    choreMaintenance that fits no other type (deps, refactor, cleanup)
    phase-1-flowSprint phase 1: on the critical path — the flow does not work without it
    needs-verificationCannot be proven by CI — verify against a deployed environment
    needs-decisionWaiting on a human decision — not implementable until answered
    model:opusRoute to Opus — standard multi-file feature work
    runtime:claudeWork runs under the Claude Code CLI (default)
    size:LAgent build time ~4h-1 day
    on Nov 9, 2026
  2. hsinatfootprintai commented on Nov 10, 2026

    @hsinatfootprintai
    ContributorAuthor

    Decisions (Q3, Q4 on #3603, defaults accepted by @hsinatfootprintai, 2026-11-10): (Q3) a client that sends no run_token is refused with a clear error. (Q4) scope is the senders and readers in this repo (CLI code run, platform MCP, agent-runtime); external platforms get the upgrade note only. PR note: do not use Closes #3581 until the daemon half and all in-repo senders and readers have landed; hold the daemon half with merge-hold until then.

  3. added
    model:fableRoute to Fable — cross-cutting, ambiguous, or previously failed
    and removed
    needs-decisionWaiting on a human decision — not implementable until answered
    model:opusRoute to Opus — standard multi-file feature work
    on Nov 10, 2026
  4. hsinatfootprintai commented on Nov 10, 2026

    @hsinatfootprintai
    ContributorAuthor

    Routed model:fable (runtime:claude): critical path of #3603, cross-cutting (daemon, every in-repo sender and reader, capability, upgrade note), and the first attempt shipped half-done and was reverted. One tier above the rubric because a stall here stalls the sprint.

  5. hsinatfootprintai commented on Nov 10, 2026

    @hsinatfootprintai
    ContributorAuthor

    agent-6c9555e4-fable is starting processing it
    Box: none

  6. hsinatfootprintai commented on Nov 10, 2026

    @hsinatfootprintai
    ContributorAuthor

    agent-6c9555e4-fable: parking before any code. Q3/Q4 name the in-repo senders and readers as CLI code run, platform MCP and agent-runtime. Reading the code, none of them is on the StartBoxRun path, so building them as senders means inventing surface the issue does not specify.

    What the code says (origin/main at 9c8a2d34)

    • internal/client/{grpc,http}.go has no StartBoxRun method. Nothing in internal/cmd, internal/mcp or cmd/ calls the RPC.
    • CLI code run (internal/cmd/code_run.go) and MCP code_run (internal/mcp/code_tools.go) start a run through an agent-box SSH session (ProcessStart) as the box owner. They never build a StartBoxRunRequest, and an owner-identity run never used the staged copy.
    • agent-runtime does not start box runs. It already passes CONTAINARIUM_JWT_TOKEN_FILE to the platform MCP from its seed, and cmd/mcp-server already reads that variable.
    • The staged principal is only copied for a run as the unprivileged run user (a box with a tool-access level). Its readers are the helper and skill-MCP binaries staged from the owner's home, plus the token path inside the staged MCP config. None of those is built from this repo, and nothing in this repo writes the principal file.
    • The PRD the original doc comment cites for the token's shape is not in this repo.

    So re-applying #3574 applies (I trial-applied it: conflicts only in box_run_start.go against the #3582 supervised spawn, resolvable), but the "senders and readers" half has no existing home here. Questions, each with the default I would take:

    # Question Default
    Q5 How does an in-repo sender come to exist? (a) add typed-client StartBoxRun plus a new daemon-API path on containarium code run (new flag), MCP code_run wrapping the same function; or (b) accept that this repo has no sender, ship daemon + refusal + capability + upgrade note, and pair it with the external platform's sender under merge-hold. (a), as a new opt-in flag; the SSH path stays the default and is untouched. This is new user-facing surface and likely pushes the PR past size L.
    Q6 Where does an in-repo sender get the token? Caller-supplied (a file flag, never argv), minted by the CLI through an existing RPC (which one, what scope and TTL), or minted by the daemon inside StartBoxRun (which would make Q3's refusal moot). caller-supplied through a --run-token-file flag; no minting in this PR, since the token's claims are specified outside this repo. MCP code_run gets no token argument (a credential in tool arguments is worse than the problem).
    Q7 Which tokenless runs does Q3 refuse? Every StartBoxRun, or only runs as the unprivileged run user (the only ones that lose a credential)? Command mode loads no agent credential today. only runs as the unprivileged run user, engine and command mode alike: FAILED_PRECONDITION naming run_token and box_run_token, before anything is rotated or spawned. Owner-identity runs are unchanged.
    Q8 Is there any in-repo reader to change? mcp-server and agent-runtime already use CONTAINARIUM_JWT_TOKEN_FILE. The staged MCP config still points at the removed principal path. no reader code change; add a test pinning that a run's MCP server takes the per-run file from the environment, and the upgrade note tells external consumers to read the variable. The daemon does not rewrite the staged config.

    Until these are answered the acceptance line "an end-to-end run with a token reaches the platform" cannot be met from this repo alone without guessing. No branch pushed, no PR.

    agent-6c9555e4-fable parked: waiting on a decision
    Box: none

  7. removed their assignment
    on Nov 10, 2026
  8. added
    needs-decisionWaiting on a human decision — not implementable until answered
    and removed
    needs-decisionWaiting on a human decision — not implementable until answered
    on Nov 10, 2026
  9. hsinatfootprintai commented on Nov 10, 2026

    @hsinatfootprintai
    ContributorAuthor

    Decisions (Q5–Q8 on #3603, defaults accepted by @hsinatfootprintai, 2026-11-11): (Q5) build an in-repo sender: typed client StartBoxRun plus an opt-in daemon-API path on code run, MCP wraps it. (Q6) token is caller-supplied via --run-token-file; no minting; no token argument on MCP. (Q7) tokenless refusal applies only to runs as the unprivileged run user (engine and command mode), FAILED_PRECONDITION before anything is rotated or spawned; owner-identity runs unchanged. (Q8) no in-repo reader change; add a pinning test and the upgrade note.
    Re-sized: Q5 takes this past size L, so per the sprint rules it is split into a chain instead of one issue: #3609 (typed client) → #3610 (CLI path + MCP) → this issue (link 3: re-apply the daemon half of #3574 on top of #3582's supervised spawn, pinning test, Upgrading entry). Link 3 must not merge before #3609 and #3610 are on main. Link 3 uses Closes #3581; links 1–2 use Refs.

  10. hsinatfootprintai commented on Nov 10, 2026

    @hsinatfootprintai
    ContributorAuthor

    agent-8216b0de-fable is starting processing it
    Box: none

    Link 3 of the chain, after Q5–Q8: #3609 is PR #3611, #3610 is PR #3615 (stacked). This PR stacks on #3615 and carries merge-hold.

  11. hsinatfootprintai commented on Nov 10, 2026

    @hsinatfootprintai
    ContributorAuthor

    agent-8216b0de-fable: PR #3616 opened at 40127009 — CI pending (https://github.com/FootprintAI/Containarium/pull/3616/checks). It is stacked on #3615 (which is stacked on #3611) and carries merge-hold: it must not merge before #3611 and #3615 are on main. Landing is /merge-steward's; a red run comes back to me as /engineer-implement fix 3616. This issue closes on merge, not now.
    Box: none

    Steward note for the stack: retarget #3615 to main before the feat/3609-client-startboxrun branch is deleted, and #3616 before feat/3610-code-run-token-file is; deleting a base branch closes the PR stacked on it.

    Verify on dev (fuller steps than the issue body; the same ones are in the PR). Needs a daemon at the deployed commit, an admin token, a box <box> with a recorded tool-access level whose run user is signed in, a box <owner-box> with no recorded level, and a platform-minted per-run token in <token-file> (0600, one line).

    1. GET /v1/system/info as admin lists box_run_token in info.capabilities.
    2. containarium code run <box> --command 'test -r "$CONTAINARIUM_JWT_TOKEN_FILE" && echo token-file-ok; test ! -e ~/.config/containarium/box-token && echo no-principal' --name verify-3581 --run-token-file <token-file> --server <host> prints token-file-ok and no-principal and exits 0.
    3. A prompt run started the same way whose task calls one platform MCP tool succeeds, and the control plane's request log shows the per-run token's subject, not the box principal's.
    4. After step 2 exits, /run/containarium/run-tokens/ in the box has no verify-3581.token.
    5. POST /v1/containers/<box>/code-runs with {"command":"true","runName":"verify-3581-b"} and no runToken is HTTP 400, gRPC code 9, naming run_token and box_run_token; containarium code runs <box> shows no verify-3581-b.
    6. The same request against <owner-box> starts and exits 0.

    Verification: #3603 (the row is already on the umbrella's queue).

    One finding for whoever plans the next step, not a blocker here: GetSystemInfo is admin-only, so only an admin principal can read box_run_token. The typed client and code run --run-token-file fail closed when they cannot read it. A non-admin sender would need another way to read the capability.

  12. hsinatfootprintai commented on Nov 10, 2026

    @hsinatfootprintai
    ContributorAuthor

    Decision (Q9 on #3603, @hsinatfootprintai, 2026-11-11): the capability read stays admin-only (GetSystemInfo) and the docs say so: sending a per-run token, including via code run --run-token-file, needs an admin token. A non-admin owner is refused. No change to the gate. A non-admin read is a possible follow-up, not part of this sprint.

  13. hsinatfootprintai commented on Nov 10, 2026

    @hsinatfootprintai
    ContributorAuthor

    Update (2026-11-11): link 3's PR #3616 now says Refs #3581 (part of #3603) instead of Closes, so merging it does not close this needs-verification issue before the batched pass. Close #3581 after its row on #3603 is ✅. Same change on #3612 (#3514) and #3614 (#3551).

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    choreMaintenance that fits no other type (deps, refactor, cleanup)model:fableRoute to Fable — cross-cutting, ambiguous, or previously failedneeds-verificationCannot be proven by CI — verify against a deployed environmentphase-1-flowSprint phase 1: on the critical path — the flow does not work without itruntime:claudeWork runs under the Claude Code CLI (default)size:LAgent build time ~4h-1 day

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions