@assuredloop/orca-bot-kit 0.25.6 → 0.26.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/src/launch.js CHANGED
@@ -40,6 +40,31 @@ export const bypassFlags = (harness) => APPROVAL[harness]['dangerously-skip'];
40
40
 
41
41
  export const HARNESSES = Object.keys(APPROVAL);
42
42
 
43
+ /** How wide a level is: `ask` is narrower than `auto`, and `auto` than `dangerously-skip`. */
44
+ export const approvalRank = (level) => ['ask', 'auto', 'dangerously-skip'].indexOf(level);
45
+
46
+ /**
47
+ * The harness flags that set approval or permissions, which only
48
+ * `obk permission approval` may change, not an extra argument (ADR 0037).
49
+ * Codex's `-a` and `-s` take their value glued on too.
50
+ */
51
+ const APPROVAL_FLAGS = {
52
+ claude: ['--permission-mode', '--dangerously-skip-permissions', '--allow-dangerously-skip-permissions', '--allowedTools', '--allowed-tools'],
53
+ codex: ['-a', '--ask-for-approval', '--approve-for-me', '--dangerously-bypass-approvals-and-sandbox', '-s', '--sandbox'],
54
+ };
55
+
56
+ /**
57
+ * Refuse extra arguments that set a session's approval or permissions on
58
+ * `harness`, before anything is written, naming the command that does it.
59
+ */
60
+ export function refuseApprovalArgs(harness, args) {
61
+ const found = (args ?? []).find((arg) => (APPROVAL_FLAGS[harness] ?? []).some((flag) => arg === flag
62
+ || arg.startsWith(`${flag}=`)
63
+ || (/^-[as]$/.test(flag) && arg.startsWith(flag) && !arg.startsWith('--'))));
64
+ if (found === undefined) return;
65
+ throw new Error(`--extra-arg=${found} sets the session's approval or permissions on ${harness}, and only obk permission approval changes those, after the user's yes. Nothing was changed.`);
66
+ }
67
+
43
68
  /** The level a session that names none runs at (ADR 0015). */
44
69
  export const DEFAULT_APPROVAL = 'auto';
45
70
 
@@ -1,28 +1,30 @@
1
1
  // The permission rules a bot may run with, written into its own Claude
2
- // settings, and only the ones its user said yes to (#344).
2
+ // settings: the kit's default set, and the rules its user said yes to (#344).
3
3
  //
4
4
  // A bot in auto mode is stopped by the harness's check for things the kit's own
5
- // rules tell every bot to do: read and send its mail, read a long message's
5
+ // rules tell every bot to do: run the kit's commands, read a long message's
6
6
  // body beside the bots folder, commit. A matching allow rule is settled before
7
- // that check (Claude Code's permissions docs), so the kit offers every Claude
8
- // bot a small default set, spelled with its real CLI and bots folder. The user
9
- // sees the exact rules and says yes; `bot change --allow` keeps that yes in
10
- // bot.yaml's `allow`, and the kit's code writes what `allow` holds into
11
- // `.claude/settings.json` in the bot folder, beside its hook (ADR 0022). A model
12
- // editing the file by hand may get the format wrong; code does not.
7
+ // that check (Claude Code's permissions docs), so the kit gives every bot a
8
+ // default set, spelled with its real CLI and bots folder, with nobody asked
9
+ // (#527, ADR 0036). A rule beyond it is the user's yes, which
10
+ // `permission allow` keeps in bot.yaml's `allow`. The kit's code writes what
11
+ // `allow` holds into `.claude/settings.json` in the bot folder, beside its hook
12
+ // (ADR 0022). A model editing the file by hand may get the format wrong; code
13
+ // does not.
13
14
  //
14
15
  // The kit owns only the entries `allow` holds. Any other entry in the file is
15
16
  // left where it is, and health names it. A rule the user says yes to taking
16
- // back leaves `allow` and the files through `bot change --disallow` (#360).
17
+ // back leaves `allow` and the files through `permission disallow` (#360).
17
18
  //
18
- // A bot on Codex gets the same yes in Codex's own form (#354, ADR 0028): each
19
+ // A bot on Codex gets the same rules in Codex's own form (#354, ADR 0039): each
19
20
  // `Bash(<words>:*)` becomes a `prefix_rule` of those words in
20
21
  // `.codex/rules/obk.rules`, a file the kit owns whole and rewrites from `allow`.
21
22
 
22
23
  import { accessSync, constants, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
23
24
  import path from 'node:path';
24
25
 
25
- import { leadsOutside } from './bot.js';
26
+ import { allowRules, leadsOutside } from './bot.js';
27
+ import { DEFAULT_COMMANDS } from './commands.js';
26
28
  import { readSettings } from './hooks.js';
27
29
  import { harnessOf, ownCli, shellWord } from './launch.js';
28
30
 
@@ -34,21 +36,21 @@ const CODEX_RULES = '.codex/rules';
34
36
  const CODEX_FILE = `${CODEX_RULES}/obk.rules`;
35
37
 
36
38
  /**
37
- * The rules every Claude bot is offered, in this order. The mail commands are
38
- * narrowed to this bots folder and spelled as the kit prints them in its
39
- * nudge and in `message to`'s answer, which is what a bot runs. `//` is Claude's
40
- * form for an absolute path.
39
+ * The rules every bot is given (ADR 0036): each of the kit's default commands,
40
+ * and none kept back for the user's yes (ADR 0037), narrowed to this bots folder and
41
+ * spelled as the kit prints them, which is what a bot runs; reading a long
42
+ * message; committing; and the check Orca's mail notice tells a session to
43
+ * run. `//` is Claude's form for an absolute path.
41
44
  */
42
45
  export function defaultRules(bots, cli = ownCli()) {
43
46
  const kit = shellWord(cli);
44
47
  const folder = shellWord(bots);
45
48
  return [
46
- `Bash(${kit} message check --bots ${folder}:*)`,
47
- `Bash(${kit} message send --bots ${folder}:*)`,
48
- `Bash(${kit} message to --bots ${folder}:*)`,
49
+ ...DEFAULT_COMMANDS.map((command) => `Bash(${kit} ${command} --bots ${folder}:*)`),
49
50
  `Read(/${bots}.messages/**)`,
50
51
  'Bash(git add:*)',
51
52
  'Bash(git commit:*)',
53
+ 'Bash(orca orchestration check --run:*)',
52
54
  ];
53
55
  }
54
56
 
@@ -73,7 +75,7 @@ const ANY = 'it lets the bot run any command';
73
75
  * Why a rule is broad, as the end of a sentence, or undefined for a narrow one
74
76
  * (#353). A charter's grants are written as narrow, exact rules only; one that
75
77
  * lets the bot run any command, a program with any arguments, or any file on
76
- * the disk or in the home is the user's to add by hand (ADR 0027).
78
+ * the disk or in the home is the user's to add by hand (ADR 0038).
77
79
  */
78
80
  export function broadness(rule) {
79
81
  if (typeof rule !== 'string') return undefined;
@@ -163,14 +165,14 @@ function commandBroadness(all) {
163
165
  }
164
166
 
165
167
  /**
166
- * Refuse the rules `--allow` was given when any is broad, before anything is
168
+ * Refuse the rules `permission allow` was given when any is broad, before anything is
167
169
  * written, naming the file the user can add it to themselves.
168
170
  */
169
171
  export function refuseBroad(home, rules) {
170
172
  for (const rule of rules) {
171
173
  const why = broadness(rule);
172
174
  if (why === undefined) continue;
173
- throw new Error(`--allow ${rule} is broad: ${why}. The kit writes only narrow, exact rules, such as Bash(gh pr merge:*), so nothing was written. If the user wants this rule for ${path.basename(home)}, they add it to ${path.join(home, FILE)} themselves.`);
175
+ throw new Error(`permission allow ${rule} is broad: ${why}. The kit writes only narrow, exact rules, such as Bash(gh pr merge:*), so nothing was written. If the user wants this rule for ${path.basename(home)}, they add it to ${path.join(home, FILE)} themselves.`);
174
176
  }
175
177
  }
176
178
 
@@ -200,14 +202,14 @@ export const runsOnCodex = (bot) => bot.harness === 'codex'
200
202
  || bot.sessions.some((session) => harnessOf(session, bot.harness) === 'codex');
201
203
 
202
204
  /**
203
- * The default rules this bot has not been allowed yet for one harness's file,
204
- * in the default order. Codex is not offered the Read rule: its sandbox reads
205
- * every file already.
205
+ * The default rules this bot is not allowed yet, in the default order. A bot
206
+ * only on Codex is not given the Read rule: its sandbox reads every file
207
+ * already.
206
208
  */
207
- function waitingFor(bots, home, bot, harness) {
209
+ function missingDefaults(bots, home, bot) {
208
210
  const allowed = allowOf(home, bot);
209
211
  return defaultRules(bots)
210
- .filter((rule) => harness === 'claude' || codexForm(rule).line !== undefined)
212
+ .filter((rule) => runsOnClaude(bot) || codexForm(rule).line !== undefined)
211
213
  .filter((rule) => !allowed.includes(rule));
212
214
  }
213
215
 
@@ -242,7 +244,7 @@ export function codexForm(rule) {
242
244
  }
243
245
 
244
246
  /**
245
- * Refuse the rules `--allow` was given for a bot that runs only on Codex when
247
+ * Refuse the rules `permission allow` was given for a bot that runs only on Codex when
246
248
  * Codex has no form for one, before anything is written: recorded, it would
247
249
  * let the bot do nothing.
248
250
  */
@@ -251,35 +253,39 @@ export function refuseNoCodexForm(bot, rules) {
251
253
  for (const rule of rules) {
252
254
  const { why } = codexForm(rule);
253
255
  if (why === undefined) continue;
254
- throw new Error(`--allow ${rule} is not for ${bot.name}: ${why}, and ${bot.name} runs only on Codex, so nothing was written.`);
256
+ throw new Error(`permission allow ${rule} is not for ${bot.name}: ${why}, and ${bot.name} runs only on Codex, so nothing was written.`);
255
257
  }
256
258
  }
257
259
 
258
- /** The one command that allows `rules` for `bot`, as the user can run it. */
259
- export const allowCommand = (bots, bot, rules) => [
260
- shellWord(ownCli()), 'bot', 'change', '--bots', shellWord(bots), '--bot', shellWord(bot),
261
- ...rules.flatMap((rule) => ['--allow', shellWord(rule)]),
262
- ].join(' ');
263
-
264
260
  /**
265
- * Write what the bot is allowed into the files of the harnesses it runs on, and
266
- * say what waits: one entry per file, Claude's first. With `keepGoing`, a file
267
- * the kit may not write is an entry with its `trouble` rather than a throw.
261
+ * Add the default rules the bot is not allowed yet to its `allow` (ADR 0036),
262
+ * then write what it is allowed into the files of the harnesses it runs on:
263
+ * one entry per file, Claude's first, each with `defaults`, the rules this run
264
+ * added. The harness files are written first and bot.yaml last, as
265
+ * `permission allow` writes them (#383). With `keepGoing`, a file the kit may
266
+ * not write is an entry with its `trouble` rather than a throw.
268
267
  */
269
268
  export function writePermissions(bots, home, bot, { keepGoing = false } = {}) {
270
- const writers = [[runsOnClaude, FILE, planClaude], [runsOnCodex, CODEX_FILE, planCodex]];
271
- return writers.filter(([runs]) => runs(bot)).map(([, file, plan]) => {
272
- try {
273
- return plan(bots, home, bot).write();
274
- } catch (error) {
275
- if (!keepGoing) throw error;
276
- return { bot: bot.name, file: path.join(home, file), written: [], waiting: [], trouble: error.message };
269
+ const writers = [[runsOnClaude, FILE], [runsOnCodex, CODEX_FILE]].filter(([runs]) => runs(bot));
270
+ let defaults = [];
271
+ try {
272
+ defaults = writers.length === 0 ? [] : missingDefaults(bots, home, bot);
273
+ if (defaults.length > 0) {
274
+ const { allow } = allowRules(bots, bot.name, defaults, { write: false });
275
+ const entries = allowIn(bots, home, { ...bot, allow })();
276
+ allowRules(bots, bot.name, defaults);
277
+ return entries.map((entry) => ({ ...entry, defaults }));
277
278
  }
278
- });
279
+ return writers.map(([runs]) => (runs === runsOnClaude ? planClaude : planCodex)(bots, home, bot).write())
280
+ .map((entry) => ({ ...entry, defaults }));
281
+ } catch (error) {
282
+ if (!keepGoing) throw error;
283
+ return writers.map(([, file]) => ({ bot: bot.name, file: path.join(home, file), written: [], defaults: [], trouble: error.message }));
284
+ }
279
285
  }
280
286
 
281
287
  /**
282
- * Check every file `bot change --allow` writes before any is written (#383):
288
+ * Check every file `permission allow` writes before any is written (#383):
283
289
  * bot.yaml, and the files of the harnesses the bot runs on, read and found
284
290
  * writable. `bot` is the bot as it will be, with the new rules in `allow`.
285
291
  * What comes back writes the harness files, one entry per file, Claude's
@@ -297,7 +303,7 @@ export function allowIn(bots, home, bot) {
297
303
  /**
298
304
  * What writing the bot's allowed rules into its Claude settings would do,
299
305
  * read and refused before anything is written: `{ answer, writes, write }`.
300
- * `write()` gives `{ bot, file, written, waiting }`, with `written` the rules
306
+ * `write()` gives `{ bot, file, written }`, with `written` the rules
301
307
  * it added.
302
308
  *
303
309
  * Only rules `allow` holds are written. Everything else in the file stays as it
@@ -307,8 +313,7 @@ export function allowIn(bots, home, bot) {
307
313
  function planClaude(bots, home, bot) {
308
314
  const file = path.join(home, FILE);
309
315
  const allowed = allowOf(home, bot);
310
- const waiting = waitingFor(bots, home, bot, 'claude');
311
- const answer = { bot: bot.name, file, written: [], waiting };
316
+ const answer = { bot: bot.name, file, written: [] };
312
317
  const none = { answer, writes: false, write: () => answer };
313
318
  if (allowed.length === 0) return none;
314
319
 
@@ -335,7 +340,7 @@ function planClaude(bots, home, bot) {
335
340
  * harnesses the bot runs on (#360): from its Claude settings every entry of
336
341
  * that exact text and nothing else, and its Codex rules rewritten from what
337
342
  * `allow` still holds. Only rules `allow` holds are taken back; one it does not
338
- * hold is refused, since the kit did not write it (ADR 0029).
343
+ * hold is refused, since the kit did not write it (ADR 0040).
339
344
  *
340
345
  * Everything is checked before anything is written, a file the kit could not
341
346
  * write included, bot.yaml too. What comes back writes the files: one entry per file,
@@ -347,6 +352,11 @@ export function takeBack(bots, home, bot, rules) {
347
352
  const allowed = allowOf(home, bot);
348
353
  const foreign = rules.find((rule) => !allowed.includes(rule));
349
354
  if (foreign !== undefined) throw new Error(notAllowed(home, bot, foreign));
355
+ // The next rules write would add it again (ADR 0040).
356
+ const kits = rules.find((rule) => defaultRules(bots).includes(rule));
357
+ if (kits !== undefined) {
358
+ throw new Error(`permission disallow ${kits} is one of the kit's default rules, which every bot has (#527). The kit writes it again at its next rules write, so it is not taken back, and nothing was changed.`);
359
+ }
350
360
  const after = { ...bot, allow: allowed.filter((rule) => !rules.includes(rule)) };
351
361
  const taken = [...new Set(rules)];
352
362
  // bot.yaml is written last, and a harness file taken out of step with it
@@ -380,7 +390,7 @@ export function takeBack(bots, home, bot, rules) {
380
390
  const allow = present.filter((rule) => !removed.includes(rule));
381
391
  writeFileSync(file, `${JSON.stringify({ ...settings, permissions: { ...settings.permissions, allow } }, null, 2)}\n`);
382
392
  }
383
- entries.push({ bot: bot.name, file, written: [], removed, waiting: waitingFor(bots, home, after, 'claude') });
393
+ entries.push({ bot: bot.name, file, written: [], removed });
384
394
  }
385
395
  if (runsOnCodex(after)) {
386
396
  const entry = planCodex(bots, home, after).write();
@@ -413,7 +423,7 @@ function refuseUnwritable(file) {
413
423
  }
414
424
  }
415
425
 
416
- /** Why `--disallow` refuses a rule `allow` does not hold, and where one of that text is. */
426
+ /** Why `permission disallow` refuses a rule `allow` does not hold, and where one of that text is. */
417
427
  function notAllowed(home, bot, rule) {
418
428
  const yaml = path.join(home, 'bot.yaml');
419
429
  const file = path.join(home, FILE);
@@ -427,7 +437,7 @@ function notAllowed(home, bot, rule) {
427
437
  const where = byHand
428
438
  ? ` ${file} allows it, which the user added by hand, not the kit: it stays theirs, to take out themselves.`
429
439
  : '';
430
- return `--disallow ${rule} is not in the allow list in ${yaml}, so it is not the kit's to take back, and nothing was changed.${where} The kit takes back only rules that list holds, spelled exactly as they are there.`;
440
+ return `permission disallow ${rule} is not in the allow list in ${yaml}, so it is not the kit's to take back, and nothing was changed.${where} The kit takes back only rules that list holds, spelled exactly as they are there.`;
431
441
  }
432
442
 
433
443
  /**
@@ -452,14 +462,14 @@ const ruleLines = (text) => text.split('\n').map((line) => line.trim()).filter((
452
462
  /**
453
463
  * What writing the Codex form of what the bot is allowed into
454
464
  * `.codex/rules/obk.rules` would do, as `planClaude` gives it. `write()` gives
455
- * `{ bot, file, written, waiting, unwritten }`, with `written` every rule in
465
+ * `{ bot, file, written, unwritten }`, with `written` every rule in
456
466
  * the file when it wrote it. The kit owns the file whole and writes it only
457
467
  * when it would change; with nothing to write and no file, it makes none.
458
468
  */
459
469
  function planCodex(bots, home, bot) {
460
470
  const file = path.join(home, CODEX_FILE);
461
471
  const { lines, unwritten } = codexOf(home, bot);
462
- const answer = { bot: bot.name, file, written: [], waiting: waitingFor(bots, home, bot, 'codex'), unwritten };
472
+ const answer = { bot: bot.name, file, written: [], unwritten };
463
473
  const none = { answer, writes: false, write: () => answer };
464
474
  if (lines.size === 0 && !existsSync(file)) return none;
465
475
 
@@ -502,11 +512,13 @@ export function permissionsTrouble(home, bot) {
502
512
  function claudeTrouble(home, bot, allowed) {
503
513
  if (!runsOnClaude(bot)) return [];
504
514
  const file = path.join(home, FILE);
505
- if (!existsSync(file) && allowed.length === 0) return [];
506
-
507
- // A link out of the bot folder is named by the hook's check already; here it
508
- // matters only when there are rules the kit would write through it.
509
- if (allowed.length === 0 && leadsOutside(home, file) !== undefined) return [];
515
+ // A file that is not there, or a link out of the bot folder, is named by the
516
+ // hook's check already, which reads this file for every bot with a Claude
517
+ // session, and `obk up` puts back the hook and the rules together. Here it
518
+ // matters only for a bot without one, when there are rules to write.
519
+ const hookNamesIt = bot.sessions.some((session) => harnessOf(session, bot.harness) === 'claude');
520
+ if (!existsSync(file) && (allowed.length === 0 || hookNamesIt)) return [];
521
+ if ((allowed.length === 0 || hookNamesIt) && leadsOutside(home, file) !== undefined) return [];
510
522
 
511
523
  let present;
512
524
  try {
@@ -519,15 +531,17 @@ function claudeTrouble(home, bot, allowed) {
519
531
  return [
520
532
  ...present.filter((rule) => !allowed.includes(rule)).map((rule) => ({
521
533
  where: file,
522
- // One `--allow` would refuse was added by the user by hand, which the
534
+ // One `permission allow` would refuse was added by the user by hand, which the
523
535
  // boundary leaves to them: named, in neutral words, and left alone.
524
536
  says: broadness(rule) === undefined
525
- ? `${file} allows ${rule}, and ${bot.name}'s bot.yaml does not: the kit did not write it. It stays where it is. If the user wants it, record their yes with obk bot change --allow; if not, take it out of the file.`
537
+ ? `${file} allows ${rule}, and ${bot.name}'s bot.yaml does not: the kit did not write it. It stays where it is. If the user wants it, record their yes with obk permission allow; if not, take it out of the file.`
526
538
  : `${file} allows ${rule}, which the user added by hand, not the kit. It stays where it is.`,
527
539
  })),
528
- ...allowed.filter((rule) => !present.includes(rule)).map((rule) => ({
540
+ // One finding for all the rules it lacks: with the kit's default set, a
541
+ // file written by hand can lack forty.
542
+ ...missingIn(allowed.filter((rule) => !present.includes(rule)), (missing) => ({
529
543
  where: file,
530
- says: `${file} does not hold ${rule}, which ${bot.name}'s bot.yaml allows, so ${bot.name}'s Claude sessions are asked about it. obk up writes it.`,
544
+ says: `${file} does not hold ${missing.length === 1 ? missing[0] : `these ${missing.length} rules`}, which ${bot.name}'s bot.yaml allows, so ${bot.name}'s Claude sessions are asked about ${missing.length === 1 ? 'it' : `them: ${missing.join(', ')}`}. obk up writes ${missing.length === 1 ? 'it' : 'them'}.`,
531
545
  })),
532
546
  ];
533
547
  }
@@ -597,6 +611,9 @@ function codexTrouble(home, bot) {
597
611
  }];
598
612
  }
599
613
 
614
+ /** One finding about `missing`, made by `finding`, or none when nothing is missing. */
615
+ const missingIn = (missing, finding) => (missing.length === 0 ? [] : [finding([...new Set(missing)])]);
616
+
600
617
  /** The file's `permissions.allow`, or a refusal when it is there and is not a list. */
601
618
  function presentIn(settings, file) {
602
619
  const permissions = settings.permissions;
package/src/temp.js CHANGED
@@ -34,7 +34,7 @@ import { setTimeout as pause } from 'node:timers/promises';
34
34
  import { readBook, takeLineTurn, TYPING_HELD, TYPING_WAIT_MS, updateBook } from './book.js';
35
35
  import { addSession, dropSession, NAME, readBot, tempRoles } from './bot.js';
36
36
  import { untrustedKitHooks } from './hook-trust.js';
37
- import { DEFAULT_APPROVAL, harnessOf, isShortPrompt, ownCli, shellWord, startPrompt, workDirOf } from './launch.js';
37
+ import { approvalRank, DEFAULT_APPROVAL, harnessOf, isShortPrompt, ownCli, refuseApprovalArgs, shellWord, startPrompt, workDirOf } from './launch.js';
38
38
  import { findSession, sessionInTab } from './message.js';
39
39
  import { orca, screenRows, tabs } from './orca.js';
40
40
  import { retireSession } from './retire.js';
@@ -75,6 +75,8 @@ export async function makeTemp(bots, { tab, ...given }) {
75
75
 
76
76
  const maker = bot.sessions.find((session) => session.name === caller.session);
77
77
  const { settings, chosen } = settingsFor(bot, maker, role?.option ?? {}, given);
78
+ refuseWiderApproval(bots, caller, bot, maker, given.approval);
79
+ refuseApprovalArgs(chosen.harness.value, given.extra_args);
78
80
  settings.name = name;
79
81
  if (given.extra_args !== undefined) settings.extra_args = given.extra_args;
80
82
  if (given.prompt !== undefined) settings.prompt = given.prompt;
@@ -173,6 +175,22 @@ function settingsFor(bot, maker, option, given) {
173
175
  return { settings, chosen };
174
176
  }
175
177
 
178
+ /**
179
+ * Refuse an approval wider than the widest of the maker's own and the bot's
180
+ * `temp_approval`, which only the user's yes through `permission approval
181
+ * --temps` widens (ADR 0037).
182
+ */
183
+ function refuseWiderApproval(bots, caller, bot, maker, asked) {
184
+ if (asked === undefined) return;
185
+ // A blank level is the kit's default at launch (ADR 0015), so it is judged as that.
186
+ const effective = (given) => (given === undefined || given === null || String(given).trim() === '' ? DEFAULT_APPROVAL : given);
187
+ const level = effective(asked);
188
+ const own = effective(maker?.approval);
189
+ const widest = approvalRank(bot.temp_approval) > approvalRank(own) ? bot.temp_approval : own;
190
+ if (approvalRank(level) <= approvalRank(widest)) return;
191
+ throw new Error(`--approval ${level} is wider than ${caller.session}'s own approval, ${own}${widest === own ? '' : `, and than ${caller.bot}'s temp_approval, ${widest}`}, and a session does not widen another's approval. The user allows it for ${caller.bot}'s temporary sessions with ${shellWord(ownCli())} permission approval --bots ${shellWord(bots)} --bot ${caller.bot} --temps --approval ${level}. Nothing was made.`);
192
+ }
193
+
176
194
  /**
177
195
  * The role and option `asked` names, as `role` or `role:option`, from the
178
196
  * bot's roles, with the option it takes; or a refusal that names what the bot