@ai-ecoverse/slicc-spectrum 1.78.0 → 1.79.1

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 CHANGED
@@ -157,10 +157,11 @@ Text is Adobe Clean Spectrum VF, falling back to Adobe Clean, and code is Spectr
157
157
  | `<slicc-monitor>` | The live monitor: vitals (`.vital[data-id]`), each a meter when it has a `ratio` (budget, fullest context) and otherwise a value, its unit and delta on one line and a sparkline from its `series` (spend, active agents, processes, process memory). Each card keeps its place whatever it shows: the value line never wraps (a unit or delta that doesn't fit is left out whole rather than cut, and stays in the line's `title` and its text; a vital's optional `hint` replaces that tooltip, such as the exact reset time behind "resets in 4 days"), a card without a `series` keeps the sparkline's room, a meter keeps its description's line, and a card left alone on the last row spans it, so short and long values, 0% and 100%, and missing data move nothing below them. Then alerts (an alert's optional `command` shows after its detail, led by its `commandLabel` or "Run", as unwrapped code with **Copy**, `[data-command]` and `data-action="copy-command"`), and the sections (the agent's Tasks, Processes and Recent problems, then terminals, tabs, pending changes and tray followers; cones and sprinkles have their own panels; any other section an agent sends, such as an older agent's `agents` or `sprinkles`, renders the same way). Each section (`li.section[data-id]`) shows its count and an attention badge, and opens its surface with **Open** where one exists; the counts line up in one column whether or not a section has **Open**, and a section without rows keeps its name in line with the others (`agents`, `terminals` and `processes` → terminal, `tabs`, `changes`). A section with rows expands into them with its toggle (`.toggle`, `aria-expanded`; `data-open` on the section): collapsed by default, expanded while a row is `warn` or `error`, until the user toggles it. Each row (`li.row[data-status]`, `data-id` from the row's `id`) shows a status light (blue active, gray idle, orange warn, red error, with the status spoken), the name, the meta line and its badges, indented by `depth` (`data-depth`, up to 6). |
158
158
  | `<slicc-freezer>` | Every conversation in the session, from `frozen()`: cones, scoops and agent runs, each with a kind badge, live ones first with a Live light, then the newest first. A live row offers **Open**, which selects that agent; a stopped one offers **Thaw** and **Remove**, which deletes its working folders after a confirmation. A thawed scoop or agent run keeps its row, which says what it was thawed as. **Delete cone**, in the agents list (or <kbd>Delete</kbd> on a cone's row) and the header's cone picker, stops a cone after a confirmation; `/freeze` does it at once. Either way its conversation stays here, and a row without a `kind` counts as a cone. |
159
159
  | `<slicc-sprinkle>` | A SLICC sprinkle (`.shtml`), as a panel or, with `inline`, as a dip in the chat that grows to fit, even when it loads out of view (the frame measures itself on load, after icons and fonts arrive, and on every resize). A dip has a tab-like handle: drag it into the dock (or onto a rail, or use its open-as-panel button) and it lives on as a sprinkle panel, with a note in the chat where it was; which dips are out is saved in `slicc-ui.dips`. It runs in a sandboxed frame (`allow-scripts` only) with SLICC's sprinkle theme (the `--s2-*` tokens and `.sprinkle-*` classes), the app's fonts, and Lucide icons (`<i data-lucide>`, `LucideIcons.render()`). It talks back through `slicc.lick({ action, data })`, which reaches the owning cone as a `sprinkle` lick, and, when the port has `call`, reads files and keeps state (see [Sprinkle bridge](#sprinkle-bridge)). Panel sprinkles sit at the top of the right rail with their Lucide icon. |
160
- | `<slicc-network>` | Network: the route and its health in plain words with the bios `detail`, a line on browser automation (`data-browser`: through the SLICC extension, through slicc on this computer (`proxy`, or a linked device on this computer), through a linked device's browser, that slicc on this computer has no browser to control when it carries the traffic and `via` is `null`, or not available with how to get it) when the status has `browser`, **Check again** (`data-action="check"`) when the port has `check`, an **Internet traffic** picker (`data-action="exit"`, in `[data-exit-picker]` with the exit's kind) when the port has `setExit`, the route line saying where traffic goes, and under the picker a warning that traffic waits while the chosen device isn't connected (`data-exit="device"`, `data-held`, with **Send directly**, `data-action="exit-direct"`, which calls `setExit(null)`) or, when `route` isn't `tailnet`, the Tailscale exit node in use (`data-exit="node"`), a **Tailscale** section (`data-tailnet` with the state) when the status has `tailnet`, a **Linked devices** section (`data-links`: `linked`, `connecting`, `reconnecting`, `offline`, `none` or `permission`; each device row has a **Prompting** or **Watching** badge, `data-controlling`, while it controls the agent, and **Allow remote control**, `sp-switch[data-action="remote-control"]`, behind a confirm when turned on, or `data-control="fixed"` for a device on this computer; with `access`, an **Allow** group of **View**, **Control** and **Terminal** switches, `data-action="access-view"`, `access-control` and `access-terminal`, each confirmed when turned on, Terminal's saying it gives a root shell, and Control disabled while View is off; a device that asks to follow shows **Asks to follow** and **Allow**, `data-action="allow-follow"`; a **Terminal open** badge, `data-terminals`, counts its terminals; with `followUrl`, the join details add **Open on another device** with a QR code and the link, `data-join="follow"` and `data-copy="follow-url"`) when the status has `links`, **Get the whole web** with the Chrome extension link (`data-action="install-extension"`, when `extensionUrl` is set) and the commands to run slicc, each in a `.command[data-command]` block with a copy button: `curl -fsSL https://get.sliccy.ai/slicc | sh` (its button is `data-action="copy-command"`), `slicc`, and, with Node, `npx sliccy@next`; folded away when the route already reaches the whole web (`proxy`, `extension`, `link` or `tailnet`, whose line names `tailnet.exitNode`) or a linked device is the exit (`exit.kind` `link`: the route line says "Through slicc on this computer" for a `local` device, or names a remote device and its `policy`, or says it's waiting for it, and `data-route` is `link`, whatever `route` says), and recent failures with host, error and time. When the model has a `tray` port, a **Sync** section (`data-sync` with the connection) follows the connection: the tray's state, the float's name, role and runtime, the join link, masked (`…/join/••••••`) until **Show** (`data-action="reveal-tray-join"`, which turns into **Hide**), with **Copy** (`data-action="copy-tray-join"`), the followers (`li[data-follower]`), and **Disconnect** (`data-action="tray-disconnect"`, confirmed) or **Reconnect** (`data-action="tray-reconnect"`). When the status has `leaders`, a **Leaders** section (`section[data-leaders]` with the count) follows Sync, since it's the other direction of the same tray link: Sync is this SLICC's own tray and who follows it, Leaders the SLICCs it follows. Each leader is an `li[data-leader]` (the id) with `data-state`: its name, its hub as a quiet second line and a status light with the state in words (Connecting, Reconnecting, Connected, Disconnected, Failed, Unlinked); `failed` adds its `error` as plain text (`[data-leader-error]`, "Couldn’t reach its hub." without one) and **Retry** (`data-action="retry-leader"`), and `unlinked` says "{name} stopped sharing with this SLICC." (`[data-unlinked]`) without actions. **Disconnect** (`data-action="disconnect-leader"`) runs at once, without a confirm, since it's reversible: the row stays as Disconnected (a neutral light), with **Reconnect** (`data-action="reconnect-leader"`) and **Forget** and no Disconnect or Retry. **Forget** (`data-action="forget-leader"`) asks "Forget {name}?" ("To follow it again, you’ll need a new invite from it.") with a negative confirm, and the row goes. Each shows only when the port has its method; while one runs, the row's buttons are disabled, and a failure shows in the row (`data-error="leader"`). The header's **Network** indicator (`data-network`, `data-health`) opens it: a status light, green for `ok`, yellow for `limited` and red for `failing`, with the state in its text and label and `detail` as its tooltip, which adds "Tailscale is connected." while the tailnet runs; on a phone, a cloud icon for the state. While a linked device asks to follow, controls the agent, watches it or has a terminal open, an indicator (`data-remote`, `ask`, `prompt`, `terminal` or `watch`) sits before it, **{name} asks to follow**, **Controlled by**, **Terminal open from** or **Watched by** the device's name (or the number of devices; in that order of precedence), says who does what in its tooltip and opens the panel too. Offered only when the model has a `network` port. |
160
+ | `<slicc-network>` | Network: the route and its health in plain words with the bios `detail`, a line on browser automation (`data-browser`: through the SLICC extension, through slicc on this computer (`proxy`, or a linked device on this computer), through a linked device's browser, that slicc on this computer has no browser to control when it carries the traffic and `via` is `null`, or not available with how to get it) when the status has `browser`, **Check again** (`data-action="check"`) when the port has `check`, an **Internet traffic** picker (`data-action="exit"`, in `[data-exit-picker]` with the exit's kind) when the port has `setExit`, the route line saying where traffic goes, and under the picker a warning that traffic waits while the chosen device isn't connected (`data-exit="device"`, `data-held`, with **Send directly**, `data-action="exit-direct"`, which calls `setExit(null)`) or, when `route` isn't `tailnet`, the Tailscale exit node in use (`data-exit="node"`), a **Tailscale** section (`data-tailnet` with the state) when the status has `tailnet`, a **Linked devices** section (`data-links`: `linked`, `connecting`, `reconnecting`, `offline`, `none` or `permission`; each device row has a **Prompting** or **Watching** badge, `data-controlling`, while it controls the agent, and **Allow remote control**, `sp-switch[data-action="remote-control"]`, behind a confirm when turned on, or `data-control="fixed"` for a device on this computer; with `access`, an **Allow** group of **View**, **Control** and **Terminal** switches, `data-action="access-view"`, `access-control` and `access-terminal`, each confirmed when turned on, Terminal's saying it gives a root shell, and Control disabled while View is off; a device that asks to follow shows **Asks to follow** and **Allow**, `data-action="allow-follow"`; a **Terminal open** badge, `data-terminals`, counts its terminals; with `followUrl`, the join details add **Open on another device** with a QR code and the link, `data-join="follow"` and `data-copy="follow-url"`) when the status has `links`, **Get the whole web** with the Chrome extension link (`data-action="install-extension"`, when `extensionUrl` is set) and the commands to run slicc, each in a `.command[data-command]` block with a copy button: `curl -fsSL https://get.sliccy.ai/slicc | sh` (its button is `data-action="copy-command"`), `slicc`, and, with Node, `npx sliccy@next`; folded away when the route already reaches the whole web (`proxy`, `extension`, `link` or `tailnet`, whose line names `tailnet.exitNode`) or a linked device is the exit (`exit.kind` `link`: the route line says "Through slicc on this computer" for a `local` device, or names a remote device and its `policy`, or says it's waiting for it, and `data-route` is `link`, whatever `route` says), and recent failures with host, error and time. When the model has a `tray` port, a **Sync** section (`data-sync` with the connection) follows the connection: the tray's state, the float's name, role and runtime, the join link, masked (`…/join/••••••`) until **Show** (`data-action="reveal-tray-join"`, which turns into **Hide**), with **Copy** (`data-action="copy-tray-join"`), the followers (`li[data-follower]`), and **Disconnect** (`data-action="tray-disconnect"`, confirmed) or **Reconnect** (`data-action="tray-reconnect"`). When the status has `leaders`, a **Leaders** section (`section[data-leaders]` with the count) follows Sync, since it's the other direction of the same tray link: Sync is this SLICC's own tray and who follows it, Leaders the SLICCs it follows. Each leader is an `li[data-leader]` (the id) with `data-state`: its name, its hub as a quiet second line and a status light with the state in words (Connecting, Reconnecting, Connected, Disconnected, Failed, Unlinked); `failed` adds its `error` as plain text (`[data-leader-error]`, "Couldn’t reach its hub." without one) and **Retry** (`data-action="retry-leader"`), and `unlinked` says "{name} stopped sharing with this SLICC." (`[data-unlinked]`) without actions. **Disconnect** (`data-action="disconnect-leader"`) runs at once, without a confirm, since it's reversible: the row stays as Disconnected (a neutral light), with **Reconnect** (`data-action="reconnect-leader"`) and **Forget** and no Disconnect or Retry. **Forget** (`data-action="forget-leader"`) asks "Forget {name}?" ("To follow it again, you’ll need a new invite from it.") with a negative confirm, and the row goes. Each shows only when the port has its method; while one runs, the row's buttons are disabled, and a failure shows in the row (`data-error="leader"`). When the model has a `guests` port, a **Guests** section (`section[data-guests]` with the count) follows Leaders: **Invite a guest** (`data-action="invite-guest"`, accent, disabled while no cone is live; frozen cones are left out of the invite too), or, while `unavailable()` (or the last `unavailable` event, when the port has no method) is a sentence, that sentence instead, subdued (`[data-guests-unavailable]`). Each seat is an `li[data-seat]` (the id) with `data-connected` (`true` or `false`): its label, the cone's name as a quiet second line (`[data-seat-cone]`), "Expires in {n} hours" under a day, "Expires in {n} days" after, or "No expiry" (`[data-expiry]`), a status light with **Connected** or **Not connected**, a count badge (`data-pending`) reading "{n} waiting", whose accessible name is "{n} waiting for review" when `pending` is above 0, and **Revoke** (`data-action="revoke-guest"`), which asks "Revoke {label}?" ("Their link stops working at once. To let them back in, invite them again.") with a negative confirm; while it runs, the rows' buttons are disabled, and a failure shows in the row (`data-error="guest"`). With no seats it says "No guests yet." (`[data-guests-empty]`). Invite opens `<slicc-guest-invite>`, a modal: **Cone** (`#guest-cone`, the active cone, or a selected scoop's cone, else the first), **Name** (`#guest-name`, required, trimmed, at most 40 characters, "Shown to you and in the cone, never set by the guest."), **Link expires** (`#guest-expiry`: 1 hour, 1 day, 7 days by default, or 30 days), **Their messages** (`#guest-messages`: You approve each one, The automatic check decides, {cone} decides, {scoop} decides for each of the cone's scoops, or No review, which are `user`, `agent`, `cone`, `scoop:<name>` and `off`) and **Their tool calls** (`#guest-tools`: `user`, `agent` and `scoop:<name>` only), both `user` by default, each with the chosen option's meaning under it. **Create link** (`data-action="create-guest-link"`) calls `mint(cone, { label, expiresAt, gates })`; while it runs the form is busy and Escape does nothing, and a rejection shows its message inline (`data-error="guest"`). On success the dialog shows the URL once (`[data-guest-invite="link"]`): a read-only field (`data-copy="guest-url"`) with **Copy** (`data-action="copy-guest-url"`), "This link is shown only once. Copy it now." and **Done** (`data-action="done-guest"`). The URL is never stored or shown again: closing the dialog drops it. The header's **Network** indicator (`data-network`, `data-health`) opens it: a status light, green for `ok`, yellow for `limited` and red for `failing`, with the state in its text and label and `detail` as its tooltip, which adds "Tailscale is connected." while the tailnet runs; on a phone, a cloud icon for the state. While a linked device asks to follow, controls the agent, watches it or has a terminal open, an indicator (`data-remote`, `ask`, `prompt`, `terminal` or `watch`) sits before it, **{name} asks to follow**, **Controlled by**, **Terminal open from** or **Watched by** the device's name (or the number of devices; in that order of precedence), says who does what in its tooltip and opens the panel too. Offered only when the model has a `network` port. |
161
161
  | `<slicc-tray>` | The tray items in the header, each opening a panel when that panel is in `panels`: the connection (`#chip`, a status light with the float's name and followers, `data-surface="network"`) opens Network at its **Sync** section, and the budget (`#usage`, `data-surface="monitor"`) opens the Monitor. Without the panel an item is a plain status with its label. |
162
162
  | `<slicc-confirm>` | The confirmation dialog `confirm()` opens. Not for direct use. |
163
163
  | `<slicc-prompt>` | The name dialog `prompt()` opens. Not for direct use. |
164
+ | `<slicc-guest-invite>` | The invite dialog of the Network panel's Guests section. Not for direct use. |
164
165
  | `<slicc-file-tree>` | Generic. [`@pierre/trees`](https://www.npmjs.com/package/@pierre/trees) through its `web-components` entry: `paths` (folders end in `/`), `gitStatus`, `expanded`, `query` (shows only the paths that match it; empty shows all), `reveal(path, focus)`. It has no search field of its own. Fires `file-open` on click and on <kbd>Enter</kbd>, and `tree-search` (`detail.key`) when a letter or digit is typed in it, for the panel's search field. |
165
166
  | `<slicc-code-view>` | Generic. A highlighted file through [`@pierre/diffs`](https://www.npmjs.com/package/@pierre/diffs): `path`, `contents`, `color`. |
166
167
  | `<slicc-diff-view>` | Generic. A diff through `@pierre/diffs`: `path`, `oldText`, `newText` (`null` for an added or deleted file), `color`, `diff-style` (`unified` or `split`). |
@@ -253,9 +254,10 @@ With an `agentId` or a `cone`, the caller reaches the shared surfaces, the chats
253
254
  | `tabs` (optional) | BIOS | `state()` returns this tab's `TabsState`: `role` (`owner` runs SLICC; `follower` shows the owner tab's SLICC; `connecting` while the owner boots or hands over; `alone` without SharedWorker), `stalled` (the owner stopped answering) and `skew` (`newer` or `older` when the owner runs another SLICC version, else `null`). Emit `tabs` when it changes. An owner shows nothing. A follower gets a quiet header light, "Another tab" (`[data-tabs][data-role]`, with `data-stalled` and `data-skew`; a dot on phones), info while connecting, and a notice while stalled or skewed, with its explanation as the tooltip. While connecting, an empty chat says "Connecting to the tab running SLICC…" (`[data-connecting]`). Skew, or else a stall, shows a warning banner at the top of every chat panel (`[data-tabs-notice="skew"\|"stalled"]`, `data-skew`): "Another tab runs a newer SLICC. Reload this tab." with **Reload** (`data-action="reload"`, calls `reload()`), "Another tab runs an older SLICC. Reload the other SLICC tab.", or "Chrome paused the tab running SLICC. Switch to it to continue.". An `alone` tab shows only the brand and "SLICC is open in another tab." (`[data-tabs="alone"]`). |
254
255
  | `approvals` (optional) | BIOS | `list()` returns waiting `Approval`s; emit `approvals` when the list changes. A `sudo` approval has `id`, `requester` (`kind` `agent` or `user`, `name`, optional `agentId` and `where`, such as a terminal's title), `command` (without `sudo`; bios joins the kernel's argv shell-quoted), optional `target` (the user to run as; shown as `sudo -u {target}` unless `root`), optional `cwd`, `createdAt`, `expiresAt` (epoch ms) and `always` (offer Always allow, which covers the requester's user and this exact command). With `decide(id, 'once' \| 'always' \| 'deny')`, which resolves once the decision is recorded and then drops the request from `list()`, the tab shows a warning alert dialog (`dialog[data-approval][data-kind][data-requester]`): "Allow sudo?", "{name} wants to run this with sudo[ in {where}]:" for an agent or "You ran this with sudo[ in {where}] (as {name}):" for a user, the command, "In {cwd}", a notice when the command changes secret rules, what Always allow does, "Denied automatically in m:ss." and how many more wait, with **Deny** (`data-action="deny"`, focused; Escape also denies), **Always allow** (`always`) and **Allow once** (`once`). A command that names `/etc/fnox` (or anything in it) or `/etc/slicc/secrets-policy.toml`, resolved against `cwd`, globs included (with quotes and backslashes removed, operands after `--` included; a path with `[`, `{` or `$` counts when the part before it could lead to them), or that names `/`, `/etc` or `/etc/slicc` in a command other than a read-only one (such as `ls`, `cat`, `stat` or `grep`), or runs with `cwd` in `/etc/fnox`, or names a relative path with `fnox` or `secrets-policy` in it wherever `cd` may have led, gets "This changes secret rules." with the path (`[data-secret-rules]`, the path in the attribute). Read-only commands count only by bare name or from `/bin` and `/usr/bin`. `/proc/<pid>/root` counts as `/` and `/proc/self/cwd` as `cwd`. Otherwise a command with a parameter expansion or command substitution (`$…`, backticks), a `~+` or `~-` path or a `~` path that climbs with `..`, a path through another process's `/proc/<pid>/cwd`, `fd` or `map_files` or through `/dev/fd`, or more than 4096 characters, gets "This may change secret rules." with the first such part in the attribute. Other symlinks aren't followed. Either way there's no Always allow, even when `always` is `true` (`decide(id, 'always')` isn't called for it), so each change to the rules is asked for. For half a second after a request shows, only Deny works, so a double-click can't approve it. A rejected `decide` shows its message in the dialog (`[data-error]`, `dialog[data-rejected]`) in place of the countdown, with only **Close** (`data-action="close"`, focused; Escape also closes), which moves on to the next waiting request or closes the dialog. Pass `decide` in every tab: the first decision wins. When a request ends without a decision in this tab, emit `ended` with `{ id, reason }` and drop it from `list()`: `answered` (another tab decided) shows "Answered in another tab", `expired` shows "Sudo request timed out", each with the command and **Close** (`data-action="close"`, `dialog[data-gone]`), and `withdrawn` (the requester went away) closes the dialog silently. Without `ended`, a request that leaves the list reads as expired past `expiresAt` and answered before it. Without `decide`, each request is a warning notice in the chat panels: "Waiting for sudo approval", who asks and the command, and "Answer it in the tab running SLICC. Denied automatically at {time}." A guest request has `kind` `guest-message` (`text`, optional `attachments` of `{ name, mimeType, size }`) or `guest-tool` (`tool`, `summary`), plus `requester` (`kind` `guest`), `agentId`, optional `agentName`, `createdAt`, `expiresAt`, `always: false` and an optional `verdict` (`{ decision: 'allow' \| 'deny', reason }`, the automatic check's view). Its dialog (`[data-approval-kind]` on the body) reads "Allow a guest’s message?" with "{name} wants to send this to {agentName ?? 'this cone'}:", the text as plain text in a scrolling box (`[data-guest-text]`, never Markdown or HTML) and the attachments as name · type · size (`[data-attachments]`), or "Allow a guest’s tool call?" with "{agentName ?? 'This cone'} wants to run {tool} for {name}:" and the summary (`[data-summary]`); then "The automatic check would allow this: {reason}" or "…deny this: {reason}" (`[data-verdict]`, the decision in the attribute). It offers only **Deny** and **Allow once**, whatever `always` says. `decide` gets no `reason` from the UI; a rejected `decide` shows its message inline (`[data-error]`) with only **Close**, as for sudo. Ended guest requests read "Answered elsewhere", "Guest message timed out" or "Guest tool call timed out". Without `decide`, a guest request's notice reads "Waiting for approval of a guest’s message" with "{name}'s message to {agentName} waits for approval on the device in control.", or "Waiting for approval of a guest’s tool call" with "{agentName} wants to run {tool} for {name}, which waits for approval on the device in control.", then "Denied automatically at {time}." |
255
256
  | `vault` (optional) | BIOS | The vault's view for SLICC, typed after `@ai-ecoverse/slicc-vault` (spectrum re-declares the types it uses, and `compat/vault.ts` checks that the package's types fit them). `status()` returns the `VaultStatus` (`locked`, `lock`, `scrub`, `waiting` requests, and optional `exportedAt` and `backupReminder`) or `null` while loading, and `list()` the `VaultList` (`secrets` as `SecretView`s, `refused` rules, `exits`, and the first `audit` page with `auditMore`) or `null`. 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. `audit(before?, limit?)` resolves with an older `AuditPage` (`before` is a `seq`). 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()` and `dismissBackupReminder()`. They reject with a `VaultFailure`, an `Error` with an `ErrorCode` `code` (`isVaultFailure` from `/ui`). Values go to the vault only through `put`, and never through another port. `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. The passkey lock (unlock and protect) happens in the vault's own popup, which only a click in that frame can open. `notices`, `acknowledge` and the `notice` event are still typed, and ignored. |
257
+ | `guests` (optional) | the tray hub | Guest seats on a cone. `list()` returns the `GuestSeat`s (`id`, `label`, `agentId`, the cone, `createdAt`, `expiresAt` or `null`, `gates`, `connected` and `pending`, how many of their messages or tool calls wait for review); a revoked or expired seat leaves it at once. Emit `seats` when it changes. `mint(agentId, { label, expiresAt, gates })` resolves with the `seat` and its `url`, which the panel shows once and never keeps, and `revoke(id)` ends a seat; both reject with an `Error` whose message is a sentence to show. The hub caps `expiresAt` at 30 days. `gates` (`{ messages, tools }`) are fixed at mint, so changing them means revoking and minting again; each is a `GuestGate`: `user` (a person with Control approves each one), `cone` (the shared cone itself decides, messages only), `agent` (the automatic check decides by the owner's intent and asks a person when unsure), `scoop:<name>` (that scoop decides) or `off` (no review: messages go straight in as follow-ups, messages only). The optional `unavailable()` returns a sentence when seats can't be minted here (no tray hub, or a follower without Control), or `null`; emit `unavailable` with it when it changes. |
256
258
  | `follow` (optional) | BIOS | Only on a follower page, a SLICC that shows another SLICC's agent over a link. `state()` returns the `FollowState`: `owner` (its display name), `link` (`connecting` for the first connection, `connected`, `reconnecting` after a connection dropped, `unlinked` when the owner unlinked this device, or `failed`), `access` (`{ view, control, terminal }`, what the owner allows this device), an optional `name` (this device's name on the owner, for the waiting page) and an optional `error` (shown when `failed`). Emit `follow` when it changes. Connected without `view` means waiting to be allowed. `leave()` forgets the link and the optional `retry()` tries again from `failed`. Pass only the surfaces a follower has to `<slicc-app>` (agents, chat, terminal and settings) and leave out `network`. |
257
259
 
258
- The dummy supports `createDummyModel({ updates: 'boot' | 'starting' | 'current' | 'available' | 'restart' | 'failure' })`, also selectable with `?updates=` on `/ui/`. Fixtures stay stable for inspection; `model.updates.setScenario('current')` completes boot. Dummy Update now and Retry simulate download and link progress; Restart agent and Reload only change fixture state, never restart a real process or reload the browser. `createDummyModel({ packages: 'mixed' | 'empty' | 'fail' | 'user' | 'off' })` (or `?packages=` on `/ui/`, off by default) adds invented optional packages in every state: Install and Retry finish after a short wait and install missing requirements too, `fail` makes them fail, `user` adds a pinned agent-kit and two additions (one with a warning) that Unpin and Remove take out of the list, and removing Python while uv is installed asks through the `confirm` option (the `/ui/` page passes spectrum's `confirm`). `updateFixtures`, `packageFixtures` and `DummyUpdates` are exported from `/dummy`. `createDummyModel({ mounts: 'mount' | 'cancel' | 'fail' | 'needs' })` (or `?mounts=` on `/ui/`) turns on the optional mount members, which are off by default: `mountFolder` mounts invented files at `/mnt/photos`, resolves with `null`, or rejects, as `model.files.mount.scenario` says; `needs` also lists `/mnt/photos` in `needsFolder()` until a folder is inserted there. `createDummyModel({ network: 'ok' | 'limited' | 'failing' | 'off' })` (or `?network=` on `/ui/`, default `limited`) sets the dummy network: slicc on this computer as a linked device (`route: 'link'`, with browser automation through it), the page fetch with CORS failures, or no route, all invented; `off` leaves out the port. `model.network.setScenario()` switches it and Check again re-emits after a short wait. `createDummyModel({ tailnet: 'off' | 'needs-login' | 'running' | 'failed' })` (or `?tailnet=` on `/ui/`, off by default) adds an invented tailnet: an auth key starting with `tskey-` signs in, `mast-pi` and `keel-router` are online exit nodes and `buoy-laptop` is offline, and Retry on `failed` connects. While the dummy tailnet runs with an exit node, the route is `tailnet` and the health `ok`. `createDummyModel({ links: 'none' | 'preparing' | 'linked' | 'reconnecting' | 'exit' | 'prompt' | 'denied' | 'remote' | 'followers' })` (or `?links=` on `/ui/`, off by default) adds invented linked devices and the exit members: `linked` has quay-studio (on this computer) and pier-nuc, which can be exits and have a Chrome each, and dock-builder, which can't; quay-studio shares `sketches` and pier-nuc `proj` and a read-only `archive`, which **Mount** attaches to the dummy files at `/mnt/<source>` (with `mountInfo` naming the source) and **Eject** or the Files strip detaches; with no other browser automation, the dummy reports `via: 'link'`; `exit` sends traffic through quay-studio; `reconnecting` holds traffic for pier-nuc (health `failing`, and `ok` while the exit device is connected, both without a `detail`, as bios reports them); `remote` has pier-nuc allowed and prompting, quay-studio watching, and an offline reef-laptop that only ever connected for control; `prompt` and `denied` wait for Local Network Access, and Try again links quay-studio. New join URL changes the URL, and `model.network.setLinks()` switches scenarios. `createDummyModel({ leaders: 'connected' | 'failed' | 'unlinked' | 'disconnected' | 'two' })` (or `?leaders=` on `/ui/`, off by default) adds invented leaders and their members: cove-studio connected, marsh-desk failed with an error, cove-studio unlinked (gone on the next status), cove-studio disconnected, or both cove-studio and marsh-desk; Retry and Reconnect move a leader to connecting, then connected, Disconnect makes it disconnected and Forget drops it, each after a short wait. `networkFixtures`, `tailnetFixtures`, `linksFixtures`, `leadersFixtures`, `joinUrl` and `DummyNetwork` are exported from `/dummy`. `createDummyModel({ changes: 'files' | 'git' | 'nogit' })` (or `?changes=` on `/ui/`, default `files`) adds a git `changes` port: `git` lists the pending changes under `/workspace/harbor` and `/workspace/skills` with their `repo` and no author, and `nogit` reports git as unavailable. `DummyChanges` is exported from `/dummy`. `createDummyModel({ notices: 'storage' | 'mixed' | 'fail' | 'off' })` (or `?notices=` on `/ui/`, off by default) adds an invented storage warning with Try again, which clears it after a short wait (`mixed` adds an informative notice without actions, and `fail` makes Try again fail). `noticeFixtures` and `DummyNotices` are exported from `/dummy`. `createDummyModel({ browser: 'idle' | 'driving' | 'off' })` (or `?browser=` on `/ui/`, default `idle`) sets the dummy browser: `idle` has a history of invented actions (about 180 on harbor's tab, enough for Show more) and no driven tab, `driving` has harbor and quiet-otter driving two tabs, with harbor's last click still running and frames every 500 ms while watched, and `off` has no tabs. Stopping an agent releases its tabs. Like bios, `actions()` keeps the latest 500 per tab from the last 24 hours, 5,000 in total (`actionLimits`). `DummyBrowser`, `actionLimits`, `browserHistory` and `page` are exported from `/dummy`. `createDummyModel({ sprinkles: 'builtin' | 'cones' })` (or `?sprinkles=` on `/ui/`, default `builtin`) adds, with `cones`, invented sprinkles of other cones: a Board in harbor and one in tide-tables, and a Changelog in release-notes. `createDummyModel({ tabs: 'owner' | 'follower' | 'connecting' | 'stalled' | 'newer' | 'older' | 'alone' | 'off' })` (or `?tabs=` on `/ui/`, off by default) adds a tabs port in that state; `model.tabs.setScenario()` switches it, and Reload connects again and becomes a follower after a short wait. `tabsFixtures` and `DummyTabs` are exported from `/dummy`.
260
+ The dummy supports `createDummyModel({ updates: 'boot' | 'starting' | 'current' | 'available' | 'restart' | 'failure' })`, also selectable with `?updates=` on `/ui/`. Fixtures stay stable for inspection; `model.updates.setScenario('current')` completes boot. Dummy Update now and Retry simulate download and link progress; Restart agent and Reload only change fixture state, never restart a real process or reload the browser. `createDummyModel({ packages: 'mixed' | 'empty' | 'fail' | 'user' | 'off' })` (or `?packages=` on `/ui/`, off by default) adds invented optional packages in every state: Install and Retry finish after a short wait and install missing requirements too, `fail` makes them fail, `user` adds a pinned agent-kit and two additions (one with a warning) that Unpin and Remove take out of the list, and removing Python while uv is installed asks through the `confirm` option (the `/ui/` page passes spectrum's `confirm`). `updateFixtures`, `packageFixtures` and `DummyUpdates` are exported from `/dummy`. `createDummyModel({ mounts: 'mount' | 'cancel' | 'fail' | 'needs' })` (or `?mounts=` on `/ui/`) turns on the optional mount members, which are off by default: `mountFolder` mounts invented files at `/mnt/photos`, resolves with `null`, or rejects, as `model.files.mount.scenario` says; `needs` also lists `/mnt/photos` in `needsFolder()` until a folder is inserted there. `createDummyModel({ network: 'ok' | 'limited' | 'failing' | 'off' })` (or `?network=` on `/ui/`, default `limited`) sets the dummy network: slicc on this computer as a linked device (`route: 'link'`, with browser automation through it), the page fetch with CORS failures, or no route, all invented; `off` leaves out the port. `model.network.setScenario()` switches it and Check again re-emits after a short wait. `createDummyModel({ tailnet: 'off' | 'needs-login' | 'running' | 'failed' })` (or `?tailnet=` on `/ui/`, off by default) adds an invented tailnet: an auth key starting with `tskey-` signs in, `mast-pi` and `keel-router` are online exit nodes and `buoy-laptop` is offline, and Retry on `failed` connects. While the dummy tailnet runs with an exit node, the route is `tailnet` and the health `ok`. `createDummyModel({ links: 'none' | 'preparing' | 'linked' | 'reconnecting' | 'exit' | 'prompt' | 'denied' | 'remote' | 'followers' })` (or `?links=` on `/ui/`, off by default) adds invented linked devices and the exit members: `linked` has quay-studio (on this computer) and pier-nuc, which can be exits and have a Chrome each, and dock-builder, which can't; quay-studio shares `sketches` and pier-nuc `proj` and a read-only `archive`, which **Mount** attaches to the dummy files at `/mnt/<source>` (with `mountInfo` naming the source) and **Eject** or the Files strip detaches; with no other browser automation, the dummy reports `via: 'link'`; `exit` sends traffic through quay-studio; `reconnecting` holds traffic for pier-nuc (health `failing`, and `ok` while the exit device is connected, both without a `detail`, as bios reports them); `remote` has pier-nuc allowed and prompting, quay-studio watching, and an offline reef-laptop that only ever connected for control; `prompt` and `denied` wait for Local Network Access, and Try again links quay-studio. New join URL changes the URL, and `model.network.setLinks()` switches scenarios. `createDummyModel({ leaders: 'connected' | 'failed' | 'unlinked' | 'disconnected' | 'two' })` (or `?leaders=` on `/ui/`, off by default) adds invented leaders and their members: cove-studio connected, marsh-desk failed with an error, cove-studio unlinked (gone on the next status), cove-studio disconnected, or both cove-studio and marsh-desk; Retry and Reconnect move a leader to connecting, then connected, Disconnect makes it disconnected and Forget drops it, each after a short wait. `networkFixtures`, `tailnetFixtures`, `linksFixtures`, `leadersFixtures`, `joinUrl` and `DummyNetwork` are exported from `/dummy`. `createDummyModel({ guests: 'on' | 'none' | 'unavailable' | 'fail' })` (or `?guests=` on `/ui/`, off by default) adds an invented `guests` port: `on` has Seat 1 (Mara) on sliccy, connected with 2 waiting and 5 days left, and Seat 2 (Ole) on harbor, not connected and without expiry; `none` has no seats; `unavailable` has none and says guests need the tray hub, so minting rejects; and `fail` makes `mint` reject with a sentence. `mint` adds a seat after a short wait and resolves with an invented `https://www.example.com/guest/…` URL, `revoke` drops the seat, and `model.guests.setScenario()` switches scenarios. `guestsFixtures`, `guestsUnavailable`, `guestsRefused` and `DummyGuests` are exported from `/dummy`. `createDummyModel({ changes: 'files' | 'git' | 'nogit' })` (or `?changes=` on `/ui/`, default `files`) adds a git `changes` port: `git` lists the pending changes under `/workspace/harbor` and `/workspace/skills` with their `repo` and no author, and `nogit` reports git as unavailable. `DummyChanges` is exported from `/dummy`. `createDummyModel({ notices: 'storage' | 'mixed' | 'fail' | 'off' })` (or `?notices=` on `/ui/`, off by default) adds an invented storage warning with Try again, which clears it after a short wait (`mixed` adds an informative notice without actions, and `fail` makes Try again fail). `noticeFixtures` and `DummyNotices` are exported from `/dummy`. `createDummyModel({ browser: 'idle' | 'driving' | 'off' })` (or `?browser=` on `/ui/`, default `idle`) sets the dummy browser: `idle` has a history of invented actions (about 180 on harbor's tab, enough for Show more) and no driven tab, `driving` has harbor and quiet-otter driving two tabs, with harbor's last click still running and frames every 500 ms while watched, and `off` has no tabs. Stopping an agent releases its tabs. Like bios, `actions()` keeps the latest 500 per tab from the last 24 hours, 5,000 in total (`actionLimits`). `DummyBrowser`, `actionLimits`, `browserHistory` and `page` are exported from `/dummy`. `createDummyModel({ sprinkles: 'builtin' | 'cones' })` (or `?sprinkles=` on `/ui/`, default `builtin`) adds, with `cones`, invented sprinkles of other cones: a Board in harbor and one in tide-tables, and a Changelog in release-notes. `createDummyModel({ tabs: 'owner' | 'follower' | 'connecting' | 'stalled' | 'newer' | 'older' | 'alone' | 'off' })` (or `?tabs=` on `/ui/`, off by default) adds a tabs port in that state; `model.tabs.setScenario()` switches it, and Reload connects again and becomes a follower after a short wait. `tabsFixtures` and `DummyTabs` are exported from `/dummy`.
259
261
 
260
262
  `createDummyModel({ follow: 'connecting' | 'waiting' | 'watch' | 'control' | 'full' | 'reconnecting' | 'unlinked' | 'failed' })` (`?follow=` on `/ui/`, which then leaves out the network port and offers only agents, chat, terminal and settings) makes the page a follower of SLICC on quay-studio: connecting, waiting to be allowed, View only, View and Control, everything, reconnecting, unlinked, or failed, where Try again connects with everything allowed. Leave counts in `model.follow.left` (the `/ui/` page then drops `?follow=`), and `model.follow.setScenario()` switches it. `followFixtures` and `DummyFollow` are exported from `/dummy`. `?links=followers` lists quay-studio (a CLI on this computer, Control always allowed), pier-nuc (a remote CLI, prompting), bay-laptop (a follower with everything allowed, watching, a terminal open), reef-phone (View only, watching) and tide-tablet (asking to follow); every links scenario has a follow link.
261
263
 
@@ -484,6 +484,88 @@ var DummyFollow = class extends Emitter {
484
484
  }
485
485
  };
486
486
 
487
+ // src/dummy/guests.ts
488
+ var hour = 36e5;
489
+ var day = 24 * hour;
490
+ var cap = 30 * day;
491
+ var guestsUnavailable = "Guests need the tray hub. Connect this SLICC to a tray to invite one.";
492
+ var guestsRefused = "The hub couldn\u2019t make a link right now. Try again in a minute.";
493
+ function guestsFixtures(scenario, now2 = Date.now()) {
494
+ if (scenario === "none" || scenario === "unavailable") return [];
495
+ return [
496
+ {
497
+ id: "seat-mara",
498
+ label: "Seat 1 (Mara)",
499
+ agentId: "cone-sliccy",
500
+ createdAt: now2 - 2 * hour,
501
+ expiresAt: now2 + 5 * day + 3 * hour,
502
+ gates: { messages: "user", tools: "user" },
503
+ connected: true,
504
+ pending: 2
505
+ },
506
+ {
507
+ id: "seat-ole",
508
+ label: "Seat 2 (Ole)",
509
+ agentId: "cone-harbor",
510
+ createdAt: now2 - 3 * day,
511
+ expiresAt: null,
512
+ gates: { messages: "scoop:quiet-otter", tools: "agent" },
513
+ connected: false,
514
+ pending: 0
515
+ }
516
+ ];
517
+ }
518
+ var DummyGuests = class extends Emitter {
519
+ scenario;
520
+ #seats;
521
+ #clock;
522
+ #count = 0;
523
+ constructor(scenario, clock2) {
524
+ super();
525
+ this.scenario = scenario;
526
+ this.#clock = clock2;
527
+ this.#seats = guestsFixtures(scenario);
528
+ }
529
+ list() {
530
+ return this.#seats;
531
+ }
532
+ unavailable() {
533
+ return this.scenario === "unavailable" ? guestsUnavailable : null;
534
+ }
535
+ setScenario(scenario) {
536
+ this.scenario = scenario;
537
+ this.#seats = guestsFixtures(scenario);
538
+ this.emit("unavailable", this.unavailable());
539
+ this.emit("seats", this.#seats);
540
+ }
541
+ async mint(agentId, draft) {
542
+ await this.#clock.sleep(4);
543
+ const refused = this.unavailable() ?? (this.scenario === "fail" ? guestsRefused : null);
544
+ if (refused) throw new Error(refused);
545
+ this.#count += 1;
546
+ const now2 = Date.now();
547
+ const seat = {
548
+ id: `seat-${now2.toString(36)}-${this.#count}`,
549
+ label: draft.label,
550
+ agentId,
551
+ createdAt: now2,
552
+ expiresAt: draft.expiresAt === null ? null : Math.min(draft.expiresAt, now2 + cap),
553
+ gates: draft.gates,
554
+ connected: false,
555
+ pending: 0
556
+ };
557
+ this.#seats = [...this.#seats, seat];
558
+ this.emit("seats", this.#seats);
559
+ return { seat, url: `https://www.example.com/guest/${seat.id}#k=Tq7Lm2Vx9Rb4` };
560
+ }
561
+ async revoke(id) {
562
+ await this.#clock.sleep(4);
563
+ if (!this.#seats.some((seat) => seat.id === id)) throw new Error("That guest is already gone.");
564
+ this.#seats = this.#seats.filter((seat) => seat.id !== id);
565
+ this.emit("seats", this.#seats);
566
+ }
567
+ };
568
+
487
569
  // src/dummy/script.ts
488
570
  var units = "/workspace/harbor/src/lib/units.ts";
489
571
  var kelvin = `
@@ -2378,16 +2460,16 @@ var status = `<!DOCTYPE html>
2378
2460
  `;
2379
2461
 
2380
2462
  // src/dummy/extras.ts
2381
- var day = 864e5;
2463
+ var day2 = 864e5;
2382
2464
  var now = Date.UTC(2026, 9, 5, 9, 0);
2383
2465
  function frozenConversation(topic, reply) {
2384
2466
  return [
2385
- { id: `f-${topic}-1`, role: "user", text: topic, createdAt: now - 3 * day },
2467
+ { id: `f-${topic}-1`, role: "user", text: topic, createdAt: now - 3 * day2 },
2386
2468
  {
2387
2469
  id: `f-${topic}-2`,
2388
2470
  role: "assistant",
2389
2471
  status: "done",
2390
- createdAt: now - 3 * day + 6e4,
2472
+ createdAt: now - 3 * day2 + 6e4,
2391
2473
  parts: [{ type: "text", text: reply }]
2392
2474
  }
2393
2475
  ];
@@ -2401,7 +2483,7 @@ var frozen = [
2401
2483
  title: "Write the onboarding guide for new contributors",
2402
2484
  model: "claude-sonnet-5-5",
2403
2485
  messages: 42,
2404
- frozenAt: now - 2 * day
2486
+ frozenAt: now - 2 * day2
2405
2487
  },
2406
2488
  messages: frozenConversation(
2407
2489
  "Write the onboarding guide for new contributors",
@@ -2416,7 +2498,7 @@ var frozen = [
2416
2498
  title: "Spike: can the cache move to KV without a cold start?",
2417
2499
  model: "claude-opus-5-5",
2418
2500
  messages: 118,
2419
- frozenAt: now - 6 * day
2501
+ frozenAt: now - 6 * day2
2420
2502
  },
2421
2503
  messages: frozenConversation(
2422
2504
  "Spike: can the cache move to KV without a cold start?",
@@ -2431,7 +2513,7 @@ var frozen = [
2431
2513
  title: "Summarize Q3 uptime and incidents",
2432
2514
  model: "claude-haiku-4-5",
2433
2515
  messages: 17,
2434
- frozenAt: now - 21 * day
2516
+ frozenAt: now - 21 * day2
2435
2517
  },
2436
2518
  messages: frozenConversation(
2437
2519
  "Summarize Q3 uptime and incidents",
@@ -2446,7 +2528,7 @@ var frozen = [
2446
2528
  title: "Check the tide tables against the harbor feed",
2447
2529
  model: "claude-haiku-4-5",
2448
2530
  messages: 9,
2449
- frozenAt: now - 1 * day
2531
+ frozenAt: now - 1 * day2
2450
2532
  },
2451
2533
  messages: frozenConversation(
2452
2534
  "Check the tide tables against the harbor feed",
@@ -2461,7 +2543,7 @@ var frozen = [
2461
2543
  title: "Write the nightly digest of merged pull requests",
2462
2544
  model: "claude-sonnet-5-5",
2463
2545
  messages: 6,
2464
- frozenAt: now - 3 * day
2546
+ frozenAt: now - 3 * day2
2465
2547
  },
2466
2548
  messages: frozenConversation(
2467
2549
  "Write the nightly digest of merged pull requests",
@@ -2477,7 +2559,7 @@ var memories = [
2477
2559
  title: "Lead with the result",
2478
2560
  body: "Keep replies short. Lead with the result, then the evidence.",
2479
2561
  tag: "feedback",
2480
- updatedAt: now - 5 * day
2562
+ updatedAt: now - 5 * day2
2481
2563
  },
2482
2564
  {
2483
2565
  id: "mem-2",
@@ -2486,7 +2568,7 @@ var memories = [
2486
2568
  title: "Tests before \u201Cfixed\u201D",
2487
2569
  body: "Run the tests before saying something is fixed, and quote the summary line.",
2488
2570
  tag: "feedback",
2489
- updatedAt: now - 9 * day
2571
+ updatedAt: now - 9 * day2
2490
2572
  },
2491
2573
  {
2492
2574
  id: "mem-3",
@@ -2495,7 +2577,7 @@ var memories = [
2495
2577
  title: "Works in UTC",
2496
2578
  body: "Schedules, timestamps and cron expressions are in UTC unless stated otherwise.",
2497
2579
  tag: "user",
2498
- updatedAt: now - 30 * day
2580
+ updatedAt: now - 30 * day2
2499
2581
  },
2500
2582
  {
2501
2583
  id: "mem-4",
@@ -2504,7 +2586,7 @@ var memories = [
2504
2586
  title: "harbor",
2505
2587
  body: "Forecast API, deployed as a worker. Cache bugs show up around midnight UTC. Staging deploys are frozen on Fridays.",
2506
2588
  tag: "project",
2507
- updatedAt: now - day
2589
+ updatedAt: now - day2
2508
2590
  },
2509
2591
  {
2510
2592
  id: "mem-5",
@@ -2513,7 +2595,7 @@ var memories = [
2513
2595
  title: "release-notes",
2514
2596
  body: "Drafted every Friday from merged pull requests, grouped as features, fixes and chores.",
2515
2597
  tag: "project",
2516
- updatedAt: now - 4 * day
2598
+ updatedAt: now - 4 * day2
2517
2599
  },
2518
2600
  {
2519
2601
  id: "mem-6",
@@ -2522,7 +2604,7 @@ var memories = [
2522
2604
  title: "Cache keys",
2523
2605
  body: "Cache keys are `city:UTC-date`. Never key on local time.",
2524
2606
  tag: "project",
2525
- updatedAt: now - 2 * day
2607
+ updatedAt: now - 2 * day2
2526
2608
  },
2527
2609
  {
2528
2610
  id: "mem-7",
@@ -2531,7 +2613,7 @@ var memories = [
2531
2613
  title: "Retries",
2532
2614
  body: "Retry upstream calls three times with exponential backoff starting at 250 ms.",
2533
2615
  tag: null,
2534
- updatedAt: now - 2 * day
2616
+ updatedAt: now - 2 * day2
2535
2617
  },
2536
2618
  {
2537
2619
  id: "mem-8",
@@ -2540,7 +2622,7 @@ var memories = [
2540
2622
  title: "Past tense, no ticket numbers",
2541
2623
  body: "One sentence per change, in the past tense, without ticket numbers.",
2542
2624
  tag: "feedback",
2543
- updatedAt: now - 7 * day
2625
+ updatedAt: now - 7 * day2
2544
2626
  },
2545
2627
  {
2546
2628
  id: "mem-9",
@@ -2549,7 +2631,7 @@ var memories = [
2549
2631
  title: "Read the tests first",
2550
2632
  body: "Start a review from the tests, then read the change they cover.",
2551
2633
  tag: "feedback",
2552
- updatedAt: now - 3 * day,
2634
+ updatedAt: now - 3 * day2,
2553
2635
  source: "entry"
2554
2636
  },
2555
2637
  {
@@ -2559,7 +2641,7 @@ var memories = [
2559
2641
  title: "Checks",
2560
2642
  body: "Flag any change to cache keys. Ask for a test when a retry count changes.",
2561
2643
  tag: null,
2562
- updatedAt: now - 3 * day,
2644
+ updatedAt: now - 3 * day2,
2563
2645
  source: "notes"
2564
2646
  }
2565
2647
  ];
@@ -5943,8 +6025,8 @@ Done in 3.8s`
5943
6025
 
5944
6026
  // src/dummy/vault.ts
5945
6027
  var minute2 = 6e4;
5946
- var hour = 60 * minute2;
5947
- var day2 = 24 * hour;
6028
+ var hour2 = 60 * minute2;
6029
+ var day3 = 24 * hour2;
5948
6030
  var harbor = { domain: "api.harbor.example", header: "Authorization", methods: [], paths: [] };
5949
6031
  var pier = {
5950
6032
  domain: "packages.pier.example",
@@ -5965,7 +6047,7 @@ function vaultSecrets(now2 = Date.now()) {
5965
6047
  rules: [harbor],
5966
6048
  exits: ["proxy", "tailscale", "device:link-pier"],
5967
6049
  exec: false,
5968
- createdAt: now2 - 12 * day2,
6050
+ createdAt: now2 - 12 * day3,
5969
6051
  usedAt: now2 - 4 * minute2
5970
6052
  },
5971
6053
  {
@@ -5979,9 +6061,9 @@ function vaultSecrets(now2 = Date.now()) {
5979
6061
  rules: [pier],
5980
6062
  exec: "argv",
5981
6063
  execArgv: [["pier", "publish"]],
5982
- createdAt: now2 - 3 * day2,
5983
- usedAt: now2 - 2 * hour,
5984
- expiresAt: now2 + 3 * day2
6064
+ createdAt: now2 - 3 * day3,
6065
+ usedAt: now2 - 2 * hour2,
6066
+ expiresAt: now2 + 3 * day3
5985
6067
  },
5986
6068
  {
5987
6069
  name: "TIDE_MAIL",
@@ -6008,7 +6090,7 @@ function vaultSecrets(now2 = Date.now()) {
6008
6090
  }
6009
6091
  ],
6010
6092
  exec: false,
6011
- createdAt: now2 - 20 * day2,
6093
+ createdAt: now2 - 20 * day3,
6012
6094
  expiresAt: now2 + 50 * minute2
6013
6095
  },
6014
6096
  {
@@ -6021,7 +6103,7 @@ function vaultSecrets(now2 = Date.now()) {
6021
6103
  domains: [],
6022
6104
  rules: [],
6023
6105
  exec: "any",
6024
- createdAt: now2 - 2 * hour
6106
+ createdAt: now2 - 2 * hour2
6025
6107
  },
6026
6108
  {
6027
6109
  name: "BUOY_OLD_TOKEN",
@@ -6033,8 +6115,8 @@ function vaultSecrets(now2 = Date.now()) {
6033
6115
  domains: ["buoy.example"],
6034
6116
  rules: [{ domain: "buoy.example", header: "X-Buoy-Key", methods: ["GET"], paths: [] }],
6035
6117
  exec: "shell",
6036
- createdAt: now2 - 40 * day2,
6037
- removedAt: now2 - day2
6118
+ createdAt: now2 - 40 * day3,
6119
+ removedAt: now2 - day3
6038
6120
  }
6039
6121
  ];
6040
6122
  }
@@ -6051,7 +6133,7 @@ function accountRecords(now2 = Date.now()) {
6051
6133
  domains: ["docs.example"],
6052
6134
  rules: [{ domain: "docs.example", header: "Authorization", methods: [], paths: [] }],
6053
6135
  exec: false,
6054
- createdAt: now2 - 6 * day2,
6136
+ createdAt: now2 - 6 * day3,
6055
6137
  usedAt: now2 - 20 * minute2
6056
6138
  }
6057
6139
  ];
@@ -6077,7 +6159,7 @@ function vaultAudit(now2 = Date.now()) {
6077
6159
  type: "materialize",
6078
6160
  seq: 11,
6079
6161
  id: "audit-11",
6080
- at: now2 - 2 * hour,
6162
+ at: now2 - 2 * hour2,
6081
6163
  name: "PIER_PUBLISH_TOKEN",
6082
6164
  exec: "argv",
6083
6165
  path: "/usr/bin/pier",
@@ -6093,7 +6175,7 @@ function vaultAudit(now2 = Date.now()) {
6093
6175
  type: "materialize",
6094
6176
  seq: 10,
6095
6177
  id: "audit-10",
6096
- at: now2 - 3 * hour,
6178
+ at: now2 - 3 * hour2,
6097
6179
  name: "PIER_PUBLISH_TOKEN",
6098
6180
  uid: 1001,
6099
6181
  tag: "docs-pass",
@@ -6112,7 +6194,7 @@ function vaultAudit(now2 = Date.now()) {
6112
6194
  type: "forward",
6113
6195
  seq: 9,
6114
6196
  id: "audit-9",
6115
- at: now2 - 6 * hour,
6197
+ at: now2 - 6 * hour2,
6116
6198
  name: "HARBOR_API_KEY",
6117
6199
  domain: "api.harbor.example",
6118
6200
  method: "POST",
@@ -6124,7 +6206,7 @@ function vaultAudit(now2 = Date.now()) {
6124
6206
  type: "policy",
6125
6207
  seq: 8,
6126
6208
  id: "audit-8",
6127
- at: now2 - day2,
6209
+ at: now2 - day3,
6128
6210
  file: "/etc/slicc/secrets-policy.toml",
6129
6211
  sha256: "4e07408562bedb8b60ce05c1decfe3ad",
6130
6212
  accepted: true,
@@ -6142,7 +6224,7 @@ function vaultAudit(now2 = Date.now()) {
6142
6224
  type: "remove",
6143
6225
  seq: 7,
6144
6226
  id: "audit-7",
6145
- at: now2 - day2 - hour,
6227
+ at: now2 - day3 - hour2,
6146
6228
  name: "BUOY_OLD_TOKEN"
6147
6229
  },
6148
6230
  {
@@ -6150,7 +6232,7 @@ function vaultAudit(now2 = Date.now()) {
6150
6232
  type: "seal",
6151
6233
  seq: 6,
6152
6234
  id: "audit-6",
6153
- at: now2 - 3 * day2,
6235
+ at: now2 - 3 * day3,
6154
6236
  name: "PIER_PUBLISH_TOKEN",
6155
6237
  source: "jerk:cone@link-pier",
6156
6238
  rotated: false
@@ -6163,7 +6245,7 @@ function vaultOlder(now2 = Date.now()) {
6163
6245
  type: "materialize",
6164
6246
  seq: 5,
6165
6247
  id: "audit-5",
6166
- at: now2 - 4 * day2,
6248
+ at: now2 - 4 * day3,
6167
6249
  name: "PIER_PUBLISH_TOKEN",
6168
6250
  uid: 1e3,
6169
6251
  tag: null,
@@ -6181,7 +6263,7 @@ function vaultOlder(now2 = Date.now()) {
6181
6263
  type: "policy",
6182
6264
  seq: 4,
6183
6265
  id: "audit-4",
6184
- at: now2 - 5 * day2,
6266
+ at: now2 - 5 * day3,
6185
6267
  file: "/etc/fnox/config.toml",
6186
6268
  sha256: "8f14e45fceea167a5a36dedd4bea2543",
6187
6269
  accepted: false,
@@ -6192,7 +6274,7 @@ function vaultOlder(now2 = Date.now()) {
6192
6274
  type: "restore",
6193
6275
  seq: 3,
6194
6276
  id: "audit-3",
6195
- at: now2 - 6 * day2,
6277
+ at: now2 - 6 * day3,
6196
6278
  name: "TIDE_MAIL",
6197
6279
  uid: 0,
6198
6280
  tag: null
@@ -6201,7 +6283,7 @@ function vaultOlder(now2 = Date.now()) {
6201
6283
  type: "mcp",
6202
6284
  seq: 2,
6203
6285
  id: "audit-2",
6204
- at: now2 - 7 * day2,
6286
+ at: now2 - 7 * day3,
6205
6287
  name: "DOCS_MCP_TOKEN",
6206
6288
  uid: 1e3,
6207
6289
  tag: "cone",
@@ -6643,6 +6725,9 @@ function vaultAccounts(accounts2) {
6643
6725
  (account) => account.auth === "api-key" || account.id === "github" ? { ...account, store: "vault" } : account
6644
6726
  );
6645
6727
  }
6728
+ function guestsPort(guests, time) {
6729
+ return guests === "off" ? {} : { guests: new DummyGuests(guests, time) };
6730
+ }
6646
6731
  function createDummyModel({
6647
6732
  delay = 30,
6648
6733
  storage = null,
@@ -6660,6 +6745,7 @@ function createDummyModel({
6660
6745
  approvals = "off",
6661
6746
  follow = "off",
6662
6747
  vault = "off",
6748
+ guests = "off",
6663
6749
  accounts: accounts2 = "settings",
6664
6750
  providers = "connected",
6665
6751
  hold = "off",
@@ -6736,7 +6822,8 @@ function createDummyModel({
6736
6822
  ...follow === "off" ? {} : { follow: new DummyFollow(follow, time) },
6737
6823
  ...vault === "off" ? {} : {
6738
6824
  vault: new DummyVault(vault, time, accounts2 === "vault" ? accountRecords() : [])
6739
- }
6825
+ },
6826
+ ...guestsPort(guests, time)
6740
6827
  };
6741
6828
  base.vault?.on("status", (status2) => {
6742
6829
  if (!status2.locked && agent.hold()?.reason === "locked") agent.setHold(null);
@@ -6751,6 +6838,7 @@ export {
6751
6838
  DummyBrowser,
6752
6839
  DummyChanges,
6753
6840
  DummyFollow,
6841
+ DummyGuests,
6754
6842
  DummyNetwork,
6755
6843
  DummyNotices,
6756
6844
  DummyTabs,
@@ -6762,6 +6850,9 @@ export {
6762
6850
  browserHistory,
6763
6851
  createDummyModel,
6764
6852
  followFixtures,
6853
+ guestsFixtures,
6854
+ guestsRefused,
6855
+ guestsUnavailable,
6765
6856
  joinUrl,
6766
6857
  leadersFixtures,
6767
6858
  linksFixtures,