@ai-ecoverse/slicc-agent 6.15.2 → 6.17.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.
Files changed (40) hide show
  1. package/README.md +3 -1
  2. package/dist/codex.js +50 -0
  3. package/dist/codex.js.map +7 -0
  4. package/dist/device.js +145 -0
  5. package/dist/device.js.map +7 -0
  6. package/dist/guests/guard.js +28 -1
  7. package/dist/guests/guard.js.map +2 -2
  8. package/dist/main.js +10 -6
  9. package/dist/main.js.map +2 -2
  10. package/dist/preferred.js +1 -0
  11. package/dist/preferred.js.map +2 -2
  12. package/dist/providers.js +16 -4
  13. package/dist/providers.js.map +2 -2
  14. package/dist/secrets/credentials.js +10 -2
  15. package/dist/secrets/credentials.js.map +2 -2
  16. package/dist/secrets/vault.js +4 -0
  17. package/dist/secrets/vault.js.map +2 -2
  18. package/dist/services.js.map +2 -2
  19. package/dist/settings.js +31 -5
  20. package/dist/settings.js.map +2 -2
  21. package/dist/sign-ins.js +25 -0
  22. package/dist/sign-ins.js.map +7 -0
  23. package/dist/spectrum/approvals-port.js +26 -2
  24. package/dist/spectrum/approvals-port.js.map +2 -2
  25. package/dist/spectrum/mcp-login.js +2 -1
  26. package/dist/spectrum/mcp-login.js.map +2 -2
  27. package/dist/spectrum/settings-port.js +65 -11
  28. package/dist/spectrum/settings-port.js.map +2 -2
  29. package/dist/types/codex.d.ts +4 -0
  30. package/dist/types/device.d.ts +24 -0
  31. package/dist/types/guests/guard.d.ts +1 -0
  32. package/dist/types/providers.d.ts +1 -0
  33. package/dist/types/secrets/vault.d.ts +6 -0
  34. package/dist/types/services.d.ts +2 -1
  35. package/dist/types/settings.d.ts +4 -0
  36. package/dist/types/sign-ins.d.ts +17 -0
  37. package/dist/types/spectrum/approvals-port.d.ts +1 -0
  38. package/dist/types/spectrum/mcp-login.d.ts +1 -0
  39. package/dist/types/spectrum/settings-port.d.ts +5 -2
  40. package/package.json +4 -4
package/README.md CHANGED
@@ -154,6 +154,8 @@ The first provider is AWS Bedrock with a Bedrock API key as the bearer token; th
154
154
 
155
155
  **Blocked providers.** `needs: 'cors-free-transport'` is the static fact; `blocked: 'cors-free-transport'`, on both `ProviderOption` and the account, says the current transport can't reach that provider right now: the worker's kernel transport is unavailable, or it is the page's own fetch without a linked exit (`transportReach(client)` in `src/net.ts`, from the transport's traits). The worker follows `client.onTraits`, so a provider unblocks as soon as an extension or a linked slicc gives a CORS-free route, and blocks again when it goes, with no reload. A blocked provider is still offered and can be added; a connected one keeps its key and its row, but its models are left out of `models` and `labels`, so the picker offers none until the route is back.
156
156
 
157
+ **Subscription sign-ins.** xAI, Kimi For Coding and ChatGPT (`openai-codex`, shown as ChatGPT) sign in with pi-ai's own device-code flows (`DEVICE_SIGN_INS` in `src/sign-ins.ts`); xAI and Kimi keep API-key entry too, so their accounts are `auth: 'oauth'` with `secret: 'api-key'` (spectrum's “Use an API key instead”) and a `keyUrl`. Spectrum's Sign in calls `connect(id, undefined, undefined, { method: 'sign-in' })` in the click, and a pasted key `put`s first and calls `connect(id, undefined, fields, { method: 'key' })` (spectrum's `ConnectOptions`); without the fourth argument an `oauth` account signs in. The adapter opens an `about:blank` popup (`slicc-sign-in`) before anything else in the click's task, then asks the worker's `signIn(id)`, which adds the account, runs the provider's `auth.oauth.login` and answers `{ url, code }` once the provider gives its device code (an `https:` page only). While it waits the account is `signing-in` with `signIn: { url, code? }`, kept in the page and never in the replicated settings state; the popup goes to `url`, and spectrum shows `code` with Copy and a link to `url` when the browser blocked the popup. The adapter's `connect` call then waits in the worker for the token (a sign-in no `connect` claims within a minute is dropped) and hands the vault `grant { provider, name, response, settings? }`, `name` being the account's `vaultName` and `response` the token response built from pi-ai's credential, as bytes zeroed after the call; the token is never stored, logged, licked or put in the state, and pi-ai's own error texts, which can quote a token response, never leave the worker. A refusal reads “The sign-in was refused.”, a timeout, network failure or malformed answer “The sign-in didn't finish. Try again.”, the vault's `EEXIST` “This name holds another provider's key.”, and Cancel aborts at once with an `AbortError`. Signing in again is a rotation that keeps the placeholder, and over a pasted key the vault keeps that key restorable. The vault refreshes the token through `egress`; after `invalid_grant` its record is `expired` and the account offers Sign in again. ChatGPT's API reads the account id from the access token's JWT, which a placeholder isn't: the grant carries `settings: { CHATGPT_ACCOUNT_ID }` (every sign-in), the credential's env gets it from the record, and the worker hands pi-ai a carrier key `slicc.<claims>.<placeholder>` that the worker's fetch turns back into `Bearer <placeholder>` before the vault fills it, so pi-ai sends `chatgpt-account-id` itself; its turns use SSE, not WebSockets. Kimi's auth host answers browsers but its API needs the CORS-free transport; xAI's API answers browsers but its device endpoints don't, so an xAI sign-in needs the CORS-free transport while its key doesn't; ChatGPT's auth host answers browsers and its API doesn't. The flows load lazily from pi-ai: xAI's and Kimi's modules import nothing from Node, and openai-codex's imports `node:http` for its browser callback, which the device flow never runs, so slicc-bios's `node:*` stubs let it load.
158
+
157
159
  **Adding providers.** The settings state's `providers` lists every provider seven can add (`ProviderOption`: `id`, `name`, `description`, `auth`, `keyUrl`, `needs`, `fields` and `vaultName`, sorted by name), and the spectrum adapter serves it as `providers()` with a `providers` event when it changes. `accounts` then lists only the added ones: Bedrock and Adobe start as added, `connect` adds a provider, and one with a vault record or stored fields counts as added too. The added list is `providers` in `/.slicc/accounts.json`. `vaultName` is the record name spectrum `put`s the key under: pi-ai's env name, or `<ID>_API_KEY` for providers that share one (Moonshot AI, OpenCode and the Qwen token plans), since a `put` under another provider's name would rebind that record; Bedrock's is `AWS_BEARER_TOKEN_BEDROCK` and Adobe's `ADOBE_IMS_TOKEN`. The agent finds a provider's record by its `provider`, never by name. Each provider has at most one secret, its key; `fields` are the non-secret inputs (`name`, `label`, `required`, `placeholder`, `help`, `customBase`, `vaultSetting`, `options` and `value`). `vaultSetting` names the vault record setting a field fills (slicc-vault's tenant-bound wildcards, V3), so the vault can bind a provider's wildcard endpoints to the user's own tenant: Cloudflare's `account` is `CLOUDFLARE_ACCOUNT_ID` and `gateway` `CLOUDFLARE_GATEWAY_ID`, and Azure's `endpoint` is `AZURE_OPENAI_RESOURCE_NAME` (spectrum applies it only to a bare resource name; an `https:` value stays a `customBase`). On an added account each field's `value` is its stored value, trimmed, so Settings can show it and knows a custom base URL to unregister after a reload; a field with a default carries that instead (Bedrock's `us-west-2`). Bedrock's `region` has `options`, the 21 regions where AWS supports Bedrock API keys (the bearer token seven uses; AWS's “Supported Regions … for Amazon Bedrock API keys”), labelled with AWS's names (`Europe (Frankfurt), eu-central-1`); a stored region outside the list comes first, under its own name, so a select never shows a region the account doesn't use. `disconnect(id)` keeps the row and its fields, so connecting again needs only the key, and `remove(id)` drops the row and its fields; for both, spectrum removes the vault record first. On an MCP row, `remove` disconnects it. `connect` with a secret is refused for every model account ("API keys are kept in seven's vault: update seven"); the only secret it still takes is an MCP sign-in's callback answer.
158
160
 
159
161
  ### In slicc-spectrum
@@ -682,7 +684,7 @@ Every approval, of a guest's prompt, of a tool call in a run a guest started, or
682
684
  - **Lifetime:** an item waits 5 minutes unless its requester sets `expiresAt`, then settles `unanswered`. Pending guest items survive a reload of the worker; pending `sudo` items are withdrawn (`restart`), because a reload ends the kernel's processes. The document keeps every pending item and the last 100 settled ones.
683
685
  - **In the worker,** `AgentHost.approvals` has `request(draft, ctx)`, `decide(id, answer, reason, by, ctx)` (`by` is a `DecidedBy`, or `{ by, device }` to record the deciding device), `settled(id, ctx)` (resolves when the item leaves `pending`), `withdraw(id, reason, ctx)` and `withdrawSeat(seat, ctx)`, which withdraws a revoked seat's pending items.
684
686
  - **Over the link,** `AgentTranscript.approvals` replicates the items, and `AgentControl.decide(id, decision, reason, ctx)` answers one, for every owner device: the owner's pages and followers with or without Control, all with the same powers. `decidedBy` comes from the link's seat (`owner` for the owner's pages, `follower` for the others), and is only a record of who answered. A guest never gets `decide`.
685
- - **In spectrum** (1.84 on, which shows guest approvals in `<slicc-approvals>` with Always allow when `always` is true, as notices without `decide`, and an `unsure` verdict), `createAgentModel` returns `approvals`, an `ApprovalsAdapter` for `SliccModel.approvals`. `list()` has the pending `user` items, oldest first, as spectrum's `Approval`s, with the cone's name as `agentName` and the item's own `always`, so a guest approval offers Always allow only when the policy did when it was asked. `decide(id, decision, reason?)` rejects with the agent's message, and `ended` reports `answered`, `expired` or `withdrawn` for an item that left without this tab's decision. Every owner device's adapter has `decide`; a guest connection's doesn't. So a notice instead of a dialog shows only where nothing can answer.
687
+ - **In spectrum** (1.84 on, which shows guest approvals in `<slicc-approvals>` with Always allow when `always` is true, as notices without `decide`, and an `unsure` verdict), `createAgentModel` returns `approvals`, an `ApprovalsAdapter` for `SliccModel.approvals`. `list()` has the pending `user` items, oldest first, as spectrum's `Approval`s, with the cone's name as `agentName` and the item's own `always`, so a guest approval offers Always allow only when the policy did when it was asked. `decide(id, decision, reason?)` rejects with the agent's message, and `ended` reports `answered`, `expired` or `withdrawn` for an item that left without this tab's decision. Every owner device's adapter has `decide`; a guest connection's doesn't. So a notice instead of a dialog shows only where nothing can answer. A `guest-tool` approval also carries `args`, the call's arguments pretty-printed as JSON (two-space indent), so spectrum can show them uncut when `summary` is empty. The worker runs every string in the arguments through the secrets scrub before it files the item, so `summary`, `detail.arguments` and `args` show a secret as its placeholder; the seat's Always rules still match the call's own arguments. `args` is capped at 64 KiB (65,536 UTF-16 code units, `ARGS_LENGTH`, never splitting a surrogate pair): longer arguments are sent as that prefix with `argsTruncated: true`, and spectrum must then offer only Deny and Always deny, never Allow, because the end wasn't seen.
686
688
  - **Kernel sudo:** `AgentOwner.approve(req, { signal, where? })` from `startAgent` takes slicc-kernel's sudo request as it comes to the kernel's `approve` hook and resolves `once`, `always` or `deny`. bios hands its hook to it while the agent runs, and to its own dialog otherwise. It rejects with `code: 'EAGAIN'` before anything is recorded when the agent can't take the request (released, restarting, or an older worker), so that's when to fall back; any other rejection is a deny. The request goes through `slicc.agent.sudo`, a service only the owner's pages get, and becomes a `sudo` item on the `user` tier with the kernel's `expiresAt`:
687
689
  - **Requester** comes from the kernel's process tag: `{ kind: 'agent', agentId }` is the cone or scoop by its name, `{ kind: 'user', where }` is the kernel user with `where` (`terminal:…` reads "a terminal", `follower:<device>` the `where` the page passes, such as "on Pat's phone", or "a linked device", with the device id kept as `requester.device`), and anything else is the kernel user alone.
688
690
  - **Detail:** the command as the kernel resolved it (`path` and the arguments, shell-quoted), `target`, `cwd`, and `alwaysScope: 'cwd'` when the request offers Always (`req.always === true`, which the page sets from its own checks), since the kernel's Always writes a rule for that directory.
package/dist/codex.js ADDED
@@ -0,0 +1,50 @@
1
+ import { openaiCodexProvider } from "@earendil-works/pi-ai/providers/openai-codex";
2
+ import { CODEX_ACCOUNT } from "./sign-ins.js";
3
+ const CLAIM = "https://api.openai.com/auth";
4
+ const MARK = "slicc";
5
+ const CARRIED = /^Bearer slicc\.[A-Za-z0-9+/=]+\.([^.\s]+)$/;
6
+ function codexKey(placeholder, account) {
7
+ const claims = btoa(JSON.stringify({ [CLAIM]: { chatgpt_account_id: account } }));
8
+ return `${MARK}.${claims}.${placeholder}`;
9
+ }
10
+ function codexProvider() {
11
+ const base = openaiCodexProvider();
12
+ return {
13
+ ...base,
14
+ name: "ChatGPT",
15
+ auth: {
16
+ ...base.auth,
17
+ apiKey: {
18
+ name: "ChatGPT sign-in",
19
+ resolve: async ({ credential, signal }) => {
20
+ signal.throwIfAborted();
21
+ const account = credential?.env?.[CODEX_ACCOUNT];
22
+ if (!credential?.key || !account) return void 0;
23
+ return {
24
+ auth: { apiKey: codexKey(credential.key, account) },
25
+ env: credential.env,
26
+ source: "vault"
27
+ };
28
+ }
29
+ }
30
+ },
31
+ stream: (model, context, options) => base.stream(model, context, { ...options, transport: "sse" }),
32
+ streamSimple: (model, context, options) => base.streamSimple(model, context, { ...options, transport: "sse" })
33
+ };
34
+ }
35
+ function codexFetch(inner) {
36
+ return (input, init) => {
37
+ const request = new Request(input, init);
38
+ const carried = CARRIED.exec(request.headers.get("authorization") ?? "");
39
+ if (!carried) return inner(request);
40
+ const headers = new Headers(request.headers);
41
+ headers.set("authorization", `Bearer ${carried[1]}`);
42
+ return inner(new Request(request, { headers }));
43
+ };
44
+ }
45
+ export {
46
+ codexFetch,
47
+ codexKey,
48
+ codexProvider
49
+ };
50
+ //# sourceMappingURL=codex.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/codex.ts"],
4
+ "sourcesContent": ["import type { Provider } from '@earendil-works/pi-ai';\nimport { openaiCodexProvider } from '@earendil-works/pi-ai/providers/openai-codex';\nimport { CODEX_ACCOUNT } from './sign-ins.ts';\n\nconst CLAIM = 'https://api.openai.com/auth';\nconst MARK = 'slicc';\nconst CARRIED = /^Bearer slicc\\.[A-Za-z0-9+/=]+\\.([^.\\s]+)$/;\n\nexport function codexKey(placeholder: string, account: string): string {\n const claims = btoa(JSON.stringify({ [CLAIM]: { chatgpt_account_id: account } }));\n return `${MARK}.${claims}.${placeholder}`;\n}\n\nexport function codexProvider(): Provider {\n const base = openaiCodexProvider() as Provider;\n return {\n ...base,\n name: 'ChatGPT',\n auth: {\n ...base.auth,\n apiKey: {\n name: 'ChatGPT sign-in',\n resolve: async ({ credential, signal }) => {\n signal.throwIfAborted();\n const account = credential?.env?.[CODEX_ACCOUNT];\n if (!credential?.key || !account) return undefined;\n return {\n auth: { apiKey: codexKey(credential.key, account) },\n env: credential.env,\n source: 'vault',\n };\n },\n },\n },\n stream: (model, context, options) =>\n base.stream(model, context, { ...options, transport: 'sse' } as typeof options),\n streamSimple: (model, context, options) =>\n base.streamSimple(model, context, { ...options, transport: 'sse' }),\n };\n}\n\nexport function codexFetch(inner: typeof fetch): typeof fetch {\n return (input, init) => {\n const request = new Request(input, init);\n const carried = CARRIED.exec(request.headers.get('authorization') ?? '');\n if (!carried) return inner(request);\n const headers = new Headers(request.headers);\n headers.set('authorization', `Bearer ${carried[1]}`);\n return inner(new Request(request, { headers }));\n };\n}\n"],
5
+ "mappings": "AACA,SAAS,2BAA2B;AACpC,SAAS,qBAAqB;AAE9B,MAAM,QAAQ;AACd,MAAM,OAAO;AACb,MAAM,UAAU;AAET,SAAS,SAAS,aAAqB,SAAyB;AACrE,QAAM,SAAS,KAAK,KAAK,UAAU,EAAE,CAAC,KAAK,GAAG,EAAE,oBAAoB,QAAQ,EAAE,CAAC,CAAC;AAChF,SAAO,GAAG,IAAI,IAAI,MAAM,IAAI,WAAW;AACzC;AAEO,SAAS,gBAA0B;AACxC,QAAM,OAAO,oBAAoB;AACjC,SAAO;AAAA,IACL,GAAG;AAAA,IACH,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,GAAG,KAAK;AAAA,MACR,QAAQ;AAAA,QACN,MAAM;AAAA,QACN,SAAS,OAAO,EAAE,YAAY,OAAO,MAAM;AACzC,iBAAO,eAAe;AACtB,gBAAM,UAAU,YAAY,MAAM,aAAa;AAC/C,cAAI,CAAC,YAAY,OAAO,CAAC,QAAS,QAAO;AACzC,iBAAO;AAAA,YACL,MAAM,EAAE,QAAQ,SAAS,WAAW,KAAK,OAAO,EAAE;AAAA,YAClD,KAAK,WAAW;AAAA,YAChB,QAAQ;AAAA,UACV;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,IACA,QAAQ,CAAC,OAAO,SAAS,YACvB,KAAK,OAAO,OAAO,SAAS,EAAE,GAAG,SAAS,WAAW,MAAM,CAAmB;AAAA,IAChF,cAAc,CAAC,OAAO,SAAS,YAC7B,KAAK,aAAa,OAAO,SAAS,EAAE,GAAG,SAAS,WAAW,MAAM,CAAC;AAAA,EACtE;AACF;AAEO,SAAS,WAAW,OAAmC;AAC5D,SAAO,CAAC,OAAO,SAAS;AACtB,UAAM,UAAU,IAAI,QAAQ,OAAO,IAAI;AACvC,UAAM,UAAU,QAAQ,KAAK,QAAQ,QAAQ,IAAI,eAAe,KAAK,EAAE;AACvE,QAAI,CAAC,QAAS,QAAO,MAAM,OAAO;AAClC,UAAM,UAAU,IAAI,QAAQ,QAAQ,OAAO;AAC3C,YAAQ,IAAI,iBAAiB,UAAU,QAAQ,CAAC,CAAC,EAAE;AACnD,WAAO,MAAM,IAAI,QAAQ,SAAS,EAAE,QAAQ,CAAC,CAAC;AAAA,EAChD;AACF;",
6
+ "names": []
7
+ }
package/dist/device.js ADDED
@@ -0,0 +1,145 @@
1
+ import { VaultRefusal } from "./secrets/vault.js";
2
+ import {
3
+ CODEX,
4
+ CODEX_ACCOUNT,
5
+ httpsUrl,
6
+ NAME_TAKEN,
7
+ SIGN_IN_LOCKED,
8
+ SIGN_IN_REFUSED,
9
+ SIGN_IN_UNFINISHED,
10
+ SIGN_IN_UNKEPT
11
+ } from "./sign-ins.js";
12
+ const ATTACH_MS = 6e4;
13
+ class Kept extends Error {
14
+ }
15
+ const cancelled = () => new DOMException("The sign-in was cancelled.", "AbortError");
16
+ function keptError(error) {
17
+ if (error instanceof VaultRefusal && error.code === "EEXIST") return new Kept(NAME_TAKEN);
18
+ if (error instanceof VaultRefusal && error.code === "EACCES" && /lock/i.test(error.message))
19
+ return new Kept(SIGN_IN_LOCKED);
20
+ return new Kept(SIGN_IN_UNKEPT);
21
+ }
22
+ function flowError(error) {
23
+ if (error instanceof Kept) return error;
24
+ const text = error instanceof Error ? error.message : String(error ?? "");
25
+ return new Error(/denied|refused/i.test(text) ? SIGN_IN_REFUSED : SIGN_IN_UNFINISHED);
26
+ }
27
+ function tokenResponse(credential, now) {
28
+ const seconds = Math.round((credential.expires - now) / 1e3);
29
+ return new TextEncoder().encode(
30
+ JSON.stringify({
31
+ access_token: credential.access,
32
+ ...credential.refresh ? { refresh_token: credential.refresh } : {},
33
+ token_type: "Bearer",
34
+ ...Number.isFinite(seconds) ? { expires_in: Math.max(0, seconds) } : {}
35
+ })
36
+ );
37
+ }
38
+ function settingsOf(providerId, credential) {
39
+ const account = credential.accountId;
40
+ return providerId === CODEX && typeof account === "string" && account ? { settings: { [CODEX_ACCOUNT]: account } } : {};
41
+ }
42
+ function follow(signal, controller) {
43
+ const abort = () => controller.abort(cancelled());
44
+ if (signal?.aborted) abort();
45
+ signal?.addEventListener("abort", abort, { once: true });
46
+ return () => signal?.removeEventListener("abort", abort);
47
+ }
48
+ const DEVICE_CODE = "device_code";
49
+ function deviceFlows(grant, options = {}) {
50
+ const flows = /* @__PURE__ */ new Map();
51
+ const now = options.now ?? Date.now;
52
+ const drop = (providerId, flow) => {
53
+ clearTimeout(flow.timer);
54
+ if (flows.get(providerId) === flow) flows.delete(providerId);
55
+ };
56
+ const stop = (providerId) => {
57
+ const flow = flows.get(providerId);
58
+ if (!flow) return;
59
+ drop(providerId, flow);
60
+ flow.controller.abort(cancelled());
61
+ };
62
+ return {
63
+ has: (providerId) => flows.has(providerId),
64
+ stop,
65
+ start(provider, name, context) {
66
+ stop(provider.id);
67
+ const oauth = provider.auth.oauth;
68
+ if (!oauth) return Promise.reject(new Error(SIGN_IN_UNFINISHED));
69
+ const controller = new AbortController();
70
+ const { signal } = controller;
71
+ const shown = Promise.withResolvers();
72
+ const attached = Promise.withResolvers();
73
+ const notify = (event) => {
74
+ if (event.type !== DEVICE_CODE) return;
75
+ if (!httpsUrl(event.verificationUri)) {
76
+ shown.reject(new Error(SIGN_IN_UNFINISHED));
77
+ controller.abort(new Error(SIGN_IN_UNFINISHED));
78
+ return;
79
+ }
80
+ shown.resolve({ url: event.verificationUri, code: event.userCode });
81
+ };
82
+ const prompt = async (asked) => {
83
+ if (asked.type === "select" && asked.options.some((option) => option.id === DEVICE_CODE))
84
+ return DEVICE_CODE;
85
+ throw new Error(SIGN_IN_UNFINISHED);
86
+ };
87
+ const until = (promise) => new Promise((resolve, reject) => {
88
+ signal.addEventListener("abort", () => reject(signal.reason), { once: true });
89
+ promise.then(resolve, reject);
90
+ });
91
+ const run = async () => {
92
+ const credential = await oauth.login({ signal, notify, prompt });
93
+ await until(attached.promise);
94
+ signal.throwIfAborted();
95
+ const response = tokenResponse(credential, now());
96
+ try {
97
+ await grant({
98
+ provider: provider.id,
99
+ name,
100
+ response,
101
+ ...settingsOf(provider.id, credential)
102
+ });
103
+ } catch (error) {
104
+ throw keptError(error);
105
+ } finally {
106
+ response.fill(0);
107
+ }
108
+ };
109
+ const done = run().catch((error) => {
110
+ throw signal.aborted && !(error instanceof Kept) ? cancelled() : flowError(error);
111
+ });
112
+ done.catch((error) => shown.reject(error));
113
+ const flow = {
114
+ controller,
115
+ attached,
116
+ done,
117
+ timer: setTimeout(() => stop(provider.id), options.attachMs ?? ATTACH_MS)
118
+ };
119
+ flows.set(provider.id, flow);
120
+ const off = follow(context.abortSignal, controller);
121
+ return shown.promise.finally(off);
122
+ },
123
+ async finish(providerId, context) {
124
+ const flow = flows.get(providerId);
125
+ if (!flow) throw new Error(SIGN_IN_UNFINISHED);
126
+ clearTimeout(flow.timer);
127
+ const off = follow(context.abortSignal, flow.controller);
128
+ flow.attached.resolve();
129
+ try {
130
+ await flow.done;
131
+ } finally {
132
+ off();
133
+ drop(providerId, flow);
134
+ }
135
+ }
136
+ };
137
+ }
138
+ export {
139
+ ATTACH_MS,
140
+ deviceFlows,
141
+ flowError,
142
+ keptError,
143
+ tokenResponse
144
+ };
145
+ //# sourceMappingURL=device.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/device.ts"],
4
+ "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { AuthEvent, AuthPrompt, OAuthCredential, Provider } from '@earendil-works/pi-ai';\nimport { VaultRefusal } from './secrets/vault.ts';\nimport {\n CODEX,\n CODEX_ACCOUNT,\n type DeviceSignIn,\n httpsUrl,\n NAME_TAKEN,\n SIGN_IN_LOCKED,\n SIGN_IN_REFUSED,\n SIGN_IN_UNFINISHED,\n SIGN_IN_UNKEPT,\n} from './sign-ins.ts';\n\nexport const ATTACH_MS = 60_000;\n\nexport interface ProviderGrant {\n provider: string;\n name: string;\n response: Uint8Array;\n settings?: Record<string, string>;\n}\n\nexport type Grant = (grant: ProviderGrant) => Promise<unknown>;\n\nexport interface DeviceFlows {\n has(providerId: string): boolean;\n start(provider: Provider, name: string, context: Context): Promise<DeviceSignIn>;\n finish(providerId: string, context: Context): Promise<void>;\n stop(providerId: string): void;\n}\n\ninterface Flow {\n controller: AbortController;\n attached: PromiseWithResolvers<void>;\n timer: ReturnType<typeof setTimeout>;\n done: Promise<void>;\n}\n\nclass Kept extends Error {}\n\nconst cancelled = () => new DOMException('The sign-in was cancelled.', 'AbortError');\n\nexport function keptError(error: unknown): Error {\n if (error instanceof VaultRefusal && error.code === 'EEXIST') return new Kept(NAME_TAKEN);\n if (error instanceof VaultRefusal && error.code === 'EACCES' && /lock/i.test(error.message))\n return new Kept(SIGN_IN_LOCKED);\n return new Kept(SIGN_IN_UNKEPT);\n}\n\nexport function flowError(error: unknown): Error {\n if (error instanceof Kept) return error;\n const text = error instanceof Error ? error.message : String(error ?? '');\n return new Error(/denied|refused/i.test(text) ? SIGN_IN_REFUSED : SIGN_IN_UNFINISHED);\n}\n\nexport function tokenResponse(credential: OAuthCredential, now: number): Uint8Array {\n const seconds = Math.round((credential.expires - now) / 1000);\n return new TextEncoder().encode(\n JSON.stringify({\n access_token: credential.access,\n ...(credential.refresh ? { refresh_token: credential.refresh } : {}),\n token_type: 'Bearer',\n ...(Number.isFinite(seconds) ? { expires_in: Math.max(0, seconds) } : {}),\n })\n );\n}\n\nfunction settingsOf(providerId: string, credential: OAuthCredential) {\n const account = credential.accountId;\n return providerId === CODEX && typeof account === 'string' && account\n ? { settings: { [CODEX_ACCOUNT]: account } }\n : {};\n}\n\nfunction follow(signal: AbortSignal | undefined, controller: AbortController): () => void {\n const abort = () => controller.abort(cancelled());\n if (signal?.aborted) abort();\n signal?.addEventListener('abort', abort, { once: true });\n return () => signal?.removeEventListener('abort', abort);\n}\n\nconst DEVICE_CODE = 'device_code';\n\nexport function deviceFlows(\n grant: Grant,\n options: { attachMs?: number; now?: () => number } = {}\n): DeviceFlows {\n const flows = new Map<string, Flow>();\n const now = options.now ?? Date.now;\n const drop = (providerId: string, flow: Flow) => {\n clearTimeout(flow.timer);\n if (flows.get(providerId) === flow) flows.delete(providerId);\n };\n const stop = (providerId: string) => {\n const flow = flows.get(providerId);\n if (!flow) return;\n drop(providerId, flow);\n flow.controller.abort(cancelled());\n };\n return {\n has: (providerId) => flows.has(providerId),\n stop,\n start(provider, name, context) {\n stop(provider.id);\n const oauth = provider.auth.oauth;\n if (!oauth) return Promise.reject(new Error(SIGN_IN_UNFINISHED));\n const controller = new AbortController();\n const { signal } = controller;\n const shown = Promise.withResolvers<DeviceSignIn>();\n const attached = Promise.withResolvers<void>();\n const notify = (event: AuthEvent) => {\n if (event.type !== DEVICE_CODE) return;\n if (!httpsUrl(event.verificationUri)) {\n shown.reject(new Error(SIGN_IN_UNFINISHED));\n controller.abort(new Error(SIGN_IN_UNFINISHED));\n return;\n }\n shown.resolve({ url: event.verificationUri, code: event.userCode });\n };\n const prompt = async (asked: AuthPrompt) => {\n if (asked.type === 'select' && asked.options.some((option) => option.id === DEVICE_CODE))\n return DEVICE_CODE;\n throw new Error(SIGN_IN_UNFINISHED);\n };\n const until = (promise: Promise<void>) =>\n new Promise<void>((resolve, reject) => {\n signal.addEventListener('abort', () => reject(signal.reason), { once: true });\n promise.then(resolve, reject);\n });\n const run = async () => {\n const credential = await oauth.login({ signal, notify, prompt });\n await until(attached.promise);\n signal.throwIfAborted();\n const response = tokenResponse(credential, now());\n try {\n await grant({\n provider: provider.id,\n name,\n response,\n ...settingsOf(provider.id, credential),\n });\n } catch (error) {\n throw keptError(error);\n } finally {\n response.fill(0);\n }\n };\n const done = run().catch((error: unknown) => {\n throw signal.aborted && !(error instanceof Kept) ? cancelled() : flowError(error);\n });\n done.catch((error: unknown) => shown.reject(error));\n const flow: Flow = {\n controller,\n attached,\n done,\n timer: setTimeout(() => stop(provider.id), options.attachMs ?? ATTACH_MS),\n };\n flows.set(provider.id, flow);\n const off = follow(context.abortSignal, controller);\n return shown.promise.finally(off);\n },\n async finish(providerId, context) {\n const flow = flows.get(providerId);\n if (!flow) throw new Error(SIGN_IN_UNFINISHED);\n clearTimeout(flow.timer);\n const off = follow(context.abortSignal, flow.controller);\n flow.attached.resolve();\n try {\n await flow.done;\n } finally {\n off();\n drop(providerId, flow);\n }\n },\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -6,6 +6,7 @@ import {
6
6
  ToolTask
7
7
  } from "@earendil-works/pi-durable";
8
8
  import { REASONS, scoopTier } from "../approvals/record.js";
9
+ import { scrubTexts } from "../secrets/scrub.js";
9
10
  import { gateKind, gateRefusal } from "./policy.js";
10
11
  import { GuestsDoc, isGate, liveSeat } from "./seats.js";
11
12
  const GUEST_GUARD = "slicc-guest-guard";
@@ -27,6 +28,25 @@ function canonical(value) {
27
28
  return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
28
29
  return JSON.stringify(value ?? null);
29
30
  }
31
+ function leaves(value, found) {
32
+ if (typeof value === "string") found.push(value);
33
+ else if (value && typeof value === "object")
34
+ for (const item of Object.values(value)) leaves(item, found);
35
+ }
36
+ function replaced(value, texts) {
37
+ if (typeof value === "string") return texts.shift();
38
+ if (Array.isArray(value)) return value.map((item) => replaced(item, texts));
39
+ if (value && typeof value === "object")
40
+ return Object.fromEntries(
41
+ Object.entries(value).map(([key, item]) => [key, replaced(item, texts)])
42
+ );
43
+ return value;
44
+ }
45
+ async function scrubbedArguments(args) {
46
+ const found = [];
47
+ leaves(args, found);
48
+ return replaced(args, await scrubTexts(found));
49
+ }
30
50
  function summaryOf(args) {
31
51
  const values = Object.values(args);
32
52
  const text = values.length === 1 && typeof values[0] === "string" ? values[0] : canonical(args);
@@ -123,6 +143,7 @@ function guestToolGate(harness) {
123
143
  context
124
144
  );
125
145
  const task = Number(api.taskId);
146
+ const shown = await scrubbedArguments(call.arguments);
126
147
  const asked = await host.approvals.request(
127
148
  {
128
149
  id,
@@ -131,7 +152,12 @@ function guestToolGate(harness) {
131
152
  agentId: seat.agentId,
132
153
  requester: { kind: "guest", name: seat.label, seat: seat.id },
133
154
  tier: taint.tier,
134
- detail: { tool: call.name, summary: summaryOf(call.arguments), arguments: args, task }
155
+ detail: {
156
+ tool: call.name,
157
+ summary: summaryOf(shown),
158
+ arguments: canonical(shown),
159
+ task
160
+ }
135
161
  },
136
162
  BACKGROUND_CONTEXT
137
163
  );
@@ -191,6 +217,7 @@ export {
191
217
  canonical,
192
218
  guestToolGate,
193
219
  reasonOf,
220
+ scrubbedArguments,
194
221
  summaryOf,
195
222
  taintsOf
196
223
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/guests/guard.ts"],
4
- "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport { awaitWithContext, BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport {\n defineExtension,\n type Extension,\n type Harness,\n type HookApi,\n hook,\n LiveDoc,\n type SubmissionId,\n type TaskId,\n type ToolHookCall,\n ToolTask,\n} from '@earendil-works/pi-durable';\nimport type { Approval, Approvals } from '../approvals/index.ts';\nimport { REASONS, scoopTier } from '../approvals/record.ts';\nimport { type GuestPolicy, gateKind, gateRefusal } from './policy.ts';\nimport { type AlwaysTool, GuestsDoc, isGate, liveSeat, type SeatRecord } from './seats.ts';\n\nexport const GUEST_GUARD = 'slicc-guest-guard';\nexport const GUEST_REQUEST = 'guest:';\nexport const GUEST_TOOL_MEMO = 'slicc.guest-tool';\nexport const SUMMARY_LENGTH = 2000;\n\nexport const GATE_REASONS = {\n unknown:\n 'A guest started this run, and its seat names no review for tool calls that this agent knows.',\n ended: \"A guest started this run, and that guest's seat has ended.\",\n rejected: 'The owner declined this tool call in a run a guest started.',\n judged: 'The automatic check declined this tool call in a run a guest started',\n unanswered: 'Nobody approved this tool call in a run a guest started in time.',\n unroutable: \"The cone can't review a tool call it is itself waiting on.\",\n withdrawn: 'The review of this tool call was withdrawn.',\n} as const;\n\nexport interface GateHost {\n approvals: Pick<Approvals, 'state' | 'request' | 'settled' | 'withdraw'>;\n policy(): GuestPolicy;\n now?: () => number;\n}\n\nexport interface GuestToolGate {\n readonly extension: Extension;\n bind(host: GateHost): void;\n}\n\ntype Held = Pick<Harness, 'submission' | 'commit' | 'waitForTask'>;\n\ntype Taint = { seat: SeatRecord | null; tier: string };\n\nexport function canonical(value: unknown): string {\n if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;\n if (value && typeof value === 'object')\n return `{${Object.keys(value)\n .sort()\n .map((key) => `${JSON.stringify(key)}:${canonical((value as Record<string, unknown>)[key])}`)\n .join(',')}}`;\n return JSON.stringify(value ?? null);\n}\n\nexport function summaryOf(args: Record<string, unknown>): string {\n const values = Object.values(args);\n const text = values.length === 1 && typeof values[0] === 'string' ? values[0] : canonical(args);\n return text.length > SUMMARY_LENGTH ? `${text.slice(0, SUMMARY_LENGTH - 1)}\u2026` : text;\n}\n\nconst SCOOP_SAID: Record<string, (name: string) => string> = {\n [REASONS.noScoop]: (name) => `there is no scoop ${name} in this cone to review it.`,\n [REASONS.scoopStopped]: (name) => `the scoop ${name} is stopped, so it can't review it.`,\n [REASONS.scoopAsked]: (name) =>\n `the scoop ${name} can't review a call that asks something of it.`,\n [REASONS.scoopUndecided]: (name) => `the scoop ${name} ended its review without deciding.`,\n};\n\nfunction scoopReason(item: Approval): string | null {\n const name = item.tier.slice('scoop:'.length);\n if (item.decidedBy?.startsWith('scoop:'))\n return `The scoop ${name} declined this tool call in a run a guest started${item.reason ? `: ${item.reason}` : '.'}`;\n const said = SCOOP_SAID[item.reason ?? ''];\n return said ? `A guest started this run, and ${said(name)}` : null;\n}\n\nexport function reasonOf(item: Approval): string {\n const scooped = item.state === 'rejected' && scoopTier(item.tier) ? scoopReason(item) : null;\n if (scooped) return scooped;\n if (item.state === 'rejected' && item.decidedBy === 'judge')\n return item.verdict\n ? `${GATE_REASONS.judged}: ${item.verdict.reason}`\n : `${GATE_REASONS.judged}.`;\n if (item.state === 'rejected')\n return item.reason && item.reason !== REASONS.unknownAnswer\n ? `${GATE_REASONS.rejected} ${item.reason}`\n : GATE_REASONS.rejected;\n if (item.state === 'withdrawn') return GATE_REASONS.withdrawn;\n if (item.reason === REASONS.unknownTier) return GATE_REASONS.unknown;\n if (item.reason === REASONS.unroutable) return GATE_REASONS.unroutable;\n return GATE_REASONS.unanswered;\n}\n\nexport async function taintsOf(\n inputs: readonly SubmissionId[],\n seats: Readonly<Record<string, SeatRecord>>,\n harness: Pick<Harness, 'submission'>,\n context: Context\n): Promise<Taint[]> {\n const found = new Map<string, Taint>();\n for (const id of inputs) {\n const seat = Object.values(seats).find((record) =>\n record.submissions?.some((entry) => entry.id === String(id))\n );\n const tier = seat?.submissions?.find((entry) => entry.id === String(id))?.tools;\n if (seat) {\n found.set(`${seat.id}\\n${tier}`, { seat, tier: String(tier) });\n continue;\n }\n const submission = await harness.submission(id, context);\n const record = submission ? await submission.status(context) : undefined;\n if (record?.requestId?.startsWith(GUEST_REQUEST)) return [{ seat: null, tier: '' }];\n }\n return [...found.values()];\n}\n\nfunction settledEarly(\n seat: SeatRecord,\n tier: string,\n policy: GuestPolicy,\n at: number,\n tool: string,\n args: string\n): string | null {\n if (!policy.allowedToolGates.includes(gateKind(tier)))\n return gateRefusal(\"a guest's tool calls\", policy.allowedToolGates);\n if (!liveSeat(seat, at)) return GATE_REASONS.ended;\n if (tier === 'off') return '';\n const kept = seat.alwaysTools?.some((rule) => rule.tool === tool && rule.arguments === args);\n return policy.always.tools && kept ? '' : null;\n}\n\nconst ended = (status: string) => status === 'terminal' || status === 'completing';\n\nexport function guestToolGate(harness: () => Held | undefined): GuestToolGate {\n let resolveHost: (host: GateHost) => void = () => {};\n const bound = new Promise<GateHost>((resolve) => {\n resolveHost = resolve;\n });\n const watched = new Set<string>();\n const watch = (host: GateHost, id: string, task: number) => {\n const held = harness();\n if (!held || watched.has(id)) return;\n watched.add(id);\n void held\n .waitForTask(task as unknown as TaskId, BACKGROUND_CONTEXT)\n .then((record) =>\n ended(record.state.status)\n ? host.approvals.withdraw(id, REASONS.aborted, BACKGROUND_CONTEXT)\n : undefined\n )\n .catch(() => undefined)\n .finally(() => watched.delete(id));\n };\n const remember = (seat: string, rule: AlwaysTool) =>\n (harness() as Held).commit(async (tx) => {\n const record = (await tx.doc(GuestsDoc)).seats[seat];\n if (!record || record.revoked !== null) return;\n const kept = (record.alwaysTools ?? []).filter(\n (entry) => entry.tool !== rule.tool || entry.arguments !== rule.arguments\n );\n record.alwaysTools = [...kept, rule];\n }, BACKGROUND_CONTEXT);\n const review = async (\n taint: Taint,\n call: ToolHookCall,\n api: HookApi,\n context: Context\n ): Promise<string | undefined> => {\n const { seat } = taint;\n if (seat === null || !isGate(taint.tier)) return GATE_REASONS.unknown;\n const host = await awaitWithContext(bound, context);\n const policy = host.policy();\n const now = host.now ?? Date.now;\n const args = canonical(call.arguments);\n const always = policy.always.tools;\n const early = settledEarly(seat, taint.tier, policy, now(), call.name, args);\n if (early !== null) return early || undefined;\n const id = await api.memo<string>(\n `${GUEST_TOOL_MEMO}:${seat.id}:${taint.tier}`,\n `apr-${crypto.randomUUID()}`,\n context\n );\n const task = Number(api.taskId);\n const asked = await host.approvals.request(\n {\n id,\n kind: 'guest-tool',\n seat: seat.id,\n agentId: seat.agentId,\n requester: { kind: 'guest', name: seat.label, seat: seat.id },\n tier: taint.tier,\n detail: { tool: call.name, summary: summaryOf(call.arguments), arguments: args, task },\n },\n BACKGROUND_CONTEXT\n );\n watch(host, id, task);\n const live = async () =>\n liveSeat((await api.snapshot(GuestsDoc, context))?.seats[seat.id], now());\n if (!(await live())) {\n await host.approvals.withdraw(id, REASONS.revoked, BACKGROUND_CONTEXT);\n return GATE_REASONS.ended;\n }\n const item = asked.state === 'pending' ? await host.approvals.settled(id, context) : asked;\n if (item.state !== 'approved') return reasonOf(item);\n if (!(await live())) return GATE_REASONS.ended;\n if (item.answer === 'always' && always)\n await remember(seat.id, {\n approval: item.id,\n tool: call.name,\n arguments: args,\n by: item.decidedBy,\n at: item.decidedAt ?? now(),\n });\n return undefined;\n };\n return {\n extension: defineExtension({\n name: GUEST_GUARD,\n hooks: [\n hook(ToolTask, {\n async beforeTool(call, api, context) {\n const live = await api.snapshot(LiveDoc, api.conversationId, context);\n const inputs = live?.run?.inputs ?? [];\n if (!inputs.length) return undefined;\n const seats = (await api.snapshot(GuestsDoc, context))?.seats ?? {};\n const held = harness() as Held;\n for (const taint of await taintsOf(inputs, seats, held, context)) {\n const block = await review(taint, call, api, context);\n if (block) return { block };\n }\n return undefined;\n },\n }),\n ],\n }),\n bind(host) {\n resolveHost(host);\n for (const item of host.approvals.state.value ?? [])\n if (item.kind === 'guest-tool' && item.state === 'pending' && item.detail.task)\n watch(host, item.id, item.detail.task);\n },\n };\n}\n"],
5
- "mappings": "AACA,SAAS,kBAAkB,0BAA0B;AACrD;AAAA,EACE;AAAA,EAIA;AAAA,EACA;AAAA,EAIA;AAAA,OACK;AAEP,SAAS,SAAS,iBAAiB;AACnC,SAA2B,UAAU,mBAAmB;AACxD,SAA0B,WAAW,QAAQ,gBAAiC;AAEvE,MAAM,cAAc;AACpB,MAAM,gBAAgB;AACtB,MAAM,kBAAkB;AACxB,MAAM,iBAAiB;AAEvB,MAAM,eAAe;AAAA,EAC1B,SACE;AAAA,EACF,OAAO;AAAA,EACP,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,WAAW;AACb;AAiBO,SAAS,UAAU,OAAwB;AAChD,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,IAAI,MAAM,IAAI,SAAS,EAAE,KAAK,GAAG,CAAC;AACnE,MAAI,SAAS,OAAO,UAAU;AAC5B,WAAO,IAAI,OAAO,KAAK,KAAK,EACzB,KAAK,EACL,IAAI,CAAC,QAAQ,GAAG,KAAK,UAAU,GAAG,CAAC,IAAI,UAAW,MAAkC,GAAG,CAAC,CAAC,EAAE,EAC3F,KAAK,GAAG,CAAC;AACd,SAAO,KAAK,UAAU,SAAS,IAAI;AACrC;AAEO,SAAS,UAAU,MAAuC;AAC/D,QAAM,SAAS,OAAO,OAAO,IAAI;AACjC,QAAM,OAAO,OAAO,WAAW,KAAK,OAAO,OAAO,CAAC,MAAM,WAAW,OAAO,CAAC,IAAI,UAAU,IAAI;AAC9F,SAAO,KAAK,SAAS,iBAAiB,GAAG,KAAK,MAAM,GAAG,iBAAiB,CAAC,CAAC,WAAM;AAClF;AAEA,MAAM,aAAuD;AAAA,EAC3D,CAAC,QAAQ,OAAO,GAAG,CAAC,SAAS,qBAAqB,IAAI;AAAA,EACtD,CAAC,QAAQ,YAAY,GAAG,CAAC,SAAS,aAAa,IAAI;AAAA,EACnD,CAAC,QAAQ,UAAU,GAAG,CAAC,SACrB,aAAa,IAAI;AAAA,EACnB,CAAC,QAAQ,cAAc,GAAG,CAAC,SAAS,aAAa,IAAI;AACvD;AAEA,SAAS,YAAY,MAA+B;AAClD,QAAM,OAAO,KAAK,KAAK,MAAM,SAAS,MAAM;AAC5C,MAAI,KAAK,WAAW,WAAW,QAAQ;AACrC,WAAO,aAAa,IAAI,oDAAoD,KAAK,SAAS,KAAK,KAAK,MAAM,KAAK,GAAG;AACpH,QAAM,OAAO,WAAW,KAAK,UAAU,EAAE;AACzC,SAAO,OAAO,iCAAiC,KAAK,IAAI,CAAC,KAAK;AAChE;AAEO,SAAS,SAAS,MAAwB;AAC/C,QAAM,UAAU,KAAK,UAAU,cAAc,UAAU,KAAK,IAAI,IAAI,YAAY,IAAI,IAAI;AACxF,MAAI,QAAS,QAAO;AACpB,MAAI,KAAK,UAAU,cAAc,KAAK,cAAc;AAClD,WAAO,KAAK,UACR,GAAG,aAAa,MAAM,KAAK,KAAK,QAAQ,MAAM,KAC9C,GAAG,aAAa,MAAM;AAC5B,MAAI,KAAK,UAAU;AACjB,WAAO,KAAK,UAAU,KAAK,WAAW,QAAQ,gBAC1C,GAAG,aAAa,QAAQ,IAAI,KAAK,MAAM,KACvC,aAAa;AACnB,MAAI,KAAK,UAAU,YAAa,QAAO,aAAa;AACpD,MAAI,KAAK,WAAW,QAAQ,YAAa,QAAO,aAAa;AAC7D,MAAI,KAAK,WAAW,QAAQ,WAAY,QAAO,aAAa;AAC5D,SAAO,aAAa;AACtB;AAEA,eAAsB,SACpB,QACA,OACA,SACA,SACkB;AAClB,QAAM,QAAQ,oBAAI,IAAmB;AACrC,aAAW,MAAM,QAAQ;AACvB,UAAM,OAAO,OAAO,OAAO,KAAK,EAAE;AAAA,MAAK,CAACA,YACtCA,QAAO,aAAa,KAAK,CAAC,UAAU,MAAM,OAAO,OAAO,EAAE,CAAC;AAAA,IAC7D;AACA,UAAM,OAAO,MAAM,aAAa,KAAK,CAAC,UAAU,MAAM,OAAO,OAAO,EAAE,CAAC,GAAG;AAC1E,QAAI,MAAM;AACR,YAAM,IAAI,GAAG,KAAK,EAAE;AAAA,EAAK,IAAI,IAAI,EAAE,MAAM,MAAM,OAAO,IAAI,EAAE,CAAC;AAC7D;AAAA,IACF;AACA,UAAM,aAAa,MAAM,QAAQ,WAAW,IAAI,OAAO;AACvD,UAAM,SAAS,aAAa,MAAM,WAAW,OAAO,OAAO,IAAI;AAC/D,QAAI,QAAQ,WAAW,WAAW,aAAa,EAAG,QAAO,CAAC,EAAE,MAAM,MAAM,MAAM,GAAG,CAAC;AAAA,EACpF;AACA,SAAO,CAAC,GAAG,MAAM,OAAO,CAAC;AAC3B;AAEA,SAAS,aACP,MACA,MACA,QACA,IACA,MACA,MACe;AACf,MAAI,CAAC,OAAO,iBAAiB,SAAS,SAAS,IAAI,CAAC;AAClD,WAAO,YAAY,wBAAwB,OAAO,gBAAgB;AACpE,MAAI,CAAC,SAAS,MAAM,EAAE,EAAG,QAAO,aAAa;AAC7C,MAAI,SAAS,MAAO,QAAO;AAC3B,QAAM,OAAO,KAAK,aAAa,KAAK,CAAC,SAAS,KAAK,SAAS,QAAQ,KAAK,cAAc,IAAI;AAC3F,SAAO,OAAO,OAAO,SAAS,OAAO,KAAK;AAC5C;AAEA,MAAM,QAAQ,CAAC,WAAmB,WAAW,cAAc,WAAW;AAE/D,SAAS,cAAc,SAAgD;AAC5E,MAAI,cAAwC,MAAM;AAAA,EAAC;AACnD,QAAM,QAAQ,IAAI,QAAkB,CAAC,YAAY;AAC/C,kBAAc;AAAA,EAChB,CAAC;AACD,QAAM,UAAU,oBAAI,IAAY;AAChC,QAAM,QAAQ,CAAC,MAAgB,IAAY,SAAiB;AAC1D,UAAM,OAAO,QAAQ;AACrB,QAAI,CAAC,QAAQ,QAAQ,IAAI,EAAE,EAAG;AAC9B,YAAQ,IAAI,EAAE;AACd,SAAK,KACF,YAAY,MAA2B,kBAAkB,EACzD;AAAA,MAAK,CAAC,WACL,MAAM,OAAO,MAAM,MAAM,IACrB,KAAK,UAAU,SAAS,IAAI,QAAQ,SAAS,kBAAkB,IAC/D;AAAA,IACN,EACC,MAAM,MAAM,MAAS,EACrB,QAAQ,MAAM,QAAQ,OAAO,EAAE,CAAC;AAAA,EACrC;AACA,QAAM,WAAW,CAAC,MAAc,SAC7B,QAAQ,EAAW,OAAO,OAAO,OAAO;AACvC,UAAM,UAAU,MAAM,GAAG,IAAI,SAAS,GAAG,MAAM,IAAI;AACnD,QAAI,CAAC,UAAU,OAAO,YAAY,KAAM;AACxC,UAAM,QAAQ,OAAO,eAAe,CAAC,GAAG;AAAA,MACtC,CAAC,UAAU,MAAM,SAAS,KAAK,QAAQ,MAAM,cAAc,KAAK;AAAA,IAClE;AACA,WAAO,cAAc,CAAC,GAAG,MAAM,IAAI;AAAA,EACrC,GAAG,kBAAkB;AACvB,QAAM,SAAS,OACb,OACA,MACA,KACA,YACgC;AAChC,UAAM,EAAE,KAAK,IAAI;AACjB,QAAI,SAAS,QAAQ,CAAC,OAAO,MAAM,IAAI,EAAG,QAAO,aAAa;AAC9D,UAAM,OAAO,MAAM,iBAAiB,OAAO,OAAO;AAClD,UAAM,SAAS,KAAK,OAAO;AAC3B,UAAM,MAAM,KAAK,OAAO,KAAK;AAC7B,UAAM,OAAO,UAAU,KAAK,SAAS;AACrC,UAAM,SAAS,OAAO,OAAO;AAC7B,UAAM,QAAQ,aAAa,MAAM,MAAM,MAAM,QAAQ,IAAI,GAAG,KAAK,MAAM,IAAI;AAC3E,QAAI,UAAU,KAAM,QAAO,SAAS;AACpC,UAAM,KAAK,MAAM,IAAI;AAAA,MACnB,GAAG,eAAe,IAAI,KAAK,EAAE,IAAI,MAAM,IAAI;AAAA,MAC3C,OAAO,OAAO,WAAW,CAAC;AAAA,MAC1B;AAAA,IACF;AACA,UAAM,OAAO,OAAO,IAAI,MAAM;AAC9B,UAAM,QAAQ,MAAM,KAAK,UAAU;AAAA,MACjC;AAAA,QACE;AAAA,QACA,MAAM;AAAA,QACN,MAAM,KAAK;AAAA,QACX,SAAS,KAAK;AAAA,QACd,WAAW,EAAE,MAAM,SAAS,MAAM,KAAK,OAAO,MAAM,KAAK,GAAG;AAAA,QAC5D,MAAM,MAAM;AAAA,QACZ,QAAQ,EAAE,MAAM,KAAK,MAAM,SAAS,UAAU,KAAK,SAAS,GAAG,WAAW,MAAM,KAAK;AAAA,MACvF;AAAA,MACA;AAAA,IACF;AACA,UAAM,MAAM,IAAI,IAAI;AACpB,UAAM,OAAO,YACX,UAAU,MAAM,IAAI,SAAS,WAAW,OAAO,IAAI,MAAM,KAAK,EAAE,GAAG,IAAI,CAAC;AAC1E,QAAI,CAAE,MAAM,KAAK,GAAI;AACnB,YAAM,KAAK,UAAU,SAAS,IAAI,QAAQ,SAAS,kBAAkB;AACrE,aAAO,aAAa;AAAA,IACtB;AACA,UAAM,OAAO,MAAM,UAAU,YAAY,MAAM,KAAK,UAAU,QAAQ,IAAI,OAAO,IAAI;AACrF,QAAI,KAAK,UAAU,WAAY,QAAO,SAAS,IAAI;AACnD,QAAI,CAAE,MAAM,KAAK,EAAI,QAAO,aAAa;AACzC,QAAI,KAAK,WAAW,YAAY;AAC9B,YAAM,SAAS,KAAK,IAAI;AAAA,QACtB,UAAU,KAAK;AAAA,QACf,MAAM,KAAK;AAAA,QACX,WAAW;AAAA,QACX,IAAI,KAAK;AAAA,QACT,IAAI,KAAK,aAAa,IAAI;AAAA,MAC5B,CAAC;AACH,WAAO;AAAA,EACT;AACA,SAAO;AAAA,IACL,WAAW,gBAAgB;AAAA,MACzB,MAAM;AAAA,MACN,OAAO;AAAA,QACL,KAAK,UAAU;AAAA,UACb,MAAM,WAAW,MAAM,KAAK,SAAS;AACnC,kBAAM,OAAO,MAAM,IAAI,SAAS,SAAS,IAAI,gBAAgB,OAAO;AACpE,kBAAM,SAAS,MAAM,KAAK,UAAU,CAAC;AACrC,gBAAI,CAAC,OAAO,OAAQ,QAAO;AAC3B,kBAAM,SAAS,MAAM,IAAI,SAAS,WAAW,OAAO,IAAI,SAAS,CAAC;AAClE,kBAAM,OAAO,QAAQ;AACrB,uBAAW,SAAS,MAAM,SAAS,QAAQ,OAAO,MAAM,OAAO,GAAG;AAChE,oBAAM,QAAQ,MAAM,OAAO,OAAO,MAAM,KAAK,OAAO;AACpD,kBAAI,MAAO,QAAO,EAAE,MAAM;AAAA,YAC5B;AACA,mBAAO;AAAA,UACT;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF,CAAC;AAAA,IACD,KAAK,MAAM;AACT,kBAAY,IAAI;AAChB,iBAAW,QAAQ,KAAK,UAAU,MAAM,SAAS,CAAC;AAChD,YAAI,KAAK,SAAS,gBAAgB,KAAK,UAAU,aAAa,KAAK,OAAO;AACxE,gBAAM,MAAM,KAAK,IAAI,KAAK,OAAO,IAAI;AAAA,IAC3C;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport { awaitWithContext, BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport {\n defineExtension,\n type Extension,\n type Harness,\n type HookApi,\n hook,\n LiveDoc,\n type SubmissionId,\n type TaskId,\n type ToolHookCall,\n ToolTask,\n} from '@earendil-works/pi-durable';\nimport type { Approval, Approvals } from '../approvals/index.ts';\nimport { REASONS, scoopTier } from '../approvals/record.ts';\nimport { scrubTexts } from '../secrets/scrub.ts';\nimport { type GuestPolicy, gateKind, gateRefusal } from './policy.ts';\nimport { type AlwaysTool, GuestsDoc, isGate, liveSeat, type SeatRecord } from './seats.ts';\n\nexport const GUEST_GUARD = 'slicc-guest-guard';\nexport const GUEST_REQUEST = 'guest:';\nexport const GUEST_TOOL_MEMO = 'slicc.guest-tool';\nexport const SUMMARY_LENGTH = 2000;\n\nexport const GATE_REASONS = {\n unknown:\n 'A guest started this run, and its seat names no review for tool calls that this agent knows.',\n ended: \"A guest started this run, and that guest's seat has ended.\",\n rejected: 'The owner declined this tool call in a run a guest started.',\n judged: 'The automatic check declined this tool call in a run a guest started',\n unanswered: 'Nobody approved this tool call in a run a guest started in time.',\n unroutable: \"The cone can't review a tool call it is itself waiting on.\",\n withdrawn: 'The review of this tool call was withdrawn.',\n} as const;\n\nexport interface GateHost {\n approvals: Pick<Approvals, 'state' | 'request' | 'settled' | 'withdraw'>;\n policy(): GuestPolicy;\n now?: () => number;\n}\n\nexport interface GuestToolGate {\n readonly extension: Extension;\n bind(host: GateHost): void;\n}\n\ntype Held = Pick<Harness, 'submission' | 'commit' | 'waitForTask'>;\n\ntype Taint = { seat: SeatRecord | null; tier: string };\n\nexport function canonical(value: unknown): string {\n if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;\n if (value && typeof value === 'object')\n return `{${Object.keys(value)\n .sort()\n .map((key) => `${JSON.stringify(key)}:${canonical((value as Record<string, unknown>)[key])}`)\n .join(',')}}`;\n return JSON.stringify(value ?? null);\n}\n\nfunction leaves(value: unknown, found: string[]): void {\n if (typeof value === 'string') found.push(value);\n else if (value && typeof value === 'object')\n for (const item of Object.values(value)) leaves(item, found);\n}\n\nfunction replaced(value: unknown, texts: string[]): unknown {\n if (typeof value === 'string') return texts.shift();\n if (Array.isArray(value)) return value.map((item) => replaced(item, texts));\n if (value && typeof value === 'object')\n return Object.fromEntries(\n Object.entries(value).map(([key, item]) => [key, replaced(item, texts)])\n );\n return value;\n}\n\nexport async function scrubbedArguments(\n args: Record<string, unknown>\n): Promise<Record<string, unknown>> {\n const found: string[] = [];\n leaves(args, found);\n return replaced(args, await scrubTexts(found)) as Record<string, unknown>;\n}\n\nexport function summaryOf(args: Record<string, unknown>): string {\n const values = Object.values(args);\n const text = values.length === 1 && typeof values[0] === 'string' ? values[0] : canonical(args);\n return text.length > SUMMARY_LENGTH ? `${text.slice(0, SUMMARY_LENGTH - 1)}\u2026` : text;\n}\n\nconst SCOOP_SAID: Record<string, (name: string) => string> = {\n [REASONS.noScoop]: (name) => `there is no scoop ${name} in this cone to review it.`,\n [REASONS.scoopStopped]: (name) => `the scoop ${name} is stopped, so it can't review it.`,\n [REASONS.scoopAsked]: (name) =>\n `the scoop ${name} can't review a call that asks something of it.`,\n [REASONS.scoopUndecided]: (name) => `the scoop ${name} ended its review without deciding.`,\n};\n\nfunction scoopReason(item: Approval): string | null {\n const name = item.tier.slice('scoop:'.length);\n if (item.decidedBy?.startsWith('scoop:'))\n return `The scoop ${name} declined this tool call in a run a guest started${item.reason ? `: ${item.reason}` : '.'}`;\n const said = SCOOP_SAID[item.reason ?? ''];\n return said ? `A guest started this run, and ${said(name)}` : null;\n}\n\nexport function reasonOf(item: Approval): string {\n const scooped = item.state === 'rejected' && scoopTier(item.tier) ? scoopReason(item) : null;\n if (scooped) return scooped;\n if (item.state === 'rejected' && item.decidedBy === 'judge')\n return item.verdict\n ? `${GATE_REASONS.judged}: ${item.verdict.reason}`\n : `${GATE_REASONS.judged}.`;\n if (item.state === 'rejected')\n return item.reason && item.reason !== REASONS.unknownAnswer\n ? `${GATE_REASONS.rejected} ${item.reason}`\n : GATE_REASONS.rejected;\n if (item.state === 'withdrawn') return GATE_REASONS.withdrawn;\n if (item.reason === REASONS.unknownTier) return GATE_REASONS.unknown;\n if (item.reason === REASONS.unroutable) return GATE_REASONS.unroutable;\n return GATE_REASONS.unanswered;\n}\n\nexport async function taintsOf(\n inputs: readonly SubmissionId[],\n seats: Readonly<Record<string, SeatRecord>>,\n harness: Pick<Harness, 'submission'>,\n context: Context\n): Promise<Taint[]> {\n const found = new Map<string, Taint>();\n for (const id of inputs) {\n const seat = Object.values(seats).find((record) =>\n record.submissions?.some((entry) => entry.id === String(id))\n );\n const tier = seat?.submissions?.find((entry) => entry.id === String(id))?.tools;\n if (seat) {\n found.set(`${seat.id}\\n${tier}`, { seat, tier: String(tier) });\n continue;\n }\n const submission = await harness.submission(id, context);\n const record = submission ? await submission.status(context) : undefined;\n if (record?.requestId?.startsWith(GUEST_REQUEST)) return [{ seat: null, tier: '' }];\n }\n return [...found.values()];\n}\n\nfunction settledEarly(\n seat: SeatRecord,\n tier: string,\n policy: GuestPolicy,\n at: number,\n tool: string,\n args: string\n): string | null {\n if (!policy.allowedToolGates.includes(gateKind(tier)))\n return gateRefusal(\"a guest's tool calls\", policy.allowedToolGates);\n if (!liveSeat(seat, at)) return GATE_REASONS.ended;\n if (tier === 'off') return '';\n const kept = seat.alwaysTools?.some((rule) => rule.tool === tool && rule.arguments === args);\n return policy.always.tools && kept ? '' : null;\n}\n\nconst ended = (status: string) => status === 'terminal' || status === 'completing';\n\nexport function guestToolGate(harness: () => Held | undefined): GuestToolGate {\n let resolveHost: (host: GateHost) => void = () => {};\n const bound = new Promise<GateHost>((resolve) => {\n resolveHost = resolve;\n });\n const watched = new Set<string>();\n const watch = (host: GateHost, id: string, task: number) => {\n const held = harness();\n if (!held || watched.has(id)) return;\n watched.add(id);\n void held\n .waitForTask(task as unknown as TaskId, BACKGROUND_CONTEXT)\n .then((record) =>\n ended(record.state.status)\n ? host.approvals.withdraw(id, REASONS.aborted, BACKGROUND_CONTEXT)\n : undefined\n )\n .catch(() => undefined)\n .finally(() => watched.delete(id));\n };\n const remember = (seat: string, rule: AlwaysTool) =>\n (harness() as Held).commit(async (tx) => {\n const record = (await tx.doc(GuestsDoc)).seats[seat];\n if (!record || record.revoked !== null) return;\n const kept = (record.alwaysTools ?? []).filter(\n (entry) => entry.tool !== rule.tool || entry.arguments !== rule.arguments\n );\n record.alwaysTools = [...kept, rule];\n }, BACKGROUND_CONTEXT);\n const review = async (\n taint: Taint,\n call: ToolHookCall,\n api: HookApi,\n context: Context\n ): Promise<string | undefined> => {\n const { seat } = taint;\n if (seat === null || !isGate(taint.tier)) return GATE_REASONS.unknown;\n const host = await awaitWithContext(bound, context);\n const policy = host.policy();\n const now = host.now ?? Date.now;\n const args = canonical(call.arguments);\n const always = policy.always.tools;\n const early = settledEarly(seat, taint.tier, policy, now(), call.name, args);\n if (early !== null) return early || undefined;\n const id = await api.memo<string>(\n `${GUEST_TOOL_MEMO}:${seat.id}:${taint.tier}`,\n `apr-${crypto.randomUUID()}`,\n context\n );\n const task = Number(api.taskId);\n const shown = await scrubbedArguments(call.arguments);\n const asked = await host.approvals.request(\n {\n id,\n kind: 'guest-tool',\n seat: seat.id,\n agentId: seat.agentId,\n requester: { kind: 'guest', name: seat.label, seat: seat.id },\n tier: taint.tier,\n detail: {\n tool: call.name,\n summary: summaryOf(shown),\n arguments: canonical(shown),\n task,\n },\n },\n BACKGROUND_CONTEXT\n );\n watch(host, id, task);\n const live = async () =>\n liveSeat((await api.snapshot(GuestsDoc, context))?.seats[seat.id], now());\n if (!(await live())) {\n await host.approvals.withdraw(id, REASONS.revoked, BACKGROUND_CONTEXT);\n return GATE_REASONS.ended;\n }\n const item = asked.state === 'pending' ? await host.approvals.settled(id, context) : asked;\n if (item.state !== 'approved') return reasonOf(item);\n if (!(await live())) return GATE_REASONS.ended;\n if (item.answer === 'always' && always)\n await remember(seat.id, {\n approval: item.id,\n tool: call.name,\n arguments: args,\n by: item.decidedBy,\n at: item.decidedAt ?? now(),\n });\n return undefined;\n };\n return {\n extension: defineExtension({\n name: GUEST_GUARD,\n hooks: [\n hook(ToolTask, {\n async beforeTool(call, api, context) {\n const live = await api.snapshot(LiveDoc, api.conversationId, context);\n const inputs = live?.run?.inputs ?? [];\n if (!inputs.length) return undefined;\n const seats = (await api.snapshot(GuestsDoc, context))?.seats ?? {};\n const held = harness() as Held;\n for (const taint of await taintsOf(inputs, seats, held, context)) {\n const block = await review(taint, call, api, context);\n if (block) return { block };\n }\n return undefined;\n },\n }),\n ],\n }),\n bind(host) {\n resolveHost(host);\n for (const item of host.approvals.state.value ?? [])\n if (item.kind === 'guest-tool' && item.state === 'pending' && item.detail.task)\n watch(host, item.id, item.detail.task);\n },\n };\n}\n"],
5
+ "mappings": "AACA,SAAS,kBAAkB,0BAA0B;AACrD;AAAA,EACE;AAAA,EAIA;AAAA,EACA;AAAA,EAIA;AAAA,OACK;AAEP,SAAS,SAAS,iBAAiB;AACnC,SAAS,kBAAkB;AAC3B,SAA2B,UAAU,mBAAmB;AACxD,SAA0B,WAAW,QAAQ,gBAAiC;AAEvE,MAAM,cAAc;AACpB,MAAM,gBAAgB;AACtB,MAAM,kBAAkB;AACxB,MAAM,iBAAiB;AAEvB,MAAM,eAAe;AAAA,EAC1B,SACE;AAAA,EACF,OAAO;AAAA,EACP,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,WAAW;AACb;AAiBO,SAAS,UAAU,OAAwB;AAChD,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,IAAI,MAAM,IAAI,SAAS,EAAE,KAAK,GAAG,CAAC;AACnE,MAAI,SAAS,OAAO,UAAU;AAC5B,WAAO,IAAI,OAAO,KAAK,KAAK,EACzB,KAAK,EACL,IAAI,CAAC,QAAQ,GAAG,KAAK,UAAU,GAAG,CAAC,IAAI,UAAW,MAAkC,GAAG,CAAC,CAAC,EAAE,EAC3F,KAAK,GAAG,CAAC;AACd,SAAO,KAAK,UAAU,SAAS,IAAI;AACrC;AAEA,SAAS,OAAO,OAAgB,OAAuB;AACrD,MAAI,OAAO,UAAU,SAAU,OAAM,KAAK,KAAK;AAAA,WACtC,SAAS,OAAO,UAAU;AACjC,eAAW,QAAQ,OAAO,OAAO,KAAK,EAAG,QAAO,MAAM,KAAK;AAC/D;AAEA,SAAS,SAAS,OAAgB,OAA0B;AAC1D,MAAI,OAAO,UAAU,SAAU,QAAO,MAAM,MAAM;AAClD,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,IAAI,CAAC,SAAS,SAAS,MAAM,KAAK,CAAC;AAC1E,MAAI,SAAS,OAAO,UAAU;AAC5B,WAAO,OAAO;AAAA,MACZ,OAAO,QAAQ,KAAK,EAAE,IAAI,CAAC,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,SAAS,MAAM,KAAK,CAAC,CAAC;AAAA,IACzE;AACF,SAAO;AACT;AAEA,eAAsB,kBACpB,MACkC;AAClC,QAAM,QAAkB,CAAC;AACzB,SAAO,MAAM,KAAK;AAClB,SAAO,SAAS,MAAM,MAAM,WAAW,KAAK,CAAC;AAC/C;AAEO,SAAS,UAAU,MAAuC;AAC/D,QAAM,SAAS,OAAO,OAAO,IAAI;AACjC,QAAM,OAAO,OAAO,WAAW,KAAK,OAAO,OAAO,CAAC,MAAM,WAAW,OAAO,CAAC,IAAI,UAAU,IAAI;AAC9F,SAAO,KAAK,SAAS,iBAAiB,GAAG,KAAK,MAAM,GAAG,iBAAiB,CAAC,CAAC,WAAM;AAClF;AAEA,MAAM,aAAuD;AAAA,EAC3D,CAAC,QAAQ,OAAO,GAAG,CAAC,SAAS,qBAAqB,IAAI;AAAA,EACtD,CAAC,QAAQ,YAAY,GAAG,CAAC,SAAS,aAAa,IAAI;AAAA,EACnD,CAAC,QAAQ,UAAU,GAAG,CAAC,SACrB,aAAa,IAAI;AAAA,EACnB,CAAC,QAAQ,cAAc,GAAG,CAAC,SAAS,aAAa,IAAI;AACvD;AAEA,SAAS,YAAY,MAA+B;AAClD,QAAM,OAAO,KAAK,KAAK,MAAM,SAAS,MAAM;AAC5C,MAAI,KAAK,WAAW,WAAW,QAAQ;AACrC,WAAO,aAAa,IAAI,oDAAoD,KAAK,SAAS,KAAK,KAAK,MAAM,KAAK,GAAG;AACpH,QAAM,OAAO,WAAW,KAAK,UAAU,EAAE;AACzC,SAAO,OAAO,iCAAiC,KAAK,IAAI,CAAC,KAAK;AAChE;AAEO,SAAS,SAAS,MAAwB;AAC/C,QAAM,UAAU,KAAK,UAAU,cAAc,UAAU,KAAK,IAAI,IAAI,YAAY,IAAI,IAAI;AACxF,MAAI,QAAS,QAAO;AACpB,MAAI,KAAK,UAAU,cAAc,KAAK,cAAc;AAClD,WAAO,KAAK,UACR,GAAG,aAAa,MAAM,KAAK,KAAK,QAAQ,MAAM,KAC9C,GAAG,aAAa,MAAM;AAC5B,MAAI,KAAK,UAAU;AACjB,WAAO,KAAK,UAAU,KAAK,WAAW,QAAQ,gBAC1C,GAAG,aAAa,QAAQ,IAAI,KAAK,MAAM,KACvC,aAAa;AACnB,MAAI,KAAK,UAAU,YAAa,QAAO,aAAa;AACpD,MAAI,KAAK,WAAW,QAAQ,YAAa,QAAO,aAAa;AAC7D,MAAI,KAAK,WAAW,QAAQ,WAAY,QAAO,aAAa;AAC5D,SAAO,aAAa;AACtB;AAEA,eAAsB,SACpB,QACA,OACA,SACA,SACkB;AAClB,QAAM,QAAQ,oBAAI,IAAmB;AACrC,aAAW,MAAM,QAAQ;AACvB,UAAM,OAAO,OAAO,OAAO,KAAK,EAAE;AAAA,MAAK,CAACA,YACtCA,QAAO,aAAa,KAAK,CAAC,UAAU,MAAM,OAAO,OAAO,EAAE,CAAC;AAAA,IAC7D;AACA,UAAM,OAAO,MAAM,aAAa,KAAK,CAAC,UAAU,MAAM,OAAO,OAAO,EAAE,CAAC,GAAG;AAC1E,QAAI,MAAM;AACR,YAAM,IAAI,GAAG,KAAK,EAAE;AAAA,EAAK,IAAI,IAAI,EAAE,MAAM,MAAM,OAAO,IAAI,EAAE,CAAC;AAC7D;AAAA,IACF;AACA,UAAM,aAAa,MAAM,QAAQ,WAAW,IAAI,OAAO;AACvD,UAAM,SAAS,aAAa,MAAM,WAAW,OAAO,OAAO,IAAI;AAC/D,QAAI,QAAQ,WAAW,WAAW,aAAa,EAAG,QAAO,CAAC,EAAE,MAAM,MAAM,MAAM,GAAG,CAAC;AAAA,EACpF;AACA,SAAO,CAAC,GAAG,MAAM,OAAO,CAAC;AAC3B;AAEA,SAAS,aACP,MACA,MACA,QACA,IACA,MACA,MACe;AACf,MAAI,CAAC,OAAO,iBAAiB,SAAS,SAAS,IAAI,CAAC;AAClD,WAAO,YAAY,wBAAwB,OAAO,gBAAgB;AACpE,MAAI,CAAC,SAAS,MAAM,EAAE,EAAG,QAAO,aAAa;AAC7C,MAAI,SAAS,MAAO,QAAO;AAC3B,QAAM,OAAO,KAAK,aAAa,KAAK,CAAC,SAAS,KAAK,SAAS,QAAQ,KAAK,cAAc,IAAI;AAC3F,SAAO,OAAO,OAAO,SAAS,OAAO,KAAK;AAC5C;AAEA,MAAM,QAAQ,CAAC,WAAmB,WAAW,cAAc,WAAW;AAE/D,SAAS,cAAc,SAAgD;AAC5E,MAAI,cAAwC,MAAM;AAAA,EAAC;AACnD,QAAM,QAAQ,IAAI,QAAkB,CAAC,YAAY;AAC/C,kBAAc;AAAA,EAChB,CAAC;AACD,QAAM,UAAU,oBAAI,IAAY;AAChC,QAAM,QAAQ,CAAC,MAAgB,IAAY,SAAiB;AAC1D,UAAM,OAAO,QAAQ;AACrB,QAAI,CAAC,QAAQ,QAAQ,IAAI,EAAE,EAAG;AAC9B,YAAQ,IAAI,EAAE;AACd,SAAK,KACF,YAAY,MAA2B,kBAAkB,EACzD;AAAA,MAAK,CAAC,WACL,MAAM,OAAO,MAAM,MAAM,IACrB,KAAK,UAAU,SAAS,IAAI,QAAQ,SAAS,kBAAkB,IAC/D;AAAA,IACN,EACC,MAAM,MAAM,MAAS,EACrB,QAAQ,MAAM,QAAQ,OAAO,EAAE,CAAC;AAAA,EACrC;AACA,QAAM,WAAW,CAAC,MAAc,SAC7B,QAAQ,EAAW,OAAO,OAAO,OAAO;AACvC,UAAM,UAAU,MAAM,GAAG,IAAI,SAAS,GAAG,MAAM,IAAI;AACnD,QAAI,CAAC,UAAU,OAAO,YAAY,KAAM;AACxC,UAAM,QAAQ,OAAO,eAAe,CAAC,GAAG;AAAA,MACtC,CAAC,UAAU,MAAM,SAAS,KAAK,QAAQ,MAAM,cAAc,KAAK;AAAA,IAClE;AACA,WAAO,cAAc,CAAC,GAAG,MAAM,IAAI;AAAA,EACrC,GAAG,kBAAkB;AACvB,QAAM,SAAS,OACb,OACA,MACA,KACA,YACgC;AAChC,UAAM,EAAE,KAAK,IAAI;AACjB,QAAI,SAAS,QAAQ,CAAC,OAAO,MAAM,IAAI,EAAG,QAAO,aAAa;AAC9D,UAAM,OAAO,MAAM,iBAAiB,OAAO,OAAO;AAClD,UAAM,SAAS,KAAK,OAAO;AAC3B,UAAM,MAAM,KAAK,OAAO,KAAK;AAC7B,UAAM,OAAO,UAAU,KAAK,SAAS;AACrC,UAAM,SAAS,OAAO,OAAO;AAC7B,UAAM,QAAQ,aAAa,MAAM,MAAM,MAAM,QAAQ,IAAI,GAAG,KAAK,MAAM,IAAI;AAC3E,QAAI,UAAU,KAAM,QAAO,SAAS;AACpC,UAAM,KAAK,MAAM,IAAI;AAAA,MACnB,GAAG,eAAe,IAAI,KAAK,EAAE,IAAI,MAAM,IAAI;AAAA,MAC3C,OAAO,OAAO,WAAW,CAAC;AAAA,MAC1B;AAAA,IACF;AACA,UAAM,OAAO,OAAO,IAAI,MAAM;AAC9B,UAAM,QAAQ,MAAM,kBAAkB,KAAK,SAAS;AACpD,UAAM,QAAQ,MAAM,KAAK,UAAU;AAAA,MACjC;AAAA,QACE;AAAA,QACA,MAAM;AAAA,QACN,MAAM,KAAK;AAAA,QACX,SAAS,KAAK;AAAA,QACd,WAAW,EAAE,MAAM,SAAS,MAAM,KAAK,OAAO,MAAM,KAAK,GAAG;AAAA,QAC5D,MAAM,MAAM;AAAA,QACZ,QAAQ;AAAA,UACN,MAAM,KAAK;AAAA,UACX,SAAS,UAAU,KAAK;AAAA,UACxB,WAAW,UAAU,KAAK;AAAA,UAC1B;AAAA,QACF;AAAA,MACF;AAAA,MACA;AAAA,IACF;AACA,UAAM,MAAM,IAAI,IAAI;AACpB,UAAM,OAAO,YACX,UAAU,MAAM,IAAI,SAAS,WAAW,OAAO,IAAI,MAAM,KAAK,EAAE,GAAG,IAAI,CAAC;AAC1E,QAAI,CAAE,MAAM,KAAK,GAAI;AACnB,YAAM,KAAK,UAAU,SAAS,IAAI,QAAQ,SAAS,kBAAkB;AACrE,aAAO,aAAa;AAAA,IACtB;AACA,UAAM,OAAO,MAAM,UAAU,YAAY,MAAM,KAAK,UAAU,QAAQ,IAAI,OAAO,IAAI;AACrF,QAAI,KAAK,UAAU,WAAY,QAAO,SAAS,IAAI;AACnD,QAAI,CAAE,MAAM,KAAK,EAAI,QAAO,aAAa;AACzC,QAAI,KAAK,WAAW,YAAY;AAC9B,YAAM,SAAS,KAAK,IAAI;AAAA,QACtB,UAAU,KAAK;AAAA,QACf,MAAM,KAAK;AAAA,QACX,WAAW;AAAA,QACX,IAAI,KAAK;AAAA,QACT,IAAI,KAAK,aAAa,IAAI;AAAA,MAC5B,CAAC;AACH,WAAO;AAAA,EACT;AACA,SAAO;AAAA,IACL,WAAW,gBAAgB;AAAA,MACzB,MAAM;AAAA,MACN,OAAO;AAAA,QACL,KAAK,UAAU;AAAA,UACb,MAAM,WAAW,MAAM,KAAK,SAAS;AACnC,kBAAM,OAAO,MAAM,IAAI,SAAS,SAAS,IAAI,gBAAgB,OAAO;AACpE,kBAAM,SAAS,MAAM,KAAK,UAAU,CAAC;AACrC,gBAAI,CAAC,OAAO,OAAQ,QAAO;AAC3B,kBAAM,SAAS,MAAM,IAAI,SAAS,WAAW,OAAO,IAAI,SAAS,CAAC;AAClE,kBAAM,OAAO,QAAQ;AACrB,uBAAW,SAAS,MAAM,SAAS,QAAQ,OAAO,MAAM,OAAO,GAAG;AAChE,oBAAM,QAAQ,MAAM,OAAO,OAAO,MAAM,KAAK,OAAO;AACpD,kBAAI,MAAO,QAAO,EAAE,MAAM;AAAA,YAC5B;AACA,mBAAO;AAAA,UACT;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF,CAAC;AAAA,IACD,KAAK,MAAM;AACT,kBAAY,IAAI;AAChB,iBAAW,QAAQ,KAAK,UAAU,MAAM,SAAS,CAAC;AAChD,YAAI,KAAK,SAAS,gBAAgB,KAAK,UAAU,aAAa,KAAK,OAAO;AACxE,gBAAM,MAAM,KAAK,IAAI,KAAK,OAAO,IAAI;AAAA,IAC3C;AAAA,EACF;AACF;",
6
6
  "names": ["record"]
7
7
  }
package/dist/main.js CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  locateWasm,
23
23
  sandboxFactory
24
24
  } from "./codemode/index.js";
25
+ import { codexFetch } from "./codex.js";
25
26
  import { attachFreezer } from "./freezer/index.js";
26
27
  import { lateGelatiere } from "./gelatiere/index.js";
27
28
  import { hostAgent } from "./host.js";
@@ -197,11 +198,13 @@ async function modelsOf(scope, options, given) {
197
198
  );
198
199
  await models.refresh({ allowNetwork: false }).catch(() => void 0);
199
200
  const bases = () => modelBases(providers, models, fields.env);
200
- scope.fetch = vaultFetch(
201
- vault,
202
- transport,
203
- modelRoute(bases, () => records.state.value),
204
- records.rejecting
201
+ scope.fetch = codexFetch(
202
+ vaultFetch(
203
+ vault,
204
+ transport,
205
+ modelRoute(bases, () => records.state.value),
206
+ records.rejecting
207
+ )
205
208
  );
206
209
  const endpoints = vaultEndpoints(vault);
207
210
  const defaultModel = async () => {
@@ -266,7 +269,8 @@ async function settingsOf(made, mcp, moved) {
266
269
  const settings = await createAgentSettings(models, { records, fields }, providers, {
267
270
  mcp,
268
271
  moved: keys,
269
- reach
272
+ reach,
273
+ vault: moved.vault
270
274
  });
271
275
  settings.state.subscribe(() => void moved.licks.flush(BACKGROUND_CONTEXT).catch(() => void 0));
272
276
  return settings;