add sandbox PoC#99
Conversation
🦋 Changeset detectedLatest commit: 986ee67 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 |
|
Hey @amir-at-bunny, this looks awesome! Can you run |
|
@jamie-at-bunny prompt added to the sandbox name as discussed. |
|
@amir-at-bunny is this PR "Ready for review"? Let's get it in 🚀 |
|
To use Codex here, create a Codex account and connect to github. |
|
@greptile-apps review |
Greptile SummaryThis PR introduces a
Confidence Score: 5/5Safe to merge as a PoC; all new functionality is additive and behind new commands with no changes to existing behaviour. Several issues flagged in the previous review round have been resolved in the current code (JSON output branch in create.ts, imagePullPolicy: always, e.type === cdn guard in findEndpointId). The remaining findings are non-blocking: the url list name filter can hide user-created endpoints with common labels, the SDK transport skips host-key verification mirroring the already-noted CLI concern, and there is a narrow close-during-connect race in SshTransport. None of these block basic sandbox usage. packages/sandbox/src/transport.ts — host-key verification and close race; packages/cli/src/commands/sandbox/url/list.ts — endpoint filter logic Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant CLI as bunny sandbox create
participant SDK as Sandbox.create()
participant MC as Magic Containers API
participant SSH as SshTransport
CLI->>SDK: "Sandbox.create({ name, region, apiKey })"
SDK->>MC: POST /apps (createApp)
MC-->>SDK: appId
SDK->>MC: "GET /apps/{appId} poll waitForSshHost"
MC-->>SDK: anycast publicHost (sshHost)
SDK->>SSH: new SshTransport(host, port, agentToken)
SDK->>SSH: waitUntilReachable(120s)
SSH-->>SDK: ready
SDK-->>CLI: Sandbox handle (appId, sshHost, agentToken)
CLI->>CLI: setSandbox() to config file
note over CLI,SSH: bunny sandbox exec / ssh
CLI->>CLI: getSandbox(name) to record
CLI->>SSH: "sshpass -e ssh root@host remoteCmd"
note over CLI,MC: bunny sandbox url add
CLI->>MC: "POST /apps/{appId}/containers/{id}/endpoints CDN"
MC-->>CLI: endpointId
CLI->>MC: "GET /apps/{appId}/endpoints poll waitForPublicHost"
MC-->>CLI: publicHost to https://publicHost
%%{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 as bunny sandbox create
participant SDK as Sandbox.create()
participant MC as Magic Containers API
participant SSH as SshTransport
CLI->>SDK: "Sandbox.create({ name, region, apiKey })"
SDK->>MC: POST /apps (createApp)
MC-->>SDK: appId
SDK->>MC: "GET /apps/{appId} poll waitForSshHost"
MC-->>SDK: anycast publicHost (sshHost)
SDK->>SSH: new SshTransport(host, port, agentToken)
SDK->>SSH: waitUntilReachable(120s)
SSH-->>SDK: ready
SDK-->>CLI: Sandbox handle (appId, sshHost, agentToken)
CLI->>CLI: setSandbox() to config file
note over CLI,SSH: bunny sandbox exec / ssh
CLI->>CLI: getSandbox(name) to record
CLI->>SSH: "sshpass -e ssh root@host remoteCmd"
note over CLI,MC: bunny sandbox url add
CLI->>MC: "POST /apps/{appId}/containers/{id}/endpoints CDN"
MC-->>CLI: endpointId
CLI->>MC: "GET /apps/{appId}/endpoints poll waitForPublicHost"
MC-->>CLI: publicHost to https://publicHost
Reviews (6): Last reviewed commit: "add cdn type to sandbox endpoint check" | Re-trigger Greptile |
yep. thanks i'll take a look at the review in a bit. |
|
@jamie-at-bunny pushed a couple of minor changes |
|
@amir-at-bunny looks really good! |
|
@greptile-apps re-review |
1 similar comment
|
@greptile-apps re-review |
8700846 to
6748221
Compare
|
@greptile-apps re-review |
* feat(packages/sandbox): sandbox sdk poc * fix(packages/sandbox): address Greptile review - validate env var names before shell interpolation (block injection) - reuse a single SFTP channel instead of opening one per file op - delete the app if exposePort fails during create() to avoid orphans - fast-fail waitForPublicHost on terminal app states - drop the unreachable throw lastErr in withRegistryRetry * fix(packages/sandbox): address Codex review - readFile returns null on SFTP NO_SUCH_FILE (numeric 2), not just ENOENT - resolve the MC client lazily so fromHandle reconnects need no API key - make port exposure idempotent and treat a slow host assignment as pending, not a failed create - thread the abort signal through the SSH reachability wait - ship @types/ssh2 as a runtime dependency for downstream consumers - settle detached commands on stream error instead of leaving wait() hanging * fmt
sshpass -p <token> exposed the root SSH password in the process argument list to any other user on the machine. Switch to sshpass -e, reading the token from the SSHPASS env var, across exec/ssh/runSshCommand. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace unverified curl | bash installers. Node now comes from a pinned NodeSource apt repo whose packages apt verifies against a signed-by key; Bun is downloaded as a version-pinned release artifact checked against its SHA256 (x64 + arm64). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
dbdf68a to
dcf11d9
Compare
|
@greptile-apps re-review |
|
@codex review |
|
@greptile-apps re-review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5ded7958d8
ℹ️ 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".
| { | ||
| displayName: "ssh", | ||
| anycast: { | ||
| type: "ipv4", |
There was a problem hiding this comment.
Use the MC Anycast enum when creating SSH endpoints
The Magic Containers spec defines the Anycast IP version as iPv4 (the existing apps endpoint code also sends that exact value), but sandbox creation sends ipv4 for the built-in SSH endpoint. In environments where the API validates enum values, every Sandbox.create / bunny sandbox create request is rejected before a sandbox can be provisioned.
Useful? React with 👍 / 👎.
| { | ||
| containerPort: SSH_PORT, | ||
| exposedPort: SSH_PORT, | ||
| protocols: ["Tcp"], |
There was a problem hiding this comment.
Send lowercase protocol names to MC
The MC endpoint schemas use lowercase protocol enum values (tcp, udp, sctp), but these sandbox endpoint requests send Tcp (and addCdnEndpoint does the same for user-exposed ports). Once the API validates the request body, this makes both the built-in SSH endpoint and sandbox url add fail endpoint creation.
Useful? React with 👍 / 👎.
| const remoteCmd = `cd ${JSON.stringify(cwd)} && ${command | ||
| .map((arg) => JSON.stringify(arg)) | ||
| .join(" ")}`; |
There was a problem hiding this comment.
Quote exec arguments with shell-safe single quotes
When bunny sandbox exec is given an argument or --cwd containing shell substitutions such as $() or backticks, JSON.stringify wraps it in double quotes, which the remote POSIX shell still expands. That means a command intended to pass a literal filename or argument can execute extra commands inside the sandbox; use the SDK's single-quote escaping/buildRemoteCommand-style quoting for these tokens.
Useful? React with 👍 / 👎.
| return [ | ||
| "sshpass", | ||
| "-e", | ||
| "ssh", |
There was a problem hiding this comment.
Avoid requiring an undeclared sshpass binary
sandbox exec and sandbox ssh now depend on sshpass, but the published Bun CLI only declares JS dependencies and does not bundle that system binary. On default macOS installs and many Linux environments this spawn fails with ENOENT before any SSH connection is attempted, so the new CLI commands are unusable unless users independently install an undocumented prerequisite.
Useful? React with 👍 / 👎.
| export function extractAnycastHost(app: App): string | null { | ||
| for (const ct of app.containerTemplates ?? []) { | ||
| for (const ep of ct.endpoints ?? []) { | ||
| if (ep.type === "anycast" && ep.publicHost) return ep.publicHost; |
There was a problem hiding this comment.
Read the assigned anycast IP for SSH
Even after the endpoint request succeeds, anycast endpoints are reached via publicIpAddresses rather than publicHost (see the existing apps endpoint formatter), so this condition never becomes true for the sandbox SSH endpoint. In that case Sandbox.create waits until the SSH-host timeout and then deletes the app instead of connecting to the assigned IP.
Useful? React with 👍 / 👎.
| body: { | ||
| displayName, | ||
| cdn: { | ||
| isSslEnabled: false, |
There was a problem hiding this comment.
Don't return HTTPS for endpoints with SSL disabled
When sandbox url add creates an endpoint through this path, SSL is explicitly disabled, but Sandbox.exposePort still returns https://${host} and the CLI prints that URL. Users exposing a port will be handed an HTTPS URL for a CDN endpoint that was created without SSL, so the advertised URL can fail TLS; either enable SSL here or return/log an http:// URL.
Useful? React with 👍 / 👎.
| if (!force) { | ||
| const ok = await confirm( | ||
| `Delete sandbox "${name}" (app ${record.app_id})?`, | ||
| ); |
There was a problem hiding this comment.
Keep sandbox delete non-interactive in JSON mode
When automation invokes bunny sandbox delete --output json without --force, this branch still opens an interactive confirmation prompt, and the success path later writes plain text to stdout instead of JSON. That violates the CLI's JSON-mode contract and can either hang scripts or make their JSON parser fail; destructure output and suppress prompts/emit a JSON result in that mode.
Useful? React with 👍 / 👎.
|
|
||
| /** Run a command, blocking for the result unless detached is set. */ | ||
| async runCommand(command: string, args?: string[]): Promise<CommandFinished>; | ||
| async runCommand(command: RunCommandOptions): Promise<Command>; |
There was a problem hiding this comment.
Match runCommand's object overload to its runtime result
The object-form overload advertises Promise<Command> for every RunCommandOptions call, but the implementation returns CommandFinished unless detached is true. SDK callers using the object form just to set cwd, env, or sudo will type-check when calling logs()/wait() on the result and then fail at runtime, so split the overloads by detached: true or return the advertised type.
Useful? React with 👍 / 👎.
| const existing = await findEndpointId(client, appId, displayName); | ||
| if (existing) return existing; |
There was a problem hiding this comment.
Don't reuse endpoint names across different ports
If a sandbox already has a CDN endpoint with this display name, exposePort returns that endpoint ID without checking whether it points at the requested container port. Re-running after a slow host assignment is fine, but a legitimate call like exposing 8080 with a label that previously targeted 3000 silently returns the old URL and leaves routing on the old port; verify the mapping or surface a conflict.
Useful? React with 👍 / 👎.
| const environmentVariables = [ | ||
| { name: "AGENT_TOKEN", value: params.agentToken }, | ||
| ...Object.entries(params.env).map(([name, value]) => ({ name, value })), |
There was a problem hiding this comment.
Reserve AGENT_TOKEN from user-provided env
When SDK callers pass env.AGENT_TOKEN, the request contains two AGENT_TOKEN environment variables. Depending on API/container resolution this can either reject creation or override the generated root password while the handle still stores the generated token, causing SSH provisioning to fail and the app to be deleted; reject this reserved key or append the generated token last.
Useful? React with 👍 / 👎.
No description provided.