Skip to content

add sandbox PoC#99

Merged
amir-at-bunny merged 16 commits into
mainfrom
poc/sandbox
Jun 30, 2026
Merged

add sandbox PoC#99
amir-at-bunny merged 16 commits into
mainfrom
poc/sandbox

Conversation

@amir-at-bunny

Copy link
Copy Markdown
Collaborator

No description provided.

@changeset-bot

changeset-bot Bot commented Jun 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 986ee67

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 7 packages
Name Type
@bunny.net/sandbox Minor
@bunny.net/cli Minor
@bunny.net/cli-linux-x64 Minor
@bunny.net/cli-linux-arm64 Minor
@bunny.net/cli-darwin-x64 Minor
@bunny.net/cli-darwin-arm64 Minor
@bunny.net/cli-windows-x64 Minor

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

@jamie-at-bunny

Copy link
Copy Markdown
Member

Hey @amir-at-bunny, this looks awesome! Can you run bun lint and fix any issues reported?

@amir-at-bunny

Copy link
Copy Markdown
Collaborator Author

@jamie-at-bunny prompt added to the sandbox name as discussed.

@jamie-at-bunny

Copy link
Copy Markdown
Member

@amir-at-bunny is this PR "Ready for review"? Let's get it in 🚀

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@jamie-at-bunny

Copy link
Copy Markdown
Member

@greptile-apps review

@greptile-apps

greptile-apps Bot commented Jun 22, 2026

Copy link
Copy Markdown

Greptile Summary

This PR introduces a @bunny.net/sandbox SDK package and a bunny sandbox CLI command group for provisioning, connecting to, and managing ephemeral sandbox environments backed by Magic Containers. It also ships a Docker image (sandbox/Dockerfile) with SSH, Bun, Node.js, and Claude Code pre-installed.

  • SDK (packages/sandbox/): Provides Sandbox.create/get/fromHandle, runCommand, writeFiles, readFile, exposePort, toHandle/fromHandle, and an ssh2-based SFTP/exec transport with retry logic.
  • CLI commands (packages/cli/src/commands/sandbox/): create, delete, list, exec, ssh, and url add/delete/list, backed by a new sandboxes section in the CLI config file.
  • Config: Adds SandboxRecordSchema (app ID, agent token, SSH host) persisted to the existing config file.

Confidence Score: 5/5

Safe 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

Filename Overview
packages/sandbox/src/transport.ts SSH/SFTP transport over ssh2. Missing host-key verification (MITM risk) and a close-during-connect race that can resurrect a supposedly closed connection.
packages/sandbox/src/provision.ts Magic Containers provisioning helpers. findEndpointId now correctly guards on e.type === cdn, fixing the previously flagged SSH-endpoint collision. Registry retry and timeout logic looks solid.
packages/sandbox/src/sandbox.ts Core Sandbox class. buildRemoteCommand shell-quotes all arguments correctly. exposePort returns https:// for an endpoint created with isSslEnabled: false (flagged in prior review).
packages/cli/src/commands/sandbox/url/list.ts Endpoint list command. Filters by hardcoded display names [api, ssh]; the api name is never system-created and silently hides user endpoints with that label — filtering on ep.type === cdn is more robust.
packages/cli/src/commands/sandbox/create.ts Sandbox create command. Now has a JSON output branch and the SDK uses imagePullPolicy: always — both issues flagged in a prior review are resolved.
sandbox/Dockerfile Ubuntu 24.04 image with Bun, Node.js, and Claude Code. Node.js and Bun installed via pinned apt/release artifacts with checksum verification — avoids the curl

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
Loading
%%{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
Loading

Reviews (6): Last reviewed commit: "add cdn type to sandbox endpoint check" | Re-trigger Greptile

Comment thread packages/cli/src/commands/sandbox/exec.ts Outdated
Comment thread packages/cli/src/commands/sandbox/create.ts Outdated
@amir-at-bunny

Copy link
Copy Markdown
Collaborator Author

@amir-at-bunny is this PR "Ready for review"? Let's get it in 🚀

yep. thanks i'll take a look at the review in a bit.

@amir-at-bunny

Copy link
Copy Markdown
Collaborator Author

@jamie-at-bunny pushed a couple of minor changes

@jamie-at-bunny

Copy link
Copy Markdown
Member

@amir-at-bunny looks really good!

@jamie-at-bunny

Copy link
Copy Markdown
Member

@greptile-apps re-review

1 similar comment
@amir-at-bunny

Copy link
Copy Markdown
Collaborator Author

@greptile-apps re-review

@amir-at-bunny

Copy link
Copy Markdown
Collaborator Author

@greptile-apps re-review

amir-at-bunny and others added 10 commits June 30, 2026 11:25
* 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>
@amir-at-bunny

Copy link
Copy Markdown
Collaborator Author

@greptile-apps re-review

Comment thread packages/sandbox/src/provision.ts
@amir-at-bunny amir-at-bunny marked this pull request as ready for review June 30, 2026 09:45
@bogdan-at-bunny

Copy link
Copy Markdown

@codex review

@amir-at-bunny

Copy link
Copy Markdown
Collaborator Author

@greptile-apps re-review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread packages/sandbox/src/provision.ts Outdated
{
containerPort: SSH_PORT,
exposedPort: SSH_PORT,
protocols: ["Tcp"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +56 to +58
const remoteCmd = `cd ${JSON.stringify(cwd)} && ${command
.map((arg) => JSON.stringify(arg))
.join(" ")}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +18 to +21
return [
"sshpass",
"-e",
"ssh",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +46 to +49
if (!force) {
const ok = await confirm(
`Delete sandbox "${name}" (app ${record.app_id})?`,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +223 to +224
const existing = await findEndpointId(client, appId, displayName);
if (existing) return existing;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +111 to +113
const environmentVariables = [
{ name: "AGENT_TOKEN", value: params.agentToken },
...Object.entries(params.env).map(([name, value]) => ({ name, value })),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@jamie-at-bunny jamie-at-bunny left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LFG

@amir-at-bunny amir-at-bunny merged commit 6c05e7f into main Jun 30, 2026
1 check passed
@amir-at-bunny amir-at-bunny deleted the poc/sandbox branch June 30, 2026 11:41
@github-actions github-actions Bot mentioned this pull request Jun 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants