@assuredloop/orca-bot-kit 0.25.4 → 0.25.6
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 +8 -7
- package/SETUP.md +20 -0
- package/package.json +4 -3
- package/rules/mail.md +4 -0
- package/skills/obk-bot-building/SKILL.md +13 -0
- package/src/book.js +3 -0
- package/src/cli.js +44 -3
- package/src/hook-trust.js +179 -0
- package/src/hooks.js +11 -0
- package/src/orca.js +15 -1
- package/src/temp.js +134 -29
- package/src/up.js +4 -1
package/README.md
CHANGED
|
@@ -643,13 +643,14 @@ npm run mutate # the mutation audit, on what your branch changed
|
|
|
643
643
|
|
|
644
644
|
The tests come in two layers, and every test file belongs to one of them.
|
|
645
645
|
|
|
646
|
-
`npm test` is `test/*.test.js`: unit tests and
|
|
647
|
-
throwaway folder, with a fake `orca` on PATH. It
|
|
648
|
-
[GitHub Actions](.github/workflows/ci.yml) runs it on
|
|
649
|
-
into shards that run side by side, on Node 25.8.0, the
|
|
650
|
-
runs every shard on 25.8.0 and again on 24.21.0, the
|
|
651
|
-
promises users, with only the kit's own dependencies
|
|
652
|
-
is published.
|
|
646
|
+
`npm test` is `test/*.test.js` and `test/dev/*.test.js`: unit tests and
|
|
647
|
+
end-to-end runs of the CLI in a throwaway folder, with a fake `orca` on PATH. It
|
|
648
|
+
needs nothing but Node, so [GitHub Actions](.github/workflows/ci.yml) runs it on
|
|
649
|
+
every pull request, split into shards that run side by side, on Node 25.8.0, the
|
|
650
|
+
current line. A release runs every shard on 25.8.0 and again on 24.21.0, the
|
|
651
|
+
floor `engines.node` promises users, with only the kit's own dependencies
|
|
652
|
+
installed, before anything is published. The floor leaves out `test/dev/`, the
|
|
653
|
+
tests that need a development tool.
|
|
653
654
|
|
|
654
655
|
`npm run test:system` is `test/system/*.test.js`: the real `obk` against the
|
|
655
656
|
real Orca and the real harnesses on your own machine. No CI runner can do that,
|
package/SETUP.md
CHANGED
|
@@ -248,6 +248,26 @@ Bash(<kit> temp answer:*)
|
|
|
248
248
|
Ask the user, and only after their yes run
|
|
249
249
|
`obk bot change --bots <their path> --bot <bot> --allow 'Bash(<kit> temp answer:*)'`.
|
|
250
250
|
|
|
251
|
+
A long-lived session's two screens go through the kit as well, not through the
|
|
252
|
+
keys. On Codex's hooks review, run
|
|
253
|
+
`obk session trust-hooks --bots <their path> --bot <bot> --session <session>`.
|
|
254
|
+
On Claude Code's `Teach auto mode` screen, run
|
|
255
|
+
`obk session answer --bots <their path> --bot <bot> --session <session>`.
|
|
256
|
+
A long-lived session is Bot Father's, so run them from outside the fleet's tabs,
|
|
257
|
+
as you do here, or from a Bot Father session. A session of any other bot is
|
|
258
|
+
refused. They make the same checks as the temp commands. Before either
|
|
259
|
+
trust-hooks command trusts hooks, the kit checks that the hooks on the review
|
|
260
|
+
are its own: the bot's `.codex/hooks.json` holds only the kit's hooks, and the
|
|
261
|
+
count on the screen is the number of the kit's hooks that Codex does not trust
|
|
262
|
+
yet. When it refuses, it says what it saw: type nothing, and tell them what it
|
|
263
|
+
said. Use the keys in the table for the screens the kit does not answer.
|
|
264
|
+
The kit reads Codex's trust from the caller's `CODEX_HOME`, else `~/.codex`;
|
|
265
|
+
a bot run under another `CODEX_HOME` is outside what the kit can see.
|
|
266
|
+
The kit also records the extra arguments it launched each session with, and
|
|
267
|
+
refuses when they mention hooks, or when there is no record. A Codex that
|
|
268
|
+
someone started again by hand in that tab, with other flags, is outside what
|
|
269
|
+
the kit records.
|
|
270
|
+
|
|
251
271
|
**Anything you do not recognise: type nothing.** Tell them what is on the
|
|
252
272
|
screen and which tab it is in, and wait. A keypress into a menu you have not read
|
|
253
273
|
is how a harness quits back to the shell.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assuredloop/orca-bot-kit",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.6",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -23,8 +23,9 @@
|
|
|
23
23
|
"SETUP.md"
|
|
24
24
|
],
|
|
25
25
|
"scripts": {
|
|
26
|
-
"test": "node --test test/*.test.js",
|
|
27
|
-
"test:shard": "node --test --test-shard=$SHARD test/*.test.js",
|
|
26
|
+
"test": "node --test test/*.test.js test/dev/*.test.js",
|
|
27
|
+
"test:shard": "node --test --test-shard=$SHARD test/*.test.js test/dev/*.test.js",
|
|
28
|
+
"test:floor": "node --test --test-shard=$SHARD test/*.test.js",
|
|
28
29
|
"test:system": "node scripts/test-system.js",
|
|
29
30
|
"mutate": "node scripts/mutate.js"
|
|
30
31
|
},
|
package/rules/mail.md
CHANGED
|
@@ -8,6 +8,10 @@ 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
9
|
answers with the address and the command that carries it.
|
|
10
10
|
|
|
11
|
+
If your harness refuses a message, do not send it again by another road. Tell
|
|
12
|
+
your maker or your user, with the harness's reason. A refusal can mean a missing
|
|
13
|
+
permission rule, which they can add through the kit after the user's yes.
|
|
14
|
+
|
|
11
15
|
Mail is queued, not an interruption. Read it when you finish what you are on.
|
|
12
16
|
|
|
13
17
|
Ask for a reply when you need one, and send one when you were asked for one.
|
|
@@ -335,6 +335,19 @@ only after a second look), refuses any other screen, and checks that it went. It
|
|
|
335
335
|
rule is `Bash(<kit> temp answer:*)`, `<kit>` the kit's CLI path as the
|
|
336
336
|
command prints it; ask the user before `obk bot change --allow` adds it.
|
|
337
337
|
|
|
338
|
+
A long-lived session's two screens go through the kit too: Codex's hooks
|
|
339
|
+
review with `obk session trust-hooks --bots <bots> --bot <bot> --session
|
|
340
|
+
<session>`, and Claude Code's `Teach auto mode` screen with `obk session
|
|
341
|
+
answer --bots <bots> --bot <bot> --session <session>`. A long-lived session is
|
|
342
|
+
Bot Father's: the caller is a Bot Father session or the user, from outside the
|
|
343
|
+
fleet's tabs, and a session of any other bot is refused. They make the same
|
|
344
|
+
checks as the temp commands. Before either trust-hooks command trusts hooks,
|
|
345
|
+
it checks that the hooks on the review are the kit's own: the bot's
|
|
346
|
+
`.codex/hooks.json` holds only the kit's, and the count on the screen is the
|
|
347
|
+
number of the kit's hooks Codex does not trust yet. A refusal says what it
|
|
348
|
+
saw; then type nothing and take it to the user. The keys in the table are for
|
|
349
|
+
the screens the kit does not answer.
|
|
350
|
+
|
|
338
351
|
Where the kit says no session came up, the shell swallowed the launch line,
|
|
339
352
|
usually while it was asking its own question. Answer the shell, then close
|
|
340
353
|
that one tab (`<orca> terminal close --terminal <handle> --tab`) and run
|
package/src/book.js
CHANGED
|
@@ -28,6 +28,9 @@ const HEADER = `# What Orca calls this bot on this machine, and where each of it
|
|
|
28
28
|
# \`rules\` is a stamp of the bot's AGENTS.md as the session last read it, so a
|
|
29
29
|
# change to it after the session started can be told apart.
|
|
30
30
|
#
|
|
31
|
+
# \`launched_with\` is the extra arguments the kit typed on the session's last
|
|
32
|
+
# launch line, as bot.yaml's extra_args gave them then.
|
|
33
|
+
#
|
|
31
34
|
# \`retired\` holds what the book knew about each session \`obk retire\` took off
|
|
32
35
|
# the bot, with when, so the conversations it had are still accounted for.
|
|
33
36
|
#
|
package/src/cli.js
CHANGED
|
@@ -31,7 +31,7 @@ import { readRoster } from './roster.js';
|
|
|
31
31
|
import { buildAgents, buildRules, CODEX_CAP } from './rules.js';
|
|
32
32
|
import { addSkill, buildSkills, linkSkills, removeSkill } from './skills.js';
|
|
33
33
|
import { addSource, fetchSources } from './sources.js';
|
|
34
|
-
import { answerTemp, listRoles, makeTemp, optionsLine, retireTemp, trustHooks } from './temp.js';
|
|
34
|
+
import { answerSession, answerTemp, listRoles, makeTemp, optionsLine, retireTemp, trustHooks, trustSessionHooks } from './temp.js';
|
|
35
35
|
import { readUsage } from './usage.js';
|
|
36
36
|
import { BOT_FATHER, bringUp, ownMailbox } from './up.js';
|
|
37
37
|
|
|
@@ -139,7 +139,8 @@ Usage:
|
|
|
139
139
|
of a Codex temporary session it made with "Trust
|
|
140
140
|
all and continue", and check the review went. It
|
|
141
141
|
refuses anything else on that screen, and types
|
|
142
|
-
nothing then.
|
|
142
|
+
nothing then. It refuses, as obk session
|
|
143
|
+
trust-hooks does, hooks that are not the kit's.
|
|
143
144
|
obk temp answer --bots <path> --name <session>
|
|
144
145
|
Run in the maker's own tab: answer Claude Code's
|
|
145
146
|
"Teach auto mode" screen on a temporary session it
|
|
@@ -266,6 +267,19 @@ Usage:
|
|
|
266
267
|
next starts where it stopped, so no call is counted
|
|
267
268
|
twice. It counts tokens and never money: what a
|
|
268
269
|
token costs is looked up live by whoever is asking.
|
|
270
|
+
obk session trust-hooks --bots <path> --bot <bot> --session <name>
|
|
271
|
+
Run by Bot Father or the user: answer the hooks
|
|
272
|
+
review of a long-lived Codex session with "Trust
|
|
273
|
+
all and continue", as obk temp trust-hooks does,
|
|
274
|
+
and check the review went. It refuses when the
|
|
275
|
+
bot's .codex/hooks.json holds a hook that is not
|
|
276
|
+
the kit's, or when the review counts other hooks
|
|
277
|
+
than the kit's own that Codex does not trust yet.
|
|
278
|
+
obk session answer --bots <path> --bot <bot> --session <name>
|
|
279
|
+
Run by Bot Father or the user: answer Claude Code's
|
|
280
|
+
"Teach auto mode" screen on a long-lived session
|
|
281
|
+
with Not now, as obk temp answer does, and check
|
|
282
|
+
the screen went.
|
|
269
283
|
obk session record --bots <path> --bot <bot>
|
|
270
284
|
For the kit's own hook, not for typing: it reads
|
|
271
285
|
what the harness says about a session starting on
|
|
@@ -337,6 +351,8 @@ const COMMANDS = {
|
|
|
337
351
|
'session mail': ['bots', 'bot'],
|
|
338
352
|
'session name': ['bots', 'bot'],
|
|
339
353
|
'session mailbox': ['bots', 'bot', 'session'],
|
|
354
|
+
'session trust-hooks': ['bots', 'bot', 'session'],
|
|
355
|
+
'session answer': ['bots', 'bot', 'session'],
|
|
340
356
|
'temp make': ['bots', 'name'],
|
|
341
357
|
'temp roles': ['bots'],
|
|
342
358
|
'temp retire': ['bots', 'name'],
|
|
@@ -845,6 +861,26 @@ const commands = {
|
|
|
845
861
|
};
|
|
846
862
|
},
|
|
847
863
|
|
|
864
|
+
async 'session trust-hooks'(bots, values) {
|
|
865
|
+
const tab = callerTab(bots, true);
|
|
866
|
+
refuseWhenOrcaIsDown();
|
|
867
|
+
const trusted = await trustSessionHooks(bots, { tab, bot: values.bot, session: values.session });
|
|
868
|
+
return {
|
|
869
|
+
answer: { bots, ...trusted },
|
|
870
|
+
lines: [`trusted ${trusted.bot} ${trusted.session}'s hooks: chose "Trust all and continue" on its hooks review, and the review has gone`],
|
|
871
|
+
};
|
|
872
|
+
},
|
|
873
|
+
|
|
874
|
+
async 'session answer'(bots, values) {
|
|
875
|
+
const tab = callerTab(bots, true);
|
|
876
|
+
refuseWhenOrcaIsDown();
|
|
877
|
+
const answered = await answerSession(bots, { tab, bot: values.bot, session: values.session });
|
|
878
|
+
return {
|
|
879
|
+
answer: { bots, ...answered },
|
|
880
|
+
lines: [`answered ${answered.bot} ${answered.session}: sent ${answered.sent} to its "Teach auto mode about your environment?" screen, and the screen has gone`],
|
|
881
|
+
};
|
|
882
|
+
},
|
|
883
|
+
|
|
848
884
|
'bot create'(bots, values) {
|
|
849
885
|
const made = createBot(bots, { name: values.name, harness: values.harness, charter: values.charter });
|
|
850
886
|
// The bot's AGENTS.md is the rules build's, here as everywhere else, so
|
|
@@ -1262,7 +1298,12 @@ const commands = {
|
|
|
1262
1298
|
function toLines(answer, bots, where) {
|
|
1263
1299
|
if (answer.trouble !== undefined) return [` ${answer.trouble}`];
|
|
1264
1300
|
if (answer.transport === 'native') {
|
|
1265
|
-
|
|
1301
|
+
// A refused call is not re-sent by another road (PRD 6.9), so the answer
|
|
1302
|
+
// says what to do instead, before the bot needs it (#521).
|
|
1303
|
+
return [
|
|
1304
|
+
` Write to ${answer.address} with your own harness's messaging. The kit does not carry that road.`,
|
|
1305
|
+
" 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.",
|
|
1306
|
+
];
|
|
1266
1307
|
}
|
|
1267
1308
|
return [
|
|
1268
1309
|
...(answer.unnamed === true
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// Which of a Codex bot's hooks Codex does not trust yet, so that the kit
|
|
2
|
+
// trusts only its own (#506).
|
|
3
|
+
//
|
|
4
|
+
// "Trust all and continue" on Codex's hooks review trusts every hook the
|
|
5
|
+
// review counts, from every source Codex reads: the user's own files, Orca's
|
|
6
|
+
// hooks among them, `-c` flags, plugins, and each `.codex/` folder from the git
|
|
7
|
+
// root down (tech notes, section 3). So the kit answers a review only when the
|
|
8
|
+
// bot's `.codex/hooks.json` holds the kit's hooks and nothing else, and the
|
|
9
|
+
// count on the screen is the number of those that Codex does not trust yet.
|
|
10
|
+
//
|
|
11
|
+
// Codex keeps its trust in the user's config.toml, one table a hook, keyed by
|
|
12
|
+
// the file, the event and the hook's place in it, with a hash of the hook. The
|
|
13
|
+
// kit reads only those tables' headers and their `trusted_hash` lines: the
|
|
14
|
+
// rest of the file can hold the user's secrets, and nothing of it is kept or
|
|
15
|
+
// said. A file it cannot read that way for one of its hooks is one where it
|
|
16
|
+
// cannot tell, and it refuses (the architect's ruling on #506).
|
|
17
|
+
|
|
18
|
+
import { createHash } from 'node:crypto';
|
|
19
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
20
|
+
import os from 'node:os';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
|
|
23
|
+
import { hooksFileOf, readSettings, runsThisKit } from './hooks.js';
|
|
24
|
+
|
|
25
|
+
/** The events whose hooks Codex hashes with no matcher, whatever the group says. */
|
|
26
|
+
const NO_MATCHER = new Set(['user_prompt_submit', 'stop', 'interrupt']);
|
|
27
|
+
|
|
28
|
+
/** A hook's timeout when its entry gives none, in seconds, as Codex takes it. */
|
|
29
|
+
const DEFAULT_TIMEOUT = 600;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* How many of the hooks in the bot at `home`'s `.codex/hooks.json` Codex does
|
|
33
|
+
* not trust yet: `{ untrusted, file }`. It throws, with `nothing` at the end,
|
|
34
|
+
* when the file holds a hook that is not the kit's own as this kit writes it,
|
|
35
|
+
* or when the kit cannot tell whether Codex trusts one.
|
|
36
|
+
*/
|
|
37
|
+
export function untrustedKitHooks(home, run, nothing) {
|
|
38
|
+
const file = hooksFileOf(home, 'codex');
|
|
39
|
+
const settings = readSettings(file, "the kit's hooks");
|
|
40
|
+
const hooks = settings.hooks ?? {};
|
|
41
|
+
if (hooks === null || typeof hooks !== 'object' || Array.isArray(hooks)) {
|
|
42
|
+
throw new Error(`${run}: ${file} has a hooks entry that is not a mapping of events, so the kit cannot tell which hooks its review covers. ${nothing}`);
|
|
43
|
+
}
|
|
44
|
+
const ours = [];
|
|
45
|
+
const foreign = [];
|
|
46
|
+
for (const [event, groups] of Object.entries(hooks)) {
|
|
47
|
+
if (!Array.isArray(groups)) {
|
|
48
|
+
foreign.push(`${event}: ${JSON.stringify(groups)}`);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
groups.forEach((group, at) => {
|
|
52
|
+
if (!Array.isArray(group?.hooks)) {
|
|
53
|
+
foreign.push(`${event}: ${JSON.stringify(group)}`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
group.hooks.forEach((hook, handler) => {
|
|
57
|
+
if (runsThisKit(hook)) ours.push({ event: snake(event), at, handler, hash: hashOf(snake(event), group.matcher, hook) });
|
|
58
|
+
else foreign.push(`${event}: ${typeof hook?.command === 'string' ? hook.command : JSON.stringify(hook)}`);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (foreign.length > 0) {
|
|
63
|
+
throw new Error(`${run}: ${file} holds hooks that are not the kit's own as this kit writes them, and "Trust all and continue" would let them run outside the sandbox: ${foreign.join('; ')}. ${nothing}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Codex names the file by the path it found it at; the kit knows the bot
|
|
67
|
+
// home by its real path, and looks under both.
|
|
68
|
+
const files = [...new Set([file, realOr(file)])];
|
|
69
|
+
const keysOf = (hook) => files.map((one) => `${one}:${hook.event}:${hook.at}:${hook.handler}`);
|
|
70
|
+
const trusted = trustedHashes(ours.flatMap(keysOf), run, nothing);
|
|
71
|
+
const untrusted = ours.filter((hook) => !keysOf(hook).some((key) => trusted.get(key) === hook.hash)).length;
|
|
72
|
+
return { untrusted, file };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** An event's name as Codex writes it in a key and a hash: `SessionStart` is `session_start`. */
|
|
76
|
+
const snake = (event) => event.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The hash Codex trusts a command hook at: SHA-256, in hex, of its event, its
|
|
80
|
+
* group's matcher and the hook as Codex reads it, as JSON with every key
|
|
81
|
+
* sorted (Codex 0.162.0, tech notes, section 3).
|
|
82
|
+
*/
|
|
83
|
+
function hashOf(event, matcher, hook) {
|
|
84
|
+
const handler = {
|
|
85
|
+
type: 'command',
|
|
86
|
+
command: hook.command,
|
|
87
|
+
timeout: hook.timeout ?? DEFAULT_TIMEOUT,
|
|
88
|
+
async: hook.async === true,
|
|
89
|
+
...(hook.statusMessage === undefined ? {} : { statusMessage: hook.statusMessage }),
|
|
90
|
+
};
|
|
91
|
+
const identity = {
|
|
92
|
+
event_name: event,
|
|
93
|
+
...(matcher === undefined || matcher === null || NO_MATCHER.has(event) ? {} : { matcher }),
|
|
94
|
+
hooks: [handler],
|
|
95
|
+
};
|
|
96
|
+
return `sha256:${createHash('sha256').update(sorted(identity)).digest('hex')}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** A value as JSON with every object's keys sorted, and no spaces. */
|
|
100
|
+
const sorted = (value) => {
|
|
101
|
+
if (Array.isArray(value)) return `[${value.map(sorted).join(',')}]`;
|
|
102
|
+
if (value !== null && typeof value === 'object') {
|
|
103
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${sorted(value[key])}`).join(',')}}`;
|
|
104
|
+
}
|
|
105
|
+
return JSON.stringify(value);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const realOr = (file) => {
|
|
109
|
+
try {
|
|
110
|
+
return realpathSync(file);
|
|
111
|
+
} catch {
|
|
112
|
+
return file;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const HEADER = /^\s*\[\s*hooks\.state\."((?:[^"\\]|\\.)*)"\s*\]\s*(?:#.*)?$/;
|
|
117
|
+
const TRUSTED = /^\s*trusted_hash\s*=\s*"(sha256:[0-9a-f]{64})"\s*(?:#.*)?$/;
|
|
118
|
+
const ANY_TABLE = /^\s*\[/;
|
|
119
|
+
|
|
120
|
+
/** The short escapes of a TOML basic string. */
|
|
121
|
+
const SHORT = { b: '\b', t: '\t', n: '\n', f: '\f', r: '\r', e: '\x1b', '"': '"', '\\': '\\' };
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Text with a TOML basic string's escapes decoded, as Codex reads them: a key
|
|
125
|
+
* written `\u002f` is the key with a `/` in it (the review of PR #517). An
|
|
126
|
+
* escape TOML does not have is left as it is.
|
|
127
|
+
*/
|
|
128
|
+
const unescaped = (text) => text.replace(/\\(?:u([0-9A-Fa-f]{4})|U([0-9A-Fa-f]{8})|x([0-9A-Fa-f]{2})|(.))/g, (all, u4, u8, x2, one) => {
|
|
129
|
+
const code = u4 ?? u8 ?? x2;
|
|
130
|
+
if (code === undefined) return SHORT[one] ?? all;
|
|
131
|
+
const point = parseInt(code, 16);
|
|
132
|
+
return point <= 0x10ffff ? String.fromCodePoint(point) : all;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The `trusted_hash` for each of `keys` that has one, by key, from the
|
|
137
|
+
* config.toml the session's Codex uses: `$CODEX_HOME`'s, or else
|
|
138
|
+
* `~/.codex`'s. No file is no trust. A key the file holds in any other form
|
|
139
|
+
* than a `[hooks.state."<key>"]` table with a `trusted_hash` line, or holds
|
|
140
|
+
* twice, is one the kit cannot tell, and it throws.
|
|
141
|
+
*/
|
|
142
|
+
function trustedHashes(keys, run, nothing) {
|
|
143
|
+
const file = path.join(process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), 'config.toml');
|
|
144
|
+
if (!existsSync(file)) return new Map();
|
|
145
|
+
const wanted = new Set(keys);
|
|
146
|
+
const text = readFileSync(file, 'utf8');
|
|
147
|
+
const cannotTell = (key, how) =>
|
|
148
|
+
new Error(`${run}: ${file} holds Codex's trust for the kit's hook ${key} ${how}, so the kit cannot tell whether Codex trusts it. ${nothing}`);
|
|
149
|
+
|
|
150
|
+
const found = new Map();
|
|
151
|
+
const headers = new Set();
|
|
152
|
+
let table;
|
|
153
|
+
for (const line of text.split(/\r?\n/)) {
|
|
154
|
+
const header = HEADER.exec(line);
|
|
155
|
+
if (header !== null) {
|
|
156
|
+
table = unescaped(header[1]);
|
|
157
|
+
if (wanted.has(table)) {
|
|
158
|
+
if (headers.has(table)) throw cannotTell(table, 'twice');
|
|
159
|
+
headers.add(table);
|
|
160
|
+
}
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (ANY_TABLE.test(line)) {
|
|
164
|
+
table = undefined;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const hash = TRUSTED.exec(line);
|
|
168
|
+
if (hash !== null && wanted.has(table)) found.set(table, hash[1]);
|
|
169
|
+
}
|
|
170
|
+
// A key in any other form is one the kit cannot read: as it stands, which
|
|
171
|
+
// a literal string keeps, backslashes and all, or with a basic string's
|
|
172
|
+
// escapes decoded (the review of PR #517).
|
|
173
|
+
const decoded = unescaped(text);
|
|
174
|
+
for (const key of wanted) {
|
|
175
|
+
if (headers.has(key) && !found.has(key)) throw cannotTell(key, 'in a table with no trusted_hash line the kit reads');
|
|
176
|
+
if (!headers.has(key) && (text.includes(key) || decoded.includes(key))) throw cannotTell(key, 'in a form the kit does not read');
|
|
177
|
+
}
|
|
178
|
+
return found;
|
|
179
|
+
}
|
package/src/hooks.js
CHANGED
|
@@ -20,6 +20,9 @@ import { ownCli, shellWord } from './launch.js';
|
|
|
20
20
|
/** Where each harness reads a project's hooks, inside the bot home. */
|
|
21
21
|
const HOOK_FILE = { claude: '.claude/settings.json', codex: '.codex/hooks.json' };
|
|
22
22
|
|
|
23
|
+
/** The hooks file `harness` reads in the bot at `home`. */
|
|
24
|
+
export const hooksFileOf = (home, harness) => path.join(home, HOOK_FILE[harness]);
|
|
25
|
+
|
|
23
26
|
/**
|
|
24
27
|
* The one thing the kit asks to be told: a session beginning. Both harnesses
|
|
25
28
|
* report it whenever a session starts, resumes or is begun again by the user —
|
|
@@ -381,6 +384,14 @@ const KIT_NUDGE = new RegExp(String.raw`^(${WORD}) session nudge --bots ${WORD}
|
|
|
381
384
|
const KIT_MAIL = new RegExp(String.raw`^(${WORD}) session mail --bots ${WORD} --bot ${WORD} 2>/dev/null \|\| true$`);
|
|
382
385
|
const KIT_NAME = new RegExp(String.raw`^(${WORD}) session name --bots ${WORD} --bot ${WORD} 2>/dev/null \|\| true$`);
|
|
383
386
|
|
|
387
|
+
/**
|
|
388
|
+
* Whether a hook entry is one of the kit's own as this kit writes it: a
|
|
389
|
+
* command of exactly a shape the kit writes, run by `cli`, the kit running now
|
|
390
|
+
* (#506). One that runs some other program, even another `obk`, is not.
|
|
391
|
+
*/
|
|
392
|
+
export const runsThisKit = (hook, cli = ownCli()) =>
|
|
393
|
+
hook?.type === 'command' && [KIT_HOOK, KIT_SENT, KIT_NUDGE, KIT_NAME].some((pattern) => programOf(hook.command, pattern) === cli);
|
|
394
|
+
|
|
384
395
|
/** The program a bot made before the kit named itself by path runs. */
|
|
385
396
|
const BARE = 'obk';
|
|
386
397
|
|
package/src/orca.js
CHANGED
|
@@ -284,8 +284,22 @@ export function asFolderProject(setupId, title) {
|
|
|
284
284
|
* Take a bot's Orca project away: the setup, the project and the repo record,
|
|
285
285
|
* in one call (tech notes, section 1). Only after its tabs are closed: a
|
|
286
286
|
* project taken away first leaves tabs no command line can reach.
|
|
287
|
+
*
|
|
288
|
+
* With `--force`, because the caller has made those checks itself: Orca's next
|
|
289
|
+
* release refuses a plain delete of a project with saved workspace details,
|
|
290
|
+
* which a bot's project keeps after its tabs close (#528). Orca 1.4.223 does
|
|
291
|
+
* not know the flag and refuses it before it changes anything, so then the
|
|
292
|
+
* plain delete is sent, which it takes with no such guard.
|
|
287
293
|
*/
|
|
288
|
-
export
|
|
294
|
+
export function deleteProject(setupId) {
|
|
295
|
+
const plain = ['project', 'setup-delete', '--setup', setupId];
|
|
296
|
+
try {
|
|
297
|
+
return orca([...plain, '--force']);
|
|
298
|
+
} catch (error) {
|
|
299
|
+
if (error.code !== 'invalid_argument' || !error.reason?.includes('Unknown flag --force')) throw error;
|
|
300
|
+
return orca(plain);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
289
303
|
|
|
290
304
|
/** Said after a run that made or renamed a project, whatever `tellWindow` answered, and after a removal `reloadWindow` could not follow. */
|
|
291
305
|
export const RELOAD_LINE = "If Orca's sidebar does not show it, reload the window with Cmd+Shift+R.";
|
package/src/temp.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// `obk temp make` and `obk temp retire`: a session's own temporary sessions
|
|
2
2
|
// (PRD 6.4, #227). And `obk temp trust-hooks` and `obk temp answer`, the
|
|
3
3
|
// answers a maker gives its Codex run's hooks review (#238) and its Claude
|
|
4
|
-
// session's Teach auto mode form (#489).
|
|
4
|
+
// session's Teach auto mode form (#489). And `obk session trust-hooks` and
|
|
5
|
+
// `obk session answer`, the same answers to a long-lived session's screens,
|
|
6
|
+
// given by Bot Father or the user (#506), on the same code.
|
|
5
7
|
//
|
|
6
8
|
// Any long-lived session can make a temporary session of its own bot for a
|
|
7
9
|
// piece of work, and retire it when the work is done, without Bot Father, who
|
|
@@ -31,11 +33,12 @@ import { setTimeout as pause } from 'node:timers/promises';
|
|
|
31
33
|
|
|
32
34
|
import { readBook, takeLineTurn, TYPING_HELD, TYPING_WAIT_MS, updateBook } from './book.js';
|
|
33
35
|
import { addSession, dropSession, NAME, readBot, tempRoles } from './bot.js';
|
|
36
|
+
import { untrustedKitHooks } from './hook-trust.js';
|
|
34
37
|
import { DEFAULT_APPROVAL, harnessOf, isShortPrompt, ownCli, shellWord, startPrompt, workDirOf } from './launch.js';
|
|
35
|
-
import { sessionInTab } from './message.js';
|
|
38
|
+
import { findSession, sessionInTab } from './message.js';
|
|
36
39
|
import { orca, screenRows, tabs } from './orca.js';
|
|
37
40
|
import { retireSession } from './retire.js';
|
|
38
|
-
import { bringUp, promptPath } from './up.js';
|
|
41
|
+
import { BOT_FATHER, bringUp, promptPath } from './up.js';
|
|
39
42
|
|
|
40
43
|
/** The settings a temporary session takes from its maker unless told otherwise. */
|
|
41
44
|
const INHERITED = ['model', 'effort', 'context', 'approval'];
|
|
@@ -342,26 +345,72 @@ const REVIEW_GONE_MS = 5000;
|
|
|
342
345
|
*/
|
|
343
346
|
export async function trustHooks(bots, { tab, name }) {
|
|
344
347
|
const caller = callerIn(bots, tab, 'trust-hooks', "answer a run's hooks review");
|
|
348
|
+
const { bot, session } = ownTemp(caller, name, 'answer for', 'Nothing was typed.');
|
|
349
|
+
await trustAll({ bots, run: `${caller.bot}/${name}`, bot: caller.bot, home: caller.home, session: name, harness: harnessOf(session, bot.harness) }, 'temp');
|
|
350
|
+
return { bot: caller.bot, session: name, maker: caller.session };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Shell text the shell passes on as it is, split at its spaces: what the kit's own launch line leaves unquoted, and spaces. */
|
|
354
|
+
const SHELL_PLAIN = /^[A-Za-z0-9,._+:@%/= -]*$/;
|
|
355
|
+
|
|
356
|
+
/** A count row of Codex's hooks review: `1 hook is new or changed.`, `3 hooks are new or changed.` */
|
|
357
|
+
const HOOKS_COUNT = /^ *(\d+) hooks? (?:is|are) new or changed\. *$/;
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Answer `target`'s hooks review with "Trust all and continue", for `temp
|
|
361
|
+
* trust-hooks` or `session trust-hooks` (`kind`). "Trust all" lets every hook
|
|
362
|
+
* the review counts run outside the sandbox, so first the bot's
|
|
363
|
+
* `.codex/hooks.json` must hold the kit's own hooks alone, and the review's
|
|
364
|
+
* count row must be the number of them Codex does not trust yet (#506, R4).
|
|
365
|
+
* Codex also takes hook trust, and hooks, from the session's own `-c` flags,
|
|
366
|
+
* which its extra_args can carry. What it was started with is the book's
|
|
367
|
+
* `launched_with`, which the kit writes at each launch: when that names hooks
|
|
368
|
+
* at all, or there is none, the kit cannot tell the count, and refuses (the
|
|
369
|
+
* review of PR #517). The arguments are not quoted: they can hold a secret.
|
|
370
|
+
*/
|
|
371
|
+
async function trustAll(target, kind) {
|
|
345
372
|
const nothing = 'Nothing was typed.';
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
373
|
+
if (target.harness !== 'codex') {
|
|
374
|
+
throw new Error(`${target.run} runs on ${target.harness}, and ${kind} trust-hooks answers a Codex session's "${HOOKS_REVIEW}" alone. ${nothing}`);
|
|
375
|
+
}
|
|
376
|
+
const launchedWith = readBook(target.home).sessions[target.session]?.launched_with;
|
|
377
|
+
if (launchedWith === undefined) {
|
|
378
|
+
throw new Error(`${target.run}'s book entry does not say what extra arguments it was launched with: it was started before the kit kept that record, or not by the kit. Codex takes hooks and their trust from a session's own -c flags as well, so the kit cannot tell which hooks its review covers. Start it again with the kit, then run this again: ${shellWord(ownCli())} restart --bots ${shellWord(target.bots)} --bot ${target.bot} --session ${target.session}. ${nothing}`);
|
|
379
|
+
}
|
|
380
|
+
// A list is the arguments themselves. A string is shell text, which the
|
|
381
|
+
// shell reads before Codex does (`'h''ooks'` is `hooks`), so it is read only
|
|
382
|
+
// when it holds nothing the shell would change (the review of PR #517).
|
|
383
|
+
if (typeof launchedWith === 'string' && !SHELL_PLAIN.test(launchedWith)) {
|
|
384
|
+
throw new Error(`${target.run} was launched with extra arguments written as shell text the kit does not read, and Codex takes hooks and their trust from a session's own -c flags as well, so the kit cannot tell which hooks its review covers. ${nothing}`);
|
|
385
|
+
}
|
|
386
|
+
const words = typeof launchedWith === 'string' ? launchedWith.split(' ') : [launchedWith].flat().map(String);
|
|
387
|
+
if (words.some((word) => /hooks/i.test(word))) {
|
|
388
|
+
throw new Error(`${target.run} was launched with extra arguments that mention hooks, and Codex takes hooks and their trust from a session's own -c flags as well, so the kit cannot tell which hooks its review covers. ${nothing}`);
|
|
351
389
|
}
|
|
352
|
-
|
|
390
|
+
// `--profile <name>` (`-p`) adds `<name>.config.toml` from Codex's home as a
|
|
391
|
+
// second user layer, and Codex reads hook trust from it too (codex-rs
|
|
392
|
+
// 0.162.0, cli/src/main.rs:1954-1962, config/src/loader/mod.rs:290-332).
|
|
393
|
+
if (words.some((word) => /profile/i.test(word) || /^-[^-]*p/.test(word))) {
|
|
394
|
+
throw new Error(`${target.run} was launched with a Codex profile, whose own config file can hold hook trust the kit does not read, so the kit cannot tell which hooks its review covers. ${nothing}`);
|
|
395
|
+
}
|
|
396
|
+
const { untrusted, file } = untrustedKitHooks(target.home, target.run, nothing);
|
|
397
|
+
await answerScreen(target, {
|
|
353
398
|
what: 'hooks review',
|
|
354
399
|
mark: HOOKS_REVIEW,
|
|
355
400
|
done: `"${TRUST_ALL}" was chosen on its hooks review`,
|
|
356
401
|
answer(rows, { send }) {
|
|
357
402
|
const keys = keysToTrustAll(rows);
|
|
358
403
|
if (keys === undefined) {
|
|
359
|
-
throw new Error(`${run}'s screen shows no "${HOOKS_REVIEW}" with its choices, so there is nothing for this to answer. It shows: ${shown(rows)}. ${nothing}`);
|
|
404
|
+
throw new Error(`${target.run}'s screen shows no "${HOOKS_REVIEW}" with its choices, so there is nothing for this to answer. It shows: ${shown(rows)}. ${nothing}`);
|
|
405
|
+
}
|
|
406
|
+
const counts = rows.filter((row) => HOOKS_COUNT.test(row));
|
|
407
|
+
if (counts.length !== 1 || Number(HOOKS_COUNT.exec(counts[0])[1]) !== untrusted) {
|
|
408
|
+
const saw = counts.length === 0 ? 'shows no row that says how many hooks it covers' : `says "${counts.map((row) => row.trim()).join('" and "')}"`;
|
|
409
|
+
throw new Error(`${target.run}'s hooks review ${saw}, but ${untrusted} of the kit's own hooks in ${file} are not trusted by Codex yet, so the review may cover hooks that are not the kit's, and "${TRUST_ALL}" would let them run outside the sandbox. ${nothing}`);
|
|
360
410
|
}
|
|
361
411
|
send(keys);
|
|
362
412
|
},
|
|
363
413
|
});
|
|
364
|
-
return { bot: caller.bot, session: name, maker: caller.session };
|
|
365
414
|
}
|
|
366
415
|
|
|
367
416
|
/**
|
|
@@ -386,23 +435,22 @@ function shown(rows) {
|
|
|
386
435
|
}
|
|
387
436
|
|
|
388
437
|
/**
|
|
389
|
-
* Answer a screen of `
|
|
390
|
-
* session's turn for a line (#482),
|
|
391
|
-
* `answer(rows, { send, look })`, which sends
|
|
392
|
-
* screen again with `look`, and throws when the
|
|
393
|
-
* Then wait for `mark` to leave the screen.
|
|
394
|
-
* says what was sent, for the sentences
|
|
395
|
-
* function of what `answer` returned.
|
|
438
|
+
* Answer a screen of the session `target`, `{ run, home, session }`, `run`
|
|
439
|
+
* being its `<bot>/<session>`: under the session's turn for a line (#482),
|
|
440
|
+
* read its screen and hand it to `answer(rows, { send, look })`, which sends
|
|
441
|
+
* keys with `send`, may read the screen again with `look`, and throws when the
|
|
442
|
+
* screen is not its to answer. Then wait for `mark` to leave the screen.
|
|
443
|
+
* `what` names the screen and `done` says what was sent, for the sentences
|
|
444
|
+
* that refuse or fail; `done` may be a function of what `answer` returned.
|
|
396
445
|
*/
|
|
397
|
-
async function answerScreen(
|
|
398
|
-
const run = `${caller.bot}/${name}`;
|
|
446
|
+
async function answerScreen({ run, home, session }, { what, mark, done, answer }) {
|
|
399
447
|
const nothing = 'Nothing was typed.';
|
|
400
|
-
const tabId = readBook(
|
|
401
|
-
const handle = tabId === undefined ? undefined : tabs(
|
|
448
|
+
const tabId = readBook(home).sessions[session]?.tab;
|
|
449
|
+
const handle = tabId === undefined ? undefined : tabs(home).find((one) => one.tabId === tabId)?.handle;
|
|
402
450
|
if (handle === undefined) {
|
|
403
451
|
throw new Error(`${run} has no tab open in Orca, so there is no ${what} of its to answer. ${nothing}`);
|
|
404
452
|
}
|
|
405
|
-
const turn = takeLineTurn(
|
|
453
|
+
const turn = takeLineTurn(home, session, TYPING_WAIT_MS);
|
|
406
454
|
if (turn === undefined) throw new Error(`${run}: ${TYPING_HELD}.`);
|
|
407
455
|
let answered;
|
|
408
456
|
try {
|
|
@@ -540,18 +588,27 @@ const wholeScreen = (rows) => rows.filter((row) => row.trim() !== '').map((row)
|
|
|
540
588
|
*/
|
|
541
589
|
export async function answerTemp(bots, { tab, name }) {
|
|
542
590
|
const caller = callerIn(bots, tab, 'answer', "answer a session's first-run screen");
|
|
591
|
+
ownTemp(caller, name, 'answer for', 'Nothing was typed.');
|
|
592
|
+
const sent = await answerTeach({ run: `${caller.bot}/${name}`, home: caller.home, session: name }, 'temp');
|
|
593
|
+
return { bot: caller.bot, session: name, maker: caller.session, sent };
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Answer `target`'s Teach auto mode screen with Not now, for `temp answer` or
|
|
598
|
+
* `session answer` (`kind`). Returns what answered it.
|
|
599
|
+
*/
|
|
600
|
+
function answerTeach(target, kind) {
|
|
601
|
+
const { run } = target;
|
|
543
602
|
const nothing = 'Nothing was typed.';
|
|
544
|
-
|
|
545
|
-
const run = `${caller.bot}/${name}`;
|
|
546
|
-
const sent = await answerScreen(caller, name, {
|
|
603
|
+
return answerScreen(target, {
|
|
547
604
|
what: `"${TEACH_TITLE}"`,
|
|
548
605
|
mark: TEACH_TITLE,
|
|
549
606
|
done: (said) => `${said} was sent to its Teach auto mode screen`,
|
|
550
607
|
answer(rows, { send, look }) {
|
|
551
608
|
const seen = teachScreen(rows);
|
|
552
609
|
if (seen.misfit !== undefined) {
|
|
553
|
-
const hooks = rows.some((row) => row.includes(HOOKS_REVIEW)) ? ` A Codex
|
|
554
|
-
throw new Error(`${run}'s screen is not one
|
|
610
|
+
const hooks = rows.some((row) => row.includes(HOOKS_REVIEW)) ? ` A Codex session's "${HOOKS_REVIEW}" is answered with ${kind} trust-hooks.` : '';
|
|
611
|
+
throw new Error(`${run}'s screen is not one ${kind} answer answers: ${seen.misfit}.${hooks} ${nothing} It shows:\n${wholeScreen(rows)}`);
|
|
555
612
|
}
|
|
556
613
|
if (seen.shape === TEACH_SHAPES[0]) {
|
|
557
614
|
send('\x1b');
|
|
@@ -574,7 +631,55 @@ export async function answerTemp(bots, { tab, name }) {
|
|
|
574
631
|
return `"${NOT_NOW_CHOICE}"`;
|
|
575
632
|
},
|
|
576
633
|
});
|
|
577
|
-
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* `obk session trust-hooks`: answer the hooks review of `bot`/`session`, a
|
|
638
|
+
* long-lived Codex session, as `temp trust-hooks` answers a temporary one's
|
|
639
|
+
* (#506). Returns `{ bot, session }`.
|
|
640
|
+
*/
|
|
641
|
+
export async function trustSessionHooks(bots, { tab, bot, session }) {
|
|
642
|
+
const target = longLivedTarget(bots, { tab, bot, session }, 'trust-hooks');
|
|
643
|
+
await trustAll(target, 'session');
|
|
644
|
+
return { bot: target.bot, session: target.session };
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* `obk session answer`: answer the Teach auto mode screen of `bot`/`session`,
|
|
649
|
+
* a long-lived Claude Code session, as `temp answer` answers a temporary
|
|
650
|
+
* one's (#506). Returns `{ bot, session, sent }`.
|
|
651
|
+
*/
|
|
652
|
+
export async function answerSession(bots, { tab, bot, session }) {
|
|
653
|
+
const target = longLivedTarget(bots, { tab, bot, session }, 'answer');
|
|
654
|
+
const sent = await answerTeach(target, 'session');
|
|
655
|
+
return { bot: target.bot, session: target.session, sent };
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* The long-lived session `bot`/`session`, whose first-run screen `session
|
|
660
|
+
* <verb>` answers, or a refusal. A long-lived session is Bot Father's, so the
|
|
661
|
+
* caller is a session of Bot Father's, or no session of this fleet: the user,
|
|
662
|
+
* or an assistant of theirs (#506, R2). A temporary session's screen is its
|
|
663
|
+
* maker's to answer, with the temp command.
|
|
664
|
+
*/
|
|
665
|
+
function longLivedTarget(bots, { tab, bot, session }, verb) {
|
|
666
|
+
const nothing = 'Nothing was typed.';
|
|
667
|
+
const caller = tab === undefined ? undefined : sessionInTab(bots, tab);
|
|
668
|
+
if (caller !== undefined && caller.bot !== BOT_FATHER) {
|
|
669
|
+
throw new Error(`session ${verb} runs in ${caller.bot}/${caller.session}'s tab, but a long-lived session is Bot Father's: Bot Father (${BOT_FATHER}) or the user answers its first-run screen. ${nothing}`);
|
|
670
|
+
}
|
|
671
|
+
let found;
|
|
672
|
+
try {
|
|
673
|
+
found = findSession(bots, `${bot}/${session}`);
|
|
674
|
+
} catch (error) {
|
|
675
|
+
throw new Error(`${error.message} ${nothing}`);
|
|
676
|
+
}
|
|
677
|
+
const run = `${found.bot}/${found.session}`;
|
|
678
|
+
const temporary = readBook(found.home).sessions[found.session]?.temporary;
|
|
679
|
+
if (temporary !== undefined) {
|
|
680
|
+
throw new Error(`${run} is a temporary session ${temporary.maker} made, so its first-run screen is ${temporary.maker}'s to answer, with temp ${verb} run in ${temporary.maker}'s tab. ${nothing}`);
|
|
681
|
+
}
|
|
682
|
+
return { bots, run, bot: found.bot, session: found.session, home: found.home, harness: found.harness };
|
|
578
683
|
}
|
|
579
684
|
|
|
580
685
|
/**
|
package/src/up.js
CHANGED
|
@@ -309,7 +309,10 @@ async function bringUpSession(bots, home, live, session, bot, title) {
|
|
|
309
309
|
// for a person or Bot Father to settle — added to whatever was already noted,
|
|
310
310
|
// because this run's scan cannot see what an earlier one found. The kit never
|
|
311
311
|
// settles it itself.
|
|
312
|
-
|
|
312
|
+
// The extra arguments typed on this line, as the kit's own record of what
|
|
313
|
+
// this harness was started with: bot.yaml says only what the next launch
|
|
314
|
+
// gets, and trust-hooks asks what this one got (#506).
|
|
315
|
+
let entry = { ...current.sessions[session.name], tab: made.tabId, launched, launched_with: session.extra_args ?? [], rules };
|
|
313
316
|
if (rules === undefined) delete entry.rules;
|
|
314
317
|
held = entry.session;
|
|
315
318
|
// Before the line is typed, so the hook finds no id here and takes the one
|