Skip to content

--runtime claude-code|codex|gemini bypasses the attested launch, and --sandbox-required passes anyway #363

Closed

Description

@tps-flint

--runtime claude-code|codex|gemini bypasses the attested launch, and --sandbox-required does not stop it

Found in security review of the v0.6.0 release (#362). Documented in the release notes and in launch-attestation.ts's header rather than left implicit — this issue is the fix.

What happens

packages/cli/bin/tps.ts branches on --runtime before the attested-launch path:

} else if (action === "start") {
  const runtimeArg = /* …--runtime… */;
  if (runtimeArg === "claude-code" || runtimeArg === "codex" || runtimeArg === "gemini") {
    // …spawns the runtime directly…

That branch never calls runAgent({action:"start"}), and runAgent is the only path to launchAttested() — which has exactly one caller, src/commands/agent.ts:877. The three runtime modules each spawn directly:

  • src/utils/claude-code-runtime.ts — spawn("claude", …)
  • src/utils/codex-runtime.ts — passes --sandbox <mode>, which is Codex's own workspace-write sandbox, not nono
  • src/utils/gemini-runtime.ts — spawn("gemini", …)

The part that is worse than the gap itself

launchesAgent() in src/utils/nono.ts recognises the command by name:

if (command === "agent") return sub === "start";

So tps agent start --id X --sandbox-required --runtime claude-code passes the launch gate — the gate sees agent start and is satisfied — and then takes the unconfined branch.

A flag that reports the sandbox is required, on a path that cannot provide one, is worse than no flag. It converts "unsandboxed" into "believed sandboxed". This is reachable from a generated unit or any wrapper that passes --runtime.

Scope, stated honestly

This is not a regression. v0.5.4 shipped no sandbox at all — neither nono.ts nor launch-attestation.ts existed. v0.6.0 confines the default path and leaves these three unconfined, which is a strict improvement over what is published today. The defect is that the claim was universal when the coverage is not.

Fix

Two parts, and the second is worth doing even if the first takes longer:

  1. Route the three runtimes through launchAttested() like the default path, carrying --sandboxed / --sandbox-required through the branch.
  2. Until then, make the branch refuse when --sandbox-required is set — a few lines. It does not confine anything, but it stops the flag from asserting a guarantee it cannot honour, which is the part that turns a known gap into a false belief.

Acceptance

  • tps agent start --runtime <each of the three> either runs under nono, or exits non-zero when --sandbox-required is set, naming the runtime and the reason.
  • A test covers all three runtimes, and fails before the change.
  • No comment or release note in the tree claims universal coverage while an exemption exists. launch-attestation.ts's header was corrected in release: v0.6.0 — first publish since February, lockstep bump + changelog #362 as part of this.

Refs #341, #350, #362

Activity

  1. moved this to Triage in Office queueon Oct 1, 2026
  2. added
    P1Next up: high value or blocking other work
    on Oct 1, 2026
  3. self-assigned this
    on Oct 2, 2026
  4. moved this from Ready to In review in Office queueon Oct 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

P1Next up: high value or blocking other work

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions