refactor(sandbox): guard detached against output callbacks, clearer c…#123
Conversation
…p errors - 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 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GrhrNWGpQR9qo7ovxQ13qY
|
@codex review |
|
79f1060
into
feat/sandbox-timeouts-file-ops
|
Codex Review: Didn't find any major issues. Can't wait for the next one! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
| const info = await stat(local).catch(() => null); | ||
| if (!info) { | ||
| throw new UserError(`Local file not found: ${local}`); | ||
| } |
There was a problem hiding this comment.
The
stat(local).catch(() => null) call swallows all errors, not just ENOENT. If the user lacks read permission on the file, info will be null and the error message will say "Local file not found" instead of something like "Permission denied". Narrowing the catch to only ENOENT and re-throwing everything else would surface a more accurate message.
| const info = await stat(local).catch(() => null); | |
| if (!info) { | |
| throw new UserError(`Local file not found: ${local}`); | |
| } | |
| const info = await stat(local).catch((err: NodeJS.ErrnoException) => { | |
| if (err.code === "ENOENT") return null; | |
| throw err; | |
| }); | |
| if (!info) { | |
| throw new UserError(`Local file not found: ${local}`); | |
| } |
* feat(sandbox): runCommand timeout/AbortSignal and file ops (listFiles, deleteFile, rename, exists) * fix(sandbox): guard rename against silent overwrite, listFiles returns [] for missing dirs * fix(sandbox): rename guard uses lstat so dangling symlinks count as existing * feat(sandbox): streaming exec callbacks, disposal, and cp command 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 * refactor(sandbox): guard detached against output callbacks, clearer cp 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> * Update .changeset/sandbox-timeouts-file-ops.md * Update .changeset/sandbox-cp-command.md * Update .changeset/sandbox-streaming-and-disposal.md * feat(sandbox): sandbox ls command and exec --timeout - 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 * refactor(sandbox): move ls under a files namespace, restore sandbox ls 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 * fix(sandbox): address review feedback on exec cancellation and typing - 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 * docs(sandbox): document env methods, disconnect, and cancellation semantics 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 --------- Co-authored-by: Claude <noreply@anthropic.com>
as RemoteRefcasts in cp by narrowing ref once?? 0fallback in collectExec's timeout path