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 +72 -0
- package/README.md +147 -2
- package/REFERENCE.md +47 -2
- package/dist/cli/eve-sandbox.d.ts +3 -0
- package/dist/cli/eve-sandbox.js +268 -0
- package/dist/cli/eve-sandbox.js.map +1 -0
- package/dist/cli/login.d.ts +1 -1
- package/dist/cli.js +11 -0
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +21 -1
- package/dist/client.js +12 -4
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +49 -0
- package/dist/errors.js +25 -0
- package/dist/errors.js.map +1 -1
- package/dist/eve-sandbox/daemon.d.ts +59 -0
- package/dist/eve-sandbox/daemon.js +664 -0
- package/dist/eve-sandbox/daemon.js.map +1 -0
- package/dist/eve-sandbox/index.d.ts +77 -0
- package/dist/eve-sandbox/index.js +352 -0
- package/dist/eve-sandbox/index.js.map +1 -0
- package/dist/eve-sandbox/protocol.d.ts +68 -0
- package/dist/eve-sandbox/protocol.js +22 -0
- package/dist/eve-sandbox/protocol.js.map +1 -0
- package/dist/index.d.ts +16 -2
- package/dist/index.js +14 -1
- package/dist/index.js.map +1 -1
- package/dist/oidc.d.ts +161 -0
- package/dist/oidc.js +391 -0
- package/dist/oidc.js.map +1 -0
- package/dist/reference.generated.js +1 -1
- package/dist/reference.generated.js.map +1 -1
- package/dist/reference.js +3 -3
- package/dist/reference.js.map +1 -1
- package/dist/unfazed.d.ts +13 -2
- package/dist/unfazed.js +45 -4
- package/dist/unfazed.js.map +1 -1
- package/package.json +14 -4
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)
|
|
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;
|
|
7
|
-
entry. Methods are async and throw an
|
|
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,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"]}
|