@assuredloop/orca-bot-kit 0.25.6 → 0.26.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 +25 -3
- package/SETUP.md +14 -14
- package/package.json +1 -1
- package/rules/limits.md +3 -3
- package/skills/obk-bot-building/SKILL.md +19 -18
- package/src/bot.js +61 -6
- package/src/cli.js +200 -128
- package/src/commands.js +84 -0
- package/src/health.js +9 -3
- package/src/launch.js +25 -0
- package/src/permissions.js +97 -64
- package/src/temp.js +19 -1
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 0041).
|
|
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
|
|
package/src/permissions.js
CHANGED
|
@@ -1,28 +1,30 @@
|
|
|
1
1
|
// The permission rules a bot may run with, written into its own Claude
|
|
2
|
-
// settings, and
|
|
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:
|
|
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
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// bot.yaml's `allow
|
|
11
|
-
// `.claude/settings.json` in the bot folder, beside its hook
|
|
12
|
-
// editing the file by hand may get the format wrong; code
|
|
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 `
|
|
17
|
+
// back leaves `allow` and the files through `permission disallow` (#360).
|
|
17
18
|
//
|
|
18
|
-
// A bot on Codex gets the same
|
|
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, NO_LONGER_DEFAULT } from './commands.js';
|
|
26
28
|
import { readSettings } from './hooks.js';
|
|
27
29
|
import { harnessOf, ownCli, shellWord } from './launch.js';
|
|
28
30
|
|
|
@@ -34,24 +36,36 @@ const CODEX_RULES = '.codex/rules';
|
|
|
34
36
|
const CODEX_FILE = `${CODEX_RULES}/obk.rules`;
|
|
35
37
|
|
|
36
38
|
/**
|
|
37
|
-
* The rules every
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
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 0041), 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}
|
|
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
|
|
|
57
|
+
/**
|
|
58
|
+
* The rules of the commands that were defaults once and are kept back now
|
|
59
|
+
* (#548), spelled as `defaultRules` spelled them, that the bot's `allow`
|
|
60
|
+
* holds. The kit cannot tell its own old entry from a user's yes to the same
|
|
61
|
+
* rule, so it names them and leaves them (ADR 0041).
|
|
62
|
+
*/
|
|
63
|
+
export function heldRules(bots, home, bot, cli = ownCli()) {
|
|
64
|
+
const allowed = allowOf(home, bot);
|
|
65
|
+
return NO_LONGER_DEFAULT.map((command) => `Bash(${shellWord(cli)} ${command} --bots ${shellWord(bots)}:*)`)
|
|
66
|
+
.filter((rule) => allowed.includes(rule));
|
|
67
|
+
}
|
|
68
|
+
|
|
55
69
|
/** Command wrappers: programs that run the command their arguments name. */
|
|
56
70
|
const WRAPPERS = new Set(['env', 'xargs', 'sudo', 'eval', 'exec', 'nohup', 'timeout', 'nice', 'command', 'time', 'watch']);
|
|
57
71
|
|
|
@@ -73,7 +87,7 @@ const ANY = 'it lets the bot run any command';
|
|
|
73
87
|
* Why a rule is broad, as the end of a sentence, or undefined for a narrow one
|
|
74
88
|
* (#353). A charter's grants are written as narrow, exact rules only; one that
|
|
75
89
|
* 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
|
|
90
|
+
* the disk or in the home is the user's to add by hand (ADR 0038).
|
|
77
91
|
*/
|
|
78
92
|
export function broadness(rule) {
|
|
79
93
|
if (typeof rule !== 'string') return undefined;
|
|
@@ -163,14 +177,14 @@ function commandBroadness(all) {
|
|
|
163
177
|
}
|
|
164
178
|
|
|
165
179
|
/**
|
|
166
|
-
* Refuse the rules
|
|
180
|
+
* Refuse the rules `permission allow` was given when any is broad, before anything is
|
|
167
181
|
* written, naming the file the user can add it to themselves.
|
|
168
182
|
*/
|
|
169
183
|
export function refuseBroad(home, rules) {
|
|
170
184
|
for (const rule of rules) {
|
|
171
185
|
const why = broadness(rule);
|
|
172
186
|
if (why === undefined) continue;
|
|
173
|
-
throw new Error(
|
|
187
|
+
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
188
|
}
|
|
175
189
|
}
|
|
176
190
|
|
|
@@ -200,14 +214,14 @@ export const runsOnCodex = (bot) => bot.harness === 'codex'
|
|
|
200
214
|
|| bot.sessions.some((session) => harnessOf(session, bot.harness) === 'codex');
|
|
201
215
|
|
|
202
216
|
/**
|
|
203
|
-
* The default rules this bot
|
|
204
|
-
*
|
|
205
|
-
*
|
|
217
|
+
* The default rules this bot is not allowed yet, in the default order. A bot
|
|
218
|
+
* only on Codex is not given the Read rule: its sandbox reads every file
|
|
219
|
+
* already.
|
|
206
220
|
*/
|
|
207
|
-
function
|
|
221
|
+
function missingDefaults(bots, home, bot) {
|
|
208
222
|
const allowed = allowOf(home, bot);
|
|
209
223
|
return defaultRules(bots)
|
|
210
|
-
.filter((rule) =>
|
|
224
|
+
.filter((rule) => runsOnClaude(bot) || codexForm(rule).line !== undefined)
|
|
211
225
|
.filter((rule) => !allowed.includes(rule));
|
|
212
226
|
}
|
|
213
227
|
|
|
@@ -242,7 +256,7 @@ export function codexForm(rule) {
|
|
|
242
256
|
}
|
|
243
257
|
|
|
244
258
|
/**
|
|
245
|
-
* Refuse the rules
|
|
259
|
+
* Refuse the rules `permission allow` was given for a bot that runs only on Codex when
|
|
246
260
|
* Codex has no form for one, before anything is written: recorded, it would
|
|
247
261
|
* let the bot do nothing.
|
|
248
262
|
*/
|
|
@@ -251,35 +265,43 @@ export function refuseNoCodexForm(bot, rules) {
|
|
|
251
265
|
for (const rule of rules) {
|
|
252
266
|
const { why } = codexForm(rule);
|
|
253
267
|
if (why === undefined) continue;
|
|
254
|
-
throw new Error(
|
|
268
|
+
throw new Error(`permission allow ${rule} is not for ${bot.name}: ${why}, and ${bot.name} runs only on Codex, so nothing was written.`);
|
|
255
269
|
}
|
|
256
270
|
}
|
|
257
271
|
|
|
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
272
|
/**
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
273
|
+
* Add the default rules the bot is not allowed yet to its `allow` (ADR 0036),
|
|
274
|
+
* then write what it is allowed into the files of the harnesses it runs on:
|
|
275
|
+
* one entry per file, Claude's first, each with `defaults`, the rules this run
|
|
276
|
+
* added, and `held`, the rules of commands no longer in the set that the bot
|
|
277
|
+
* still holds (#548). The harness files are written first and bot.yaml last, as
|
|
278
|
+
* `permission allow` writes them (#383). With `keepGoing`, a file the kit may
|
|
279
|
+
* not write is an entry with its `trouble` rather than a throw.
|
|
268
280
|
*/
|
|
269
281
|
export function writePermissions(bots, home, bot, { keepGoing = false } = {}) {
|
|
270
|
-
const writers = [[runsOnClaude, FILE
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
282
|
+
const writers = [[runsOnClaude, FILE], [runsOnCodex, CODEX_FILE]].filter(([runs]) => runs(bot));
|
|
283
|
+
let defaults = [];
|
|
284
|
+
// Read before anything is written, so a file's trouble still names them.
|
|
285
|
+
let held = [];
|
|
286
|
+
try {
|
|
287
|
+
held = heldRules(bots, home, bot);
|
|
288
|
+
defaults = writers.length === 0 ? [] : missingDefaults(bots, home, bot);
|
|
289
|
+
if (defaults.length > 0) {
|
|
290
|
+
const { allow } = allowRules(bots, bot.name, defaults, { write: false });
|
|
291
|
+
const entries = allowIn(bots, home, { ...bot, allow })();
|
|
292
|
+
allowRules(bots, bot.name, defaults);
|
|
293
|
+
return entries.map((entry) => ({ ...entry, defaults, held }));
|
|
277
294
|
}
|
|
278
|
-
|
|
295
|
+
return writers.map(([runs]) => (runs === runsOnClaude ? planClaude : planCodex)(bots, home, bot).write())
|
|
296
|
+
.map((entry) => ({ ...entry, defaults, held }));
|
|
297
|
+
} catch (error) {
|
|
298
|
+
if (!keepGoing) throw error;
|
|
299
|
+
return writers.map(([, file]) => ({ bot: bot.name, file: path.join(home, file), written: [], defaults: [], held, trouble: error.message }));
|
|
300
|
+
}
|
|
279
301
|
}
|
|
280
302
|
|
|
281
303
|
/**
|
|
282
|
-
* Check every file `
|
|
304
|
+
* Check every file `permission allow` writes before any is written (#383):
|
|
283
305
|
* bot.yaml, and the files of the harnesses the bot runs on, read and found
|
|
284
306
|
* writable. `bot` is the bot as it will be, with the new rules in `allow`.
|
|
285
307
|
* What comes back writes the harness files, one entry per file, Claude's
|
|
@@ -297,7 +319,7 @@ export function allowIn(bots, home, bot) {
|
|
|
297
319
|
/**
|
|
298
320
|
* What writing the bot's allowed rules into its Claude settings would do,
|
|
299
321
|
* read and refused before anything is written: `{ answer, writes, write }`.
|
|
300
|
-
* `write()` gives `{ bot, file, written
|
|
322
|
+
* `write()` gives `{ bot, file, written }`, with `written` the rules
|
|
301
323
|
* it added.
|
|
302
324
|
*
|
|
303
325
|
* Only rules `allow` holds are written. Everything else in the file stays as it
|
|
@@ -307,8 +329,7 @@ export function allowIn(bots, home, bot) {
|
|
|
307
329
|
function planClaude(bots, home, bot) {
|
|
308
330
|
const file = path.join(home, FILE);
|
|
309
331
|
const allowed = allowOf(home, bot);
|
|
310
|
-
const
|
|
311
|
-
const answer = { bot: bot.name, file, written: [], waiting };
|
|
332
|
+
const answer = { bot: bot.name, file, written: [] };
|
|
312
333
|
const none = { answer, writes: false, write: () => answer };
|
|
313
334
|
if (allowed.length === 0) return none;
|
|
314
335
|
|
|
@@ -335,7 +356,7 @@ function planClaude(bots, home, bot) {
|
|
|
335
356
|
* harnesses the bot runs on (#360): from its Claude settings every entry of
|
|
336
357
|
* that exact text and nothing else, and its Codex rules rewritten from what
|
|
337
358
|
* `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
|
|
359
|
+
* hold is refused, since the kit did not write it (ADR 0040).
|
|
339
360
|
*
|
|
340
361
|
* Everything is checked before anything is written, a file the kit could not
|
|
341
362
|
* write included, bot.yaml too. What comes back writes the files: one entry per file,
|
|
@@ -347,6 +368,11 @@ export function takeBack(bots, home, bot, rules) {
|
|
|
347
368
|
const allowed = allowOf(home, bot);
|
|
348
369
|
const foreign = rules.find((rule) => !allowed.includes(rule));
|
|
349
370
|
if (foreign !== undefined) throw new Error(notAllowed(home, bot, foreign));
|
|
371
|
+
// The next rules write would add it again (ADR 0040).
|
|
372
|
+
const kits = rules.find((rule) => defaultRules(bots).includes(rule));
|
|
373
|
+
if (kits !== undefined) {
|
|
374
|
+
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.`);
|
|
375
|
+
}
|
|
350
376
|
const after = { ...bot, allow: allowed.filter((rule) => !rules.includes(rule)) };
|
|
351
377
|
const taken = [...new Set(rules)];
|
|
352
378
|
// bot.yaml is written last, and a harness file taken out of step with it
|
|
@@ -380,7 +406,7 @@ export function takeBack(bots, home, bot, rules) {
|
|
|
380
406
|
const allow = present.filter((rule) => !removed.includes(rule));
|
|
381
407
|
writeFileSync(file, `${JSON.stringify({ ...settings, permissions: { ...settings.permissions, allow } }, null, 2)}\n`);
|
|
382
408
|
}
|
|
383
|
-
entries.push({ bot: bot.name, file, written: [], removed
|
|
409
|
+
entries.push({ bot: bot.name, file, written: [], removed });
|
|
384
410
|
}
|
|
385
411
|
if (runsOnCodex(after)) {
|
|
386
412
|
const entry = planCodex(bots, home, after).write();
|
|
@@ -413,7 +439,7 @@ function refuseUnwritable(file) {
|
|
|
413
439
|
}
|
|
414
440
|
}
|
|
415
441
|
|
|
416
|
-
/** Why
|
|
442
|
+
/** Why `permission disallow` refuses a rule `allow` does not hold, and where one of that text is. */
|
|
417
443
|
function notAllowed(home, bot, rule) {
|
|
418
444
|
const yaml = path.join(home, 'bot.yaml');
|
|
419
445
|
const file = path.join(home, FILE);
|
|
@@ -427,7 +453,7 @@ function notAllowed(home, bot, rule) {
|
|
|
427
453
|
const where = byHand
|
|
428
454
|
? ` ${file} allows it, which the user added by hand, not the kit: it stays theirs, to take out themselves.`
|
|
429
455
|
: '';
|
|
430
|
-
return
|
|
456
|
+
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
457
|
}
|
|
432
458
|
|
|
433
459
|
/**
|
|
@@ -452,14 +478,14 @@ const ruleLines = (text) => text.split('\n').map((line) => line.trim()).filter((
|
|
|
452
478
|
/**
|
|
453
479
|
* What writing the Codex form of what the bot is allowed into
|
|
454
480
|
* `.codex/rules/obk.rules` would do, as `planClaude` gives it. `write()` gives
|
|
455
|
-
* `{ bot, file, written,
|
|
481
|
+
* `{ bot, file, written, unwritten }`, with `written` every rule in
|
|
456
482
|
* the file when it wrote it. The kit owns the file whole and writes it only
|
|
457
483
|
* when it would change; with nothing to write and no file, it makes none.
|
|
458
484
|
*/
|
|
459
485
|
function planCodex(bots, home, bot) {
|
|
460
486
|
const file = path.join(home, CODEX_FILE);
|
|
461
487
|
const { lines, unwritten } = codexOf(home, bot);
|
|
462
|
-
const answer = { bot: bot.name, file, written: [],
|
|
488
|
+
const answer = { bot: bot.name, file, written: [], unwritten };
|
|
463
489
|
const none = { answer, writes: false, write: () => answer };
|
|
464
490
|
if (lines.size === 0 && !existsSync(file)) return none;
|
|
465
491
|
|
|
@@ -502,11 +528,13 @@ export function permissionsTrouble(home, bot) {
|
|
|
502
528
|
function claudeTrouble(home, bot, allowed) {
|
|
503
529
|
if (!runsOnClaude(bot)) return [];
|
|
504
530
|
const file = path.join(home, FILE);
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
//
|
|
508
|
-
// matters only when there are rules
|
|
509
|
-
|
|
531
|
+
// A file that is not there, or a link out of the bot folder, is named by the
|
|
532
|
+
// hook's check already, which reads this file for every bot with a Claude
|
|
533
|
+
// session, and `obk up` puts back the hook and the rules together. Here it
|
|
534
|
+
// matters only for a bot without one, when there are rules to write.
|
|
535
|
+
const hookNamesIt = bot.sessions.some((session) => harnessOf(session, bot.harness) === 'claude');
|
|
536
|
+
if (!existsSync(file) && (allowed.length === 0 || hookNamesIt)) return [];
|
|
537
|
+
if ((allowed.length === 0 || hookNamesIt) && leadsOutside(home, file) !== undefined) return [];
|
|
510
538
|
|
|
511
539
|
let present;
|
|
512
540
|
try {
|
|
@@ -519,15 +547,17 @@ function claudeTrouble(home, bot, allowed) {
|
|
|
519
547
|
return [
|
|
520
548
|
...present.filter((rule) => !allowed.includes(rule)).map((rule) => ({
|
|
521
549
|
where: file,
|
|
522
|
-
// One
|
|
550
|
+
// One `permission allow` would refuse was added by the user by hand, which the
|
|
523
551
|
// boundary leaves to them: named, in neutral words, and left alone.
|
|
524
552
|
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
|
|
553
|
+
? `${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
554
|
: `${file} allows ${rule}, which the user added by hand, not the kit. It stays where it is.`,
|
|
527
555
|
})),
|
|
528
|
-
|
|
556
|
+
// One finding for all the rules it lacks: with the kit's default set, a
|
|
557
|
+
// file written by hand can lack forty.
|
|
558
|
+
...missingIn(allowed.filter((rule) => !present.includes(rule)), (missing) => ({
|
|
529
559
|
where: file,
|
|
530
|
-
says: `${file} does not hold ${
|
|
560
|
+
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
561
|
})),
|
|
532
562
|
];
|
|
533
563
|
}
|
|
@@ -597,6 +627,9 @@ function codexTrouble(home, bot) {
|
|
|
597
627
|
}];
|
|
598
628
|
}
|
|
599
629
|
|
|
630
|
+
/** One finding about `missing`, made by `finding`, or none when nothing is missing. */
|
|
631
|
+
const missingIn = (missing, finding) => (missing.length === 0 ? [] : [finding([...new Set(missing)])]);
|
|
632
|
+
|
|
600
633
|
/** The file's `permissions.allow`, or a refusal when it is there and is not a list. */
|
|
601
634
|
function presentIn(settings, file) {
|
|
602
635
|
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 0041).
|
|
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
|