@ai-ecoverse/slicc-spectrum 2.9.2 → 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
@@ -963,10 +963,12 @@ An `alone` tab shows only the brand and "SLICC is open in another tab."
963
963
 
964
964
  Later backed by: BIOS.
965
965
  `list()` returns waiting `Approval`s; emit `approvals` when the list changes.
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) 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).
967
- 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.
968
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.
969
- 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.
970
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.
971
973
  Pass `decide` in every tab: the first decision wins.
972
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.
@@ -1137,8 +1139,8 @@ The dummy's `addServer` and `removeServer` record each call in `settings.serverE
1137
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.
1138
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.
1139
1141
 
1140
- `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."
1141
- (`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.
1142
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.
1143
1145
  `createDummyModel({ asks: 'long' })` (`?asks=long`) adds a pending sudo request with that seven-line command to kitchen-sink (`k-long`).
1144
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