@ai-ecoverse/slicc-spectrum 2.0.3 → 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 +21 -2
- package/dist/slicc-dummy.js +354 -9
- package/dist/slicc-dummy.js.map +3 -3
- package/dist/slicc-ui.js +4288 -3848
- package/dist/slicc-ui.js.map +4 -4
- package/dist/types/app/approvals.d.ts +14 -1
- package/dist/types/app/chat.d.ts +2 -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/agent.d.ts +6 -1
- package/dist/types/dummy/approvals.d.ts +5 -3
- package/dist/types/dummy/index.d.ts +2 -1
- package/dist/types/dummy/vault.d.ts +20 -1
- package/dist/types/dummy.d.ts +1 -1
- package/dist/types/model/types.d.ts +104 -4
- package/dist/types/ui.d.ts +3 -3
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -307,6 +307,7 @@ When a live agent's model, or `Settings.model`, is no longer offered and its pro
|
|
|
307
307
|
The chat re-runs this on `follow` and `readOnly` too, and `<slicc-app>` does it as well, when it binds, on those events and on `follow` and `readOnly`, so a new cone that starts on a provider that isn't set up moves at once.
|
|
308
308
|
The transcript pads its end, and its `scroll-padding-block-end`, by the composer's measured height plus the height its frosted layer fades out over (`--composer-fade`, 72 px, 0 under `prefers-reduced-transparency` and `forced-colors` and without a composer), so at the bottom the last line sits a line above the glass, and a focused element scrolls clear of it.
|
|
309
309
|
The log follows new content only while it sits within one line (its `line-height`) of the bottom. Scrolling up by wheel, touch, keys or the scrollbar stops that, and the log then keeps its place, whatever re-renders or ticks; content that grows above the view, such as a tool card opening, doesn't move what's in view either. New output while the reader is scrolled up shows **Jump to latest** (`data-action="jump-latest"`), floating over the log's bottom edge above the composer, with the number of new items, "Jump to latest (2 new)", once there are any; it, or scrolling back to the bottom, resumes following, and so does the reader's own send. While it follows, a log that gets shorter (a notice above it, a taller composer, a shorter panel) or a latest card that grows (a lick opened, a sprinkle loading) doesn't scroll that card's top out of view: a card that fits shows whole, and a taller one keeps its top at the top of the log, with the rest under the composer, instead of its end above the composer's padding; a new item, a streaming reply and Jump to latest still go to the very bottom.
|
|
310
|
+
With the agent port's optional `hasOlder(agentId)`, the log pages back through the conversation: while it is less than a screen from its top (which includes a log that doesn't fill the view) and `hasOlder` is true, it calls `older(agentId, id)` with the id of the first message in `messages()`, one load at a time per agent, and renders the earlier messages from `messages()` by id once the port emits `messages`, never from what `older` resolves with. The first turn in view keeps its place: its offset is measured before the render and put back before the next paint (scroll anchoring does nothing at the very top), and the log's follow bookkeeping moves with it, so the jump never reads as the reader scrolling. It loads until the log is more than a screen above the view or the start is reached; an empty answer, or one that leaves the first message as it was, is the start, and the chat doesn't ask again for that agent until its first message changes or it is shown again. While there is more, the top of the log reads "Loading earlier messages…" (`[data-older=loading]`, `aria-busy` while a load runs), and at the start it shows nothing; a rejected `older` shows "Couldn’t load earlier messages." with **Retry** (`[data-older=failed]`, `role="alert"`, `data-action="retry-older"`) instead of trying again. A load that ends after another agent is shown touches only its own conversation. Without `hasOlder` (an older agent), it never calls `older`.
|
|
310
311
|
While a reply streams, its status icon follows what it is doing: a pencil for an edit, a page for a read, a magnifier for a search, an eye for the browser and an hourglass for any other tool; thinking and text keep the AI logo.
|
|
311
312
|
Between the header and the transcript, a `<slicc-notices>` shows the model's notices.
|
|
312
313
|
On a follower without Control, or while the agent port's `readOnly()` returns a line, a note (`p[data-follow="watch"]`, that line first, else the follow-based one) takes the composer's place, the header shows the model's name instead of the pickers and New conversation, and there's no suggested follow-up.
|
|
@@ -334,6 +335,11 @@ Sudo and guest requests show the verdict under the command, in a live region (`.
|
|
|
334
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.
|
|
335
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.
|
|
336
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.
|
|
337
343
|
|
|
338
344
|
#### `<slicc-follow>`
|
|
339
345
|
|
|
@@ -479,7 +485,7 @@ The secrets, sorted by name (`li[data-secret][data-state]`, with `[data-rules]`,
|
|
|
479
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.
|
|
480
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".
|
|
481
487
|
Each action shows only when the port has its method.
|
|
482
|
-
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"`).
|
|
483
489
|
The section's `data-vault` is `locked`, `unlocked` or `loading`.
|
|
484
490
|
A popup the browser blocked shows how to allow it (`[data-popup=blocked]`).
|
|
485
491
|
Widened-rule notices aren't shown.
|
|
@@ -489,8 +495,14 @@ An open **Removed** list (`details[data-removed]`) stays open across a layout sw
|
|
|
489
495
|
|
|
490
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.
|
|
491
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
|
+
|
|
492
502
|
#### `<slicc-value-request>`
|
|
493
503
|
|
|
504
|
+
For the older `value` requests only, until bios stops sending them: `ask` requests show in `<slicc-approvals>` instead.
|
|
505
|
+
|
|
494
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.
|
|
495
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`).
|
|
496
508
|
Locked, it shows the vault's Unlock button and disables Save.
|
|
@@ -765,6 +777,7 @@ The optional `toolLabel(agentId, callIds)` resolves with a short label for a run
|
|
|
765
777
|
Spectrum asks once a reply's status has two calls and again each time it grows, once per set of ids, and shows the label once the status is complete.
|
|
766
778
|
The optional `readOnly()` returns a line, or `null`, with a `readOnly` event when it changes: while it's a non-empty string, such as when this follower runs a different major version from its owner, every surface is watch-only just as a follower without Control is (no composer, no cone or conversation actions, no pickers, no Stop, no Accept or Revert in Changes, inline sprinkles view only, no model settling), and the line takes the place of the follow-based watch-only note under the chat.
|
|
767
779
|
The optional `starting()` returns a line, or `null`, with a `starting` event when it changes: while it's a non-empty string, there's no agent yet but one is on its way (such as while seven installs its first agent, or replaces one), the chat drops its empty state and the composer queues what's sent until an agent is there (see `<slicc-composer>`); without it, or at `null`, nothing changes.
|
|
780
|
+
The optional `hasOlder(agentId)` says whether `older(agentId, before)` can load messages before the first one in `messages()`; `older` resolves once at least one earlier message has arrived in `messages()`, emitting `messages`, or with `[]` at the start, and the chat pages back with them (see `<slicc-chat>`).
|
|
768
781
|
|
|
769
782
|
#### `files`
|
|
770
783
|
|
|
@@ -965,6 +978,9 @@ It has no `always`: the dialog never offers Always allow for it, even when the i
|
|
|
965
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**.
|
|
966
979
|
Ended tray requests read "Answered in another tab" or "Tray request timed out", and a withdrawn one closes silently.
|
|
967
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}."
|
|
968
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.
|
|
969
985
|
|
|
970
986
|
#### `vault` (optional)
|
|
@@ -975,10 +991,12 @@ The vault's view for SLICC, typed after `@ai-ecoverse/slicc-vault` (spectrum re-
|
|
|
975
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.
|
|
976
992
|
`audit(before?, limit?)` resolves with an older `AuditPage` (`before` is a `seq`).
|
|
977
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.
|
|
978
|
-
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`.
|
|
979
995
|
Values go to the vault only through `put`, and never through another port.
|
|
980
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.
|
|
981
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`.
|
|
982
1000
|
`notices`, `acknowledge` and the `notice` event are still typed, and ignored.
|
|
983
1001
|
|
|
984
1002
|
#### `guests` (optional)
|
|
@@ -1099,6 +1117,7 @@ Import offers five invented candidates (one replaces a secret, one is a model ke
|
|
|
1099
1117
|
Its buttons are plain Spectrum buttons that answer at once: Unlock unlocks, and created buttons are in `model.vault.buttons`.
|
|
1100
1118
|
`vaultFixtures`, `DummyVault` and `DummyVaultButton` are exported from `/dummy`.
|
|
1101
1119
|
`createDummyModel({ hold: 'waiting' | 'locked' | 'off' })` (`?hold=` on `/ui/`) makes the agent hold two tool results; a `locked` hold clears when the dummy vault unlocks, and `model.agent.setHold(hold)` changes it.
|
|
1120
|
+
`createDummyModel({ older: 'paged' })` (`?older=paged` on `/ui/`, off by default) holds back all but the last six messages of every conversation, and the agent's `older` brings them back four at a time after a short wait (`model.agent.olderTicks`, 10 ticks); `model.agent.paginate(keep, size, ids?)` holds them back again for those agents (all by default), `failOlder(times)` makes the next loads reject, and `olderCalls` lists the agents each load was for. `hasOlder` is false wherever nothing is held back.
|
|
1102
1121
|
|
|
1103
1122
|
On phones the update status and the tray connection show only their status light (the tray's budget is hidden) and the network indicator a cloud icon, with the text in their labels and tooltips; the agent picker keeps room for the cone's name, cut with an ellipsis when long, and the bottom rail scrolls when needed.
|
|
1104
1123
|
On tablets the agent picker gives up room before the View menu does, so the header keeps the View menu and its last button in view from 640 to 1199 px, the tray's chips stay on one line each, the budget chip leaves out its window and a tray that isn't live shows only its state.
|
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
|
};
|
|
@@ -1916,6 +2026,11 @@ var DummyAgent = class extends Emitter {
|
|
|
1916
2026
|
#ice = /* @__PURE__ */ new Map();
|
|
1917
2027
|
#suggestions = /* @__PURE__ */ new Map();
|
|
1918
2028
|
#runs = /* @__PURE__ */ new Map();
|
|
2029
|
+
#earlier = /* @__PURE__ */ new Map();
|
|
2030
|
+
#page = 0;
|
|
2031
|
+
#failing = 0;
|
|
2032
|
+
olderTicks = 10;
|
|
2033
|
+
olderCalls = [];
|
|
1919
2034
|
#active;
|
|
1920
2035
|
#clock;
|
|
1921
2036
|
#effects;
|
|
@@ -2278,6 +2393,7 @@ var DummyAgent = class extends Emitter {
|
|
|
2278
2393
|
const messages = this.#messages.get(agentId);
|
|
2279
2394
|
if (agent.kind === "cone" && messages.length) this.#shelve(agent, messages);
|
|
2280
2395
|
this.#messages.set(agentId, []);
|
|
2396
|
+
this.#earlier.delete(agentId);
|
|
2281
2397
|
this.#queues.set(agentId, []);
|
|
2282
2398
|
this.#suggestions.delete(agentId);
|
|
2283
2399
|
this.emit("messages", agentId);
|
|
@@ -2309,8 +2425,36 @@ var DummyAgent = class extends Emitter {
|
|
|
2309
2425
|
agent.thinking = thinking;
|
|
2310
2426
|
this.#changed();
|
|
2311
2427
|
}
|
|
2312
|
-
|
|
2313
|
-
|
|
2428
|
+
paginate(keep, size, ids = [...this.#messages.keys()]) {
|
|
2429
|
+
this.#page = size;
|
|
2430
|
+
for (const agentId of ids) {
|
|
2431
|
+
const all = [...this.#earlier.get(agentId) ?? [], ...this.#messages.get(agentId) ?? []];
|
|
2432
|
+
const cut = Math.max(0, all.length - keep);
|
|
2433
|
+
this.#earlier.set(agentId, all.slice(0, cut));
|
|
2434
|
+
this.#messages.set(agentId, all.slice(cut));
|
|
2435
|
+
this.emit("messages", agentId);
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
failOlder(times = 1) {
|
|
2439
|
+
this.#failing = times;
|
|
2440
|
+
}
|
|
2441
|
+
hasOlder(agentId) {
|
|
2442
|
+
return !!this.#earlier.get(agentId)?.length;
|
|
2443
|
+
}
|
|
2444
|
+
async older(agentId) {
|
|
2445
|
+
this.olderCalls.push(agentId);
|
|
2446
|
+
await this.#clock.sleep(this.olderTicks);
|
|
2447
|
+
if (this.#failing > 0) {
|
|
2448
|
+
this.#failing--;
|
|
2449
|
+
throw new Error("The earlier messages couldn\u2019t be read.");
|
|
2450
|
+
}
|
|
2451
|
+
const earlier = this.#earlier.get(agentId) ?? [];
|
|
2452
|
+
const page2 = earlier.splice(Math.max(0, earlier.length - this.#page));
|
|
2453
|
+
if (page2.length) {
|
|
2454
|
+
this.#messages.get(agentId)?.unshift(...page2);
|
|
2455
|
+
this.emit("messages", agentId);
|
|
2456
|
+
}
|
|
2457
|
+
return page2;
|
|
2314
2458
|
}
|
|
2315
2459
|
commands(agentId) {
|
|
2316
2460
|
if (this.#agent(agentId)?.kind !== "cone") return [];
|
|
@@ -2354,6 +2498,7 @@ var DummyAgent = class extends Emitter {
|
|
|
2354
2498
|
this.stop(agentId);
|
|
2355
2499
|
this.#agents = this.#agents.filter((candidate) => candidate !== agent);
|
|
2356
2500
|
this.#messages.delete(agentId);
|
|
2501
|
+
this.#earlier.delete(agentId);
|
|
2357
2502
|
this.#queues.delete(agentId);
|
|
2358
2503
|
this.#suggestions.delete(agentId);
|
|
2359
2504
|
if (this.#active === agentId) this.select(agent.parentId ?? this.#agents[0].id);
|
|
@@ -6316,7 +6461,8 @@ function vaultAudit(now2 = Date.now()) {
|
|
|
6316
6461
|
secret: "HARBOR_API_KEY",
|
|
6317
6462
|
from: ["proxy", "device:link-pier"],
|
|
6318
6463
|
to: ["proxy", "tailscale", "device:link-pier"]
|
|
6319
|
-
}
|
|
6464
|
+
},
|
|
6465
|
+
{ kind: "reveal", field: "agent", from: true, to: false }
|
|
6320
6466
|
]
|
|
6321
6467
|
},
|
|
6322
6468
|
{
|
|
@@ -6715,6 +6861,151 @@ var DummyVaultButton = class {
|
|
|
6715
6861
|
this.#emit({ event: "popup", intent: resolved, state: "done" });
|
|
6716
6862
|
}
|
|
6717
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
|
+
};
|
|
6718
7009
|
var DummyVault = class extends Emitter {
|
|
6719
7010
|
#status;
|
|
6720
7011
|
#list;
|
|
@@ -6724,6 +7015,9 @@ var DummyVault = class extends Emitter {
|
|
|
6724
7015
|
#older = vaultOlder();
|
|
6725
7016
|
#serial = 0;
|
|
6726
7017
|
buttons = [];
|
|
7018
|
+
frames = [];
|
|
7019
|
+
answered = /* @__PURE__ */ new Set();
|
|
7020
|
+
revealed = [];
|
|
6727
7021
|
puts = [];
|
|
6728
7022
|
endpointChanges = [];
|
|
6729
7023
|
settingsCalls = [];
|
|
@@ -6998,6 +7292,55 @@ var DummyVault = class extends Emitter {
|
|
|
6998
7292
|
this.buttons.push(button);
|
|
6999
7293
|
return button;
|
|
7000
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
|
+
}
|
|
7001
7344
|
autoLabel() {
|
|
7002
7345
|
return this.#status.locked ? "Vault locked" : "Vault unlocked";
|
|
7003
7346
|
}
|
|
@@ -7111,7 +7454,8 @@ function createDummyModel({
|
|
|
7111
7454
|
monitor = "dummy",
|
|
7112
7455
|
import: imports = "off",
|
|
7113
7456
|
login,
|
|
7114
|
-
budget = "plain"
|
|
7457
|
+
budget = "plain",
|
|
7458
|
+
older = "off"
|
|
7115
7459
|
} = {}) {
|
|
7116
7460
|
const time = clock(delay);
|
|
7117
7461
|
const mount = mounts === "off" ? null : { scenario: mounts, point: mountPoint, files: mounted };
|
|
@@ -7136,8 +7480,10 @@ function createDummyModel({
|
|
|
7136
7480
|
browser.release(agentId);
|
|
7137
7481
|
};
|
|
7138
7482
|
importPort(imports, agent, time);
|
|
7483
|
+
if (older === "paged") agent.paginate(6, 4);
|
|
7139
7484
|
if (hold !== "off") agent.setHold({ reason: hold, held: 2 });
|
|
7140
7485
|
const noticePort = noticesPort(agent, time, notices, readonly, arrival);
|
|
7486
|
+
const vaultPort = vault === "off" ? void 0 : new DummyVault(vault, time, vaultRecords(accounts2, mcp));
|
|
7141
7487
|
if (scenario === "driving") void browser.run(drive);
|
|
7142
7488
|
const base = {
|
|
7143
7489
|
agent,
|
|
@@ -7180,11 +7526,9 @@ function createDummyModel({
|
|
|
7180
7526
|
...changes === "files" ? {} : { changes: new DummyChanges(files3, changes === "git") },
|
|
7181
7527
|
...noticePort ? { notices: noticePort } : {},
|
|
7182
7528
|
...tabs2 === "off" ? {} : { tabs: new DummyTabs(tabs2, time) },
|
|
7183
|
-
...approvals === "off" ? {} : { approvals: new DummyApprovals(approvals, time) },
|
|
7529
|
+
...approvals === "off" ? {} : { approvals: new DummyApprovals(approvals, time, vaultPort) },
|
|
7184
7530
|
...follow === "off" ? {} : { follow: new DummyFollow(follow, time) },
|
|
7185
|
-
...
|
|
7186
|
-
vault: new DummyVault(vault, time, vaultRecords(accounts2, mcp))
|
|
7187
|
-
},
|
|
7531
|
+
...vaultPort ? { vault: vaultPort } : {},
|
|
7188
7532
|
...guestsPort(guests, time)
|
|
7189
7533
|
};
|
|
7190
7534
|
base.vault?.on("status", (status2) => {
|
|
@@ -7207,6 +7551,7 @@ export {
|
|
|
7207
7551
|
DummyNotices,
|
|
7208
7552
|
DummyTabs,
|
|
7209
7553
|
DummyUpdates,
|
|
7554
|
+
DummyValueFrame,
|
|
7210
7555
|
DummyVault,
|
|
7211
7556
|
DummyVaultButton,
|
|
7212
7557
|
actionLimits,
|