@ai-ecoverse/slicc-spectrum 2.9.1 → 2.10.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 CHANGED
@@ -431,10 +431,11 @@ Sessions stay alive while hidden; `exit` closes the tab.
431
431
 
432
432
  What the agents' browser control is doing.
433
433
  It opens on the most recently driven tab (`.focused`), or on the tab overview: cards (`li[data-id]`, `data-controlled` while an agent drives the tab, `data-pulse` when another tab starts being driven, without switching to it) with a thumbnail, the agent and a small **Stop <agent>** (`data-action="stop-agent"`), and an address bar to open a URL.
434
- The focused tab has **All tabs** (`data-action="all-tabs"`), a tab picker (a list of tabs at 720 px and wider), the URL with **Copy** (`data-action="copy-url"`) and **Open** (`data-action="open-tab"`, activates the tab), who's using it (`.driving[data-driving=agent|user|none]`; a controlled tab without an agent reads **In use** and has no Stop) with a small **Stop <agent>** (`data-action="stop-agent"`) in the same row, which keeps its height with or without an agent, a view-only preview (`figure[data-view]`: `live` while watching with a frame under 3 s old, `shot` with "Updated N s ago", `none`) and the recent actions (`li[data-action-id][data-kind][data-status]`, failures with their error inline), the tab's own filtered first, then the latest 50, with **Show more** (`data-action="more-actions"`) adding 50 earlier ones each time up to all the port has ("Showing the latest N of M"); the list scrolls and takes focus.
434
+ The focused tab is preview-first: **All tabs** (`data-action="all-tabs"`) with a tab picker (a list of tabs at 720 px and wider) on one row, the URL with **Copy** (`data-action="copy-url"`) and **Open** (`data-action="open-tab"`, activates the tab) on the next, who's using it (`.driving[data-driving=agent|user|none]`; **Watching <agent>** while an agent drives, **In use** when a controlled tab has no agent and no Stop) with a small **Stop <agent>** (`data-action="stop-agent"`) in the same row, which keeps its height with or without an agent, a watching-only preview (`figure[data-view]`: `live` while watching with a frame under 3 s old, `shot` with "Updated N s ago", `none`; the caption says **Watching only**) that keeps a 16:10 frame, and a **Recent actions** feed (`.feed`) that fills the leftover height.
435
+ Recent actions (`li[data-action-id][data-kind][data-status]`, failures with their error inline) are newest first: the tab's own filtered first, then the latest 50, with **Show more** (`data-action="more-actions"`) adding 50 earlier ones each time up to all the port has ("Showing the latest N of M"); the list scrolls and takes focus.
436
+ Consecutive identical done actions fold into one row (`li[data-group]`) with a count (`×n`).
435
437
  Actions with a URL (`open`, `goto`, `request`) expand (`data-action="action-url"`, `aria-expanded`) to show the full URL, or the request line, under the shortened one.
436
- Below 480 px they fold into **Recent actions (n)**, a chevron disclosure counting all of them, with the last one showing.
437
- The tab the user opened (or **All tabs**), expanded URLs and an open **Recent actions** hold for every view of the panel on the same model, so a layout switch keeps them; a closed tab lets go, and the panel opens on the most recently driven tab again.
438
+ The tab the user opened (or **All tabs**) and expanded URLs hold for every view of the panel on the same model, so a layout switch keeps them; a closed tab lets go, and the panel opens on the most recently driven tab again.
438
439
  Labels, URLs and errors are text only.
439
440
  With no tabs and `browser.via` `null` in the network status, it says browser control isn't connected (`data-browser="none"`) with the extension link and the commands to install and run slicc (`curl -fsSL https://get.sliccy.ai/slicc | sh`, then `slicc`, or `npx sliccy@next` with Node).
440
441
  The Browser rail button carries a dot (`data-driven`) while a tab is in use.
@@ -962,10 +963,12 @@ An `alone` tab shows only the brand and "SLICC is open in another tab."
962
963
 
963
964
  Later backed by: BIOS.
964
965
  `list()` returns waiting `Approval`s; emit `approvals` when the list changes.
965
- 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` and `verdict` (`{ level?: 1 | 2 | 3, aligned?: boolean, decision?: 'allow' | 'deny' | 'unsure', reason }`, the judge's view: `level` 1 routine, 2 changes permissions, 3 could harm the system; `aligned`, whether it serves what the owner asked for; `decision`, the older shape, which may be missing), optional `judging` (`true` while the judge runs) and `verdictError` (why there is no verdict), `createdAt`, `expiresAt` (epoch ms) and `always` (for a sudo request with a level verdict, offer Always allow, which covers the requester's user and this exact command; without a level it is ignored).
966
- 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}", the verdict, 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`) when offered and **Allow once** (`once`).
966
+ 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` and `verdict` (`{ level?: 1 | 2 | 3, aligned?: boolean, decision?: 'allow' | 'deny' | 'unsure', reason }`, the judge's view: `level` 1 routine, 2 changes permissions, 3 could harm the system; `aligned`, whether it serves what the owner asked for; `decision`, the older shape, which may be missing), optional `judging` (`true` while the judge runs) and `verdictError` (why there is no verdict), `createdAt`, `expiresAt` (epoch ms), `always` (for a sudo request with a level verdict, offer Always allow, which covers the requester's user and this exact command; without a level it is ignored) and optional `stoppable` (`true` when the requester is a cone or scoop with a turn to stop).
967
+ With `decide(id, 'once' | 'always' | 'deny' | 'stop')`, 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}", the verdict, what Always allow does, "Denied automatically in m:ss." and how many more wait, with **Deny** (`data-action="deny"`, focused; Escape also denies), **Deny and stop** (`stop`) when `stoppable` is `true` on a request from an agent with an `agentId`, **Always allow** (`always`) when offered and **Allow once** (`once`).
968
+ **Deny and stop** sends `stop`: deny this request and stop the turn of the agent that asked. It is a deny, so like Deny it needs neither the half second below nor the end of the command in view, and like every button it waits while another decision is pending; `decide(id, 'stop')` is ignored unless the live request with that id is `stoppable`. An agent that doesn't know `stop` should reject the request as it does an unknown answer. When the stop withdraws the agent's other waiting requests, emit `ended` with `withdrawn` for each in the same update that settles the stopped one, so they close without a word and the next request from someone else shows.
969
+ A request with Deny and stop lays its buttons out in two rows: Deny and Deny and stop at the start of the first, Always allow and Allow once at the end of the second, so Always allow appearing or going with the verdict moves no other button. Set `stoppable` in the same item as the requester, never later: it decides the layout. Other requests keep one row, Deny at its start and Allow once at its end.
967
970
  A sudo request without a level verdict never offers Always allow, even when `always` is `true` (`decide(id, 'always')` is ignored for it); only Deny and Allow once show. With a level verdict, `always` alone decides whether Always allow is offered.
968
- For half a second (`arming`, in ms) after a request shows and after any change to its buttons (one appearing or going, being enabled or disabled, changing its label or moving), no button click counts, Deny's included: a mouse or touch click counts only when its press started after that half second, and a keyboard click only when it comes after it, so a double-click or a quick second key press can't answer the next request and a click aimed before Always allow appeared can't land on it. The buttons are checked on every render, when they or the dialog change size, and when the window resizes, so a resize that moves them counts at once rather than at the next countdown tick. `decide()` refuses anything but `deny` for that half second too; <kbd>Escape</kbd> denies at once.
971
+ For half a second (`arming`, in ms) after a request shows and after any change to its buttons (one appearing or going, being enabled or disabled, changing its label or moving), no button click counts, Deny's included: a mouse or touch click counts only when its press started after that half second, and a keyboard click only when it comes after it, so a double-click or a quick second key press can't answer the next request and a click aimed before Always allow appeared can't land on it. The buttons are checked on every render, when they or the dialog change size, and when the window resizes, so a resize that moves them counts at once rather than at the next countdown tick. `decide()` refuses anything but `deny` and `stop` for that half second too; <kbd>Escape</kbd> denies at once.
969
972
  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.
970
973
  Pass `decide` in every tab: the first decision wins.
971
974
  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.
@@ -1136,8 +1139,8 @@ The dummy's `addServer` and `removeServer` record each call in `settings.serverE
1136
1139
  `createDummyModel({ budget: 'plain' | 'adobe' | 'xai' })` (`?budget=` on `/ui/`, `plain` by default) sets the tray budget's `provider`: none, so it reads "Budget 32%", or Adobe or SuperGrok, which the tray and the monitor's budget vital name.
1137
1140
  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.
1138
1141
 
1139
- `createDummyModel({ approvals: 'agent' | 'user' | 'many' | 'follower' | 'fail' | 'soon' | 'elsewhere' | 'withdrawn' | 'secrets' | 'judge-denied' | 'level-1' | 'level-2' | 'level-3' | 'checking' | 'no-verdict' | 'legacy' | 'guest' | 'guest-follower' | 'guest-always' | 'long' | 'controls-cr' | 'controls-lf' | 'controls-place' | 'controls-cwd' | '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 (`judge-denied`) a terminal user’s sudo request the automatic check would deny, or one sudo request per verdict: `level-1` (docs-pass reading `/etc/slicc/secrets-policy.toml` in `/`, with Always allow), `level-2` (a terminal user editing it), `level-3` (quiet-otter running `rm -rf /`), `checking` (`judging`), `no-verdict` (a `verdictError`) and `legacy` (an older agent's `unsure` decision without a level and `always: true` for a copy over `/etc/fnox/config.toml`, which offers only Deny and Allow once), 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."
1140
- (`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`).
1142
+ `createDummyModel({ approvals: 'agent' | 'user' | 'many' | 'follower' | 'fail' | 'soon' | 'elsewhere' | 'withdrawn' | 'secrets' | 'judge-denied' | 'level-1' | 'level-2' | 'level-3' | 'checking' | 'no-verdict' | 'legacy' | 'guest' | 'guest-follower' | 'guest-always' | 'long' | 'controls-cr' | 'controls-lf' | 'controls-place' | 'controls-cwd' | 'guest-args' | 'guest-empty' | 'guest-truncated' | 'tray' | 'tray-url' | 'tray-rotate' | 'tray-unlink' | 'tray-expired' | 'stop' })` (`?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 (`judge-denied`) a terminal user’s sudo request the automatic check would deny, or one sudo request per verdict: `level-1` (docs-pass reading `/etc/slicc/secrets-policy.toml` in `/`, with Always allow), `level-2` (a terminal user editing it), `level-3` (quiet-otter running `rm -rf /`), `checking` (`judging`), `no-verdict` (a `verdictError`) and `legacy` (an older agent's `unsure` decision without a level and `always: true` for a copy over `/etc/fnox/config.toml`, which offers only Deny and Allow once), 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."
1143
+ (`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`), or (`stop`) four sudo requests: two `stoppable` ones from docs-pass, a terminal user's and a `stoppable` one from quiet-otter. Its `decide(id, 'stop')` on a `stoppable` request ends it as `answered` and withdraws every other sudo and capture request from the same agent in one update; `stop` on any other request is a plain deny.
1141
1144
  `controls-cr` and `controls-lf` show control characters in a sudo command's quoted word; `controls-place` shows them in the requester name, place and target; `controls-cwd` shows them in the cwd.
1142
1145
  `createDummyModel({ asks: 'long' })` (`?asks=long`) adds a pending sudo request with that seven-line command to kitchen-sink (`k-long`).
1143
1146
  `createDummyModel({ approvals: 'click' | 'click-remote' | 'capture' })` adds one capture request: a click request from sliccy for `media` to choose what to share (`click-share`, `here: true`), the same with `here: false` on Studio Mac (`click-remote`), or a guest capture from Seat 2 (Mara) through sliccy, recording the screen for up to 10 seconds with the microphone, with a level 2 verdict (`capture-screen`).
@@ -468,6 +468,19 @@ function sudoFixtures(scenario, now2) {
468
468
  alwaysScope: "cwd"
469
469
  }
470
470
  ];
471
+ if (scenario === "stop")
472
+ return [
473
+ { ...agent, stoppable: true },
474
+ {
475
+ ...agent,
476
+ id: "sudo-docs-build",
477
+ command: "npm run build --prefix /opt/harbor-docs",
478
+ createdAt: now2 + 1,
479
+ stoppable: true
480
+ },
481
+ { ...user, createdAt: now2 + 2 },
482
+ { ...cache, stoppable: true }
483
+ ];
471
484
  if (scenario === "user") return [user];
472
485
  if (scenario === "many") return [agent, user, cache];
473
486
  if (scenario === "elsewhere" || scenario === "withdrawn") return [agent, user];
@@ -537,7 +550,19 @@ var DummyApprovals = class extends Emitter {
537
550
  this.#quiet.add(id);
538
551
  await this.#vault.cancel(id);
539
552
  }
540
- this.end(id, "answered");
553
+ if (decision === "stop" && approval?.kind === "sudo" && approval.stoppable)
554
+ this.#stop(approval);
555
+ else this.end(id, "answered");
556
+ }
557
+ #stop(approval) {
558
+ const agent = approval.requester.agentId;
559
+ const ended = this.#approvals.filter(
560
+ (item) => item.id === approval.id || (item.kind === "sudo" || item.kind === "capture") && item.requester.agentId === agent
561
+ );
562
+ for (const item of ended)
563
+ this.emit("ended", { id: item.id, reason: item === approval ? "answered" : "withdrawn" });
564
+ this.#approvals = this.#approvals.filter((item) => !ended.includes(item));
565
+ this.emit("approvals", this.list());
541
566
  }
542
567
  };
543
568
 
@@ -721,10 +746,15 @@ var DummyBrowser = class extends Emitter {
721
746
  this.finish(action.id, "Stopped");
722
747
  if (changed) this.emit("tabs", this.list());
723
748
  }
724
- watch(tabId) {
749
+ #watch = null;
750
+ lastWatch() {
751
+ return this.#watch;
752
+ }
753
+ watch(tabId, size) {
725
754
  const tab = () => this.#tabs.find((candidate) => candidate.id === tabId);
726
755
  if (!tab()?.controlled) return () => {
727
756
  };
757
+ this.#watch = { tabId, size: size ? { ...size } : void 0 };
728
758
  const send = () => {
729
759
  const now2 = tab();
730
760
  if (!now2?.controlled) return;
@@ -732,7 +762,10 @@ var DummyBrowser = class extends Emitter {
732
762
  };
733
763
  const timer = setInterval(send, 500);
734
764
  void this.#clock.sleep(0).then(send);
735
- return () => clearInterval(timer);
765
+ return () => {
766
+ clearInterval(timer);
767
+ if (this.#watch?.tabId === tabId) this.#watch = null;
768
+ };
736
769
  }
737
770
  #changed() {
738
771
  this.emit("tabs", this.list());