Skip to content

fix(sandbox): don't install session keep-alive in non-interactive create#118

Open
tcerqueira wants to merge 4 commits into
mainfrom
tc/sandbox-create-noninteractive
Open

fix(sandbox): don't install session keep-alive in non-interactive create#118
tcerqueira wants to merge 4 commits into
mainfrom
tc/sandbox-create-noninteractive

Conversation

@tcerqueira

Copy link
Copy Markdown

Problem

sandbox create --json --non-interactive hangs forever in CI (rough edge from #112).

create defaults to --timeout session. A session timeout keeps the sandbox alive only while the creating process (the primary client) stays connected; the CLI implements this by installing a SIGINT keep-alive and blocking until Ctrl+C. That keep-alive had no TTY / --non-interactive guard, while the JSON-emit-and-exit path only ran for non-session timeouts. So with the default session timeout in non-interactive mode the process installed the keep-alive and blocked indefinitely, never emitting JSON nor exiting.

Fix

  • In non-interactive mode (isNonInteractive), reject the default session timeout up front — before creating an orphan sandbox — with a USAGE / NON_INTERACTIVE_REQUIRED envelope (exit 2) whose hint points to an explicit --timeout (e.g. --timeout 15m). A session-scoped sandbox would be destroyed the instant this process exits, so silently returning would hand back a dead id; requiring an explicit, self-sufficient timeout is the correct and least-surprising CI semantics.
  • Gate the SIGINT keep-alive (and the ssh-fallback keep-alive) on interactivity; the non-interactive ssh fallback now emits the result and exits instead of blocking.
  • Route keep-alive/disconnect status chrome to stderr (stdout discipline) and enrich the --json create payload with org and timeout.

Interactive behavior (keep-alive until Ctrl+C) is unchanged.

Tests

  • Added a deterministic regression test (tests/agent.test.ts): sandbox create --json --non-interactive with the default session timeout now fails fast with a clean stdout and a NON_INTERACTIVE_REQUIRED envelope (exit 2) — the test would hang without the fix.
  • deno fmt --check, deno lint, deno check all pass.
`sandbox create` defaults to `--timeout session`, which keeps the sandbox
alive only while this process (the primary client) stays connected and
blocks on a SIGINT keep-alive. In non-interactive / CI mode that loop never
terminated: `create --json --non-interactive` hung forever, never emitting
JSON nor exiting.

Reject the default session timeout up front in non-interactive mode (before
creating an orphan) with a USAGE / NON_INTERACTIVE_REQUIRED envelope that
points to an explicit `--timeout`, since a session-scoped sandbox would be
destroyed the moment the process exits. Gate the SIGINT keep-alive (and the
ssh fallback) on interactivity, route status chrome to stderr, and enrich
the `--json` create payload with org and timeout.
…mmands

Standardize the output contract so stdout carries ONLY data payloads (the
--json result, or in human mode the list table / piped value) while every
success / confirmation / status message goes to stderr, in both --json and
human modes.

sandbox/mod.ts:
- create: "Created sandbox with id" and "Exposed port ... to ..." -> stderr
  (the exposeHttp branch no longer needs a --json special-case)
- kill: "Sandbox ... killed successfully." -> stderr
- deploy: "Successfully deployed ..." -> stderr
- ssh helper: the "ssh <conn>" echo and the connect-info fallback -> stderr

sandbox/volumes.ts, sandbox/snapshot.ts:
- delete: "Successfully deleted volume/snapshot ..." -> stderr

config.ts (shared spine, reached by every sandbox command):
- "Created configuration file at ..." and the interactive "Selected
  organization/application" confirmations -> stderr. The config-file message
  in particular was leaking onto stdout and corrupting `sandbox create --json`
  whenever it ran in a directory without an existing deno.json(c).

Data outputs (sandbox/volume/snapshot ids, list tables, extend result, exec
output) stay on stdout unchanged.
`sandbox create --ssh` resolved interactivity only on the ssh *failure*
branch (`sshIntoSandbox` returning false, which happens solely when the
`ssh` binary is absent). When `ssh` is installed — the normal CI/dev case —
`sshIntoSandbox` reached `command.spawn()` with `stdin: "inherit"` against a
non-TTY stdin and blocked reading the inherited pipe until EOF, reintroducing
the exact hang this PR removes, with no JSON ever emitted.

Resolve the non-interactive condition BEFORE invoking `sshIntoSandbox`: in
non-interactive mode, never open an interactive ssh session. Instead expose
ssh, emit the JSON result enriched with the connection details
(`ssh.hostname` / `ssh.username`) so the caller can connect themselves, and
exit 0; the sandbox lives for its explicit (non-session) timeout. The
interactive `--ssh` path is unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant