@brunobracaioli/aibrain 0.3.0-beta.13 → 0.3.0-beta.15
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 +14 -3
- package/dist/assets/claude/.claude-plugin/plugin.json +1 -1
- package/dist/assets/claude/hooks/native-host-contract-runtime.ts +18 -0
- package/dist/assets/claude/hooks/native-host-contract.test.ts +42 -0
- package/dist/assets/jev-paid-pilot-terminal.mjs +314 -35
- package/dist/claude-compatibility.mjs +506 -0
- package/dist/cli.mjs +23 -27
- package/dist/codex-diagnostics.mjs +19 -2
- package/dist/launch.mjs +11 -4
- package/dist/package.json +1 -1
- package/dist/platform.mjs +2 -1
- package/package.json +1 -1
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
chmod,
|
|
5
|
+
cp,
|
|
6
|
+
lstat,
|
|
7
|
+
mkdir,
|
|
8
|
+
mkdtemp,
|
|
9
|
+
readdir,
|
|
10
|
+
realpath,
|
|
11
|
+
rm,
|
|
12
|
+
writeFile,
|
|
13
|
+
} from 'node:fs/promises';
|
|
14
|
+
import { dirname, join, resolve } from 'node:path';
|
|
15
|
+
import { tmpdir } from 'node:os';
|
|
16
|
+
import { spawn } from 'node:child_process';
|
|
17
|
+
|
|
18
|
+
const MAX_OUTPUT_BYTES = 96 * 1024;
|
|
19
|
+
const PROBE_TIMEOUT_MS = 30_000;
|
|
20
|
+
const MAX_PLUGIN_ASSET_BYTES = 4 * 1024 * 1024;
|
|
21
|
+
const MAX_PLUGIN_TOTAL_BYTES = 16 * 1024 * 1024;
|
|
22
|
+
const HOST_MAJOR = 2;
|
|
23
|
+
const HOST_MINOR = 1;
|
|
24
|
+
const QUALIFIED_VERSIONS = new Set([
|
|
25
|
+
'2.1.294',
|
|
26
|
+
'2.1.295',
|
|
27
|
+
'2.1.296',
|
|
28
|
+
'2.1.299',
|
|
29
|
+
'2.1.300',
|
|
30
|
+
'2.1.301',
|
|
31
|
+
'2.1.302',
|
|
32
|
+
'2.1.303',
|
|
33
|
+
]);
|
|
34
|
+
const REQUIRED_FLAGS = ['--settings', '--mcp-config', '--allowedTools', '--plugin-dir'];
|
|
35
|
+
const REQUIRED_INVENTORY = [
|
|
36
|
+
'prompt.submit',
|
|
37
|
+
'tool.call',
|
|
38
|
+
'session.end',
|
|
39
|
+
'turn.complete',
|
|
40
|
+
'session.compact',
|
|
41
|
+
'$.mcp.call',
|
|
42
|
+
'$.process.run',
|
|
43
|
+
'$.session.id',
|
|
44
|
+
'$.session.messages',
|
|
45
|
+
'$.session.usage',
|
|
46
|
+
'$.session.append',
|
|
47
|
+
'$.session.compact',
|
|
48
|
+
'$.store.get',
|
|
49
|
+
'$.store.set',
|
|
50
|
+
'$.store.delete',
|
|
51
|
+
'$.store.keys',
|
|
52
|
+
'$.ui.log',
|
|
53
|
+
'$.ui.toast',
|
|
54
|
+
];
|
|
55
|
+
const ALLOWED_PLUGIN_FILES = Object.freeze([
|
|
56
|
+
'.claude-plugin/plugin.json',
|
|
57
|
+
'hooks/hooks.json',
|
|
58
|
+
'hooks/context-compaction.ts',
|
|
59
|
+
'hooks/provider-window.js',
|
|
60
|
+
'hooks/state-fitting.mjs',
|
|
61
|
+
'hooks/state-fitting.d.mts',
|
|
62
|
+
'hooks/native-host-contract.test.ts',
|
|
63
|
+
'hooks/native-host-contract-runtime.ts',
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
/** @param {string} executable */
|
|
67
|
+
function safePath(executable) {
|
|
68
|
+
return [...new Set([dirname(executable), dirname(process.execPath), '/usr/bin', '/bin'])].join(
|
|
69
|
+
':',
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** @param {string} executable @returns {Promise<string>} */
|
|
74
|
+
async function readNativeVersion(executable) {
|
|
75
|
+
const root = await mkdtemp(join(tmpdir(), 'aibrain-claude-version-'));
|
|
76
|
+
const cwd = join(root, 'cwd');
|
|
77
|
+
const home = join(root, 'home');
|
|
78
|
+
const config = join(root, 'config');
|
|
79
|
+
await mkdir(cwd, { recursive: true, mode: 0o700 });
|
|
80
|
+
try {
|
|
81
|
+
/** @type {NodeJS.ProcessEnv} */
|
|
82
|
+
const environment = {
|
|
83
|
+
PATH: safePath(executable),
|
|
84
|
+
HOME: home,
|
|
85
|
+
CLAUDE_CONFIG_DIR: config,
|
|
86
|
+
CLAUDE_COMPAT_CWD: cwd,
|
|
87
|
+
NO_COLOR: '1',
|
|
88
|
+
CI: '1',
|
|
89
|
+
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
|
|
90
|
+
};
|
|
91
|
+
await mkdir(home, { recursive: true, mode: 0o700 });
|
|
92
|
+
await mkdir(config, { recursive: true, mode: 0o700 });
|
|
93
|
+
const result = await runBounded(executable, ['--version'], environment, {
|
|
94
|
+
timeoutMs: 10_000,
|
|
95
|
+
maxOutputBytes: 2_048,
|
|
96
|
+
});
|
|
97
|
+
if (result.code !== 0 || result.overflow || result.timedOut) return '';
|
|
98
|
+
return result.stdout.split(/\r?\n/u, 1)[0]?.trim() ?? '';
|
|
99
|
+
} finally {
|
|
100
|
+
try {
|
|
101
|
+
await rm(root, { recursive: true, force: true });
|
|
102
|
+
} catch {
|
|
103
|
+
throw new Error('probe_cleanup_failed');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** @typedef {'qualified'|'host_checked'|'rejected'} ClaudeCompatibilityStatus */
|
|
109
|
+
|
|
110
|
+
/** @typedef {object} ClaudeCompatibility
|
|
111
|
+
* @property {ClaudeCompatibilityStatus} status
|
|
112
|
+
* @property {string|null} executable
|
|
113
|
+
* @property {string|null} version
|
|
114
|
+
* @property {string} reason
|
|
115
|
+
* @property {string[]} [checks]
|
|
116
|
+
*/
|
|
117
|
+
|
|
118
|
+
/** @param {string} output */
|
|
119
|
+
export function parseClaudeStableVersion(output) {
|
|
120
|
+
const match = output
|
|
121
|
+
.trim()
|
|
122
|
+
.match(
|
|
123
|
+
/^(?:(?:claude(?:\s+code)?)\s+)?(0|[1-9]\d{0,2})\.(0|[1-9]\d{0,2})\.(0|[1-9]\d{0,2})(-[A-Za-z0-9.-]+)?(?:\s+\(Claude\s+Code\))?$/iu,
|
|
124
|
+
);
|
|
125
|
+
if (!match) return null;
|
|
126
|
+
return {
|
|
127
|
+
major: Number(match[1]),
|
|
128
|
+
minor: Number(match[2]),
|
|
129
|
+
patch: Number(match[3]),
|
|
130
|
+
prerelease: match[4] !== undefined,
|
|
131
|
+
text: `${match[1]}.${match[2]}.${match[3]}`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** @param {string} output */
|
|
136
|
+
function parseVersion(output) {
|
|
137
|
+
return parseClaudeStableVersion(output) ?? null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** @param {string} executable @param {string[]} args @param {NodeJS.ProcessEnv} environment @param {{timeoutMs?:number,maxOutputBytes?:number}} [limits] */
|
|
141
|
+
function runBounded(executable, args, environment, limits = {}) {
|
|
142
|
+
const timeoutMs = limits.timeoutMs ?? PROBE_TIMEOUT_MS;
|
|
143
|
+
const maxOutputBytes = limits.maxOutputBytes ?? MAX_OUTPUT_BYTES;
|
|
144
|
+
return new Promise((resolvePromise) => {
|
|
145
|
+
const child = spawn(executable, args, {
|
|
146
|
+
cwd: environment.CLAUDE_COMPAT_CWD,
|
|
147
|
+
env: environment,
|
|
148
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
149
|
+
detached: process.platform !== 'win32',
|
|
150
|
+
});
|
|
151
|
+
let stdout = '';
|
|
152
|
+
let stderr = '';
|
|
153
|
+
let bytes = 0;
|
|
154
|
+
let overflow = false;
|
|
155
|
+
let settled = false;
|
|
156
|
+
let timedOut = false;
|
|
157
|
+
const timer = setTimeout(() => {
|
|
158
|
+
timedOut = true;
|
|
159
|
+
if (process.platform !== 'win32' && child.pid !== undefined) {
|
|
160
|
+
try {
|
|
161
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
162
|
+
} catch {
|
|
163
|
+
child.kill('SIGKILL');
|
|
164
|
+
}
|
|
165
|
+
} else child.kill('SIGKILL');
|
|
166
|
+
}, timeoutMs);
|
|
167
|
+
/** @param {string} target @param {Buffer} chunk */
|
|
168
|
+
const append = (target, chunk) => {
|
|
169
|
+
bytes += chunk.byteLength;
|
|
170
|
+
if (bytes > maxOutputBytes) {
|
|
171
|
+
overflow = true;
|
|
172
|
+
return target;
|
|
173
|
+
}
|
|
174
|
+
return `${target}${chunk.toString('utf8')}`;
|
|
175
|
+
};
|
|
176
|
+
/** @param {number|null} code */
|
|
177
|
+
const finish = (code) => {
|
|
178
|
+
if (settled) return;
|
|
179
|
+
settled = true;
|
|
180
|
+
clearTimeout(timer);
|
|
181
|
+
resolvePromise({ code, stdout, stderr, overflow, timedOut });
|
|
182
|
+
};
|
|
183
|
+
child.stdout.on('data', (chunk) => {
|
|
184
|
+
stdout = append(stdout, chunk);
|
|
185
|
+
});
|
|
186
|
+
child.stderr.on('data', (chunk) => {
|
|
187
|
+
stderr = append(stderr, chunk);
|
|
188
|
+
});
|
|
189
|
+
child.once('error', () => finish(null));
|
|
190
|
+
child.once('close', (code) => finish(code));
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** @param {string} root @param {string} relativePath */
|
|
195
|
+
async function assertNoSymlinkPath(root, relativePath) {
|
|
196
|
+
let current = resolve(root);
|
|
197
|
+
const parts = relativePath.split('/');
|
|
198
|
+
for (const part of parts) {
|
|
199
|
+
current = join(current, part);
|
|
200
|
+
const info = await lstat(current);
|
|
201
|
+
if (info.isSymbolicLink()) throw new Error('plugin_asset_symlink_rejected');
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** @param {string} source @param {string} destination */
|
|
206
|
+
async function copyAllowlistedPlugin(source, destination) {
|
|
207
|
+
await mkdir(destination, { recursive: true, mode: 0o700 });
|
|
208
|
+
let totalBytes = 0;
|
|
209
|
+
for (const file of ALLOWED_PLUGIN_FILES) {
|
|
210
|
+
const input = join(source, file);
|
|
211
|
+
await assertNoSymlinkPath(source, file);
|
|
212
|
+
const info = await lstat(input);
|
|
213
|
+
if (!info.isFile() || info.isSymbolicLink()) throw new Error('plugin_asset_invalid');
|
|
214
|
+
const uid = process.getuid?.();
|
|
215
|
+
if (uid !== undefined && info.uid !== uid) throw new Error('plugin_asset_owner_mismatch');
|
|
216
|
+
if (info.size > MAX_PLUGIN_ASSET_BYTES) throw new Error('plugin_asset_too_large');
|
|
217
|
+
totalBytes += info.size;
|
|
218
|
+
if (totalBytes > MAX_PLUGIN_TOTAL_BYTES) throw new Error('plugin_assets_too_large');
|
|
219
|
+
const output = join(destination, file);
|
|
220
|
+
await mkdir(dirname(output), { recursive: true, mode: 0o700 });
|
|
221
|
+
await cp(input, output, { force: true, errorOnExist: true, verbatimSymlinks: true });
|
|
222
|
+
await chmod(output, 0o600);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** @param {string} directory */
|
|
227
|
+
async function assertEmptyDirectory(directory) {
|
|
228
|
+
const entries = await readdir(directory);
|
|
229
|
+
if (entries.length !== 0) throw new Error('probe_directory_not_empty');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** @param {unknown} value @returns {value is Record<string, unknown>} */
|
|
233
|
+
function record(value) {
|
|
234
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** @param {unknown} value @returns {value is string[]} */
|
|
238
|
+
function stringArray(value) {
|
|
239
|
+
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** @param {string} executable @param {string} pluginRoot @param {{timeoutMs?:number,maxOutputBytes?:number}} [limits] */
|
|
243
|
+
async function runHostProbe(executable, pluginRoot, limits = {}) {
|
|
244
|
+
const root = await mkdtemp(join(tmpdir(), 'aibrain-claude-host-'));
|
|
245
|
+
const home = join(root, 'home');
|
|
246
|
+
const config = join(root, 'config');
|
|
247
|
+
const cwd = join(root, 'cwd');
|
|
248
|
+
const copiedPlugin = join(root, 'plugin');
|
|
249
|
+
const contractPlugin = join(root, 'contract-plugin');
|
|
250
|
+
await Promise.all([
|
|
251
|
+
mkdir(home, { recursive: true, mode: 0o700 }),
|
|
252
|
+
mkdir(config, { recursive: true, mode: 0o700 }),
|
|
253
|
+
mkdir(cwd, { recursive: true, mode: 0o700 }),
|
|
254
|
+
]);
|
|
255
|
+
try {
|
|
256
|
+
await copyAllowlistedPlugin(pluginRoot, copiedPlugin);
|
|
257
|
+
await copyAllowlistedPlugin(pluginRoot, contractPlugin);
|
|
258
|
+
await writeFile(
|
|
259
|
+
join(contractPlugin, 'hooks', 'hooks.json'),
|
|
260
|
+
JSON.stringify({ modules: ['./native-host-contract-runtime.ts'] }),
|
|
261
|
+
{ mode: 0o600 },
|
|
262
|
+
);
|
|
263
|
+
/** @type {NodeJS.ProcessEnv} */
|
|
264
|
+
const environment = {
|
|
265
|
+
PATH: safePath(executable),
|
|
266
|
+
HOME: home,
|
|
267
|
+
CLAUDE_CONFIG_DIR: config,
|
|
268
|
+
CLAUDE_COMPAT_CWD: cwd,
|
|
269
|
+
CLAUDE_CODE_ENABLE_FUNCTION_HOOKS: '1',
|
|
270
|
+
AIBRAIN_NATIVE_CONTRACT: '1',
|
|
271
|
+
CLAUDE_CODE_DISABLE_AUTO_MEMORY: '1',
|
|
272
|
+
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
|
|
273
|
+
NO_COLOR: '1',
|
|
274
|
+
CI: '1',
|
|
275
|
+
};
|
|
276
|
+
const help = await runBounded(executable, ['--help'], environment, limits);
|
|
277
|
+
if (
|
|
278
|
+
help.code !== 0 ||
|
|
279
|
+
help.overflow ||
|
|
280
|
+
REQUIRED_FLAGS.some((flag) => !help.stdout.includes(flag))
|
|
281
|
+
)
|
|
282
|
+
return { ok: false, reason: 'launch_flags_missing' };
|
|
283
|
+
const validate = await runBounded(
|
|
284
|
+
executable,
|
|
285
|
+
['plugin', 'validate', '--strict', '--json', copiedPlugin],
|
|
286
|
+
environment,
|
|
287
|
+
limits,
|
|
288
|
+
);
|
|
289
|
+
if (validate.code !== 0 || validate.overflow)
|
|
290
|
+
return { ok: false, reason: 'plugin_validation_failed' };
|
|
291
|
+
/** @type {Record<string, unknown>} */
|
|
292
|
+
let report;
|
|
293
|
+
try {
|
|
294
|
+
const parsed = JSON.parse(validate.stdout);
|
|
295
|
+
if (!record(parsed)) return { ok: false, reason: 'plugin_validation_malformed' };
|
|
296
|
+
report = parsed;
|
|
297
|
+
} catch {
|
|
298
|
+
return { ok: false, reason: 'plugin_validation_malformed' };
|
|
299
|
+
}
|
|
300
|
+
if (report.success !== true || report.strict !== true)
|
|
301
|
+
return { ok: false, reason: 'plugin_validation_failed' };
|
|
302
|
+
if (
|
|
303
|
+
!record(report.manifest) ||
|
|
304
|
+
!stringArray(report.manifest.errors) ||
|
|
305
|
+
!stringArray(report.manifest.warnings)
|
|
306
|
+
)
|
|
307
|
+
return { ok: false, reason: 'plugin_validation_malformed' };
|
|
308
|
+
if (
|
|
309
|
+
!Array.isArray(report.contents) ||
|
|
310
|
+
report.contents.length !== 1 ||
|
|
311
|
+
!report.contents.every(record)
|
|
312
|
+
)
|
|
313
|
+
return { ok: false, reason: 'plugin_validation_malformed' };
|
|
314
|
+
const contents = report.contents;
|
|
315
|
+
if (
|
|
316
|
+
!contents.every(
|
|
317
|
+
(item) => stringArray(item.errors) && stringArray(item.warnings) && stringArray(item.notes),
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
return { ok: false, reason: 'plugin_validation_malformed' };
|
|
321
|
+
const manifest = report.manifest;
|
|
322
|
+
const errors = [
|
|
323
|
+
...(Array.isArray(manifest.errors) ? manifest.errors : []),
|
|
324
|
+
...(Array.isArray(manifest.warnings) ? manifest.warnings : []),
|
|
325
|
+
...contents.flatMap((item) => [
|
|
326
|
+
...(Array.isArray(item.errors) ? item.errors : []),
|
|
327
|
+
...(Array.isArray(item.warnings) ? item.warnings : []),
|
|
328
|
+
]),
|
|
329
|
+
];
|
|
330
|
+
if (errors.length > 0) return { ok: false, reason: 'plugin_validation_failed' };
|
|
331
|
+
const notes = contents.flatMap((item) => item.notes);
|
|
332
|
+
const hookNote = notes.find((note) => typeof note === 'string' && note.includes(' hooks: '));
|
|
333
|
+
const callNote = notes.find((note) => typeof note === 'string' && note.includes(' calls: '));
|
|
334
|
+
const hooks = new Set(
|
|
335
|
+
typeof hookNote === 'string'
|
|
336
|
+
? hookNote
|
|
337
|
+
.split(' hooks: ', 2)[1]
|
|
338
|
+
.split(', ')
|
|
339
|
+
.map((item) => item.trim())
|
|
340
|
+
: [],
|
|
341
|
+
);
|
|
342
|
+
const calls = new Set(
|
|
343
|
+
typeof callNote === 'string'
|
|
344
|
+
? callNote
|
|
345
|
+
.split(' calls: ', 2)[1]
|
|
346
|
+
.replace(/\s+\(via[^)]*\)/gu, '')
|
|
347
|
+
.split(', ')
|
|
348
|
+
.map((item) => item.trim())
|
|
349
|
+
: [],
|
|
350
|
+
);
|
|
351
|
+
const inventory = new Set([...hooks, ...calls]);
|
|
352
|
+
if (inventory.size === 0 || REQUIRED_INVENTORY.some((item) => !inventory.has(item)))
|
|
353
|
+
return { ok: false, reason: 'host_inventory_incomplete' };
|
|
354
|
+
const test = await runBounded(
|
|
355
|
+
executable,
|
|
356
|
+
['plugin', 'test', contractPlugin],
|
|
357
|
+
environment,
|
|
358
|
+
limits,
|
|
359
|
+
);
|
|
360
|
+
const passMatch = (test.stdout + test.stderr).match(/(?:^|\s)(\d+)\s+pass(?:ed)?(?:\s|$)/iu);
|
|
361
|
+
const failMatch = (test.stdout + test.stderr).match(/(?:^|\s)(\d+)\s+fail(?:ed)?(?:\s|$)/iu);
|
|
362
|
+
if (test.timedOut) return { ok: false, reason: 'native_plugin_tests_failed' };
|
|
363
|
+
if (test.code !== 0) return { ok: false, reason: 'native_plugin_tests_failed' };
|
|
364
|
+
if (test.overflow) return { ok: false, reason: 'native_plugin_tests_failed' };
|
|
365
|
+
if (passMatch === null || Number(passMatch[1]) < 1)
|
|
366
|
+
return { ok: false, reason: 'native_plugin_tests_failed' };
|
|
367
|
+
if (failMatch === null || Number(failMatch[1]) !== 0)
|
|
368
|
+
return { ok: false, reason: 'native_plugin_tests_failed' };
|
|
369
|
+
if (
|
|
370
|
+
!test.stdout.includes(
|
|
371
|
+
'AI Brain enabled host contract invokes usage and native compaction through the dispatcher',
|
|
372
|
+
)
|
|
373
|
+
)
|
|
374
|
+
return { ok: false, reason: 'native_plugin_tests_failed' };
|
|
375
|
+
await assertEmptyDirectory(cwd);
|
|
376
|
+
return {
|
|
377
|
+
ok: true,
|
|
378
|
+
checks: [
|
|
379
|
+
'launch_flags',
|
|
380
|
+
'plugin_validate_strict',
|
|
381
|
+
'native_plugin_test',
|
|
382
|
+
'isolated_filesystem',
|
|
383
|
+
],
|
|
384
|
+
};
|
|
385
|
+
} catch (error) {
|
|
386
|
+
const reason = error instanceof Error ? error.message : '';
|
|
387
|
+
const allowedReasons = new Set([
|
|
388
|
+
'plugin_asset_invalid',
|
|
389
|
+
'plugin_asset_symlink_rejected',
|
|
390
|
+
'plugin_asset_owner_mismatch',
|
|
391
|
+
'plugin_asset_too_large',
|
|
392
|
+
'plugin_assets_too_large',
|
|
393
|
+
'probe_directory_not_empty',
|
|
394
|
+
]);
|
|
395
|
+
return { ok: false, reason: allowedReasons.has(reason) ? reason : 'host_probe_failed' };
|
|
396
|
+
} finally {
|
|
397
|
+
try {
|
|
398
|
+
await rm(root, { recursive: true, force: true });
|
|
399
|
+
} catch {
|
|
400
|
+
throw new Error('probe_cleanup_failed');
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** @param {string} executable @param {string} packageRoot */
|
|
406
|
+
/** @param {string} executable @param {string} packageRoot @returns {Promise<ClaudeCompatibility>} */
|
|
407
|
+
export async function resolveClaudeCompatibility(executable, packageRoot) {
|
|
408
|
+
let pinnedExecutable;
|
|
409
|
+
try {
|
|
410
|
+
pinnedExecutable = await realpath(executable);
|
|
411
|
+
} catch {
|
|
412
|
+
return {
|
|
413
|
+
status: 'rejected',
|
|
414
|
+
executable: null,
|
|
415
|
+
version: null,
|
|
416
|
+
reason: 'executable_unavailable',
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
let raw;
|
|
420
|
+
try {
|
|
421
|
+
raw = await readNativeVersion(pinnedExecutable);
|
|
422
|
+
} catch {
|
|
423
|
+
return {
|
|
424
|
+
status: 'rejected',
|
|
425
|
+
executable: pinnedExecutable,
|
|
426
|
+
version: null,
|
|
427
|
+
reason: 'version_probe_cleanup_failed',
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
const version = parseVersion(raw);
|
|
431
|
+
if (!version)
|
|
432
|
+
return {
|
|
433
|
+
status: 'rejected',
|
|
434
|
+
executable: pinnedExecutable,
|
|
435
|
+
version: null,
|
|
436
|
+
reason: 'version_unavailable',
|
|
437
|
+
};
|
|
438
|
+
if (version.prerelease)
|
|
439
|
+
return {
|
|
440
|
+
status: 'rejected',
|
|
441
|
+
executable: pinnedExecutable,
|
|
442
|
+
version: version.text,
|
|
443
|
+
reason: 'prerelease',
|
|
444
|
+
};
|
|
445
|
+
if (version.major !== HOST_MAJOR || version.minor !== HOST_MINOR) {
|
|
446
|
+
return {
|
|
447
|
+
status: 'rejected',
|
|
448
|
+
executable: pinnedExecutable,
|
|
449
|
+
version: version.text,
|
|
450
|
+
reason: 'major_minor_unsupported',
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
if (QUALIFIED_VERSIONS.has(version.text))
|
|
454
|
+
return {
|
|
455
|
+
status: 'qualified',
|
|
456
|
+
executable: pinnedExecutable,
|
|
457
|
+
version: version.text,
|
|
458
|
+
reason: 'known_stable',
|
|
459
|
+
};
|
|
460
|
+
if (version.patch < 303)
|
|
461
|
+
return {
|
|
462
|
+
status: 'rejected',
|
|
463
|
+
executable: pinnedExecutable,
|
|
464
|
+
version: version.text,
|
|
465
|
+
reason: 'unsupported_stable_patch',
|
|
466
|
+
};
|
|
467
|
+
let probe;
|
|
468
|
+
try {
|
|
469
|
+
probe = await runHostProbe(pinnedExecutable, join(resolve(packageRoot), 'assets', 'claude'));
|
|
470
|
+
} catch {
|
|
471
|
+
return {
|
|
472
|
+
status: 'rejected',
|
|
473
|
+
executable: pinnedExecutable,
|
|
474
|
+
version: version.text,
|
|
475
|
+
reason: 'host_probe_cleanup_failed',
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
if (!probe.ok)
|
|
479
|
+
return {
|
|
480
|
+
status: 'rejected',
|
|
481
|
+
executable: pinnedExecutable,
|
|
482
|
+
version: version.text,
|
|
483
|
+
reason: probe.reason ?? 'host_probe_failed',
|
|
484
|
+
};
|
|
485
|
+
return {
|
|
486
|
+
status: 'host_checked',
|
|
487
|
+
executable: pinnedExecutable,
|
|
488
|
+
version: version.text,
|
|
489
|
+
reason: 'native_host_contract',
|
|
490
|
+
...(probe.checks === undefined ? {} : { checks: probe.checks }),
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** @param {ClaudeCompatibility} decision */
|
|
495
|
+
export function claudeCompatibilityError(decision) {
|
|
496
|
+
const detected = decision.version ?? 'unavailable';
|
|
497
|
+
return `claude_version_unverified: Claude Code detected version ${detected}; reason ${decision.reason}; run aibrain doctor --json or install a compatible stable native version.`;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export const claudeCompatibilityInternals = Object.freeze({
|
|
501
|
+
REQUIRED_FLAGS,
|
|
502
|
+
REQUIRED_INVENTORY,
|
|
503
|
+
ALLOWED_PLUGIN_FILES,
|
|
504
|
+
parseVersion,
|
|
505
|
+
runHostProbe,
|
|
506
|
+
});
|
package/dist/cli.mjs
CHANGED
|
@@ -15,21 +15,16 @@ import {
|
|
|
15
15
|
stateRoot,
|
|
16
16
|
tokenDigest,
|
|
17
17
|
} from './state.mjs';
|
|
18
|
-
import {
|
|
19
|
-
isTestedNativeVersion,
|
|
20
|
-
nativeVersion,
|
|
21
|
-
nativeVersionDiagnostic,
|
|
22
|
-
platformCheck,
|
|
23
|
-
resolveNative,
|
|
24
|
-
} from './platform.mjs';
|
|
18
|
+
import { nativeVersion, platformCheck, resolveNative } from './platform.mjs';
|
|
25
19
|
import { launchClaude, launchCodex } from './launch.mjs';
|
|
20
|
+
import { claudeCompatibilityError, resolveClaudeCompatibility } from './claude-compatibility.mjs';
|
|
26
21
|
import { codexCompatibilityError, resolveCodexCompatibility } from './codex-compatibility.mjs';
|
|
27
22
|
import { providerFreeProbe } from './mcp.mjs';
|
|
28
23
|
import { codexPolicyJson, makePolicy, publicPolicy, isProfile } from './policy.mjs';
|
|
29
24
|
import { readDiagnostics } from './claude-observation.mjs';
|
|
30
25
|
import { readCodexDiagnostics } from './codex-diagnostics.mjs';
|
|
31
26
|
|
|
32
|
-
const VERSION = '0.3.0-beta.
|
|
27
|
+
const VERSION = '0.3.0-beta.15';
|
|
33
28
|
const ENDPOINT = 'https://mcp.ai-brain.aisac.cloud/mcp';
|
|
34
29
|
const PACKAGE_ROOT = dirname(fileURLToPath(import.meta.url));
|
|
35
30
|
const LEGACY_DISCLOSURE_VERSION = 'aibrain-paid-context-v1-2026-10-09';
|
|
@@ -444,10 +439,13 @@ async function setup(args) {
|
|
|
444
439
|
if ((clients === 'claude' || clients === 'both') && !claude) throw new Error('claude_not_found');
|
|
445
440
|
if ((clients === 'codex' || clients === 'both') && (!codex || !node))
|
|
446
441
|
throw new Error('codex_or_node_not_found');
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
442
|
+
const claudeCompatibility =
|
|
443
|
+
(clients === 'claude' || clients === 'both') && claude !== null
|
|
444
|
+
? await resolveClaudeCompatibility(claude, PACKAGE_ROOT)
|
|
445
|
+
: null;
|
|
446
|
+
if (claudeCompatibility?.status === 'rejected')
|
|
447
|
+
throw new Error(claudeCompatibilityError(claudeCompatibility));
|
|
448
|
+
const claudeVersion = claudeCompatibility?.version ?? (claude ? nativeVersion(claude) : null);
|
|
451
449
|
let codexCompatibility = null;
|
|
452
450
|
if (clients === 'codex' || clients === 'both') {
|
|
453
451
|
codexCompatibility = codex ? await resolveCodexCompatibility(codex) : null;
|
|
@@ -519,12 +517,14 @@ async function doctor(json) {
|
|
|
519
517
|
codex: resolveNative('codex'),
|
|
520
518
|
node: resolveNative('node'),
|
|
521
519
|
};
|
|
520
|
+
const claudeCompatibility =
|
|
521
|
+
native.claude !== null ? await resolveClaudeCompatibility(native.claude, PACKAGE_ROOT) : null;
|
|
522
|
+
const codexCompatibility =
|
|
523
|
+
native.codex !== null ? await resolveCodexCompatibility(native.codex) : null;
|
|
522
524
|
const rawNativeVersions = {
|
|
523
|
-
claude:
|
|
525
|
+
claude: claudeCompatibility?.version ?? '',
|
|
524
526
|
codex: native.codex ? nativeVersion(native.codex) : '',
|
|
525
527
|
};
|
|
526
|
-
const codexCompatibility =
|
|
527
|
-
native.codex !== null ? await resolveCodexCompatibility(native.codex) : null;
|
|
528
528
|
const nativeVersions = {
|
|
529
529
|
...rawNativeVersions,
|
|
530
530
|
codex: codexCompatibility?.version ?? rawNativeVersions.codex,
|
|
@@ -533,7 +533,9 @@ async function doctor(json) {
|
|
|
533
533
|
const needsCodex = config?.clientSelection === 'codex' || config?.clientSelection === 'both';
|
|
534
534
|
const nativeReady =
|
|
535
535
|
(!needsClaude ||
|
|
536
|
-
(native.claude !== null &&
|
|
536
|
+
(native.claude !== null &&
|
|
537
|
+
(claudeCompatibility?.status === 'qualified' ||
|
|
538
|
+
claudeCompatibility?.status === 'host_checked'))) &&
|
|
537
539
|
(!needsCodex ||
|
|
538
540
|
(native.codex !== null &&
|
|
539
541
|
native.node !== null &&
|
|
@@ -552,6 +554,9 @@ async function doctor(json) {
|
|
|
552
554
|
node: native.node !== null,
|
|
553
555
|
},
|
|
554
556
|
nativeVersions,
|
|
557
|
+
claudeCompatibility: claudeCompatibility?.status ?? 'unavailable',
|
|
558
|
+
claudeCompatibilityReason: claudeCompatibility?.reason ?? 'claude_not_found',
|
|
559
|
+
claudeCompatibilityVersion: claudeCompatibility?.version ?? null,
|
|
555
560
|
codexCompatibility: codexCompatibility?.status ?? 'unavailable',
|
|
556
561
|
codexCompatibilityReason: codexCompatibility?.reason ?? 'codex_not_found',
|
|
557
562
|
codexCompatibilityVersion: codexCompatibility?.version ?? null,
|
|
@@ -663,17 +668,8 @@ async function main(argv) {
|
|
|
663
668
|
throw new Error('disclosure_outdated');
|
|
664
669
|
if (config.clientSelection !== command && config.clientSelection !== 'both')
|
|
665
670
|
throw new Error('client_not_configured');
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
(typeof config.claudeVersion !== 'string' ||
|
|
669
|
-
!isTestedNativeVersion('claude', config.claudeVersion))
|
|
670
|
-
)
|
|
671
|
-
throw new Error(
|
|
672
|
-
nativeVersionDiagnostic(
|
|
673
|
-
'claude',
|
|
674
|
-
typeof config.claudeVersion === 'string' ? config.claudeVersion : '',
|
|
675
|
-
),
|
|
676
|
-
);
|
|
671
|
+
// The native executable is authoritative. The stored setup version is
|
|
672
|
+
// metadata only, so patch updates do not block a fresh compatibility check.
|
|
677
673
|
if (command === 'codex' && typeof config.codexVersion !== 'string')
|
|
678
674
|
throw new Error('codex_version_unverified');
|
|
679
675
|
const root = stateRoot();
|
|
@@ -2,11 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import { chmod, lstat, mkdir, open, readFile, readdir, rename, rm, stat } from 'node:fs/promises';
|
|
5
|
-
import { constants } from 'node:fs';
|
|
5
|
+
import { constants, readFileSync } from 'node:fs';
|
|
6
6
|
import { join, resolve } from 'node:path';
|
|
7
7
|
|
|
8
8
|
export const CODEX_DIAGNOSTIC_SCHEMA_VERSION = 1;
|
|
9
|
-
|
|
9
|
+
const PACKAGE_VERSION_PATTERN =
|
|
10
|
+
/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?$/u;
|
|
11
|
+
function packageVersion() {
|
|
12
|
+
for (const packageUrl of [
|
|
13
|
+
new URL('./package.json', import.meta.url),
|
|
14
|
+
new URL('../package.json', import.meta.url),
|
|
15
|
+
]) {
|
|
16
|
+
try {
|
|
17
|
+
const value = JSON.parse(readFileSync(packageUrl, 'utf8'));
|
|
18
|
+
if (typeof value?.version === 'string' && PACKAGE_VERSION_PATTERN.test(value.version))
|
|
19
|
+
return value.version;
|
|
20
|
+
} catch {
|
|
21
|
+
// Source and packaged dist use different relative package metadata locations.
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
throw new Error('codex_diagnostics_package_version_unavailable');
|
|
25
|
+
}
|
|
26
|
+
export const CODEX_DIAGNOSTIC_ADAPTER_VERSION = packageVersion();
|
|
10
27
|
export const CODEX_DIAGNOSTIC_MAX_LOG_BYTES = 512 * 1024;
|
|
11
28
|
export const CODEX_DIAGNOSTIC_MAX_LOG_FILES = 3;
|
|
12
29
|
export const CODEX_DIAGNOSTIC_MAX_RECORDS = 128;
|
package/dist/launch.mjs
CHANGED
|
@@ -4,7 +4,8 @@ import { access, chmod, lstat, mkdir, mkdtemp, open, rm, writeFile } from 'node:
|
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import { constants } from 'node:fs';
|
|
6
6
|
import { isAbsolute, join, resolve, sep } from 'node:path';
|
|
7
|
-
import {
|
|
7
|
+
import { nativeVersion, resolveNative } from './platform.mjs';
|
|
8
|
+
import { claudeCompatibilityError, resolveClaudeCompatibility } from './claude-compatibility.mjs';
|
|
8
9
|
import { codexCompatibilityError, resolveCodexCompatibility } from './codex-compatibility.mjs';
|
|
9
10
|
import {
|
|
10
11
|
claudeCheckpointConfigBinding,
|
|
@@ -407,8 +408,14 @@ export async function launchClaude(config, root, args) {
|
|
|
407
408
|
assertClaudeArgs(launchArgs.args);
|
|
408
409
|
const executable = resolveNative('claude');
|
|
409
410
|
if (!executable) throw new Error('claude_not_found');
|
|
410
|
-
const
|
|
411
|
-
|
|
411
|
+
const compatibility = await resolveClaudeCompatibility(executable, validated.packageRoot);
|
|
412
|
+
if (compatibility.status === 'rejected') throw new Error(claudeCompatibilityError(compatibility));
|
|
413
|
+
const pinnedExecutable = compatibility.executable ?? executable;
|
|
414
|
+
if (compatibility.status === 'host_checked')
|
|
415
|
+
process.stderr.write(
|
|
416
|
+
`aibrain: Claude ${compatibility.version ?? 'unknown'} accepted by the local native host contract; end-to-end recovery remains separately qualified.\n`,
|
|
417
|
+
);
|
|
418
|
+
const currentVersion = compatibility.version ?? nativeVersion(pinnedExecutable);
|
|
412
419
|
const pluginRoot = await assertClaudeAssets(validated.packageRoot);
|
|
413
420
|
const telemetryActive =
|
|
414
421
|
validated.telemetry?.enabled === true && validated.telemetry?.disclosedAt != null;
|
|
@@ -533,7 +540,7 @@ export async function launchClaude(config, root, args) {
|
|
|
533
540
|
let child;
|
|
534
541
|
try {
|
|
535
542
|
child = spawn(
|
|
536
|
-
|
|
543
|
+
pinnedExecutable,
|
|
537
544
|
[
|
|
538
545
|
'--settings',
|
|
539
546
|
settingsPath,
|
package/dist/package.json
CHANGED