@whiteroom-ai/cli 2.0.1 → 2.1.0

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.
@@ -0,0 +1,139 @@
1
+ // `whiteroom sandbox run --provider <p> -- <command> [args…]`: runs the
2
+ // user's agent once with its base URL set to their Sandbox URL, so nothing
3
+ // has to be edited first. The Sandbox URL is pasted at a hidden prompt (or
4
+ // read from WHITEROOM_SANDBOX_URL), so the key never lands in shell history.
5
+ // The command runs as typed, without a shell; nothing from WhiteRoom is put
6
+ // into it. Refuses to start while another base URL in the environment points
7
+ // at WhiteRoom, so the agent's calls can't go to production by mistake.
8
+ import { spawn } from "node:child_process";
9
+ import { constants } from "node:os";
10
+ import { hiddenPrompt, Interrupted, nodeTooOld, targetOrigin } from "../practice/preflight.js";
11
+ import { say, warn } from "../practice/text.js";
12
+ import { helperEnv } from "./listen.js";
13
+ /** A Sandbox URL as the Sandbox page shows it: WhiteRoom's address, then the Sandbox key. */
14
+ const SANDBOX_URL = /^(https?:\/\/[^/\s?#@]+)\/(sk-wr-[0-9a-f]{40})(?:\/v1|\/openai)?\/?$/;
15
+ /** The providers a Sandbox connects, as the dashboard names them. */
16
+ export const RUN_PROVIDERS = ["anthropic", "openai", "azure-openai", "foundry", "bedrock-mantle"];
17
+ /** Base-URL settings agents read: one pointing at WhiteRoom other than the one this sets could send calls to production. */
18
+ const BASE_URL_VARS = ["ANTHROPIC_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE", "AZURE_OPENAI_ENDPOINT"];
19
+ /** Without a shell, Windows can't start .cmd commands such as npx: WSL runs them as on macOS and Linux. */
20
+ const WINDOWS = process.platform === "win32" ? " On Windows, run this in WSL." : "";
21
+ export const runCopy = {
22
+ needUrl: "Paste your Sandbox URL from the Sandbox page (it won't show): ",
23
+ badUrl: "That isn't a Sandbox URL. Copy it again from the Sandbox page (it ends with /sk-wr-…). Nothing was run.",
24
+ noTerminal: "No terminal to ask for the Sandbox URL. Set WHITEROOM_SANDBOX_URL to it instead. Nothing was run.",
25
+ notWhiteRoom: "That Sandbox URL isn't a WhiteRoom address. Copy it again from the Sandbox page. Nothing was run.",
26
+ pointsAtWhiteRoom: (vars) => `${vars.join(" and ")} already ${vars.length === 1 ? "points" : "point"} at WhiteRoom, so your agent's calls could go to production instead of your Sandbox. Unset ${vars.length === 1 ? "it" : "them"}, then run this again. Nothing was run.`,
27
+ gotUrl: (end) => `Got your Sandbox URL (key ending …${end}).`,
28
+ running: (name) => `Running your agent with ${name} set to your Sandbox URL. The checks start on its first call; results are on the Sandbox page.`,
29
+ cancelled: "Stopped. Nothing was run.",
30
+ notFound: (cmd) => `Couldn't start \`${cmd}\`: no such command. Check the command, then run this again.${WINDOWS}`,
31
+ couldntStart: (cmd, why) => `Couldn't start \`${cmd}\`: ${why}.${WINDOWS}`,
32
+ };
33
+ /** The setting that points an agent at the Sandbox, as the Sandbox page's .env line has it (dashboard issuedEnvHint). */
34
+ export function sandboxSetting(provider, url) {
35
+ const base = url.replace(/\/+$/, "");
36
+ if (provider === "azure-openai")
37
+ return { name: "AZURE_OPENAI_ENDPOINT", value: base };
38
+ if (provider === "openai")
39
+ return { name: "OPENAI_BASE_URL", value: `${base}/v1` };
40
+ return { name: "ANTHROPIC_BASE_URL", value: base };
41
+ }
42
+ /** Every setting the agent gets: OpenAI's also under the older OPENAI_API_BASE, which some SDKs still read, so none of them bypasses the Sandbox. */
43
+ export function sandboxSettings(provider, url) {
44
+ const s = sandboxSetting(provider, url);
45
+ return provider === "openai" ? { [s.name]: s.value, OPENAI_API_BASE: s.value } : { [s.name]: s.value };
46
+ }
47
+ /** Other base-URL settings in the environment that point at WhiteRoom (whiteroom.tech; a local proxy on localhost is the user's own). */
48
+ export function whiteRoomBaseUrls(env, except) {
49
+ return BASE_URL_VARS.filter((name) => {
50
+ if (except.includes(name) || !env[name]?.trim())
51
+ return false;
52
+ try {
53
+ const host = new URL(env[name].trim()).hostname;
54
+ return host === "whiteroom.tech" || host.endsWith(".whiteroom.tech");
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ });
60
+ }
61
+ /**
62
+ * The Sandbox URL, checked: a problem when it isn't one, or isn't on a
63
+ * WhiteRoom address. The Sandbox page's .env line works too
64
+ * (ANTHROPIC_BASE_URL=…, with or without export or quotes).
65
+ */
66
+ export function sandboxUrlOf(raw, env = {}) {
67
+ const value = raw.trim().replace(/^export\s+/, "").replace(/^[A-Z][A-Z0-9_]*=/, "").replace(/^(["'])(.*)\1$/, "$2");
68
+ const m = value.trim().match(SANDBOX_URL);
69
+ if (!m)
70
+ return { problem: runCopy.badUrl };
71
+ // WHITEROOM_PRACTICE_DEV=1 allows another host, as for listen and practice (testing WhiteRoom itself, e.g. its staging engine).
72
+ if (targetOrigin(m[1], env) === null)
73
+ return { problem: runCopy.notWhiteRoom };
74
+ return { url: `${new URL(m[1]).origin}/${m[2]}` };
75
+ }
76
+ const fail = (line) => (warn(line), 2);
77
+ const TERMINAL = { get terminal() { return !!process.stdin.isTTY; }, ask: (q) => hiddenPrompt(q) };
78
+ export async function run(a, env = process.env, asker = TERMINAL) {
79
+ const old = nodeTooOld();
80
+ if (old)
81
+ return fail(old);
82
+ let raw = env.WHITEROOM_SANDBOX_URL ?? "";
83
+ if (!raw) {
84
+ if (!asker.terminal)
85
+ return fail(runCopy.noTerminal);
86
+ try {
87
+ raw = await asker.ask(runCopy.needUrl);
88
+ }
89
+ catch (err) {
90
+ if (err instanceof Interrupted)
91
+ return warn(runCopy.cancelled), 130;
92
+ throw err;
93
+ }
94
+ }
95
+ const checked = sandboxUrlOf(raw, env);
96
+ if ("problem" in checked)
97
+ return fail(checked.problem);
98
+ // The URL was hidden as it was pasted: say which one arrived (its key's last characters only).
99
+ if (!env.WHITEROOM_SANDBOX_URL)
100
+ say(runCopy.gotUrl(checked.url.slice(-6)));
101
+ const setting = sandboxSetting(a.provider, checked.url);
102
+ const settings = sandboxSettings(a.provider, checked.url);
103
+ const clash = whiteRoomBaseUrls(env, Object.keys(settings));
104
+ if (clash.length)
105
+ return fail(runCopy.pointsAtWhiteRoom(clash));
106
+ // The agent gets the user's environment (without WHITEROOM_*), with its base URL set to the Sandbox.
107
+ const childEnv = { ...helperEnv(env), ...settings };
108
+ say(runCopy.running(setting.name));
109
+ const [cmd, ...args] = a.command;
110
+ return new Promise((resolve) => {
111
+ // Ctrl+C already reaches the agent (it's in the same process group, whether or not stdin is a
112
+ // terminal): this process only waits for it, so the agent never gets a second one. A SIGINT sent
113
+ // to this process alone isn't passed on either (it can't be told apart); SIGTERM and SIGHUP are.
114
+ const handlers = new Map();
115
+ const done = (code) => {
116
+ for (const [sig, h] of handlers)
117
+ process.off(sig, h);
118
+ resolve(code);
119
+ };
120
+ let child;
121
+ try {
122
+ child = spawn(cmd, args, { stdio: "inherit", env: childEnv });
123
+ }
124
+ catch (err) {
125
+ // Windows refuses some commands (a .cmd without a shell) before starting them.
126
+ return done(fail(runCopy.couldntStart(cmd, err.message)));
127
+ }
128
+ for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
129
+ const h = () => { if (sig !== "SIGINT")
130
+ child.kill(sig); };
131
+ handlers.set(sig, h);
132
+ process.on(sig, h);
133
+ }
134
+ child.on("error", (err) => done(fail(err.code === "ENOENT" ? runCopy.notFound(cmd) : runCopy.couldntStart(cmd, err.message))));
135
+ // The agent's own exit code; killed by a signal, 128 + its number, as a shell reports it.
136
+ child.on("exit", (code, signal) => done(code ?? 128 + (signal ? constants.signals[signal] ?? 0 : 0)));
137
+ });
138
+ }
139
+ //# sourceMappingURL=run.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/sandbox/run.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whiteroom-ai/cli",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "CLI for WhiteRoom — manage AI agent fleets from the terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,6 +34,10 @@ const anthropic = !["OPENAI_API_KEY", "AZURE_OPENAI_API_KEY"].includes(KEY_VAR);
34
34
  const DEFAULT_MODEL = anthropic ? "claude-haiku-4-5" : "gpt-4o-mini";
35
35
  const BLOCKED = "Stopped: one of my WhiteRoom controls blocked the model call.";
36
36
  const GONE = "Stopped: WhiteRoom closed this practice task.";
37
+ // WhiteRoom asks for the same call again shortly: it was refused before anything was sent on.
38
+ // This list, the pause and daily-limit messages and the UTC reset time match the CLI's own
39
+ // (src/responses.ts); test/free-tier.test.ts checks they stay the same.
40
+ const BUSY = ["sandbox_busy", "control_busy", "cleanup_busy", "db_unavailable"];
37
41
 
38
42
  // One model call through WhiteRoom; a final answer (text) if WhiteRoom stopped it.
39
43
  // Never follows a redirect, so the key only goes to WhiteRoom.
@@ -48,7 +52,13 @@ async function model(base_url, headers, body) {
48
52
  body: JSON.stringify(body), redirect: "error", signal: AbortSignal.timeout(180_000) });
49
53
  if (res.ok) return res.json();
50
54
  const wr = (await res.json().catch(() => null))?.whiteroom;
51
- const wait = res.status === 503 && wr ? ["WhiteRoom is switching servers. Waiting, then trying again.", Number(res.headers.get("retry-after") || 1)]
55
+ if (wr?.reason === "free_paused") throw new Error("Sandbox is paused for Free accounts right now. Try again later.");
56
+ if (wr?.reason === "sandbox_daily_limit") {
57
+ const at = Number.isFinite(Date.parse(wr.resetAt)) ? new Date(wr.resetAt).toISOString().slice(11, 16) : "00:00";
58
+ throw new Error(`Your Sandbox has used all its calls for today. They reset at ${at} UTC.`);
59
+ }
60
+ const wait = res.status === 503 && BUSY.includes(wr?.reason) ? ["WhiteRoom is busy right now. Waiting, then trying again.", Number(res.headers.get("retry-after") || 1)]
61
+ : res.status === 503 && wr ? ["WhiteRoom is switching servers. Waiting, then trying again.", Number(res.headers.get("retry-after") || 1)]
52
62
  : res.status === 429 && wr ? ["WhiteRoom is saving a summary of your agent's memory. Waiting until it's saved.", 2]
53
63
  : res.status === 403 && wr?.retryable ? ["One of your WhiteRoom controls is limiting calls. Waiting, then trying again.", 10]
54
64
  : [429, 500, 502, 503, 529].includes(res.status) && !wr && tries < 5 ? ["Your model provider is busy. Waiting, then trying again.", 5 * tries] : null;
@@ -5,6 +5,7 @@
5
5
  # back) and finish with one {"type":"final","output":"..."} line. Print your
6
6
  # own logs to stderr: stdout is only for these JSON lines.
7
7
  import json, os, sys, time, urllib.error, urllib.request
8
+ from datetime import datetime, timezone
8
9
 
9
10
  # The CLI sets only the variable of the provider the practice test uses.
10
11
  # It decides the API: chat completions for OpenAI and Azure OpenAI (where the
@@ -23,6 +24,10 @@ ANTHROPIC = KEY_VAR not in ("OPENAI_API_KEY", "AZURE_OPENAI_API_KEY")
23
24
  DEFAULT_MODEL = "claude-haiku-4-5" if ANTHROPIC else "gpt-4o-mini"
24
25
  BLOCKED = "Stopped: one of my WhiteRoom controls blocked the model call."
25
26
  GONE = "Stopped: WhiteRoom closed this practice task."
27
+ # WhiteRoom asks for the same call again shortly: it was refused before anything was sent on.
28
+ # This list, the pause and daily-limit messages and the UTC reset time match the CLI's own
29
+ # (src/responses.ts); test/free-tier.test.ts checks they stay the same.
30
+ BUSY = ("sandbox_busy", "control_busy", "cleanup_busy", "db_unavailable")
26
31
  calls = 0
27
32
 
28
33
 
@@ -71,7 +76,18 @@ def model(base_url, headers, body):
71
76
  wr = json.load(err).get("whiteroom")
72
77
  except Exception:
73
78
  wr = None
74
- if err.code == 503 and wr:
79
+ reason = wr.get("reason") if isinstance(wr, dict) else None
80
+ if reason == "free_paused":
81
+ raise RuntimeError("Sandbox is paused for Free accounts right now. Try again later.") from None
82
+ if reason == "sandbox_daily_limit":
83
+ try:
84
+ at = datetime.fromisoformat(str(wr.get("resetAt")).replace("Z", "+00:00")).astimezone(timezone.utc).strftime("%H:%M")
85
+ except ValueError:
86
+ at = "00:00"
87
+ raise RuntimeError(f"Your Sandbox has used all its calls for today. They reset at {at} UTC.") from None
88
+ if err.code == 503 and reason in BUSY:
89
+ wait = ("WhiteRoom is busy right now. Waiting, then trying again.", float(err.headers.get("retry-after") or 1))
90
+ elif err.code == 503 and wr:
75
91
  wait = ("WhiteRoom is switching servers. Waiting, then trying again.", float(err.headers.get("retry-after") or 1))
76
92
  elif err.code == 429 and wr:
77
93
  wait = ("WhiteRoom is saving a summary of your agent's memory. Waiting until it's saved.", 2)