unfazed 0.8.1 → 0.10.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,77 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.0
4
+
5
+ eve sandboxes on unfazed boxes: an eve app on unfazed gets a real sandbox (a
6
+ browser through `@agent-browser/eve`, `bash`, files) backed by a box, with no
7
+ stored secret.
8
+
9
+ **Added**
10
+
11
+ - `unfazed/eve-sandbox`: `UnfazedSandbox`, an eve sandbox provider
12
+ (`defineSandboxProvider`, eve 0.90 or newer, an optional peer).
13
+ `UnfazedSandbox.environment({ box: "<alias>" })` in `agent/sandbox.ts`.
14
+ - Every eve session gets its own Unix user, home and workspace on the box,
15
+ kept across restarts and redeploys until the session ends.
16
+ - The app authenticates with its workload identity token (audience: the
17
+ box's URL).
18
+ - Keyed MCP tool sessions keep their sandbox between calls.
19
+ - Build-time workspace and skill files travel in the eve artifact; a
20
+ `prepare` callback that runs commands at build time is refused (install
21
+ tools on the box instead).
22
+ - `npx unfazed eve-sandbox setup|allow|deny|status|serve`, run as root inside
23
+ a box.
24
+ - `setup` installs the sandbox daemon on the box's `:3000`, agent-browser
25
+ and Chromium, and a boot hook that starts the daemon on every boot.
26
+ - Without `--allow`, every app of the box's own owner may use the box.
27
+ - Idle sessions lose their processes after 20 minutes (`--idle-minutes`);
28
+ sessions nobody used for 14 days are deleted (`--retention-days`).
29
+ - `--proxy <url>` routes the browsers through a proxy.
30
+
31
+ ## 0.9.0
32
+
33
+ Workload identity: an app gets OIDC tokens from the platform with its own
34
+ state token (`POST /oidc/token`, live on unfazed.dev since 2026-10-25). The
35
+ rest of the SDK needs nothing newer than 0.8.1 did.
36
+
37
+ **Added**
38
+
39
+ - `unfazed.oidc.token(audience, { ttlSeconds?, signal? })`: a JWT for
40
+ `audience`, issued to the app the code runs in (issuer
41
+ `https://unfazed.dev/oidc`). Needs only `UNFZ_STATE_TOKEN`, which the
42
+ platform sets in every service and cron VM; without it the call throws
43
+ `not_in_app`. The audience (1 to 256 printable ASCII characters, no
44
+ whitespace) and `ttlSeconds` (60 to 3600) are checked before any request.
45
+ Tokens are cached per (audience, ttl) until 80% of their lifetime (the
46
+ JWT's `exp - iat`), concurrent calls share one request, and 503s (and
47
+ gateway 502/504s), network failures and 429s (after their `Retry-After`,
48
+ up to 10 s) are retried up to 3 times; no other 4xx is. When a refresh
49
+ still fails that way, the previous token comes back while it is valid.
50
+ - `unfazed.oidc.tokenResponse(...)`: the same as `{ token, expiresAt }`.
51
+ - `unfazed.oidc.fetchWithToken(audience, { ttlSeconds?, fetch? })`: a `fetch`
52
+ that sends `Authorization: Bearer <token>` and everything else as given,
53
+ for a client that takes a `fetch` (the OpenAI SDK's `fetch` option).
54
+ - Without an instance: `oidc` (the process's `UnfazedOIDC`, one cache for the
55
+ process) and `oidcToken(audience)`.
56
+ - `UnfazedOIDCError` (`code`, `status`, `description`, `retryable`,
57
+ `retryAfterMs`); `code` is the platform's `error` (`invalid_token`,
58
+ `invalid_audience`, `invalid_request`, `rate_limited`, `oidc_disabled`,
59
+ `temporarily_unavailable`) or the SDK's (`not_in_app`, `unreachable`,
60
+ `invalid_response`, `http_error`).
61
+ - Class `UnfazedOIDC`, constants `STATE_TOKEN_ENV_VAR` and `OIDC_TOKEN_PATH`,
62
+ types `OIDCErrorCode`, `OIDCOptions`, `OIDCToken`, `OIDCTokenOptions`,
63
+ `OIDCTransport`, `FetchWithTokenOptions` and `TokenFetch`.
64
+
65
+ **Changed**
66
+
67
+ - `new Unfazed()` with no API token and no login no longer throws, since an
68
+ app has only its state token and `oidc` needs nothing more. The first
69
+ method that calls the API, or reading `unfazed.client`, throws the same
70
+ `UnfazedAuthError` with the same message. `new UnfazedClient()` still
71
+ throws at once.
72
+ - `unfazed.client` is a getter (read-only, as before).
73
+ - `help("token")` finds `oidc.token`: a method name matches after a `.` too.
74
+
3
75
  ## 0.8.1
4
76
 
5
77
  Follows the platform's final `waitForDeployment` and timings contract
package/README.md CHANGED
@@ -25,6 +25,9 @@ SDK: one script. Three requests. You read the four lines you cared about.
25
25
  - `npx unfazed workflow inspect runs --app <alias>`: Workflow SDK's CLI and web
26
26
  UI read your app's runs through the platform, from any directory
27
27
  ([details](#inspect-runs-from-your-terminal)).
28
+ - `oidcToken(audience)`: inside an app, a short-lived OIDC token the platform
29
+ issues to it, for services that take one instead of an API key
30
+ ([details](#workload-identity-oidc-tokens-for-your-app)).
28
31
  - Strict TypeScript, generated from the platform's own GraphQL schema.
29
32
  - Bun-first, runs unchanged on Node >= 20 and Deno.
30
33
 
@@ -57,6 +60,12 @@ const unfazed = new Unfazed(); // UNFZ_API_TOKEN, else your `npx unfaze
57
60
  const unfazed = new Unfazed({ token }); // or pass a token explicitly
58
61
  ```
59
62
 
63
+ **An app running on unfazed** needs neither for its own OIDC tokens
64
+ (`unfazed.oidc`, `oidcToken`): they use the `UNFZ_STATE_TOKEN` the platform
65
+ sets in each of its VMs ([details](#workload-identity-oidc-tokens-for-your-app)).
66
+ `new Unfazed()` without a credential is made all the same; a method that calls
67
+ the API then throws `UnfazedAuthError`, saying how to sign in.
68
+
60
69
  Both endpoint and token are configurable. `UNFZ_API_URL` overrides the endpoint
61
70
  (default `https://unfazed.dev/query`, or the stored login's); `baseUrl` is a
62
71
  convenience for self-hosted control planes where only the host differs.
@@ -203,7 +212,8 @@ Errors are typed and specific: `UnfazedAuthError`, `UnfazedHTTPError` (carries
203
212
  `status`/`body`), `UnfazedGraphQLError` (carries `errors` **and any partial
204
213
  `data`**), `UnfazedTimeoutError`, `UnfazedDeployError`, `UnfazedConflictError`,
205
214
  `UnfazedNotFoundError`, `UnfazedWriteError` (carries the write's `result` and
206
- the `failed` paths). All extend `UnfazedError`.
215
+ the `failed` paths), `UnfazedOIDCError` (carries `code`, `status`,
216
+ `description`, `retryable`). All extend `UnfazedError`.
207
217
 
208
218
  ### 2. Workflow layer — `Unfazed`
209
219
 
@@ -307,6 +317,13 @@ The chains an agent actually performs. Reach `client` from it any time:
307
317
  | `runConsole(id, { tailBytes? })` | live tail while RUNNING |
308
318
  | `runTranscript(id, { tailBytes? })` | the stored transcript once settled |
309
319
 
320
+ **Workload identity** (the app's own OIDC tokens; no API token needed)
321
+ | Method | Notes |
322
+ | --- | --- |
323
+ | `oidc.token(audience, { ttlSeconds?, signal? })` | a JWT for `audience`, cached until 80% of its lifetime |
324
+ | `oidc.tokenResponse(audience, opts?)` | the same, as `{ token, expiresAt }` |
325
+ | `oidc.fetchWithToken(audience, { ttlSeconds?, fetch? })` | a `fetch` that sends `Authorization: Bearer <token>` |
326
+
310
327
  ## The two things worth knowing
311
328
 
312
329
  ### `writeFiles` is one request and one commit
@@ -593,6 +610,134 @@ So an agent working inside a run can create workspaces, deploy, and read logs
593
610
  through this same SDK with zero configuration and no credential ever written
594
611
  into the prompt.
595
612
 
613
+ ## Workload identity: OIDC tokens for your app
614
+
615
+ Code running in an app on unfazed (a service or a cron job) can prove which
616
+ app it is. The platform issues it short-lived OIDC tokens (JWTs) for any
617
+ audience, and a service that trusts the issuer `https://unfazed.dev/oidc`
618
+ takes one instead of a long-lived API key kept in the app's env.
619
+
620
+ ```ts
621
+ import { oidcToken } from "unfazed";
622
+
623
+ const jwt = await oidcToken("https://api.example.com"); // cached and refreshed for you
624
+ const res = await fetch("https://api.example.com/v1/things", {
625
+ headers: { Authorization: `Bearer ${jwt}` },
626
+ });
627
+ ```
628
+
629
+ A client that takes a `fetch` (here the OpenAI SDK, against an
630
+ OpenAI-compatible endpoint) gets one that puts a fresh token on every request:
631
+
632
+ ```ts
633
+ import OpenAI from "openai";
634
+ import { oidc } from "unfazed";
635
+
636
+ const openai = new OpenAI({
637
+ baseURL: process.env.PRIVATEINFERENCE_BASE_URL,
638
+ apiKey: "unused", // the token replaces it
639
+ fetch: oidc.fetchWithToken(process.env.PRIVATEINFERENCE_AUDIENCE),
640
+ });
641
+ ```
642
+
643
+ `oidc` is the process's own; an instance has the same methods as
644
+ `unfazed.oidc` (`token`, `tokenResponse` for `{ token, expiresAt }`,
645
+ `fetchWithToken`), asking the platform its client talks to (`baseUrl`,
646
+ `endpoint`, `UNFZ_API_URL`; default `https://unfazed.dev`).
647
+
648
+ - **Credential:** only the app's own `UNFZ_STATE_TOKEN`, which the platform
649
+ sets in each of its VMs. No `UNFZ_API_TOKEN` and no login: `new Unfazed()`
650
+ in an app works for `oidc` alone. Outside an app the call throws
651
+ `UnfazedOIDCError` with `code: "not_in_app"` ("not running inside an unfazed
652
+ app"), before any request.
653
+ - **The token:** issuer `https://unfazed.dev/oidc`; claims include `sub`,
654
+ `aud`, `owner_id`, `project_id`, `app_id`, `alias`, `environment` and
655
+ `deployment_id`. `ttlSeconds` (60 to 3600) asks for a lifetime; without it
656
+ the platform picks. An audience is 1 to 256 printable ASCII characters
657
+ without whitespace, checked before any request (`fetchWithToken` checks it
658
+ when called, so an unset variable fails at startup).
659
+ - **Cache:** per (audience, ttl), reused until 80% of the token's lifetime has
660
+ passed. The lifetime is the JWT's own (`exp - iat`), so a VM clock that is
661
+ off does not shorten or stretch it. Concurrent calls for one pair make one
662
+ request; the cache is the process's (an instance with its own `fetch` keeps
663
+ its own). `{ signal }` stops a call's wait, not the request others share.
664
+ - **Retries:** a 503 (or a gateway's 502/504) and network failures are retried
665
+ up to 3 times (200, 400, 800 ms); a 429 too, after its `Retry-After` (up to
666
+ 10 s; a longer one throws at once, with `retryAfterMs`). No other 4xx is
667
+ retried. If a refresh still fails that way while the token it replaces is
668
+ valid, that token comes back.
669
+
670
+ Errors are `UnfazedOIDCError`, with the platform's `error` as `code` and its
671
+ `error_description` as `description`, plus `status` and `retryable`:
672
+
673
+ | `code` | Status | Means |
674
+ | --- | --- | --- |
675
+ | `invalid_token` | 401 | the platform refused `UNFZ_STATE_TOKEN` |
676
+ | `invalid_audience` | 400 | the audience is malformed or not allowed (also refused locally) |
677
+ | `invalid_request` | 400 | a malformed request, `ttlSeconds` out of range among them |
678
+ | `rate_limited` | 429 | too many requests, after the retries |
679
+ | `oidc_disabled` | 404 | the platform issues no OIDC tokens to this app |
680
+ | `temporarily_unavailable` | 503 | after the retries |
681
+ | `not_in_app` | | `UNFZ_STATE_TOKEN` is not set |
682
+ | `unreachable` | | network failures outlasted the retries |
683
+
684
+ ## eve sandboxes on a box: `unfazed/eve-sandbox`
685
+
686
+ [eve](https://eve.dev) runs browsers, `bash` and file tools only inside a
687
+ sandbox, and an unfazed service has no Docker. `unfazed/eve-sandbox` backs
688
+ eve sandboxes with an unfazed **box** (a persistent Linux VM): every eve
689
+ session gets its own Unix user, home and workspace there, which survive box
690
+ restarts and app redeploys until the session ends.
691
+
692
+ **1. Turn a box into a sandbox host.** Inside the box, as root (`ssh <box>@unfz.to`):
693
+
694
+ ```bash
695
+ npx -y unfazed@latest eve-sandbox setup
696
+ ```
697
+
698
+ This installs:
699
+
700
+ - the sandbox daemon on the box's `:3000`, which serves the box's URL;
701
+ - agent-browser and Chromium for every session;
702
+ - a boot hook that starts the daemon on every boot.
703
+
704
+ It then prints the line for your app. Who may use the box:
705
+
706
+ - Without `--allow`, every app of the box's own owner.
707
+ - `--allow <app-id>,owner:<id>` names apps explicitly, and `npx unfazed eve-sandbox allow|deny <id>` changes the list later.
708
+ - A refused app's id is in the daemon's 403 answer.
709
+
710
+ Other options:
711
+
712
+ - `--proxy <url>` routes the browsers through a proxy, for sites that block datacenter IPs.
713
+ - `--idle-minutes 20` sets how long an idle session keeps its processes.
714
+ - `--retention-days 14` sets when an unused session is deleted.
715
+ - `npx unfazed eve-sandbox status` shows the daemon, the allow list and the sessions.
716
+
717
+ **2. Point the eve app at it** (`eve` 0.90 or newer, an optional peer):
718
+
719
+ ```ts
720
+ // agent/sandbox.ts
721
+ import { defineSandbox } from "eve/sandbox";
722
+ import { UnfazedSandbox } from "unfazed/eve-sandbox";
723
+
724
+ export const environment = UnfazedSandbox.environment({ box: "my-sandbox-box" });
725
+ export default defineSandbox(() => environment.open());
726
+ ```
727
+
728
+ With `@agent-browser/eve` mounted (`agent/extensions/browser.ts`), the
729
+ agent's `browser__*` tools run in the box. The agent-browser that `setup`
730
+ installs is `0.41.1`. Pass `--agent-browser <version>` to match another
731
+ `@agent-browser/eve` minor.
732
+
733
+ How it behaves:
734
+
735
+ - **No stored secret.** The app authenticates with its workload identity token, audience the box's URL (see "Workload identity" above). The box checks the issuer, audience and allow list.
736
+ - **Sessions are users.** A session's commands (`bash -lc`, streamed) and file operations run as its own Unix user, so sessions cannot read each other's files or browser profiles.
737
+ - **Keyed MCP tool sessions** (`dev.eve/tool-sessions`) keep their sandbox between calls. A sandbox nobody used for the retention period is deleted.
738
+ - **Templates.** The agent's workspace and skill files are captured at `eve build` and seeded into each new session. A sandbox `prepare` callback that runs commands is refused at build time: install tools on the box instead (it is yours, with root).
739
+ - **Placement.** Put the box near the app (the same server or region): every sandbox call is an HTTPS round trip to the box.
740
+
596
741
  ## Durable workflows: `unfazed/workflow`
597
742
 
598
743
  [Workflow SDK](https://workflow-sdk.dev) runs durable functions: a
@@ -956,7 +1101,7 @@ After bumping the `@workflow/world-postgres` devDependency, run
956
1101
  `REFERENCE.md` is what `help()` returns: after editing it run
957
1102
  `bun run embed-reference` (`bun run check` fails while
958
1103
  `src/reference.generated.ts` is stale). `test/reference.test.ts` fails when a
959
- public method of `Unfazed` is missing from it.
1104
+ public method of `Unfazed` (or of `unfazed.oidc`) is missing from it.
960
1105
 
961
1106
  ### Regenerating from the schema
962
1107
 
package/REFERENCE.md CHANGED
@@ -3,8 +3,9 @@
3
3
  Every method of `Unfazed`, with its signature, in one page. In a script:
4
4
  `import { Unfazed, help } from "unfazed"; const unfazed = new Unfazed();`
5
5
  (run_script injects the credential; elsewhere `UNFZ_API_TOKEN` or
6
- `npx unfazed login`). `help()` returns this page; `help("writeFiles")` one
7
- entry. Methods are async and throw an `UnfazedError` subclass on failure.
6
+ `npx unfazed login`; `oidc` needs neither). `help()` returns this page;
7
+ `help("writeFiles")` one entry. Methods are async and throw an
8
+ `UnfazedError` subclass on failure.
8
9
  Every method takes an optional last `options` with `signal` (and, for single
9
10
  requests, `timeoutMs`). The names follow the MCP tools: `listCommits` is
10
11
  `list_commits`, `waitForEvents` is `wait_for_events`.
@@ -291,6 +292,50 @@ Answers a BLOCKED run's question.
291
292
 
292
293
  ### `listRunHarnesses(): Promise<RunHarness[]>`
293
294
 
295
+ ## Workload identity
296
+
297
+ OIDC tokens the platform issues to the app the code runs in (a service or a
298
+ cron job): short-lived JWTs for one audience, from the issuer
299
+ `https://unfazed.dev/oidc`, which a service that trusts it takes instead of
300
+ an API key. Claims include `sub`, `aud`, `owner_id`, `project_id`, `app_id`,
301
+ `alias`, `environment` and `deployment_id`. The only credential is the app's
302
+ `UNFZ_STATE_TOKEN`, set in each of its VMs: no API token, so a bare
303
+ `new Unfazed()` works in an app for this. Without an instance:
304
+ `import { oidc, oidcToken } from "unfazed"` (`oidcToken(audience)` is
305
+ `oidc.token(audience)`).
306
+
307
+ ### `oidc.token(audience: string, options?: { ttlSeconds?, signal? }): Promise<string>`
308
+ A JWT for `audience` (1 to 256 printable ASCII characters, no whitespace);
309
+ `ttlSeconds` from 60 to 3600, else the platform's default. Cached per
310
+ (audience, ttl) until 80% of its lifetime; concurrent calls share one
311
+ request. 503s, network failures and 429s (after their `Retry-After`) are
312
+ retried up to 3 times; if a refresh still fails that way, the previous token
313
+ comes back while it is valid. Throws `UnfazedOIDCError` (`code`, `status`,
314
+ `description`, `retryable`, `retryAfterMs`): `not_in_app` (no
315
+ `UNFZ_STATE_TOKEN`), `invalid_audience`, `invalid_request`, `invalid_token`,
316
+ `rate_limited`, `oidc_disabled`, `temporarily_unavailable`, `unreachable`.
317
+
318
+ ```ts
319
+ const jwt = await unfazed.oidc.token("https://api.example.com", { ttlSeconds: 300 });
320
+ await fetch("https://api.example.com/v1/things", { headers: { Authorization: `Bearer ${jwt}` } });
321
+ ```
322
+
323
+ ### `oidc.tokenResponse(audience: string, options?: { ttlSeconds?, signal? }): Promise<{ token, expiresAt: Date }>`
324
+ `oidc.token` with the token's expiry.
325
+
326
+ ### `oidc.fetchWithToken(audience: string, options?: { ttlSeconds?, fetch? }): fetch`
327
+ A `fetch` that sends each request with `Authorization: Bearer <token>`
328
+ (replacing one already there), everything else as given: for a client that
329
+ takes a `fetch`. An invalid audience (an unset variable) throws at once.
330
+
331
+ ```ts
332
+ const openai = new OpenAI({
333
+ baseURL: process.env.PRIVATEINFERENCE_BASE_URL,
334
+ apiKey: "unused",
335
+ fetch: unfazed.oidc.fetchWithToken(process.env.PRIVATEINFERENCE_AUDIENCE),
336
+ });
337
+ ```
338
+
294
339
  ## Anything else
295
340
 
296
341
  `unfazed.client` is the typed GraphQL transport over the same credential:
@@ -0,0 +1,3 @@
1
+ /** The agent-browser release `@agent-browser/eve` 0.41 drives; match the extension's minor. */
2
+ export declare const DEFAULT_AGENT_BROWSER = "0.41.1";
3
+ export declare function runEveSandbox(argv: readonly string[]): Promise<number>;
@@ -0,0 +1,268 @@
1
+ /// <reference types="node" />
2
+ /**
3
+ * `npx unfazed eve-sandbox <command>`, run as root inside an unfazed box:
4
+ * turns the box into a sandbox host for eve apps (`unfazed/eve-sandbox`).
5
+ *
6
+ * setup [--allow <app-id|owner:id>,…] [--no-browser] [--agent-browser <version>]
7
+ * [--proxy <url>] [--idle-minutes <n>] [--retention-days <n>]
8
+ * allow <app-id|owner:id> add to the allow list and restart the daemon
9
+ * deny <app-id|owner:id> remove from it and restart the daemon
10
+ * status the daemon's health, allow list and sessions
11
+ * serve run the daemon in the foreground
12
+ *
13
+ * `setup` copies the daemon to /data/eve-sandbox, writes its config and a box
14
+ * boot hook (/data/boot.d/50-eve-sandbox, run on every boot by the box's
15
+ * init), installs agent-browser and Chromium for every session, and (re)starts
16
+ * the daemon on :3000, which unfazed publishes at the box's URL. Without
17
+ * --allow, every app of the box's own owner may use it.
18
+ */
19
+ import { execFileSync, spawn } from "node:child_process";
20
+ import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, symlinkSync, writeFileSync, } from "node:fs";
21
+ import { dirname, join } from "node:path";
22
+ import { fileURLToPath } from "node:url";
23
+ import { parseArgs } from "node:util";
24
+ /** The agent-browser release `@agent-browser/eve` 0.41 drives; match the extension's minor. */
25
+ export const DEFAULT_AGENT_BROWSER = "0.41.1";
26
+ const ROOT = "/data/eve-sandbox";
27
+ const LIB = `${ROOT}/lib`;
28
+ const CONFIG = `${ROOT}/config.json`;
29
+ const HOOK = "/data/boot.d/50-eve-sandbox";
30
+ const BROWSERS = "/opt/agent-browser/browsers";
31
+ const HELP = `unfazed eve-sandbox — run eve sandboxes on this unfazed box (as root, inside the box)
32
+
33
+ Usage:
34
+ npx unfazed eve-sandbox setup [--allow <app-id|owner:id>,…] [--no-browser]
35
+ [--agent-browser <version>] [--proxy <url>]
36
+ [--idle-minutes <n>] [--retention-days <n>]
37
+ npx unfazed eve-sandbox allow <app-id|owner:id>
38
+ npx unfazed eve-sandbox deny <app-id|owner:id>
39
+ npx unfazed eve-sandbox status
40
+ npx unfazed eve-sandbox serve
41
+
42
+ setup installs the daemon (on :3000, the box's URL), agent-browser ${DEFAULT_AGENT_BROWSER}
43
+ and Chromium, and a boot hook that starts the daemon on every boot. Without
44
+ --allow, every app of this box's owner may use the box. An eve app then
45
+ points its agent/sandbox.ts at it with UnfazedSandbox (unfazed/eve-sandbox):
46
+
47
+ export const environment = UnfazedSandbox.environment({ box: "<this box's alias>" });
48
+ export default defineSandbox(() => environment.open());
49
+ `;
50
+ function readConfig() {
51
+ if (!existsSync(CONFIG))
52
+ return { allow: [] };
53
+ const parsed = JSON.parse(readFileSync(CONFIG, "utf8"));
54
+ return { ...parsed, allow: Array.isArray(parsed.allow) ? parsed.allow : [] };
55
+ }
56
+ function writeConfig(config) {
57
+ const temp = `${CONFIG}.tmp`;
58
+ writeFileSync(temp, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
59
+ renameSync(temp, CONFIG);
60
+ }
61
+ function sh(file, args, options = {}) {
62
+ return execFileSync(file, args, { encoding: "utf8", stdio: options.quiet ? ["ignore", "pipe", "pipe"] : ["ignore", "pipe", "inherit"] });
63
+ }
64
+ function requireBox() {
65
+ if (process.getuid?.() !== 0)
66
+ throw new Error("unfazed eve-sandbox: run this as root inside the box");
67
+ if (!existsSync("/sbin/unfazed-box-init") || !process.env["UNFZ_PUBLIC_URL"]) {
68
+ throw new Error("unfazed eve-sandbox: this is not an unfazed box (no /sbin/unfazed-box-init or UNFZ_PUBLIC_URL)");
69
+ }
70
+ }
71
+ /** The box's own owner, from its workload identity token: the default allow entry. */
72
+ async function ownOwner() {
73
+ const state = process.env["UNFZ_STATE_TOKEN"];
74
+ const audience = process.env["UNFZ_PUBLIC_URL"];
75
+ if (!state)
76
+ throw new Error("unfazed eve-sandbox: UNFZ_STATE_TOKEN is not set; pass --allow");
77
+ const res = await fetch("https://unfazed.dev/oidc/token", {
78
+ method: "POST",
79
+ headers: { authorization: `Bearer ${state}`, "content-type": "application/json", "user-agent": "unfazed-eve-sandbox-setup" },
80
+ body: JSON.stringify({ audience }),
81
+ });
82
+ if (!res.ok)
83
+ throw new Error(`unfazed eve-sandbox: could not read the box's owner (${res.status}); pass --allow`);
84
+ const { token } = (await res.json());
85
+ const claims = JSON.parse(Buffer.from(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8"));
86
+ if (!claims.owner_id)
87
+ throw new Error("unfazed eve-sandbox: the box's token names no owner; pass --allow");
88
+ return `owner:${claims.owner_id}`;
89
+ }
90
+ function installDaemon() {
91
+ mkdirSync(LIB, { recursive: true });
92
+ chmodSync(ROOT, 0o755);
93
+ chmodSync(LIB, 0o755);
94
+ const source = join(dirname(fileURLToPath(import.meta.url)), "..", "eve-sandbox");
95
+ for (const file of ["daemon.js", "protocol.js"]) {
96
+ copyFileSync(join(source, file), join(LIB, file));
97
+ chmodSync(join(LIB, file), 0o644);
98
+ }
99
+ mkdirSync(dirname(HOOK), { recursive: true });
100
+ writeFileSync(HOOK, [
101
+ "#!/bin/sh",
102
+ "# eve sandbox daemon (unfazed/eve-sandbox) on :3000, restarted if it exits.",
103
+ `cd ${ROOT} || exit 1`,
104
+ "while true; do",
105
+ ` ${existsSync("/usr/local/bin/node") ? "/usr/local/bin/node" : process.execPath} ${LIB}/daemon.js serve --config ${CONFIG} >>${ROOT}/daemon.log 2>&1`,
106
+ " sleep 2",
107
+ "done",
108
+ "",
109
+ ].join("\n"), { mode: 0o755 });
110
+ }
111
+ /** Installs agent-browser and Chromium once for every session; returns the Chromium binary. */
112
+ function installBrowser(version) {
113
+ process.stderr.write(`installing agent-browser@${version} and Chromium (a few minutes the first time)…\n`);
114
+ const env = { ...process.env, DEBIAN_FRONTEND: "noninteractive" };
115
+ execFileSync("npm", ["install", "-g", "--silent", `agent-browser@${version}`], { stdio: "inherit", env });
116
+ // agent-browser keeps browsers in the installing user's home; sessions are
117
+ // other users. Root's browser directory becomes the shared one, so a
118
+ // browser already there is found and not downloaded again.
119
+ mkdirSync(BROWSERS, { recursive: true });
120
+ const own = join(process.env["HOME"] ?? "/root", ".agent-browser", "browsers");
121
+ mkdirSync(dirname(own), { recursive: true });
122
+ if (existsSync(own) && !lstatSync(own).isSymbolicLink()) {
123
+ for (const entry of readdirSync(own)) {
124
+ if (!existsSync(join(BROWSERS, entry)))
125
+ renameSync(join(own, entry), join(BROWSERS, entry));
126
+ }
127
+ rmSync(own, { recursive: true, force: true });
128
+ }
129
+ if (!existsSync(own))
130
+ symlinkSync(BROWSERS, own);
131
+ execFileSync("agent-browser", ["install", "--with-deps"], { stdio: "inherit", env });
132
+ sh("chmod", ["-R", "a+rX", "/opt/agent-browser"]);
133
+ const latest = readdirSync(BROWSERS)
134
+ .filter((e) => e.startsWith("chrome-"))
135
+ .sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
136
+ .at(-1);
137
+ const binary = latest ? join(BROWSERS, latest, "chrome") : "";
138
+ if (!binary || !existsSync(binary))
139
+ throw new Error("unfazed eve-sandbox: Chromium was not found after the install");
140
+ return binary;
141
+ }
142
+ function restartDaemon() {
143
+ // The boot hook's loop first (or it starts the old daemon again), then any
144
+ // daemon, wherever an earlier setup put it.
145
+ for (const pattern of ["[b]oot\\.d/50-eve-sandbox", "[e]ve-sandbox[^ ]*daemon[^ ]*\\.js serve"]) {
146
+ try {
147
+ sh("pkill", ["-f", pattern], { quiet: true });
148
+ }
149
+ catch {
150
+ // none running
151
+ }
152
+ }
153
+ execFileSync("sleep", ["1"]);
154
+ const child = spawn("setsid", [HOOK], { detached: true, stdio: "ignore" });
155
+ child.unref();
156
+ }
157
+ async function health() {
158
+ for (let i = 0; i < 20; i++) {
159
+ try {
160
+ const res = await fetch("http://127.0.0.1:3000/v1/health", { signal: AbortSignal.timeout(2000) });
161
+ if (res.ok)
162
+ return (await res.json());
163
+ }
164
+ catch {
165
+ // not up yet
166
+ }
167
+ await new Promise((resolve) => setTimeout(resolve, 500));
168
+ }
169
+ return { ok: false };
170
+ }
171
+ function sessionCount() {
172
+ return sh("getent", ["passwd"], { quiet: true })
173
+ .split("\n")
174
+ .filter((line) => /^eve[0-9a-f]{12}:/.test(line)).length;
175
+ }
176
+ export async function runEveSandbox(argv) {
177
+ const { values, positionals } = parseArgs({
178
+ args: [...argv],
179
+ allowPositionals: true,
180
+ options: {
181
+ allow: { type: "string" },
182
+ "no-browser": { type: "boolean" },
183
+ "agent-browser": { type: "string" },
184
+ proxy: { type: "string" },
185
+ "idle-minutes": { type: "string" },
186
+ "retention-days": { type: "string" },
187
+ config: { type: "string" },
188
+ help: { type: "boolean", short: "h" },
189
+ },
190
+ });
191
+ const [command, arg] = positionals;
192
+ if (values.help || command === undefined || command === "help") {
193
+ process.stdout.write(HELP);
194
+ return command === undefined ? 1 : 0;
195
+ }
196
+ if (command === "serve") {
197
+ const daemon = await import("../eve-sandbox/daemon.js");
198
+ const config = daemon.loadConfig(values.config ?? CONFIG);
199
+ await daemon.createDaemon(config).listen();
200
+ process.stdout.write(`eve sandbox daemon on :${config.port} for ${config.audience}\n`);
201
+ await new Promise(() => { });
202
+ }
203
+ requireBox();
204
+ const url = process.env["UNFZ_PUBLIC_URL"];
205
+ switch (command) {
206
+ case "setup": {
207
+ const config = readConfig();
208
+ const allow = values.allow
209
+ ? values.allow.split(",").map((s) => s.trim()).filter(Boolean)
210
+ : config.allow.length > 0
211
+ ? []
212
+ : [await ownOwner()];
213
+ config.allow = [...new Set([...config.allow, ...allow])];
214
+ const number = (flag, name) => {
215
+ if (flag === undefined)
216
+ return undefined;
217
+ const n = Number(flag);
218
+ if (!Number.isInteger(n) || n < 0)
219
+ throw new Error(`unfazed eve-sandbox: --${name} must be a whole number`);
220
+ return n;
221
+ };
222
+ config.idleMinutes = number(values["idle-minutes"], "idle-minutes") ?? config.idleMinutes ?? 20;
223
+ config.retentionDays = number(values["retention-days"], "retention-days") ?? config.retentionDays ?? 14;
224
+ config.env = { ...(config.env ?? {}) };
225
+ if (values.proxy)
226
+ config.env["AGENT_BROWSER_PROXY"] = values.proxy;
227
+ installDaemon();
228
+ if (!values["no-browser"])
229
+ config.env["AGENT_BROWSER_EXECUTABLE_PATH"] = installBrowser(values["agent-browser"] ?? DEFAULT_AGENT_BROWSER);
230
+ writeConfig(config);
231
+ restartDaemon();
232
+ const up = await health();
233
+ if (!up.ok)
234
+ throw new Error(`unfazed eve-sandbox: the daemon did not come up; see ${ROOT}/daemon.log`);
235
+ const alias = new URL(url).hostname.split(".")[0];
236
+ process.stdout.write(`eve sandbox ready at ${url} (daemon ${up.version}); allowed: ${config.allow.join(", ")}\n\n` +
237
+ `In the eve app:\n export const environment = UnfazedSandbox.environment({ box: "${alias}" });\n`);
238
+ return 0;
239
+ }
240
+ case "allow":
241
+ case "deny": {
242
+ if (!arg)
243
+ throw new Error(`unfazed eve-sandbox: ${command} needs an app id or owner:<id>`);
244
+ const config = readConfig();
245
+ config.allow = command === "allow" ? [...new Set([...config.allow, arg])] : config.allow.filter((entry) => entry !== arg);
246
+ if (config.allow.length === 0)
247
+ throw new Error("unfazed eve-sandbox: that would allow nothing; allow another app first");
248
+ writeConfig(config);
249
+ restartDaemon();
250
+ const up = await health();
251
+ process.stdout.write(`${up.ok ? "restarted" : "NOT restarted (see daemon.log)"}; allowed: ${config.allow.join(", ")}\n`);
252
+ return up.ok ? 0 : 1;
253
+ }
254
+ case "status": {
255
+ const config = readConfig();
256
+ const up = await health();
257
+ process.stdout.write(`${url}: ${up.ok ? `up (daemon ${up.version})` : "DOWN"}\n` +
258
+ `allowed: ${config.allow.join(", ") || "(none)"}\n` +
259
+ `sessions: ${sessionCount()}\n` +
260
+ `browser: ${config.env?.["AGENT_BROWSER_EXECUTABLE_PATH"] ?? "(not installed)"}\n`);
261
+ return up.ok ? 0 : 1;
262
+ }
263
+ default:
264
+ process.stderr.write(HELP);
265
+ return 1;
266
+ }
267
+ }
268
+ //# sourceMappingURL=eve-sandbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eve-sandbox.js","sourceRoot":"","sources":["../../src/cli/eve-sandbox.ts"],"names":[],"mappings":"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","sourcesContent":["/// <reference types=\"node\" />\n/**\n * `npx unfazed eve-sandbox <command>`, run as root inside an unfazed box:\n * turns the box into a sandbox host for eve apps (`unfazed/eve-sandbox`).\n *\n * setup [--allow <app-id|owner:id>,…] [--no-browser] [--agent-browser <version>]\n * [--proxy <url>] [--idle-minutes <n>] [--retention-days <n>]\n * allow <app-id|owner:id> add to the allow list and restart the daemon\n * deny <app-id|owner:id> remove from it and restart the daemon\n * status the daemon's health, allow list and sessions\n * serve run the daemon in the foreground\n *\n * `setup` copies the daemon to /data/eve-sandbox, writes its config and a box\n * boot hook (/data/boot.d/50-eve-sandbox, run on every boot by the box's\n * init), installs agent-browser and Chromium for every session, and (re)starts\n * the daemon on :3000, which unfazed publishes at the box's URL. Without\n * --allow, every app of the box's own owner may use it.\n */\nimport { execFileSync, spawn } from \"node:child_process\";\nimport {\n chmodSync,\n copyFileSync,\n existsSync,\n lstatSync,\n mkdirSync,\n readdirSync,\n readFileSync,\n renameSync,\n rmSync,\n symlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { parseArgs } from \"node:util\";\n\n/** The agent-browser release `@agent-browser/eve` 0.41 drives; match the extension's minor. */\nexport const DEFAULT_AGENT_BROWSER = \"0.41.1\";\n\nconst ROOT = \"/data/eve-sandbox\";\nconst LIB = `${ROOT}/lib`;\nconst CONFIG = `${ROOT}/config.json`;\nconst HOOK = \"/data/boot.d/50-eve-sandbox\";\nconst BROWSERS = \"/opt/agent-browser/browsers\";\n\nconst HELP = `unfazed eve-sandbox — run eve sandboxes on this unfazed box (as root, inside the box)\n\nUsage:\n npx unfazed eve-sandbox setup [--allow <app-id|owner:id>,…] [--no-browser]\n [--agent-browser <version>] [--proxy <url>]\n [--idle-minutes <n>] [--retention-days <n>]\n npx unfazed eve-sandbox allow <app-id|owner:id>\n npx unfazed eve-sandbox deny <app-id|owner:id>\n npx unfazed eve-sandbox status\n npx unfazed eve-sandbox serve\n\nsetup installs the daemon (on :3000, the box's URL), agent-browser ${DEFAULT_AGENT_BROWSER}\nand Chromium, and a boot hook that starts the daemon on every boot. Without\n--allow, every app of this box's owner may use the box. An eve app then\npoints its agent/sandbox.ts at it with UnfazedSandbox (unfazed/eve-sandbox):\n\n export const environment = UnfazedSandbox.environment({ box: \"<this box's alias>\" });\n export default defineSandbox(() => environment.open());\n`;\n\ninterface Config {\n allow: string[];\n idleMinutes?: number;\n retentionDays?: number;\n env?: Record<string, string>;\n [key: string]: unknown;\n}\n\nfunction readConfig(): Config {\n if (!existsSync(CONFIG)) return { allow: [] };\n const parsed = JSON.parse(readFileSync(CONFIG, \"utf8\")) as Config;\n return { ...parsed, allow: Array.isArray(parsed.allow) ? parsed.allow : [] };\n}\n\nfunction writeConfig(config: Config): void {\n const temp = `${CONFIG}.tmp`;\n writeFileSync(temp, JSON.stringify(config, null, 2) + \"\\n\", { mode: 0o600 });\n renameSync(temp, CONFIG);\n}\n\nfunction sh(file: string, args: readonly string[], options: { quiet?: boolean } = {}): string {\n return execFileSync(file, args, { encoding: \"utf8\", stdio: options.quiet ? [\"ignore\", \"pipe\", \"pipe\"] : [\"ignore\", \"pipe\", \"inherit\"] });\n}\n\nfunction requireBox(): void {\n if (process.getuid?.() !== 0) throw new Error(\"unfazed eve-sandbox: run this as root inside the box\");\n if (!existsSync(\"/sbin/unfazed-box-init\") || !process.env[\"UNFZ_PUBLIC_URL\"]) {\n throw new Error(\"unfazed eve-sandbox: this is not an unfazed box (no /sbin/unfazed-box-init or UNFZ_PUBLIC_URL)\");\n }\n}\n\n/** The box's own owner, from its workload identity token: the default allow entry. */\nasync function ownOwner(): Promise<string> {\n const state = process.env[\"UNFZ_STATE_TOKEN\"];\n const audience = process.env[\"UNFZ_PUBLIC_URL\"]!;\n if (!state) throw new Error(\"unfazed eve-sandbox: UNFZ_STATE_TOKEN is not set; pass --allow\");\n const res = await fetch(\"https://unfazed.dev/oidc/token\", {\n method: \"POST\",\n headers: { authorization: `Bearer ${state}`, \"content-type\": \"application/json\", \"user-agent\": \"unfazed-eve-sandbox-setup\" },\n body: JSON.stringify({ audience }),\n });\n if (!res.ok) throw new Error(`unfazed eve-sandbox: could not read the box's owner (${res.status}); pass --allow`);\n const { token } = (await res.json()) as { token: string };\n const claims = JSON.parse(Buffer.from(token.split(\".\")[1]!.replace(/-/g, \"+\").replace(/_/g, \"/\"), \"base64\").toString(\"utf8\")) as {\n owner_id?: string;\n };\n if (!claims.owner_id) throw new Error(\"unfazed eve-sandbox: the box's token names no owner; pass --allow\");\n return `owner:${claims.owner_id}`;\n}\n\nfunction installDaemon(): void {\n mkdirSync(LIB, { recursive: true });\n chmodSync(ROOT, 0o755);\n chmodSync(LIB, 0o755);\n const source = join(dirname(fileURLToPath(import.meta.url)), \"..\", \"eve-sandbox\");\n for (const file of [\"daemon.js\", \"protocol.js\"]) {\n copyFileSync(join(source, file), join(LIB, file));\n chmodSync(join(LIB, file), 0o644);\n }\n mkdirSync(dirname(HOOK), { recursive: true });\n writeFileSync(\n HOOK,\n [\n \"#!/bin/sh\",\n \"# eve sandbox daemon (unfazed/eve-sandbox) on :3000, restarted if it exits.\",\n `cd ${ROOT} || exit 1`,\n \"while true; do\",\n ` ${existsSync(\"/usr/local/bin/node\") ? \"/usr/local/bin/node\" : process.execPath} ${LIB}/daemon.js serve --config ${CONFIG} >>${ROOT}/daemon.log 2>&1`,\n \" sleep 2\",\n \"done\",\n \"\",\n ].join(\"\\n\"),\n { mode: 0o755 },\n );\n}\n\n/** Installs agent-browser and Chromium once for every session; returns the Chromium binary. */\nfunction installBrowser(version: string): string {\n process.stderr.write(`installing agent-browser@${version} and Chromium (a few minutes the first time)…\\n`);\n const env = { ...process.env, DEBIAN_FRONTEND: \"noninteractive\" };\n execFileSync(\"npm\", [\"install\", \"-g\", \"--silent\", `agent-browser@${version}`], { stdio: \"inherit\", env });\n // agent-browser keeps browsers in the installing user's home; sessions are\n // other users. Root's browser directory becomes the shared one, so a\n // browser already there is found and not downloaded again.\n mkdirSync(BROWSERS, { recursive: true });\n const own = join(process.env[\"HOME\"] ?? \"/root\", \".agent-browser\", \"browsers\");\n mkdirSync(dirname(own), { recursive: true });\n if (existsSync(own) && !lstatSync(own).isSymbolicLink()) {\n for (const entry of readdirSync(own)) {\n if (!existsSync(join(BROWSERS, entry))) renameSync(join(own, entry), join(BROWSERS, entry));\n }\n rmSync(own, { recursive: true, force: true });\n }\n if (!existsSync(own)) symlinkSync(BROWSERS, own);\n execFileSync(\"agent-browser\", [\"install\", \"--with-deps\"], { stdio: \"inherit\", env });\n sh(\"chmod\", [\"-R\", \"a+rX\", \"/opt/agent-browser\"]);\n const latest = readdirSync(BROWSERS)\n .filter((e) => e.startsWith(\"chrome-\"))\n .sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))\n .at(-1);\n const binary = latest ? join(BROWSERS, latest, \"chrome\") : \"\";\n if (!binary || !existsSync(binary)) throw new Error(\"unfazed eve-sandbox: Chromium was not found after the install\");\n return binary;\n}\n\nfunction restartDaemon(): void {\n // The boot hook's loop first (or it starts the old daemon again), then any\n // daemon, wherever an earlier setup put it.\n for (const pattern of [\"[b]oot\\\\.d/50-eve-sandbox\", \"[e]ve-sandbox[^ ]*daemon[^ ]*\\\\.js serve\"]) {\n try {\n sh(\"pkill\", [\"-f\", pattern], { quiet: true });\n } catch {\n // none running\n }\n }\n execFileSync(\"sleep\", [\"1\"]);\n const child = spawn(\"setsid\", [HOOK], { detached: true, stdio: \"ignore\" });\n child.unref();\n}\n\nasync function health(): Promise<{ ok: boolean; version?: string }> {\n for (let i = 0; i < 20; i++) {\n try {\n const res = await fetch(\"http://127.0.0.1:3000/v1/health\", { signal: AbortSignal.timeout(2000) });\n if (res.ok) return (await res.json()) as { ok: boolean; version?: string };\n } catch {\n // not up yet\n }\n await new Promise((resolve) => setTimeout(resolve, 500));\n }\n return { ok: false };\n}\n\nfunction sessionCount(): number {\n return sh(\"getent\", [\"passwd\"], { quiet: true })\n .split(\"\\n\")\n .filter((line) => /^eve[0-9a-f]{12}:/.test(line)).length;\n}\n\nexport async function runEveSandbox(argv: readonly string[]): Promise<number> {\n const { values, positionals } = parseArgs({\n args: [...argv],\n allowPositionals: true,\n options: {\n allow: { type: \"string\" },\n \"no-browser\": { type: \"boolean\" },\n \"agent-browser\": { type: \"string\" },\n proxy: { type: \"string\" },\n \"idle-minutes\": { type: \"string\" },\n \"retention-days\": { type: \"string\" },\n config: { type: \"string\" },\n help: { type: \"boolean\", short: \"h\" },\n },\n });\n const [command, arg] = positionals;\n if (values.help || command === undefined || command === \"help\") {\n process.stdout.write(HELP);\n return command === undefined ? 1 : 0;\n }\n if (command === \"serve\") {\n const daemon = await import(\"../eve-sandbox/daemon.js\");\n const config = daemon.loadConfig(values.config ?? CONFIG);\n await daemon.createDaemon(config).listen();\n process.stdout.write(`eve sandbox daemon on :${config.port} for ${config.audience}\\n`);\n await new Promise(() => {});\n }\n requireBox();\n const url = process.env[\"UNFZ_PUBLIC_URL\"]!;\n switch (command) {\n case \"setup\": {\n const config = readConfig();\n const allow = values.allow\n ? values.allow.split(\",\").map((s) => s.trim()).filter(Boolean)\n : config.allow.length > 0\n ? []\n : [await ownOwner()];\n config.allow = [...new Set([...config.allow, ...allow])];\n const number = (flag: string | undefined, name: string) => {\n if (flag === undefined) return undefined;\n const n = Number(flag);\n if (!Number.isInteger(n) || n < 0) throw new Error(`unfazed eve-sandbox: --${name} must be a whole number`);\n return n;\n };\n config.idleMinutes = number(values[\"idle-minutes\"], \"idle-minutes\") ?? config.idleMinutes ?? 20;\n config.retentionDays = number(values[\"retention-days\"], \"retention-days\") ?? config.retentionDays ?? 14;\n config.env = { ...(config.env ?? {}) };\n if (values.proxy) config.env[\"AGENT_BROWSER_PROXY\"] = values.proxy;\n installDaemon();\n if (!values[\"no-browser\"]) config.env[\"AGENT_BROWSER_EXECUTABLE_PATH\"] = installBrowser(values[\"agent-browser\"] ?? DEFAULT_AGENT_BROWSER);\n writeConfig(config);\n restartDaemon();\n const up = await health();\n if (!up.ok) throw new Error(`unfazed eve-sandbox: the daemon did not come up; see ${ROOT}/daemon.log`);\n const alias = new URL(url).hostname.split(\".\")[0];\n process.stdout.write(\n `eve sandbox ready at ${url} (daemon ${up.version}); allowed: ${config.allow.join(\", \")}\\n\\n` +\n `In the eve app:\\n export const environment = UnfazedSandbox.environment({ box: \"${alias}\" });\\n`,\n );\n return 0;\n }\n case \"allow\":\n case \"deny\": {\n if (!arg) throw new Error(`unfazed eve-sandbox: ${command} needs an app id or owner:<id>`);\n const config = readConfig();\n config.allow = command === \"allow\" ? [...new Set([...config.allow, arg])] : config.allow.filter((entry) => entry !== arg);\n if (config.allow.length === 0) throw new Error(\"unfazed eve-sandbox: that would allow nothing; allow another app first\");\n writeConfig(config);\n restartDaemon();\n const up = await health();\n process.stdout.write(`${up.ok ? \"restarted\" : \"NOT restarted (see daemon.log)\"}; allowed: ${config.allow.join(\", \")}\\n`);\n return up.ok ? 0 : 1;\n }\n case \"status\": {\n const config = readConfig();\n const up = await health();\n process.stdout.write(\n `${url}: ${up.ok ? `up (daemon ${up.version})` : \"DOWN\"}\\n` +\n `allowed: ${config.allow.join(\", \") || \"(none)\"}\\n` +\n `sessions: ${sessionCount()}\\n` +\n `browser: ${config.env?.[\"AGENT_BROWSER_EXECUTABLE_PATH\"] ?? \"(not installed)\"}\\n`,\n );\n return up.ok ? 0 : 1;\n }\n default:\n process.stderr.write(HELP);\n return 1;\n }\n}\n"]}