@mybase-dev/cli 1.0.8 → 1.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.
Files changed (2) hide show
  1. package/dist/mybase.js +123 -2
  2. package/package.json +1 -1
package/dist/mybase.js CHANGED
@@ -117,6 +117,7 @@ var KEY_RESOURCES = [
117
117
  { id: "users", name: "Users", description: "Your app's users and sessions (read), revoking and deleting them, and sending sign-in events (write)" },
118
118
  { id: "captcha", name: "Captcha", description: "Captcha settings (read), and verifying tokens (write)" },
119
119
  { id: "realtime", name: "Realtime", description: "Channels, members, and settings (read), and publishing messages (write)" },
120
+ { id: "sandbox", name: "Agent sandboxes", description: "Sandboxes, executions, files, artifacts, and browser connections" },
120
121
  { id: "audit", name: "Audit log", description: "Who did what: sign-ins, key and project changes, secret reads (read only)" }
121
122
  ];
122
123
  function parseDotenv(text) {
@@ -140,7 +141,7 @@ function parseDotenv(text) {
140
141
  // package.json
141
142
  var package_default = {
142
143
  name: "@mybase-dev/cli",
143
- version: "1.0.8",
144
+ version: "1.0.9",
144
145
  description: "The mybase CLI: projects, keys, cron, queues, cache, rate limits, analytics, docs, and agent setup for a self-hosted mybase.",
145
146
  type: "module",
146
147
  repository: {
@@ -262,15 +263,18 @@ function confirm(yes, warning, expected, what = expected) {
262
263
  var config = await loadConfig();
263
264
  var API_URL = (process.env.MYBASE_API_URL || config.url || DEFAULT_API_URL).replace(/\/$/, "");
264
265
  var TOKEN = process.env.MYBASE_TOKEN || config.token;
266
+ var apiToken = () => TOKEN;
265
267
  var CLI_VERSION = package_default.version;
266
268
  async function api(path, init) {
267
269
  const token = init?.token === undefined ? TOKEN : init.token;
268
270
  const base = init?.url ?? API_URL;
271
+ const headers = new Headers({ "Content-Type": "application/json", "User-Agent": `mybase-cli/${CLI_VERSION}`, ...token ? { Authorization: `Bearer ${token}` } : {} });
272
+ new Headers(init?.headers).forEach((value, key) => headers.set(key, value));
269
273
  let res;
270
274
  try {
271
275
  res = await fetch(`${base}${path}`, {
272
276
  ...init,
273
- headers: { "Content-Type": "application/json", "User-Agent": `mybase-cli/${CLI_VERSION}`, ...token ? { Authorization: `Bearer ${token}` } : {} }
277
+ headers
274
278
  });
275
279
  } catch {
276
280
  throw new CliError(`can't reach the API at ${base}. Check that it's running, or point MYBASE_API_URL / \`mybase login --url\` at it.`);
@@ -278,6 +282,8 @@ async function api(path, init) {
278
282
  init?.onStatus?.(res.status);
279
283
  if (res.status === 204)
280
284
  return;
285
+ if (res.ok && init?.raw)
286
+ return res;
281
287
  const body = await res.json().catch(() => ({}));
282
288
  if (res.status === 401 && !path.startsWith("/auth/login")) {
283
289
  const message = token ? "the API rejected your token. Run `mybase login` again." : "not signed in. Run `mybase login` (or set MYBASE_TOKEN).";
@@ -2508,6 +2514,93 @@ async function search(sub, args) {
2508
2514
  }
2509
2515
  }
2510
2516
 
2517
+ // src/sandbox.ts
2518
+ var sandboxUsage = ` sandbox enable <project> Enable isolated agent compute (requires sandbox worker)
2519
+ sandbox disable <project> [--yes] Stop sandboxes and delete their artifacts
2520
+ sandbox create <project> --key <key> [--ttl 300] [--idle 120]
2521
+ sandbox list|get|usage <project> [<id>]
2522
+ sandbox terminate <project> <id> [--yes]
2523
+ sandbox exec <project> <id> --key <key> [--code file.js | --command '["node","script.js"]'] [--timeout 30000]
2524
+ sandbox runs|artifacts|browser <project> <id>
2525
+ sandbox run <project> <id> <execution>
2526
+ sandbox cancel <project> <id> <execution> [--yes]
2527
+ sandbox upload|download <project> <id> <workspace-path> <local-file>
2528
+ sandbox export <project> <id> <workspace-path> [--type image/png]
2529
+ sandbox artifact <project> <id> <artifact-id> <local-file>
2530
+ Creation/execution return promptly. Poll get/run. Cancelling execution terminates sandbox.
2531
+ Browser credentials expire after 30s; authenticated CDP connection lasts at most 60s.`;
2532
+ async function sandbox(sub, args) {
2533
+ const known = ["enable", "disable", "create", "list", "get", "usage", "terminate", "exec", "runs", "run", "cancel", "upload", "download", "export", "artifacts", "artifact", "browser"];
2534
+ if (!known.includes(sub))
2535
+ return false;
2536
+ const { positional, flags } = parseArgs(args);
2537
+ const [project, id, item, local] = positional;
2538
+ const p = need(project, "project name");
2539
+ const base = `/sandbox/${encodeURIComponent(p)}`;
2540
+ const root = () => `${base}/${encodeURIComponent(need(id, "sandbox id"))}`;
2541
+ const value = (key) => {
2542
+ const found = flags.get(key);
2543
+ return typeof found === "string" ? found : undefined;
2544
+ };
2545
+ const number = (key) => value(key) === undefined ? undefined : Number(value(key));
2546
+ let result;
2547
+ if (sub === "enable") {
2548
+ result = await enableService(p, "sandbox", flags);
2549
+ } else if (sub === "disable") {
2550
+ confirm(flags.has("yes"), `This stops every sandbox and permanently deletes sandbox artifacts for "${p}".`, p, "the project name");
2551
+ await api(`/projects/${encodeURIComponent(p)}/services/sandbox`, { method: "DELETE" });
2552
+ result = { disabled: "sandbox", project: p };
2553
+ } else if (sub === "create")
2554
+ result = await api(base, { method: "POST", body: JSON.stringify({ key: need(value("key"), "--key"), ttlSeconds: number("ttl"), idleSeconds: number("idle"), profile: "browser" }) });
2555
+ else if (sub === "list" || sub === "usage")
2556
+ result = await api(sub === "list" ? base : `${base}/usage`);
2557
+ else if (sub === "get")
2558
+ result = await api(root());
2559
+ else if (sub === "terminate") {
2560
+ confirm(flags.has("yes"), `This stops sandbox "${need(id, "sandbox id")}" and deletes its temporary workspace.`, id, "the sandbox id");
2561
+ result = await api(root(), { method: "DELETE" });
2562
+ } else if (sub === "exec") {
2563
+ const codePath = value("code");
2564
+ const command = value("command");
2565
+ if (!!codePath === !!command)
2566
+ usageError(`provide exactly one of --code file.js or --command '["node","script.js"]'`);
2567
+ let argv;
2568
+ if (command) {
2569
+ try {
2570
+ argv = JSON.parse(command);
2571
+ } catch {
2572
+ usageError("--command must be a JSON argument array");
2573
+ }
2574
+ }
2575
+ result = await api(`${root()}/executions`, { method: "POST", body: JSON.stringify({ key: need(value("key"), "--key"), code: codePath ? await Bun.file(codePath).text() : undefined, command: argv, timeoutMs: number("timeout") }) });
2576
+ } else if (sub === "runs")
2577
+ result = await api(`${root()}/executions`);
2578
+ else if (sub === "run" || sub === "cancel") {
2579
+ if (sub === "cancel")
2580
+ confirm(flags.has("yes"), `Cancelling stops the entire sandbox "${need(id, "sandbox id")}" and deletes its temporary workspace.`, id, "the sandbox id");
2581
+ result = await api(`${root()}/executions/${encodeURIComponent(need(item, "execution id"))}`, { method: sub === "run" ? "GET" : "DELETE" });
2582
+ } else if (sub === "upload") {
2583
+ const bytes = await Bun.file(need(local, "local file")).arrayBuffer();
2584
+ await api(`${root()}/files?path=${encodeURIComponent(need(item, "workspace path"))}`, { method: "PUT", body: bytes, headers: { "Content-Type": "application/octet-stream" } });
2585
+ result = { uploaded: item, bytes: bytes.byteLength };
2586
+ } else if (sub === "download" || sub === "artifact") {
2587
+ const path = sub === "download" ? `${root()}/files?path=${encodeURIComponent(need(item, "workspace path"))}` : `${root()}/artifacts/${encodeURIComponent(need(item, "artifact id"))}`;
2588
+ const response = await api(path, { raw: true });
2589
+ const size = await Bun.write(need(local, "local file"), response);
2590
+ result = { file: local, bytes: size };
2591
+ } else if (sub === "export")
2592
+ result = await api(`${root()}/artifacts`, { method: "POST", body: JSON.stringify({ path: need(item, "workspace path"), type: value("type") }) });
2593
+ else if (sub === "artifacts")
2594
+ result = await api(`${root()}/artifacts`);
2595
+ else if (sub === "browser") {
2596
+ const connection = await api(`${root()}/browser`, { method: "POST", body: "{}" });
2597
+ connection.headers.Authorization = `Bearer ${apiToken()}`;
2598
+ result = connection;
2599
+ }
2600
+ print(result, () => console.log(JSON.stringify(result, null, 2)));
2601
+ return true;
2602
+ }
2603
+
2511
2604
  // ../../node_modules/.bun/@clack+core@1.5.1/node_modules/@clack/core/dist/index.mjs
2512
2605
  import { styleText } from "util";
2513
2606
  import { stdout, stdin } from "process";
@@ -3899,6 +3992,8 @@ var SERVICE_COLORS = {
3899
3992
  users: color.green,
3900
3993
  captcha: color.blue,
3901
3994
  realtime: color.magenta,
3995
+ sandbox: color.yellow,
3996
+ "sandbox-worker": color.yellow,
3902
3997
  "db-public": color.cyan,
3903
3998
  dev: color.gray
3904
3999
  };
@@ -4084,6 +4179,8 @@ services:
4084
4179
  # 1 for one restart turns the admin's 2FA off (lost phone and recovery codes).
4085
4180
  MYBASE_ADMIN_RESET_MFA: \${MYBASE_ADMIN_RESET_MFA:-}
4086
4181
  LOG_LEVEL: \${LOG_LEVEL:-info}
4182
+ MYBASE_SANDBOX_WORKER_URL: \${MYBASE_SANDBOX_WORKER_URL:-}
4183
+ MYBASE_SANDBOX_WORKER_KEY: \${MYBASE_SANDBOX_WORKER_KEY:-}
4087
4184
  ports:
4088
4185
  - "\${MYBASE_BIND:-0.0.0.0}:\${MYBASE_PORT:-4000}:4000"
4089
4186
 
@@ -4161,6 +4258,27 @@ services:
4161
4258
  - source: searxng
4162
4259
  target: /etc/searxng/settings.yml
4163
4260
 
4261
+ # Opt-in trusted agent host. Requires runsc/cgroup v2 on the Docker host and a pre-pulled
4262
+ # sandbox image. See docs/sandboxes.md. No guest ever receives this socket or worker key.
4263
+ sandbox-worker:
4264
+ image: \${MYBASE_SANDBOX_WORKER_IMAGE:-ghcr.io/landonharter/mybase-sandbox-worker:latest}
4265
+ profiles: [sandbox]
4266
+ restart: \${MYBASE_RESTART:-unless-stopped}
4267
+ stop_grace_period: 45s
4268
+ environment:
4269
+ MYBASE_SANDBOX_WORKER_KEY: \${MYBASE_SANDBOX_WORKER_KEY:-}
4270
+ MYBASE_SANDBOX_DENY_HOSTS: \${MYBASE_SANDBOX_DENY_HOSTS:-}
4271
+ MYBASE_SANDBOX_IMAGE: \${MYBASE_SANDBOX_IMAGE:-ghcr.io/landonharter/mybase-sandbox:latest}
4272
+ LOG_LEVEL: \${LOG_LEVEL:-info}
4273
+ volumes:
4274
+ - /var/run/docker.sock:/var/run/docker.sock
4275
+ healthcheck:
4276
+ test: ["CMD", "bun", "-e", "fetch('http://127.0.0.1:4010/health',{headers:{Authorization:'Bearer '+process.env.MYBASE_SANDBOX_WORKER_KEY}}).then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"]
4277
+ interval: 30s
4278
+ timeout: 5s
4279
+ start_period: 30s
4280
+ # Private Compose network only. No published worker or browser-debugging ports.
4281
+
4164
4282
  # Cloudflare Tunnel: public HTTPS without opening router ports. Off unless COMPOSE_PROFILES=tunnel.
4165
4283
  # Hostnames are set up in the Cloudflare dashboard (see the top of this file).
4166
4284
  cloudflared:
@@ -5975,6 +6093,7 @@ var USAGE = {
5975
6093
  ratelimit: ratelimitUsage,
5976
6094
  queue: queueUsage,
5977
6095
  search: searchUsage,
6096
+ sandbox: sandboxUsage,
5978
6097
  secrets: secretsUsage,
5979
6098
  vector: vectorUsage,
5980
6099
  users: usersUsage,
@@ -6022,6 +6141,8 @@ async function main(args) {
6022
6141
  return audit(args.slice(1));
6023
6142
  if (command === "alerts")
6024
6143
  return alerts(args.slice(1));
6144
+ if (command === "sandbox" && sub && await sandbox(sub, rest))
6145
+ return;
6025
6146
  if (command === "project" && sub === "create") {
6026
6147
  const name = need(rest[0], "project name");
6027
6148
  const project = await api("/projects", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mybase-dev/cli",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "The mybase CLI: projects, keys, cron, queues, cache, rate limits, analytics, docs, and agent setup for a self-hosted mybase.",
5
5
  "type": "module",
6
6
  "repository": {