@whiteroom-ai/cli 1.1.0 → 2.0.1

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 (39) hide show
  1. package/README.md +79 -0
  2. package/contract/catalog.json +264 -0
  3. package/dist/index.js +19 -15
  4. package/dist/index.js.map +1 -1
  5. package/dist/legacy-args.d.ts +4 -0
  6. package/dist/legacy-args.js +17 -0
  7. package/dist/legacy-args.js.map +1 -0
  8. package/dist/practice/adapter.d.ts +59 -0
  9. package/dist/practice/adapter.js +169 -0
  10. package/dist/practice/adapter.js.map +1 -0
  11. package/dist/practice/catalog.d.ts +27 -0
  12. package/dist/practice/catalog.js +6 -0
  13. package/dist/practice/catalog.js.map +1 -0
  14. package/dist/practice/engine.d.ts +52 -0
  15. package/dist/practice/engine.js +151 -0
  16. package/dist/practice/engine.js.map +1 -0
  17. package/dist/practice/index.d.ts +2 -0
  18. package/dist/practice/index.js +79 -0
  19. package/dist/practice/index.js.map +1 -0
  20. package/dist/practice/preflight.d.ts +64 -0
  21. package/dist/practice/preflight.js +204 -0
  22. package/dist/practice/preflight.js.map +1 -0
  23. package/dist/practice/run.d.ts +13 -0
  24. package/dist/practice/run.js +380 -0
  25. package/dist/practice/run.js.map +1 -0
  26. package/dist/practice/text.d.ts +107 -0
  27. package/dist/practice/text.js +185 -0
  28. package/dist/practice/text.js.map +1 -0
  29. package/dist/sandbox/index.d.ts +2 -0
  30. package/dist/sandbox/index.js +62 -0
  31. package/dist/sandbox/index.js.map +1 -0
  32. package/dist/sandbox/listen.d.ts +57 -0
  33. package/dist/sandbox/listen.js +299 -0
  34. package/dist/sandbox/listen.js.map +1 -0
  35. package/package.json +10 -3
  36. package/starters/wr-adapter.mjs +104 -0
  37. package/starters/wr_adapter.py +136 -0
  38. package/src/index.ts +0 -144
  39. package/tsconfig.json +0 -16
@@ -0,0 +1,169 @@
1
+ // Runs one practice task through the adapter (spec §5 Agents, §9.4): one
2
+ // child per task in its own process group, NDJSON over stdin/stdout, the
3
+ // provider key in its environment and nothing of WhiteRoom's. Stderr is
4
+ // passed through; stdout that isn't JSON is shown here and never uploaded.
5
+ import { spawn } from "node:child_process";
6
+ import { isKeyVar } from "./preflight.js";
7
+ import { clean, say } from "./text.js";
8
+ export const MAX_LINE_BYTES = 1024 * 1024;
9
+ export const MAX_TOOL_CALLS = 200;
10
+ /** A tool call's request to WhiteRoom must fit its 64 KiB body limit. */
11
+ export const MAX_TOOL_BODY_BYTES = 64 * 1024;
12
+ /** After SIGTERM, how long the group has before SIGKILL. */
13
+ export const KILL_GRACE_MS = 5_000;
14
+ /** The adapter's environment: the CLI's, minus WhiteRoom's settings and every provider key, with only the run's key added. */
15
+ export function childEnv(parent, keyVar, key) {
16
+ const env = {};
17
+ for (const [k, v] of Object.entries(parent)) {
18
+ if (!/^WHITEROOM_/i.test(k) && !isKeyVar(k))
19
+ env[k] = v;
20
+ }
21
+ env[keyVar] = key;
22
+ return env;
23
+ }
24
+ const live = new Set();
25
+ const groupAlive = (pid) => {
26
+ try {
27
+ process.kill(-pid, 0);
28
+ return true;
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ };
34
+ const signalGroup = (pid, sig) => {
35
+ try {
36
+ process.kill(-pid, sig);
37
+ }
38
+ catch {
39
+ // Already gone.
40
+ }
41
+ };
42
+ /** SIGTERM to the adapter's whole process group, then SIGKILL if anything is left after KILL_GRACE_MS. */
43
+ export async function killGroup(child) {
44
+ const pid = child.pid;
45
+ if (!pid) {
46
+ live.delete(child); // Never started: nothing to kill.
47
+ return;
48
+ }
49
+ signalGroup(pid, "SIGTERM");
50
+ const until = Date.now() + KILL_GRACE_MS;
51
+ while (groupAlive(pid) && Date.now() < until)
52
+ await new Promise((r) => setTimeout(r, 100));
53
+ if (groupAlive(pid))
54
+ signalGroup(pid, "SIGKILL");
55
+ // Only now: if the CLI exits during the grace period, the exit handler still SIGKILLs the group.
56
+ live.delete(child);
57
+ }
58
+ // Whatever happens to the CLI, no adapter outlives it.
59
+ process.on("exit", () => {
60
+ for (const c of live)
61
+ if (c.pid)
62
+ signalGroup(c.pid, "SIGKILL");
63
+ });
64
+ /** Calls `onLine` per line; a line over MAX_LINE_BYTES calls `onTooLong` instead and drops the rest of the stream. */
65
+ function readLines(stream, onLine, onTooLong) {
66
+ let buf = "";
67
+ let over = false;
68
+ stream.setEncoding("utf8");
69
+ stream.on("data", (chunk) => {
70
+ if (over)
71
+ return;
72
+ buf += chunk;
73
+ const tooLong = () => {
74
+ over = true;
75
+ buf = "";
76
+ onTooLong();
77
+ };
78
+ for (let i = buf.indexOf("\n"); i >= 0; i = buf.indexOf("\n")) {
79
+ const line = buf.slice(0, i).replace(/\r$/, "");
80
+ buf = buf.slice(i + 1);
81
+ if (Buffer.byteLength(line) > MAX_LINE_BYTES)
82
+ return tooLong();
83
+ onLine(line);
84
+ }
85
+ if (Buffer.byteLength(buf) > MAX_LINE_BYTES)
86
+ tooLong();
87
+ });
88
+ stream.on("end", () => {
89
+ if (buf && !over)
90
+ onLine(buf);
91
+ });
92
+ }
93
+ export function runAdapter(o) {
94
+ const child = "shell" in o.launch
95
+ ? spawn(o.launch.shell, { shell: true, detached: true, stdio: ["pipe", "pipe", "pipe"], env: o.env })
96
+ : spawn(o.launch.file, o.launch.args, { detached: true, stdio: ["pipe", "pipe", "pipe"], env: o.env });
97
+ live.add(child);
98
+ const tail = [];
99
+ let toolCalls = 0;
100
+ let settled = false;
101
+ // Lines are handled one at a time, in order: a tool call finishes before the next line is read.
102
+ let chain = Promise.resolve();
103
+ return new Promise((resolve) => {
104
+ const finish = (out) => {
105
+ if (settled)
106
+ return;
107
+ settled = true;
108
+ clearTimeout(timer);
109
+ o.signal.removeEventListener("abort", onAbort);
110
+ void killGroup(child).then(() => resolve(out));
111
+ };
112
+ const onAbort = () => finish({ kind: "stopped" });
113
+ // setTimeout fires at once past 2^31-1 ms, so a far deadline is clamped.
114
+ const timer = setTimeout(() => finish({ kind: "deadline" }), Math.min(2 ** 31 - 1, Math.max(0, o.deadline - Date.now())));
115
+ o.signal.addEventListener("abort", onAbort, { once: true });
116
+ if (o.signal.aborted)
117
+ onAbort();
118
+ const write = (msg) => {
119
+ if (!settled)
120
+ child.stdin.write(JSON.stringify(msg) + "\n");
121
+ };
122
+ child.stdin.on("error", () => { }); // The adapter closed stdin: its exit is reported below.
123
+ child.on("error", (err) => finish({ kind: "exited", how: err.message, tail }));
124
+ const handle = async (line) => {
125
+ if (settled || !line.trim())
126
+ return;
127
+ let msg;
128
+ try {
129
+ msg = JSON.parse(line);
130
+ }
131
+ catch {
132
+ say(` adapter: ${line}`);
133
+ return;
134
+ }
135
+ if (!msg || typeof msg !== "object" || Array.isArray(msg))
136
+ return say(` adapter: ${line}`);
137
+ if (msg.type === "final") {
138
+ return finish({ kind: "final", output: typeof msg.output === "string" ? msg.output : JSON.stringify(msg.output ?? null) });
139
+ }
140
+ if (msg.type !== "tool_call")
141
+ return finish({ kind: "broke", why: "sent a message the CLI doesn't know" });
142
+ const { id, name, input } = msg;
143
+ if (typeof id !== "string" || !id || id.length > 128 || typeof name !== "string" || !name || name.length > 64) {
144
+ return finish({ kind: "broke", why: "sent a tool call without a valid id and name" });
145
+ }
146
+ if (++toolCalls > MAX_TOOL_CALLS)
147
+ return finish({ kind: "broke", why: `made more than ${MAX_TOOL_CALLS} tool calls in one practice task` });
148
+ if (Buffer.byteLength(JSON.stringify(input ?? null)) > MAX_TOOL_BODY_BYTES - 512) {
149
+ return finish({ kind: "broke", why: "sent a tool call bigger than 64 KiB" });
150
+ }
151
+ const answer = await o.tool({ id, name, input: input ?? null });
152
+ if ("end" in answer)
153
+ return finish({ kind: "ended", by: answer.end });
154
+ write({ type: "tool_result", id, result: answer.result });
155
+ };
156
+ readLines(child.stdout, (l) => { chain = chain.then(() => handle(l)).then(() => { }); }, () => finish({ kind: "broke", why: "sent a line longer than 1 MiB" }));
157
+ readLines(child.stderr, (l) => {
158
+ process.stderr.write(clean(l) + "\n");
159
+ tail.push(clean(l).slice(0, 500));
160
+ if (tail.length > 5)
161
+ tail.shift();
162
+ }, () => { });
163
+ child.on("close", (code, sig) => {
164
+ chain = chain.then(() => finish({ kind: "exited", how: sig ? `stopped by ${sig}` : `exit code ${code}`, tail }));
165
+ });
166
+ write(o.message);
167
+ });
168
+ }
169
+ //# sourceMappingURL=adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.js","sourceRoot":"","sources":["../../src/practice/adapter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ /** A provider's name, its key's variable, the header the key travels in (`bearer`: Authorization: Bearer) and its API. */
2
+ export interface ProviderInfo {
3
+ label: string;
4
+ keyVar: string;
5
+ header: "x-api-key" | "api-key" | "bearer";
6
+ api: "messages" | "chat_completions";
7
+ }
8
+ export declare const CATALOG: {
9
+ taskNames: Record<string, string>;
10
+ disclaimer: string;
11
+ providers: Record<string, ProviderInfo>;
12
+ keyRefusals: Record<string, string>;
13
+ runCallCap: number;
14
+ statusLabels: Record<"passed" | "failed" | "not_tested" | "unsupported" | "error", string>;
15
+ verdictLabels: Record<"passed" | "not_passed" | "incomplete", string>;
16
+ /** The seven pre-flight rows: members are check-id prefixes ("a." takes every agent check under it; "ctl.spend_cap" its rules). */
17
+ rows: Array<{
18
+ id: string;
19
+ name: string;
20
+ group: string;
21
+ members: string[];
22
+ }>;
23
+ rowStatusLabels: Record<"passed" | "failed" | "not_set_up" | "not_checked", string>;
24
+ controls: {
25
+ modes: Record<"enforce" | "watch" | "off", string>;
26
+ };
27
+ };
@@ -0,0 +1,6 @@
1
+ // The engine's practice-test catalog (contract/catalog.json): the CLI ships it
2
+ // and the dashboard imports the same file, so task, status, provider and
3
+ // refusal words are the engine's. Refreshed by scripts/sync-practice-contract.sh.
4
+ import { readFileSync } from "node:fs";
5
+ export const CATALOG = JSON.parse(readFileSync(new URL("../../contract/catalog.json", import.meta.url), "utf8"));
6
+ //# sourceMappingURL=catalog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.js","sourceRoot":"","sources":["../../src/practice/catalog.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,yEAAyE;AACzE,kFAAkF;AAElF,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAKvC,MAAM,CAAC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,GAAG,CAAC,6BAA6B,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAO9G,CAAC"}
@@ -0,0 +1,52 @@
1
+ /** How long the CLI keeps retrying while WhiteRoom can't be reached or asks it to wait. */
2
+ export declare const OFFLINE_MS: number;
3
+ export declare const sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
4
+ /** Waits until one more request keeps the last minute at or under PER_MINUTE. */
5
+ export declare function pace(): Promise<void>;
6
+ /** A reply from WhiteRoom; `body` is null when it wasn't JSON. */
7
+ export interface Reply {
8
+ status: number;
9
+ /** The route's JSON, read field by field by the caller. */
10
+ body: any;
11
+ }
12
+ /**
13
+ * A reply that tried to send the CLI somewhere else. Requests that carry the
14
+ * key or the session never follow a redirect (`redirect: "error"`): nothing is
15
+ * resent, and the caller stops with plain copy.
16
+ */
17
+ export declare const REDIRECTED = 0;
18
+ export declare const isRedirectError: (err: unknown) => boolean;
19
+ /** WhiteRoom couldn't be reached (or kept asking the CLI to wait) for OFFLINE_MS. */
20
+ export declare class Unreachable extends Error {
21
+ }
22
+ /** The session's stop signal fired (Ctrl+C, or WhiteRoom ended the test). */
23
+ export declare class Stopped extends Error {
24
+ }
25
+ export declare class Engine {
26
+ readonly origin: string;
27
+ private readonly offlineMs;
28
+ private readonly timeoutMs;
29
+ session: string | null;
30
+ /** Aborts any request in flight when the session stops. */
31
+ signal: AbortSignal | null;
32
+ /** `offlineMs`: how long to keep retrying; `timeoutMs`: one request, body included (tests shorten both). */
33
+ constructor(origin: string, offlineMs?: number, timeoutMs?: number);
34
+ /** One request, its body read inside the same timeout: a server that stalls mid-body fails like one that never answers. */
35
+ private send;
36
+ /**
37
+ * Ends the run (best effort, one try, not paced): after Ctrl+C, or a crashed adapter
38
+ * (`keepResults`: with a result over the tasks done). Null if WhiteRoom didn't answer.
39
+ */
40
+ cancel(keepResults?: boolean): Promise<Reply | null>;
41
+ /** One request, best effort: not paced, not retried, not stopped by the session's signal. Null if WhiteRoom didn't answer. */
42
+ once(path: string, body: unknown, timeoutMs?: number): Promise<Reply | null>;
43
+ /**
44
+ * POSTs to a route. WhiteRoom's waits (503, a 429 that isn't an answer) are
45
+ * retried as Retry-After says. `retryNetwork`: also retry a network error or
46
+ * another 5xx, for requests that are safe to repeat; otherwise those throw
47
+ * Unreachable. A redirect is returned as status REDIRECTED, never followed.
48
+ */
49
+ post(path: string, body: unknown, o?: {
50
+ retryNetwork?: boolean;
51
+ }): Promise<Reply>;
52
+ }
@@ -0,0 +1,151 @@
1
+ // The CLI's requests to WhiteRoom's /api/readiness routes (spec §4, §7):
2
+ // plain fetch, paced to 60 a minute (§4 Run), with WhiteRoom's own waits
3
+ // (an IP-limit 429, a 503 while it switches servers) retried.
4
+ import { copy, warn } from "./text.js";
5
+ /** The CLI's own requests stay at or under this many a minute; WhiteRoom allows 120 per IP. */
6
+ const PER_MINUTE = 60;
7
+ /** How long the CLI keeps retrying while WhiteRoom can't be reached or asks it to wait. */
8
+ export const OFFLINE_MS = 5 * 60_000;
9
+ const REQUEST_TIMEOUT_MS = 90_000;
10
+ const sent = [];
11
+ export const sleep = (ms, signal) => new Promise((resolve) => {
12
+ const done = () => {
13
+ clearTimeout(t);
14
+ signal?.removeEventListener("abort", done);
15
+ resolve();
16
+ };
17
+ const t = setTimeout(done, ms);
18
+ signal?.addEventListener("abort", done, { once: true });
19
+ });
20
+ /** Waits until one more request keeps the last minute at or under PER_MINUTE. */
21
+ export async function pace() {
22
+ for (;;) {
23
+ const now = Date.now();
24
+ while (sent.length && sent[0] <= now - 60_000)
25
+ sent.shift();
26
+ if (sent.length < PER_MINUTE) {
27
+ sent.push(now);
28
+ return;
29
+ }
30
+ await sleep(sent[0] + 60_000 - now);
31
+ }
32
+ }
33
+ /**
34
+ * A reply that tried to send the CLI somewhere else. Requests that carry the
35
+ * key or the session never follow a redirect (`redirect: "error"`): nothing is
36
+ * resent, and the caller stops with plain copy.
37
+ */
38
+ export const REDIRECTED = 0;
39
+ export const isRedirectError = (err) => /redirect/i.test(String(err?.cause?.message ?? ""));
40
+ /** WhiteRoom couldn't be reached (or kept asking the CLI to wait) for OFFLINE_MS. */
41
+ export class Unreachable extends Error {
42
+ }
43
+ /** The session's stop signal fired (Ctrl+C, or WhiteRoom ended the test). */
44
+ export class Stopped extends Error {
45
+ }
46
+ /** 429s that are answers, not a request to wait. */
47
+ const ANSWERS_429 = new Set(["busy", "tool_limit", "starts_today", "owner_busy"]);
48
+ export class Engine {
49
+ origin;
50
+ offlineMs;
51
+ timeoutMs;
52
+ session = null;
53
+ /** Aborts any request in flight when the session stops. */
54
+ signal = null;
55
+ /** `offlineMs`: how long to keep retrying; `timeoutMs`: one request, body included (tests shorten both). */
56
+ constructor(origin, offlineMs = OFFLINE_MS, timeoutMs = REQUEST_TIMEOUT_MS) {
57
+ this.origin = origin;
58
+ this.offlineMs = offlineMs;
59
+ this.timeoutMs = timeoutMs;
60
+ }
61
+ /** One request, its body read inside the same timeout: a server that stalls mid-body fails like one that never answers. */
62
+ async send(path, body, timeoutMs, signal) {
63
+ const ac = new AbortController();
64
+ const abort = () => ac.abort();
65
+ const timer = setTimeout(abort, timeoutMs);
66
+ signal?.addEventListener("abort", abort, { once: true });
67
+ try {
68
+ const res = await fetch(this.origin + path, {
69
+ method: "POST",
70
+ headers: { "content-type": "application/json", ...(this.session && { authorization: `Bearer ${this.session}` }) },
71
+ body: JSON.stringify(body),
72
+ redirect: "error",
73
+ signal: ac.signal,
74
+ });
75
+ const text = await res.text();
76
+ let json = null;
77
+ try {
78
+ json = JSON.parse(text);
79
+ }
80
+ catch {
81
+ // Not JSON: the caller reads a null body.
82
+ }
83
+ return { status: res.status, retryAfter: res.headers.get("retry-after"), json };
84
+ }
85
+ finally {
86
+ clearTimeout(timer);
87
+ signal?.removeEventListener("abort", abort);
88
+ }
89
+ }
90
+ /**
91
+ * Ends the run (best effort, one try, not paced): after Ctrl+C, or a crashed adapter
92
+ * (`keepResults`: with a result over the tasks done). Null if WhiteRoom didn't answer.
93
+ */
94
+ async cancel(keepResults = false) {
95
+ return this.once("/api/readiness/cancel", keepResults ? { keepResults } : {});
96
+ }
97
+ /** One request, best effort: not paced, not retried, not stopped by the session's signal. Null if WhiteRoom didn't answer. */
98
+ async once(path, body, timeoutMs = 5_000) {
99
+ return this.send(path, body, timeoutMs, null).then((r) => ({ status: r.status, body: r.json }), () => null);
100
+ }
101
+ /**
102
+ * POSTs to a route. WhiteRoom's waits (503, a 429 that isn't an answer) are
103
+ * retried as Retry-After says. `retryNetwork`: also retry a network error or
104
+ * another 5xx, for requests that are safe to repeat; otherwise those throw
105
+ * Unreachable. A redirect is returned as status REDIRECTED, never followed.
106
+ */
107
+ async post(path, body, o = {}) {
108
+ const started = Date.now();
109
+ let warned = false;
110
+ for (;;) {
111
+ await pace();
112
+ if (this.signal?.aborted)
113
+ throw new Stopped();
114
+ let res;
115
+ try {
116
+ res = await this.send(path, body, this.timeoutMs, this.signal);
117
+ }
118
+ catch (err) {
119
+ if (this.signal?.aborted)
120
+ throw new Stopped();
121
+ if (isRedirectError(err))
122
+ return { status: REDIRECTED, body: { reason: "redirected" } };
123
+ if (!o.retryNetwork || Date.now() - started > this.offlineMs)
124
+ throw new Unreachable();
125
+ if (!warned)
126
+ warn(copy.offline);
127
+ warned = true;
128
+ await sleep(Math.min(5_000, this.offlineMs), this.signal ?? undefined);
129
+ continue;
130
+ }
131
+ const { json } = res;
132
+ if (res.status >= 500 && res.status !== 503) {
133
+ // It may have been handled: only a request that is safe to repeat is sent again.
134
+ if (!o.retryNetwork || Date.now() - started > this.offlineMs)
135
+ throw new Unreachable();
136
+ await sleep(Math.min(5_000, this.offlineMs), this.signal ?? undefined);
137
+ continue;
138
+ }
139
+ const wait = res.status === 503 || (res.status === 429 && !ANSWERS_429.has(json?.reason));
140
+ if (wait) {
141
+ if (Date.now() - started > this.offlineMs)
142
+ throw new Unreachable();
143
+ const after = res.retryAfter;
144
+ await sleep(after === null ? 5_000 : Math.min(Math.max(Number(after) || 0, 1), 60) * 1000, this.signal ?? undefined);
145
+ continue;
146
+ }
147
+ return { status: res.status, body: json };
148
+ }
149
+ }
150
+ }
151
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/practice/engine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare const PRACTICE_USAGE: string;
2
+ export declare function practice(argv: string[]): Promise<number>;
@@ -0,0 +1,79 @@
1
+ // `whiteroom practice` (hidden alias `readiness`), spec §10 CLI. Usage
2
+ // problems exit 2, never 1 (§8); so does anything unexpected.
3
+ import { readFileSync } from "node:fs";
4
+ import { parseArgs } from "node:util";
5
+ import { practiceRun, STARTERS_DIR } from "./run.js";
6
+ import { CATALOG } from "./catalog.js";
7
+ import { parseRequire, warn } from "./text.js";
8
+ const ROW_IDS = CATALOG.rows.map((r) => r.id).join(", ");
9
+ export const PRACTICE_USAGE = `whiteroom practice — run a WhiteRoom practice test on your agent
10
+
11
+ Usage:
12
+ whiteroom practice run [--adapter "<command>"] [--key-env <VAR>] [--require <checks>] [--code-stdin] [--url <https url>]
13
+ whiteroom practice adapter --lang js|python
14
+
15
+ run Starts the practice test from the run code shown in the dashboard.
16
+ Without --adapter, WhiteRoom's sample agent runs the practice tasks.
17
+ adapter Prints a starter adapter to connect your own agent, for example:
18
+ whiteroom practice adapter --lang js > wr-adapter.mjs
19
+
20
+ Options:
21
+ --adapter "<command>" The command that starts your agent's adapter, e.g. "node wr-adapter.mjs"
22
+ --key-env <VAR> The variable your agent's key is in, e.g. ANTHROPIC_API_KEY, when several are set
23
+ --require <checks> Exit 1 unless these checks pass in Enforce, e.g. spend_cap,keyword (for CI). Checks:
24
+ ${ROW_IDS}
25
+ --code-stdin Read the run code from stdin instead of asking for it
26
+ --url <https url> Only for testing WhiteRoom itself (a whiteroom.tech address or localhost)
27
+ `;
28
+ const STARTERS = { js: "wr-adapter.mjs", python: "wr_adapter.py" };
29
+ function usage(problem) {
30
+ if (!problem)
31
+ return process.stdout.write(PRACTICE_USAGE), 0;
32
+ warn(`${problem}\nRun whiteroom practice --help to see the commands.`);
33
+ return 2;
34
+ }
35
+ export async function practice(argv) {
36
+ const [sub, ...rest] = argv;
37
+ if (!sub || sub === "--help" || sub === "-h" || sub === "help")
38
+ return usage();
39
+ let values;
40
+ try {
41
+ ({ values } = parseArgs({
42
+ args: rest,
43
+ strict: true,
44
+ allowPositionals: false,
45
+ options: sub === "run"
46
+ ? { adapter: { type: "string" }, "key-env": { type: "string" }, require: { type: "string" }, "code-stdin": { type: "boolean" }, url: { type: "string" }, help: { type: "boolean", short: "h" } }
47
+ : { lang: { type: "string" }, help: { type: "boolean", short: "h" } },
48
+ }));
49
+ }
50
+ catch (err) {
51
+ return usage(err.message);
52
+ }
53
+ if (values.help)
54
+ return usage();
55
+ try {
56
+ if (sub === "run") {
57
+ if (values.adapter !== undefined && !values.adapter.trim())
58
+ return usage("--adapter needs the command that starts your adapter.");
59
+ const require = values.require === undefined ? [] : parseRequire(values.require);
60
+ if (!Array.isArray(require))
61
+ return usage(require.problem);
62
+ return await practiceRun({ adapter: values.adapter, codeStdin: !!values["code-stdin"], url: values.url, keyEnv: values["key-env"], require });
63
+ }
64
+ if (sub === "adapter") {
65
+ const file = STARTERS[values.lang];
66
+ if (!file)
67
+ return usage("Choose a starter with --lang js or --lang python.");
68
+ process.stdout.write(readFileSync(new URL(file, STARTERS_DIR), "utf8"));
69
+ return 0;
70
+ }
71
+ return usage(`Unknown practice command: ${sub}`);
72
+ }
73
+ catch (err) {
74
+ // Any adapter still running is killed as the process exits (adapter.ts).
75
+ warn(`Something went wrong in the CLI: ${err?.message ?? String(err)}`);
76
+ return 2;
77
+ }
78
+ }
79
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/practice/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ export declare const PRODUCTION = "https://proxy.whiteroom.tech";
2
+ export declare const RUN_CODE: RegExp;
3
+ /**
4
+ * The origin practice commands talk to: production, or `--url` if it is
5
+ * WhiteRoom's own (https on whiteroom.tech) or loopback, since the provider
6
+ * key goes there. Any other https host only with WHITEROOM_PRACTICE_DEV=1
7
+ * (testing WhiteRoom itself). Null if `--url` isn't allowed.
8
+ */
9
+ export declare function targetOrigin(url: string | undefined, env?: NodeJS.ProcessEnv): string | null;
10
+ /** Splits a command line into words as a POSIX shell would for plain words and quotes (enough to find files). */
11
+ export declare function words(cmd: string): string[];
12
+ /**
13
+ * Why `--adapter` can't start, or null: its program is on PATH (or a file), and
14
+ * script files it names (by extension) exist. Commands with shell syntax or a
15
+ * builtin first word are left to the shell.
16
+ */
17
+ export declare function adapterProblem(cmd: string, cwd?: string, path?: string): string | null;
18
+ /**
19
+ * The adapter as reported to WhiteRoom (§7 "What was tested"): only the
20
+ * program's name and its script file's name, e.g. "python3 wr_adapter.py".
21
+ * Never environment assignments, flags, flag values or directories.
22
+ */
23
+ export declare function adapterLabel(cmd: string): string;
24
+ /** Where a practice test's model calls go, as the claim names it: one of the catalog's providers. */
25
+ export type Provider = string;
26
+ export declare const isProvider: (v: unknown) => v is Provider;
27
+ /** The variable a provider's key is read from, and the adapter gets it in. */
28
+ export declare const keyVarOf: (p: Provider) => string;
29
+ export declare const isKeyVar: (v: string) => boolean;
30
+ /** OpenAI and Azure OpenAI take chat completions (their SDKs want a base URL ending in /v1); the others Anthropic's Messages API. */
31
+ export declare const chatCompletions: (p: Provider) => boolean;
32
+ /** The header a provider's key travels in, as the catalog says: Azure's api-key, a Bearer token, or x-api-key. */
33
+ export declare function keyHeader(p: Provider, key: string): Record<string, string>;
34
+ /** A direct key's provider, by its shape: what an older WhiteRoom that names no provider means. */
35
+ export declare const providerByShape: (key: string) => Provider;
36
+ /**
37
+ * What's wrong with a key's shape, before it's sent (uses no try), or null:
38
+ * pasted twice (a hidden prompt shows nothing, so a second paste is easy),
39
+ * or with spaces or line breaks inside.
40
+ */
41
+ export declare function keyShapeProblem(key: string): "twice" | "spaces" | null;
42
+ /** Short-term Bedrock keys can expire during a test; WhiteRoom refuses them without using a try. */
43
+ export declare const isShortTermBedrockKey: (key: string) => boolean;
44
+ export interface KeyChoice {
45
+ /** The variable the key came from; null when it was pasted. */
46
+ from: string | null;
47
+ key: string;
48
+ }
49
+ /**
50
+ * The key from `keyEnv` (`--key-env`, the variable the dashboard's command
51
+ * names), else from the one provider variable that is set; null to prompt.
52
+ * When more than one is set, the variables: before the claim the CLI can't
53
+ * tell which provider the run uses, and a guess could use a try.
54
+ */
55
+ export declare function keyFromEnv(env: NodeJS.ProcessEnv, keyEnv?: string): KeyChoice | string[] | null;
56
+ export declare class Interrupted extends Error {
57
+ }
58
+ type Terminal = Pick<NodeJS.ReadStream, "setRawMode" | "setEncoding" | "resume" | "pause" | "on" | "off">;
59
+ /** Reads a line from the terminal without echoing it. Ctrl+C rejects with Interrupted. */
60
+ export declare function hiddenPrompt(question: string, stdin?: Terminal): Promise<string>;
61
+ /** The first line of stdin (`--code-stdin`). */
62
+ export declare function firstLineOfStdin(): Promise<string>;
63
+ export declare const nodeTooOld: (version?: string) => string | null;
64
+ export {};