@ai-ecoverse/slicc-spectrum 1.103.2 → 1.104.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 +25 -12
- package/dist/slicc-dummy.js +78 -1
- package/dist/slicc-dummy.js.map +2 -2
- package/dist/slicc-ui.js +136 -72
- package/dist/slicc-ui.js.map +3 -3
- package/dist/types/app/approvals.d.ts +3 -1
- package/dist/types/dummy/approvals.d.ts +1 -1
- package/dist/types/dummy/vault.d.ts +2 -1
- package/dist/types/dummy.d.ts +1 -1
- package/dist/types/model/types.d.ts +27 -2
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -259,6 +259,7 @@ Every row has up to two lines, aligned to the top under the name, starting with
|
|
|
259
259
|
A cone with scoops has a ribbon line below: one segment per scoop, coloured by status at 3:1 or more on every row background, then its scoops in words, naming only the busy ones, as in "2 scoops · 1 waiting" ("2 scoops" when all are idle).
|
|
260
260
|
Its `aria-description` sums up every status, as in "2 scoops: 1 working, 1 idle".
|
|
261
261
|
Rows without scoops or errors have no empty ribbon or error line.
|
|
262
|
+
With no agents while the agent port's `starting()` returns a line, the panel says "The agent shows up here once it starts." (`[data-empty="starting"]`, subdued) and has no **New cone**; the line goes, and New cone comes back, once an agent arrives or the line clears.
|
|
262
263
|
The drop or delete button sits in its own column on the row's right edge, under the context fill, so the title ellipsizes before it and the focus ring stays whole; it shows on hover, on keyboard focus of the row, when <kbd>Tab</kbd> from the selected row reaches it, and always on touch, and it hides again once a pointer that clicked it leaves.
|
|
263
264
|
The selected row has an accent bar on its inline-start edge.
|
|
264
265
|
Under forced colors, the dots, segments, unread circle and selection bar keep `CanvasText`, `Highlight` and `HighlightText`.
|
|
@@ -277,7 +278,7 @@ User messages (with attachments and a quiet timestamp, `time.when`, by that rule
|
|
|
277
278
|
A lick with a body is a `details.lick` whose head (`summary.lick-head`, with a quiet chevron that turns when open) toggles it with a click, <kbd>Enter</kbd> or <kbd>Space</kbd>; clicking the body doesn't close it.
|
|
278
279
|
A reply's tool calls are the steps of its response status (`swc-response-status[slot=status]`, `data-id` the reply's), at the top of the reply: one `swc-response-status-step` per call (`id` `run-` and the call's id, `data-tool`, `data-status`, `data-kind`, and `data-id` the message the call came from), labelled with the tool and its `title`, `active` while it runs, `complete` when done and `stopped` when it failed or was cancelled, which its label says in words (" · Failed", " · Cancelled").
|
|
279
280
|
The status keeps `data-action="open-cluster"` and each step `data-action="open-run"` with `data-target` the call's id: clicking the host from script opens it, as the old strip's buttons did, and opening a step opens its status too.
|
|
280
|
-
A step whose call has anything to show labels itself with a button (`.run-toggle`, `aria-expanded`, `aria-controls`) that opens the call under the step's row, at the full width of the column and indented to the step's text, in our own region (`.run-detail#detail-` and the call's id, `role="listitem"`, `hidden` while closed, `data-target` the call's id), not in Spectrum's 340 px description; the step carries `data-open` while it's open. The region is the call: its input (a shell command after `$`), an edit's `diff` shown as a diff view, its output, both kept on their lines and scrolling sideways rather than wrapping, with its ANSI styles shown (bold, dim, italic, underline, inverse, and the 16, 256 and truecolor foregrounds and backgrounds; the 16 foregrounds come from `--slicc-terminal-color-0` to `-15` when an embedder sets them, else the terminal panel's light tokens, which adapt in dark (so a card in dark uses Spectrum's colors, not the panel's VS Code ones); the 16 backgrounds are Spectrum's subtle tints under the default text, black a solid grey under the lightest grey, so text keeps 4.5:1; a 256 or truecolor background shows as a tint, blended toward the card (12% in light, 25% in dark), and one too close to the card to see, such as white on a light card, gets a grey tint toward the card's text instead; a 256 or truecolor foreground keeps its hue but moves toward black or white until it has 4.5:1 on the card or its tint, and other text on such a tint takes the card's text colour under the same floor; the card and its text colour are read once for light and once for dark, so the colours follow the theme through `light-dark()
|
|
281
|
+
A step whose call has anything to show labels itself with a button (`.run-toggle`, `aria-expanded`, `aria-controls`) that opens the call under the step's row, at the full width of the column and indented to the step's text, in our own region (`.run-detail#detail-` and the call's id, `role="listitem"`, `hidden` while closed, `data-target` the call's id), not in Spectrum's 340 px description; the step carries `data-open` while it's open. The region is the call: its input (a shell command after `$`), an edit's `diff` shown as a diff view, its output, both kept on their lines and scrolling sideways rather than wrapping, with its ANSI styles shown (bold, dim, italic, underline, inverse, and the 16, 256 and truecolor foregrounds and backgrounds; the 16 foregrounds come from `--slicc-terminal-color-0` to `-15` when an embedder sets them, else the terminal panel's light tokens, which adapt in dark (so a card in dark uses Spectrum's colors, not the panel's VS Code ones); the 16 backgrounds are Spectrum's subtle tints under the default text, black a solid grey under the lightest grey, so text keeps 4.5:1; a 256 or truecolor background shows as a tint, blended toward the card (12% in light, 25% in dark), and one too close to the card to see, such as white on a light card, gets a grey tint toward the card's text instead; a 256 or truecolor foreground keeps its hue but moves toward black or white until it has 4.5:1 on the card or its tint, and other text on such a tint takes the card's text colour under the same floor; the card and its text colour are read once for light and once for dark, so the colours follow the theme through `light-dark()`, and a chat that can't read them (outside the Spectrum theme, where it shows the colours as sent) tries again only when it reconnects or the theme changes; dim subdues default text and turns a bright color into its normal one) and every other escape stripped, a partial escape at the end of streamed output waiting for the rest (the model still sees the raw bytes, and a copy gets the text without escapes), an `image`, such as a screenshot, an optional `meta` such as a timeout, the time a joined call ran ("Ran at 16:12", `.ran`, by the two-minute rule), and its `notes` (`{ severity, text }`, such as "Exited with code 5" or "Full output in /tmp/…") as lines under the output, `li[data-note][data-severity]`: `error` with an alert icon in the negative colour, `warn` with an alert icon and `info` with an info icon, both subdued.
|
|
281
282
|
The status's header is always one line: while the reply streams or the agent works on the run, it is `active`, with the running call (or the last one) as its label and that call's kind as its loader, and each label stays for at least a second before the next one replaces it, so streamed arguments don't make it flicker; a reply without calls shows "Generating response" only until its first text or visible thinking.
|
|
282
283
|
Done, the status stays as `complete`, also when a call never left `running` (the step stays `active`): one call reads as its tool and `title`, more as the model's label for them or, until there is one, a count of the two most common kinds and the rest, such as "3 commands, 1 read" or "5 commands, 4 agent steps and 9 more", led by any failures ("1 failed, 1 cancelled · "), so the one-line header never cuts them off; a stopped reply says so.
|
|
283
284
|
Every call of a reply is in its one status, above its text; a later reply with text starts a new run, while a standalone tool message or a reply with only tool calls, streaming or done, joins the status of the run before it on the same day (hidden thinking and source links don't count as text), so a turn never collapses when its calls finish and past messages never move down.
|
|
@@ -307,7 +308,7 @@ Between the header and the transcript, a `<slicc-notices>` shows the model's not
|
|
|
307
308
|
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.
|
|
308
309
|
An empty chat says "No messages yet.
|
|
309
310
|
Ask sliccy something." with the agent's name, and only "No messages yet." while watch-only.
|
|
310
|
-
While the agent port's `starting()` returns a line, the chat has no empty state (`[data-empty]`): the embedder's notice says what's going on.
|
|
311
|
+
While the agent port's `starting()` returns a line, the chat has no empty state (`[data-empty]`): the embedder's notice says what's going on, and the header, with no agent yet, shows a neutral "Starting…" light (`[data-starting]`) instead of "No agent".
|
|
311
312
|
With the model's `seat` set, the chat is a guest's page: the header shows only the cone's name (`[data-seat-cone]`) and its status, with no pickers, meter or New conversation; the guest's own messages (`origin` `guest` whose `seat` is the seat's `id`, or, without a `seat`, whose `from` is its `label`) read as the reader's own, with no name, while other guests keep "From {label}" and the host's messages read "From the host"; an open question or a permission request takes the normal card border, without its buttons, form or countdown, and stays in the log (no `data-pending`), and says "The host answers this."
|
|
312
313
|
(`[data-host-answers]`, subdued); once answered, it shows the answer as everyone sees it; a reviewed guest message's outcome (a notice titled "It was approved.", "Nobody approved this in time.", "It was declined." or "It was withdrawn.", with the message as its text) reads as the quoted message, cut to one line, then the outcome (`.marker[data-ended]`); an approved one shows only "It was approved.", since the delivered message follows right after it; inline sprinkles inert with "View only on this device", error cards have no action and there's no suggested follow-up; the models aren't settled, and files can't be dropped.
|
|
313
314
|
An empty guest chat says "No messages yet.
|
|
@@ -325,6 +326,7 @@ A request that `secretRules()` flags shows a warning (`[data-secret-rules]`, the
|
|
|
325
326
|
The command (`[data-command]`, or a guest tool call's `[data-summary]`) wraps in full; past four lines the dialog shows the first four and **Show the full command** (as in the chat, `data-action="full-command"` with `data-shown`), and shown, it scrolls inside its own box of at most `min(16lh, 50dvh)`. Always allow, its hint and Allow once wait until the end of the command has been in view (a zero-size `[data-end]` after its last line), and so does the element's `decide()` for anything but `deny`; once seen, the end stays seen while the request waits, and hiding the command again hides them again. Until then a subdued line (`[data-allow-hint]`) says "Show the full command to allow it." or, shown, "Scroll to the end of the command to allow it." The dialog keeps one width, so only its height changes. A closed request shows its command whole.
|
|
326
327
|
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.
|
|
327
328
|
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.
|
|
329
|
+
Tray requests (`tray`) get their own title and one sentence (`#asking`), and only **Deny** (`deny`, focused) and **Allow once** (`once`), never Always allow.
|
|
328
330
|
|
|
329
331
|
#### `<slicc-follow>`
|
|
330
332
|
|
|
@@ -341,7 +343,7 @@ The message box.
|
|
|
341
343
|
`/` completes commands (`/clear`, `/compact`, `/model`, `/thinking`, `/scoop`, `/stop`, `/freeze` when the port can freeze, `/theme`) and their arguments, `@` mentions agents and files.
|
|
342
344
|
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), 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).
|
|
343
345
|
<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.
|
|
344
|
-
Queued messages wait above it, 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.
|
|
346
|
+
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.
|
|
345
347
|
Model and thinking pickers.
|
|
346
348
|
Where the browser has speech recognition, the `+` menu ends with **Enable push to talk** (`settings.pushToTalk`, optional and off by default), a checkable item (`menuitemcheckbox`, `aria-checked`) described as "Hold Alt+M to dictate", which stays open when toggled; Settings has the same switch.
|
|
347
349
|
While it is on, holding <kbd>Alt</kbd>+<kbd>M</kbd> in the field (by `code`, so <kbd>Option</kbd>+<kbd>M</kbd> on a Mac types no µ; not with Ctrl, ⌘ or Shift, nor during IME composition) dictates into it, the field's description ends with "hold Alt+M to dictate", and releasing either key, or leaving the window, stops it.
|
|
@@ -387,7 +389,7 @@ One file (`path`), highlighted, with its pending change and a link to the diff.
|
|
|
387
389
|
|
|
388
390
|
#### `<slicc-changes>`
|
|
389
391
|
|
|
390
|
-
Pending changes with their author.
|
|
392
|
+
Pending changes with their author, by name, which follows `agents` so an agent that arrives later replaces its id (also in `<slicc-diff-panel>`'s bar).
|
|
391
393
|
Opens the diff, accepts or reverts one change or all of them (**Accept all** and **Revert all** are `data-action="accept-all"` and `data-action="revert-all"`).
|
|
392
394
|
While watch-only (`watchOnly()`: a follower without Control, or the agent port's `readOnly()`), the list and the diffs stay but no Accept or Revert shows: a subdued "Watch only" (`[data-watch-note]`, plain text, not a live region) stands where Accept all and Revert all were, and a revert confirmed after that is dropped.
|
|
393
395
|
With nothing pending it says "No pending changes.
|
|
@@ -467,7 +469,7 @@ The secrets, sorted by name (`li[data-secret][data-state]`, with `[data-rules]`,
|
|
|
467
469
|
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.
|
|
468
470
|
Vault errors show their message, EACCES as "Unlock the vault first" and ENOTSUP as "This arrives with a later vault version".
|
|
469
471
|
Each action shows only when the port has its method.
|
|
470
|
-
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, and a policy entry lists its changes, its warning and, when accepted, how to change the file back; **Only reveals to programs**, `data-action="reveals"`, and **Show older**, `data-action="older"`).
|
|
472
|
+
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; **Only reveals to programs**, `data-action="reveals"`, and **Show older**, `data-action="older"`).
|
|
471
473
|
The section's `data-vault` is `locked`, `unlocked` or `loading`.
|
|
472
474
|
A popup the browser blocked shows how to allow it (`[data-popup=blocked]`).
|
|
473
475
|
Widened-rule notices aren't shown.
|
|
@@ -948,6 +950,12 @@ It offers **Deny** and **Allow once**, and, when `always` is `true`, **Always al
|
|
|
948
950
|
`decide` gets no `reason` from the UI; a rejected `decide` shows its message inline (`[data-error]`) with only **Close**, as for sudo.
|
|
949
951
|
Ended guest requests read "Answered elsewhere", "Guest message timed out" or "Guest tool call timed out".
|
|
950
952
|
Without `decide`, a guest request's notice reads "Waiting for approval of a guest’s message" with "{name}'s message to {agentName} waits for the owner's approval.", or "Waiting for approval of a guest’s tool call" with "{agentName} wants to run {tool} for {name}, which waits for the owner's approval.", then "Denied automatically at {time}."
|
|
953
|
+
A tray request (`kind` `tray`, a `TrayApproval`) asks the owner before `slicc tray` hands out or changes this seven's join URL: `op` (`url`, `rotate` or `unlink`), `requester`, an optional `device` (`{ id, name }`, the device to unlink), `createdAt` and `expiresAt`.
|
|
954
|
+
It has no `always`: the dialog never offers Always allow for it, even when the item carries `always: true`, and `decide()` gets only `once` or `deny`.
|
|
955
|
+
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**.
|
|
956
|
+
Ended tray requests read "Answered in another tab" or "Tray request timed out", and a withdrawn one closes silently.
|
|
957
|
+
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}."
|
|
958
|
+
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.
|
|
951
959
|
|
|
952
960
|
#### `vault` (optional)
|
|
953
961
|
|
|
@@ -1064,13 +1072,13 @@ The dummy's `docs` and `tickets` MCP accounts have a `url`, the dummy vault reco
|
|
|
1064
1072
|
`createDummyModel({ monitor: 'agent' })` (`?monitor=agent` on `/ui/`) adds the agent's Tasks (a tree by `depth`), Processes and Recent problems sections to the monitor, and its Running vital.
|
|
1065
1073
|
Like slicc-agent's snapshot, the dummy's vitals are the budget, Spend, Active agents, Processes and Process memory (both with a `series`) and Fullest context.
|
|
1066
1074
|
|
|
1067
|
-
`createDummyModel({ approvals: 'agent' | 'user' | 'many' | 'follower' | 'fail' | 'soon' | 'elsewhere' | 'withdrawn' | 'secrets' | 'guest' | 'guest-follower' | 'guest-always' | 'long' | 'guest-args' | 'guest-empty' | 'guest-truncated' })` (`?approvals=` on `/ui/`) adds waiting sudo requests: one from an agent, one from a terminal user, three in a row (the last as `www-data`), one without `decide`, one whose `decide` fails, one that expires in 24 seconds, or two whose first is answered in another tab or withdrawn after a few seconds, or one from an agent that copies a file over `/etc/fnox/config.toml`, or (`guest`) three guest requests: a message from Seat 2 (Mara) with attachments and an allow verdict, a tool call with a deny verdict, and a message whose `decide` rejects with "Someone else already answered this."
|
|
1068
|
-
(`guest-follower`: the same without `decide`), or (`guest-always`) one guest tool call from Seat 2 (Mara) to `read_file` with `always: true`, or (`long`) one sudo request whose seven-line command ends in `curl … | sh`, or one guest `write_file` call with a blank `summary`: with seven lines of `args` ending in the same tail (`guest-args`), with no `args` (`guest-empty`), or with a prefix of them and `argsTruncated` (`guest-truncated`).
|
|
1075
|
+
`createDummyModel({ approvals: 'agent' | 'user' | 'many' | 'follower' | 'fail' | 'soon' | 'elsewhere' | 'withdrawn' | 'secrets' | 'guest' | 'guest-follower' | 'guest-always' | 'long' | 'guest-args' | 'guest-empty' | 'guest-truncated' | 'tray' | 'tray-url' | 'tray-rotate' | 'tray-unlink' | 'tray-expired' })` (`?approvals=` on `/ui/`) adds waiting sudo requests: one from an agent, one from a terminal user, three in a row (the last as `www-data`), one without `decide`, one whose `decide` fails, one that expires in 24 seconds, or two whose first is answered in another tab or withdrawn after a few seconds, or one from an agent that copies a file over `/etc/fnox/config.toml`, or (`guest`) three guest requests: a message from Seat 2 (Mara) with attachments and an allow verdict, a tool call with a deny verdict, and a message whose `decide` rejects with "Someone else already answered this."
|
|
1076
|
+
(`guest-follower`: the same without `decide`), or (`guest-always`) one guest tool call from Seat 2 (Mara) to `read_file` with `always: true`, or (`long`) one sudo request whose seven-line command ends in `curl … | sh`, or one guest `write_file` call with a blank `summary`: with seven lines of `args` ending in the same tail (`guest-args`), with no `args` (`guest-empty`), or with a prefix of them and `argsTruncated` (`guest-truncated`), or tray requests from sliccy: one to share the join URL (`tray-url`), one to make a new one (`tray-rotate`), one to unlink Kitchen tablet (`tray-unlink`), all three in a row (`tray`), or the first, which times out after a few seconds (`tray-expired`).
|
|
1069
1077
|
`createDummyModel({ asks: 'long' })` (`?asks=long`) adds a pending sudo request with that seven-line command to kitchen-sink (`k-long`).
|
|
1070
1078
|
Decisions are recorded in `model.approvals.decisions`, and `model.approvals.end(id, reason)` ends a request.
|
|
1071
1079
|
|
|
1072
|
-
`createDummyModel({ vault: 'empty' | 'secrets' | 'policy' | 'locked' | 'request' | 'backup' | 'waiting' | 'purged' | '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; 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`.
|
|
1073
|
-
`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.
|
|
1080
|
+
`createDummyModel({ vault: 'empty' | 'secrets' | 'policy' | 'locked' | '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; 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`.
|
|
1081
|
+
`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`.
|
|
1074
1082
|
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.
|
|
1075
1083
|
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.
|
|
1076
1084
|
`model.vault.fail(code, message)` makes the next op fail.
|
|
@@ -1153,15 +1161,20 @@ npm test
|
|
|
1153
1161
|
```
|
|
1154
1162
|
|
|
1155
1163
|
`npm test` builds `dist/` and runs the integration tests in headless Chromium over raw CDP, through the [harness from slicc-shared-web](https://github.com/ai-ecoverse/slicc-shared-web#integration-test-harness), against a fake backend (`test/integration/page/fake-backend.js`).
|
|
1156
|
-
It writes CPU profiles, screenshots and console logs to `artifacts/`, and V8 coverage of `dist/` to `coverage/` only with `SLICC_COVERAGE=1` (CI leaves it off, since nothing reads it).
|
|
1164
|
+
It writes CPU profiles, screenshots and console logs to `artifacts/`, and V8 coverage of `dist/` to `coverage/` only with `SLICC_COVERAGE=1` (CI leaves it off, since nothing reads it; the unit tests' diff coverage is separate).
|
|
1157
1165
|
The test page is served cross-origin isolated (COOP/COEP), as slicc-kernel requires.
|
|
1158
1166
|
`test/integration/kernel.test.mjs` runs `bash -i` end to end on `@ai-ecoverse/slicc-kernel` (a pinned dev dependency), with `@ai-ecoverse/wasm-bash` and `@ai-ecoverse/wasm-coreutils` installed into OPFS by the page (`/kernel.html`).
|
|
1159
1167
|
`npm start` serves the test page on port 8080, and the UI at `/ui/`.
|
|
1160
1168
|
`test/integration/kernel-ui.test.mjs` boots `<slicc-app>` on the kernel model (`/kernel-ui/`): a command in the terminal, its file in the tree, an edit in a file tab read back in the terminal, a second terminal, and a reload.
|
|
1161
1169
|
`test/integration/ui.test.mjs` covers the shell, the layout (moving, closing, floating, reopening and reloading), streaming a reply with a tool call, stopping it, switching themes and the keyboard, and saves screenshots to `artifacts/ui/`.
|
|
1162
1170
|
|
|
1163
|
-
The Biome, TypeScript, lefthook, Renovate and CI configuration comes from [slicc-shared-web](https://github.com/ai-ecoverse/slicc-shared-web), which also provides the `slicc-lint`, `slicc-lint-comments` and `slicc-
|
|
1164
|
-
|
|
1171
|
+
The Biome, TypeScript, lefthook, Renovate and CI configuration comes from [slicc-shared-web](https://github.com/ai-ecoverse/slicc-shared-web), which also provides the `slicc-lint`, `slicc-lint-comments`, `slicc-agents-md`, `slicc-no-unit-tests`, `slicc-unit-cover` and `slicc-diff-cover` commands used by `npm run lint` and the pre-commit hook.
|
|
1172
|
+
|
|
1173
|
+
Unit tests live in `test/unit/`, which is gitignored: each change brings fresh unit tests for its own lines, and they are never committed or kept.
|
|
1174
|
+
The pre-commit hook runs them in Node against happy-dom under monocart and fails the commit unless the staged lines in `src/` are fully covered (`slicc-unit-cover` runs the unit tests the change reaches, then `slicc-diff-cover`, which runs `diff-cover --fail-under 100` and needs [uv](https://docs.astral.sh/uv/)).
|
|
1175
|
+
`npm run test:unit` uses this repo's `mcr.config.mjs`, which extends shared-web's, preloads `unit-setup.mjs` into every test process, and picks the untested `src/` files with a function instead of the glob `**/*.{js,mjs,ts}`.
|
|
1176
|
+
`unit-setup.mjs` registers happy-dom's globals and turns `.css`, `.shtml` and `dockview-core/css` imports into text modules, as the build does, so a fresh unit test can import any module in `src/`.
|
|
1177
|
+
The glob's `**` skips dot directories, so in a checkout under one (a bb worktree in `~/.bb/`) untested files were left out of the report and the hook passed on lines no unit test ran.
|
|
1165
1178
|
|
|
1166
1179
|
## Design review
|
|
1167
1180
|
|
package/dist/slicc-dummy.js
CHANGED
|
@@ -128,6 +128,25 @@ function guestCall(scenario, now2) {
|
|
|
128
128
|
always: true
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
|
+
function trayFixtures(scenario, now2) {
|
|
132
|
+
const requester = { kind: "agent", name: "sliccy", agentId: "cone-sliccy" };
|
|
133
|
+
const base = { requester, createdAt: now2, expiresAt: now2 + 5 * minutes };
|
|
134
|
+
const all = [
|
|
135
|
+
{ ...base, id: "tray-url", kind: "tray", op: "url" },
|
|
136
|
+
{ ...base, id: "tray-rotate", kind: "tray", op: "rotate", createdAt: now2 + 1 },
|
|
137
|
+
{
|
|
138
|
+
...base,
|
|
139
|
+
id: "tray-unlink",
|
|
140
|
+
kind: "tray",
|
|
141
|
+
op: "unlink",
|
|
142
|
+
device: { id: "device-kitchen", name: "Kitchen tablet" },
|
|
143
|
+
createdAt: now2 + 2
|
|
144
|
+
}
|
|
145
|
+
];
|
|
146
|
+
if (scenario === "tray") return all;
|
|
147
|
+
const id = scenario === "tray-expired" ? "tray-url" : scenario;
|
|
148
|
+
return all.filter((approval) => approval.id === id);
|
|
149
|
+
}
|
|
131
150
|
function approvalFixtures(scenario, now2 = Date.now()) {
|
|
132
151
|
const agent = {
|
|
133
152
|
id: "sudo-jq",
|
|
@@ -173,6 +192,7 @@ function approvalFixtures(scenario, now2 = Date.now()) {
|
|
|
173
192
|
];
|
|
174
193
|
if (scenario === "guest" || scenario === "guest-follower") return guestFixtures(now2);
|
|
175
194
|
if (scenario === "guest-always") return guestAlways(now2);
|
|
195
|
+
if (scenario.startsWith("tray")) return trayFixtures(scenario, now2);
|
|
176
196
|
if (scenario.startsWith("guest-")) return [guestCall(scenario, now2)];
|
|
177
197
|
if (scenario === "none") return [];
|
|
178
198
|
if (scenario === "long") return [{ ...agent, id: "sudo-release", command: longCommand }];
|
|
@@ -196,6 +216,8 @@ var DummyApprovals = class extends Emitter {
|
|
|
196
216
|
this.decide = (id, decision) => this.#decide(id, decision);
|
|
197
217
|
if (scenario === "elsewhere" || scenario === "withdrawn")
|
|
198
218
|
void time.sleep(100).then(() => this.end("sudo-jq", scenario === "elsewhere" ? "answered" : "withdrawn"));
|
|
219
|
+
if (scenario === "tray-expired")
|
|
220
|
+
void time.sleep(100).then(() => this.end("tray-url", "expired"));
|
|
199
221
|
}
|
|
200
222
|
raise(approval) {
|
|
201
223
|
this.#approvals = [...this.#approvals, approval];
|
|
@@ -6197,6 +6219,59 @@ function vaultPinned(now2 = Date.now()) {
|
|
|
6197
6219
|
pinned: { name: "DOCS_MCP_TOKEN", server: "docs", host: "docs.example" }
|
|
6198
6220
|
};
|
|
6199
6221
|
}
|
|
6222
|
+
function vaultSessions(now2 = Date.now()) {
|
|
6223
|
+
const who = { uid: 1e3, tag: "cone" };
|
|
6224
|
+
return [
|
|
6225
|
+
{
|
|
6226
|
+
...who,
|
|
6227
|
+
type: "session",
|
|
6228
|
+
seq: 15,
|
|
6229
|
+
id: "audit-15",
|
|
6230
|
+
at: now2 - minute3,
|
|
6231
|
+
name: "TIDE_MAIL",
|
|
6232
|
+
session: "kernel-session-tide-0015",
|
|
6233
|
+
domain: "mail.tide.example",
|
|
6234
|
+
port: 587,
|
|
6235
|
+
protocol: "smtp",
|
|
6236
|
+
position: "auth-plain",
|
|
6237
|
+
rule: 2,
|
|
6238
|
+
verifiedBy: "roots",
|
|
6239
|
+
exit: "proxy"
|
|
6240
|
+
},
|
|
6241
|
+
{
|
|
6242
|
+
...who,
|
|
6243
|
+
type: "session",
|
|
6244
|
+
seq: 14,
|
|
6245
|
+
id: "audit-14",
|
|
6246
|
+
at: now2 - 2 * minute3,
|
|
6247
|
+
name: "REEF_DEPLOY_KEY",
|
|
6248
|
+
session: "kernel-session-reef-0014",
|
|
6249
|
+
domain: "db.reef.example",
|
|
6250
|
+
port: 5432,
|
|
6251
|
+
protocol: "pg",
|
|
6252
|
+
position: "scram-sha-256",
|
|
6253
|
+
rule: 3,
|
|
6254
|
+
verifiedBy: "anchored",
|
|
6255
|
+
anchor: "q83vEjRWeJA=",
|
|
6256
|
+
exit: "device:link-pier"
|
|
6257
|
+
},
|
|
6258
|
+
{
|
|
6259
|
+
...who,
|
|
6260
|
+
type: "session",
|
|
6261
|
+
seq: 13,
|
|
6262
|
+
id: "audit-13",
|
|
6263
|
+
at: now2 - 3 * minute3,
|
|
6264
|
+
name: "HARBOR_API_KEY",
|
|
6265
|
+
session: "kernel-session-harbor-0013",
|
|
6266
|
+
domain: "quay.harbor.example",
|
|
6267
|
+
port: 8443,
|
|
6268
|
+
protocol: "http1",
|
|
6269
|
+
position: "header:authorization",
|
|
6270
|
+
rule: 1,
|
|
6271
|
+
exit: "tailscale"
|
|
6272
|
+
}
|
|
6273
|
+
];
|
|
6274
|
+
}
|
|
6200
6275
|
function vaultPolicy(now2 = Date.now()) {
|
|
6201
6276
|
return {
|
|
6202
6277
|
type: "policy",
|
|
@@ -6289,6 +6364,7 @@ function vaultFixtures(scenario, now2 = Date.now()) {
|
|
|
6289
6364
|
audit: empty ? [] : [
|
|
6290
6365
|
...policy ? [vaultPolicy(now2)] : [],
|
|
6291
6366
|
...scenario === "purged" ? [vaultPinned(now2), vaultPurged(now2)] : [],
|
|
6367
|
+
...scenario === "session" ? vaultSessions(now2) : [],
|
|
6292
6368
|
...vaultAudit(now2)
|
|
6293
6369
|
],
|
|
6294
6370
|
auditMore: !empty,
|
|
@@ -6912,6 +6988,7 @@ export {
|
|
|
6912
6988
|
vaultPolicy,
|
|
6913
6989
|
vaultPurged,
|
|
6914
6990
|
vaultRefused,
|
|
6915
|
-
vaultSecrets
|
|
6991
|
+
vaultSecrets,
|
|
6992
|
+
vaultSessions
|
|
6916
6993
|
};
|
|
6917
6994
|
//# sourceMappingURL=slicc-dummy.js.map
|