@ai-ecoverse/slicc-spectrum 2.4.1 → 2.5.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 +11 -8
- package/dist/slicc-dummy.js +79 -8
- package/dist/slicc-dummy.js.map +2 -2
- package/dist/slicc-ui.js +482 -29
- package/dist/slicc-ui.js.map +3 -3
- package/dist/types/app/composer.d.ts +13 -0
- package/dist/types/app/secrets.d.ts +10 -0
- package/dist/types/app/settings.d.ts +13 -1
- package/dist/types/app/vault.d.ts +7 -1
- package/dist/types/dummy/index.d.ts +2 -1
- package/dist/types/dummy/vault.d.ts +4 -1
- package/dist/types/model/types.d.ts +21 -2
- package/dist/types/ui.d.ts +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -342,6 +342,7 @@ Secret requests (`secret`, `dialog[data-kind=secret]`) read "Enter a value for {
|
|
|
342
342
|
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.
|
|
343
343
|
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.
|
|
344
344
|
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.
|
|
345
|
+
`valueEntry` (from `/ui`, `true`) says that every value entry point in this UI (the composer's Share a secret, Settings › Secrets Add and Replace, Settings › Accounts key fields) uses `vault.enter` and `valueFrame` whenever the port has `enter`, so an embedder can stop taking values through `put` only from a UI that has it; `<slicc-value-request>` still answers older `value` requests with `put`.
|
|
345
346
|
`approvalKinds` (from `/ui`) is a frozen array of the kinds this build shows (`sudo`, `guest-message`, `guest-tool`, `tray`, `secret`, `click`, `capture`), so an embedder can hold back the kinds an older UI would leave waiting unseen.
|
|
346
347
|
|
|
347
348
|
#### `<slicc-follow>`
|
|
@@ -357,7 +358,7 @@ Renders nothing without the port.
|
|
|
357
358
|
The message box uses Spectrum Web Components 2.0.0-beta.6. Its explicit accessible names stay "Message" for the textarea, "Send" ("Steer" while busy) for send, "Add" for upload and "Stop" for stop, using `accessible-label`, `send-accessible-label`, `upload-accessible-label` and `stop-accessible-label`. Attachment navigation stays "Show previous attachments" / "Show more attachments", and the strip stays "Uploaded assets strip", through their `*-accessible-label` attributes.
|
|
358
359
|
<kbd>Enter</kbd> sends, or steers a running reply: the message joins after the current tool round, without interrupting; <kbd>Ctrl</kbd>/<kbd>⌘</kbd>+<kbd>Enter</kbd> queues a follow-up that runs after the reply and can be withdrawn; <kbd>Shift</kbd>+<kbd>Enter</kbd> adds a line; <kbd>Tab</kbd> takes the suggested follow-up; <kbd>↑</kbd>/<kbd>↓</kbd> at the start or end walk this agent's history; <kbd>Esc</kbd> stops.
|
|
359
360
|
`/` completes commands (`/clear`, `/compact`, `/model`, `/thinking`, `/scoop`, `/stop`, `/freeze` when the port can freeze, `/theme`) and their arguments, `@` mentions agents and files.
|
|
360
|
-
Attach from the computer, from SLICC's files, a browser screenshot or a shared secret (with a `vault.put`, the popup takes the value too, checks it like Settings › Secrets, puts it in the vault and attaches the name, showing errors inline and clearing the field; an empty value shares a name the vault already has, and a new value for one asks before it replaces it; while the vault is away (`availability()` `connecting` or `waiting`) or locked, it takes no value and says so; without one, it attaches the name and says to add the value in Settings › Secrets; the popup closes on Cancel (`data-action="cancel-secret"`, disabled while a share is pending), on a press outside the form (ignored while a share is pending) and on Escape, each clearing the typed value), or paste and drop files (25 MB each; a dropped folder, told apart by its drag entry, becomes a “Folders can’t be attached” chip, and a file that can't be read a “Could not read the file” chip, instead of an attachment).
|
|
361
|
+
Attach from the computer, from SLICC's files, a browser screenshot or a shared secret (with `vault.enter` and `valueFrame`, the popup (`form.popup.secret`) takes only the name (`input[name=name]`): **Share** (`data-action="share-secret"`) attaches a name the vault already has at once, and for a new name opens `vault.enter({ name })` and shows the vault's own field (`slicc-vault-value`, `form.popup.secret[data-entry]`) in place of a value field, attaching the name on the frame's `done`; for a name the vault has, **Replace value** (`data-action="replace-secret"`) asks before it replaces the value and then opens `vault.enter({ name, replace: true })`; **Back** (`data-action="back-secret"`) cancels the request and goes back to the name, and **Cancel** (`data-action="cancel-secret"`), a press outside the form, Escape and the frame's `escape` cancel it and close the popup (Cancel is disabled and a press outside is ignored while a share is pending), and `enter`'s refusals, the frame's `error` and a `gone` frame ("The vault’s field timed out. Try again.") show inline; a locked vault unlocks in that field; while the vault is away it attaches the name only and opens no request, and a field already open stays through it; with `enter` it never shows a value input. Without `enter`, with a `vault.put`, the popup takes the value too, checks it like Settings › Secrets, puts it in the vault and attaches the name, showing errors inline and clearing the field; an empty value shares a name the vault already has, and a new value for one asks before it replaces it; while the vault is away (`availability()` `connecting` or `waiting`) or locked, it takes no value and says so; without one, it attaches the name and says to add the value in Settings › Secrets; the popup closes on Cancel (`data-action="cancel-secret"`, disabled while a share is pending), on a press outside the form (ignored while a share is pending) and on Escape, each clearing the typed value), or paste and drop files (25 MB each; a dropped folder, told apart by its drag entry, becomes a “Folders can’t be attached” chip, and a file that can't be read a “Could not read the file” chip, instead of an attachment).
|
|
361
362
|
<kbd>Esc</kbd> in the `+` menu closes it, from any item, and puts focus back on `+`; it goes no further, so it doesn't also stop a reply.
|
|
362
363
|
Queued messages wait above it in a dashed box (`--swc-neutral-visual-color`, 3:1 or more against the composer in both themes), to send now or drop; a steer the agent still holds (`mode: 'steer'`, `li.queued[data-mode=steer]`) reads "Sends at the next step" and can only be dropped.
|
|
363
364
|
Model and thinking pickers.
|
|
@@ -459,7 +460,7 @@ With `settings.removeServer` every MCP row adds **Remove** (`data-action="remove
|
|
|
459
460
|
An `api-key` account asks for its `secret`: “{provider} API key” by default, “{provider} token” for `secret: 'token'`; when it is `expired` it reads **API key rejected** or **Token rejected**.
|
|
460
461
|
An MCP server with `auth: 'oauth'` signs in in a pop-up: its button reads **Sign in**, and it reads **Needs sign-in** when `disconnected` and **Sign-in expired** when `expired`.
|
|
461
462
|
An account with `fields` (`{ name, label, required?, placeholder?, help?, customBase?, options?, value? }`) shows a plain labelled input for each, with its `placeholder`, its current `value` and its `help` as a line under it, and their values go to `connect(id, secret?, fields)`; the key field above them gets a visible `sp-field-label` too ("Amazon Bedrock API key"), and both span the form's width. A field with `options` (`{ value, label }`) is an `sp-picker[data-account-field]` instead, labelled by an `sp-field-label` (clicking it focuses the picker), `aria-required` when required, with its `help` under it and in its `description` slot, and when refused `invalid`, with “Select a {label}.” in its `description` slot and under it without a second icon, so a provider can offer only the values it accepts, such as Bedrock's regions; it starts on the field's current `value`, otherwise, or when `value` is none of the options, on the first option, and it never holds back Connect in the provider picker, since it always has a choice. An empty `options` list is a disabled picker reading “None available”, never a free-text input, with the help text “Nothing to choose from yet. You can connect without it.” under it in the same field group (`[data-empty]`), and doesn't hold up Save even when `required`. The plain inputs' border is `--swc-neutral-visual-color`, which keeps 3:1 against the page. The dummy's Amazon Bedrock account has a region picker; connecting keeps the chosen values as the fields' `value` and shows the chosen options' labels in the row (“API key · Europe (Frankfurt)”). A field with `vaultSetting` fills that vault setting: the first save puts the key with `settings` holding every such field that has a value (a `customBase` field only when its value is a bare name, such as an Azure resource; an `https:` one goes through `endpoints` instead), and once the key's record is live and the vault port has `settings` (and `put`), the key may be left empty, so **Connect** waits only for the required fields and an edit sends the full object through `vault.settings(name, 'model', settings)` before the endpoint change and `connect`. A refusal shows on the field it names (its `vaultSetting`, name or label), else on the key or the form, and stops before `connect`; when a keyless edit fails after its `settings` call, the record's previous settings are put back. Records saved without settings keep working until they're saved again.
|
|
462
|
-
An account with `store: 'vault'` keeps its key in the vault when the `vault` port has `put`: the masked key field (C5's value rules) goes to `vault.put(name, key, undefined, { kind: 'model', provider: account.id })` (or `{ kind: 'mcp', server }` for `mcp:<server>` accounts, named `MCP_<SERVER>_TOKEN`; model records use pi-ai's environment-variable name), then `connect(id)` runs without the secret; a connected one says “Key kept in the vault”, “Token kept in the vault” or “Kept in the vault”, and Disconnect removes the record first.
|
|
463
|
+
With `vault.enter` and `valueFrame`, a `store: 'vault'` account's key form (`form.key[data-store=vault]`) has no key field: **Next** (`data-action="enter-key"`, or Enter in a field) checks its fields (and confirms an MCP server's URL), then opens `vault.enter({ name, kind: 'model', provider, settings })` (or `{ name, kind: 'mcp', server }`), with `replace: true` when the record exists, so the vault checks the settings there, and shows the vault's own field (`[data-entry] slicc-vault-value`) with **Back** (`data-action="back-key"`); the frame's `done` registers the endpoints and connects as below, and a refusal at `enter` shows on the field it names. An account without fields opens the field at once, and Back then leaves the form. A kept key whose settings can change alone also offers **Save settings** (`data-action="save"`), which sends only `settings`. While the vault is away, the form says "Waiting for the vault" and Next stays disabled; with `enter` it never shows a key input, and an open field stays. Leaving the form, a change of vault and removing the element cancel the open request. Without `enter`, an account with `store: 'vault'` keeps its key in the vault when the `vault` port has `put`: the masked key field (C5's value rules) goes to `vault.put(name, key, undefined, { kind: 'model', provider: account.id })` (or `{ kind: 'mcp', server }` for `mcp:<server>` accounts, named `MCP_<SERVER>_TOKEN`; model records use pi-ai's environment-variable name), then `connect(id)` runs without the secret; a connected one says “Key kept in the vault”, “Token kept in the vault” or “Kept in the vault”, and Disconnect removes the record first.
|
|
463
464
|
Without `store: 'vault'` or `vault.put`, the key goes to `connect(id, secret)` as before.
|
|
464
465
|
An `mcp:<server>` account with an `https:` `url` (the server's URL), when the `vault` port has `endpoints`, asks before its token is saved: “Connect to {url}?”, “SLICC sends this token only to {url}.”, **Connect** with Cancel focused; then the token goes to `vault.put`, the URL to `vault.endpoints({ add: { mcp: [{ server, url }] } })` and the account to `connect`, and a refusal shows on the token field and stops. The page keeps its own registrations (server and URL, not secret) in `localStorage` under `slicc-spectrum.mcp-endpoints`, written after a successful add or change and cleared after Disconnect takes the URL back. When the account's `url` differs from that registration, or, with none, from a first-use pin (the record's `url` while `SecretView.pinned` is `true`; a bridge's loopback `url` without `pinned` never counts), the row adds **Reconnect** (`data-action=reconnect-mcp`) and the note “The server moved to {url}. Reconnect to send the token there.” (`[data-notice=moved]`); after the same confirm, one `endpoints` call removes the old registration (none for a pin, which a registration overrides) and adds the new, then `connect` runs. Disconnect takes the URL back after removing the token. Settings registers MCP URLs itself, never through the agent.
|
|
465
466
|
A `show-surface` whose `section` is an account id scrolls to that row and focuses its button, and `accounts` scrolls to the Accounts heading (`h2[data-section=accounts]`).
|
|
@@ -482,10 +483,10 @@ The head shows **Locked** or **Unlocked**, then, when `status.policy` marks a fi
|
|
|
482
483
|
A status line (`[data-availability]`) says "Waiting for the vault" while `availability()` is `connecting` or `waiting` (or `status()` is `null` without it), and "Unlock the vault to continue" while `scrub.needed` and not `scrub.ready`.
|
|
483
484
|
Waiting requests show under **Waiting for you** (`[data-request]`, the agent's `prompt` as plain text under "From the agent, not checked by SLICC:", capped at 200 characters, and "Expires in N min"), with **Enter value** (`data-action="answer"`, which fires `value-request` with `{ id, vault }` for the `<slicc-value-request>` on the same vault) for `value` requests and **Cancel request** (`data-action="cancel-request"`); `oauth`, `jerk-seal`, `jerk` and `unseal` requests say what they ask for and offer Cancel request only.
|
|
484
485
|
With `backupReminder: 'due'`, an alert (`[data-backup]`) offers **Export backup** (`data-action="export"`) and **Not now** (`data-action="dismiss-backup"`, with `dismissBackupReminder`).
|
|
485
|
-
**Add secret** (`data-action="add-secret"`, `[data-form=add]`, a name and a value; a name that exists asks before it replaces the value), **Import…** (`data-action="import"`, `[data-form=import]`: Bundle, KeePass or six, a file, an optional password, then a pick of candidates, all checked but the `refused` ones (disabled, with their reason), marked "Replaces a secret" when they exist, with their suggested rules as untrusted text and a note that the file's rules aren't applied, and **Import N**, which sends `{ name, kind }` for a candidate of another `kind`; then "Imported N" and "Skipped NAME: reason" for each skipped name, from `reasons`) and **Export…** (`data-action="export"`, `[data-form=export]`: a passphrase twice, at least 8 bytes, behind a confirmation, downloaded as `slicc-secrets-<yyyy-mm-dd>.slicc-vault`, with "Last exported" or "Never exported" from `exportedAt`).
|
|
486
|
+
**Add secret** (`data-action="add-secret"`, `[data-form=add]`, a name and a value; a name that exists asks before it replaces the value; with `vault.enter` and `valueFrame`, a name and **Next** (`data-action="enter-value"`), which opens `vault.enter` (with `replace: true` after that question) and shows the vault's own field (`[data-form][data-entry] slicc-vault-value`); **Replace value** opens `vault.enter({ name, kind, provider | server, replace: true })` and that field at once; the frame's `done` closes the form and says it was saved (with the name the form asked for when `done` carries none), and Cancel, Escape, the frame's `escape`, another form and a change of vault cancel the request), **Import…** (`data-action="import"`, `[data-form=import]`: Bundle, KeePass or six, a file, an optional password, then a pick of candidates, all checked but the `refused` ones (disabled, with their reason), marked "Replaces a secret" when they exist, with their suggested rules as untrusted text and a note that the file's rules aren't applied, and **Import N**, which sends `{ name, kind }` for a candidate of another `kind`; then "Imported N" and "Skipped NAME: reason" for each skipped name, from `reasons`) and **Export…** (`data-action="export"`, `[data-form=export]`: a passphrase twice, at least 8 bytes, behind a confirmation, downloaded as `slicc-secrets-<yyyy-mm-dd>.slicc-vault`, with "Last exported" or "Never exported" from `exportedAt`).
|
|
486
487
|
Saved shows "Saved NAME in the vault" (`[data-said]`).
|
|
487
488
|
The secrets, sorted by name (`li[data-secret][data-state]`, with `[data-rules]`, `[data-exits]` only when restricted, `[data-exec]` (any command, the listed commands, every bash) and how often programs were given it today, `[data-source]`, `[data-used]`, `[data-expires]`; a model record or an MCP token no rule names reads “Used for” (`[data-used-for]`, model requests to its provider or requests to its MCP server, by the agent) instead of where it's sent, with its account's state as its light, and shows its Provider (`[data-provider]`, the account's name) or Server (`[data-server]`)) offer **Replace value** (`data-action="replace"`, for `settings`, `process` and `import:*` secrets that aren't sign-ins) and **Remove** (`data-action="remove"`, behind a confirmation, passing the secret's `kind`), and removed ones, under **Removed**, **Restore** (`data-action="restore"`; ENOENT reads "Removed too long ago to restore").
|
|
488
|
-
Locked, every form (Add secret
|
|
489
|
+
Locked, every form (Add secret and Replace value without `vault.enter`, Import, Export; with it, the vault's field unlocks itself) 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.
|
|
489
490
|
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".
|
|
490
491
|
Each action shows only when the port has its method.
|
|
491
492
|
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"`).
|
|
@@ -500,7 +501,7 @@ Places `vault.button()`'s element (the vault's own button frame), posts its `con
|
|
|
500
501
|
|
|
501
502
|
#### `<slicc-vault-value>`
|
|
502
503
|
|
|
503
|
-
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
|
|
504
|
+
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, as soon as the frame is `ready`. It fires `vault-value` (bubbles, composed) with each `done`, `gone`, `error` and `escape` event. The detail holds only `event`, `request`, `state`, `code`, `message`, `name`, `placeholder` and `record` (each when it's a string), so anything else the frame sends goes no further.
|
|
504
505
|
|
|
505
506
|
#### `<slicc-value-request>`
|
|
506
507
|
|
|
@@ -1014,11 +1015,13 @@ The vault's view for SLICC, typed after `@ai-ecoverse/slicc-vault` (spectrum re-
|
|
|
1014
1015
|
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.
|
|
1015
1016
|
`audit(before?, limit?)` resolves with an older `AuditPage` (`before` is a `seq`).
|
|
1016
1017
|
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.
|
|
1017
|
-
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.
|
|
1018
|
+
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.24.0, whose ask and entry types `compat/vault.ts` also checks: `Requester`, `ValueConfig`, `ValueEvent`, `EnterResult` and, the other way, `ValueMessage` and `EnterOptions`); `isPolicyPaused` still identifies only `policy-paused`.
|
|
1018
1019
|
Values go to the vault only through `put`, and never through another port.
|
|
1019
1020
|
`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.
|
|
1020
1021
|
The passkey lock (unlock and protect) happens in the vault's own popup, which only a click in that frame can open.
|
|
1021
|
-
|
|
1022
|
+
Each surface acts on a `vault-value` event only when it comes from the field of the request still open there (and, when it names one, for that request), so a late or stale `done`, `gone`, `error` or `escape` changes nothing.
|
|
1023
|
+
The optional `enter(options)` opens a page-started value request (`EnterOptions`: `name`, optional `kind` (`secret`, `model` or `mcp`), `provider`, `server`, `settings` and `replace`) and resolves with `{ request }`; the vault checks the name and binding there, refuses an existing live record without `replace` (`EEXIST`), takes the value only in `valueFrame(request)`, and ends it with `cancel(request)` or after 5 minutes. Spectrum then never holds a value and calls `put` with one only without `enter`.
|
|
1024
|
+
The optional `valueFrame(request)` returns a new `VaultValueFrame` for an `ask` or `enter` 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', name?, placeholder?, record? }` or `done { request, state: '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.
|
|
1022
1025
|
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`.
|
|
1023
1026
|
`notices`, `acknowledge` and the `notice` event are still typed, and ignored.
|
|
1024
1027
|
|
|
@@ -1136,7 +1139,7 @@ Like slicc-agent's snapshot, the dummy's vitals are the budget, Spend, Active ag
|
|
|
1136
1139
|
Kitchen-sink's "Here is where things stand." ends with a `media` call that waited for a click in seven's tab (`k-media`).
|
|
1137
1140
|
Decisions are recorded in `model.approvals.decisions`, and `model.approvals.end(id, reason)` ends a request.
|
|
1138
1141
|
|
|
1139
|
-
`createDummyModel({ vault: 'empty' | 'secrets' | 'policy' | 'locked' | 'paused' | 'request' | 'backup' | 'waiting' | 'purged' | 'session' | 'endpoints-fail' | 'off' })` (`?vault=` on `/ui/`, off by default; `notice` is kept as another name for `policy`) adds an invented vault: no secrets yet; five secrets (an API key, a jerk lease from pier-nuc revealed to `pier publish`, a Tide Mail sign-in, one without a rule and a removed one) with an activity log that pages back once; the same with a refused rule and a policy change in the log; locked before the first unlock, with a waiting request from docs-pass; the secrets with `/etc/slicc/secrets-policy.toml` missing and an earlier pause of `/etc/fnox/config.toml` in the log; unlocked with a `fnox set` request for `TIDE_STATION_TOKEN`; the secrets with a due backup reminder; or a vault that hasn't answered yet (`model.vault.arrive()` brings it); Cloudflare's Account ID and AI Gateway ID and Azure's endpoint carry `vaultSetting`s; the dummy vault keeps `settings` on its records, logs each `settings` call in `model.vault.settingsCalls`, and refuses a setting whose value starts with `bad` (“The vault refused {KEY}. Check that value and try again.”). `endpoints-fail` is the secrets with an `endpoints` that refuses (an MCP URL with “The vault refused {url} for this token. Check the server's address, then try again.”, a model URL with “The vault refused that endpoint. Check the address and try again.”). The dummy vault records each `endpoints` change in `model.vault.endpointChanges`.
|
|
1142
|
+
`createDummyModel({ vault: 'empty' | 'secrets' | 'policy' | 'locked' | 'paused' | 'request' | 'backup' | 'waiting' | 'purged' | 'session' | 'endpoints-fail' | 'off' })` (`?vault=` on `/ui/`, off by default; `notice` is kept as another name for `policy`; `entry: 'frame'` (`?entry=frame`) gives it `enter`, so page entry goes through its stand-in value frame, and the default `entry: 'input'` leaves `enter` out, as an older bios does) adds an invented vault: no secrets yet; five secrets (an API key, a jerk lease from pier-nuc revealed to `pier publish`, a Tide Mail sign-in, one without a rule and a removed one) with an activity log that pages back once; the same with a refused rule and a policy change in the log; locked before the first unlock, with a waiting request from docs-pass; the secrets with `/etc/slicc/secrets-policy.toml` missing and an earlier pause of `/etc/fnox/config.toml` in the log; unlocked with a `fnox set` request for `TIDE_STATION_TOKEN`; the secrets with a due backup reminder; or a vault that hasn't answered yet (`model.vault.arrive()` brings it); Cloudflare's Account ID and AI Gateway ID and Azure's endpoint carry `vaultSetting`s; the dummy vault keeps `settings` on its records, logs each `settings` call in `model.vault.settingsCalls`, and refuses a setting whose value starts with `bad` (“The vault refused {KEY}. Check that value and try again.”). `endpoints-fail` is the secrets with an `endpoints` that refuses (an MCP URL with “The vault refused {url} for this token. Check the server's address, then try again.”, a model URL with “The vault refused that endpoint. Check the address and try again.”). The dummy vault records each `endpoints` change in `model.vault.endpointChanges`.
|
|
1140
1143
|
`purged` is `secrets` plus two `endpoints` audit entries: one with `purged` and `adopted`, and one `pinned` (`DOCS_MCP_TOKEN` on docs.example) that Unpin turns into an `unpinned` entry; `vaultPurged` and `vaultPinned` are exported. `session` is `secrets` plus three `session` audit entries (`vaultSessions`, exported): SMTP trusted by public roots, Postgres trusted by a pinned anchor through pier-nuc, and HTTP/1.1 with no `verifiedBy`.
|
|
1141
1144
|
It implements every op with the vault's checks, and keeps only names: `model.vault.puts` records `{ name, request? }`, `cancelled` the cancelled requests, `imports` each import's kind, size and pick, and `exported` the exports.
|
|
1142
1145
|
Import offers five invented candidates (one replaces a secret, one is a model key, one is refused with a reason that the done stage repeats; KeePass adds a `_2` duplicate), six answers ENOTSUP, page ops are recorded with `uid: null, tag: null`, and value ops while locked answer EACCES.
|
package/dist/slicc-dummy.js
CHANGED
|
@@ -7099,9 +7099,20 @@ var DummyValueFrame = class {
|
|
|
7099
7099
|
problem.textContent = "";
|
|
7100
7100
|
this.#finished = true;
|
|
7101
7101
|
try {
|
|
7102
|
-
if (reveal)
|
|
7103
|
-
|
|
7104
|
-
|
|
7102
|
+
if (reveal) {
|
|
7103
|
+
await this.#vault.allow(this.request);
|
|
7104
|
+
this.#emit({ event: "done", request: this.request, state: "allowed" });
|
|
7105
|
+
} else {
|
|
7106
|
+
const { name: name2, placeholder, state } = await this.#vault.seal(this.request, value);
|
|
7107
|
+
this.#emit({
|
|
7108
|
+
event: "done",
|
|
7109
|
+
request: this.request,
|
|
7110
|
+
state: "sealed",
|
|
7111
|
+
name: name2,
|
|
7112
|
+
placeholder,
|
|
7113
|
+
record: state
|
|
7114
|
+
});
|
|
7115
|
+
}
|
|
7105
7116
|
} catch (error) {
|
|
7106
7117
|
this.#finished = false;
|
|
7107
7118
|
const { code, message } = error;
|
|
@@ -7120,6 +7131,8 @@ var DummyVault = class extends Emitter {
|
|
|
7120
7131
|
#serial = 0;
|
|
7121
7132
|
buttons = [];
|
|
7122
7133
|
frames = [];
|
|
7134
|
+
entered = [];
|
|
7135
|
+
#entries = /* @__PURE__ */ new Map();
|
|
7123
7136
|
answered = /* @__PURE__ */ new Set();
|
|
7124
7137
|
revealed = [];
|
|
7125
7138
|
puts = [];
|
|
@@ -7174,6 +7187,10 @@ var DummyVault = class extends Emitter {
|
|
|
7174
7187
|
if (this.#available === "waiting") return "waiting";
|
|
7175
7188
|
return this.#status.locked ? "locked" : "ready";
|
|
7176
7189
|
}
|
|
7190
|
+
wait() {
|
|
7191
|
+
this.#available = "waiting";
|
|
7192
|
+
this.emit("availability", this.availability());
|
|
7193
|
+
}
|
|
7177
7194
|
arrive() {
|
|
7178
7195
|
this.#available = "ready";
|
|
7179
7196
|
this.emit("availability", this.availability());
|
|
@@ -7309,7 +7326,7 @@ var DummyVault = class extends Emitter {
|
|
|
7309
7326
|
async cancel(request) {
|
|
7310
7327
|
await this.#time.sleep(5);
|
|
7311
7328
|
this.#failed();
|
|
7312
|
-
this.#take(request);
|
|
7329
|
+
if (!this.#ended(request)) this.#take(request);
|
|
7313
7330
|
this.cancelled.push(request);
|
|
7314
7331
|
return "cancelled";
|
|
7315
7332
|
}
|
|
@@ -7409,7 +7426,44 @@ var DummyVault = class extends Emitter {
|
|
|
7409
7426
|
this.emit("status", this.#status);
|
|
7410
7427
|
}
|
|
7411
7428
|
ask(request) {
|
|
7412
|
-
return this.#status.waiting.find(({ id, kind }) => id === request && kind === "ask");
|
|
7429
|
+
return this.#entries.get(request)?.pending ?? this.#status.waiting.find(({ id, kind }) => id === request && kind === "ask");
|
|
7430
|
+
}
|
|
7431
|
+
enter = (options) => this.#open(options);
|
|
7432
|
+
async #open(options) {
|
|
7433
|
+
await this.#time.sleep(5);
|
|
7434
|
+
this.#failed();
|
|
7435
|
+
const { name: name2, kind = "secret", replace } = options;
|
|
7436
|
+
const bad = nameProblem(name2);
|
|
7437
|
+
if (bad) throw vaultFailure("EINVAL", bad);
|
|
7438
|
+
const live = this.#list.secrets.some(
|
|
7439
|
+
(secret) => secret.name === name2 && secret.kind === kind && secret.state !== "removed"
|
|
7440
|
+
);
|
|
7441
|
+
if (live && !replace)
|
|
7442
|
+
throw vaultFailure("EEXIST", `${name2} already holds a value; replace it instead`);
|
|
7443
|
+
if (options.settings) refused(options.settings);
|
|
7444
|
+
this.#serial += 1;
|
|
7445
|
+
const request = `enter-${this.#serial}`;
|
|
7446
|
+
const now2 = Date.now();
|
|
7447
|
+
this.entered.push({ ...options });
|
|
7448
|
+
this.#entries.set(request, {
|
|
7449
|
+
options,
|
|
7450
|
+
pending: {
|
|
7451
|
+
id: request,
|
|
7452
|
+
kind: "ask",
|
|
7453
|
+
name: name2,
|
|
7454
|
+
mode: "enter",
|
|
7455
|
+
uid: 0,
|
|
7456
|
+
tag: null,
|
|
7457
|
+
createdAt: now2,
|
|
7458
|
+
expiresAt: now2 + 5 * minute3
|
|
7459
|
+
}
|
|
7460
|
+
});
|
|
7461
|
+
return { request };
|
|
7462
|
+
}
|
|
7463
|
+
#ended(request) {
|
|
7464
|
+
if (!this.#entries.delete(request)) return false;
|
|
7465
|
+
this.emit("status", this.#status);
|
|
7466
|
+
return true;
|
|
7413
7467
|
}
|
|
7414
7468
|
#asked(request) {
|
|
7415
7469
|
this.#unlocked();
|
|
@@ -7423,8 +7477,18 @@ var DummyVault = class extends Emitter {
|
|
|
7423
7477
|
const problem = valueProblem(value);
|
|
7424
7478
|
if (problem) throw vaultFailure("EINVAL", problem);
|
|
7425
7479
|
this.answered.add(request);
|
|
7426
|
-
this.#
|
|
7427
|
-
|
|
7480
|
+
const entry = this.#entries.get(request);
|
|
7481
|
+
if (!entry) {
|
|
7482
|
+
this.#take(request);
|
|
7483
|
+
return this.#seal(found.name, "settings");
|
|
7484
|
+
}
|
|
7485
|
+
this.#ended(request);
|
|
7486
|
+
const { kind = "secret", provider, server, settings } = entry.options;
|
|
7487
|
+
return this.#seal(found.name, "settings", kind, {
|
|
7488
|
+
...provider === void 0 ? {} : { provider },
|
|
7489
|
+
...server === void 0 ? {} : { server },
|
|
7490
|
+
...settings === void 0 ? {} : { settings: { ...settings } }
|
|
7491
|
+
});
|
|
7428
7492
|
}
|
|
7429
7493
|
async allow(request) {
|
|
7430
7494
|
await this.#time.sleep(5);
|
|
@@ -7506,6 +7570,12 @@ function vaultRecords(accounts2, mcp) {
|
|
|
7506
7570
|
if (accounts2 === "settings") return [];
|
|
7507
7571
|
return accountRecords(Date.now(), mcp === "moved" ? docsUrl : void 0);
|
|
7508
7572
|
}
|
|
7573
|
+
function dummyVault(vault, entry, time, records) {
|
|
7574
|
+
if (vault === "off") return void 0;
|
|
7575
|
+
const port = new DummyVault(vault, time, records);
|
|
7576
|
+
if (entry === "input") port.enter = void 0;
|
|
7577
|
+
return port;
|
|
7578
|
+
}
|
|
7509
7579
|
function guestsPort(guests, time) {
|
|
7510
7580
|
return guests === "off" ? {} : { guests: new DummyGuests(guests, time) };
|
|
7511
7581
|
}
|
|
@@ -7546,6 +7616,7 @@ function createDummyModel({
|
|
|
7546
7616
|
switches = "off",
|
|
7547
7617
|
follow = "off",
|
|
7548
7618
|
vault = "off",
|
|
7619
|
+
entry = "input",
|
|
7549
7620
|
guests = "off",
|
|
7550
7621
|
accounts: accounts2 = "settings",
|
|
7551
7622
|
providers = "connected",
|
|
@@ -7589,7 +7660,7 @@ function createDummyModel({
|
|
|
7589
7660
|
if (older === "paged") agent.paginate(6, 4);
|
|
7590
7661
|
if (hold !== "off") agent.setHold({ reason: hold, held: 2 });
|
|
7591
7662
|
const noticePort = noticesPort(agent, time, notices, readonly, arrival);
|
|
7592
|
-
const vaultPort =
|
|
7663
|
+
const vaultPort = dummyVault(vault, entry, time, vaultRecords(accounts2, mcp));
|
|
7593
7664
|
if (scenario === "driving") void browser.run(drive);
|
|
7594
7665
|
const base = {
|
|
7595
7666
|
agent,
|