@assuredloop/orca-bot-kit 0.9.0 → 0.11.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
@@ -543,6 +543,14 @@ runs `obk session mailbox` before the harness. For the same reason a session's
543
543
  mail is read in its own tab. `message check` from any other tab reads nothing,
544
544
  binds nothing, and says so; the mail waits.
545
545
 
546
+ For one session, that step, a `message check`, and `up` writing a new tab for it
547
+ into the book take turns. So two starts of the same session at once leave its
548
+ mailbox bound to the tab the book names, and no mail is read in the other one.
549
+ Each waits a minute at most for its turn. A step that waits it out leaves the
550
+ mailbox where it was and says so, and the harness starts all the same. A check
551
+ holds its turn for 40 seconds at most: with more mail than it reads in that
552
+ time, it shows what it read and says the rest is still waiting.
553
+
546
554
  Nothing in a mailbox wakes anybody, so `message send` also types one line into
547
555
  the receiver's tab telling it to look. Both harnesses take a typed line as the
548
556
  next thing to do rather than cutting into what they are doing, which is the
package/SETUP.md CHANGED
@@ -125,9 +125,14 @@ This makes the folder a git repository, seeds it, writes Bot Father, and opens
125
125
  its two tabs in Orca: the management tab where they will talk to it, and an ops
126
126
  tab, which is a plain shell for work across the whole fleet.
127
127
 
128
- Read everything it prints. Two parts of it matter to them:
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
136
  - **anything it says about Orca's own default launch arguments.** Orca adds
132
137
  these to every agent it launches, relaunches and resumes. When they carry a
133
138
  permission bypass, every session runs in that mode whatever approval level the
@@ -186,19 +191,21 @@ What you will see, and the usual answer. Each goes as one
186
191
  text and **no** `--enter`, because a menu takes a return as the keypress it is
187
192
  waiting for:
188
193
 
189
- | On screen | Send | Which is |
190
- |---|---|---|
191
- | Claude Code's folder trust list | `\x1b[B\r` | down, then return: its selection starts on **No, exit** |
192
- | Codex's directory trust, `1. Trust and continue` (older: `1. Yes, continue`) | `1\r` | yes |
193
- | Codex's `Hooks need review` | `2\r` | trust all and continue |
194
- | Codex's `/new`: `Where should the new conversation run?` | `1\r` | current checkout (bot home) |
195
- | Codex's update offer, `1. Update now` | `1\r` | accept it |
196
- | `[oh-my-zsh] Would you like to update?` | `n` | they update their own shell |
197
- | Claude Code's `Teach auto mode about your environment?` | `2\r` | **Not now**: it would write settings of theirs |
198
-
199
- The numbers there are what these menus showed when this was written. Read them
200
- off the screen in front of you rather than trusting the table: a harness that has
201
- added an option since has moved them all down one.
194
+ | On screen | Send | Which is | Proven on |
195
+ |---|---|---|---|
196
+ | Claude Code's folder trust list | `\x1b[B\r` | down, then return: its selection starts on **No, exit** | Claude Code 2.1.283 |
197
+ | Codex's directory trust, `1. Trust and continue` (older: `1. Yes, continue`) | `\r` | return: its selection starts on **Trust and continue** | Codex 0.157.1 |
198
+ | Codex's `Hooks need review` | `\x1b[B\r` | down, then return, to **Trust all and continue**; if the selection already starts there, return alone | Codex 0.157.1 |
199
+ | Codex's `/new`: `Where should the new conversation run?` | `\r` | return: its selection starts on **Current checkout** (bot home) | Codex 0.157.1 |
200
+ | Codex's update offer, `1. Update now` | `\r` | return: its selection starts on **Update now**, which accepts it | Codex 0.156.1; 0.157.1 has had no newer release to offer |
201
+ | `[oh-my-zsh] Would you like to update?` | `n` | they update their own shell | oh-my-zsh bf77e35 |
202
+ | Claude Code's `Teach auto mode about your environment?` | `\x1b` | Esc, which is **Not now**: it would write settings of theirs | Not yet proven: Esc is Not now in Claude Code 2.1.283's code; last seen live on 2.1.278, answered with 2. Not now |
203
+
204
+ Each key counts on where the selection starts, as it did on the version named.
205
+ Read the screen in front of you before you send: if the selection is somewhere
206
+ else, or the options have moved, go to the answer with the arrows and then
207
+ return. Never send a digit and then return: on Codex's update offer that took
208
+ the highlighted **Update now**, whatever the digit was.
202
209
 
203
210
  Three things to know rather than guess at.
204
211
 
@@ -210,8 +217,7 @@ empty book and nothing says why.
210
217
  Codex's trust question applies to the **repository root**, which for a bot means
211
218
  the whole bots folder rather than the one bot. That is what they are agreeing to.
212
219
 
213
- Codex's `/new` question (seen on 0.156.1; 0.157.1 did not show it in the runs
214
- seen so far) is always answered with the current checkout, which is the bot
220
+ Codex's `/new` question (seen on 0.156.1 and 0.157.1) is always answered with the current checkout, which is the bot
215
221
  home. The kit never makes a git worktree, so
216
222
  `2. New worktree` is never the answer.
217
223
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assuredloop/orca-bot-kit",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
package/rules/limits.md CHANGED
@@ -16,3 +16,7 @@ publishing, releasing, spending money, anything another person receives.
16
16
 
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
+
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 bot change --allow <rule>` records
22
+ and writes them. Never edit them into such a file yourself, Bot Father included.
package/rules/mail.md CHANGED
@@ -6,8 +6,7 @@ applies: all
6
6
 
7
7
  To reach another session, ask the kit for the road rather than guessing an
8
8
  address: `"${OBK_CLI:-obk}" message to --bots <bots> --to <bot>/<session>`. It
9
- answers with the address and the command that carries it. Your bots folder is
10
- two levels above the folder you start in.
9
+ answers with the address and the command that carries it.
11
10
 
12
11
  Mail is queued, not an interruption. Read it when you finish what you are on.
13
12
 
@@ -201,6 +201,39 @@ say that you edited it by hand.
201
201
  Run `obk --help` for the flags of the version actually installed rather than
202
202
  trusting a line you remember.
203
203
 
204
+ A bot needs a few permission rules so that auto mode, or Codex's sandbox and
205
+ reviewer, does not stop it for what the kit's own rules tell it to do: its
206
+ mail through the kit, reading a long message kept beside the bots folder, and
207
+ its commits. `obk bot create`, `obk up` and `obk rules build` list the rules
208
+ still waiting for a bot and give one command that allows them. Show the user
209
+ those rules word for word and say in a line what each lets the bot do. Run the
210
+ command only after their yes: it records the yes in the bot's `bot.yaml` and
211
+ writes the rules into its settings, or for Codex into its
212
+ `.codex/rules/obk.rules`. On a no, run nothing, and nothing is written. When
213
+ the kit lists new rules for a bot later, ask again. Never write a permission
214
+ rule into a settings or rules file by hand.
215
+
216
+ The charter decides what else the bot needs. What it lets the bot do without
217
+ asking, such as merging pull requests, auto mode or Codex's sandbox can still
218
+ stop, so each grant needs its own rule. When the charter is written, and again
219
+ whenever it changes, list the exact rules it grants: `Bash(gh pr merge:*)` for
220
+ a bot that merges pull requests, `Bash(gh issue close:*)` for one that closes
221
+ issues. Spell each command as the bot will run it, the kit by the path the
222
+ bot's rules give it and every folder absolute, or the rule will not match.
223
+ Spell them in Claude Code's form for a Codex bot too, `Bash(<command>:*)`: the
224
+ kit writes Codex's own form of each, and says which ones Codex has none for,
225
+ such as an `Edit` rule or one exact command.
226
+ Show the user the list word for word, with a line on what each lets the bot do,
227
+ and only after their yes run `obk bot change --allow`, once per rule they said
228
+ yes to. On a no, run nothing. `obk bot change --charter` names the rules the
229
+ bot is allowed now; say which of them the new charter no longer grants, and
230
+ that they stay allowed until the user takes them out, since the kit takes none
231
+ out. Keep
232
+ each rule narrow: a program and what it does. The kit refuses a rule that lets
233
+ the bot run any command, a program with any arguments, or a shell or
234
+ interpreter with any arguments; a user who wants one for a bot adds it to that
235
+ bot's settings themselves. Propose no rule for what the charter does not grant.
236
+
204
237
  A bot that should learn across its sessions can use its harness's own memory,
205
238
  turned on for that bot alone, whatever the user's own setting is for everything
206
239
  else. The harness writes it, loads it and keeps it tidy itself. On Claude Code
@@ -245,23 +278,31 @@ otherwise `/Applications/Orca.app/Contents/Resources/bin/orca`. A bare
245
278
  `orca` can fail. Send each answer as one `<orca> terminal send --terminal
246
279
  <handle> --text …`, with the return inside the text and no `--enter`:
247
280
 
248
- | On screen | Send | Which is |
249
- |---|---|---|
250
- | Claude Code's folder trust list | `\x1b[B\r` | down, return: it starts on **No, exit** |
251
- | Codex's `Trust this folder?`, `1. Trust and continue` | `1\r` | yes (older: `1. Yes, continue`) |
252
- | Codex's `Hooks need review` | `2\r` | trust all and continue |
253
- | Codex's `/new`: `Where should the new conversation run?` | `1\r` | current checkout (bot home) |
254
- | Codex's update offer, `1. Update now` | `1\r` | accept it |
255
- | `[oh-my-zsh] Would you like to update?` | `n` | no: the shell is the user's to update |
256
- | Claude Code's `Teach auto mode about…` | `2\r` | **Not now**: it writes the user's settings |
257
-
258
- Read the numbers off the screen in front of you rather than trusting the
259
- table: a harness that has added an option has moved them. Codex's hooks
281
+ | On screen | Send | Which is | Proven on |
282
+ |---|---|---|---|
283
+ | Claude Code's trust list | `\x1b[B\r` | down, return: off **No, exit** | Claude Code 2.1.283 |
284
+ | Codex's `Trust this folder?` | `\r` | return: it starts on yes | Codex 0.157.1 |
285
+ | Codex's `Hooks need review` | `\x1b[B\r` | **Trust all and continue** | Codex 0.157.1 |
286
+ | Codex's `Where should the new conversation run?` | `\r` | **Current checkout** | Codex 0.157.1 |
287
+ | Codex's update offer, `1. Update now` | `\r` | **Update now**, where it starts | Codex 0.156.1 |
288
+ | `[oh-my-zsh] Would you like to update?` | `n` | no: the user updates it | oh-my-zsh bf77e35 |
289
+ | Claude Code's `Teach auto mode…` | `\x1b` | Esc, **Not now** | Not yet proven, below |
290
+
291
+ Each key counts on where the selection starts on the version named. Read the
292
+ screen before you send: if the selection is elsewhere or the options have
293
+ moved, go to the answer with the arrows, then return. Never a digit and then
294
+ return: on Codex's update offer that took the highlighted **Update now**,
295
+ whatever the digit was. Codex's trust said `1. Yes, continue` on older
296
+ versions. Its hooks review sometimes starts on `2` already: then return alone.
297
+ 0.157.1 has had no newer release to offer, so its update offer was proven on
298
+ 0.156.1. Claude Code's `Teach auto mode` has not been seen live on 2.1.283:
299
+ Esc is Not now in its code, and 2.1.278 was answered with `2. Not now`.
300
+ Not now, because Yes writes the user's settings. Codex's hooks
260
301
  question matters most. The kit's hook is how the book learns which
261
302
  conversation the session is running, and until it is answered the
262
303
  conversation has not started. Codex's trust applies to the repository root,
263
304
  which is the whole bots folder. Codex's `/new` may ask where the new
264
- conversation runs (0.156.1 did; 0.157.1 has not so far); the answer is
305
+ conversation runs (0.156.1 and 0.157.1 both did); the answer is
265
306
  always the current checkout, the bot home, and never `2. New worktree`:
266
307
  the kit never makes a git worktree.
267
308
 
package/src/book.js CHANGED
@@ -6,7 +6,7 @@
6
6
  // theirs. Beside each session's tab it holds the harness session id that
7
7
  // session is running under, and every id it ran under before.
8
8
 
9
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
9
+ import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from 'node:fs';
10
10
  import path from 'node:path';
11
11
  import { DatabaseSync } from 'node:sqlite';
12
12
  import { isDeepStrictEqual } from 'node:util';
@@ -141,18 +141,26 @@ export async function updateBook(home, change) {
141
141
  * empty and leaves no journal beside it.
142
142
  */
143
143
  function takeLock(home) {
144
- const file = lockFile(home);
144
+ try {
145
+ return lockOn(lockFile(home), WAIT_MS);
146
+ } catch (error) {
147
+ throw waitedTooLong(home, error);
148
+ }
149
+ }
150
+
151
+ /** The lock on `file`, waited for `waitMs` at most: see `takeLock` for why it is this one. */
152
+ function lockOn(file, waitMs) {
145
153
  mkdirSync(path.dirname(file), { recursive: true });
146
154
 
147
155
  const db = new DatabaseSync(file);
148
156
  try {
149
157
  // A writer that arrives while another is working waits for it rather than
150
158
  // failing at once, and gives up saying so rather than waiting for ever.
151
- db.exec(`PRAGMA busy_timeout = ${WAIT_MS}`);
159
+ db.exec(`PRAGMA busy_timeout = ${waitMs}`);
152
160
  db.exec('BEGIN IMMEDIATE');
153
161
  } catch (error) {
154
162
  db.close();
155
- throw waitedTooLong(home, error);
163
+ throw error;
156
164
  }
157
165
 
158
166
  return {
@@ -190,6 +198,54 @@ const lockFile = (home) => path.join(
190
198
  `${encodeURIComponent(path.basename(home))}.lock`,
191
199
  );
192
200
 
201
+ /**
202
+ * One session's turn at its mailbox (#321): held by `session mailbox` for its
203
+ * whole step, by `message check` in the session's own tab for its bind, read and
204
+ * ack, and by `up` while it writes a new tab for the session into the book. So
205
+ * the book never moves to another tab while a step or a check is at work, and a
206
+ * step or check that waited reads the book again once it has its turn: two
207
+ * starts of one session at once leave its mailbox bound to the tab the book
208
+ * names, and nothing is read from the one it no longer does.
209
+ *
210
+ * A lock of its own, and not the book's: the book's is held for one read and one
211
+ * write, and a session's hook must never wait behind Orca for it. Taken before
212
+ * the book's and never while holding it, so neither waits on the other.
213
+ *
214
+ * Returns `{ release }`, or undefined when the turn did not come in
215
+ * `MAILBOX_WAIT_MS`: the caller says what it left, and a launch line goes on
216
+ * to its harness whatever happened here.
217
+ */
218
+ export function takeMailboxTurn(home, session) {
219
+ try {
220
+ return lockOn(mailboxLockFile(home, session), MAILBOX_WAIT_MS);
221
+ } catch (error) {
222
+ if (error.errcode === SQLITE_BUSY) return undefined;
223
+ throw error;
224
+ }
225
+ }
226
+
227
+ /**
228
+ * How long a step, a check or an `up` waits for a session's turn. Longer than a
229
+ * step can hold it — three Orca calls of twenty seconds each at most — and short
230
+ * enough that a harness behind a stuck one still starts.
231
+ */
232
+ export const MAILBOX_WAIT_MS = 60_000;
233
+
234
+ /** SQLite's own code for a lock another connection holds. */
235
+ const SQLITE_BUSY = 5;
236
+
237
+ /**
238
+ * Beside the book's lock, one file per session. Names are lower-case letters,
239
+ * digits and hyphens, so a dot between the bot's and the session's cannot be
240
+ * read two ways. Resolved first, because the same bot is reached through a
241
+ * link by one command and by its real path by another, and two paths would be
242
+ * two turns.
243
+ */
244
+ const mailboxLockFile = (home, session) => {
245
+ const real = realpathSync(home);
246
+ return path.join(`${path.dirname(path.dirname(real))}.locks`, `${path.basename(real)}.${session}.mailbox.lock`);
247
+ };
248
+
193
249
  /**
194
250
  * Something else has been writing the book for longer than this run is prepared
195
251
  * to wait. Every writer holds its turn for one read and one write, so this is a
package/src/bot.js CHANGED
@@ -136,6 +136,9 @@ export function readBot(home, name = path.basename(home)) {
136
136
  // and a list the build cannot follow is that build's to report (PRD 6.6).
137
137
  charter: bot.charter,
138
138
  rules: bot.rules,
139
+ // The permission rules the user said yes to, judged where they are written
140
+ // (src/permissions.js), as `rules` is judged by the build.
141
+ allow: bot.allow,
139
142
  // A paused bot is one `obk up` leaves closed; its sessions keep their book.
140
143
  ...(bot.paused === true ? { paused: true } : {}),
141
144
  sessions: sessions.map((session) => {
@@ -250,6 +253,42 @@ export function changeBot(bots, bot, { charter }) {
250
253
  return { bot, home: botDir(bots, bot), charter: text };
251
254
  }
252
255
 
256
+ /**
257
+ * The bot's `allow` list as it is, after refusing what `allowRules` would refuse:
258
+ * an empty rule, or a list already there that is not a list of rules. Asked
259
+ * before anything is written, so a refused `bot change` changes nothing, its
260
+ * charter included.
261
+ */
262
+ export function allowedNow(bots, bot, rules) {
263
+ if (rules.some((rule) => rule.trim() === '')) {
264
+ throw new Error('--allow is empty. Give it the exact permission rule the user said yes to, such as Bash(git add:*).');
265
+ }
266
+ const home = existingBot(bots, bot);
267
+ const was = readBot(home, bot).allow ?? [];
268
+ if (!Array.isArray(was) || was.some((rule) => typeof rule !== 'string' || rule.trim() === '')) {
269
+ throw new Error(`the allow entry in ${path.join(home, BOT_YAML)} is not a list of permission rules, so nothing was written. Fix it, then run the command again.`);
270
+ }
271
+ return { home, was };
272
+ }
273
+
274
+ /**
275
+ * Add the permission rules the user said yes to, `rules`, to the bot's `allow`,
276
+ * after the ones already there and never twice. Returns { bot, home, allow,
277
+ * added }: the whole list now, and what this call put in it.
278
+ *
279
+ * Only the list: the rules are written into the harness's settings by the
280
+ * caller, as the rules build is run by the caller of `changeBot`.
281
+ */
282
+ export function allowRules(bots, bot, rules) {
283
+ const { home, was } = allowedNow(bots, bot, rules);
284
+ const added = [...new Set(rules)].filter((rule) => !was.includes(rule));
285
+ const allow = [...was, ...added];
286
+ if (added.length > 0) {
287
+ editBot(bots, bot, `allow ${added.join(', ')} for ${bot}`, (doc) => doc.set('allow', allow), (before) => ({ ...before, allow }));
288
+ }
289
+ return { bot, home, allow, added };
290
+ }
291
+
253
292
  /**
254
293
  * Change the settings of the session `name` of the bot `bot`. Returns
255
294
  * { bot, home, session } — the session as it now reads.
@@ -414,7 +453,7 @@ export const SESSION_FIELDS = [
414
453
  ];
415
454
 
416
455
  /** Everything the top of a bot's own file can hold. */
417
- const BOT_FIELDS = ['name', 'harness', 'charter', 'rules', 'skills', 'sessions', 'paused'];
456
+ const BOT_FIELDS = ['name', 'harness', 'charter', 'rules', 'skills', 'allow', 'sessions', 'paused'];
418
457
 
419
458
  /**
420
459
  * The keys in a bot's file that the kit does not know, and so that nothing