@ai-ecoverse/slicc-spectrum 1.112.1 → 2.0.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
@@ -326,7 +326,7 @@ The model's `notices` as in-line alerts with their actions and a close button (n
326
326
 
327
327
  The model's waiting `approvals` as a modal alert dialog, one at a time, oldest first; `<slicc-app>` mounts it.
328
328
  Renders nothing without `approvals.decide`.
329
- **Always allow** (**Always allow in this directory**, with the cwd in its hint, when the approval has `alwaysScope: 'cwd'` and a `cwd`) shows when `offersAlways(approval)` is true, and the element's `decide()` sends `always` only when it holds for the live approval with that id in `list()`, so a stale dialog can't send it after the request lost it. With a level 1, 2 or 3 verdict, `offersAlways` is the approval's `always`: the embedder decides. Until every embedder sends levels, an approval without one (any other `level` counts as none) keeps the old gates: a sudo request with a `deny` decision or one `secretRules()` flags offers no Always allow, and the flagged one shows the old warning (`[data-secret-rules]`, the path or token) or the once-only hint. These gates go with the `secretRules` export.
329
+ **Always allow** (**Always allow in this directory**, with the cwd in its hint, when the approval has `alwaysScope: 'cwd'` and a `cwd`) shows when `offersAlways(approval)` is true, and the element's `decide()` sends `always` only when it holds for the live approval with that id in `list()`, so a stale dialog can't send it after the request lost it. For a sudo request, `offersAlways` is true only when `always` is true and the verdict has a level 1, 2 or 3: a level-less sudo (an older agent) never offers Always allow, even when `always` is true. Guest requests still offer Always allow whenever `always` is true. Tray requests never do.
330
330
  The command (`[data-command]`, or a guest tool call's `[data-summary]`) wraps in full; past four lines the dialog shows the first four and **Show the full command** (as in the chat, `data-action="full-command"` with `data-shown`), and shown, it scrolls inside its own box of at most `min(16lh, 50dvh)`. Always allow, its hint and Allow once wait until the end of the command has been in view (a zero-size `[data-end]` after its last line), and so does the element's `decide()` for anything but `deny`; once seen, the end stays seen while the request waits, and hiding the command again hides them again. Until then a subdued line (`[data-allow-hint]`) says "Show the full command to allow it." or, shown, "Scroll to the end of the command to allow it." The dialog keeps one width, so only its height changes. A closed request shows its command whole.
331
331
  Control characters in the sudo command, target, requester name and place, and cwd are shown: C0 controls use Unicode Control Pictures (including ␍ for CR, ␊ for LF and ␉ for TAB), DEL uses ␡, and C1, format, default-ignorable characters and line/paragraph separators use uppercase `U+XXXX` notation (at least four digits). LF keeps its line break after ␊ in the command and shows only ␊ in paragraphs; ordinary spaces stay spaces. Each shown control has a chip with its character name or notation as its accessible label and title; Control Pictures also show a readable abbreviation or notation. Joiners and variation selectors use the same chips. The clamped and full views show the same symbols.
332
332
  Sudo and guest requests show the verdict under the command, in a live region (`.judge[aria-live=polite]`). A level verdict is a block (`.judged[data-verdict][data-level]`): level 1 "Routine." (`routine`, positive subtle background), level 2 "Changes permissions." (`permissions`, notice) or level 3 "Could harm the system." (`harm`, negative, with the alert-triangle icon), then the reason, then "It fits what you asked for." or "It isn’t clear it serves what you asked for." when `aligned` is set. While `judging` it reads "Checking…" (`checking`); with no verdict and a `verdictError`, "No verdict: {verdictError}" (`none`). A verdict with only a `decision` keeps the line "The automatic check would {decision} this: {reason}" or "The automatic check wasn’t sure: {reason}" (`p.verdict[data-verdict]`, the decision in the attribute). The reason and `verdictError` show control characters as chips, as in the command, and are cut at 300 characters. A sudo request without Always allow says "Always allow isn’t offered for permission changes." at level 2 and "Always allow appears only with a level 1 verdict." with no verdict (`[data-verdict-note]`). No level adds a confirm step: Deny and Allow once work at every level.
@@ -677,34 +677,6 @@ import { prompt } from './dist/slicc-ui.js';
677
677
  const name = await prompt({ title: 'New cone', label: 'Name', action: 'Create', submit: (name) => model.agent.createCone(name).then(() => {}) });
678
678
  ```
679
679
 
680
- ### Secret rules check
681
-
682
- `secretRules(command, cwd)`, exported from `dist/slicc-ui.js` (`@ai-ecoverse/slicc-spectrum/ui`), is the check behind the sudo dialog's secret-rules warning.
683
- It takes a sudo request's command (without `sudo` and its options) and working folder, and returns `{ certain, maybe, once }`:
684
-
685
- - `certain` is an absolute path when the command names a file or folder holding SLICC's secret rules (`/etc/fnox`, `/etc/fnox/config.toml`, `/etc/slicc/secrets-policy.toml`), a folder above them it changes as a whole (`/`, `/etc`, `/etc/slicc`), a glob or brace that matches one, or a file named like them (`fnox`, `secrets-policy`) anywhere, as `/tmp/fnox/config.toml`.
686
- It sees through quotes, backslashes, `$'…'` and `$"…"`, `cd` and `pushd` where a command starts (also inside `bash -c`, `bash -lc` or `su -c`, and after `if`, `then`, `do`, `{`, `!`, `function NAME` and the like, but not after a `-c` of another program, such as `chown -c cd /etc`), relative paths and `..`, extglobs, `/proc/self/root` and `/proc/self/cwd`, option clusters like `-t/etc/fnox`, and comma lists.
687
- When every command in it is read-only and nothing in it expands, quoted text is data: a jq or yq program such as `'.a // .b | {x: .y}'` isn't split into paths, and `*`, `?`, `[` and `{` in quotes (and `$` in single quotes) are literal, as the shell treats them. Other commands, such as `sh -c '…'`, are still looked into.
688
- - `maybe` is the token when the command may reach one of them and the check can't tell where it leads: an expansion (`$X`, `$(…)`, backticks), a `~user/..`, `~+` or `~-` path, a `/proc/<pid>/cwd` or `/dev/fd` path, or a command over 4096 characters (its first 40 followed by `…`).
689
- It's also set, only when nothing above applies (a separate check that never clears or changes those verdicts), when an explicit write names its target from data the check can't see: `yq -s`, `--split-exp` or `--split-exp-file` (`yq -s`), or `xargs` running a command that can write or run others, such as `xargs rm < list` (`xargs rm`). `xargs` running a read-only command (`xargs cat < list`, `xargs grep x -- < list`, `xargs --replace=X cat X < list`, and `xargs sort`, `xargs find`, `xargs uniq -c` or `xargs yq .` with no write flag in the command; with `--replace` or `--eof` split from its value, both readings must be read-only) is a plain read and stays clean.
690
- - `once` is set, when the other two are `null`, to `xargs` and the program when `xargs` runs a program that's read-only only without its write flags (`sort`, `find`, `file`, `tree`, `base64`, `uniq`, `xxd`, `yq`) and shows none: `xargs sort < list` gives `once: 'xargs sort'`. The command is a plain read, but `xargs` adds arguments from its input, which could be a write flag (`printf -- '-o\n/etc/slicc/secrets-policy.toml\n' | xargs sort`) or, for `uniq` and `xxd`, an output file. So it can be allowed once but not always. `xargs cat`, `xargs grep` and other programs without write flags leave it `null`. No other read-only program takes arguments from data.
691
- Leading assignments (`A=1`, `A+=1`, `a[1]=x`) and the wrappers `command`, `env`, `exec`, `ionice`, `nice`, `nohup`, `setsid`, `stdbuf`, `time` and `timeout`, with their options (abbreviated long ones too), don't hide `xargs` or `yq` (`command xargs rm < list`, `env -i A=1 nice -n 5 xargs rm < list`, `timeout --sig KILL 5 xargs rm < list`); `env -S`/`--split-string` is split into the command it runs (`env -S'xargs rm'`; one with a `\` escape or `$` gives `env -S`), a string run by a shell's `-c` (also in a cluster, `bash -lc '…'`, after `sh`, `bash`, `dash`, `zsh`, `ksh` or `fish`) or by `eval` is looked into, the word after `!`, `{`, `if`, `then`, `else`, `elif`, `do`, `while`, `until` and `coproc` is a command, and `command -v xargs` runs nothing.
692
-
693
- All three are `null` for a clean command, and at most one is set.
694
- The dialog shows "This changes secret rules" for `certain` and "This may change secret rules" for `maybe`, and offers no Always allow for either.
695
- For `once` it shows no notice and offers no Always allow, with "Always allow isn’t offered: xargs adds arguments from its input, and they could include a write flag." (`[data-once-only]`, the program in the attribute) when the request has `always`.
696
- The dialog runs this check only for a sudo request without a level verdict (see `<slicc-approvals>`); with a level, the judge's verdict replaces it.
697
- An embedder that records "always" decisions itself, such as slicc-bios, must refuse "always" (answer "once" at most) whenever any of the three is set, so a decision sent without the dialog can't add a rule that runs it unasked.
698
-
699
- ```js
700
- import { secretRules } from './dist/slicc-ui.js';
701
-
702
- secretRules("bash -c 'rm /etc/fn\\ox/config.toml'");
703
- secretRules('rm -rf $TARGET', '/home/cone');
704
- ```
705
-
706
- The first gives `{ certain: '/etc/fnox/config.toml', maybe: null, once: null }`, the second `{ certain: null, maybe: '$TARGET', once: null }`. The result's type is `SecretRulesVerdict`.
707
-
708
680
  ### Sprinkle bridge
709
681
 
710
682
  A sprinkle's frame gets `window.slicc` with `name` and `lick(event)`. When the model's `sprinkles` port has `call`, it also gets:
@@ -971,15 +943,9 @@ An `alone` tab shows only the brand and "SLICC is open in another tab."
971
943
 
972
944
  Later backed by: BIOS.
973
945
  `list()` returns waiting `Approval`s; emit `approvals` when the list changes.
974
- 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` (offer Always allow, which covers the requester's user and this exact command).
975
- 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`) and **Allow once** (`once`).
976
- 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`, `grep`, `jq`, `cut`, `od`, `sha256sum` or `md5sum`, and coreutils' pure output programs, which take no output file and run nothing: `basename`, `dirname`, `printf`, `seq`, `tr`, `fold`, `paste`, `comm`, `join`, `expand`, `unexpand`, `fmt`, `numfmt`, `pr`, `expr`, `factor`, `sum`, `tsort`, `pathchk`, `base32`, `basenc`, `true`, `false`, `id`, `groups`, `whoami`, `logname`, `printenv`, `uname` and `nproc`, so `ls | xargs -n1 basename` is a read; `find` without `-exec`, `-execdir`, `-ok`, `-okdir`, `-delete` or `-fprint`/`-fls`; `sort` without `-o`, `-T` or `--compress-program`; `yq` without `-i`, `-s` or `--split-exp-file`; `base64` without `-o`; `tree` without `-o` or `-R`; `file` without `-C`; `uniq` and `xxd` with one operand at most, counting everything after `--`; a flag counts inside a short cluster, with a value attached (`-o/x`, `-i.bak`), as `--name=value` and abbreviated, but for getopt-style programs (`sort`, `uniq`, `base64`, `file`, `yq`) a cluster ends at `=` or at an option that takes a value, so `sort -to /etc` and `yq -o=json` don't count as writes; `tree` takes each option's value from the next word and reads on in the cluster, so any `o` or `R` in a `tree` cluster counts (`tree -Ho /tmp/x /etc` writes); `xxd` is read with its own rules (one option per word, values attached or in the next word, options end at the first operand), and an option's value isn't a `uniq` or `xxd` operand (`uniq -f 1 /etc`, `xxd -l 16 /etc`); `xargs` counts as read-only when every command it may run is read-only and has no gated write flags (`find /etc … | xargs grep x`, but not `xargs sort`, whose input could add `-o`); `less` and other pagers aren't, since they run commands), 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).
977
- Read-only commands count only by bare name or from `/bin` and `/usr/bin`.
978
- In a command made only of read-only commands and with no live expansion, the quoted words count whole: `jq '.a // .b' f` doesn't name `/`, and `jq '{a: .b}' f` in `/etc` isn't a brace that could lead to the rules.
979
- `/proc/<pid>/root` counts as `/` and `/proc/self/cwd` as `cwd`.
980
- 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`, an explicit write whose target comes from data (`yq -s`, `--split-exp`, `--split-exp-file`, or `xargs` running a command that isn't read-only), or more than 4096 characters, gets "This may change secret rules." with the first such part in the attribute.
981
- Other symlinks aren't followed.
982
- For a request without a level verdict, 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. The same goes for `xargs` running a gated read-only program (`once`), which shows no notice but says why Always allow isn’t offered. With a level verdict none of this applies.
946
+ 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).
947
+ 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`).
948
+ 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.
983
949
  For half a second after a request shows, only Deny works, so a double-click can't approve it.
984
950
  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.
985
951
  Pass `decide` in every tab: the first decision wins.
@@ -1118,7 +1084,7 @@ The dummy's `addServer` and `removeServer` record each call in `settings.serverE
1118
1084
  `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.
1119
1085
  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.
1120
1086
 
1121
- `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 keeps the old warning and offers no Always allow), 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."
1087
+ `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."
1122
1088
  (`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`).
1123
1089
  `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.
1124
1090
  `createDummyModel({ asks: 'long' })` (`?asks=long`) adds a pending sudo request with that seven-line command to kitchen-sink (`k-long`).
@@ -232,7 +232,12 @@ function approvalFixtures(scenario, now2 = Date.now()) {
232
232
  cwd: "/home/cone/harbor",
233
233
  createdAt: now2,
234
234
  expiresAt: now2 + (scenario === "soon" ? 0.4 * minutes : 5 * minutes),
235
- always: true
235
+ always: true,
236
+ verdict: {
237
+ level: 1,
238
+ aligned: true,
239
+ reason: "It installs a package the owner asked for."
240
+ }
236
241
  };
237
242
  const user = {
238
243
  id: "sudo-chown",
@@ -241,7 +246,12 @@ function approvalFixtures(scenario, now2 = Date.now()) {
241
246
  command: "chown -R cone /home/cone/shared",
242
247
  createdAt: now2 + 1,
243
248
  expiresAt: now2 + 5 * minutes,
244
- always: true
249
+ always: true,
250
+ verdict: {
251
+ level: 1,
252
+ aligned: true,
253
+ reason: "It hands the owner their own files."
254
+ }
245
255
  };
246
256
  const cache = {
247
257
  id: "sudo-cache",
@@ -251,7 +261,12 @@ function approvalFixtures(scenario, now2 = Date.now()) {
251
261
  target: "www-data",
252
262
  createdAt: now2 + 2,
253
263
  expiresAt: now2 + 5 * minutes,
254
- always: false
264
+ always: false,
265
+ verdict: {
266
+ level: 3,
267
+ aligned: false,
268
+ reason: "It deletes cache files that other services may need."
269
+ }
255
270
  };
256
271
  if (scenario === "judge-denied")
257
272
  return [
@@ -283,12 +298,30 @@ function approvalFixtures(scenario, now2 = Date.now()) {
283
298
  if (scenario.startsWith("tray")) return trayFixtures(scenario, now2);
284
299
  if (scenario.startsWith("guest-")) return [guestCall(scenario, now2)];
285
300
  if (scenario === "none") return [];
286
- if (scenario === "long") return [{ ...agent, id: "sudo-release", command: longCommand }];
301
+ if (scenario === "long")
302
+ return [
303
+ {
304
+ ...agent,
305
+ id: "sudo-release",
306
+ command: longCommand,
307
+ always: true,
308
+ verdict: {
309
+ level: 3,
310
+ aligned: false,
311
+ reason: "It pipes a remote script into a shell."
312
+ }
313
+ }
314
+ ];
287
315
  if (scenario === "controls-cr" || scenario === "controls-lf")
288
316
  return [
289
317
  {
290
318
  ...agent,
291
- command: `printf '%s' 'harbor${scenario === "controls-cr" ? "\r" : "\n"}schedule'`
319
+ command: `printf '%s' 'harbor${scenario === "controls-cr" ? "\r" : "\n"}schedule'`,
320
+ verdict: {
321
+ level: 1,
322
+ aligned: true,
323
+ reason: "It prints a short string and changes nothing."
324
+ }
292
325
  }
293
326
  ];
294
327
  if (scenario === "controls-place")
@@ -296,7 +329,12 @@ function approvalFixtures(scenario, now2 = Date.now()) {
296
329
  {
297
330
  ...agent,
298
331
  requester: { ...agent.requester, name: "docs\rpass", where: "harbor\rterminal\nseat" },
299
- target: "harbor\ruser"
332
+ target: "harbor\ruser",
333
+ verdict: {
334
+ level: 1,
335
+ aligned: true,
336
+ reason: "It installs a package as the named user."
337
+ }
300
338
  }
301
339
  ];
302
340
  if (scenario === "controls-cwd")