feat(sandbox): runCommand timeout/AbortSignal and file ops#122
Conversation
…, deleteFile, rename, exists)
|
@codex review |
🦋 Changeset detectedLatest commit: 0ec3434 The changes in this PR will be included in the next version bump. This PR includes changesets to release 7 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Greptile SummaryThis PR adds several capabilities to the sandbox SDK and CLI:
Confidence Score: 5/5Safe to merge; all three issues from the previous review round are addressed, and the new functionality is well-tested with no blocking defects. The collectExec extraction is thoroughly unit-tested including the settled guard. The new SFTP methods follow the established null/bool convention for missing-file cases. The exec timeout implementation correctly sets a timedOut flag before calling proc.kill() and clears the timer on normal exit. The only asymmetry is that the upload path in cp.ts doesn't auto-detect a remote directory the way the download path detects local directories. packages/cli/src/commands/sandbox/cp.ts — the upload path could be made more consistent with the download path's directory detection. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant CLI
participant Sandbox
participant SshTransport
participant collectExec
CLI->>Sandbox: "runCommand({ cmd, timeout, signal, onStdout })"
Sandbox->>Sandbox: validate timeout / detached guards
Sandbox->>SshTransport: "exec(remote, { timeoutMs, signal, onData })"
SshTransport->>SshTransport: signal.throwIfAborted()
SshTransport->>SshTransport: execStream(command)
SshTransport->>collectExec: collectExec(stream, limits)
collectExec->>collectExec: register data/stderr handlers (settled guard)
collectExec->>collectExec: set setTimeout(timeoutMs)
alt normal completion
collectExec-->>Sandbox: "{ stdout, stderr, exitCode }"
Sandbox-->>CLI: CommandFinished
else timeout fires
collectExec->>collectExec: "cleanup() settled=true, clearTimeout"
collectExec->>SshTransport: stream.signal(KILL), stream.close()
collectExec-->>Sandbox: reject(CommandTimeoutError)
else AbortSignal fires
collectExec->>collectExec: "cleanup() settled=true"
collectExec->>SshTransport: stream.signal(KILL), stream.close()
collectExec-->>Sandbox: reject(signal.reason)
end
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant CLI
participant Sandbox
participant SshTransport
participant collectExec
CLI->>Sandbox: "runCommand({ cmd, timeout, signal, onStdout })"
Sandbox->>Sandbox: validate timeout / detached guards
Sandbox->>SshTransport: "exec(remote, { timeoutMs, signal, onData })"
SshTransport->>SshTransport: signal.throwIfAborted()
SshTransport->>SshTransport: execStream(command)
SshTransport->>collectExec: collectExec(stream, limits)
collectExec->>collectExec: register data/stderr handlers (settled guard)
collectExec->>collectExec: set setTimeout(timeoutMs)
alt normal completion
collectExec-->>Sandbox: "{ stdout, stderr, exitCode }"
Sandbox-->>CLI: CommandFinished
else timeout fires
collectExec->>collectExec: "cleanup() settled=true, clearTimeout"
collectExec->>SshTransport: stream.signal(KILL), stream.close()
collectExec-->>Sandbox: reject(CommandTimeoutError)
else AbortSignal fires
collectExec->>collectExec: "cleanup() settled=true"
collectExec->>SshTransport: stream.signal(KILL), stream.close()
collectExec-->>Sandbox: reject(signal.reason)
end
Reviews (9): Last reviewed commit: "docs(sandbox): document env methods, dis..." | Re-trigger Greptile |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7fe65490f8
ℹ️ 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".
|
@codex re-review |
|
To use Codex here, create a Codex account and connect to github. |
Additive quick wins folded onto the timeouts/file-ops work: - SDK: `runCommand` accepts `onStdout`/`onStderr` callbacks to observe output live in the blocking path. Wired through `ExecLimits.onData` in `collectExec`, so streaming composes with `timeout` and `signal`. - SDK: implement `Symbol.dispose`/`Symbol.asyncDispose` so `using` / `await using` release the SSH connection on scope exit. Disposal disconnects but never deletes the sandbox. - CLI: add `bunny sandbox cp` to copy files to/from a sandbox over SFTP using `<sandbox>:<path>` refs, preserving local file mode on upload. Docs (README, package READMEs, AGENTS.md) and changesets updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xq3KNv1JcjWZS77ZjcRmYA
|
@amir-at-bunny I needed a |
…p errors (#123) - runCommand now rejects onStdout/onStderr combined with detached (they were silently ignored); the guard message points to command.kill()/logs() - sandbox cp reports a clear error for directory sources instead of a misleading "file not found", and stats the source once instead of twice - drop the repeated `as RemoteRef` casts in cp by narrowing ref once - reuse LogChunk for exec streaming chunks instead of a duplicated inline type - remove a dead `?? 0` fallback in collectExec's timeout path - trim redundant comments Claude-Session: https://claude.ai/code/session_01GrhrNWGpQR9qo7ovxQ13qY Co-authored-by: Claude <noreply@anthropic.com>
- new `bunny sandbox ls <sandbox>[:<path>]` lists files in a sandbox directory over SFTP via the SDK's listFiles (table/json/csv/markdown); the `ls` alias moves off `sandbox list` to make room - `bunny sandbox exec --timeout <seconds>` closes the SSH connection and exits 124 when the command outlives the limit - fix `sandbox exec` `--` handling: the documented `-- <command>` form was dropped by yargs, and a greedy `--env` array swallowed the command after it (now nargs:1, still repeatable) - consolidate sandbox record -> SDK handle boilerplate: parseRemoteRef and env/resolve.ts's sandboxFromName move to sandbox/resolve.ts, with connectSandbox layered on top for SSH-requiring commands (cp, ls) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GrhrNWGpQR9qo7ovxQ13qY
…s alias `bunny sandbox files list` (alias: ls) now houses the file listing, mirroring `bunny storage files`, and gives future file ops (rm, mv) a home. `bunny sandbox ls` is once again an alias of `sandbox list`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GrhrNWGpQR9qo7ovxQ13qY
- collectExec stops delivering onData chunks once the promise has settled, so a timeout/abort rejection is the last thing a streaming caller sees - transport.exec rejects an already-aborted signal before opening the SSH channel, so the remote command never starts - RunCommandOptions is now a blocking/detached union: timeout, signal, and output callbacks no longer type-check with detached: true, and dynamic options resolve to Command | CommandFinished instead of the wrong overload Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GrhrNWGpQR9qo7ovxQ13qY
…antics The SDK README was missing the env-var surface (create env, per-command env, getEnv/setEnv/unsetEnv) and disconnect() from the API table. Also note that streaming options are blocking-only (type-enforced) and that a pre-aborted signal never starts the remote command. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GrhrNWGpQR9qo7ovxQ13qY
* main: feat(sandbox): runCommand timeout/AbortSignal and file ops (BunnyWay#122) # Conflicts: # packages/sandbox/src/errors.ts # packages/sandbox/src/index.ts # packages/sandbox/src/transport.ts
@amir-at-bunny I'm experimenting with this for project "rabbit's foot".