@ai-ecoverse/slicc-spectrum 2.1.0 → 2.2.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/README.md +18 -2
- package/dist/slicc-dummy.js +314 -6
- package/dist/slicc-dummy.js.map +3 -3
- package/dist/slicc-ui.js +4180 -3842
- package/dist/slicc-ui.js.map +4 -4
- package/dist/types/app/approvals.d.ts +14 -1
- package/dist/types/app/command-display.d.ts +1 -0
- package/dist/types/app/command.d.ts +1 -0
- package/dist/types/app/secrets.d.ts +2 -1
- package/dist/types/app/vault.d.ts +29 -1
- package/dist/types/dummy/approvals.d.ts +5 -3
- package/dist/types/dummy/vault.d.ts +20 -1
- package/dist/types/dummy.d.ts +1 -1
- package/dist/types/model/types.d.ts +103 -4
- package/dist/types/ui.d.ts +3 -3
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -335,6 +335,11 @@ Sudo and guest requests show the verdict under the command, in a live region (`.
|
|
|
335
335
|
Focus starts on Deny (on Close once `decide` rejects) and goes back, after Deny, Always allow, Allow once, <kbd>Esc</kbd> or Close, to what had it when the dialog opened; inside a terminal, to that `<slicc-terminal>` through its own `focus()`, even when it remounted meanwhile.
|
|
336
336
|
Guest requests (`guest-message`, `guest-tool`) get their own copy, the guest's text as plain text (`[data-guest-text]`), attachments (`[data-attachments]`), the automatic check's verdict (`[data-verdict]`), and Deny and Allow once, plus **Always allow** (`data-action="always"`) with a hint saying what it covers when the approval's `always` is `true`; the dialog body carries `[data-approval-kind]` and fills the dialog's width at every size.
|
|
337
337
|
Tray requests (`tray`) get their own title and one sentence (`#asking`), and only **Deny** (`deny`, focused) and **Allow once** (`once`), never Always allow.
|
|
338
|
+
Secret requests (`secret`, `dialog[data-kind=secret]`) read "Enter a value for {name}?" (`mode: 'enter'`) or "Reveal {name} to a program?" (`reveal`). Under the title comes the trusted block, which only bios and the kernel fill: "{name} ran this[ in {where}]:" for an agent or "You ran this[ in {where}] (as {name}):" for a user, then the command (`[data-command]`), which is `process.exe` followed by `argv[1…]`, each word shell-quoted (`argv[0]` is the program's own choice), then "In {cwd}". The command follows sudo's rules: the same scrolling box, the same control-character chips, and the same end-in-view rule. Then the untrusted `prompt`, if any, in its own block (`[data-prompt]`) under "From the agent, not checked by SLICC:", with control characters as chips and cut at 200 characters. With `reveal`, a negative block (`[data-reveal]`) says "{exe} gets the plaintext. It can send it anywhere.", and adds "The agent asked for this." when `requester.kind` is `agent`.
|
|
339
|
+
The body is the vault's own frame (`[data-value-frame]`, a `<slicc-vault-value>` on `vault.valueFrame(id)`). The value is typed and Save or Allow clicked there, so spectrum never sees the value and can't fake the consent. The frame is `inert` until the end of the command has been in view, and "Scroll to the end of the command to answer it." (`[data-allow-hint]`) shows while it isn't. Without `vault.valueFrame`, "The vault isn’t available here, so this request can only be denied." (`[data-id="no-frame"]`) stands in its place.
|
|
340
|
+
The title shows control characters in the name as chips, and so does the notice. The only button is **Deny** (`deny`, focused). The element's `decide()` takes only `deny` for a secret, and only while that request is the one showing: not once it was denied, answered in its frame, closed, or ended (timed out, answered elsewhere, withdrawn). A late `escape` from the frame does nothing either.
|
|
341
|
+
When a listed request is replaced (the same `id` with another `createdAt`, command, mode, name, `reveal`, prompt or requester), or an `id` comes back after it left `list()`, the dialog counts it as a new request: a new frame, the end of its command unseen again, and the half-second guard again. Escape on the dialog denies, and so does the frame's `escape` event, posted when Escape is pressed inside the frame. The frame's `done` for this request closes the dialog and moves on to the next request, with no "answered" dialog. The frame's `gone` shows "Secret request timed out" (`expired`) or "Answered elsewhere", or closes silently (`withdrawn`), except for a request this tab denied or answered. The frame's `error` shows its message under the frame (`[data-frame-error]`). A secret request that leaves `list()` without `ended` before `expiresAt` closes silently, because the frame's `done` and the list change can arrive in either order. Focus then goes back as for sudo, to the `<slicc-terminal>` that had it.
|
|
342
|
+
`approvalKinds` (from `/ui`) is a frozen array of the kinds this build shows (`sudo`, `guest-message`, `guest-tool`, `tray`, `secret`), so an embedder can hold back the kinds an older UI would leave waiting unseen.
|
|
338
343
|
|
|
339
344
|
#### `<slicc-follow>`
|
|
340
345
|
|
|
@@ -480,7 +485,7 @@ The secrets, sorted by name (`li[data-secret][data-state]`, with `[data-rules]`,
|
|
|
480
485
|
Locked, every form (Add secret, Replace value, Import, Export) says "Unlock the vault first" (`p[data-locked]`) next to the vault's own small **Unlock** button (`slicc-vault-button[data-action=unlock]`) and disables its submit; once unlocked, the form stays open with what was typed and its submit enables.
|
|
481
486
|
Vault errors show their message, EACCES as "Unlock the vault first", an EACCES with `cause: 'policy-paused'` as "Secrets are paused until the missing policy file is back", and ENOTSUP as "This arrives with a later vault version".
|
|
482
487
|
Each action shows only when the port has its method.
|
|
483
|
-
Then **Rules SLICC refused** (`[data-refused]`), where the rules live (`[data-policy]`) and **Activity** (`ol.audit li[data-seq][data-type]`; an entry with `uid` and `tag` both `null` reads "by you", a `refresh` entry "Refreshed automatically" (with the sign-in it used), an `endpoints` entry (vault-originated, no "by …") one part per field it carries: (1.91.0's `EndpointsEntry` had a required `purged` with `url`; no vault wrote it before 1.13.0, so the shape was corrected rather than kept, with `purged` optional and `host` replacing `url`) `purged` "Removed custom model URLs an agent set, automatically: {provider name} at {host}, …. Only you can set a model URL now, in Settings › Accounts" (the provider's name from `settings.accounts()`, else its id), `adopted` "Kept the MCP servers your tokens use, automatically: {server} at {host}, …", `pinned` "{name} pinned to {host} automatically, on first use" with **Unpin** (`data-action=unpin`, calling the vault port's optional `unpin([name])`; disabled while the vault is locked; a failure shows in the row as `[data-error=unpin]`, in the same words as other vault failures; it's hidden once a newer `unpinned` entry for that name is in the log), and `unpinned` "{name} unpinned from {host}; the next use pins it again", an `mcp` entry "Sent to the MCP server SERVER at DOMAIN" with its request and exit, a `session` entry (a value the vault handed the kernel for a verified TLS session, vault 1.15.0) "Sent over {HTTP/1.1, SMTP or Postgres} to {domain}:{port}, {trusted by public roots, or trusted by its pinned anchor}, through {exit}" (no trust part when an older entry has no `verifiedBy` or a newer vault sends one spectrum doesn't know; a protocol it doesn't know reads as its raw name), and a policy entry lists its changes, its warning and, when accepted, how to change the file back, except that an entry with `absent` reads "{file} was removed. Secrets are paused until it’s back." and the accepted entry that ends its pause reads "{file} is back. Secrets are running again." ("Secrets stay paused until the other file is back." while the other file is still missing), neither with the way back; **Only reveals to programs**, `data-action="reveals"`, and **Show older**, `data-action="older"`).
|
|
488
|
+
Then **Rules SLICC refused** (`[data-refused]`), where the rules live (`[data-policy]`) and **Activity** (`ol.audit li[data-seq][data-type]`; an entry with `uid` and `tag` both `null` reads "by you", a `refresh` entry "Refreshed automatically" (with the sign-in it used), an `endpoints` entry (vault-originated, no "by …") one part per field it carries: (1.91.0's `EndpointsEntry` had a required `purged` with `url`; no vault wrote it before 1.13.0, so the shape was corrected rather than kept, with `purged` optional and `host` replacing `url`) `purged` "Removed custom model URLs an agent set, automatically: {provider name} at {host}, …. Only you can set a model URL now, in Settings › Accounts" (the provider's name from `settings.accounts()`, else its id), `adopted` "Kept the MCP servers your tokens use, automatically: {server} at {host}, …", `pinned` "{name} pinned to {host} automatically, on first use" with **Unpin** (`data-action=unpin`, calling the vault port's optional `unpin([name])`; disabled while the vault is locked; a failure shows in the row as `[data-error=unpin]`, in the same words as other vault failures; it's hidden once a newer `unpinned` entry for that name is in the log), and `unpinned` "{name} unpinned from {host}; the next use pins it again", an `mcp` entry "Sent to the MCP server SERVER at DOMAIN" with its request and exit, a `reveal` entry (vault 1.23.0, a secret ask whose plaintext the user allowed) "Revealed to {argv} in {cwd}, allowed in a dialog", (the argv, cwd and "by …" of `materialize` and `reveal` entries, and of a secret's "Revealed to programs…, last by … with {argv}" line, show control characters as chips, as the approval dialog does), which counts as a reveal for **Only reveals to programs** and for the secret's "Revealed to programs…" line, a policy change of `[reveal] agent` "Programs an agent runs may no longer ask for a secret’s plaintext" (or "…may ask for a secret’s plaintext again, in a dialog each time"), a `session` entry (a value the vault handed the kernel for a verified TLS session, vault 1.15.0) "Sent over {HTTP/1.1, SMTP or Postgres} to {domain}:{port}, {trusted by public roots, or trusted by its pinned anchor}, through {exit}" (no trust part when an older entry has no `verifiedBy` or a newer vault sends one spectrum doesn't know; a protocol it doesn't know reads as its raw name), and a policy entry lists its changes, its warning and, when accepted, how to change the file back, except that an entry with `absent` reads "{file} was removed. Secrets are paused until it’s back." and the accepted entry that ends its pause reads "{file} is back. Secrets are running again." ("Secrets stay paused until the other file is back." while the other file is still missing), neither with the way back; **Only reveals to programs**, `data-action="reveals"`, and **Show older**, `data-action="older"`).
|
|
484
489
|
The section's `data-vault` is `locked`, `unlocked` or `loading`.
|
|
485
490
|
A popup the browser blocked shows how to allow it (`[data-popup=blocked]`).
|
|
486
491
|
Widened-rule notices aren't shown.
|
|
@@ -490,8 +495,14 @@ An open **Removed** list (`details[data-removed]`) stays open across a layout sw
|
|
|
490
495
|
|
|
491
496
|
Places `vault.button()`'s element (the vault's own button frame), posts its `config` (`ButtonConfig` plus `theme`) once the button is `ready`, sets the frame's `color-scheme` to `theme` (or leaves it to the page) so the frame stays transparent, takes the reported `size` (with the focus-ring `inset` as negative margin) and `label` (as its title), moves focus to the control before or after it when the frame reports `focus` (`previous` or `next`), and fires `vault-popup` (bubbles, composed) with each `popup` event's `{ intent, state }`. spectrum sends only the `unlock` and `protect` intents.
|
|
492
497
|
|
|
498
|
+
#### `<slicc-vault-value>`
|
|
499
|
+
|
|
500
|
+
Places `vault.valueFrame(request)`'s element (the vault's `value.html` frame) and, once the frame is `ready`, posts `{ type: 'slicc-vault', v: 1, op: 'value', request, config }`, where `config` is a `ValueConfig` (`theme`, `dir`, `lang`, `scale`). It posts again when `config` changes, and mounts a new frame when `vault` or `request` changes. It sets the frame's `color-scheme` to `theme`. From `size` it takes the height, at most 480 px (and at most `45dvh`, beyond which the frame scrolls), and makes the frame its own width, with the `inset` (at most 4 px) as a negative margin above and below only, so the dialog never scrolls sideways. A value that isn't a finite number counts as 0. The element clips the frame to its own box (with room above and below for the largest inset it allows, 4 px, for the focus ring), so a frame can't paint over the command above it or the dialog's buttons. On the frame's `focus` event it moves focus to the control after or before the frame (`next` or `previous`), within its `<dialog>` and wrapping around. `focus()` posts `{ op: 'focus' }`, which focuses the frame's first control. It fires `vault-value` (bubbles, composed) with each `done`, `gone`, `error` and `escape` event. The detail holds only `event`, `request`, `state`, `code` and `message` (each when it's a string), so anything else the frame sends goes no further.
|
|
501
|
+
|
|
493
502
|
#### `<slicc-value-request>`
|
|
494
503
|
|
|
504
|
+
For the older `value` requests only, until bios stops sending them: `ask` requests show in `<slicc-approvals>` instead.
|
|
505
|
+
|
|
495
506
|
The vault's waiting `value` requests (`fnox set` on a terminal) as a modal dialog, one at a time, oldest first, with "N more waiting"; `<slicc-app>` mounts it.
|
|
496
507
|
`dialog[data-request]`: "Enter a value for NAME" (a Name field when the request has no name), who asks, as on the Settings card ("docs-pass asks for NAME", or "A program running as uid N asks for NAME"), the prompt under "From the agent, not checked by SLICC:", a masked value field with the same rules as Settings › Secrets, "Expires in N min", and **Later** (`data-action="later"`, or Escape: closes, the request stays in Settings › Secrets, whose **Enter value** reopens it), **Cancel request** (`data-action="cancel-request"`, with `cancel`) and **Save** (`data-action="save"`, `put(name, value, request.id)`, with `put`).
|
|
497
508
|
Locked, it shows the vault's Unlock button and disables Save.
|
|
@@ -967,6 +978,9 @@ It has no `always`: the dialog never offers Always allow for it, even when the i
|
|
|
967
978
|
Its dialog reads "Share this seven’s join URL?" with "{name} wants this seven’s join URL. Anyone with it can ask to link to this seven.", "Make a new join URL?" with "{name} wants a new join URL. Linked devices move to it, and the old one stops working.", or "Unlink {device.name}?" with "{name} wants to unlink {device.name} from this seven." ("Unlink a device?" and "…unlink a device from this seven." without `device`), then the countdown, **Deny** and **Allow once**.
|
|
968
979
|
Ended tray requests read "Answered in another tab" or "Tray request timed out", and a withdrawn one closes silently.
|
|
969
980
|
Without `decide`, its notice reads "Waiting for tray approval" with the same sentence, then "It waits for the owner's approval. Denied automatically at {time}."
|
|
981
|
+
A secret request (`kind` `secret`, a `SecretApproval`) is a program asking for a secret through `/dev/sealed`. Its fields are `mode` (`enter` when there is no live secret of that name, `reveal` when there is), `name`, an optional `prompt` (the program's own words, ≤ 200 characters, untrusted), `reveal` (the process gets the plaintext back too), `requester` (`kind` `agent` or `user`, `name`, optional `where`), `process` (`exe`, the resolved program file; `argv`; `cwd`, all from the kernel), `createdAt` and `expiresAt`.
|
|
982
|
+
`decide(id, 'deny')` cancels it in the vault, and the UI never sends `once` or `always` for it: the answer is given in the vault's frame. Send `ended` (`answered`, `expired` or `withdrawn`) only for a request that ended outside this tab: not for one this tab denied, and not for one whose frame in this tab posted `done`.
|
|
983
|
+
Without `decide`, its notice (`[data-approval-kind=secret]`) reads "Waiting for a secret" with "{name} ran {exe}, which asks for a value for {secret}." ("You ran…" for a user, "…the plaintext of {secret}." for `reveal`), then "It waits for the owner's answer. Denied automatically at {time}."
|
|
970
984
|
An approval whose `kind` this build doesn't know shows neither a dialog nor a notice, so it waits until it ends and is never allowed from here.
|
|
971
985
|
|
|
972
986
|
#### `vault` (optional)
|
|
@@ -977,10 +991,12 @@ The vault's view for SLICC, typed after `@ai-ecoverse/slicc-vault` (spectrum re-
|
|
|
977
991
|
Emit `status` and `list` when they change (bios re-lists after the vault's `changed` events), and `availability` when the optional `availability()` (`connecting`, `waiting`, `locked` or `ready`) changes.
|
|
978
992
|
`audit(before?, limit?)` resolves with an older `AuditPage` (`before` is a `seq`).
|
|
979
993
|
The optional page ops, each hiding its control when missing: `put(name, value, request?)` (a value request's `put` must use its `name`), `cancel(request)`, `remove(name, kind?)`, `restore(name, kind?)`, `import(kind, bytes, { password?, pick? })` (a `pick` stage, then `done`), `export(passphrase)` (the bundle's bytes), `lock()`, `dismissBackupReminder()` and `endpoints({ add?, remove? })`, which registers or takes back the URLs a page-sealed credential may be filled on (each an `EndpointSet`, `{ model?: [{ name, url }], mcp?: [{ server, url }] }`, a model URL bound to one record by name), checks the whole change first (`EINVAL` changes nothing) and rejects with a sentence. Settings registers them itself, never through the agent. `settings(name, kind, settings)` replaces a record's tenant settings as a whole without touching its key (the vault's `put` without a value); `put`'s options take the same `settings` on a first save.
|
|
980
|
-
They reject with a `VaultFailure`, an `Error` with an `ErrorCode` `code` (`isVaultFailure` from `/ui`) and an optional `ErrorCause` `cause`, matching slicc-vault 1.22.0’s stable refusal causes; `isPolicyPaused` still identifies only `policy-paused`.
|
|
994
|
+
They reject with a `VaultFailure`, an `Error` with an `ErrorCode` `code` (`isVaultFailure` from `/ui`) and an optional `ErrorCause` `cause`, matching slicc-vault 1.22.0’s stable refusal causes (checked against the pinned 1.23.0, whose ask types `compat/vault.ts` also checks: `Requester`, `ValueConfig`, `ValueEvent` and, the other way, `ValueMessage`); `isPolicyPaused` still identifies only `policy-paused`.
|
|
981
995
|
Values go to the vault only through `put`, and never through another port.
|
|
982
996
|
`button()` returns a new `VaultButton`, `{ element, post(message), listen(fn) }` returning an unsubscribe: bios wraps the vault's `button.html` frame and its postMessage protocol, and spectrum only sends `ButtonMessage`s and receives `ButtonEvent`s.
|
|
983
997
|
The passkey lock (unlock and protect) happens in the vault's own popup, which only a click in that frame can open.
|
|
998
|
+
The optional `valueFrame(request)` returns a new `VaultValueFrame` for an `ask` request, `{ element, post(message), listen(fn) }` like `button()`. bios wraps the vault's `value.html` frame. spectrum sends only `ValueMessage`s (`op: 'value'` with the request and a `ValueConfig`, and `op: 'focus'`) and receives only `ValueEvent`s (`ready`, `size`, `focus`, `done { request, state: 'sealed' | 'allowed' }`, `gone { request, state }`, `error { code, message }`, `escape`). The value is typed in that frame, and a reveal is allowed only by a click there, so the page never holds the value or the consent.
|
|
999
|
+
An `ask` request in `status.waiting` also carries `mode`, `reveal` and `requester` (`{ kind, name?, where?, exe, argv, cwd, tty, stdin }`). Settings › Secrets lists it with Cancel request only, and `RequestState` includes `withdrawn`.
|
|
984
1000
|
`notices`, `acknowledge` and the `notice` event are still typed, and ignored.
|
|
985
1001
|
|
|
986
1002
|
#### `guests` (optional)
|
package/dist/slicc-dummy.js
CHANGED
|
@@ -148,6 +148,86 @@ function trayFixtures(scenario, now2) {
|
|
|
148
148
|
const id = scenario === "tray-expired" ? "tray-url" : scenario;
|
|
149
149
|
return all.filter((approval) => approval.id === id);
|
|
150
150
|
}
|
|
151
|
+
function secretFixtures(scenario, now2) {
|
|
152
|
+
const base = { createdAt: now2, expiresAt: now2 + 5 * minutes };
|
|
153
|
+
const enter = {
|
|
154
|
+
...base,
|
|
155
|
+
id: "ask-harbor",
|
|
156
|
+
kind: "secret",
|
|
157
|
+
mode: "enter",
|
|
158
|
+
name: "HARBOR_DEPLOY_TOKEN",
|
|
159
|
+
prompt: "Paste the harbor deploy token from the release checklist.",
|
|
160
|
+
reveal: false,
|
|
161
|
+
requester: { kind: "user", name: "cone", where: "Terminal 1" },
|
|
162
|
+
process: {
|
|
163
|
+
exe: "/usr/bin/fnox",
|
|
164
|
+
argv: ["fnox", "set", "HARBOR_DEPLOY_TOKEN"],
|
|
165
|
+
cwd: "/home/cone/harbor"
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
const reveal = {
|
|
169
|
+
...base,
|
|
170
|
+
id: "ask-lagoon",
|
|
171
|
+
kind: "secret",
|
|
172
|
+
mode: "reveal",
|
|
173
|
+
name: "LAGOON_API_KEY",
|
|
174
|
+
prompt: "The tide report script signs its uploads with this key.",
|
|
175
|
+
reveal: true,
|
|
176
|
+
requester: { kind: "agent", name: "docs-pass", where: "scoop:docs" },
|
|
177
|
+
process: {
|
|
178
|
+
exe: "/usr/bin/sealed",
|
|
179
|
+
argv: ["sealed", "ask", "LAGOON_API_KEY", "--reveal"],
|
|
180
|
+
cwd: "/home/cone/harbor"
|
|
181
|
+
},
|
|
182
|
+
createdAt: now2 + 1
|
|
183
|
+
};
|
|
184
|
+
if (scenario === "secret-enter") return [enter];
|
|
185
|
+
if (scenario === "secret-reveal") return [reveal];
|
|
186
|
+
if (scenario === "secret-long")
|
|
187
|
+
return [
|
|
188
|
+
{
|
|
189
|
+
...enter,
|
|
190
|
+
id: "ask-long",
|
|
191
|
+
reveal: true,
|
|
192
|
+
requester: { kind: "agent", name: "quiet-otter" },
|
|
193
|
+
prompt: `Paste it\u202Eevil\r${"and wait. ".repeat(30)}`,
|
|
194
|
+
process: {
|
|
195
|
+
exe: "/usr/bin/sh",
|
|
196
|
+
argv: [
|
|
197
|
+
"sh",
|
|
198
|
+
"-c",
|
|
199
|
+
[longCommand, longCommand, longCommand].join(" && \\\n"),
|
|
200
|
+
"tide\rreport"
|
|
201
|
+
],
|
|
202
|
+
cwd: "/home/cone/harbor\rferry"
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
];
|
|
206
|
+
return [enter, reveal];
|
|
207
|
+
}
|
|
208
|
+
function pendingAsk(approval) {
|
|
209
|
+
const { kind, name: name2, where } = approval.requester;
|
|
210
|
+
return {
|
|
211
|
+
id: approval.id,
|
|
212
|
+
kind: "ask",
|
|
213
|
+
name: approval.name,
|
|
214
|
+
...approval.prompt ? { prompt: approval.prompt } : {},
|
|
215
|
+
mode: approval.mode,
|
|
216
|
+
reveal: approval.reveal,
|
|
217
|
+
requester: {
|
|
218
|
+
kind,
|
|
219
|
+
name: name2,
|
|
220
|
+
...where ? { where } : {},
|
|
221
|
+
...approval.process,
|
|
222
|
+
tty: kind === "user" ? "/dev/pts/1" : null,
|
|
223
|
+
stdin: kind === "user" ? "tty" : "pipe"
|
|
224
|
+
},
|
|
225
|
+
uid: 1e3,
|
|
226
|
+
tag: kind,
|
|
227
|
+
createdAt: approval.createdAt,
|
|
228
|
+
expiresAt: approval.expiresAt
|
|
229
|
+
};
|
|
230
|
+
}
|
|
151
231
|
function levelFixture(scenario, now2) {
|
|
152
232
|
const base = {
|
|
153
233
|
id: `sudo-${scenario}`,
|
|
@@ -296,6 +376,7 @@ function approvalFixtures(scenario, now2 = Date.now()) {
|
|
|
296
376
|
if (scenario === "guest" || scenario === "guest-follower") return guestFixtures(now2);
|
|
297
377
|
if (scenario === "guest-always") return guestAlways(now2);
|
|
298
378
|
if (scenario.startsWith("tray")) return trayFixtures(scenario, now2);
|
|
379
|
+
if (scenario.startsWith("secret-") || scenario === "secret") return secretFixtures(scenario, now2);
|
|
299
380
|
if (scenario.startsWith("guest-")) return [guestCall(scenario, now2)];
|
|
300
381
|
if (scenario === "none") return [];
|
|
301
382
|
if (scenario === "long")
|
|
@@ -355,12 +436,20 @@ var DummyApprovals = class extends Emitter {
|
|
|
355
436
|
#approvals;
|
|
356
437
|
#time;
|
|
357
438
|
#fail;
|
|
439
|
+
#vault;
|
|
440
|
+
#quiet = /* @__PURE__ */ new Set();
|
|
358
441
|
decisions = [];
|
|
359
442
|
decide;
|
|
360
|
-
constructor(scenario, time) {
|
|
443
|
+
constructor(scenario, time, vault) {
|
|
361
444
|
super();
|
|
362
445
|
this.#approvals = approvalFixtures(scenario);
|
|
363
446
|
this.#time = time;
|
|
447
|
+
this.#vault = vault;
|
|
448
|
+
if (vault) {
|
|
449
|
+
for (const approval of this.#approvals)
|
|
450
|
+
if (approval.kind === "secret") vault.hold(pendingAsk(approval));
|
|
451
|
+
vault.on("status", (status2) => this.#left(status2.waiting));
|
|
452
|
+
}
|
|
364
453
|
this.#fail = scenario === "fail";
|
|
365
454
|
if (scenario !== "follower" && scenario !== "guest-follower")
|
|
366
455
|
this.decide = (id, decision) => this.#decide(id, decision);
|
|
@@ -370,10 +459,24 @@ var DummyApprovals = class extends Emitter {
|
|
|
370
459
|
void time.sleep(100).then(() => this.end("tray-url", "expired"));
|
|
371
460
|
}
|
|
372
461
|
raise(approval) {
|
|
462
|
+
if (approval.kind === "secret") this.#vault?.hold(pendingAsk(approval));
|
|
373
463
|
this.#approvals = [...this.#approvals.filter(({ id }) => id !== approval.id), approval];
|
|
374
464
|
this.emit("approvals", this.list());
|
|
375
465
|
}
|
|
466
|
+
#left(waiting2) {
|
|
467
|
+
const vault = this.#vault;
|
|
468
|
+
for (const approval of this.#approvals) {
|
|
469
|
+
if (approval.kind !== "secret" || waiting2.some(({ id }) => id === approval.id)) continue;
|
|
470
|
+
if (vault.answered.has(approval.id) || this.#quiet.has(approval.id)) this.#drop(approval.id);
|
|
471
|
+
else this.end(approval.id, "answered");
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
#drop(id) {
|
|
475
|
+
this.#approvals = this.#approvals.filter((approval) => approval.id !== id);
|
|
476
|
+
this.emit("approvals", this.list());
|
|
477
|
+
}
|
|
376
478
|
end(id, reason) {
|
|
479
|
+
if (!this.#approvals.some((approval) => approval.id === id)) return;
|
|
377
480
|
this.emit("ended", { id, reason });
|
|
378
481
|
this.#approvals = this.#approvals.filter((approval) => approval.id !== id);
|
|
379
482
|
this.emit("approvals", this.list());
|
|
@@ -385,7 +488,14 @@ var DummyApprovals = class extends Emitter {
|
|
|
385
488
|
await this.#time.sleep(10);
|
|
386
489
|
if (this.#fail) throw new Error("The kernel stopped waiting. Run the command again.");
|
|
387
490
|
if (id === "guest-late") throw new Error("Someone else already answered this.");
|
|
491
|
+
const approval = this.#approvals.find((item) => item.id === id);
|
|
492
|
+
if (approval?.kind === "secret" && decision !== "deny")
|
|
493
|
+
throw new Error("Save or Allow in the vault\u2019s frame.");
|
|
388
494
|
this.decisions.push([id, decision]);
|
|
495
|
+
if (approval?.kind === "secret" && this.#vault?.ask(id)) {
|
|
496
|
+
this.#quiet.add(id);
|
|
497
|
+
await this.#vault.cancel(id);
|
|
498
|
+
}
|
|
389
499
|
this.end(id, "answered");
|
|
390
500
|
}
|
|
391
501
|
};
|
|
@@ -6351,7 +6461,8 @@ function vaultAudit(now2 = Date.now()) {
|
|
|
6351
6461
|
secret: "HARBOR_API_KEY",
|
|
6352
6462
|
from: ["proxy", "device:link-pier"],
|
|
6353
6463
|
to: ["proxy", "tailscale", "device:link-pier"]
|
|
6354
|
-
}
|
|
6464
|
+
},
|
|
6465
|
+
{ kind: "reveal", field: "agent", from: true, to: false }
|
|
6355
6466
|
]
|
|
6356
6467
|
},
|
|
6357
6468
|
{
|
|
@@ -6750,6 +6861,151 @@ var DummyVaultButton = class {
|
|
|
6750
6861
|
this.#emit({ event: "popup", intent: resolved, state: "done" });
|
|
6751
6862
|
}
|
|
6752
6863
|
};
|
|
6864
|
+
var DummyValueFrame = class {
|
|
6865
|
+
element;
|
|
6866
|
+
request;
|
|
6867
|
+
config = null;
|
|
6868
|
+
messages = [];
|
|
6869
|
+
arming = 500;
|
|
6870
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
6871
|
+
#vault;
|
|
6872
|
+
#box;
|
|
6873
|
+
#view = "";
|
|
6874
|
+
#armed = false;
|
|
6875
|
+
#finished = false;
|
|
6876
|
+
#sizes = new ResizeObserver(() => this.#size());
|
|
6877
|
+
constructor(vault, time, request) {
|
|
6878
|
+
this.#vault = vault;
|
|
6879
|
+
this.request = request;
|
|
6880
|
+
this.element = document.createElement("div");
|
|
6881
|
+
this.element.dataset.vaultValue = "";
|
|
6882
|
+
this.#box = document.createElement("div");
|
|
6883
|
+
this.#box.style.padding = "2px";
|
|
6884
|
+
this.element.append(this.#box);
|
|
6885
|
+
this.element.addEventListener("keydown", (event) => {
|
|
6886
|
+
if (event.key !== "Escape" || event.isComposing) return;
|
|
6887
|
+
event.preventDefault();
|
|
6888
|
+
event.stopPropagation();
|
|
6889
|
+
this.#emit({ event: "escape" });
|
|
6890
|
+
});
|
|
6891
|
+
vault.on("status", () => this.#render());
|
|
6892
|
+
this.#sizes.observe(this.#box);
|
|
6893
|
+
void time.sleep(0).then(() => this.#emit({ event: "ready" }));
|
|
6894
|
+
}
|
|
6895
|
+
post(message) {
|
|
6896
|
+
this.messages.push(message);
|
|
6897
|
+
if (message.op === "focus") {
|
|
6898
|
+
this.#box.querySelector("input, swc-button")?.focus();
|
|
6899
|
+
return;
|
|
6900
|
+
}
|
|
6901
|
+
this.request = message.request;
|
|
6902
|
+
this.config = message.config;
|
|
6903
|
+
this.element.style.colorScheme = message.config.theme ?? "";
|
|
6904
|
+
this.#render();
|
|
6905
|
+
}
|
|
6906
|
+
listen(listener) {
|
|
6907
|
+
this.#listeners.add(listener);
|
|
6908
|
+
return () => this.#listeners.delete(listener);
|
|
6909
|
+
}
|
|
6910
|
+
#emit(event) {
|
|
6911
|
+
const message = { type: "slicc-vault", v: 1, ...event };
|
|
6912
|
+
for (const listener of [...this.#listeners]) listener(message);
|
|
6913
|
+
}
|
|
6914
|
+
#size() {
|
|
6915
|
+
const { width, height } = this.#box.getBoundingClientRect();
|
|
6916
|
+
this.#emit({ event: "size", width: Math.ceil(width), height: Math.ceil(height), inset: 2 });
|
|
6917
|
+
}
|
|
6918
|
+
#render() {
|
|
6919
|
+
if (!this.config || this.#finished) return;
|
|
6920
|
+
const ask2 = this.#vault.ask(this.request);
|
|
6921
|
+
if (!ask2) {
|
|
6922
|
+
this.#finished = true;
|
|
6923
|
+
this.#box.replaceChildren();
|
|
6924
|
+
this.#emit({ event: "gone", request: this.request, state: "cancelled" });
|
|
6925
|
+
return;
|
|
6926
|
+
}
|
|
6927
|
+
const view = this.#vault.status()?.locked ? "locked" : ask2.mode ?? "enter";
|
|
6928
|
+
if (view === this.#view) return;
|
|
6929
|
+
if (view !== "locked" && (this.#view === "" || this.#view === "locked"))
|
|
6930
|
+
setTimeout(() => {
|
|
6931
|
+
this.#armed = true;
|
|
6932
|
+
this.#arm();
|
|
6933
|
+
}, this.arming);
|
|
6934
|
+
this.#view = view;
|
|
6935
|
+
this.#box.replaceChildren(...this.#controls(view, ask2));
|
|
6936
|
+
this.#arm();
|
|
6937
|
+
this.#size();
|
|
6938
|
+
}
|
|
6939
|
+
#arm() {
|
|
6940
|
+
for (const button of this.#box.querySelectorAll("[data-action=save], [data-action=allow]"))
|
|
6941
|
+
button.toggleAttribute("disabled", !this.#armed);
|
|
6942
|
+
}
|
|
6943
|
+
#button(action, label2, variant, click) {
|
|
6944
|
+
const button = document.createElement("swc-button");
|
|
6945
|
+
button.dataset.action = action;
|
|
6946
|
+
button.setAttribute("variant", variant);
|
|
6947
|
+
button.textContent = label2;
|
|
6948
|
+
button.addEventListener("click", click);
|
|
6949
|
+
return button;
|
|
6950
|
+
}
|
|
6951
|
+
#controls(view, ask2) {
|
|
6952
|
+
const text = document.createElement("p");
|
|
6953
|
+
text.style.margin = "0 0 8px";
|
|
6954
|
+
const problem = document.createElement("p");
|
|
6955
|
+
problem.dataset.problem = "";
|
|
6956
|
+
problem.style.margin = "8px 0 0";
|
|
6957
|
+
if (view === "locked") {
|
|
6958
|
+
text.textContent = "Unlock the vault to answer.";
|
|
6959
|
+
return [
|
|
6960
|
+
text,
|
|
6961
|
+
this.#button("unlock", "Unlock", "accent", () => {
|
|
6962
|
+
void this.#vault.answer({ kind: "unlock" });
|
|
6963
|
+
})
|
|
6964
|
+
];
|
|
6965
|
+
}
|
|
6966
|
+
if (view === "reveal") {
|
|
6967
|
+
text.textContent = `${ask2.requester?.exe} gets the plaintext of ${ask2.name}`;
|
|
6968
|
+
return [
|
|
6969
|
+
text,
|
|
6970
|
+
this.#button("allow", "Allow", "negative", () => void this.#answer(problem, "")),
|
|
6971
|
+
problem
|
|
6972
|
+
];
|
|
6973
|
+
}
|
|
6974
|
+
const label2 = document.createElement("label");
|
|
6975
|
+
label2.textContent = `Value for ${ask2.name}`;
|
|
6976
|
+
label2.style.display = "grid";
|
|
6977
|
+
const input = document.createElement("input");
|
|
6978
|
+
input.type = "password";
|
|
6979
|
+
input.dataset.field = "value";
|
|
6980
|
+
input.autocomplete = "off";
|
|
6981
|
+
label2.append(input);
|
|
6982
|
+
text.append(label2);
|
|
6983
|
+
return [
|
|
6984
|
+
text,
|
|
6985
|
+
this.#button("save", "Save", "accent", () => {
|
|
6986
|
+
const value = input.value;
|
|
6987
|
+
input.value = "";
|
|
6988
|
+
void this.#answer(problem, value);
|
|
6989
|
+
}),
|
|
6990
|
+
problem
|
|
6991
|
+
];
|
|
6992
|
+
}
|
|
6993
|
+
async #answer(problem, value) {
|
|
6994
|
+
const reveal = this.#view === "reveal";
|
|
6995
|
+
problem.textContent = "";
|
|
6996
|
+
this.#finished = true;
|
|
6997
|
+
try {
|
|
6998
|
+
if (reveal) await this.#vault.allow(this.request);
|
|
6999
|
+
else await this.#vault.seal(this.request, value);
|
|
7000
|
+
this.#emit({ event: "done", request: this.request, state: reveal ? "allowed" : "sealed" });
|
|
7001
|
+
} catch (error) {
|
|
7002
|
+
this.#finished = false;
|
|
7003
|
+
const { code, message } = error;
|
|
7004
|
+
if (code === "EINVAL") problem.textContent = message;
|
|
7005
|
+
else this.#emit({ event: "error", code, message });
|
|
7006
|
+
}
|
|
7007
|
+
}
|
|
7008
|
+
};
|
|
6753
7009
|
var DummyVault = class extends Emitter {
|
|
6754
7010
|
#status;
|
|
6755
7011
|
#list;
|
|
@@ -6759,6 +7015,9 @@ var DummyVault = class extends Emitter {
|
|
|
6759
7015
|
#older = vaultOlder();
|
|
6760
7016
|
#serial = 0;
|
|
6761
7017
|
buttons = [];
|
|
7018
|
+
frames = [];
|
|
7019
|
+
answered = /* @__PURE__ */ new Set();
|
|
7020
|
+
revealed = [];
|
|
6762
7021
|
puts = [];
|
|
6763
7022
|
endpointChanges = [];
|
|
6764
7023
|
settingsCalls = [];
|
|
@@ -7033,6 +7292,55 @@ var DummyVault = class extends Emitter {
|
|
|
7033
7292
|
this.buttons.push(button);
|
|
7034
7293
|
return button;
|
|
7035
7294
|
}
|
|
7295
|
+
valueFrame(request) {
|
|
7296
|
+
const frame = new DummyValueFrame(this, this.#time, request);
|
|
7297
|
+
this.frames.push(frame);
|
|
7298
|
+
return frame;
|
|
7299
|
+
}
|
|
7300
|
+
hold(request) {
|
|
7301
|
+
this.#status = {
|
|
7302
|
+
...this.#status,
|
|
7303
|
+
waiting: [...this.#status.waiting.filter(({ id }) => id !== request.id), request]
|
|
7304
|
+
};
|
|
7305
|
+
this.emit("status", this.#status);
|
|
7306
|
+
}
|
|
7307
|
+
ask(request) {
|
|
7308
|
+
return this.#status.waiting.find(({ id, kind }) => id === request && kind === "ask");
|
|
7309
|
+
}
|
|
7310
|
+
#asked(request) {
|
|
7311
|
+
this.#unlocked();
|
|
7312
|
+
const found = this.ask(request);
|
|
7313
|
+
if (!found) throw vaultFailure("ENOENT", "That request is no longer waiting.");
|
|
7314
|
+
return found;
|
|
7315
|
+
}
|
|
7316
|
+
async seal(request, value) {
|
|
7317
|
+
await this.#time.sleep(5);
|
|
7318
|
+
const found = this.#asked(request);
|
|
7319
|
+
const problem = valueProblem(value);
|
|
7320
|
+
if (problem) throw vaultFailure("EINVAL", problem);
|
|
7321
|
+
this.answered.add(request);
|
|
7322
|
+
this.#take(request);
|
|
7323
|
+
return this.#seal(found.name, "settings");
|
|
7324
|
+
}
|
|
7325
|
+
async allow(request) {
|
|
7326
|
+
await this.#time.sleep(5);
|
|
7327
|
+
const found = this.#asked(request);
|
|
7328
|
+
this.answered.add(request);
|
|
7329
|
+
this.revealed.push(request);
|
|
7330
|
+
this.#take(request);
|
|
7331
|
+
const { exe, argv, cwd, kind } = found.requester;
|
|
7332
|
+
this.#log(found.name, {
|
|
7333
|
+
type: "reveal",
|
|
7334
|
+
uid: found.uid,
|
|
7335
|
+
tag: found.tag,
|
|
7336
|
+
exe,
|
|
7337
|
+
argv: argv.join(" "),
|
|
7338
|
+
cwd,
|
|
7339
|
+
requester: kind,
|
|
7340
|
+
request
|
|
7341
|
+
});
|
|
7342
|
+
this.emit("list", this.#list);
|
|
7343
|
+
}
|
|
7036
7344
|
autoLabel() {
|
|
7037
7345
|
return this.#status.locked ? "Vault locked" : "Vault unlocked";
|
|
7038
7346
|
}
|
|
@@ -7175,6 +7483,7 @@ function createDummyModel({
|
|
|
7175
7483
|
if (older === "paged") agent.paginate(6, 4);
|
|
7176
7484
|
if (hold !== "off") agent.setHold({ reason: hold, held: 2 });
|
|
7177
7485
|
const noticePort = noticesPort(agent, time, notices, readonly, arrival);
|
|
7486
|
+
const vaultPort = vault === "off" ? void 0 : new DummyVault(vault, time, vaultRecords(accounts2, mcp));
|
|
7178
7487
|
if (scenario === "driving") void browser.run(drive);
|
|
7179
7488
|
const base = {
|
|
7180
7489
|
agent,
|
|
@@ -7217,11 +7526,9 @@ function createDummyModel({
|
|
|
7217
7526
|
...changes === "files" ? {} : { changes: new DummyChanges(files3, changes === "git") },
|
|
7218
7527
|
...noticePort ? { notices: noticePort } : {},
|
|
7219
7528
|
...tabs2 === "off" ? {} : { tabs: new DummyTabs(tabs2, time) },
|
|
7220
|
-
...approvals === "off" ? {} : { approvals: new DummyApprovals(approvals, time) },
|
|
7529
|
+
...approvals === "off" ? {} : { approvals: new DummyApprovals(approvals, time, vaultPort) },
|
|
7221
7530
|
...follow === "off" ? {} : { follow: new DummyFollow(follow, time) },
|
|
7222
|
-
...
|
|
7223
|
-
vault: new DummyVault(vault, time, vaultRecords(accounts2, mcp))
|
|
7224
|
-
},
|
|
7531
|
+
...vaultPort ? { vault: vaultPort } : {},
|
|
7225
7532
|
...guestsPort(guests, time)
|
|
7226
7533
|
};
|
|
7227
7534
|
base.vault?.on("status", (status2) => {
|
|
@@ -7244,6 +7551,7 @@ export {
|
|
|
7244
7551
|
DummyNotices,
|
|
7245
7552
|
DummyTabs,
|
|
7246
7553
|
DummyUpdates,
|
|
7554
|
+
DummyValueFrame,
|
|
7247
7555
|
DummyVault,
|
|
7248
7556
|
DummyVaultButton,
|
|
7249
7557
|
actionLimits,
|