@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 CHANGED
@@ -128,8 +128,8 @@ obk up --bots /path/to/my-bots
128
128
  that file and the rule units, `CLAUDE.md` as a symlink to it, and a `.gitignore`
129
129
  for `work/`.
130
130
  `session add` writes one session into `bot.yaml`: its harness (the bot's unless
131
- it says otherwise), model, effort, context window, approval level, start prompt,
132
- work dir and any extra arguments for the harness. A start prompt is either text
131
+ it says otherwise), model, effort, context window, start prompt,
132
+ work dir and any extra arguments for the harness, and the approval level. A start prompt is either text
133
133
  (`--prompt`) or, when it is long, a file in the bot home the session points at
134
134
  (`--prompt-file prompts/reviewer.md`), and it reaches the harness as written,
135
135
  apart from two things the kit does on purpose: blank space at either end is
@@ -138,7 +138,18 @@ note about it. Anything you leave out is the
138
138
  harness's own default; the kit names no model of its own. The approval level is
139
139
  `auto` — the harness's real auto mode — unless you ask for `ask` or, in those
140
140
  words, `dangerously-skip`
141
- ([ADR 0015](docs/adr/0015-three-approval-levels-auto-by-default.md)).
141
+ ([ADR 0015](docs/adr/0015-three-approval-levels-auto-by-default.md)). Only
142
+ `obk permission approval` changes it, after your yes: `session add` and
143
+ `session change` take no approval, and refuse an extra argument that sets one
144
+ ([ADR 0037](docs/adr/0037-permission-changes-and-risky-operations-have-commands-of-their-own.md)).
145
+
146
+ Every bot gets the kit's default permission rules, written by the kit with
147
+ nobody asked: its own commands for this bots folder, reading a long message,
148
+ `git add` and `git commit`, and Orca's `orca orchestration check --run`
149
+ ([ADR 0036](docs/adr/0036-the-kit-writes-every-bots-default-permission-rules.md)).
150
+ `obk permission`, `obk retire`, `obk pause` and `obk init` are not in the set,
151
+ and keep your yes. A rule beyond the set is written by `obk permission allow`,
152
+ after your yes to the exact rule.
142
153
 
143
154
  Neither command touches Orca. `obk up` is what makes it all real there: an Orca
144
155
  project per bot, a tab per session, each started at the bot home with its own
@@ -443,6 +454,11 @@ Bot Father does these for you when you ask; these are the commands it runs.
443
454
 
444
455
  ```sh
445
456
  obk bot change --bots /path/to/my-bots --bot api-bot --charter "…"
457
+ obk bot change --bots /path/to/my-bots --bot api-bot --role-cap reviewer=2
458
+ obk permission allow --bots /path/to/my-bots --bot api-bot --rule 'Bash(gh pr merge:*)'
459
+ obk permission disallow --bots /path/to/my-bots --bot api-bot --rule 'Bash(gh pr merge:*)'
460
+ obk permission approval --bots /path/to/my-bots --bot api-bot --session daily --approval ask
461
+ obk permission approval --bots /path/to/my-bots --bot api-bot --temps --approval dangerously-skip
446
462
  obk session change --bots /path/to/my-bots --bot api-bot --session daily --model opus
447
463
  obk skills remove --bots /path/to/my-bots --bot api-bot --skill kit:obk-tdd
448
464
  obk pause --bots /path/to/my-bots --bot api-bot [--session daily]
package/SETUP.md CHANGED
@@ -128,11 +128,12 @@ tab, which is a plain shell for work across the whole fleet.
128
128
  Read everything it prints. Three parts of it matter to them:
129
129
 
130
130
  - **what it created** — say it in a sentence, not as a file listing;
131
- - **the permission rules it says wait for their yes**, when Bot Father runs on
132
- Claude Code: its mail through the kit, reading a long message beside the
133
- bots folder, and its commits, so auto mode does not stop it for those. Show
134
- them the rules word for word and ask. Only on a yes, run the one command
135
- `init` printed for them; on a no, run nothing, and nothing is written;
131
+ - **the permission rules it wrote**: the kit's default set, so auto mode does
132
+ not stop Bot Father for the kit's own commands, reading a long message
133
+ beside the bots folder, and its commits. The kit writes this set for every
134
+ bot, with nobody asked. Say in a sentence what it allows, and that
135
+ `obk permission`, `obk retire`, `obk pause` and `obk init` are not in it and
136
+ keep their yes;
136
137
  - **anything it says about Orca's own default launch arguments.** Orca adds
137
138
  these to every agent it launches, relaunches and resumes. When they carry a
138
139
  permission bypass, every session runs in that mode whatever approval level the
@@ -228,8 +229,8 @@ home. The kit never makes a git worktree, so
228
229
  `2. New worktree` is never the answer.
229
230
 
230
231
  A bot's own temporary sessions are answered by their maker, through the kit, so
231
- that one narrow permission rule allows it rather than a rule to type into any
232
- tab. On Codex's hooks review, the maker runs
232
+ that a narrow permission rule of the kit's default set allows it rather than a
233
+ rule to type into any tab. On Codex's hooks review, the maker runs
233
234
  `obk temp trust-hooks --bots <their path> --name <session>`. On Claude Code's
234
235
  `Teach auto mode` screen, it runs `obk temp answer --bots <their path> --name <session>`.
235
236
  That command reads the screen and answers only when it holds one of the two
@@ -237,17 +238,16 @@ shapes in the table exactly as the kit knows it: Esc for the 2.1.283 form, and
237
238
  **2. Not now** for the 2.1.289 list. On the list it sends the arrows, reads the
238
239
  screen again, and sends the return only with the selection on **2. Not now**.
239
240
  It refuses any other screen, types nothing then, and prints what it saw.
240
- Afterwards it checks that the screen has gone. In
241
- auto mode the maker's bot needs this rule, with the kit's full CLI path for
242
- `<kit>`, as the command's own output prints it:
241
+ Afterwards it checks that the screen has gone. The kit's default set allows
242
+ both commands for every bot, so there is nothing to ask the user. The rules
243
+ are these, with the kit's full CLI path for `<kit>`, as the command's own
244
+ output prints it:
243
245
 
244
246
  ```text
245
- Bash(<kit> temp answer:*)
247
+ Bash(<kit> temp answer --bots <their path>:*)
248
+ Bash(<kit> temp trust-hooks --bots <their path>:*)
246
249
  ```
247
250
 
248
- Ask the user, and only after their yes run
249
- `obk bot change --bots <their path> --bot <bot> --allow 'Bash(<kit> temp answer:*)'`.
250
-
251
251
  A long-lived session's two screens go through the kit as well, not through the
252
252
  keys. On Codex's hooks review, run
253
253
  `obk session trust-hooks --bots <their path> --bot <bot> --session <session>`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assuredloop/orca-bot-kit",
3
- "version": "0.25.6",
3
+ "version": "0.26.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
package/rules/limits.md CHANGED
@@ -17,6 +17,6 @@ publishing, releasing, spending money, anything another person receives.
17
17
  When something you were told to use is missing (a model, a tool, a file), say
18
18
  so and ask. Quietly using a different one hides the change.
19
19
 
20
- Permission rules in a harness's settings or rules files are the kit's to write,
21
- after the user's yes to the exact rules: `"${OBK_CLI:-obk}" bot change --allow <rule>` or
22
- `--disallow <rule>`. Never edit one in or out yourself, Bot Father included.
20
+ Permission rules and approvals are the kit's to write: its default set at once,
21
+ anything else after the user's yes to it, by `"${OBK_CLI:-obk}" permission allow`,
22
+ `disallow` or `approval`. Never edit one in or out yourself, Bot Father included.
@@ -194,7 +194,7 @@ The kit's command line does the writing, and the files belong to the user:
194
194
  source add` records where online skills come from and `obk skills fetch`
195
195
  clones them, `obk skills add` puts one on a bot, `obk skills build` links
196
196
  them, and `obk up` opens what is missing in Orca. `obk bot change` gives a
197
- bot a new charter, `obk session change` changes a session's settings, and
197
+ bot a new charter or a role's cap, `obk session change` changes a session's settings, and
198
198
  `obk skills remove` takes a skill off a list (then `obk skills build`). `obk
199
199
  pause` stops a bot or a session for now and `obk unpause` brings it back;
200
200
  `obk retire` ends one. After `obk up`, `obk restart` or `obk unpause`, read
@@ -206,16 +206,15 @@ Run `obk --help` for the flags of the version actually installed rather than
206
206
  trusting a line you remember.
207
207
 
208
208
  A bot needs a few permission rules so that auto mode, or Codex's sandbox and
209
- reviewer, does not stop it for what the kit's own rules tell it to do: its
210
- mail through the kit, reading a long message kept beside the bots folder, and
211
- its commits. `obk bot create`, `obk up` and `obk rules build` list the rules
212
- still waiting for a bot and give one command that allows them. Show the user
213
- those rules word for word and say in a line what each lets the bot do. Run the
214
- command only after their yes: it records the yes in the bot's `bot.yaml` and
215
- writes the rules into its settings, or for Codex into its
216
- `.codex/rules/obk.rules`. On a no, run nothing, and nothing is written. When
217
- the kit lists new rules for a bot later, ask again. Never write a permission
218
- rule into a settings or rules file by hand.
209
+ reviewer, does not stop it for what the kit's own rules tell it to do: the
210
+ kit's own commands, reading a long message kept beside the bots folder, and
211
+ its commits. The kit writes this default set itself, with nobody asked:
212
+ `obk bot create`, `obk up` and `obk rules build` add each default rule a bot
213
+ does not have yet to its `bot.yaml` and its settings, or for Codex its
214
+ `.codex/rules/obk.rules`, and say which rules they wrote. The set leaves out
215
+ the commands that keep the user's yes: `obk permission`, `obk retire`, `obk
216
+ pause` and `obk init`. Never write a permission rule into a settings or rules
217
+ file by hand.
219
218
 
220
219
  The charter decides what else the bot needs. What it lets the bot do without
221
220
  asking, such as merging pull requests, auto mode or Codex's sandbox can still
@@ -228,14 +227,14 @@ Spell them in Claude Code's form for a Codex bot too, `Bash(<command>:*)`: the
228
227
  kit writes Codex's own form of each, and says which ones Codex has none for,
229
228
  such as an `Edit` rule or one exact command.
230
229
  Show the user the list word for word, with a line on what each lets the bot do,
231
- and only after their yes run `obk bot change --allow`, once per rule they said
232
- yes to. On a no, run nothing. `obk bot change --charter` names the rules the
230
+ and only after their yes run `obk permission allow --rule <rule>`, once per
231
+ rule they said yes to. On a no, run nothing. `obk bot change --charter` names the rules the
233
232
  bot is allowed now; say which of them the new charter no longer grants, show
234
233
  each word for word, and ask whether to take it back. Only after their yes run
235
- `obk bot change --disallow`, once per rule, which takes it out of `bot.yaml`
234
+ `obk permission disallow --rule <rule>`, once per rule, which takes it out of `bot.yaml`
236
235
  and the bot's settings or Codex rules; on a no, run nothing, and it stays
237
236
  allowed. A rule the user added to a settings file by hand is theirs to take
238
- out; the kit refuses it. Keep
237
+ out; the kit refuses it, and it refuses a rule of its own default set too. Keep
239
238
  each rule narrow: a program and what it does. The kit refuses a rule that lets
240
239
  the bot run any command, a program with any arguments, or a shell or
241
240
  interpreter with any arguments; a user who wants one for a bot adds it to that
@@ -331,9 +330,8 @@ into any tab. The same goes for Claude Code's `Teach auto mode` screen on a
331
330
  temporary session you made: answer it with `obk temp answer --bots <bots>
332
331
  --name <session>`. It answers only a shape in the table exactly as the kit
333
332
  knows it (Esc on the 2.1.283 form, 2. Not now on the 2.1.289 list, the return
334
- only after a second look), refuses any other screen, and checks that it went. Its
335
- rule is `Bash(<kit> temp answer:*)`, `<kit>` the kit's CLI path as the
336
- command prints it; ask the user before `obk bot change --allow` adds it.
333
+ only after a second look), refuses any other screen, and checks that it went. The kit's
334
+ default set allows it, as it allows `obk temp trust-hooks`.
337
335
 
338
336
  A long-lived session's two screens go through the kit too: Codex's hooks
339
337
  review with `obk session trust-hooks --bots <bots> --bot <bot> --session
package/src/bot.js CHANGED
@@ -11,7 +11,7 @@ import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSy
11
11
  import os from 'node:os';
12
12
  import path from 'node:path';
13
13
  import { isDeepStrictEqual } from 'node:util';
14
- import { parse, parseDocument, stringify } from 'yaml';
14
+ import { isSeq, parse, parseDocument, stringify } from 'yaml';
15
15
 
16
16
  import { DEFAULT_APPROVAL, HARNESSES, harnessOf, sessionTrouble } from './launch.js';
17
17
 
@@ -161,6 +161,9 @@ export function readBot(home, name = path.basename(home)) {
161
161
  allow: bot.allow,
162
162
  // The roles its temporary sessions can be made in, judged by tempRoles.
163
163
  ...(bot.temp_roles === undefined ? {} : { temp_roles: bot.temp_roles }),
164
+ // The widest approval its temporary sessions may be made at, the user's yes
165
+ // kept by `permission approval --temps` (ADR 0037).
166
+ ...(bot.temp_approval === undefined ? {} : { temp_approval: bot.temp_approval }),
164
167
  // A paused bot is one `obk up` leaves closed; its sessions keep their book.
165
168
  ...(bot.paused === true ? { paused: true } : {}),
166
169
  sessions: sessions.map((session) => {
@@ -278,10 +281,10 @@ export function changeBot(bots, bot, { charter }) {
278
281
  /**
279
282
  * The bot's `allow` list as it is, after refusing what `allowRules` or
280
283
  * `disallowRules` would refuse: an empty rule, or a list already there that is
281
- * not a list of rules. Asked before anything is written, so a refused `bot
282
- * change` changes nothing, its charter included.
284
+ * not a list of rules. Asked before anything is written, so a refused
285
+ * `permission allow` or `permission disallow` changes nothing.
283
286
  */
284
- export function allowedNow(bots, bot, rules, flag = '--allow') {
287
+ export function allowedNow(bots, bot, rules, flag = '--rule') {
285
288
  if (rules.some((rule) => rule.trim() === '')) {
286
289
  throw new Error(`${flag} is empty. Give it the exact permission rule the user said yes to, such as Bash(git add:*).`);
287
290
  }
@@ -320,7 +323,7 @@ export function allowRules(bots, bot, rules, { write = true } = {}) {
320
323
  * false it only refuses what the edit would refuse, and writes nothing.
321
324
  */
322
325
  export function disallowRules(bots, bot, rules, { write = true } = {}) {
323
- const { home, was } = allowedNow(bots, bot, rules, '--disallow');
326
+ const { home, was } = allowedNow(bots, bot, rules);
324
327
  const disallowed = [...new Set(rules)];
325
328
  const allow = was.filter((rule) => !disallowed.includes(rule));
326
329
  editBot(bots, bot, `take back ${disallowed.join(', ')} for ${bot}`, (doc) => doc.set('allow', allow), (before) => ({ ...before, allow }), write);
@@ -357,6 +360,58 @@ export function changeSession(bots, bot, name, settings) {
357
360
  return { bot, home, session };
358
361
  }
359
362
 
363
+ /**
364
+ * Record the widest approval the bot's temporary sessions may be made at,
365
+ * `level`, as `temp_approval` (ADR 0037). Returns { bot, home, level }.
366
+ */
367
+ export function setTempApproval(bots, bot, level) {
368
+ editBot(bots, bot, `let ${bot}'s temporary sessions be made at ${level}`, (doc) => doc.set('temp_approval', level), (was) => ({ ...was, temp_approval: level }));
369
+ return { bot, home: botDir(bots, bot), level };
370
+ }
371
+
372
+ /**
373
+ * Set the caps of roles in the bot's `temp_roles`: `caps` is a list of
374
+ * `[role, cap]`, a cap of undefined taking it off. A role written as a list of
375
+ * options becomes a mapping that holds that list as `options` beside its cap.
376
+ * A role the bot does not have is refused before anything is written. Returns
377
+ * { bot, home, caps }. With `write` false it only refuses what the edit would
378
+ * refuse, and writes nothing.
379
+ */
380
+ export function setRoleCaps(bots, bot, caps, { write = true } = {}) {
381
+ const home = existingBot(bots, bot);
382
+ const roles = readBot(home, bot).temp_roles;
383
+ const file = path.join(home, BOT_YAML);
384
+ for (const [role] of caps) {
385
+ if (roles === null || typeof roles !== 'object' || Array.isArray(roles) || !Object.hasOwn(roles, role)) {
386
+ const has = roles !== null && typeof roles === 'object' && !Array.isArray(roles) ? Object.keys(roles) : [];
387
+ throw new Error(`${bot} has no temporary-session role called ${role} in ${file}${has.length === 0 ? ', and no temp_roles at all' : `. It has: ${has.join(', ')}`}. Nothing was changed.`);
388
+ }
389
+ }
390
+ const capped = (written, cap) => {
391
+ const { cap: was, ...rest } = Array.isArray(written) ? { options: written } : written;
392
+ return cap === undefined ? rest : { ...rest, cap };
393
+ };
394
+ editBot(bots, bot, `set the caps of ${caps.map(([role]) => role).join(', ')}`, (doc) => {
395
+ for (const [role, cap] of caps) {
396
+ const node = doc.getIn(['temp_roles', role], true);
397
+ if (isSeq(node)) {
398
+ doc.setIn(['temp_roles', role], doc.createNode(capped(node.toJSON(), cap)));
399
+ } else if (cap === undefined) {
400
+ doc.deleteIn(['temp_roles', role, 'cap']);
401
+ } else {
402
+ doc.setIn(['temp_roles', role, 'cap'], cap);
403
+ }
404
+ }
405
+ }, (was) => ({
406
+ ...was,
407
+ temp_roles: Object.fromEntries(Object.entries(was.temp_roles).map(([role, written]) => {
408
+ const asked = caps.findLast(([one]) => one === role);
409
+ return [role, asked === undefined ? written : capped(written, asked[1])];
410
+ })),
411
+ }), write);
412
+ return { bot, home, caps };
413
+ }
414
+
360
415
  /**
361
416
  * Mark the bot `bot`, or its session `name`, paused or not. Returns whether
362
417
  * the file changed. A paused bot or session is one `obk up` leaves closed.
@@ -497,7 +552,7 @@ export const SESSION_FIELDS = [
497
552
  ];
498
553
 
499
554
  /** Everything the top of a bot's own file can hold. */
500
- const BOT_FIELDS = ['name', 'harness', 'charter', 'rules', 'skills', 'allow', 'sessions', 'paused', 'temp_roles'];
555
+ const BOT_FIELDS = ['name', 'harness', 'charter', 'rules', 'skills', 'allow', 'sessions', 'paused', 'temp_roles', 'temp_approval'];
501
556
 
502
557
  /**
503
558
  * The keys in a bot's file that the kit does not know, and so that nothing