@brunobracaioli/aibrain 0.4.0-beta.1 → 0.4.0-beta.11
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 +50 -6
- package/dist/assets/claude/hooks/context-compaction.ts +5 -1
- package/dist/assets/codex-context-compaction.mjs +243 -5
- package/dist/assets/jev-paid-pilot-terminal.mjs +6 -0
- package/dist/claude-observation.mjs +1 -1
- package/dist/cli.mjs +91 -5
- package/dist/codex-compatibility.mjs +52 -2
- package/dist/codex-engineering.mjs +165 -0
- package/dist/codex-protocol-fingerprint.json +10 -10
- package/dist/engineering-pack-public-key.json +10 -0
- package/dist/engineering-pack.mjs +845 -0
- package/dist/engineering.mjs +15 -11
- package/dist/extensions-catalog.mjs +310 -0
- package/dist/extensions-cli.mjs +486 -0
- package/dist/extensions-preferences.mjs +151 -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 +143 -8
- package/dist/mcp-servers.mjs +192 -0
- package/dist/monitor.mjs +33 -0
- package/dist/package.json +1 -1
- package/dist/platform.mjs +50 -10
- package/dist/token-guide.mjs +105 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# aisac
|
|
2
2
|
|
|
3
|
-
The AISAC client for Claude Code and Codex on Linux
|
|
3
|
+
The AISAC client for Claude Code and Codex on Linux, WSL and macOS, with
|
|
4
|
+
Node.js 22 or newer:
|
|
4
5
|
AI Brain memory, context management and, for AISAC Founding Cohort members,
|
|
5
6
|
AISAC Engineering.
|
|
6
7
|
|
|
@@ -14,8 +15,44 @@ both provide the `aisac` and `aibrain` commands. State stays in `~/.aibrain`.
|
|
|
14
15
|
shows the answer and sends the request only to the configured AI Brain address.
|
|
15
16
|
`aisac status` stays offline.
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
For licensed accounts, `aisac claude` downloads the AISAC Engineering pack for
|
|
19
|
+
the session, verifies its Ed25519 signature against the key pinned in this
|
|
20
|
+
release, and loads it as the `aisac-engineering` plugin from the session's
|
|
21
|
+
private folder, which is deleted on exit. Any failure starts Claude without the
|
|
22
|
+
pack. `aisac setup --lang pt-BR|en` picks the pack language without a network
|
|
23
|
+
call. The pack is never part of this npm package.
|
|
24
|
+
|
|
25
|
+
`aisac codex` (Codex 0.164.0 or later) loads the same verified pack for that
|
|
26
|
+
session only: its skills are added to the Codex app-server the launch owns
|
|
27
|
+
(call one with `$`, for example `$spec`) and its agents become session agent
|
|
28
|
+
roles. Nothing is written to `~/.codex`, `~/.agents` or the project, and the
|
|
29
|
+
session's folder is deleted on exit. An older Codex starts without the pack and
|
|
30
|
+
says `codex_outdated`.
|
|
31
|
+
|
|
32
|
+
Licensed `aisac claude` sessions also load guardrail hooks as the session plugin
|
|
33
|
+
`aisac-guardrails`.
|
|
34
|
+
- **Default on:** `secrets-guard` refuses secret files by name. Four more hooks
|
|
35
|
+
keep continuity: a repository snapshot at start and after a compaction, a
|
|
36
|
+
docs reminder, and the NOTES/spec/CHANGELOG pass.
|
|
37
|
+
- **Default off:** `git-guard`, `destructive-guard` and `format-on-edit`.
|
|
38
|
+
- **Changing them:** `aisac hooks` lists them, and `aisac hooks enable|disable
|
|
39
|
+
<id>` or `aisac hooks reset` changes the next session. The agent cannot change
|
|
40
|
+
them from inside a session.
|
|
41
|
+
- **Code:** it ships in this package and runs locally. Nothing comes from the
|
|
42
|
+
server.
|
|
43
|
+
|
|
44
|
+
Licensed sessions of `aisac claude` and `aisac codex` can also load reviewed MCP
|
|
45
|
+
servers: `github` (read-only, token in `GITHUB_PERSONAL_ACCESS_TOKEN`), `supabase`
|
|
46
|
+
(read-only), `vercel`, `playwright` (pinned, isolated) and `context7`.
|
|
47
|
+
- **Default:** every server is off. `aisac mcp` lists them, `aisac mcp enable|disable
|
|
48
|
+
<id>` or `aisac mcp reset` changes the next session, and `aisac mcp info <id>` says
|
|
49
|
+
what a server reaches and how to log in.
|
|
50
|
+
- **Login:** the host does it (`/mcp` in Claude Code, `aisac mcp login <id>` for Codex)
|
|
51
|
+
and keeps it in its own store. No token reaches `~/.aibrain` or AISAC.
|
|
52
|
+
|
|
53
|
+
On Linux/WSL, Codex capture also requires the `flock` utility from `util-linux`,
|
|
54
|
+
normally included. On macOS it uses `/usr/bin/perl`, `/usr/sbin/sysctl` and
|
|
55
|
+
`/bin/ps` instead. The launcher and doctor check availability before a
|
|
19
56
|
monitored Codex session starts. Capture coordination is released by the kernel
|
|
20
57
|
after a process exits unexpectedly. An unidentifiable empty lock left by an
|
|
21
58
|
older client requires a verified support migration; do not delete checkpoints,
|
|
@@ -23,7 +60,7 @@ notes or lock files to work around a reported storage error.
|
|
|
23
60
|
|
|
24
61
|
The reviewed native compatibility set is Claude Code 2.1.294, 2.1.295, 2.1.296,
|
|
25
62
|
2.1.299, 2.1.300, 2.1.301, 2.1.302 or 2.1.303, and Codex CLI 0.161.0,
|
|
26
|
-
0.162.0, 0.162.1, 0.163.0 or 0.
|
|
63
|
+
0.162.0, 0.162.1, 0.163.0, 0.164.0 or 0.166.0 (0.165.0 was never qualified). A newer stable Codex 0.x release may continue only
|
|
27
64
|
after its locally generated experimental protocol schemas and transport flags
|
|
28
65
|
match the bundled adapter contract; prereleases, older releases and new majors
|
|
29
66
|
remain rejected. Starting with beta14, newer stable Claude 2.1 patches can
|
|
@@ -39,6 +76,11 @@ record formats. Native launch/resume qualification is separate from a new
|
|
|
39
76
|
model/JEV compaction qualification. A later native release that changes the
|
|
40
77
|
consumed contract can still require an AI Brain compatibility update.
|
|
41
78
|
|
|
79
|
+
`0.4.0-beta.9` refreshes the bundled contract to Codex 0.166.0. Its delta is
|
|
80
|
+
additive: optional item timing, an image generation id, an attach-to-source fork
|
|
81
|
+
flag and two error codes. See the
|
|
82
|
+
[Codex 0.166 qualification record](../../docs/reference/codex-166-compatibility-2026-11-04.md).
|
|
83
|
+
|
|
42
84
|
The Codex terminal bridge permits the native session picker's separate read-only
|
|
43
85
|
connection while keeping the primary conversation and monitor intact. A picker
|
|
44
86
|
cannot compact, start a turn, inject history or take ownership of the primary
|
|
@@ -61,8 +103,10 @@ Installation does not contact AI Brain, change native client configuration, or
|
|
|
61
103
|
enable all paid accounts. Setup requires an existing AI Brain PAT, explicit
|
|
62
104
|
versioned disclosure, and successful provider-free pilot access. Claude and
|
|
63
105
|
Codex continue to own their own authentication. Windows native is unsupported;
|
|
64
|
-
run the beta inside WSL.
|
|
65
|
-
|
|
106
|
+
run the beta inside WSL. On macOS, Codex capture locks with the system
|
|
107
|
+
`/usr/bin/perl` (`flock(2)`) and identifies the lock owner with `sysctl` and
|
|
108
|
+
`ps`, so nothing extra needs installing. Pricing and general customer
|
|
109
|
+
activation are outside this beta.
|
|
66
110
|
|
|
67
111
|
For reviewed noninteractive automation only, use a private token file and
|
|
68
112
|
explicitly acknowledge the displayed disclosure with
|
|
@@ -151,7 +151,7 @@ type Register = (
|
|
|
151
151
|
) => void;
|
|
152
152
|
|
|
153
153
|
const MAX_RESULT_BYTES = 256 * 1024;
|
|
154
|
-
const CLAUDE_HOOK_VERSION = '0.4.0-beta.
|
|
154
|
+
const CLAUDE_HOOK_VERSION = '0.4.0-beta.7';
|
|
155
155
|
const MAX_DIAGNOSTIC_BYTE_COUNT = 128 * 1024 * 1024;
|
|
156
156
|
const COOLDOWN_MS = 60_000;
|
|
157
157
|
const MAX_TRUNCATED_CODE_POINTS = 300;
|
|
@@ -463,6 +463,10 @@ function v2WireOptions(options: V2Options): Record<string, unknown> {
|
|
|
463
463
|
|
|
464
464
|
function safeErrorCategory(error: unknown): string {
|
|
465
465
|
const record = isRecord(error) ? error : {};
|
|
466
|
+
// A coded error names its own reason; the keyword guess below would read
|
|
467
|
+
// "AI Brain plan does not meet the minimum estimated reduction" as invalid_plan.
|
|
468
|
+
if (COMPACT_ERROR_CODES.includes(record.compactErrorCode as CompactErrorCode))
|
|
469
|
+
return record.compactErrorCode as CompactErrorCode;
|
|
466
470
|
const code = typeof record.code === 'string' ? record.code.toLowerCase() : '';
|
|
467
471
|
const name = typeof record.name === 'string' ? record.name.toLowerCase() : '';
|
|
468
472
|
const message = typeof record.message === 'string' ? record.message.toLowerCase() : '';
|
|
@@ -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;
|
|
@@ -1001,6 +1007,128 @@ async function processIdentity(pid) {
|
|
|
1001
1007
|
}
|
|
1002
1008
|
}
|
|
1003
1009
|
|
|
1010
|
+
// macOS has flock(2) but no util-linux helper and no /proc (ADR-0049, spec
|
|
1011
|
+
// codex-macos). The guard helper and the owner identity use fixed system paths:
|
|
1012
|
+
// never PATH lookups, never a shell.
|
|
1013
|
+
const DARWIN_CAPTURE_TOOLS = Object.freeze({
|
|
1014
|
+
perl: '/usr/bin/perl',
|
|
1015
|
+
sysctl: '/usr/sbin/sysctl',
|
|
1016
|
+
ps: '/bin/ps',
|
|
1017
|
+
});
|
|
1018
|
+
const DARWIN_FLOCK_ARGS = Object.freeze([
|
|
1019
|
+
'-MFcntl=:flock',
|
|
1020
|
+
'-e',
|
|
1021
|
+
'open(my $f, "+<&=", 3) or exit 2; exit(flock($f, LOCK_EX | LOCK_NB) ? 0 : 1)',
|
|
1022
|
+
]);
|
|
1023
|
+
const DARWIN_TOOL_ENV = Object.freeze({ LC_ALL: 'C', TZ: 'UTC', PATH: '/usr/bin:/bin:/usr/sbin' });
|
|
1024
|
+
const DARWIN_TOOL_TIMEOUT_MS = 2_000;
|
|
1025
|
+
const DARWIN_TOOL_MAX_OUTPUT_BYTES = 4096;
|
|
1026
|
+
const DARWIN_START_TIME = /^[\x20-\x7e]{1,64}$/u;
|
|
1027
|
+
const BOOT_SESSION = /^[a-f0-9-]{36}$/iu;
|
|
1028
|
+
const PS_NO_MATCHING_PROCESS = 1;
|
|
1029
|
+
|
|
1030
|
+
/** Platform and tool paths of the capture lock; replaced only by tests. */
|
|
1031
|
+
let captureLockHost = { platform: process.platform, tools: DARWIN_CAPTURE_TOOLS };
|
|
1032
|
+
|
|
1033
|
+
export function __testSetCaptureLockHost(host) {
|
|
1034
|
+
const previous = captureLockHost;
|
|
1035
|
+
captureLockHost = host;
|
|
1036
|
+
return previous;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function guardHelperCommand() {
|
|
1040
|
+
return captureLockHost.platform === 'darwin'
|
|
1041
|
+
? [captureLockHost.tools.perl, [...DARWIN_FLOCK_ARGS]]
|
|
1042
|
+
: ['flock', ['-x', '-n', '3']];
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/** @returns {Promise<{ code: number | null, stdout: string }>} */
|
|
1046
|
+
function runDarwinTool(command, args) {
|
|
1047
|
+
return new Promise((resolvePromise) => {
|
|
1048
|
+
const child = spawn(command, args, {
|
|
1049
|
+
env: DARWIN_TOOL_ENV,
|
|
1050
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1051
|
+
});
|
|
1052
|
+
let stdout = '';
|
|
1053
|
+
let settled = false;
|
|
1054
|
+
const finish = (code) => {
|
|
1055
|
+
if (settled) return;
|
|
1056
|
+
settled = true;
|
|
1057
|
+
clearTimeout(timer);
|
|
1058
|
+
resolvePromise({ code, stdout });
|
|
1059
|
+
};
|
|
1060
|
+
const timer = setTimeout(() => {
|
|
1061
|
+
child.kill('SIGKILL');
|
|
1062
|
+
finish(null);
|
|
1063
|
+
}, DARWIN_TOOL_TIMEOUT_MS);
|
|
1064
|
+
child.stdout.setEncoding('utf8');
|
|
1065
|
+
child.stdout.on('data', (chunk) => {
|
|
1066
|
+
stdout += chunk;
|
|
1067
|
+
if (Buffer.byteLength(stdout) > DARWIN_TOOL_MAX_OUTPUT_BYTES) {
|
|
1068
|
+
child.kill('SIGKILL');
|
|
1069
|
+
finish(null);
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
child.once('error', () => finish(null));
|
|
1073
|
+
child.once('close', (code) => finish(code));
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
async function darwinBootSession() {
|
|
1078
|
+
const result = await runDarwinTool(captureLockHost.tools.sysctl, ['-n', 'kern.bootsessionuuid']);
|
|
1079
|
+
const value = result.stdout.trim();
|
|
1080
|
+
return result.code === 0 && BOOT_SESSION.test(value) ? value.toUpperCase() : null;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
async function darwinProcessIdentity(pid) {
|
|
1084
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return { status: 'invalid' };
|
|
1085
|
+
const bootSession = await darwinBootSession();
|
|
1086
|
+
if (bootSession === null) return { status: 'unverifiable' };
|
|
1087
|
+
const result = await runDarwinTool(captureLockHost.tools.ps, [
|
|
1088
|
+
'-o',
|
|
1089
|
+
'lstart=',
|
|
1090
|
+
'-p',
|
|
1091
|
+
String(pid),
|
|
1092
|
+
]);
|
|
1093
|
+
const startTime = result.stdout.trim();
|
|
1094
|
+
// BSD ps exits 1 with no output when no process matches the pid.
|
|
1095
|
+
if (result.code === PS_NO_MATCHING_PROCESS && startTime === '') return { status: 'dead' };
|
|
1096
|
+
if (result.code !== 0 || !DARWIN_START_TIME.test(startTime)) return { status: 'unverifiable' };
|
|
1097
|
+
return { status: 'live', startTime, bootSession };
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
async function darwinSentinelOwner(value) {
|
|
1101
|
+
if (
|
|
1102
|
+
!isRecord(value) ||
|
|
1103
|
+
!hasOnlyKeys(value, [
|
|
1104
|
+
'schema',
|
|
1105
|
+
'backend',
|
|
1106
|
+
'platform',
|
|
1107
|
+
'pid',
|
|
1108
|
+
'startTime',
|
|
1109
|
+
'bootSession',
|
|
1110
|
+
'nonce',
|
|
1111
|
+
]) ||
|
|
1112
|
+
value.schema !== 1 ||
|
|
1113
|
+
value.backend !== 'flock' ||
|
|
1114
|
+
value.platform !== 'darwin' ||
|
|
1115
|
+
!isSafeBoundedInteger(value.pid, 1, 4_000_000_000) ||
|
|
1116
|
+
typeof value.startTime !== 'string' ||
|
|
1117
|
+
!DARWIN_START_TIME.test(value.startTime) ||
|
|
1118
|
+
typeof value.bootSession !== 'string' ||
|
|
1119
|
+
!BOOT_SESSION.test(value.bootSession) ||
|
|
1120
|
+
typeof value.nonce !== 'string' ||
|
|
1121
|
+
!isUuid(value.nonce)
|
|
1122
|
+
)
|
|
1123
|
+
return { status: 'unverifiable' };
|
|
1124
|
+
const observer = await darwinBootSession();
|
|
1125
|
+
if (observer === null) return { status: 'unverifiable' };
|
|
1126
|
+
if (observer !== value.bootSession.toUpperCase()) return { status: 'dead' };
|
|
1127
|
+
const current = await darwinProcessIdentity(value.pid);
|
|
1128
|
+
if (current.status !== 'live') return { status: current.status };
|
|
1129
|
+
return current.startTime === value.startTime ? { status: 'live' } : { status: 'dead' };
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1004
1132
|
async function captureSentinelOwner(lockPath, fsOps) {
|
|
1005
1133
|
let info;
|
|
1006
1134
|
try {
|
|
@@ -1019,6 +1147,7 @@ async function captureSentinelOwner(lockPath, fsOps) {
|
|
|
1019
1147
|
} catch {
|
|
1020
1148
|
return { status: 'unverifiable' };
|
|
1021
1149
|
}
|
|
1150
|
+
if (captureLockHost.platform === 'darwin') return darwinSentinelOwner(value);
|
|
1022
1151
|
if (
|
|
1023
1152
|
!isRecord(value) ||
|
|
1024
1153
|
!hasOnlyKeys(value, [
|
|
@@ -1054,10 +1183,23 @@ async function captureSentinelOwner(lockPath, fsOps) {
|
|
|
1054
1183
|
return current.startTime === value.startTime ? { status: 'live' } : { status: 'dead' };
|
|
1055
1184
|
}
|
|
1056
1185
|
|
|
1057
|
-
async function
|
|
1186
|
+
async function ownSentinelRecord() {
|
|
1187
|
+
if (captureLockHost.platform === 'darwin') {
|
|
1188
|
+
const identity = await darwinProcessIdentity(process.pid);
|
|
1189
|
+
if (identity.status !== 'live') throw captureLockError('capture_lock_owner_unverifiable');
|
|
1190
|
+
return {
|
|
1191
|
+
schema: 1,
|
|
1192
|
+
backend: 'flock',
|
|
1193
|
+
platform: 'darwin',
|
|
1194
|
+
pid: process.pid,
|
|
1195
|
+
startTime: identity.startTime,
|
|
1196
|
+
bootSession: identity.bootSession,
|
|
1197
|
+
nonce: randomUUID(),
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1058
1200
|
const identity = await processIdentity(process.pid);
|
|
1059
1201
|
if (identity.status !== 'live') throw captureLockError('capture_lock_owner_unverifiable');
|
|
1060
|
-
|
|
1202
|
+
return {
|
|
1061
1203
|
schema: 1,
|
|
1062
1204
|
backend: 'flock',
|
|
1063
1205
|
pid: process.pid,
|
|
@@ -1065,7 +1207,11 @@ async function ensureCaptureSentinel(lockPath, task) {
|
|
|
1065
1207
|
bootId: identity.bootId,
|
|
1066
1208
|
pidNamespace: identity.pidNamespace,
|
|
1067
1209
|
nonce: randomUUID(),
|
|
1068
|
-
}
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
async function ensureCaptureSentinel(lockPath, task) {
|
|
1214
|
+
const metadata = `${JSON.stringify(await ownSentinelRecord())}\n`;
|
|
1069
1215
|
while (true) {
|
|
1070
1216
|
const temporary = `${lockPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
1071
1217
|
let contention = false;
|
|
@@ -1143,7 +1289,8 @@ async function withAdvisoryCaptureLock(directory, task) {
|
|
|
1143
1289
|
while (true) {
|
|
1144
1290
|
let helper;
|
|
1145
1291
|
try {
|
|
1146
|
-
|
|
1292
|
+
const [command, args] = guardHelperCommand();
|
|
1293
|
+
helper = spawn(command, args, {
|
|
1147
1294
|
stdio: ['ignore', 'ignore', 'ignore', guardHandle.fd],
|
|
1148
1295
|
});
|
|
1149
1296
|
const acquired = await new Promise((resolvePromise, rejectPromise) => {
|
|
@@ -2707,6 +2854,8 @@ export function createCodexContextProxy({
|
|
|
2707
2854
|
createDiagnosticSink = null,
|
|
2708
2855
|
brainServer = process.env.AI_BRAIN_MCP_SERVER ?? 'brain',
|
|
2709
2856
|
clientVersion = process.env.CODEX_CLIENT_VERSION ?? '0.161.0',
|
|
2857
|
+
engineeringSkillsRoot = null,
|
|
2858
|
+
engineeringAckTimeoutMs = ENGINEERING_SKILLS_ACK_TIMEOUT_MS,
|
|
2710
2859
|
}) {
|
|
2711
2860
|
const contextPolicy = normalizeContextPolicy(policy);
|
|
2712
2861
|
const effectiveThreshold = contextPolicy?.compactAtPercent ?? threshold;
|
|
@@ -2719,6 +2868,15 @@ export function createCodexContextProxy({
|
|
|
2719
2868
|
const telemetryByThread = new Map();
|
|
2720
2869
|
const diagnosticSinks = new Map();
|
|
2721
2870
|
let stopped = false;
|
|
2871
|
+
// AISAC Engineering skills root: 'off' without a root; 'waiting' for the
|
|
2872
|
+
// client's first successful initialize; 'holding' app-server output until
|
|
2873
|
+
// skills/extraRoots/set is answered (or times out); then 'done'.
|
|
2874
|
+
let engineeringState =
|
|
2875
|
+
typeof engineeringSkillsRoot === 'string' && engineeringSkillsRoot.length > 0
|
|
2876
|
+
? 'waiting'
|
|
2877
|
+
: 'off';
|
|
2878
|
+
const initializeRequestKeys = new Set();
|
|
2879
|
+
let engineeringHeld = null;
|
|
2722
2880
|
function emitDiagnostic(event) {
|
|
2723
2881
|
try {
|
|
2724
2882
|
const phase = typeof event?.phase === 'string' ? event.phase : '';
|
|
@@ -4131,6 +4289,11 @@ export function createCodexContextProxy({
|
|
|
4131
4289
|
const owned = ownedRequests.get(message.id);
|
|
4132
4290
|
ownedRequests.delete(message.id);
|
|
4133
4291
|
retire(message.id);
|
|
4292
|
+
if (owned.kind === 'engineering') {
|
|
4293
|
+
// An error leaves the session without the pack skills; it is never shown.
|
|
4294
|
+
releaseEngineering();
|
|
4295
|
+
return false;
|
|
4296
|
+
}
|
|
4134
4297
|
if (
|
|
4135
4298
|
owned.kind === 'read' ||
|
|
4136
4299
|
owned.kind === 'page' ||
|
|
@@ -4364,6 +4527,14 @@ export function createCodexContextProxy({
|
|
|
4364
4527
|
if (!validRequestId) return;
|
|
4365
4528
|
const requestKey = clientRequestKey(requestId);
|
|
4366
4529
|
if (requestKey === null) return;
|
|
4530
|
+
if (
|
|
4531
|
+
message.method === 'initialize' &&
|
|
4532
|
+
engineeringState === 'waiting' &&
|
|
4533
|
+
initializeRequestKeys.size < MAX_TRACKED_INITIALIZE_REQUESTS
|
|
4534
|
+
) {
|
|
4535
|
+
initializeRequestKeys.add(requestKey);
|
|
4536
|
+
return;
|
|
4537
|
+
}
|
|
4367
4538
|
if (['thread/start', 'thread/resume', 'thread/fork'].includes(message.method)) {
|
|
4368
4539
|
const threadId =
|
|
4369
4540
|
isRecord(message.params) && typeof message.params.threadId === 'string'
|
|
@@ -4428,7 +4599,7 @@ export function createCodexContextProxy({
|
|
|
4428
4599
|
let passthrough = false;
|
|
4429
4600
|
let discardOversized = false;
|
|
4430
4601
|
let pausedForBackpressure = false;
|
|
4431
|
-
const
|
|
4602
|
+
const writeOut = (text) => {
|
|
4432
4603
|
if (!stdout.write(text) && !pausedForBackpressure) {
|
|
4433
4604
|
pausedForBackpressure = true;
|
|
4434
4605
|
child.stdout.pause();
|
|
@@ -4438,6 +4609,54 @@ export function createCodexContextProxy({
|
|
|
4438
4609
|
});
|
|
4439
4610
|
}
|
|
4440
4611
|
};
|
|
4612
|
+
const forward = (text) => {
|
|
4613
|
+
if (engineeringHeld === null) {
|
|
4614
|
+
writeOut(text);
|
|
4615
|
+
return;
|
|
4616
|
+
}
|
|
4617
|
+
engineeringHeld.lines.push(text);
|
|
4618
|
+
engineeringHeld.bytes += Buffer.byteLength(text, 'utf8');
|
|
4619
|
+
if (engineeringHeld.bytes > MAX_LINE_BYTES) releaseEngineering();
|
|
4620
|
+
};
|
|
4621
|
+
/** The client's first successful initialize answer, which starts the hold. */
|
|
4622
|
+
function isEngineeringTrigger(message) {
|
|
4623
|
+
if (engineeringState !== 'waiting' || !isRecord(message)) return false;
|
|
4624
|
+
if (message.method !== undefined || !('result' in message)) return false;
|
|
4625
|
+
const key = clientRequestKey(message.id);
|
|
4626
|
+
return key !== null && initializeRequestKeys.has(key);
|
|
4627
|
+
}
|
|
4628
|
+
function holdForEngineering(text) {
|
|
4629
|
+
engineeringState = 'holding';
|
|
4630
|
+
initializeRequestKeys.clear();
|
|
4631
|
+
engineeringHeld = {
|
|
4632
|
+
lines: [text],
|
|
4633
|
+
bytes: Buffer.byteLength(text, 'utf8'),
|
|
4634
|
+
id: null,
|
|
4635
|
+
cancel: null,
|
|
4636
|
+
};
|
|
4637
|
+
try {
|
|
4638
|
+
engineeringHeld.id = sendOwned(
|
|
4639
|
+
'skills/extraRoots/set',
|
|
4640
|
+
{ extraRoots: [engineeringSkillsRoot] },
|
|
4641
|
+
'engineering',
|
|
4642
|
+
null,
|
|
4643
|
+
);
|
|
4644
|
+
} catch {
|
|
4645
|
+
releaseEngineering();
|
|
4646
|
+
return;
|
|
4647
|
+
}
|
|
4648
|
+
engineeringHeld.cancel = scheduleTimeout(() => releaseEngineering(), engineeringAckTimeoutMs);
|
|
4649
|
+
}
|
|
4650
|
+
/** Forward everything held, in order. Idempotent; the session never waits on the pack. */
|
|
4651
|
+
function releaseEngineering() {
|
|
4652
|
+
if (engineeringHeld === null) return;
|
|
4653
|
+
const held = engineeringHeld;
|
|
4654
|
+
engineeringHeld = null;
|
|
4655
|
+
engineeringState = 'done';
|
|
4656
|
+
held.cancel?.();
|
|
4657
|
+
if (held.id !== null && ownedRequests.has(held.id)) cleanupRequest(held.id);
|
|
4658
|
+
for (const text of held.lines) writeOut(text);
|
|
4659
|
+
}
|
|
4441
4660
|
const processLine = (line) => {
|
|
4442
4661
|
if (Buffer.byteLength(line, 'utf8') + 1 > MAX_LINE_BYTES) {
|
|
4443
4662
|
const ownedId = internalRequestId(line);
|
|
@@ -4451,6 +4670,10 @@ export function createCodexContextProxy({
|
|
|
4451
4670
|
try {
|
|
4452
4671
|
const message = JSON.parse(line.endsWith('\r') ? line.slice(0, -1) : line);
|
|
4453
4672
|
observeClientResponse(message);
|
|
4673
|
+
if (isEngineeringTrigger(message)) {
|
|
4674
|
+
holdForEngineering(`${line}\n`);
|
|
4675
|
+
return;
|
|
4676
|
+
}
|
|
4454
4677
|
forwardLine = handle(message);
|
|
4455
4678
|
} catch {
|
|
4456
4679
|
/* preserve malformed external protocol lines */
|
|
@@ -4536,6 +4759,7 @@ export function createCodexContextProxy({
|
|
|
4536
4759
|
}
|
|
4537
4760
|
});
|
|
4538
4761
|
child.stdout.on('end', () => {
|
|
4762
|
+
releaseEngineering();
|
|
4539
4763
|
const tail = inputBuffer + decoder.end();
|
|
4540
4764
|
if (tail && !discardOversized) forward(tail);
|
|
4541
4765
|
inputBuffer = '';
|
|
@@ -4565,6 +4789,7 @@ export function createCodexContextProxy({
|
|
|
4565
4789
|
observeClientRequest,
|
|
4566
4790
|
observeClientResponse,
|
|
4567
4791
|
async stop() {
|
|
4792
|
+
releaseEngineering();
|
|
4568
4793
|
if (stopped) {
|
|
4569
4794
|
await Promise.all(
|
|
4570
4795
|
[...diagnosticSinks.values()].map((sink) =>
|
|
@@ -4594,6 +4819,18 @@ export function createCodexContextProxy({
|
|
|
4594
4819
|
};
|
|
4595
4820
|
}
|
|
4596
4821
|
|
|
4822
|
+
/**
|
|
4823
|
+
* The launcher's AISAC Engineering skills root, or null. Only an absolute,
|
|
4824
|
+
* normalized path without control characters is used.
|
|
4825
|
+
*/
|
|
4826
|
+
export function engineeringSkillsRootFrom(value) {
|
|
4827
|
+
if (typeof value !== 'string' || value.length === 0) return null;
|
|
4828
|
+
if (value.length > MAX_ENGINEERING_PATH_LENGTH || /[\u0000-\u001f\u007f]/u.test(value))
|
|
4829
|
+
return null;
|
|
4830
|
+
if (!value.startsWith('/') || resolve(value) !== value) return null;
|
|
4831
|
+
return value;
|
|
4832
|
+
}
|
|
4833
|
+
|
|
4597
4834
|
async function main() {
|
|
4598
4835
|
if (process.argv.includes('--help')) {
|
|
4599
4836
|
process.stderr.write(
|
|
@@ -4648,6 +4885,7 @@ async function main() {
|
|
|
4648
4885
|
timeoutMs: numberEnv('CODEX_CONTEXT_COMPACT_TIMEOUT_MS', DEFAULT_TIMEOUT_MS),
|
|
4649
4886
|
policy,
|
|
4650
4887
|
createDiagnosticSink: diagnosticFactory,
|
|
4888
|
+
engineeringSkillsRoot: engineeringSkillsRootFrom(process.env[ENGINEERING_SKILLS_ROOT_ENV]),
|
|
4651
4889
|
});
|
|
4652
4890
|
const clientDecoder = new StringDecoder('utf8');
|
|
4653
4891
|
let clientInputBuffer = '';
|
|
@@ -20,6 +20,8 @@ const MAX_AUXILIARY_IN_FLIGHT = 32;
|
|
|
20
20
|
const MAX_AUXILIARY_NOTIFICATIONS = 256;
|
|
21
21
|
const DEFAULT_AUXILIARY_IDLE_TIMEOUT_MS = 120_000;
|
|
22
22
|
const NORMAL_DISCOVERY_CLOSE_CODES = new Set([1000, 1001, 1005]);
|
|
23
|
+
// Smallest sun_path among supported hosts: macOS 104 bytes, minus the terminator.
|
|
24
|
+
const UNIX_SOCKET_PATH_MAX_BYTES = 103;
|
|
23
25
|
|
|
24
26
|
function repoRoot() {
|
|
25
27
|
return fileURLToPath(new URL('..', import.meta.url)).replace(/\/$/u, '');
|
|
@@ -1022,6 +1024,10 @@ async function main() {
|
|
|
1022
1024
|
});
|
|
1023
1025
|
});
|
|
1024
1026
|
try {
|
|
1027
|
+
// macOS sun_path holds 104 bytes including the terminator (Linux 108); refuse a
|
|
1028
|
+
// path that the kernel would truncate or reject instead of binding elsewhere.
|
|
1029
|
+
if (Buffer.byteLength(socketPath) > UNIX_SOCKET_PATH_MAX_BYTES)
|
|
1030
|
+
throw new Error('terminal_bridge_socket_path_too_long');
|
|
1025
1031
|
await new Promise((resolve, reject) => {
|
|
1026
1032
|
server.once('error', reject);
|
|
1027
1033
|
server.listen(socketPath, async () => {
|
|
@@ -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.11';
|
|
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']);
|