@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/README.md +19 -3
- package/SETUP.md +14 -14
- package/package.json +1 -1
- package/rules/limits.md +3 -3
- package/skills/obk-bot-building/SKILL.md +16 -18
- package/src/bot.js +61 -6
- package/src/cli.js +195 -128
- package/src/commands.js +74 -0
- package/src/health.js +9 -3
- package/src/launch.js +25 -0
- package/src/permissions.js +81 -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 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
|
|
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 } 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
|
|
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 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}
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
204
|
-
*
|
|
205
|
-
*
|
|
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
|
|
209
|
+
function missingDefaults(bots, home, bot) {
|
|
208
210
|
const allowed = allowOf(home, bot);
|
|
209
211
|
return defaultRules(bots)
|
|
210
|
-
.filter((rule) =>
|
|
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
|
|
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(
|
|
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
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
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
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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 `
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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: [],
|
|
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
|
-
|
|
506
|
-
|
|
507
|
-
//
|
|
508
|
-
// matters only when there are rules
|
|
509
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 ${
|
|
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
|