@assuredloop/orca-bot-kit 0.25.4 → 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 +27 -10
- package/SETUP.md +33 -13
- package/package.json +4 -3
- package/rules/limits.md +3 -3
- package/rules/mail.md +4 -0
- package/skills/obk-bot-building/SKILL.md +29 -18
- package/src/book.js +3 -0
- package/src/bot.js +61 -6
- package/src/cli.js +237 -129
- package/src/commands.js +74 -0
- package/src/health.js +9 -3
- package/src/hook-trust.js +179 -0
- package/src/hooks.js +11 -0
- package/src/launch.js +25 -0
- package/src/orca.js +15 -1
- package/src/permissions.js +81 -64
- package/src/temp.js +153 -30
- package/src/up.js +4 -1
package/src/cli.js
CHANGED
|
@@ -12,15 +12,16 @@ import path from 'node:path';
|
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
import { parseArgs } from 'node:util';
|
|
14
14
|
|
|
15
|
-
import { addSession, allowedNow, allowRules, botDir, changeBot, changeSession, createBot, disallowRules, leadsOutside, readBot, SESSION_FIELDS } from './bot.js';
|
|
15
|
+
import { addSession, allowedNow, allowRules, botDir, changeBot, changeSession, createBot, disallowRules, leadsOutside, readBot, SESSION_FIELDS, setRoleCaps, setTempApproval } from './bot.js';
|
|
16
|
+
import { COMMANDS } from './commands.js';
|
|
16
17
|
import { addCommand, groomCommand, grooming, upCommand } from './groom.js';
|
|
17
18
|
import { checkHealth, orcaSettingFindings } from './health.js';
|
|
18
19
|
import { initBots } from './init.js';
|
|
19
20
|
import { nameSession } from './name.js';
|
|
20
|
-
import { APPROVALS, HARNESSES, ownCli, shellWord, workDirOf } from './launch.js';
|
|
21
|
+
import { APPROVALS, harnessOf, HARNESSES, ownCli, refuseApprovalArgs, shellWord, workDirOf } from './launch.js';
|
|
21
22
|
import { checkMail, decideLeftNudges, lookUp, noMailboxYet, sendMessage, sessionInTab, stillUnread, WATCH_MS } from './message.js';
|
|
22
23
|
import { orcaCli, orcaTrouble, RELOAD_LINE, TERMINAL_ENV } from './orca.js';
|
|
23
|
-
import {
|
|
24
|
+
import { allowIn, beyondDefaults, refuseBroad, refuseNoCodexForm, runsOnClaude, runsOnCodex, takeBack, writePermissions } from './permissions.js';
|
|
24
25
|
import { pauseSessions, unpauseSessions } from './pause.js';
|
|
25
26
|
import { recordSession, SHELL_ENV, TAB_ENV } from './record.js';
|
|
26
27
|
import { restartSessions } from './restart.js';
|
|
@@ -31,7 +32,7 @@ import { readRoster } from './roster.js';
|
|
|
31
32
|
import { buildAgents, buildRules, CODEX_CAP } from './rules.js';
|
|
32
33
|
import { addSkill, buildSkills, linkSkills, removeSkill } from './skills.js';
|
|
33
34
|
import { addSource, fetchSources } from './sources.js';
|
|
34
|
-
import { answerTemp, listRoles, makeTemp, optionsLine, retireTemp, trustHooks } from './temp.js';
|
|
35
|
+
import { answerSession, answerTemp, listRoles, makeTemp, optionsLine, retireTemp, trustHooks, trustSessionHooks } from './temp.js';
|
|
35
36
|
import { readUsage } from './usage.js';
|
|
36
37
|
import { BOT_FATHER, bringUp, ownMailbox } from './up.js';
|
|
37
38
|
|
|
@@ -48,11 +49,12 @@ Usage:
|
|
|
48
49
|
opened in Orca until you run obk up.
|
|
49
50
|
obk session add --bots <path> --bot <bot> --name <session>
|
|
50
51
|
[--harness claude|codex] [--model <m>] [--effort <e>]
|
|
51
|
-
[--context <c>]
|
|
52
|
+
[--context <c>]
|
|
52
53
|
[--prompt <text> | --prompt-file <path>] [--work-dir <path>]
|
|
53
54
|
[--extra-arg=<arg>]
|
|
54
55
|
Add a session to a bot. Anything left out is the
|
|
55
|
-
harness's own default; approval is auto
|
|
56
|
+
harness's own default; approval is auto, and only
|
|
57
|
+
obk permission approval changes it. A long
|
|
56
58
|
start prompt lives in a file in the bot home, and
|
|
57
59
|
--prompt-file names it.
|
|
58
60
|
A value of your own that starts with a dash is
|
|
@@ -60,20 +62,32 @@ Usage:
|
|
|
60
62
|
as ours: --prompt='- a bullet', and
|
|
61
63
|
--extra-arg=--search, once per extra argument.
|
|
62
64
|
obk bot change --bots <path> --bot <bot> [--charter <text>]
|
|
63
|
-
[--
|
|
65
|
+
[--role-cap <role>=<n> ...]
|
|
64
66
|
Give a bot a new charter and rebuild its AGENTS.md.
|
|
65
67
|
A running session reads it when it next starts.
|
|
66
|
-
--
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
--role-cap sets the cap of a role in the bot's
|
|
69
|
+
temp_roles, once per role; <role>= takes it off.
|
|
70
|
+
obk permission allow --bots <path> --bot <bot> --rule <rule> ...
|
|
71
|
+
Record a permission rule the user said yes to,
|
|
72
|
+
once per rule, and write it into the bot's Claude
|
|
73
|
+
settings and Codex rules. A broad rule, such as
|
|
74
|
+
Bash(gh:*), is refused and nothing is written.
|
|
75
|
+
Every bot has the kit's own default rules already,
|
|
76
|
+
with nobody asked.
|
|
77
|
+
obk permission disallow --bots <path> --bot <bot> --rule <rule> ...
|
|
78
|
+
Take back a rule the bot was allowed, once the user
|
|
79
|
+
has said yes to that, from bot.yaml and the bot's
|
|
80
|
+
settings. A rule the user added by hand is not the
|
|
81
|
+
kit's, and a rule of the kit's default set is
|
|
82
|
+
written again, so both are refused.
|
|
83
|
+
obk permission approval --bots <path> --bot <bot>
|
|
84
|
+
(--session <name> | --temps) --approval ${APPROVALS.join('|')}
|
|
85
|
+
Set a session's approval level once the user has
|
|
86
|
+
said yes to it, or with --temps the widest level
|
|
87
|
+
the bot's temporary sessions may be made at. A
|
|
88
|
+
running session takes it when it next starts.
|
|
74
89
|
obk session change --bots <path> --bot <bot> --session <session>
|
|
75
90
|
[--model <m>] [--effort <e>] [--context <c>]
|
|
76
|
-
[--approval ${APPROVALS.join('|')}]
|
|
77
91
|
[--prompt <text> | --prompt-file <path>] [--work-dir <path>]
|
|
78
92
|
[--extra-arg=<arg>]
|
|
79
93
|
Change a session's settings. What you leave out
|
|
@@ -111,7 +125,9 @@ Usage:
|
|
|
111
125
|
a time, and one it made makes none. It takes your
|
|
112
126
|
harness, model, effort, context and approval unless
|
|
113
127
|
you say otherwise; on another harness than yours
|
|
114
|
-
it takes only your approval.
|
|
128
|
+
it takes only your approval. --approval is refused
|
|
129
|
+
wider than yours, unless obk permission approval
|
|
130
|
+
--temps allowed it for the bot. It works in
|
|
115
131
|
work/<session>, and has the task as its start
|
|
116
132
|
prompt. The book records it as temporary, made by
|
|
117
133
|
you.
|
|
@@ -139,7 +155,8 @@ Usage:
|
|
|
139
155
|
of a Codex temporary session it made with "Trust
|
|
140
156
|
all and continue", and check the review went. It
|
|
141
157
|
refuses anything else on that screen, and types
|
|
142
|
-
nothing then.
|
|
158
|
+
nothing then. It refuses, as obk session
|
|
159
|
+
trust-hooks does, hooks that are not the kit's.
|
|
143
160
|
obk temp answer --bots <path> --name <session>
|
|
144
161
|
Run in the maker's own tab: answer Claude Code's
|
|
145
162
|
"Teach auto mode" screen on a temporary session it
|
|
@@ -266,6 +283,19 @@ Usage:
|
|
|
266
283
|
next starts where it stopped, so no call is counted
|
|
267
284
|
twice. It counts tokens and never money: what a
|
|
268
285
|
token costs is looked up live by whoever is asking.
|
|
286
|
+
obk session trust-hooks --bots <path> --bot <bot> --session <name>
|
|
287
|
+
Run by Bot Father or the user: answer the hooks
|
|
288
|
+
review of a long-lived Codex session with "Trust
|
|
289
|
+
all and continue", as obk temp trust-hooks does,
|
|
290
|
+
and check the review went. It refuses when the
|
|
291
|
+
bot's .codex/hooks.json holds a hook that is not
|
|
292
|
+
the kit's, or when the review counts other hooks
|
|
293
|
+
than the kit's own that Codex does not trust yet.
|
|
294
|
+
obk session answer --bots <path> --bot <bot> --session <name>
|
|
295
|
+
Run by Bot Father or the user: answer Claude Code's
|
|
296
|
+
"Teach auto mode" screen on a long-lived session
|
|
297
|
+
with Not now, as obk temp answer does, and check
|
|
298
|
+
the screen went.
|
|
269
299
|
obk session record --bots <path> --bot <bot>
|
|
270
300
|
For the kit's own hook, not for typing: it reads
|
|
271
301
|
what the harness says about a session starting on
|
|
@@ -299,50 +329,12 @@ Usage:
|
|
|
299
329
|
obk --help Print this text.
|
|
300
330
|
|
|
301
331
|
Every command is safe to run again. Most only add what is missing; the ones
|
|
302
|
-
that close a tab or take something away say so above.
|
|
332
|
+
that close a tab or take something away say so above. Every bot is allowed
|
|
333
|
+
the kit's commands by default, except obk permission, retire, pause and init,
|
|
334
|
+
which keep the user's yes.
|
|
303
335
|
Add --json to any of them for the same answer as JSON.
|
|
304
336
|
`;
|
|
305
337
|
|
|
306
|
-
/** The commands, and the flags each one cannot do without. */
|
|
307
|
-
const COMMANDS = {
|
|
308
|
-
init: ['bots', 'harness'],
|
|
309
|
-
up: ['bots'],
|
|
310
|
-
restart: ['bots', 'bot'],
|
|
311
|
-
pause: ['bots', 'bot'],
|
|
312
|
-
unpause: ['bots', 'bot'],
|
|
313
|
-
retire: ['bots', 'bot'],
|
|
314
|
-
health: ['bots'],
|
|
315
|
-
groom: ['bots'],
|
|
316
|
-
roster: ['bots'],
|
|
317
|
-
usage: ['bots'],
|
|
318
|
-
'bot create': ['bots', 'name', 'harness'],
|
|
319
|
-
'bot change': ['bots', 'bot'],
|
|
320
|
-
'rules build': ['bots'],
|
|
321
|
-
'skills add': ['bots', 'bot', 'skill'],
|
|
322
|
-
'skills remove': ['bots', 'bot', 'skill'],
|
|
323
|
-
'skills build': ['bots'],
|
|
324
|
-
'skills fetch': ['bots'],
|
|
325
|
-
'skills update': ['bots'],
|
|
326
|
-
'source add': ['bots', 'name', 'repo', 'ref'],
|
|
327
|
-
'session add': ['bots', 'bot', 'name'],
|
|
328
|
-
'session change': ['bots', 'bot', 'session'],
|
|
329
|
-
'session clear': ['bots', 'bot', 'session'],
|
|
330
|
-
'session compact': ['bots', 'bot', 'session'],
|
|
331
|
-
'message to': ['bots', 'to'],
|
|
332
|
-
'message send': ['bots', 'to', 'subject'],
|
|
333
|
-
'message check': ['bots'],
|
|
334
|
-
'session record': ['bots', 'bot'],
|
|
335
|
-
'session sent': ['bots', 'bot'],
|
|
336
|
-
'session nudge': ['bots', 'bot'],
|
|
337
|
-
'session mail': ['bots', 'bot'],
|
|
338
|
-
'session name': ['bots', 'bot'],
|
|
339
|
-
'session mailbox': ['bots', 'bot', 'session'],
|
|
340
|
-
'temp make': ['bots', 'name'],
|
|
341
|
-
'temp roles': ['bots'],
|
|
342
|
-
'temp retire': ['bots', 'name'],
|
|
343
|
-
'temp trust-hooks': ['bots', 'name'],
|
|
344
|
-
'temp answer': ['bots', 'name'],
|
|
345
|
-
};
|
|
346
338
|
|
|
347
339
|
/** What each flag is for, in the sentence a caller reads when it is missing. */
|
|
348
340
|
const NEEDED = {
|
|
@@ -363,10 +355,15 @@ const NEEDED = {
|
|
|
363
355
|
from: '--from <bot>/<session>: which session is writing',
|
|
364
356
|
subject: '--subject <text>: what the message is about',
|
|
365
357
|
role: '--role <role>[:<option>]: which role of the bot\'s temp_roles',
|
|
358
|
+
rule: '--rule <rule>: the exact permission rule the user said yes to',
|
|
359
|
+
approval: `--approval ${APPROVALS.join('|')}: the approval level the user said yes to`,
|
|
366
360
|
};
|
|
367
361
|
|
|
368
|
-
/**
|
|
369
|
-
|
|
362
|
+
/**
|
|
363
|
+
* The flags that name something. A name that is empty names nothing. A rule
|
|
364
|
+
* and an approval are judged by their own commands.
|
|
365
|
+
*/
|
|
366
|
+
const IDENTIFIERS = Object.keys(NEEDED).filter((flag) => flag !== 'rule' && flag !== 'approval');
|
|
370
367
|
|
|
371
368
|
/**
|
|
372
369
|
* The session settings a flag can carry, as `[flag, field]`: every field a
|
|
@@ -383,9 +380,10 @@ function version() {
|
|
|
383
380
|
}
|
|
384
381
|
|
|
385
382
|
async function run(argv) {
|
|
386
|
-
const { values, positionals } = parseArgs({
|
|
383
|
+
const { values, positionals, tokens } = parseArgs({
|
|
387
384
|
args: argv,
|
|
388
385
|
allowPositionals: true,
|
|
386
|
+
tokens: true,
|
|
389
387
|
options: {
|
|
390
388
|
bots: { type: 'string' },
|
|
391
389
|
harness: { type: 'string' },
|
|
@@ -414,6 +412,9 @@ async function run(argv) {
|
|
|
414
412
|
charter: { type: 'string' },
|
|
415
413
|
allow: { type: 'string', multiple: true },
|
|
416
414
|
disallow: { type: 'string', multiple: true },
|
|
415
|
+
rule: { type: 'string', multiple: true },
|
|
416
|
+
temps: { type: 'boolean' },
|
|
417
|
+
'role-cap': { type: 'string', multiple: true },
|
|
417
418
|
...Object.fromEntries(SETTINGS.map(([flag]) => [flag, { type: 'string' }])),
|
|
418
419
|
'extra-arg': { type: 'string', multiple: true },
|
|
419
420
|
'run-on': { type: 'string' },
|
|
@@ -438,9 +439,16 @@ async function run(argv) {
|
|
|
438
439
|
return 1;
|
|
439
440
|
}
|
|
440
441
|
|
|
441
|
-
//
|
|
442
|
-
//
|
|
443
|
-
|
|
442
|
+
// A permission rule narrows a command to one bots folder by its first words,
|
|
443
|
+
// whatever comes after them (ADR 0036), and the last of two `--bots` would
|
|
444
|
+
// win: so a second one is refused before anything runs.
|
|
445
|
+
if (tokens.filter((token) => token.kind === 'option' && token.name === 'bots').length > 1) {
|
|
446
|
+
throw new Error('--bots is given more than once. Give it once: one command works on one bots folder. Nothing was done.');
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// `bot`, `permission`, `rules`, `skills`, `session`, `source`, `message` and
|
|
450
|
+
// `temp` are commands of two words; the rest are one.
|
|
451
|
+
const words = ['bot', 'permission', 'rules', 'skills', 'session', 'source', 'message', 'temp'].includes(positionals[0]) ? 2 : 1;
|
|
444
452
|
const command = positionals.slice(0, words).join(' ');
|
|
445
453
|
const extra = positionals.slice(words);
|
|
446
454
|
|
|
@@ -840,11 +848,31 @@ const commands = {
|
|
|
840
848
|
answer: { bots, ...answered },
|
|
841
849
|
lines: [
|
|
842
850
|
`answered ${answered.bot} ${answered.session}, a temporary session of ${answered.maker}'s: sent ${answered.sent} to its "Teach auto mode about your environment?" screen, and the screen has gone`,
|
|
843
|
-
`rule a maker's bot runs this under
|
|
851
|
+
`rule a maker's bot runs this under a rule of the kit's default set, which every bot has: Bash(${shellWord(ownCli())} temp answer --bots ${shellWord(bots)}:*)`,
|
|
844
852
|
],
|
|
845
853
|
};
|
|
846
854
|
},
|
|
847
855
|
|
|
856
|
+
async 'session trust-hooks'(bots, values) {
|
|
857
|
+
const tab = callerTab(bots, true);
|
|
858
|
+
refuseWhenOrcaIsDown();
|
|
859
|
+
const trusted = await trustSessionHooks(bots, { tab, bot: values.bot, session: values.session });
|
|
860
|
+
return {
|
|
861
|
+
answer: { bots, ...trusted },
|
|
862
|
+
lines: [`trusted ${trusted.bot} ${trusted.session}'s hooks: chose "Trust all and continue" on its hooks review, and the review has gone`],
|
|
863
|
+
};
|
|
864
|
+
},
|
|
865
|
+
|
|
866
|
+
async 'session answer'(bots, values) {
|
|
867
|
+
const tab = callerTab(bots, true);
|
|
868
|
+
refuseWhenOrcaIsDown();
|
|
869
|
+
const answered = await answerSession(bots, { tab, bot: values.bot, session: values.session });
|
|
870
|
+
return {
|
|
871
|
+
answer: { bots, ...answered },
|
|
872
|
+
lines: [`answered ${answered.bot} ${answered.session}: sent ${answered.sent} to its "Teach auto mode about your environment?" screen, and the screen has gone`],
|
|
873
|
+
};
|
|
874
|
+
},
|
|
875
|
+
|
|
848
876
|
'bot create'(bots, values) {
|
|
849
877
|
const made = createBot(bots, { name: values.name, harness: values.harness, charter: values.charter });
|
|
850
878
|
// The bot's AGENTS.md is the rules build's, here as everywhere else, so
|
|
@@ -853,14 +881,14 @@ const commands = {
|
|
|
853
881
|
// A new bot is given what the lists already name, so it is whole before
|
|
854
882
|
// anybody opens a tab on it.
|
|
855
883
|
const skills = [linkSkills(bots, made.home, readBot(made.home))];
|
|
856
|
-
// Nothing is allowed yet, so this writes nothing: it says which of the
|
|
857
|
-
// kit's rules wait for the user's yes (#344).
|
|
858
|
-
const permissions = writePermissions(bots, made.home, readBot(made.home));
|
|
859
|
-
const answer = { bots, bot: made.bot, home: made.home, created: made.created, rules, skills, permissions };
|
|
860
884
|
// A bot whose rules would not build is made but not finished: it has no
|
|
861
885
|
// instructions, so `up` will not start it, and saying "give it a session"
|
|
862
886
|
// would send the caller past the thing that needs settling first.
|
|
863
887
|
const trouble = rules[0].trouble !== undefined;
|
|
888
|
+
// The kit's default rules (ADR 0036), for a bot that is set up to run: one
|
|
889
|
+
// with no instructions gets them at the rules build that gives it some.
|
|
890
|
+
const permissions = trouble ? [] : writePermissions(bots, made.home, readBot(made.home));
|
|
891
|
+
const answer = { bots, bot: made.bot, home: made.home, created: made.created, rules, skills, permissions };
|
|
864
892
|
return {
|
|
865
893
|
answer,
|
|
866
894
|
lines: [
|
|
@@ -880,29 +908,18 @@ const commands = {
|
|
|
880
908
|
if (values.harness !== undefined) {
|
|
881
909
|
throw new Error(`bot change does not change a bot's harness: its sessions' conversations belong to the harness they ran on. To move to ${values.harness}, give it a session on ${values.harness} with obk session add, or retire the bot with obk retire and create a new one.`);
|
|
882
910
|
}
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
if (
|
|
887
|
-
throw new Error(
|
|
911
|
+
// Permission changes have commands of their own, which keep the user's yes
|
|
912
|
+
// (ADR 0037): the old spelling refuses, and changes nothing.
|
|
913
|
+
const moved = [['allow', 'permission allow'], ['disallow', 'permission disallow']].filter(([flag]) => values[flag] !== undefined);
|
|
914
|
+
if (moved.length > 0) {
|
|
915
|
+
throw new Error(`bot change no longer takes ${moved.map(([flag]) => `--${flag}`).join(' or ')}: permission rules change through ${moved.map(([, now]) => `${shellWord(ownCli())} ${now} --bots ${shellWord(bots)} --bot ${values.bot} --rule <rule>`).join(' and ')}, after the user's yes. Nothing was changed.`);
|
|
888
916
|
}
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
let allowInto;
|
|
892
|
-
if (values.allow !== undefined) {
|
|
893
|
-
const { home } = allowedNow(bots, values.bot, values.allow);
|
|
894
|
-
refuseBroad(home, values.allow);
|
|
895
|
-
const bot = readBot(home, values.bot);
|
|
896
|
-
refuseNoCodexForm(bot, values.allow);
|
|
897
|
-
const { allow } = allowRules(bots, values.bot, values.allow, { write: false });
|
|
898
|
-
allowInto = allowIn(bots, home, { ...bot, allow });
|
|
899
|
-
}
|
|
900
|
-
let takeBackFrom;
|
|
901
|
-
if (values.disallow !== undefined) {
|
|
902
|
-
const { home } = allowedNow(bots, values.bot, values.disallow, '--disallow');
|
|
903
|
-
takeBackFrom = takeBack(bots, home, readBot(home, values.bot), values.disallow);
|
|
904
|
-
disallowRules(bots, values.bot, values.disallow, { write: false });
|
|
917
|
+
if (values.charter === undefined && values['role-cap'] === undefined) {
|
|
918
|
+
throw new Error('bot change needs --charter <text> or --role-cap <role>=<n>: what to change.');
|
|
905
919
|
}
|
|
920
|
+
// Refused before anything is written, so a bad cap leaves the charter as it was too.
|
|
921
|
+
const caps = values['role-cap'] === undefined ? undefined : values['role-cap'].map(roleCap);
|
|
922
|
+
if (caps !== undefined) setRoleCaps(bots, values.bot, caps.map(({ role, cap }) => [role, cap]), { write: false });
|
|
906
923
|
|
|
907
924
|
const answer = { bots, bot: values.bot, home: botDir(bots, values.bot) };
|
|
908
925
|
const lines = [];
|
|
@@ -926,7 +943,7 @@ const commands = {
|
|
|
926
943
|
// An allow list that is not a list is named by rules build and health,
|
|
927
944
|
// and a charter already written is not undone for it.
|
|
928
945
|
const bot = readBot(changed.home, changed.bot);
|
|
929
|
-
if (
|
|
946
|
+
if (runsOnClaude(bot) || runsOnCodex(bot)) {
|
|
930
947
|
try {
|
|
931
948
|
answer.beyondDefaults = beyondDefaults(bots, changed.home, bot);
|
|
932
949
|
lines.push(...charterRulesLines(bots, bot, answer.beyondDefaults));
|
|
@@ -935,15 +952,31 @@ const commands = {
|
|
|
935
952
|
}
|
|
936
953
|
}
|
|
937
954
|
}
|
|
938
|
-
if (
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
955
|
+
if (caps !== undefined) {
|
|
956
|
+
setRoleCaps(bots, values.bot, caps.map(({ role, cap }) => [role, cap]));
|
|
957
|
+
answer.caps = Object.fromEntries(caps.map(({ role, cap }) => [role, cap ?? null]));
|
|
958
|
+
lines.push(...caps.map(({ role, cap }) => (cap === undefined
|
|
959
|
+
? `changed the role ${role} in ${path.join('bots', values.bot, 'bot.yaml')}: no cap`
|
|
960
|
+
: `changed the role ${role} in ${path.join('bots', values.bot, 'bot.yaml')}: cap ${cap}`)));
|
|
961
|
+
}
|
|
962
|
+
return { answer, lines, code: trouble ? 1 : 0 };
|
|
963
|
+
},
|
|
964
|
+
|
|
965
|
+
'permission allow'(bots, values) {
|
|
966
|
+
// Refused before anything is written, a bad list already there included.
|
|
967
|
+
const { home } = allowedNow(bots, values.bot, values.rule);
|
|
968
|
+
refuseBroad(home, values.rule);
|
|
969
|
+
const bot = readBot(home, values.bot);
|
|
970
|
+
refuseNoCodexForm(bot, values.rule);
|
|
971
|
+
const { allow } = allowRules(bots, values.bot, values.rule, { write: false });
|
|
972
|
+
// The harness's files first, bot.yaml last, so a write that fails leaves
|
|
973
|
+
// `allow` as it was, and the same command can run again (#383).
|
|
974
|
+
const permissions = allowIn(bots, home, { ...bot, allow })();
|
|
975
|
+
const allowed = allowRules(bots, values.bot, values.rule);
|
|
976
|
+
const codex = permissions.find((entry) => entry.unwritten !== undefined);
|
|
977
|
+
return {
|
|
978
|
+
answer: { bots, bot: values.bot, home, allow: allowed.allow, permissions },
|
|
979
|
+
lines: [
|
|
947
980
|
...allowed.added.map((rule) => `${'allowed'.padEnd(9)} ${rule}`),
|
|
948
981
|
...permissionsLines(permissions, bots),
|
|
949
982
|
// Codex reads its rules when a session starts, not while it runs.
|
|
@@ -953,16 +986,22 @@ const commands = {
|
|
|
953
986
|
allowed.added.length === 0
|
|
954
987
|
? `${allowed.bot} was allowed every one of these already.`
|
|
955
988
|
: `${allowed.bot}'s allow list in ${path.join('bots', allowed.bot, 'bot.yaml')} holds the user's yes.`,
|
|
956
|
-
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
989
|
+
],
|
|
990
|
+
};
|
|
991
|
+
},
|
|
992
|
+
|
|
993
|
+
'permission disallow'(bots, values) {
|
|
994
|
+
const { home } = allowedNow(bots, values.bot, values.rule);
|
|
995
|
+
const takeBackFrom = takeBack(bots, home, readBot(home, values.bot), values.rule);
|
|
996
|
+
disallowRules(bots, values.bot, values.rule, { write: false });
|
|
997
|
+
// The harness's files first, bot.yaml last: a write that fails leaves the
|
|
998
|
+
// rules in `allow`, so the same command can take them back again (#360).
|
|
999
|
+
const permissions = takeBackFrom();
|
|
1000
|
+
const taken = disallowRules(bots, values.bot, values.rule);
|
|
1001
|
+
const codex = permissions.find((entry) => entry.unwritten !== undefined);
|
|
1002
|
+
return {
|
|
1003
|
+
answer: { bots, bot: values.bot, home, allow: taken.allow, disallowed: taken.disallowed, permissions },
|
|
1004
|
+
lines: [
|
|
966
1005
|
...taken.disallowed.map((rule) => `${'took back'.padEnd(9)} ${rule}`),
|
|
967
1006
|
...permissions.filter((entry) => entry.removed?.length > 0).map((entry) => `${'wrote'.padEnd(9)} ${path.relative(bots, entry.file)} ${entry.removed.length} permission rule${entry.removed.length === 1 ? '' : 's'} taken out`),
|
|
968
1007
|
...permissionsLines(permissions, bots),
|
|
@@ -970,9 +1009,36 @@ const commands = {
|
|
|
970
1009
|
`Codex reads ${codex.file} when a session starts: a Codex session of ${taken.bot} that is running now keeps ${codex.removed.length === 1 ? 'this rule' : 'these rules'} until its next start (obk restart).`,
|
|
971
1010
|
]),
|
|
972
1011
|
`${taken.bot}'s allow list in ${path.join('bots', taken.bot, 'bot.yaml')} no longer holds ${taken.disallowed.length === 1 ? 'it' : 'them'}.`,
|
|
973
|
-
|
|
1012
|
+
],
|
|
1013
|
+
};
|
|
1014
|
+
},
|
|
1015
|
+
|
|
1016
|
+
'permission approval'(bots, values) {
|
|
1017
|
+
if ((values.session === undefined) === (values.temps !== true)) {
|
|
1018
|
+
throw new Error('permission approval needs --session <name>, for a session, or --temps, for the bot\'s temporary sessions, and not both. Nothing was changed.');
|
|
974
1019
|
}
|
|
975
|
-
|
|
1020
|
+
if (!APPROVALS.includes(values.approval)) {
|
|
1021
|
+
throw new Error(`--approval is ${APPROVALS.join(', ')}, and got: ${values.approval === '' ? 'nothing' : values.approval}. Nothing was changed.`);
|
|
1022
|
+
}
|
|
1023
|
+
if (values.temps) {
|
|
1024
|
+
const set = setTempApproval(bots, values.bot, values.approval);
|
|
1025
|
+
return {
|
|
1026
|
+
answer: { bots, bot: set.bot, home: set.home, temp_approval: set.level },
|
|
1027
|
+
lines: [
|
|
1028
|
+
`changed temp_approval in ${path.join('bots', set.bot, 'bot.yaml')}: ${set.level}`,
|
|
1029
|
+
`${set.bot}'s temporary sessions may now be made at ${set.level}, or at their maker's own level where that is wider.`,
|
|
1030
|
+
],
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
const changed = changeSession(bots, values.bot, values.session, { approval: values.approval });
|
|
1034
|
+
const restart = `${shellWord(ownCli())} restart --bots ${shellWord(bots)} --bot ${changed.bot} --session ${values.session}`;
|
|
1035
|
+
return {
|
|
1036
|
+
answer: { bots, bot: changed.bot, home: changed.home, session: changed.session, restart },
|
|
1037
|
+
lines: [
|
|
1038
|
+
`changed session ${values.session} in ${path.join('bots', changed.bot, 'bot.yaml')}: approval ${values.approval}`,
|
|
1039
|
+
`A running session takes this when it next starts: ${restart}`,
|
|
1040
|
+
],
|
|
1041
|
+
};
|
|
976
1042
|
},
|
|
977
1043
|
|
|
978
1044
|
'skills build'(bots, values) {
|
|
@@ -1050,6 +1116,8 @@ const commands = {
|
|
|
1050
1116
|
...(values.effort === undefined ? {} : { effort: values.effort }),
|
|
1051
1117
|
...(values['extra-arg'] === undefined ? {} : { extra_args: values['extra-arg'] }),
|
|
1052
1118
|
};
|
|
1119
|
+
// A run's launch may not widen its approval (ADR 0037).
|
|
1120
|
+
if (values['run-on'] === 'codex') refuseApprovalArgs('codex', values['extra-arg']);
|
|
1053
1121
|
const groom = grooming(bots, { at: values.at, ask, run });
|
|
1054
1122
|
return { answer: { bots, groom }, lines: groomLines(groom, bots) };
|
|
1055
1123
|
},
|
|
@@ -1181,6 +1249,7 @@ const commands = {
|
|
|
1181
1249
|
},
|
|
1182
1250
|
|
|
1183
1251
|
'session add'(bots, values) {
|
|
1252
|
+
refuseApproval(bots, values, 'session add');
|
|
1184
1253
|
const added = addSession(bots, values.bot, settingsOf(values));
|
|
1185
1254
|
const found = workDirFound(values, added.bot, added.home, added.session.name);
|
|
1186
1255
|
const answer = { bots, bot: added.bot, home: added.home, session: added.session, found };
|
|
@@ -1213,6 +1282,7 @@ const commands = {
|
|
|
1213
1282
|
if (values.harness !== undefined) {
|
|
1214
1283
|
throw new Error(`session change does not change a session's harness: its conversations belong to the harness they ran on. To move it, retire it with obk retire and add one on ${values.harness} with obk session add.`);
|
|
1215
1284
|
}
|
|
1285
|
+
refuseApproval(bots, values, 'session change');
|
|
1216
1286
|
const { name, ...settings } = settingsOf(values);
|
|
1217
1287
|
const changed = changeSession(bots, values.bot, values.session, settings);
|
|
1218
1288
|
const restart = `${shellWord(ownCli())} restart --bots ${shellWord(bots)} --bot ${changed.bot} --session ${values.session}`;
|
|
@@ -1262,7 +1332,12 @@ const commands = {
|
|
|
1262
1332
|
function toLines(answer, bots, where) {
|
|
1263
1333
|
if (answer.trouble !== undefined) return [` ${answer.trouble}`];
|
|
1264
1334
|
if (answer.transport === 'native') {
|
|
1265
|
-
|
|
1335
|
+
// A refused call is not re-sent by another road (PRD 6.9), so the answer
|
|
1336
|
+
// says what to do instead, before the bot needs it (#521).
|
|
1337
|
+
return [
|
|
1338
|
+
` Write to ${answer.address} with your own harness's messaging. The kit does not carry that road.`,
|
|
1339
|
+
" If your harness refuses that call, do not send the message again by another road. Tell your maker or your user that it was refused, with the harness's reason. A refusal can mean a missing permission rule, which they can add through the kit after the user's yes.",
|
|
1340
|
+
];
|
|
1266
1341
|
}
|
|
1267
1342
|
return [
|
|
1268
1343
|
...(answer.unnamed === true
|
|
@@ -1330,6 +1405,43 @@ function workDirFound(values, bot, home, session) {
|
|
|
1330
1405
|
}];
|
|
1331
1406
|
}
|
|
1332
1407
|
|
|
1408
|
+
/**
|
|
1409
|
+
* Refuse what would change a session's approval through `session add` or
|
|
1410
|
+
* `session change` (ADR 0037): `--approval`, and an extra argument that sets
|
|
1411
|
+
* it on the session's harness. Only `permission approval` changes it.
|
|
1412
|
+
*/
|
|
1413
|
+
function refuseApproval(bots, values, command) {
|
|
1414
|
+
if (values.approval !== undefined) {
|
|
1415
|
+
throw new Error(`${command} no longer takes --approval: a session's approval changes through ${shellWord(ownCli())} permission approval --bots ${shellWord(bots)} --bot ${values.bot} --session ${values.session ?? values.name} --approval <level>, after the user's yes. Nothing was changed.`);
|
|
1416
|
+
}
|
|
1417
|
+
if (values['extra-arg'] === undefined) return;
|
|
1418
|
+
let bot;
|
|
1419
|
+
try {
|
|
1420
|
+
bot = readBot(botDir(bots, values.bot), values.bot);
|
|
1421
|
+
} catch {
|
|
1422
|
+
// A bot that is not there is the command's own to refuse.
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
const session = bot.sessions.find((one) => one.name === (values.session ?? values.name)) ?? {};
|
|
1426
|
+
refuseApprovalArgs(harnessOf({ ...session, ...(values.harness === undefined ? {} : { harness: values.harness }) }, bot.harness), values['extra-arg']);
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
/**
|
|
1430
|
+
* One `--role-cap <role>=<n>`, as `{ role, cap }`: a cap of undefined, from
|
|
1431
|
+
* `<role>=`, takes it off. A cap is a whole number of open sessions, 1 or more.
|
|
1432
|
+
*/
|
|
1433
|
+
function roleCap(given) {
|
|
1434
|
+
const at = given.indexOf('=');
|
|
1435
|
+
if (at <= 0) throw new Error(`--role-cap is <role>=<n>, such as reviewer=2, or <role>= to take a cap off, and got: ${given}. Nothing was changed.`);
|
|
1436
|
+
const role = given.slice(0, at);
|
|
1437
|
+
const text = given.slice(at + 1);
|
|
1438
|
+
if (text === '') return { role };
|
|
1439
|
+
if (!/^\d+$/.test(text) || Number(text) < 1) {
|
|
1440
|
+
throw new Error(`--role-cap ${given}: a cap is a whole number of open sessions, 1 or more, such as ${role}=2. Nothing was changed.`);
|
|
1441
|
+
}
|
|
1442
|
+
return { role, cap: Number(text) };
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1333
1445
|
/** The settings a `session add` was given, as they go into bot.yaml. */
|
|
1334
1446
|
function settingsOf(values) {
|
|
1335
1447
|
const settings = { name: values.name };
|
|
@@ -1661,7 +1773,7 @@ function permissionsOf(bots, name) {
|
|
|
1661
1773
|
*/
|
|
1662
1774
|
function charterRulesLines(bots, bot, rules) {
|
|
1663
1775
|
const name = bot.name;
|
|
1664
|
-
const until = `No new rule is written until the user answers: list the rules the new charter grants, show them to the user word for word, and allow the ones they say yes to with ${shellWord(ownCli())}
|
|
1776
|
+
const until = `No new rule is written until the user answers: list the rules the new charter grants, show them to the user word for word, and allow the ones they say yes to with ${shellWord(ownCli())} permission allow --bots ${shellWord(bots)} --bot ${shellWord(name)} --rule <rule>`;
|
|
1665
1777
|
if (rules.length === 0) return [`${name} is allowed no permission rules beyond the kit's defaults. ${until}`];
|
|
1666
1778
|
const settings = runsOnClaude(bot) ? ` and ${path.join('bots', name, '.claude', 'settings.json')}` : '';
|
|
1667
1779
|
// The kit owns the Codex file whole and rewrites it from bot.yaml (#354).
|
|
@@ -1669,34 +1781,30 @@ function charterRulesLines(bots, bot, rules) {
|
|
|
1669
1781
|
return [
|
|
1670
1782
|
`${name} is allowed these permission rules beyond the kit's defaults, from before this change:`,
|
|
1671
1783
|
...rules.map((rule) => ` ${rule}`),
|
|
1672
|
-
`They stay allowed, whatever the user answers, until the user says yes to taking one back out of ${path.join('bots', name, 'bot.yaml')}${settings}${codex}. For one the new charter no longer grants, ask the user, and only after their yes run ${shellWord(ownCli())}
|
|
1784
|
+
`They stay allowed, whatever the user answers, until the user says yes to taking one back out of ${path.join('bots', name, 'bot.yaml')}${settings}${codex}. For one the new charter no longer grants, ask the user, and only after their yes run ${shellWord(ownCli())} permission disallow --bots ${shellWord(bots)} --bot ${shellWord(name)} --rule <rule>. ${until}`,
|
|
1673
1785
|
];
|
|
1674
1786
|
}
|
|
1675
1787
|
|
|
1676
1788
|
/**
|
|
1677
|
-
* What became of each bot's permission rules:
|
|
1678
|
-
*
|
|
1679
|
-
*
|
|
1680
|
-
* it (#344). Nothing for a bot with nothing written and nothing waiting.
|
|
1789
|
+
* What became of each bot's permission rules: each of the kit's default rules
|
|
1790
|
+
* the run added to the bot, word for word (ADR 0036), and the file the allowed
|
|
1791
|
+
* ones were written into. Nothing for a bot with nothing added or written.
|
|
1681
1792
|
*/
|
|
1682
1793
|
function permissionsLines(permissions, bots) {
|
|
1683
|
-
// A bot on both harnesses has an entry for each file,
|
|
1684
|
-
//
|
|
1794
|
+
// A bot on both harnesses has an entry for each file, with the same
|
|
1795
|
+
// defaults: they are listed once, with the first.
|
|
1685
1796
|
const listed = new Set();
|
|
1686
1797
|
return permissions.flatMap((entry) => [
|
|
1687
1798
|
...(entry.trouble === undefined ? [] : [`${'refused'.padEnd(9)} ${entry.trouble}`]),
|
|
1799
|
+
...((entry.defaults ?? []).length === 0 || listed.has(entry.bot) || !listed.add(entry.bot) ? []
|
|
1800
|
+
: entry.defaults.map((rule) => `${'allowed'.padEnd(9)} ${entry.bot} ${rule}`)),
|
|
1688
1801
|
...(entry.written.length === 0
|
|
1689
1802
|
? []
|
|
1690
|
-
: [`${'wrote'.padEnd(9)} ${path.relative(bots, entry.file)} ${entry.written.length} permission rule${entry.written.length === 1 ? '' : 's'}
|
|
1803
|
+
: [`${'wrote'.padEnd(9)} ${path.relative(bots, entry.file)} ${entry.written.length} permission rule${entry.written.length === 1 ? '' : 's'} from its allow list`]),
|
|
1691
1804
|
// What the user allowed and Codex has no form for is said at every build,
|
|
1692
1805
|
// so a bot given a Codex session later hears of it too (#354). For a bot
|
|
1693
|
-
// only on Codex,
|
|
1806
|
+
// only on Codex, `permission allow` refused it already.
|
|
1694
1807
|
...(entry.unwritten ?? []).map((one) => `${'not'.padEnd(9)} written for Codex: ${one.rule}. ${one.why}.`),
|
|
1695
|
-
...(entry.waiting.length === 0 || listed.has(entry.bot) || !listed.add(entry.bot) ? [] : [
|
|
1696
|
-
`${'waiting'.padEnd(9)} ${entry.bot}: these permission rules wait for the user's yes, and none of them is written until then:`,
|
|
1697
|
-
...entry.waiting.map((rule) => ` ${rule}`),
|
|
1698
|
-
` Show them to the user. Once they say yes: ${allowCommand(bots, entry.bot, entry.waiting)}`,
|
|
1699
|
-
]),
|
|
1700
1808
|
]);
|
|
1701
1809
|
}
|
|
1702
1810
|
|