Skip to content

feat(sandbox): add environment variable support#111

Merged
amir-at-bunny merged 9 commits into
mainfrom
sandbox/env
Jul 3, 2026
Merged

feat(sandbox): add environment variable support#111
amir-at-bunny merged 9 commits into
mainfrom
sandbox/env

Conversation

@amir-at-bunny

Copy link
Copy Markdown
Collaborator

No description provided.

@changeset-bot

changeset-bot Bot commented Jul 2, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2649a6b

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

@amir-at-bunny amir-at-bunny marked this pull request as ready for review July 2, 2026 09:29
@bogdan-at-bunny

Copy link
Copy Markdown

@codex review

@amir-at-bunny

Copy link
Copy Markdown
Collaborator Author

@greptile-apps review

@greptile-apps

greptile-apps Bot commented Jul 2, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds full environment variable support across the sandbox SDK and CLI: persisted vars baked in at create-time or managed post-creation via setEnv/getEnv/unsetEnv, and temporary vars injected per-invocation for exec and ssh. Previously flagged issues \u2014 the missing AGENT_TOKEN guard on Sandbox.create() and the env/procEnv variable shadow in ssh.ts \u2014 are both resolved here.

  • SDK (packages/sandbox): Sandbox.create now validates reserved keys before any network call; new getEnv/setEnv/unsetEnv methods do a full read-modify-write against the MC API, guarding reserved keys consistently.
  • CLI (packages/cli/src/commands/sandbox): Shared env-args.ts provides --env/--env-file parsing with quote handling and inline-comment stripping; create, exec, and ssh consume it; a new sandbox env namespace (set/list/delete) manages persisted vars.
  • provision.ts: setContainerEnv uses (client as any).PUT with a flat-map body that differs from the Array<{name,value}> format used by createApp and extractEnv \u2014 this inconsistency needs verification against the MC API spec.

Confidence Score: 4/5

Safe to merge with low risk outside of the setEnv/unsetEnv path; the env body format sent to the MC PUT endpoint needs verification before those operations can be relied on in production.

The create/exec/ssh env injection paths are well-constructed and the previously unguarded AGENT_TOKEN case is now correctly handled. The only genuine uncertainty is in setContainerEnv: it sends a flat Record body to a PUT endpoint while every other MC API interaction in the file uses Array-of-{name,value} objects, and the (client as any) cast means no compile-time check catches a format mismatch. If the endpoint requires the array format, sandbox env set, env delete, and unsetEnv would all fail at runtime on every invocation.

packages/sandbox/src/provision.ts — specifically the setContainerEnv body format and the lack of type coverage for the PUT endpoint.

Important Files Changed

Filename Overview
packages/sandbox/src/provision.ts Adds extractEnv (array→flat-map) and setContainerEnv (PUT with flat-map body); body format differs from createApp’s array-of-{name,value} and bypasses type-checking via (client as any).PUT
packages/sandbox/src/sandbox.ts Adds getEnv/setEnv/unsetEnv; AGENT_TOKEN guard on create() is now present; reserved-key enforcement is consistent across all three methods
packages/cli/src/commands/sandbox/env-args.ts New shared env-parsing helpers; handles quotes, export prefix, inline comments (space-hash); key validation pattern is correct
packages/cli/src/commands/sandbox/env/set.ts New sandbox env set command; correctly delegates validation and reserved-key enforcement to SDK layer; SandboxError is re-wrapped as UserError
packages/cli/src/commands/sandbox/env/delete.ts New sandbox env delete command; correctly surfaces which keys were removed vs. not found; errors when nothing was removed
packages/cli/src/commands/sandbox/ssh.ts Adds -e/--env and --env-file; env shadow fixed by renaming callback param to procEnv; prefix injected before exec /bin/bash -l
packages/cli/src/commands/sandbox/exec.ts Adds --env/--env-file with shortAlias:false to avoid consuming -e before the remote command; envPrefix injected into remote shell command
packages/cli/src/commands/sandbox/env-args.test.ts New unit tests covering splitPair, parseDotenv, collectEnv, and envPrefix; good coverage of quote handling and error cases

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant CLI
    participant envArgs as env-args.ts
    participant Sandbox as sandbox.ts
    participant Provision as provision.ts
    participant MC as MC API

    note over CLI,MC: sandbox create --env KEY=VAL --env-file .env
    CLI->>envArgs: collectEnv(entries, envFile)
    envArgs-->>CLI: "Record<string,string>"
    CLI->>Sandbox: "Sandbox.create({ env })"
    Sandbox->>Sandbox: assertValidEnvKey + reserved check
    Sandbox->>Provision: "createApp(client, { env })"
    Provision->>MC: POST /apps (environmentVariables array)
    MC-->>Provision: appId
    Provision-->>Sandbox: appId
    Sandbox-->>CLI: Sandbox instance

    note over CLI,MC: sandbox env set name KEY=VAL
    CLI->>Sandbox: sandbox.setEnv(vars)
    Sandbox->>Sandbox: assertValidEnvKey + reserved check
    Sandbox->>Provision: getApp(client, appId)
    MC-->>Provision: App (with environmentVariables array)
    Provision-->>Sandbox: app
    Sandbox->>Provision: extractEnv(app) flat map
    Sandbox->>Provision: setContainerEnv(client, appId, containerId, merged)
    Provision->>MC: "PUT /apps/{appId}/containers/{containerId}/env (flat map body)"
    MC-->>Provision: success/error
    Provision-->>Sandbox: void or SandboxError

    note over CLI,MC: sandbox exec name --env KEY=VAL -- cmd
    CLI->>envArgs: collectEnv(entries, envFile)
    envArgs-->>CLI: "Record<string,string>"
    CLI->>CLI: envPrefix(vars)
    CLI->>CLI: ssh remote cmd with inline prefix
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
    participant envArgs as env-args.ts
    participant Sandbox as sandbox.ts
    participant Provision as provision.ts
    participant MC as MC API

    note over CLI,MC: sandbox create --env KEY=VAL --env-file .env
    CLI->>envArgs: collectEnv(entries, envFile)
    envArgs-->>CLI: "Record<string,string>"
    CLI->>Sandbox: "Sandbox.create({ env })"
    Sandbox->>Sandbox: assertValidEnvKey + reserved check
    Sandbox->>Provision: "createApp(client, { env })"
    Provision->>MC: POST /apps (environmentVariables array)
    MC-->>Provision: appId
    Provision-->>Sandbox: appId
    Sandbox-->>CLI: Sandbox instance

    note over CLI,MC: sandbox env set name KEY=VAL
    CLI->>Sandbox: sandbox.setEnv(vars)
    Sandbox->>Sandbox: assertValidEnvKey + reserved check
    Sandbox->>Provision: getApp(client, appId)
    MC-->>Provision: App (with environmentVariables array)
    Provision-->>Sandbox: app
    Sandbox->>Provision: extractEnv(app) flat map
    Sandbox->>Provision: setContainerEnv(client, appId, containerId, merged)
    Provision->>MC: "PUT /apps/{appId}/containers/{containerId}/env (flat map body)"
    MC-->>Provision: success/error
    Provision-->>Sandbox: void or SandboxError

    note over CLI,MC: sandbox exec name --env KEY=VAL -- cmd
    CLI->>envArgs: collectEnv(entries, envFile)
    envArgs-->>CLI: "Record<string,string>"
    CLI->>CLI: envPrefix(vars)
    CLI->>CLI: ssh remote cmd with inline prefix
Loading

Reviews (2): Last reviewed commit: "style(sandbox): organize imports and for..." | Re-trigger Greptile

Comment thread packages/cli/src/commands/sandbox/env-args.ts
Comment thread packages/cli/src/commands/sandbox/ssh.ts Outdated

@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: a2be486e9e

ℹ️ 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".

export function withEnvOptions<T>(yargs: Argv<T>): Argv<T> {
return yargs
.option("env", {
alias: "e",

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 consume remote -e flags

Because sandbox exec registers the shared env option with alias -e, yargs treats -e after the sandbox name as a configured CLI option rather than part of the variadic remote command; unknown-options-as-args only preserves options that are not configured. A common command like bunny sandbox exec s node -e 'console.log(1)' now has -e consumed as --env, so collectEnv tries to parse the JS snippet as KEY=VALUE or the remote command loses its flag. Keep the short alias off commands that forward arbitrary argv, or require env parsing only before an explicit delimiter.

Useful? React with 👍 / 👎.

Comment on lines +53 to +55
if (output === "json") {
logger.log(JSON.stringify({ removed, missing }, null, 2));
return;

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 Return an error for JSON deletes that remove nothing

In --output json, this returns success before the existing text-mode check for removed.length === 0. When every requested key is absent, e.g. sandbox env delete s MISSING -o json, scripts receive exit 0 with { removed: [], missing: [...] } even though no variable was removed and the non-JSON path treats the same case as a UserError. Move the zero-removed check before the JSON output so exit semantics match.

Useful? React with 👍 / 👎.

onDebug: (msg) => logger.debug(msg, true),
name: sandboxName,
region,
env: envVars,

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 Reject reserved AGENT_TOKEN during sandbox creation

The new create-time env forwarding allows --env AGENT_TOKEN=... to reach sandbox provisioning, while the SDK generates its own agent token for SSH and later setEnv explicitly reserves AGENT_TOKEN. Passing it at creation sends a duplicate or overriding AGENT_TOKEN to Magic Containers, which can leave the container using a different SSH token than the one saved locally. Apply the same reserved-key check before forwarding create env vars.

Useful? React with 👍 / 👎.

process.exitCode = await withSshEnv(record, async (env) => {
const proc = Bun.spawn(
sshArgs(record, `cd ${WORKPLACE} && exec bash -l`, { tty: true }),
sshArgs(record, `cd ${WORKPLACE} && ${prefix}exec bash -l`, {

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 Use an absolute bash path for env SSH sessions

When a user supplies a temporary PATH, this builds a remote command like PATH='...' exec bash -l; the remote shell applies that assignment before the exec builtin resolves bash, so bunny sandbox ssh s -e PATH=/custom can fail to start the session with bash: not found instead of opening a shell with the requested environment. Use an absolute /bin/bash (or apply the env after the shell starts) so the session bootstrap is not affected by the injected PATH.

Useful? React with 👍 / 👎.

Comment thread packages/sandbox/src/sandbox.ts Outdated
}
const app = await getApp(this.client, this.appId);
const env = extractEnv(app);
const removed = keys.filter((key) => key in env);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 Badge Check only own env keys before redeploying

For a delete request whose missing key is inherited from Object.prototype (for example constructor or toString), key in env is true even though the sandbox variable is not set. That makes unsetEnv report the variable as removed and call the full environment replacement endpoint, unnecessarily redeploying/restarting the sandbox. Use Object.hasOwn(env, key) or a null-prototype map for this membership test.

Useful? React with 👍 / 👎.

if (eq === -1) continue;
const key = body.slice(0, eq).trim();
assertValidKey(key);
let value = body.slice(eq + 1).trim();

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 Strip inline comments from env-file values

When a dotenv file uses a standard unquoted inline comment such as PORT=8080 # web server, this parser keeps the comment text in the value and sends 8080 # web server to sandbox create, sandbox exec, or sandbox env set. That can break sandboxes that reuse existing .env files with annotated values; strip inline comments for unquoted values before storing them.

Useful? React with 👍 / 👎.

@amir-at-bunny

Copy link
Copy Markdown
Collaborator Author

@greptile-apps review

@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.

:shipit:

@amir-at-bunny amir-at-bunny merged commit 87e2c3d into main Jul 3, 2026
2 checks passed
@amir-at-bunny amir-at-bunny deleted the sandbox/env branch July 3, 2026 13:38
@github-actions github-actions Bot mentioned this pull request Jul 3, 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