@brunobracaioli/aibrain 0.4.0-beta.2 → 0.4.0-beta.4
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 +19 -0
- package/dist/assets/codex-context-compaction.mjs +98 -1
- package/dist/claude-observation.mjs +1 -1
- package/dist/cli.mjs +20 -2
- package/dist/codex-engineering.mjs +165 -0
- package/dist/engineering-pack.mjs +139 -16
- package/dist/extensions-catalog.mjs +140 -0
- package/dist/extensions-cli.mjs +214 -0
- package/dist/extensions-preferences.mjs +122 -0
- package/dist/guardrails/continuity.mjs +239 -0
- package/dist/guardrails/format.mjs +111 -0
- package/dist/guardrails/guards.mjs +397 -0
- package/dist/guardrails/run.mjs +211 -0
- package/dist/guardrails/shell-words.mjs +206 -0
- package/dist/guardrails-plugin.mjs +149 -0
- package/dist/launch.mjs +41 -2
- package/dist/monitor.mjs +21 -0
- package/dist/package.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* A small POSIX-style shell word splitter for guardrail decisions. It never
|
|
4
|
+
* runs anything. It keeps quotes, escapes and command substitutions inside
|
|
5
|
+
* their word, splits commands on control operators, and marks redirection
|
|
6
|
+
* targets. When the text cannot be split (an unterminated quote), `ok` is
|
|
7
|
+
* false and callers fall back to a word-boundary scan of the raw text.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {object} ShellCommand
|
|
12
|
+
* @property {string[]} words Words of one simple command, without redirections.
|
|
13
|
+
* @property {string[]} targets File targets of its redirections (`>`, `>>`, `<`, `&>`, ...).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const CONTROL_OPERATORS = ['&&', '||', ';;', '|&', ';', '|', '&', '(', ')'];
|
|
17
|
+
const REDIRECTIONS = ['&>>', '&>', '>>', '>|', '<>', '<<<', '<<', '>', '<'];
|
|
18
|
+
const HEREDOC_OPERATORS = new Set(['<<', '<<<']);
|
|
19
|
+
const MAX_COMMAND_LENGTH = 64 * 1024;
|
|
20
|
+
|
|
21
|
+
/** @param {string} text @param {number} index @param {string[]} candidates */
|
|
22
|
+
function operatorAt(text, index, candidates) {
|
|
23
|
+
return candidates.find((candidate) => text.startsWith(candidate, index));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Read a `$(...)` or backtick substitution verbatim, so its text stays in the word.
|
|
28
|
+
* @param {string} text @param {number} start index of `$(` or the backtick
|
|
29
|
+
* @returns {number} index after the substitution, or -1 when it never closes
|
|
30
|
+
*/
|
|
31
|
+
function substitutionEnd(text, start) {
|
|
32
|
+
if (text[start] === '`') {
|
|
33
|
+
const close = text.indexOf('`', start + 1);
|
|
34
|
+
return close === -1 ? -1 : close + 1;
|
|
35
|
+
}
|
|
36
|
+
let depth = 0;
|
|
37
|
+
for (let index = start; index < text.length; index += 1) {
|
|
38
|
+
if (text[index] === '(') depth += 1;
|
|
39
|
+
else if (text[index] === ')') {
|
|
40
|
+
depth -= 1;
|
|
41
|
+
if (depth === 0) return index + 1;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return -1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Split a shell command line into simple commands.
|
|
49
|
+
* @param {string} text
|
|
50
|
+
* @returns {{ ok: boolean, commands: ShellCommand[] }}
|
|
51
|
+
*/
|
|
52
|
+
export function splitShell(text) {
|
|
53
|
+
if (typeof text !== 'string' || text.length > MAX_COMMAND_LENGTH)
|
|
54
|
+
return { ok: false, commands: [] };
|
|
55
|
+
/** @type {ShellCommand[]} */
|
|
56
|
+
const commands = [];
|
|
57
|
+
/** @type {ShellCommand} */
|
|
58
|
+
let current = { words: [], targets: [] };
|
|
59
|
+
let word = '';
|
|
60
|
+
let inWord = false;
|
|
61
|
+
/** @type {string | null} */
|
|
62
|
+
let pendingRedirection = null;
|
|
63
|
+
|
|
64
|
+
const endWord = () => {
|
|
65
|
+
if (!inWord) return;
|
|
66
|
+
if (pendingRedirection !== null) {
|
|
67
|
+
if (!HEREDOC_OPERATORS.has(pendingRedirection)) current.targets.push(word);
|
|
68
|
+
pendingRedirection = null;
|
|
69
|
+
} else current.words.push(word);
|
|
70
|
+
word = '';
|
|
71
|
+
inWord = false;
|
|
72
|
+
};
|
|
73
|
+
const endCommand = () => {
|
|
74
|
+
endWord();
|
|
75
|
+
if (current.words.length > 0 || current.targets.length > 0) commands.push(current);
|
|
76
|
+
current = { words: [], targets: [] };
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
let index = 0;
|
|
80
|
+
while (index < text.length) {
|
|
81
|
+
const char = text[index];
|
|
82
|
+
if (char === ' ' || char === '\t') {
|
|
83
|
+
endWord();
|
|
84
|
+
index += 1;
|
|
85
|
+
} else if (char === '\n') {
|
|
86
|
+
endCommand();
|
|
87
|
+
index += 1;
|
|
88
|
+
} else if (char === '#' && !inWord) {
|
|
89
|
+
const newline = text.indexOf('\n', index);
|
|
90
|
+
index = newline === -1 ? text.length : newline;
|
|
91
|
+
} else if (char === "'") {
|
|
92
|
+
const close = text.indexOf("'", index + 1);
|
|
93
|
+
if (close === -1) return { ok: false, commands: [] };
|
|
94
|
+
word += text.slice(index + 1, close);
|
|
95
|
+
inWord = true;
|
|
96
|
+
index = close + 1;
|
|
97
|
+
} else if (char === '"') {
|
|
98
|
+
let cursor = index + 1;
|
|
99
|
+
let closed = false;
|
|
100
|
+
while (cursor < text.length) {
|
|
101
|
+
const inner = text[cursor];
|
|
102
|
+
if (
|
|
103
|
+
inner === '\\' &&
|
|
104
|
+
cursor + 1 < text.length &&
|
|
105
|
+
'"\\$`\n'.includes(text[cursor + 1] ?? '')
|
|
106
|
+
) {
|
|
107
|
+
if (text[cursor + 1] !== '\n') word += text[cursor + 1];
|
|
108
|
+
cursor += 2;
|
|
109
|
+
} else if (inner === '"') {
|
|
110
|
+
closed = true;
|
|
111
|
+
cursor += 1;
|
|
112
|
+
break;
|
|
113
|
+
} else if (inner === '$' && text[cursor + 1] === '(') {
|
|
114
|
+
const end = substitutionEnd(text, cursor + 1);
|
|
115
|
+
if (end === -1) return { ok: false, commands: [] };
|
|
116
|
+
word += text.slice(cursor, end);
|
|
117
|
+
cursor = end;
|
|
118
|
+
} else {
|
|
119
|
+
word += inner;
|
|
120
|
+
cursor += 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (!closed) return { ok: false, commands: [] };
|
|
124
|
+
inWord = true;
|
|
125
|
+
index = cursor;
|
|
126
|
+
} else if (char === '\\') {
|
|
127
|
+
if (text[index + 1] === '\n') index += 2;
|
|
128
|
+
else {
|
|
129
|
+
word += text[index + 1] ?? '';
|
|
130
|
+
inWord = true;
|
|
131
|
+
index += 2;
|
|
132
|
+
}
|
|
133
|
+
} else if ((char === '$' && text[index + 1] === '(') || char === '`') {
|
|
134
|
+
const end = substitutionEnd(text, char === '`' ? index : index + 1);
|
|
135
|
+
if (end === -1) return { ok: false, commands: [] };
|
|
136
|
+
word += text.slice(index, end);
|
|
137
|
+
inWord = true;
|
|
138
|
+
index = end;
|
|
139
|
+
} else {
|
|
140
|
+
const redirection = operatorAt(text, index, REDIRECTIONS);
|
|
141
|
+
const descriptorWord = inWord && /^\d+$/u.test(word);
|
|
142
|
+
if (redirection !== undefined && (!inWord || descriptorWord)) {
|
|
143
|
+
if (descriptorWord) {
|
|
144
|
+
word = '';
|
|
145
|
+
inWord = false;
|
|
146
|
+
} else endWord();
|
|
147
|
+
index += redirection.length;
|
|
148
|
+
// `2>&1` and `>&2` duplicate a descriptor; they name no file.
|
|
149
|
+
if (text[index] === '&' && /\d|-/u.test(text[index + 1] ?? '')) {
|
|
150
|
+
index += 2;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
pendingRedirection = redirection;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const operator = operatorAt(text, index, CONTROL_OPERATORS);
|
|
157
|
+
if (operator !== undefined) {
|
|
158
|
+
endCommand();
|
|
159
|
+
index += operator.length;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
word += char;
|
|
163
|
+
inWord = true;
|
|
164
|
+
index += 1;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
endCommand();
|
|
168
|
+
return { ok: true, commands };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Basename of a program word (`/usr/bin/git` -> `git`). @param {string} word */
|
|
172
|
+
export function programName(word) {
|
|
173
|
+
const slash = word.lastIndexOf('/');
|
|
174
|
+
return slash === -1 ? word : word.slice(slash + 1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const WRAPPERS = new Set([
|
|
178
|
+
'sudo',
|
|
179
|
+
'doas',
|
|
180
|
+
'env',
|
|
181
|
+
'command',
|
|
182
|
+
'nice',
|
|
183
|
+
'nohup',
|
|
184
|
+
'time',
|
|
185
|
+
'exec',
|
|
186
|
+
'npx',
|
|
187
|
+
'bunx',
|
|
188
|
+
]);
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The words of a command with leading `VAR=value` assignments and wrappers
|
|
192
|
+
* such as `sudo` or `env` removed, so `sudo rm -rf /` is seen as `rm -rf /`.
|
|
193
|
+
* @param {string[]} words
|
|
194
|
+
*/
|
|
195
|
+
export function effectiveWords(words) {
|
|
196
|
+
let index = 0;
|
|
197
|
+
while (index < words.length) {
|
|
198
|
+
const word = words[index] ?? '';
|
|
199
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=/u.test(word)) index += 1;
|
|
200
|
+
else if (WRAPPERS.has(programName(word))) {
|
|
201
|
+
index += 1;
|
|
202
|
+
while ((words[index] ?? '').startsWith('-')) index += 1;
|
|
203
|
+
} else break;
|
|
204
|
+
}
|
|
205
|
+
return words.slice(index);
|
|
206
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* The session plugin `aisac-guardrails` (spec aisac-engineering-extensions
|
|
4
|
+
* FR5): written into the launch's private folder with only the enabled
|
|
5
|
+
* hooks, and deleted with that folder on exit. Every command is the absolute
|
|
6
|
+
* Node executable plus the dispatcher inside the installed package; nothing
|
|
7
|
+
* comes from the server (ADR-0048).
|
|
8
|
+
*/
|
|
9
|
+
import { access, mkdir, writeFile } from 'node:fs/promises';
|
|
10
|
+
import { isAbsolute, join } from 'node:path';
|
|
11
|
+
import { hookById } from './extensions-catalog.mjs';
|
|
12
|
+
import { enabledHookIds, preferencesPath, readPreferences } from './extensions-preferences.mjs';
|
|
13
|
+
import { readStoredLocale, resolvePackLocale } from './engineering-pack.mjs';
|
|
14
|
+
|
|
15
|
+
export const GUARDRAILS_PLUGIN_NAME = 'aisac-guardrails';
|
|
16
|
+
const PLUGIN_VERSION = '1.0.0';
|
|
17
|
+
const PLUGIN_DIRECTORY = 'guardrails';
|
|
18
|
+
const STATE_DIRECTORY = 'guardrails-state';
|
|
19
|
+
const DISPATCHER = join('guardrails', 'run.mjs');
|
|
20
|
+
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/u;
|
|
21
|
+
/** @type {Readonly<Record<string, number>>} */
|
|
22
|
+
const TIMEOUT_SECONDS = Object.freeze({
|
|
23
|
+
PreToolUse: 5,
|
|
24
|
+
PostToolUse: 5,
|
|
25
|
+
UserPromptSubmit: 5,
|
|
26
|
+
Stop: 5,
|
|
27
|
+
SessionStart: 10,
|
|
28
|
+
PreCompact: 10,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/** POSIX single-quote quoting for the host's shell. @param {string} value */
|
|
32
|
+
export function shellQuote(value) {
|
|
33
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @param {string} path */
|
|
37
|
+
function safeAbsolute(path) {
|
|
38
|
+
return isAbsolute(path) && !CONTROL_CHARACTERS.test(path);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The plugin's hooks.json for the enabled ids: one entry per event, whose
|
|
43
|
+
* matcher is the union of the enabled hooks' matchers for that event.
|
|
44
|
+
* @param {string[]} enabled
|
|
45
|
+
* @param {{ node: string, dispatcher: string, stateDir: string, prefsPath: string, locale: 'pt-BR' | 'en' }} paths
|
|
46
|
+
*/
|
|
47
|
+
export function guardrailsHooks(enabled, paths) {
|
|
48
|
+
/** @type {Map<string, { ids: string[], matchers: Set<string>, unmatched: boolean }>} */
|
|
49
|
+
const events = new Map();
|
|
50
|
+
for (const id of enabled) {
|
|
51
|
+
for (const binding of hookById(id)?.bindings ?? []) {
|
|
52
|
+
const current = events.get(binding.event) ?? {
|
|
53
|
+
ids: [],
|
|
54
|
+
matchers: new Set(),
|
|
55
|
+
unmatched: false,
|
|
56
|
+
};
|
|
57
|
+
if (!current.ids.includes(id)) current.ids.push(id);
|
|
58
|
+
if (binding.matcher === undefined) current.unmatched = true;
|
|
59
|
+
else for (const name of binding.matcher.split('|')) current.matchers.add(name);
|
|
60
|
+
events.set(binding.event, current);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** @type {Record<string, unknown[]>} */
|
|
64
|
+
const hooks = {};
|
|
65
|
+
for (const [event, group] of events) {
|
|
66
|
+
const command = [
|
|
67
|
+
shellQuote(paths.node),
|
|
68
|
+
shellQuote(paths.dispatcher),
|
|
69
|
+
'--event',
|
|
70
|
+
event,
|
|
71
|
+
'--hooks',
|
|
72
|
+
group.ids.join(','),
|
|
73
|
+
'--state',
|
|
74
|
+
shellQuote(paths.stateDir),
|
|
75
|
+
'--prefs',
|
|
76
|
+
shellQuote(paths.prefsPath),
|
|
77
|
+
'--locale',
|
|
78
|
+
paths.locale,
|
|
79
|
+
].join(' ');
|
|
80
|
+
hooks[event] = [
|
|
81
|
+
{
|
|
82
|
+
...(group.unmatched || group.matchers.size === 0
|
|
83
|
+
? {}
|
|
84
|
+
: { matcher: [...group.matchers].join('|') }),
|
|
85
|
+
hooks: [{ type: 'command', command, timeout: TIMEOUT_SECONDS[event] ?? 5 }],
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
description: 'AISAC Engineering guardrails for this session (aisac hooks to change them).',
|
|
91
|
+
hooks,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Write the plugin for a licensed launch. Null when no hook is enabled or the
|
|
97
|
+
* package lacks the dispatcher; the launch then goes on without it.
|
|
98
|
+
* @param {{ directory: string, root: string, packageRoot: string }} input
|
|
99
|
+
* @returns {Promise<{ pluginRoot: string, count: number, line: string } | null>}
|
|
100
|
+
*/
|
|
101
|
+
export async function prepareGuardrails(input) {
|
|
102
|
+
const enabled = enabledHookIds(await readPreferences(input.root));
|
|
103
|
+
if (enabled.length === 0) return null;
|
|
104
|
+
const dispatcher = join(input.packageRoot, DISPATCHER);
|
|
105
|
+
const prefsPath = preferencesPath(input.root);
|
|
106
|
+
const pluginRoot = join(input.directory, PLUGIN_DIRECTORY);
|
|
107
|
+
const stateDir = join(input.directory, STATE_DIRECTORY);
|
|
108
|
+
if (![process.execPath, dispatcher, prefsPath, pluginRoot, stateDir].every(safeAbsolute))
|
|
109
|
+
return null;
|
|
110
|
+
if (
|
|
111
|
+
!(await access(dispatcher).then(
|
|
112
|
+
() => true,
|
|
113
|
+
() => false,
|
|
114
|
+
))
|
|
115
|
+
)
|
|
116
|
+
return null;
|
|
117
|
+
const locale = resolvePackLocale(await readStoredLocale(input.root));
|
|
118
|
+
await mkdir(join(pluginRoot, '.claude-plugin'), { recursive: true, mode: 0o700 });
|
|
119
|
+
await mkdir(join(pluginRoot, 'hooks'), { mode: 0o700 });
|
|
120
|
+
await mkdir(stateDir, { mode: 0o700 });
|
|
121
|
+
const manifest = {
|
|
122
|
+
name: GUARDRAILS_PLUGIN_NAME,
|
|
123
|
+
version: PLUGIN_VERSION,
|
|
124
|
+
description: 'AISAC Engineering guardrail hooks, loaded for this session only.',
|
|
125
|
+
author: { name: 'AISAC' },
|
|
126
|
+
hooks: './hooks/hooks.json',
|
|
127
|
+
};
|
|
128
|
+
const hooks = guardrailsHooks(enabled, {
|
|
129
|
+
node: process.execPath,
|
|
130
|
+
dispatcher,
|
|
131
|
+
stateDir,
|
|
132
|
+
prefsPath,
|
|
133
|
+
locale,
|
|
134
|
+
});
|
|
135
|
+
await writeFile(
|
|
136
|
+
join(pluginRoot, '.claude-plugin', 'plugin.json'),
|
|
137
|
+
`${JSON.stringify(manifest, null, 2)}\n`,
|
|
138
|
+
{ mode: 0o600, flag: 'wx' },
|
|
139
|
+
);
|
|
140
|
+
await writeFile(join(pluginRoot, 'hooks', 'hooks.json'), `${JSON.stringify(hooks, null, 2)}\n`, {
|
|
141
|
+
mode: 0o600,
|
|
142
|
+
flag: 'wx',
|
|
143
|
+
});
|
|
144
|
+
return {
|
|
145
|
+
pluginRoot,
|
|
146
|
+
count: enabled.length,
|
|
147
|
+
line: `AISAC Engineering: ${enabled.length} guardrail${enabled.length === 1 ? '' : 's'} on (aisac hooks to change them).`,
|
|
148
|
+
};
|
|
149
|
+
}
|
package/dist/launch.mjs
CHANGED
|
@@ -18,6 +18,12 @@ import {
|
|
|
18
18
|
sweepStaleLaunches,
|
|
19
19
|
writeLaunchMarker,
|
|
20
20
|
} from './engineering-pack.mjs';
|
|
21
|
+
import { prepareGuardrails } from './guardrails-plugin.mjs';
|
|
22
|
+
import {
|
|
23
|
+
CODEX_ENGINEERING_ENV,
|
|
24
|
+
CODEX_ENGINEERING_SKILLS_ROOT_ENV,
|
|
25
|
+
supportsCodexEngineering,
|
|
26
|
+
} from './codex-engineering.mjs';
|
|
21
27
|
import {
|
|
22
28
|
CODEX_LAUNCH_ENVELOPE,
|
|
23
29
|
CODEX_MONITOR_CWD,
|
|
@@ -132,6 +138,8 @@ function childEnvironment(root) {
|
|
|
132
138
|
delete environment.CODEX_CONTEXT_COMPACT_AT;
|
|
133
139
|
delete environment[CODEX_LAUNCH_ENVELOPE];
|
|
134
140
|
delete environment[CODEX_MONITOR_CWD];
|
|
141
|
+
delete environment[CODEX_ENGINEERING_ENV];
|
|
142
|
+
delete environment[CODEX_ENGINEERING_SKILLS_ROOT_ENV];
|
|
135
143
|
environment.AIBRAIN_STATE_DIR = resolve(root);
|
|
136
144
|
return environment;
|
|
137
145
|
}
|
|
@@ -421,6 +429,7 @@ function waitForChild(child, directory) {
|
|
|
421
429
|
/**
|
|
422
430
|
* @typedef {object} LaunchDependencies
|
|
423
431
|
* @property {typeof prepareEngineeringPack} [preparePack] AISAC Engineering pack loader (E4b)
|
|
432
|
+
* @property {typeof prepareGuardrails} [prepareGuardrails] Guardrail hooks plugin writer (E6a)
|
|
424
433
|
*/
|
|
425
434
|
|
|
426
435
|
/**
|
|
@@ -431,6 +440,7 @@ function waitForChild(child, directory) {
|
|
|
431
440
|
*/
|
|
432
441
|
export async function launchClaude(config, root, args, dependencies = {}) {
|
|
433
442
|
const preparePack = dependencies.preparePack ?? prepareEngineeringPack;
|
|
443
|
+
const writeGuardrails = dependencies.prepareGuardrails ?? prepareGuardrails;
|
|
434
444
|
const validated = assertConfig(config);
|
|
435
445
|
const launchArgs = await parseClaudeLaunchArgs(args, process.cwd());
|
|
436
446
|
assertClaudeArgs(launchArgs.args);
|
|
@@ -571,6 +581,15 @@ export async function launchClaude(config, root, args, dependencies = {}) {
|
|
|
571
581
|
packageRoot: validated.packageRoot,
|
|
572
582
|
}).catch(() => ({ pluginRoot: null, line: null }));
|
|
573
583
|
if (engineering.line !== null) process.stderr.write(`${engineering.line}\n`);
|
|
584
|
+
// Guardrails need the license this launch confirmed (the pack loaded), and
|
|
585
|
+
// like the pack they never block the launch (spec aisac-engineering-extensions FR5).
|
|
586
|
+
const guardrails =
|
|
587
|
+
engineering.pluginRoot === null
|
|
588
|
+
? null
|
|
589
|
+
: await writeGuardrails({ directory, root, packageRoot: validated.packageRoot }).catch(
|
|
590
|
+
() => null,
|
|
591
|
+
);
|
|
592
|
+
if (guardrails !== null) process.stderr.write(`${guardrails.line}\n`);
|
|
574
593
|
const environment = childEnvironment(root);
|
|
575
594
|
environment.CLAUDE_CODE_ENABLE_FUNCTION_HOOKS = '1';
|
|
576
595
|
/** @type {import('node:child_process').ChildProcess} */
|
|
@@ -588,6 +607,7 @@ export async function launchClaude(config, root, args, dependencies = {}) {
|
|
|
588
607
|
'--plugin-dir',
|
|
589
608
|
pluginRoot,
|
|
590
609
|
...(engineering.pluginRoot === null ? [] : ['--plugin-dir', engineering.pluginRoot]),
|
|
610
|
+
...(guardrails === null ? [] : ['--plugin-dir', guardrails.pluginRoot]),
|
|
591
611
|
...launchArgs.args,
|
|
592
612
|
],
|
|
593
613
|
{ cwd: process.cwd(), env: environment, stdio: 'inherit' },
|
|
@@ -602,8 +622,14 @@ export async function launchClaude(config, root, args, dependencies = {}) {
|
|
|
602
622
|
return finished;
|
|
603
623
|
}
|
|
604
624
|
|
|
605
|
-
/**
|
|
606
|
-
|
|
625
|
+
/**
|
|
626
|
+
* @param {LaunchConfig} config
|
|
627
|
+
* @param {string} root
|
|
628
|
+
* @param {string[]} args
|
|
629
|
+
* @param {LaunchDependencies} [dependencies]
|
|
630
|
+
*/
|
|
631
|
+
export async function launchCodex(config, root, args, dependencies = {}) {
|
|
632
|
+
const preparePack = dependencies.preparePack ?? prepareEngineeringPack;
|
|
607
633
|
const validated = assertConfig(config);
|
|
608
634
|
assertCodexArgs(args);
|
|
609
635
|
const captureLockPlatform = captureLockPlatformCheck();
|
|
@@ -654,6 +680,19 @@ export async function launchCodex(config, root, args) {
|
|
|
654
680
|
await rm(directory, { recursive: true, force: true });
|
|
655
681
|
throw error;
|
|
656
682
|
}
|
|
683
|
+
// The pack never blocks the launch: every failure starts Codex without it.
|
|
684
|
+
const engineering = await preparePack({
|
|
685
|
+
endpoint: validated.endpoint,
|
|
686
|
+
token: validated.token,
|
|
687
|
+
root,
|
|
688
|
+
directory,
|
|
689
|
+
packageRoot: validated.packageRoot,
|
|
690
|
+
client: 'codex',
|
|
691
|
+
hostSupported: supportsCodexEngineering(compatibility.version),
|
|
692
|
+
}).catch(() => ({ pluginRoot: null, codex: null, line: null }));
|
|
693
|
+
if (engineering.line !== null) process.stderr.write(`${engineering.line}\n`);
|
|
694
|
+
if (engineering.codex != null)
|
|
695
|
+
environment[CODEX_ENGINEERING_ENV] = JSON.stringify(engineering.codex);
|
|
657
696
|
environment.AI_BRAIN_MCP_SERVER = MCP_ALIAS;
|
|
658
697
|
environment.CODEX_BIN = executable;
|
|
659
698
|
environment.CODEX_MONITOR_COMMAND = monitor;
|
package/dist/monitor.mjs
CHANGED
|
@@ -12,6 +12,12 @@ import {
|
|
|
12
12
|
ownedAppServerConfigArgs,
|
|
13
13
|
parseCodexLaunchEnvelope,
|
|
14
14
|
} from './codex-permissions.mjs';
|
|
15
|
+
import {
|
|
16
|
+
CODEX_ENGINEERING_ENV,
|
|
17
|
+
CODEX_ENGINEERING_SKILLS_ROOT_ENV,
|
|
18
|
+
codexEngineeringConfigArgs,
|
|
19
|
+
parseCodexEngineering,
|
|
20
|
+
} from './codex-engineering.mjs';
|
|
15
21
|
|
|
16
22
|
const root = dirname(fileURLToPath(import.meta.url));
|
|
17
23
|
const endpoint = process.env.AIBRAIN_MCP_URL?.trim();
|
|
@@ -32,6 +38,17 @@ if (parsed.search) throw new Error('aibrain_monitor_endpoint_query');
|
|
|
32
38
|
const canonical = join(root, 'assets', 'codex-context-compaction.mjs');
|
|
33
39
|
await access(canonical, constants.R_OK);
|
|
34
40
|
const launchEnvelope = parseCodexLaunchEnvelope(process.env[CODEX_LAUNCH_ENVELOPE]);
|
|
41
|
+
// AISAC Engineering (E4c): a malformed value starts the session without the pack.
|
|
42
|
+
let engineering = null;
|
|
43
|
+
try {
|
|
44
|
+
const stateDir = process.env.AIBRAIN_STATE_DIR;
|
|
45
|
+
engineering = parseCodexEngineering(
|
|
46
|
+
process.env[CODEX_ENGINEERING_ENV],
|
|
47
|
+
stateDir === undefined ? {} : { stateDir },
|
|
48
|
+
);
|
|
49
|
+
} catch {
|
|
50
|
+
engineering = null;
|
|
51
|
+
}
|
|
35
52
|
const policyText = process.env.CODEX_CONTEXT_POLICY_JSON;
|
|
36
53
|
let policy;
|
|
37
54
|
if (policyText === undefined) {
|
|
@@ -89,6 +106,9 @@ const env = {
|
|
|
89
106
|
};
|
|
90
107
|
delete env[CODEX_LAUNCH_ENVELOPE];
|
|
91
108
|
delete env[CODEX_APP_SERVER_PROFILE];
|
|
109
|
+
delete env[CODEX_ENGINEERING_ENV];
|
|
110
|
+
delete env[CODEX_ENGINEERING_SKILLS_ROOT_ENV];
|
|
111
|
+
if (engineering !== null) env[CODEX_ENGINEERING_SKILLS_ROOT_ENV] = engineering.skillsRoot;
|
|
92
112
|
if (launchEnvelope?.profile !== undefined) env[CODEX_APP_SERVER_PROFILE] = launchEnvelope.profile;
|
|
93
113
|
if (policy !== null) env.CODEX_CONTEXT_POLICY_JSON = JSON.stringify(policy);
|
|
94
114
|
else delete env.CODEX_CONTEXT_POLICY_JSON;
|
|
@@ -109,6 +129,7 @@ const child = spawn(
|
|
|
109
129
|
'-c',
|
|
110
130
|
'mcp_servers.aibrain_context.bearer_token_env_var="CODEX_RECOVERY_MCP_TOKEN"',
|
|
111
131
|
...(launchEnvelope === null ? [] : ownedAppServerConfigArgs(launchEnvelope)),
|
|
132
|
+
...(engineering === null ? [] : codexEngineeringConfigArgs(engineering)),
|
|
112
133
|
],
|
|
113
134
|
{ cwd: process.cwd(), env, stdio: 'inherit' },
|
|
114
135
|
);
|
package/dist/package.json
CHANGED
package/package.json
CHANGED