@assuredloop/orca-bot-kit 0.25.4 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -10
- package/SETUP.md +33 -13
- package/package.json +4 -3
- package/rules/limits.md +3 -3
- package/rules/mail.md +4 -0
- package/skills/obk-bot-building/SKILL.md +29 -18
- package/src/book.js +3 -0
- package/src/bot.js +61 -6
- package/src/cli.js +237 -129
- package/src/commands.js +74 -0
- package/src/health.js +9 -3
- package/src/hook-trust.js +179 -0
- package/src/hooks.js +11 -0
- package/src/launch.js +25 -0
- package/src/orca.js +15 -1
- package/src/permissions.js +81 -64
- package/src/temp.js +153 -30
- package/src/up.js +4 -1
package/src/commands.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
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 0037). 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', 'restart', 'unpause', 'health', 'groom', 'roster', 'usage',
|
|
60
|
+
'bot create', '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 0037): the ones that change permissions, close a long-lived
|
|
72
|
+
* session, or make the bots folder.
|
|
73
|
+
*/
|
|
74
|
+
export const KEPT_BACK = ['init', 'retire', 'pause', 'permission allow', 'permission disallow', 'permission approval'];
|
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 0037).
|
|
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
|
|
|
@@ -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/launch.js
CHANGED
|
@@ -40,6 +40,31 @@ export const bypassFlags = (harness) => APPROVAL[harness]['dangerously-skip'];
|
|
|
40
40
|
|
|
41
41
|
export const HARNESSES = Object.keys(APPROVAL);
|
|
42
42
|
|
|
43
|
+
/** How wide a level is: `ask` is narrower than `auto`, and `auto` than `dangerously-skip`. */
|
|
44
|
+
export const approvalRank = (level) => ['ask', 'auto', 'dangerously-skip'].indexOf(level);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The harness flags that set approval or permissions, which only
|
|
48
|
+
* `obk permission approval` may change, not an extra argument (ADR 0037).
|
|
49
|
+
* Codex's `-a` and `-s` take their value glued on too.
|
|
50
|
+
*/
|
|
51
|
+
const APPROVAL_FLAGS = {
|
|
52
|
+
claude: ['--permission-mode', '--dangerously-skip-permissions', '--allow-dangerously-skip-permissions', '--allowedTools', '--allowed-tools'],
|
|
53
|
+
codex: ['-a', '--ask-for-approval', '--approve-for-me', '--dangerously-bypass-approvals-and-sandbox', '-s', '--sandbox'],
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Refuse extra arguments that set a session's approval or permissions on
|
|
58
|
+
* `harness`, before anything is written, naming the command that does it.
|
|
59
|
+
*/
|
|
60
|
+
export function refuseApprovalArgs(harness, args) {
|
|
61
|
+
const found = (args ?? []).find((arg) => (APPROVAL_FLAGS[harness] ?? []).some((flag) => arg === flag
|
|
62
|
+
|| arg.startsWith(`${flag}=`)
|
|
63
|
+
|| (/^-[as]$/.test(flag) && arg.startsWith(flag) && !arg.startsWith('--'))));
|
|
64
|
+
if (found === undefined) return;
|
|
65
|
+
throw new Error(`--extra-arg=${found} sets the session's approval or permissions on ${harness}, and only obk permission approval changes those, after the user's yes. Nothing was changed.`);
|
|
66
|
+
}
|
|
67
|
+
|
|
43
68
|
/** The level a session that names none runs at (ADR 0015). */
|
|
44
69
|
export const DEFAULT_APPROVAL = 'auto';
|
|
45
70
|
|
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.";
|