@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/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';
|
|
@@ -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.
|
|
@@ -313,52 +329,12 @@ Usage:
|
|
|
313
329
|
obk --help Print this text.
|
|
314
330
|
|
|
315
331
|
Every command is safe to run again. Most only add what is missing; the ones
|
|
316
|
-
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.
|
|
317
335
|
Add --json to any of them for the same answer as JSON.
|
|
318
336
|
`;
|
|
319
337
|
|
|
320
|
-
/** The commands, and the flags each one cannot do without. */
|
|
321
|
-
const COMMANDS = {
|
|
322
|
-
init: ['bots', 'harness'],
|
|
323
|
-
up: ['bots'],
|
|
324
|
-
restart: ['bots', 'bot'],
|
|
325
|
-
pause: ['bots', 'bot'],
|
|
326
|
-
unpause: ['bots', 'bot'],
|
|
327
|
-
retire: ['bots', 'bot'],
|
|
328
|
-
health: ['bots'],
|
|
329
|
-
groom: ['bots'],
|
|
330
|
-
roster: ['bots'],
|
|
331
|
-
usage: ['bots'],
|
|
332
|
-
'bot create': ['bots', 'name', 'harness'],
|
|
333
|
-
'bot change': ['bots', 'bot'],
|
|
334
|
-
'rules build': ['bots'],
|
|
335
|
-
'skills add': ['bots', 'bot', 'skill'],
|
|
336
|
-
'skills remove': ['bots', 'bot', 'skill'],
|
|
337
|
-
'skills build': ['bots'],
|
|
338
|
-
'skills fetch': ['bots'],
|
|
339
|
-
'skills update': ['bots'],
|
|
340
|
-
'source add': ['bots', 'name', 'repo', 'ref'],
|
|
341
|
-
'session add': ['bots', 'bot', 'name'],
|
|
342
|
-
'session change': ['bots', 'bot', 'session'],
|
|
343
|
-
'session clear': ['bots', 'bot', 'session'],
|
|
344
|
-
'session compact': ['bots', 'bot', 'session'],
|
|
345
|
-
'message to': ['bots', 'to'],
|
|
346
|
-
'message send': ['bots', 'to', 'subject'],
|
|
347
|
-
'message check': ['bots'],
|
|
348
|
-
'session record': ['bots', 'bot'],
|
|
349
|
-
'session sent': ['bots', 'bot'],
|
|
350
|
-
'session nudge': ['bots', 'bot'],
|
|
351
|
-
'session mail': ['bots', 'bot'],
|
|
352
|
-
'session name': ['bots', 'bot'],
|
|
353
|
-
'session mailbox': ['bots', 'bot', 'session'],
|
|
354
|
-
'session trust-hooks': ['bots', 'bot', 'session'],
|
|
355
|
-
'session answer': ['bots', 'bot', 'session'],
|
|
356
|
-
'temp make': ['bots', 'name'],
|
|
357
|
-
'temp roles': ['bots'],
|
|
358
|
-
'temp retire': ['bots', 'name'],
|
|
359
|
-
'temp trust-hooks': ['bots', 'name'],
|
|
360
|
-
'temp answer': ['bots', 'name'],
|
|
361
|
-
};
|
|
362
338
|
|
|
363
339
|
/** What each flag is for, in the sentence a caller reads when it is missing. */
|
|
364
340
|
const NEEDED = {
|
|
@@ -379,10 +355,15 @@ const NEEDED = {
|
|
|
379
355
|
from: '--from <bot>/<session>: which session is writing',
|
|
380
356
|
subject: '--subject <text>: what the message is about',
|
|
381
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`,
|
|
382
360
|
};
|
|
383
361
|
|
|
384
|
-
/**
|
|
385
|
-
|
|
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');
|
|
386
367
|
|
|
387
368
|
/**
|
|
388
369
|
* The session settings a flag can carry, as `[flag, field]`: every field a
|
|
@@ -399,9 +380,10 @@ function version() {
|
|
|
399
380
|
}
|
|
400
381
|
|
|
401
382
|
async function run(argv) {
|
|
402
|
-
const { values, positionals } = parseArgs({
|
|
383
|
+
const { values, positionals, tokens } = parseArgs({
|
|
403
384
|
args: argv,
|
|
404
385
|
allowPositionals: true,
|
|
386
|
+
tokens: true,
|
|
405
387
|
options: {
|
|
406
388
|
bots: { type: 'string' },
|
|
407
389
|
harness: { type: 'string' },
|
|
@@ -430,6 +412,9 @@ async function run(argv) {
|
|
|
430
412
|
charter: { type: 'string' },
|
|
431
413
|
allow: { type: 'string', multiple: true },
|
|
432
414
|
disallow: { type: 'string', multiple: true },
|
|
415
|
+
rule: { type: 'string', multiple: true },
|
|
416
|
+
temps: { type: 'boolean' },
|
|
417
|
+
'role-cap': { type: 'string', multiple: true },
|
|
433
418
|
...Object.fromEntries(SETTINGS.map(([flag]) => [flag, { type: 'string' }])),
|
|
434
419
|
'extra-arg': { type: 'string', multiple: true },
|
|
435
420
|
'run-on': { type: 'string' },
|
|
@@ -454,9 +439,16 @@ async function run(argv) {
|
|
|
454
439
|
return 1;
|
|
455
440
|
}
|
|
456
441
|
|
|
457
|
-
//
|
|
458
|
-
//
|
|
459
|
-
|
|
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;
|
|
460
452
|
const command = positionals.slice(0, words).join(' ');
|
|
461
453
|
const extra = positionals.slice(words);
|
|
462
454
|
|
|
@@ -856,7 +848,7 @@ const commands = {
|
|
|
856
848
|
answer: { bots, ...answered },
|
|
857
849
|
lines: [
|
|
858
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`,
|
|
859
|
-
`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)}:*)`,
|
|
860
852
|
],
|
|
861
853
|
};
|
|
862
854
|
},
|
|
@@ -889,14 +881,14 @@ const commands = {
|
|
|
889
881
|
// A new bot is given what the lists already name, so it is whole before
|
|
890
882
|
// anybody opens a tab on it.
|
|
891
883
|
const skills = [linkSkills(bots, made.home, readBot(made.home))];
|
|
892
|
-
// Nothing is allowed yet, so this writes nothing: it says which of the
|
|
893
|
-
// kit's rules wait for the user's yes (#344).
|
|
894
|
-
const permissions = writePermissions(bots, made.home, readBot(made.home));
|
|
895
|
-
const answer = { bots, bot: made.bot, home: made.home, created: made.created, rules, skills, permissions };
|
|
896
884
|
// A bot whose rules would not build is made but not finished: it has no
|
|
897
885
|
// instructions, so `up` will not start it, and saying "give it a session"
|
|
898
886
|
// would send the caller past the thing that needs settling first.
|
|
899
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 };
|
|
900
892
|
return {
|
|
901
893
|
answer,
|
|
902
894
|
lines: [
|
|
@@ -916,29 +908,18 @@ const commands = {
|
|
|
916
908
|
if (values.harness !== undefined) {
|
|
917
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.`);
|
|
918
910
|
}
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
if (
|
|
923
|
-
throw new Error(
|
|
911
|
+
// Permission changes have commands of their own, which keep the user's yes
|
|
912
|
+
// (ADR 0041): 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.`);
|
|
924
916
|
}
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
let allowInto;
|
|
928
|
-
if (values.allow !== undefined) {
|
|
929
|
-
const { home } = allowedNow(bots, values.bot, values.allow);
|
|
930
|
-
refuseBroad(home, values.allow);
|
|
931
|
-
const bot = readBot(home, values.bot);
|
|
932
|
-
refuseNoCodexForm(bot, values.allow);
|
|
933
|
-
const { allow } = allowRules(bots, values.bot, values.allow, { write: false });
|
|
934
|
-
allowInto = allowIn(bots, home, { ...bot, allow });
|
|
935
|
-
}
|
|
936
|
-
let takeBackFrom;
|
|
937
|
-
if (values.disallow !== undefined) {
|
|
938
|
-
const { home } = allowedNow(bots, values.bot, values.disallow, '--disallow');
|
|
939
|
-
takeBackFrom = takeBack(bots, home, readBot(home, values.bot), values.disallow);
|
|
940
|
-
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.');
|
|
941
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 });
|
|
942
923
|
|
|
943
924
|
const answer = { bots, bot: values.bot, home: botDir(bots, values.bot) };
|
|
944
925
|
const lines = [];
|
|
@@ -962,7 +943,7 @@ const commands = {
|
|
|
962
943
|
// An allow list that is not a list is named by rules build and health,
|
|
963
944
|
// and a charter already written is not undone for it.
|
|
964
945
|
const bot = readBot(changed.home, changed.bot);
|
|
965
|
-
if (
|
|
946
|
+
if (runsOnClaude(bot) || runsOnCodex(bot)) {
|
|
966
947
|
try {
|
|
967
948
|
answer.beyondDefaults = beyondDefaults(bots, changed.home, bot);
|
|
968
949
|
lines.push(...charterRulesLines(bots, bot, answer.beyondDefaults));
|
|
@@ -971,15 +952,31 @@ const commands = {
|
|
|
971
952
|
}
|
|
972
953
|
}
|
|
973
954
|
}
|
|
974
|
-
if (
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
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: [
|
|
983
980
|
...allowed.added.map((rule) => `${'allowed'.padEnd(9)} ${rule}`),
|
|
984
981
|
...permissionsLines(permissions, bots),
|
|
985
982
|
// Codex reads its rules when a session starts, not while it runs.
|
|
@@ -989,16 +986,22 @@ const commands = {
|
|
|
989
986
|
allowed.added.length === 0
|
|
990
987
|
? `${allowed.bot} was allowed every one of these already.`
|
|
991
988
|
: `${allowed.bot}'s allow list in ${path.join('bots', allowed.bot, 'bot.yaml')} holds the user's yes.`,
|
|
992
|
-
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
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: [
|
|
1002
1005
|
...taken.disallowed.map((rule) => `${'took back'.padEnd(9)} ${rule}`),
|
|
1003
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`),
|
|
1004
1007
|
...permissionsLines(permissions, bots),
|
|
@@ -1006,9 +1009,36 @@ const commands = {
|
|
|
1006
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).`,
|
|
1007
1010
|
]),
|
|
1008
1011
|
`${taken.bot}'s allow list in ${path.join('bots', taken.bot, 'bot.yaml')} no longer holds ${taken.disallowed.length === 1 ? 'it' : 'them'}.`,
|
|
1009
|
-
|
|
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.');
|
|
1010
1019
|
}
|
|
1011
|
-
|
|
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
|
+
};
|
|
1012
1042
|
},
|
|
1013
1043
|
|
|
1014
1044
|
'skills build'(bots, values) {
|
|
@@ -1086,6 +1116,8 @@ const commands = {
|
|
|
1086
1116
|
...(values.effort === undefined ? {} : { effort: values.effort }),
|
|
1087
1117
|
...(values['extra-arg'] === undefined ? {} : { extra_args: values['extra-arg'] }),
|
|
1088
1118
|
};
|
|
1119
|
+
// A run's launch may not widen its approval (ADR 0041).
|
|
1120
|
+
if (values['run-on'] === 'codex') refuseApprovalArgs('codex', values['extra-arg']);
|
|
1089
1121
|
const groom = grooming(bots, { at: values.at, ask, run });
|
|
1090
1122
|
return { answer: { bots, groom }, lines: groomLines(groom, bots) };
|
|
1091
1123
|
},
|
|
@@ -1217,6 +1249,7 @@ const commands = {
|
|
|
1217
1249
|
},
|
|
1218
1250
|
|
|
1219
1251
|
'session add'(bots, values) {
|
|
1252
|
+
refuseApproval(bots, values, 'session add');
|
|
1220
1253
|
const added = addSession(bots, values.bot, settingsOf(values));
|
|
1221
1254
|
const found = workDirFound(values, added.bot, added.home, added.session.name);
|
|
1222
1255
|
const answer = { bots, bot: added.bot, home: added.home, session: added.session, found };
|
|
@@ -1249,6 +1282,7 @@ const commands = {
|
|
|
1249
1282
|
if (values.harness !== undefined) {
|
|
1250
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.`);
|
|
1251
1284
|
}
|
|
1285
|
+
refuseApproval(bots, values, 'session change');
|
|
1252
1286
|
const { name, ...settings } = settingsOf(values);
|
|
1253
1287
|
const changed = changeSession(bots, values.bot, values.session, settings);
|
|
1254
1288
|
const restart = `${shellWord(ownCli())} restart --bots ${shellWord(bots)} --bot ${changed.bot} --session ${values.session}`;
|
|
@@ -1371,6 +1405,43 @@ function workDirFound(values, bot, home, session) {
|
|
|
1371
1405
|
}];
|
|
1372
1406
|
}
|
|
1373
1407
|
|
|
1408
|
+
/**
|
|
1409
|
+
* Refuse what would change a session's approval through `session add` or
|
|
1410
|
+
* `session change` (ADR 0041): `--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
|
+
|
|
1374
1445
|
/** The settings a `session add` was given, as they go into bot.yaml. */
|
|
1375
1446
|
function settingsOf(values) {
|
|
1376
1447
|
const settings = { name: values.name };
|
|
@@ -1702,7 +1773,7 @@ function permissionsOf(bots, name) {
|
|
|
1702
1773
|
*/
|
|
1703
1774
|
function charterRulesLines(bots, bot, rules) {
|
|
1704
1775
|
const name = bot.name;
|
|
1705
|
-
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>`;
|
|
1706
1777
|
if (rules.length === 0) return [`${name} is allowed no permission rules beyond the kit's defaults. ${until}`];
|
|
1707
1778
|
const settings = runsOnClaude(bot) ? ` and ${path.join('bots', name, '.claude', 'settings.json')}` : '';
|
|
1708
1779
|
// The kit owns the Codex file whole and rewrites it from bot.yaml (#354).
|
|
@@ -1710,34 +1781,35 @@ function charterRulesLines(bots, bot, rules) {
|
|
|
1710
1781
|
return [
|
|
1711
1782
|
`${name} is allowed these permission rules beyond the kit's defaults, from before this change:`,
|
|
1712
1783
|
...rules.map((rule) => ` ${rule}`),
|
|
1713
|
-
`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}`,
|
|
1714
1785
|
];
|
|
1715
1786
|
}
|
|
1716
1787
|
|
|
1717
1788
|
/**
|
|
1718
|
-
* What became of each bot's permission rules:
|
|
1719
|
-
*
|
|
1720
|
-
*
|
|
1721
|
-
*
|
|
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), each rule of a command no
|
|
1791
|
+
* longer in the set that the bot still holds, with the command that takes it
|
|
1792
|
+
* back (ADR 0041), and the file the allowed ones were written into. Nothing for
|
|
1793
|
+
* a bot with nothing added, held or written.
|
|
1722
1794
|
*/
|
|
1723
1795
|
function permissionsLines(permissions, bots) {
|
|
1724
|
-
// A bot on both harnesses has an entry for each file,
|
|
1725
|
-
//
|
|
1796
|
+
// A bot on both harnesses has an entry for each file, with the same
|
|
1797
|
+
// defaults and held rules: they are listed once, with the first.
|
|
1726
1798
|
const listed = new Set();
|
|
1799
|
+
const disallow = (bot, rule) => `${shellWord(ownCli())} permission disallow --bots ${shellWord(bots)} --bot ${shellWord(bot)} --rule ${shellWord(rule)}`;
|
|
1727
1800
|
return permissions.flatMap((entry) => [
|
|
1728
1801
|
...(entry.trouble === undefined ? [] : [`${'refused'.padEnd(9)} ${entry.trouble}`]),
|
|
1802
|
+
...(listed.has(entry.bot) || !listed.add(entry.bot) ? [] : [
|
|
1803
|
+
...(entry.defaults ?? []).map((rule) => `${'allowed'.padEnd(9)} ${entry.bot} ${rule}`),
|
|
1804
|
+
...(entry.held ?? []).map((rule) => `${'holds'.padEnd(9)} ${entry.bot} ${rule}, which is no longer one of the kit's defaults. To take it back: ${disallow(entry.bot, rule)}`),
|
|
1805
|
+
]),
|
|
1729
1806
|
...(entry.written.length === 0
|
|
1730
1807
|
? []
|
|
1731
|
-
: [`${'wrote'.padEnd(9)} ${path.relative(bots, entry.file)} ${entry.written.length} permission rule${entry.written.length === 1 ? '' : 's'}
|
|
1808
|
+
: [`${'wrote'.padEnd(9)} ${path.relative(bots, entry.file)} ${entry.written.length} permission rule${entry.written.length === 1 ? '' : 's'} from its allow list`]),
|
|
1732
1809
|
// What the user allowed and Codex has no form for is said at every build,
|
|
1733
1810
|
// so a bot given a Codex session later hears of it too (#354). For a bot
|
|
1734
|
-
// only on Codex,
|
|
1811
|
+
// only on Codex, `permission allow` refused it already.
|
|
1735
1812
|
...(entry.unwritten ?? []).map((one) => `${'not'.padEnd(9)} written for Codex: ${one.rule}. ${one.why}.`),
|
|
1736
|
-
...(entry.waiting.length === 0 || listed.has(entry.bot) || !listed.add(entry.bot) ? [] : [
|
|
1737
|
-
`${'waiting'.padEnd(9)} ${entry.bot}: these permission rules wait for the user's yes, and none of them is written until then:`,
|
|
1738
|
-
...entry.waiting.map((rule) => ` ${rule}`),
|
|
1739
|
-
` Show them to the user. Once they say yes: ${allowCommand(bots, entry.bot, entry.waiting)}`,
|
|
1740
|
-
]),
|
|
1741
1813
|
]);
|
|
1742
1814
|
}
|
|
1743
1815
|
|
package/src/commands.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// The kit's commands, each with the flags it cannot do without, which `obk`
|
|
2
|
+
// reads to parse a command line; and the two lists every command is on one of:
|
|
3
|
+
// those every bot is allowed by default (#527, ADR 0036), and those kept back
|
|
4
|
+
// for the user's yes (ADR 0041). A new command is on neither until someone
|
|
5
|
+
// puts it on one, and a test holds every command to that choice.
|
|
6
|
+
|
|
7
|
+
/** The commands, and the flags each one cannot do without. */
|
|
8
|
+
export const COMMANDS = {
|
|
9
|
+
init: ['bots', 'harness'],
|
|
10
|
+
up: ['bots'],
|
|
11
|
+
restart: ['bots', 'bot'],
|
|
12
|
+
pause: ['bots', 'bot'],
|
|
13
|
+
unpause: ['bots', 'bot'],
|
|
14
|
+
retire: ['bots', 'bot'],
|
|
15
|
+
health: ['bots'],
|
|
16
|
+
groom: ['bots'],
|
|
17
|
+
roster: ['bots'],
|
|
18
|
+
usage: ['bots'],
|
|
19
|
+
'bot create': ['bots', 'name', 'harness'],
|
|
20
|
+
'bot change': ['bots', 'bot'],
|
|
21
|
+
'permission allow': ['bots', 'bot', 'rule'],
|
|
22
|
+
'permission disallow': ['bots', 'bot', 'rule'],
|
|
23
|
+
'permission approval': ['bots', 'bot', 'approval'],
|
|
24
|
+
'rules build': ['bots'],
|
|
25
|
+
'skills add': ['bots', 'bot', 'skill'],
|
|
26
|
+
'skills remove': ['bots', 'bot', 'skill'],
|
|
27
|
+
'skills build': ['bots'],
|
|
28
|
+
'skills fetch': ['bots'],
|
|
29
|
+
'skills update': ['bots'],
|
|
30
|
+
'source add': ['bots', 'name', 'repo', 'ref'],
|
|
31
|
+
'session add': ['bots', 'bot', 'name'],
|
|
32
|
+
'session change': ['bots', 'bot', 'session'],
|
|
33
|
+
'session clear': ['bots', 'bot', 'session'],
|
|
34
|
+
'session compact': ['bots', 'bot', 'session'],
|
|
35
|
+
'message to': ['bots', 'to'],
|
|
36
|
+
'message send': ['bots', 'to', 'subject'],
|
|
37
|
+
'message check': ['bots'],
|
|
38
|
+
'session record': ['bots', 'bot'],
|
|
39
|
+
'session sent': ['bots', 'bot'],
|
|
40
|
+
'session nudge': ['bots', 'bot'],
|
|
41
|
+
'session mail': ['bots', 'bot'],
|
|
42
|
+
'session name': ['bots', 'bot'],
|
|
43
|
+
'session mailbox': ['bots', 'bot', 'session'],
|
|
44
|
+
'session trust-hooks': ['bots', 'bot', 'session'],
|
|
45
|
+
'session answer': ['bots', 'bot', 'session'],
|
|
46
|
+
'temp make': ['bots', 'name'],
|
|
47
|
+
'temp roles': ['bots'],
|
|
48
|
+
'temp retire': ['bots', 'name'],
|
|
49
|
+
'temp trust-hooks': ['bots', 'name'],
|
|
50
|
+
'temp answer': ['bots', 'name'],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The commands every bot is allowed by default, each narrowed to its bots
|
|
55
|
+
* folder (ADR 0036). Only a command named here is in the default set; one that
|
|
56
|
+
* runs whatever its arguments say never is.
|
|
57
|
+
*/
|
|
58
|
+
export const DEFAULT_COMMANDS = [
|
|
59
|
+
'up', 'unpause', 'health', 'groom', 'roster', 'usage',
|
|
60
|
+
'bot change', 'rules build',
|
|
61
|
+
'skills add', 'skills remove', 'skills build', 'skills fetch', 'skills update', 'source add',
|
|
62
|
+
'session add', 'session change', 'session clear', 'session compact',
|
|
63
|
+
'message to', 'message send', 'message check',
|
|
64
|
+
'session record', 'session sent', 'session nudge', 'session mail', 'session name', 'session mailbox',
|
|
65
|
+
'session trust-hooks', 'session answer',
|
|
66
|
+
'temp make', 'temp roles', 'temp retire', 'temp trust-hooks', 'temp answer',
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The commands kept out of the default permission set, which keep the user's
|
|
71
|
+
* yes (ADR 0041): the ones that change permissions, close a long-lived
|
|
72
|
+
* session, make the bots folder, or make a bot, which the owner leaves to Bot
|
|
73
|
+
* Father (#548).
|
|
74
|
+
*/
|
|
75
|
+
export const KEPT_BACK = [
|
|
76
|
+
'init', 'retire', 'pause', 'restart', 'bot create',
|
|
77
|
+
'permission allow', 'permission disallow', 'permission approval',
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The commands kit 0.26.0 gave every bot by default and that are kept back now
|
|
82
|
+
* (#548). A bot may still hold their rules, which the kit names and leaves.
|
|
83
|
+
*/
|
|
84
|
+
export const NO_LONGER_DEFAULT = ['restart', 'bot create'];
|
package/src/health.js
CHANGED
|
@@ -25,7 +25,7 @@ import { bookFile, readBook, sessionIdsIn, tabIdsIn } from './book.js';
|
|
|
25
25
|
import { botDir, botNames, botsDir, readBot, tempRoles, unknownKeys } from './bot.js';
|
|
26
26
|
import { transcriptsIn } from './conversations.js';
|
|
27
27
|
import { hookTrouble } from './hooks.js';
|
|
28
|
-
import { bypassFlags, harnessOf, HARNESSES, isAddressOf, ownCli, sessionTrouble, SHELL_ENV, shellWord } from './launch.js';
|
|
28
|
+
import { APPROVALS, bypassFlags, harnessOf, HARNESSES, isAddressOf, ownCli, sessionTrouble, SHELL_ENV, shellWord } from './launch.js';
|
|
29
29
|
import { frontOfTab, orcaDefaultArgs, projects, runMissing, tabs, wordsOfProcess } from './orca.js';
|
|
30
30
|
import { permissionsTrouble } from './permissions.js';
|
|
31
31
|
import { TAB_ENV } from './record.js';
|
|
@@ -213,10 +213,16 @@ function sessionSettings(home, bot) {
|
|
|
213
213
|
/** What is wrong with the roles the bot's temporary sessions are made in (#465). */
|
|
214
214
|
function rolesTrouble(home, bot) {
|
|
215
215
|
const where = path.join(home, 'bot.yaml');
|
|
216
|
+
// The widest approval they may be made at, the user's yes (ADR 0041).
|
|
217
|
+
const level = bot.temp_approval;
|
|
218
|
+
const approval = level === undefined || APPROVALS.includes(level) ? [] : [{
|
|
219
|
+
where,
|
|
220
|
+
says: `${where} has a temp_approval of ${JSON.stringify(level)}, and the levels are ${APPROVALS.join(', ')}, so obk temp make holds the bot's temporary sessions to their maker's own approval. Set it with obk permission approval --temps.`,
|
|
221
|
+
}];
|
|
216
222
|
try {
|
|
217
|
-
return tempRoles(home, bot).flatMap((role) => role.trouble.map((says) => ({ where, says })));
|
|
223
|
+
return [...tempRoles(home, bot).flatMap((role) => role.trouble.map((says) => ({ where, says }))), ...approval];
|
|
218
224
|
} catch (error) {
|
|
219
|
-
return [{ where, says: error.message }];
|
|
225
|
+
return [{ where, says: error.message }, ...approval];
|
|
220
226
|
}
|
|
221
227
|
}
|
|
222
228
|
|