@sanlabs/sanbox-cli 0.0.5 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,23 +15,60 @@ installed_cli_version="$(sanbox --version)"
15
15
  test "$installed_cli_version" = "$latest_cli_version"
16
16
  ```
17
17
 
18
- Always use the latest published CLI. CLI 0.0.5 adds organization discovery from the API key,
19
- Hermes-aware template metadata, permanent paused sandboxes, and same-sandbox follow-ups.
18
+ Always use the latest published CLI. CLI 0.0.9 adds user login for private SSH access to running
19
+ Hermes Computers.
20
+ CLI 0.0.6 added Anthropic self-hosted environment inspection and administration.
20
21
 
21
22
  ## Configure
22
23
 
24
+ For an interactive local terminal, sign in with your Sanbox account:
25
+
26
+ ```bash
27
+ sanbox login
28
+ ```
29
+
30
+ The CLI opens the console for approval, binds the session to one organization, and stores it in
31
+ `~/.sanbox/credentials.json`. The file is written atomically and limited to the current user on
32
+ POSIX systems. Run `sanbox logout` to revoke and remove that session.
33
+
34
+ For non-interactive automation, continue to use an organization API key:
35
+
23
36
  ```bash
24
37
  export SANBOX_API_URL=https://console.sanbox.cloud
25
38
  export SANBOX_API_KEY=sbx_live_...
26
39
  export SANBOX_TEMPLATE=<template-id-or-slug>
27
40
  ```
28
41
 
29
- `SANBOX_API_URL` is optional for the hosted service. `SANBOX_TEMPLATE` can instead come from `--template` or `.sanbox/config.json` `default_template`.
30
- The CLI derives the organization automatically from `SANBOX_API_KEY`; use an organization-scoped
31
- key that has access to exactly one organization.
42
+ `SANBOX_API_URL` is optional for the hosted service. `SANBOX_TEMPLATE` can instead come from
43
+ `--template` or `.sanbox/config.json` `default_template`. Organization API keys are for automation;
44
+ they cannot open terminal or SSH sessions.
32
45
 
33
46
  An org admin configures provider credentials and templates in the console. Do not pass provider keys to the CLI or a runner.
34
47
 
48
+ ## Connect An Anthropic Self-Hosted Environment
49
+
50
+ Create a `self_hosted` environment in Claude Console and generate its environment key. Then connect
51
+ it to Sanbox:
52
+
53
+ ```bash
54
+ export ANTHROPIC_ENVIRONMENT_KEY='<environment-key>'
55
+ sanbox anthropic-environments connect env_example --json
56
+ unset ANTHROPIC_ENVIRONMENT_KEY
57
+
58
+ sanbox anthropic-environments get env_example --json
59
+ sanbox anthropic-environments list --json
60
+ ```
61
+
62
+ The environment key is read only from `ANTHROPIC_ENVIRONMENT_KEY`; there is no secret-bearing CLI
63
+ flag. Connect, key replacement, and disconnect require an organization-admin user session or
64
+ operator credential. Regular organization-scoped member keys can list and inspect connections.
65
+
66
+ Disconnecting requires explicit confirmation and preserves existing session-to-workspace mappings:
67
+
68
+ ```bash
69
+ sanbox anthropic-environments disconnect env_example --force --json
70
+ ```
71
+
35
72
  ## Check Readiness
36
73
 
37
74
  ```bash
@@ -44,7 +81,32 @@ sanbox doctor --json
44
81
 
45
82
  Model IDs are provider-scoped. The CLI never guesses or silently substitutes a provider, model, or template.
46
83
  For waited task runs, choose a template with `runnable: true`, `template_type: "runner"`, and
47
- `runner_config.harness: "opencode"`. Hermes service templates are always-on.
84
+ `runner_config.harness: "opencode"` or `"browser-use"`. Browser Use is for one-shot web tasks whose
85
+ target domains are already approved. Hermes service templates are always-on.
86
+
87
+ ## Create A Browser Use Template
88
+
89
+ Browser Use runs local headless Chromium inside the Firecracker sandbox. It requires OpenAI,
90
+ Anthropic, or Google Gemini and at least one explicit browser target:
91
+
92
+ ```bash
93
+ sanbox templates create \
94
+ --name "Web research" \
95
+ --harness browser-use \
96
+ --model-provider openai \
97
+ --model '<model-id>' \
98
+ --browser-domain example.com \
99
+ --browser-domain '*.example.org' \
100
+ --browser-max-steps 30 \
101
+ --browser-step-timeout-seconds 120 \
102
+ --browser-vision-mode auto \
103
+ --browser-viewport 1280x720 \
104
+ --browser-download-policy allow
105
+ ```
106
+
107
+ Targets are limited to hostnames and leading wildcards on ports 80 and 443. The remaining browser
108
+ security controls are fixed. Runs return `browser-use-response.md`, a sanitized run ledger, and any
109
+ allowed downloads. Browser Use templates do not support retained follow-up messages.
48
110
 
49
111
  ## Run Idempotently
50
112
 
@@ -91,21 +153,66 @@ sanbox runs download <run-id> \
91
153
 
92
154
  Downloads are path-safe and return their byte counts and SHA-256 digests. Existing files are preserved unless `--overwrite` is explicit.
93
155
 
94
- ## Continue A Run
156
+ ## Share A Running Filesystem
157
+
158
+ Create one read-only bearer URL for the running sandbox's entire root filesystem:
159
+
160
+ ```bash
161
+ sanbox runs share <run-id> --expires 1h --json
162
+ sanbox runs shares <run-id> --json
163
+ sanbox runs unshare <run-id> <access-point-id> --json
164
+ ```
165
+
166
+ The URL always starts at `/`; there is no root or output selector. Append a path to read an arbitrary
167
+ ordinary file, or add `?format=json` to a directory URL for agent-friendly navigation. The URL stops
168
+ working when the exact sandbox session stops, the link expires, or it is revoked. It is shown only
169
+ when created and must be handled as a bearer secret. See
170
+ [Live Filesystem Access](../docs/live-filesystem-access.md) for the HTTP contract and exclusions.
171
+
172
+ ## Resume Or Pause A Sandbox
173
+
174
+ ```bash
175
+ sanbox runs get <run-id> --json
176
+ sanbox runs resume <run-id> --wait --json
177
+ sanbox runs share <run-id> --expires 1h --json
178
+ sanbox runs pause <run-id> --wait --json
179
+ ```
180
+
181
+ Resume restores the latest writable Firecracker snapshot without starting the configured agent
182
+ harness. It acquires an exclusive manual lease and leaves the sandbox running for live filesystem
183
+ access until Pause creates the next snapshot generation. Do not submit agent work while that lease is
184
+ active. Paused state has no retention TTL and remains available until explicitly deleted.
185
+
186
+ ## SSH Into A Hermes Computer
187
+
188
+ ```bash
189
+ sanbox login
190
+ sanbox ssh <run-id>
191
+ ```
192
+
193
+ The run must be a currently running Hermes Computer created from the SSH-capable template artifact.
194
+ The signed-in user must own the run or be an organization admin. The command uses your local OpenSSH
195
+ client, but it does not expose the microVM on a public IP or port. The CLI generates a temporary
196
+ Ed25519 identity, pins the microVM's runtime host key, and tunnels the SSH stream over a one-time
197
+ authenticated WebSocket. The temporary key is deleted when the connection closes, and the saved
198
+ user token is not passed to the OpenSSH child process.
199
+
200
+ ## Continue An OpenCode Run
95
201
 
96
202
  ```bash
97
203
  sanbox runs messages <run-id> --json
98
204
  sanbox runs message <run-id> "Summarize the retained output" --wait --json
99
205
  ```
100
206
 
101
- The JSON response includes the user `message`, submitted `chat_job`, matching terminal `followup_event`, and conversation messages. The waiter matches `payload.chat_job_id`, so another concurrent follow-up cannot complete the wrong command.
102
-
103
- Completed runs pause into a durable Firecracker snapshot. `sanbox runs message <run-id> --message "..."`
104
- resumes the same writable sandbox and OpenCode session, then pauses it again. There is no retention
105
- TTL; paused sandboxes and their artifacts remain available until explicitly deleted.
207
+ The JSON response includes the user `message`, submitted `chat_job`, matching terminal
208
+ `followup_event`, and conversation messages. The waiter matches `payload.chat_job_id`, so another
209
+ concurrent follow-up cannot complete the wrong command. The follow-up resumes OpenCode for one turn,
210
+ then pauses the sandbox again.
106
211
 
107
212
  Check `sanbox runs get <run-id> --json` before a follow-up and require `sandbox_state: "paused"` with
108
213
  a positive `snapshot_generation`. Do not submit concurrent follow-ups to one run.
214
+ Treat `browser_use_followup_unsupported` as a signal to create a new browser agent run; its persisted
215
+ sandbox can still be resumed manually when a snapshot exists.
109
216
 
110
217
  ## Batch Work
111
218
 
package/dist/api.js CHANGED
@@ -150,6 +150,21 @@ export class SanboxClient {
150
150
  async listProviderModels(providerId) {
151
151
  return this.request(await this.orgPath(`/model-providers/${encodeURIComponent(providerId)}/models`));
152
152
  }
153
+ async listAnthropicEnvironments() {
154
+ return this.request(await this.orgPath("/anthropic-environments"));
155
+ }
156
+ async getAnthropicEnvironment(environmentId) {
157
+ return this.request(await this.orgPath(`/anthropic-environments/${encodeURIComponent(environmentId)}`));
158
+ }
159
+ async connectAnthropicEnvironment(environmentId, environmentKey) {
160
+ return this.request(await this.orgPath(`/anthropic-environments/${encodeURIComponent(environmentId)}`), {
161
+ method: "PUT",
162
+ body: JSON.stringify({ environment_key: environmentKey })
163
+ });
164
+ }
165
+ async disconnectAnthropicEnvironment(environmentId) {
166
+ return this.request(await this.orgPath(`/anthropic-environments/${encodeURIComponent(environmentId)}`), { method: "DELETE" });
167
+ }
153
168
  async listTemplates() {
154
169
  return this.request(await this.orgPath("/templates"));
155
170
  }
@@ -168,6 +183,12 @@ export class SanboxClient {
168
183
  async getRun(runId, signal) {
169
184
  return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}`), { signal });
170
185
  }
186
+ async createSSHSession(runId, publicKey) {
187
+ return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}/ssh-sessions`), {
188
+ method: "POST",
189
+ body: JSON.stringify({ public_key: publicKey })
190
+ });
191
+ }
171
192
  async listEvents(runId, afterEventId = 0, limit = 200, signal) {
172
193
  return this.request(`${await this.orgPath(`/runs/${encodeURIComponent(runId)}/events`)}?after_event_id=${afterEventId}&limit=${limit}`, { signal });
173
194
  }
@@ -177,6 +198,18 @@ export class SanboxClient {
177
198
  body: "{}"
178
199
  });
179
200
  }
201
+ async resumeRun(runId) {
202
+ return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}/resume`), {
203
+ method: "POST",
204
+ body: "{}"
205
+ });
206
+ }
207
+ async pauseRun(runId) {
208
+ return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}/pause`), {
209
+ method: "POST",
210
+ body: "{}"
211
+ });
212
+ }
180
213
  async sendMessage(runId, message, payload = {}) {
181
214
  return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}/messages`), {
182
215
  method: "POST",
@@ -189,6 +222,18 @@ export class SanboxClient {
189
222
  async listArtifacts(runId) {
190
223
  return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}/artifacts`));
191
224
  }
225
+ async listFileAccessPoints(runId) {
226
+ return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}/file-access-points`));
227
+ }
228
+ async createFileAccessPoint(runId, input) {
229
+ return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}/file-access-points`), {
230
+ method: "POST",
231
+ body: JSON.stringify(input)
232
+ });
233
+ }
234
+ async revokeFileAccessPoint(runId, accessPointId) {
235
+ return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}/file-access-points/${encodeURIComponent(accessPointId)}`), { method: "DELETE" });
236
+ }
192
237
  async downloadArtifact(runId, artifactPath) {
193
238
  return this.rawRequest(`${await this.orgPath(`/runs/${encodeURIComponent(runId)}/artifacts`)}?path=${encodeURIComponent(artifactPath)}`);
194
239
  }
package/dist/args.js CHANGED
@@ -1,4 +1,10 @@
1
- const multiFlags = new Set(["input", "include", "artifact", "telegram-allowed-user"]);
1
+ const multiFlags = new Set([
2
+ "input",
3
+ "include",
4
+ "artifact",
5
+ "telegram-allowed-user",
6
+ "browser-domain"
7
+ ]);
2
8
  export const booleanFlags = new Set([
3
9
  "help",
4
10
  "version",
@@ -12,7 +18,8 @@ export const booleanFlags = new Set([
12
18
  "verbose",
13
19
  "force",
14
20
  "write",
15
- "overwrite"
21
+ "overwrite",
22
+ "no-browser"
16
23
  ]);
17
24
  export const parseArgs = (argv) => {
18
25
  const command = [];