@brunobracaioli/aibrain 0.4.0-beta.2 → 0.4.0-beta.3
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 +7 -0
- package/dist/assets/codex-context-compaction.mjs +98 -1
- package/dist/claude-observation.mjs +1 -1
- package/dist/cli.mjs +2 -2
- package/dist/codex-engineering.mjs +165 -0
- package/dist/engineering-pack.mjs +139 -16
- package/dist/launch.mjs +28 -2
- package/dist/monitor.mjs +21 -0
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,6 +21,13 @@ private folder, which is deleted on exit. Any failure starts Claude without the
|
|
|
21
21
|
pack. `aisac setup --lang pt-BR|en` picks the pack language without a network
|
|
22
22
|
call. The pack is never part of this npm package.
|
|
23
23
|
|
|
24
|
+
`aisac codex` (Codex 0.164.0 or later) loads the same verified pack for that
|
|
25
|
+
session only: its skills are added to the Codex app-server the launch owns
|
|
26
|
+
(call one with `$`, for example `$spec`) and its agents become session agent
|
|
27
|
+
roles. Nothing is written to `~/.codex`, `~/.agents` or the project, and the
|
|
28
|
+
session's folder is deleted on exit. An older Codex starts without the pack and
|
|
29
|
+
says `codex_outdated`.
|
|
30
|
+
|
|
24
31
|
Codex capture also requires the `flock` utility from `util-linux`, normally
|
|
25
32
|
included in Linux/WSL. The launcher and doctor check availability before a
|
|
26
33
|
monitored Codex session starts. Capture coordination is released by the kernel
|
|
@@ -54,6 +54,12 @@ const MAX_OWNED_REQUESTS = 1_000;
|
|
|
54
54
|
const MAX_RETIRED_REQUESTS = 2_000;
|
|
55
55
|
const MAX_CLIENT_REQUEST_BINDINGS = 128;
|
|
56
56
|
const MAX_OWNED_THREAD_IDS = 256;
|
|
57
|
+
// AISAC Engineering (ADR-0047 E4c): the launcher's skills root is added to the
|
|
58
|
+
// owned app-server before the client sees its initialize answer.
|
|
59
|
+
const ENGINEERING_SKILLS_ROOT_ENV = 'AISAC_ENGINEERING_SKILLS_ROOT';
|
|
60
|
+
const ENGINEERING_SKILLS_ACK_TIMEOUT_MS = 2_000;
|
|
61
|
+
const MAX_ENGINEERING_PATH_LENGTH = 4_096;
|
|
62
|
+
const MAX_TRACKED_INITIALIZE_REQUESTS = 4;
|
|
57
63
|
const MAX_RECOVERY_ITEMS = 128;
|
|
58
64
|
const MAX_RECOVERY_SOURCE_INDEX = 65_536;
|
|
59
65
|
const MAX_NOTE_BYTES = 128 * 1024;
|
|
@@ -2707,6 +2713,8 @@ export function createCodexContextProxy({
|
|
|
2707
2713
|
createDiagnosticSink = null,
|
|
2708
2714
|
brainServer = process.env.AI_BRAIN_MCP_SERVER ?? 'brain',
|
|
2709
2715
|
clientVersion = process.env.CODEX_CLIENT_VERSION ?? '0.161.0',
|
|
2716
|
+
engineeringSkillsRoot = null,
|
|
2717
|
+
engineeringAckTimeoutMs = ENGINEERING_SKILLS_ACK_TIMEOUT_MS,
|
|
2710
2718
|
}) {
|
|
2711
2719
|
const contextPolicy = normalizeContextPolicy(policy);
|
|
2712
2720
|
const effectiveThreshold = contextPolicy?.compactAtPercent ?? threshold;
|
|
@@ -2719,6 +2727,15 @@ export function createCodexContextProxy({
|
|
|
2719
2727
|
const telemetryByThread = new Map();
|
|
2720
2728
|
const diagnosticSinks = new Map();
|
|
2721
2729
|
let stopped = false;
|
|
2730
|
+
// AISAC Engineering skills root: 'off' without a root; 'waiting' for the
|
|
2731
|
+
// client's first successful initialize; 'holding' app-server output until
|
|
2732
|
+
// skills/extraRoots/set is answered (or times out); then 'done'.
|
|
2733
|
+
let engineeringState =
|
|
2734
|
+
typeof engineeringSkillsRoot === 'string' && engineeringSkillsRoot.length > 0
|
|
2735
|
+
? 'waiting'
|
|
2736
|
+
: 'off';
|
|
2737
|
+
const initializeRequestKeys = new Set();
|
|
2738
|
+
let engineeringHeld = null;
|
|
2722
2739
|
function emitDiagnostic(event) {
|
|
2723
2740
|
try {
|
|
2724
2741
|
const phase = typeof event?.phase === 'string' ? event.phase : '';
|
|
@@ -4131,6 +4148,11 @@ export function createCodexContextProxy({
|
|
|
4131
4148
|
const owned = ownedRequests.get(message.id);
|
|
4132
4149
|
ownedRequests.delete(message.id);
|
|
4133
4150
|
retire(message.id);
|
|
4151
|
+
if (owned.kind === 'engineering') {
|
|
4152
|
+
// An error leaves the session without the pack skills; it is never shown.
|
|
4153
|
+
releaseEngineering();
|
|
4154
|
+
return false;
|
|
4155
|
+
}
|
|
4134
4156
|
if (
|
|
4135
4157
|
owned.kind === 'read' ||
|
|
4136
4158
|
owned.kind === 'page' ||
|
|
@@ -4364,6 +4386,14 @@ export function createCodexContextProxy({
|
|
|
4364
4386
|
if (!validRequestId) return;
|
|
4365
4387
|
const requestKey = clientRequestKey(requestId);
|
|
4366
4388
|
if (requestKey === null) return;
|
|
4389
|
+
if (
|
|
4390
|
+
message.method === 'initialize' &&
|
|
4391
|
+
engineeringState === 'waiting' &&
|
|
4392
|
+
initializeRequestKeys.size < MAX_TRACKED_INITIALIZE_REQUESTS
|
|
4393
|
+
) {
|
|
4394
|
+
initializeRequestKeys.add(requestKey);
|
|
4395
|
+
return;
|
|
4396
|
+
}
|
|
4367
4397
|
if (['thread/start', 'thread/resume', 'thread/fork'].includes(message.method)) {
|
|
4368
4398
|
const threadId =
|
|
4369
4399
|
isRecord(message.params) && typeof message.params.threadId === 'string'
|
|
@@ -4428,7 +4458,7 @@ export function createCodexContextProxy({
|
|
|
4428
4458
|
let passthrough = false;
|
|
4429
4459
|
let discardOversized = false;
|
|
4430
4460
|
let pausedForBackpressure = false;
|
|
4431
|
-
const
|
|
4461
|
+
const writeOut = (text) => {
|
|
4432
4462
|
if (!stdout.write(text) && !pausedForBackpressure) {
|
|
4433
4463
|
pausedForBackpressure = true;
|
|
4434
4464
|
child.stdout.pause();
|
|
@@ -4438,6 +4468,54 @@ export function createCodexContextProxy({
|
|
|
4438
4468
|
});
|
|
4439
4469
|
}
|
|
4440
4470
|
};
|
|
4471
|
+
const forward = (text) => {
|
|
4472
|
+
if (engineeringHeld === null) {
|
|
4473
|
+
writeOut(text);
|
|
4474
|
+
return;
|
|
4475
|
+
}
|
|
4476
|
+
engineeringHeld.lines.push(text);
|
|
4477
|
+
engineeringHeld.bytes += Buffer.byteLength(text, 'utf8');
|
|
4478
|
+
if (engineeringHeld.bytes > MAX_LINE_BYTES) releaseEngineering();
|
|
4479
|
+
};
|
|
4480
|
+
/** The client's first successful initialize answer, which starts the hold. */
|
|
4481
|
+
function isEngineeringTrigger(message) {
|
|
4482
|
+
if (engineeringState !== 'waiting' || !isRecord(message)) return false;
|
|
4483
|
+
if (message.method !== undefined || !('result' in message)) return false;
|
|
4484
|
+
const key = clientRequestKey(message.id);
|
|
4485
|
+
return key !== null && initializeRequestKeys.has(key);
|
|
4486
|
+
}
|
|
4487
|
+
function holdForEngineering(text) {
|
|
4488
|
+
engineeringState = 'holding';
|
|
4489
|
+
initializeRequestKeys.clear();
|
|
4490
|
+
engineeringHeld = {
|
|
4491
|
+
lines: [text],
|
|
4492
|
+
bytes: Buffer.byteLength(text, 'utf8'),
|
|
4493
|
+
id: null,
|
|
4494
|
+
cancel: null,
|
|
4495
|
+
};
|
|
4496
|
+
try {
|
|
4497
|
+
engineeringHeld.id = sendOwned(
|
|
4498
|
+
'skills/extraRoots/set',
|
|
4499
|
+
{ extraRoots: [engineeringSkillsRoot] },
|
|
4500
|
+
'engineering',
|
|
4501
|
+
null,
|
|
4502
|
+
);
|
|
4503
|
+
} catch {
|
|
4504
|
+
releaseEngineering();
|
|
4505
|
+
return;
|
|
4506
|
+
}
|
|
4507
|
+
engineeringHeld.cancel = scheduleTimeout(() => releaseEngineering(), engineeringAckTimeoutMs);
|
|
4508
|
+
}
|
|
4509
|
+
/** Forward everything held, in order. Idempotent; the session never waits on the pack. */
|
|
4510
|
+
function releaseEngineering() {
|
|
4511
|
+
if (engineeringHeld === null) return;
|
|
4512
|
+
const held = engineeringHeld;
|
|
4513
|
+
engineeringHeld = null;
|
|
4514
|
+
engineeringState = 'done';
|
|
4515
|
+
held.cancel?.();
|
|
4516
|
+
if (held.id !== null && ownedRequests.has(held.id)) cleanupRequest(held.id);
|
|
4517
|
+
for (const text of held.lines) writeOut(text);
|
|
4518
|
+
}
|
|
4441
4519
|
const processLine = (line) => {
|
|
4442
4520
|
if (Buffer.byteLength(line, 'utf8') + 1 > MAX_LINE_BYTES) {
|
|
4443
4521
|
const ownedId = internalRequestId(line);
|
|
@@ -4451,6 +4529,10 @@ export function createCodexContextProxy({
|
|
|
4451
4529
|
try {
|
|
4452
4530
|
const message = JSON.parse(line.endsWith('\r') ? line.slice(0, -1) : line);
|
|
4453
4531
|
observeClientResponse(message);
|
|
4532
|
+
if (isEngineeringTrigger(message)) {
|
|
4533
|
+
holdForEngineering(`${line}\n`);
|
|
4534
|
+
return;
|
|
4535
|
+
}
|
|
4454
4536
|
forwardLine = handle(message);
|
|
4455
4537
|
} catch {
|
|
4456
4538
|
/* preserve malformed external protocol lines */
|
|
@@ -4536,6 +4618,7 @@ export function createCodexContextProxy({
|
|
|
4536
4618
|
}
|
|
4537
4619
|
});
|
|
4538
4620
|
child.stdout.on('end', () => {
|
|
4621
|
+
releaseEngineering();
|
|
4539
4622
|
const tail = inputBuffer + decoder.end();
|
|
4540
4623
|
if (tail && !discardOversized) forward(tail);
|
|
4541
4624
|
inputBuffer = '';
|
|
@@ -4565,6 +4648,7 @@ export function createCodexContextProxy({
|
|
|
4565
4648
|
observeClientRequest,
|
|
4566
4649
|
observeClientResponse,
|
|
4567
4650
|
async stop() {
|
|
4651
|
+
releaseEngineering();
|
|
4568
4652
|
if (stopped) {
|
|
4569
4653
|
await Promise.all(
|
|
4570
4654
|
[...diagnosticSinks.values()].map((sink) =>
|
|
@@ -4594,6 +4678,18 @@ export function createCodexContextProxy({
|
|
|
4594
4678
|
};
|
|
4595
4679
|
}
|
|
4596
4680
|
|
|
4681
|
+
/**
|
|
4682
|
+
* The launcher's AISAC Engineering skills root, or null. Only an absolute,
|
|
4683
|
+
* normalized path without control characters is used.
|
|
4684
|
+
*/
|
|
4685
|
+
export function engineeringSkillsRootFrom(value) {
|
|
4686
|
+
if (typeof value !== 'string' || value.length === 0) return null;
|
|
4687
|
+
if (value.length > MAX_ENGINEERING_PATH_LENGTH || /[\u0000-\u001f\u007f]/u.test(value))
|
|
4688
|
+
return null;
|
|
4689
|
+
if (!value.startsWith('/') || resolve(value) !== value) return null;
|
|
4690
|
+
return value;
|
|
4691
|
+
}
|
|
4692
|
+
|
|
4597
4693
|
async function main() {
|
|
4598
4694
|
if (process.argv.includes('--help')) {
|
|
4599
4695
|
process.stderr.write(
|
|
@@ -4648,6 +4744,7 @@ async function main() {
|
|
|
4648
4744
|
timeoutMs: numberEnv('CODEX_CONTEXT_COMPACT_TIMEOUT_MS', DEFAULT_TIMEOUT_MS),
|
|
4649
4745
|
policy,
|
|
4650
4746
|
createDiagnosticSink: diagnosticFactory,
|
|
4747
|
+
engineeringSkillsRoot: engineeringSkillsRootFrom(process.env[ENGINEERING_SKILLS_ROOT_ENV]),
|
|
4651
4748
|
});
|
|
4652
4749
|
const clientDecoder = new StringDecoder('utf8');
|
|
4653
4750
|
let clientInputBuffer = '';
|
|
@@ -16,7 +16,7 @@ export const MAX_LOG_FILES = 3;
|
|
|
16
16
|
const LOCK_WAIT_MS = 250;
|
|
17
17
|
const LOCK_STALE_MS = 30_000;
|
|
18
18
|
const ADMISSION_MS = 60 * 60 * 1000;
|
|
19
|
-
export const ADAPTER_VERSION = '0.4.0-beta.
|
|
19
|
+
export const ADAPTER_VERSION = '0.4.0-beta.3';
|
|
20
20
|
const MAX_DIAGNOSTIC_COUNT = 10_000_000;
|
|
21
21
|
const MAX_DIAGNOSTIC_BYTES = 128 * 1024 * 1024;
|
|
22
22
|
const CHECKPOINT_BACKENDS = new Set(['native_store', 'private_file']);
|
package/dist/cli.mjs
CHANGED
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
writeStoredLocale,
|
|
38
38
|
} from './engineering-pack.mjs';
|
|
39
39
|
|
|
40
|
-
const VERSION = '0.4.0-beta.
|
|
40
|
+
const VERSION = '0.4.0-beta.3';
|
|
41
41
|
// The command is `aisac`; `aibrain` is an alias installed from the same entry.
|
|
42
42
|
const COMMAND = 'aisac';
|
|
43
43
|
const ENDPOINT = 'https://mcp.ai-brain.aisac.cloud/mcp';
|
|
@@ -55,7 +55,7 @@ function usage() {
|
|
|
55
55
|
'Claude shadow: setup --claude-shadow on|off and --claude-diagnostics on|off (preview stays applied=false).',
|
|
56
56
|
);
|
|
57
57
|
console.log(
|
|
58
|
-
'AISAC Engineering: `aisac claude`
|
|
58
|
+
'AISAC Engineering: `aisac claude` and `aisac codex` (Codex 0.164+) load the licensed method; setup --lang pt-BR|en picks its language; setup and doctor report the license; status stays offline. `aibrain` is an alias of `aisac`.',
|
|
59
59
|
);
|
|
60
60
|
}
|
|
61
61
|
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AISAC Engineering for Codex (ADR-0047 wave E4c, spec FR4).
|
|
5
|
+
*
|
|
6
|
+
* Codex has no session plugin directory, so the method reaches only the
|
|
7
|
+
* app-server this launch owns, and nothing is written to `~/.codex`,
|
|
8
|
+
* `~/.agents` or the project:
|
|
9
|
+
* - skills: the adapter sends `skills/extraRoots/set` with the pack's
|
|
10
|
+
* `skills/` directory right after the client's initialize (in-memory in that
|
|
11
|
+
* process);
|
|
12
|
+
* - agents: one role file per pack agent, declared with `-c agents.<name>.*`
|
|
13
|
+
* overrides on the owned app-server (the session-flags config layer).
|
|
14
|
+
* The launcher hands the monitor one JSON value; the monitor validates it
|
|
15
|
+
* again before turning it into arguments.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
19
|
+
|
|
20
|
+
export const CODEX_ENGINEERING_ENV = 'AISAC_ENGINEERING_CODEX';
|
|
21
|
+
export const CODEX_ENGINEERING_SKILLS_ROOT_ENV = 'AISAC_ENGINEERING_SKILLS_ROOT';
|
|
22
|
+
export const CODEX_AGENTS_DIRECTORY = 'codex-agents';
|
|
23
|
+
const SKILLS_DIRECTORY = 'skills';
|
|
24
|
+
const PACK_DIRECTORY = 'engineering';
|
|
25
|
+
const RUNTIME_DIRECTORY = 'runtime';
|
|
26
|
+
const CODEX_LAUNCH_DIRECTORY = /^codex-[A-Za-z0-9]{6}$/u;
|
|
27
|
+
const SCHEMA_VERSION = 1;
|
|
28
|
+
const MAX_SPEC_BYTES = 16 * 1024;
|
|
29
|
+
const MAX_AGENTS = 16;
|
|
30
|
+
const MAX_PATH_LENGTH = 4096;
|
|
31
|
+
export const MAX_AGENT_DESCRIPTION_LENGTH = 1024;
|
|
32
|
+
export const AGENT_NAME = /^[a-z][a-z0-9-]{0,63}$/u;
|
|
33
|
+
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/u;
|
|
34
|
+
/** First Codex release whose session mechanism was verified (spike Q4, 2026-11-03). */
|
|
35
|
+
const MIN_CODEX_VERSION = /** @type {const} */ ([0, 164, 0]);
|
|
36
|
+
|
|
37
|
+
/** @typedef {{ name: string, description: string, configFile: string }} CodexAgentRole */
|
|
38
|
+
/** @typedef {{ schemaVersion: 1, skillsRoot: string, agents: CodexAgentRole[] }} CodexEngineering */
|
|
39
|
+
|
|
40
|
+
/** @param {string} code @returns {never} */
|
|
41
|
+
function fail(code) {
|
|
42
|
+
throw new Error(`codex_engineering_${code}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @param {unknown} value @returns {value is Record<string, unknown>} */
|
|
46
|
+
function isRecord(value) {
|
|
47
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Whether this Codex version receives the pack. A null or malformed version
|
|
52
|
+
* never does.
|
|
53
|
+
* @param {string | null | undefined} version
|
|
54
|
+
*/
|
|
55
|
+
export function supportsCodexEngineering(version) {
|
|
56
|
+
const match = /^(\d+)\.(\d+)\.(\d+)$/u.exec(version ?? '');
|
|
57
|
+
if (match === null) return false;
|
|
58
|
+
const parts = match.slice(1).map(Number);
|
|
59
|
+
for (let index = 0; index < MIN_CODEX_VERSION.length; index += 1) {
|
|
60
|
+
const part = /** @type {number} */ (parts[index]);
|
|
61
|
+
const minimum = MIN_CODEX_VERSION[index] ?? 0;
|
|
62
|
+
if (part !== minimum) return part > minimum;
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** @param {unknown} value */
|
|
68
|
+
function isCleanAbsolutePath(value) {
|
|
69
|
+
return (
|
|
70
|
+
typeof value === 'string' &&
|
|
71
|
+
value.length > 0 &&
|
|
72
|
+
value.length <= MAX_PATH_LENGTH &&
|
|
73
|
+
!CONTROL_CHARACTERS.test(value) &&
|
|
74
|
+
value.startsWith('/') &&
|
|
75
|
+
resolve(value) === value
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {unknown} value
|
|
81
|
+
* @param {{ stateDir?: string }} [options] when given, the pack must sit in a
|
|
82
|
+
* Codex launch directory of that state directory
|
|
83
|
+
* @returns {CodexEngineering}
|
|
84
|
+
*/
|
|
85
|
+
export function validateCodexEngineering(value, options = {}) {
|
|
86
|
+
if (!isRecord(value)) fail('invalid');
|
|
87
|
+
const keys = Object.keys(value).sort().join(',');
|
|
88
|
+
if (keys !== 'agents,schemaVersion,skillsRoot') fail('invalid');
|
|
89
|
+
if (value.schemaVersion !== SCHEMA_VERSION) fail('schema');
|
|
90
|
+
const skillsRoot = value.skillsRoot;
|
|
91
|
+
if (!isCleanAbsolutePath(skillsRoot)) fail('path');
|
|
92
|
+
const skills = /** @type {string} */ (skillsRoot);
|
|
93
|
+
if (basename(skills) !== SKILLS_DIRECTORY) fail('path');
|
|
94
|
+
const base = dirname(skills);
|
|
95
|
+
if (basename(base) !== PACK_DIRECTORY) fail('path');
|
|
96
|
+
if (options.stateDir !== undefined) {
|
|
97
|
+
const launch = dirname(base);
|
|
98
|
+
if (
|
|
99
|
+
!CODEX_LAUNCH_DIRECTORY.test(basename(launch)) ||
|
|
100
|
+
dirname(launch) !== join(resolve(options.stateDir), RUNTIME_DIRECTORY)
|
|
101
|
+
)
|
|
102
|
+
fail('path');
|
|
103
|
+
}
|
|
104
|
+
if (!Array.isArray(value.agents) || value.agents.length > MAX_AGENTS) fail('agents');
|
|
105
|
+
const seen = new Set();
|
|
106
|
+
/** @type {CodexAgentRole[]} */
|
|
107
|
+
const agents = [];
|
|
108
|
+
for (const agent of value.agents) {
|
|
109
|
+
if (!isRecord(agent) || Object.keys(agent).sort().join(',') !== 'configFile,description,name')
|
|
110
|
+
fail('agents');
|
|
111
|
+
const { name, description, configFile } = agent;
|
|
112
|
+
if (typeof name !== 'string' || !AGENT_NAME.test(name) || seen.has(name)) fail('agents');
|
|
113
|
+
if (
|
|
114
|
+
typeof description !== 'string' ||
|
|
115
|
+
description.trim().length === 0 ||
|
|
116
|
+
description.length > MAX_AGENT_DESCRIPTION_LENGTH ||
|
|
117
|
+
CONTROL_CHARACTERS.test(description)
|
|
118
|
+
)
|
|
119
|
+
fail('agents');
|
|
120
|
+
if (configFile !== join(base, CODEX_AGENTS_DIRECTORY, `${name}.toml`)) fail('agents');
|
|
121
|
+
seen.add(name);
|
|
122
|
+
agents.push({ name, description, configFile });
|
|
123
|
+
}
|
|
124
|
+
return { schemaVersion: SCHEMA_VERSION, skillsRoot: skills, agents };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Parse the monitor's environment value; null when absent.
|
|
129
|
+
* @param {string | undefined} raw
|
|
130
|
+
* @param {{ stateDir?: string }} [options]
|
|
131
|
+
* @returns {CodexEngineering | null}
|
|
132
|
+
*/
|
|
133
|
+
export function parseCodexEngineering(raw, options = {}) {
|
|
134
|
+
if (raw === undefined || raw === '') return null;
|
|
135
|
+
if (Buffer.byteLength(raw, 'utf8') > MAX_SPEC_BYTES) fail('too_large');
|
|
136
|
+
let value;
|
|
137
|
+
try {
|
|
138
|
+
value = JSON.parse(raw);
|
|
139
|
+
} catch {
|
|
140
|
+
fail('invalid');
|
|
141
|
+
}
|
|
142
|
+
return validateCodexEngineering(value, options);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* A TOML basic string. JSON escapes are TOML escapes; DEL is escaped too.
|
|
147
|
+
* @param {string} value
|
|
148
|
+
*/
|
|
149
|
+
export function tomlBasicString(value) {
|
|
150
|
+
return JSON.stringify(value).replace(/\u007f/gu, '\\u007F');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Session-only `-c` overrides for the owned app-server, one role per agent.
|
|
155
|
+
* @param {CodexEngineering} engineering
|
|
156
|
+
* @returns {string[]}
|
|
157
|
+
*/
|
|
158
|
+
export function codexEngineeringConfigArgs(engineering) {
|
|
159
|
+
return engineering.agents.flatMap((agent) => [
|
|
160
|
+
'-c',
|
|
161
|
+
`agents.${agent.name}.description=${tomlBasicString(agent.description)}`,
|
|
162
|
+
'-c',
|
|
163
|
+
`agents.${agent.name}.config_file=${tomlBasicString(agent.configFile)}`,
|
|
164
|
+
]);
|
|
165
|
+
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* AISAC Engineering pack for Claude Code (ADR-0047 wave E4b
|
|
4
|
+
* AISAC Engineering pack for Claude Code (ADR-0047 wave E4b) and Codex (E4c),
|
|
5
|
+
* spec FR4/FR8.
|
|
5
6
|
*
|
|
6
7
|
* One request per launch fetches the account's pack; it doubles as the live
|
|
7
8
|
* license check. The pack is trusted instruction for the user's agent, so it
|
|
8
9
|
* is accepted only when it is signed by a pinned key, matches the requested
|
|
9
10
|
* locale and passes the FR3 rules again here. It is written into the launch's
|
|
10
|
-
* private directory and handed to Claude Code as a second `--plugin-dir
|
|
11
|
+
* private directory and handed to Claude Code as a second `--plugin-dir`, or
|
|
12
|
+
* to the Codex app-server this launch owns (see codex-engineering.mjs).
|
|
11
13
|
* Any failure starts the session without the pack; nothing here can block a
|
|
12
14
|
* launch or treat a failure as licensed.
|
|
13
15
|
*/
|
|
@@ -35,6 +37,13 @@ import {
|
|
|
35
37
|
unavailableReasonFor,
|
|
36
38
|
} from './engineering.mjs';
|
|
37
39
|
import { atomicJson } from './state.mjs';
|
|
40
|
+
import {
|
|
41
|
+
AGENT_NAME,
|
|
42
|
+
CODEX_AGENTS_DIRECTORY,
|
|
43
|
+
MAX_AGENT_DESCRIPTION_LENGTH,
|
|
44
|
+
tomlBasicString,
|
|
45
|
+
validateCodexEngineering,
|
|
46
|
+
} from './codex-engineering.mjs';
|
|
38
47
|
|
|
39
48
|
const PACK_PATH = '/engineering/v1/pack';
|
|
40
49
|
export const PACK_TIMEOUT_MS = 10_000;
|
|
@@ -86,6 +95,10 @@ const PLUGIN_NAME = 'aisac-engineering';
|
|
|
86
95
|
const PLUGIN_LICENSE = 'LicenseRef-AISAC-Engineering';
|
|
87
96
|
const PACK_DIRECTORY = 'engineering';
|
|
88
97
|
const OUTPUT_STYLES_PREFIX = 'output-styles/';
|
|
98
|
+
const SKILLS_DIRECTORY = 'skills';
|
|
99
|
+
const AGENT_FILE = /^agents\/([a-z][a-z0-9-]{0,63})\.md$/u;
|
|
100
|
+
const WATERMARK_KEY = 'aisac_watermark';
|
|
101
|
+
const WATERMARK_VALUE = /^[0-9a-f]{16}$/u;
|
|
89
102
|
export const PUBLIC_KEY_FILE = 'engineering-pack-public-key.json';
|
|
90
103
|
const LOCALE_FILE = 'engineering-locale.json';
|
|
91
104
|
const LAST_LAUNCH_FILE = 'engineering-last-launch.json';
|
|
@@ -113,11 +126,12 @@ const SAFE_REASON = /^[a-z][a-z0-9_]{0,63}$/u;
|
|
|
113
126
|
* | { outcome: 'unavailable', reason: string }
|
|
114
127
|
* | { outcome: 'rejected', reason: string }} PackOutcome
|
|
115
128
|
*/
|
|
129
|
+
/** @typedef {'claude' | 'codex'} PackClient */
|
|
116
130
|
/**
|
|
117
131
|
* @typedef {{
|
|
118
132
|
* schemaVersion: 1,
|
|
119
133
|
* at: string,
|
|
120
|
-
* client:
|
|
134
|
+
* client: PackClient,
|
|
121
135
|
* state: PackOutcome['outcome'],
|
|
122
136
|
* reason: string | null,
|
|
123
137
|
* packVersion: string | null,
|
|
@@ -198,7 +212,7 @@ export async function readLastLaunch(root) {
|
|
|
198
212
|
value.schemaVersion !== 1 ||
|
|
199
213
|
typeof value.at !== 'string' ||
|
|
200
214
|
!ISO_INSTANT.test(value.at) ||
|
|
201
|
-
value.client !== 'claude' ||
|
|
215
|
+
(value.client !== 'claude' && value.client !== 'codex') ||
|
|
202
216
|
!['loaded', 'inactive', 'not_offered', 'unavailable', 'rejected'].includes(
|
|
203
217
|
/** @type {string} */ (value.state),
|
|
204
218
|
) ||
|
|
@@ -519,12 +533,13 @@ const PLUGIN_DESCRIPTION = {
|
|
|
519
533
|
};
|
|
520
534
|
|
|
521
535
|
/**
|
|
522
|
-
* Write
|
|
523
|
-
*
|
|
536
|
+
* Write the pack files into `engineering/` of the launch's private directory:
|
|
537
|
+
* `0700` directories, `0600` files, exclusive creation, contained paths.
|
|
524
538
|
* @param {VerifiedPack} pack
|
|
525
539
|
* @param {string} launchDirectory
|
|
540
|
+
* @returns {Promise<string>} the pack root
|
|
526
541
|
*/
|
|
527
|
-
|
|
542
|
+
async function writePackFiles(pack, launchDirectory) {
|
|
528
543
|
const base = resolve(launchDirectory, PACK_DIRECTORY);
|
|
529
544
|
await mkdir(base, { mode: 0o700 });
|
|
530
545
|
for (const file of pack.files) {
|
|
@@ -534,6 +549,17 @@ export async function materializePack(pack, launchDirectory) {
|
|
|
534
549
|
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
535
550
|
await writeFile(target, file.content, { mode: 0o600, flag: 'wx' });
|
|
536
551
|
}
|
|
552
|
+
return base;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Write a verified pack as a Claude Code plugin inside the launch's private
|
|
557
|
+
* directory and return the plugin root. The manifest is the client's own.
|
|
558
|
+
* @param {VerifiedPack} pack
|
|
559
|
+
* @param {string} launchDirectory
|
|
560
|
+
*/
|
|
561
|
+
export async function materializePack(pack, launchDirectory) {
|
|
562
|
+
const base = await writePackFiles(pack, launchDirectory);
|
|
537
563
|
const manifest = {
|
|
538
564
|
name: PLUGIN_NAME,
|
|
539
565
|
version: `0.0.0-${pack.packVersion}`,
|
|
@@ -553,6 +579,92 @@ export async function materializePack(pack, launchDirectory) {
|
|
|
553
579
|
return base;
|
|
554
580
|
}
|
|
555
581
|
|
|
582
|
+
/**
|
|
583
|
+
* Front-matter values (one `key: value` per line, lower-cased keys, matching
|
|
584
|
+
* outer quotes removed) and the body after it; null without front matter.
|
|
585
|
+
* @param {string} content
|
|
586
|
+
* @returns {{ values: Map<string, string>, body: string } | null}
|
|
587
|
+
*/
|
|
588
|
+
export function splitFrontMatter(content) {
|
|
589
|
+
if (!content.startsWith(FRONT_MATTER_FENCE)) return null;
|
|
590
|
+
const end = content.indexOf(FRONT_MATTER_CLOSE, FRONT_MATTER_FENCE.length - 1);
|
|
591
|
+
if (end === -1) return null;
|
|
592
|
+
/** @type {Map<string, string>} */
|
|
593
|
+
const values = new Map();
|
|
594
|
+
for (const line of content.slice(FRONT_MATTER_FENCE.length, end + 1).split('\n')) {
|
|
595
|
+
const key = FRONT_MATTER_KEY.exec(line)?.[1];
|
|
596
|
+
if (key === undefined) continue;
|
|
597
|
+
let value = line.slice(line.indexOf(':') + 1).trim();
|
|
598
|
+
if (value.length >= 2 && (value[0] === '"' || value[0] === "'") && value.at(-1) === value[0])
|
|
599
|
+
value = value.slice(1, -1);
|
|
600
|
+
values.set(key.toLowerCase(), value);
|
|
601
|
+
}
|
|
602
|
+
return { values, body: content.slice(end + FRONT_MATTER_CLOSE.length).replace(/^\n+/u, '') };
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* A Codex agent role file from a pack agent (`agents/<name>.md`): the front
|
|
607
|
+
* matter's name and description, the body as developer instructions, and the
|
|
608
|
+
* origin mark kept as a comment. Null when the agent cannot be a role.
|
|
609
|
+
* @param {PackFile} file
|
|
610
|
+
* @returns {{ name: string, description: string, toml: string } | null}
|
|
611
|
+
*/
|
|
612
|
+
export function codexRoleOf(file) {
|
|
613
|
+
const fileName = AGENT_FILE.exec(file.path)?.[1];
|
|
614
|
+
const parsed = splitFrontMatter(file.content);
|
|
615
|
+
if (fileName === undefined || parsed === null) return null;
|
|
616
|
+
const name = parsed.values.get('name');
|
|
617
|
+
const description = parsed.values.get('description');
|
|
618
|
+
if (name !== fileName || !AGENT_NAME.test(name)) return null;
|
|
619
|
+
if (
|
|
620
|
+
description === undefined ||
|
|
621
|
+
description.length === 0 ||
|
|
622
|
+
description.length > MAX_AGENT_DESCRIPTION_LENGTH ||
|
|
623
|
+
description === '>' ||
|
|
624
|
+
description === '|' ||
|
|
625
|
+
parsed.body.trim().length === 0
|
|
626
|
+
)
|
|
627
|
+
return null;
|
|
628
|
+
const watermark = parsed.values.get(WATERMARK_KEY);
|
|
629
|
+
const lines = [
|
|
630
|
+
'# AISAC Engineering. Licensed to your account; do not redistribute.',
|
|
631
|
+
...(watermark !== undefined && WATERMARK_VALUE.test(watermark)
|
|
632
|
+
? [`# ${WATERMARK_KEY}: ${watermark}`]
|
|
633
|
+
: []),
|
|
634
|
+
`name = ${tomlBasicString(name)}`,
|
|
635
|
+
`description = ${tomlBasicString(description)}`,
|
|
636
|
+
`developer_instructions = ${tomlBasicString(parsed.body)}`,
|
|
637
|
+
];
|
|
638
|
+
return { name, description, toml: `${lines.join('\n')}\n` };
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Write a verified pack for the Codex app-server this launch owns: the files
|
|
643
|
+
* as for Claude (without the Claude manifest), one role file per agent in
|
|
644
|
+
* `codex-agents/`, and the description the monitor turns into session
|
|
645
|
+
* arguments. Output styles have no Codex equivalent and stay unused.
|
|
646
|
+
* @param {VerifiedPack} pack
|
|
647
|
+
* @param {string} launchDirectory
|
|
648
|
+
* @returns {Promise<import('./codex-engineering.mjs').CodexEngineering>}
|
|
649
|
+
*/
|
|
650
|
+
export async function materializeCodexPack(pack, launchDirectory) {
|
|
651
|
+
const base = await writePackFiles(pack, launchDirectory);
|
|
652
|
+
const skillsRoot = join(base, SKILLS_DIRECTORY);
|
|
653
|
+
await mkdir(skillsRoot, { recursive: true, mode: 0o700 });
|
|
654
|
+
const rolesDirectory = join(base, CODEX_AGENTS_DIRECTORY);
|
|
655
|
+
await mkdir(rolesDirectory, { mode: 0o700 });
|
|
656
|
+
/** @type {import('./codex-engineering.mjs').CodexAgentRole[]} */
|
|
657
|
+
const agents = [];
|
|
658
|
+
for (const file of pack.files) {
|
|
659
|
+
const role = codexRoleOf(file);
|
|
660
|
+
if (role === null) continue;
|
|
661
|
+
const configFile = join(rolesDirectory, `${role.name}.toml`);
|
|
662
|
+
await writeFile(configFile, role.toml, { mode: 0o600, flag: 'wx' });
|
|
663
|
+
agents.push({ name: role.name, description: role.description, configFile });
|
|
664
|
+
}
|
|
665
|
+
return validateCodexEngineering({ schemaVersion: 1, skillsRoot, agents });
|
|
666
|
+
}
|
|
667
|
+
|
|
556
668
|
/**
|
|
557
669
|
* One line for the launch, in the pack locale; null when the capability is
|
|
558
670
|
* not offered, so memory-only users see no change.
|
|
@@ -594,13 +706,14 @@ export function packNotice(outcome, locale) {
|
|
|
594
706
|
/**
|
|
595
707
|
* @param {PackOutcome} outcome
|
|
596
708
|
* @param {PackLocale} locale
|
|
709
|
+
* @param {PackClient} client
|
|
597
710
|
* @returns {LastLaunch}
|
|
598
711
|
*/
|
|
599
|
-
function lastLaunchOf(outcome, locale) {
|
|
712
|
+
function lastLaunchOf(outcome, locale, client) {
|
|
600
713
|
return {
|
|
601
714
|
schemaVersion: 1,
|
|
602
715
|
at: new Date().toISOString(),
|
|
603
|
-
client
|
|
716
|
+
client,
|
|
604
717
|
state: outcome.outcome,
|
|
605
718
|
reason:
|
|
606
719
|
outcome.outcome === 'unavailable' || outcome.outcome === 'rejected' ? outcome.reason : null,
|
|
@@ -610,13 +723,17 @@ function lastLaunchOf(outcome, locale) {
|
|
|
610
723
|
}
|
|
611
724
|
|
|
612
725
|
/**
|
|
613
|
-
* Fetch, verify and write the pack for one Claude Code launch. Never
|
|
726
|
+
* Fetch, verify and write the pack for one Claude Code or Codex launch. Never
|
|
727
|
+
* throws. `hostSupported: false` (a Codex release older than the verified
|
|
728
|
+
* mechanism) still checks the license but loads nothing.
|
|
614
729
|
* @param {{ endpoint: string, token: string, root: string, directory: string,
|
|
615
|
-
* packageRoot: string,
|
|
616
|
-
* timeoutMs?: number }} input
|
|
617
|
-
* @returns {Promise<{ pluginRoot: string | null,
|
|
730
|
+
* packageRoot: string, client?: PackClient, hostSupported?: boolean,
|
|
731
|
+
* env?: NodeJS.ProcessEnv, fetchImpl?: typeof fetch, timeoutMs?: number }} input
|
|
732
|
+
* @returns {Promise<{ pluginRoot: string | null,
|
|
733
|
+
* codex: import('./codex-engineering.mjs').CodexEngineering | null, line: string | null }>}
|
|
618
734
|
*/
|
|
619
735
|
export async function prepareEngineeringPack(input) {
|
|
736
|
+
const client = input.client ?? 'claude';
|
|
620
737
|
const locale = resolvePackLocale(await readStoredLocale(input.root), input.env);
|
|
621
738
|
/** @type {PackOutcome} */
|
|
622
739
|
let outcome;
|
|
@@ -632,21 +749,27 @@ export async function prepareEngineeringPack(input) {
|
|
|
632
749
|
} catch {
|
|
633
750
|
outcome = { outcome: 'unavailable', reason: 'client_error' };
|
|
634
751
|
}
|
|
752
|
+
if (outcome.outcome === 'loaded' && input.hostSupported === false)
|
|
753
|
+
outcome = { outcome: 'unavailable', reason: `${client}_outdated` };
|
|
635
754
|
let pluginRoot = null;
|
|
755
|
+
/** @type {import('./codex-engineering.mjs').CodexEngineering | null} */
|
|
756
|
+
let codex = null;
|
|
636
757
|
if (outcome.outcome === 'loaded') {
|
|
637
758
|
try {
|
|
638
|
-
|
|
759
|
+
if (client === 'codex') codex = await materializeCodexPack(outcome.pack, input.directory);
|
|
760
|
+
else pluginRoot = await materializePack(outcome.pack, input.directory);
|
|
639
761
|
} catch {
|
|
762
|
+
codex = null;
|
|
640
763
|
await rm(join(input.directory, PACK_DIRECTORY), { recursive: true, force: true }).catch(
|
|
641
764
|
() => undefined,
|
|
642
765
|
);
|
|
643
766
|
outcome = { outcome: 'rejected', reason: 'materialize_failed' };
|
|
644
767
|
}
|
|
645
768
|
}
|
|
646
|
-
await atomicJson(join(input.root, LAST_LAUNCH_FILE), lastLaunchOf(outcome, locale)).catch(
|
|
769
|
+
await atomicJson(join(input.root, LAST_LAUNCH_FILE), lastLaunchOf(outcome, locale, client)).catch(
|
|
647
770
|
() => undefined,
|
|
648
771
|
);
|
|
649
|
-
return { pluginRoot, line: packNotice(outcome, locale) };
|
|
772
|
+
return { pluginRoot, codex, line: packNotice(outcome, locale) };
|
|
650
773
|
}
|
|
651
774
|
|
|
652
775
|
/**
|
package/dist/launch.mjs
CHANGED
|
@@ -18,6 +18,11 @@ import {
|
|
|
18
18
|
sweepStaleLaunches,
|
|
19
19
|
writeLaunchMarker,
|
|
20
20
|
} from './engineering-pack.mjs';
|
|
21
|
+
import {
|
|
22
|
+
CODEX_ENGINEERING_ENV,
|
|
23
|
+
CODEX_ENGINEERING_SKILLS_ROOT_ENV,
|
|
24
|
+
supportsCodexEngineering,
|
|
25
|
+
} from './codex-engineering.mjs';
|
|
21
26
|
import {
|
|
22
27
|
CODEX_LAUNCH_ENVELOPE,
|
|
23
28
|
CODEX_MONITOR_CWD,
|
|
@@ -132,6 +137,8 @@ function childEnvironment(root) {
|
|
|
132
137
|
delete environment.CODEX_CONTEXT_COMPACT_AT;
|
|
133
138
|
delete environment[CODEX_LAUNCH_ENVELOPE];
|
|
134
139
|
delete environment[CODEX_MONITOR_CWD];
|
|
140
|
+
delete environment[CODEX_ENGINEERING_ENV];
|
|
141
|
+
delete environment[CODEX_ENGINEERING_SKILLS_ROOT_ENV];
|
|
135
142
|
environment.AIBRAIN_STATE_DIR = resolve(root);
|
|
136
143
|
return environment;
|
|
137
144
|
}
|
|
@@ -602,8 +609,14 @@ export async function launchClaude(config, root, args, dependencies = {}) {
|
|
|
602
609
|
return finished;
|
|
603
610
|
}
|
|
604
611
|
|
|
605
|
-
/**
|
|
606
|
-
|
|
612
|
+
/**
|
|
613
|
+
* @param {LaunchConfig} config
|
|
614
|
+
* @param {string} root
|
|
615
|
+
* @param {string[]} args
|
|
616
|
+
* @param {LaunchDependencies} [dependencies]
|
|
617
|
+
*/
|
|
618
|
+
export async function launchCodex(config, root, args, dependencies = {}) {
|
|
619
|
+
const preparePack = dependencies.preparePack ?? prepareEngineeringPack;
|
|
607
620
|
const validated = assertConfig(config);
|
|
608
621
|
assertCodexArgs(args);
|
|
609
622
|
const captureLockPlatform = captureLockPlatformCheck();
|
|
@@ -654,6 +667,19 @@ export async function launchCodex(config, root, args) {
|
|
|
654
667
|
await rm(directory, { recursive: true, force: true });
|
|
655
668
|
throw error;
|
|
656
669
|
}
|
|
670
|
+
// The pack never blocks the launch: every failure starts Codex without it.
|
|
671
|
+
const engineering = await preparePack({
|
|
672
|
+
endpoint: validated.endpoint,
|
|
673
|
+
token: validated.token,
|
|
674
|
+
root,
|
|
675
|
+
directory,
|
|
676
|
+
packageRoot: validated.packageRoot,
|
|
677
|
+
client: 'codex',
|
|
678
|
+
hostSupported: supportsCodexEngineering(compatibility.version),
|
|
679
|
+
}).catch(() => ({ pluginRoot: null, codex: null, line: null }));
|
|
680
|
+
if (engineering.line !== null) process.stderr.write(`${engineering.line}\n`);
|
|
681
|
+
if (engineering.codex != null)
|
|
682
|
+
environment[CODEX_ENGINEERING_ENV] = JSON.stringify(engineering.codex);
|
|
657
683
|
environment.AI_BRAIN_MCP_SERVER = MCP_ALIAS;
|
|
658
684
|
environment.CODEX_BIN = executable;
|
|
659
685
|
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