@brunobracaioli/aibrain 0.4.0-beta.11 → 0.4.0-beta.13

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.
@@ -6,11 +6,18 @@
6
6
  * Node executable plus the dispatcher inside the installed package; nothing
7
7
  * comes from the server (ADR-0048).
8
8
  */
9
- import { access, mkdir, writeFile } from 'node:fs/promises';
9
+ import { access, mkdir, realpath, writeFile } from 'node:fs/promises';
10
10
  import { isAbsolute, join } from 'node:path';
11
11
  import { hookById } from './extensions-catalog.mjs';
12
12
  import { enabledHookIds, preferencesPath, readPreferences } from './extensions-preferences.mjs';
13
13
  import { readStoredLocale, resolvePackLocale } from './engineering-pack.mjs';
14
+ import {
15
+ codexHookConfigArgs,
16
+ codexHooksLine,
17
+ codexHooksValue,
18
+ summarizeCodexHooks,
19
+ } from './codex-hooks.mjs';
20
+ import { readCodexHookList } from './codex-permissions.mjs';
14
21
 
15
22
  export const GUARDRAILS_PLUGIN_NAME = 'aisac-guardrails';
16
23
  const PLUGIN_VERSION = '1.0.0';
@@ -147,3 +154,54 @@ export async function prepareGuardrails(input) {
147
154
  line: `AISAC Engineering: ${enabled.length} guardrail${enabled.length === 1 ? '' : 's'} on (aisac hooks to change them).`,
148
155
  };
149
156
  }
157
+
158
+ /**
159
+ * What Codex reports for the AISAC hooks the user has on, read with Codex's own
160
+ * `hooks/list` for the exact flags the monitor passes (spec FR6, E6b). Null when
161
+ * no Codex hook is enabled or the package lacks the dispatcher; `summary` is null
162
+ * when the listing failed.
163
+ * @param {{ root: string, packageRoot: string, executable: string, cwd: string, profile: string | undefined, environment: NodeJS.ProcessEnv }} input
164
+ * @param {{ listHooks?: typeof readCodexHookList }} [dependencies]
165
+ * @returns {Promise<{ enabled: string[], summary: import('./codex-hooks.mjs').CodexHooksSummary | null } | null>}
166
+ */
167
+ export async function readCodexGuardrails(input, dependencies = {}) {
168
+ const listHooks = dependencies.listHooks ?? readCodexHookList;
169
+ const enabled = enabledHookIds(await readPreferences(input.root)).filter(
170
+ (id) => hookById(id)?.clients.includes('codex') === true,
171
+ );
172
+ if (enabled.length === 0) return null;
173
+ // The monitor resolves the same file from its own folder; both use the real path.
174
+ const dispatcher = await realpath(join(input.packageRoot, DISPATCHER)).catch(() => null);
175
+ if (dispatcher === null || !safeAbsolute(dispatcher) || !safeAbsolute(process.execPath))
176
+ return null;
177
+ const paths = { node: process.execPath, dispatcher };
178
+ const summary = await listHooks({
179
+ executable: input.executable,
180
+ cwd: input.cwd,
181
+ profile: input.profile,
182
+ environment: input.environment,
183
+ configArgs: codexHookConfigArgs(paths),
184
+ }).then(
185
+ (listed) => summarizeCodexHooks(listed, paths, enabled),
186
+ () => null,
187
+ );
188
+ return { enabled, summary };
189
+ }
190
+
191
+ /**
192
+ * The guardrails for a licensed Codex launch: the value the launcher hands the
193
+ * monitor, and the launch line. The line comes from `hooks/list`, so a guardrail
194
+ * that waits for the user's review in `/hooks` is never called on.
195
+ * @param {{ root: string, packageRoot: string, executable: string, cwd: string, profile: string | undefined, environment: NodeJS.ProcessEnv }} input
196
+ * @param {{ listHooks?: typeof readCodexHookList }} [dependencies]
197
+ * @returns {Promise<{ value: string, line: string } | null>}
198
+ */
199
+ export async function prepareCodexGuardrails(input, dependencies = {}) {
200
+ const read = await readCodexGuardrails(input, dependencies);
201
+ if (read === null) return null;
202
+ const locale = resolvePackLocale(await readStoredLocale(input.root));
203
+ return {
204
+ value: codexHooksValue(read.enabled, locale),
205
+ line: codexHooksLine(read.enabled.length, read.summary),
206
+ };
207
+ }
package/dist/launch.mjs CHANGED
@@ -18,7 +18,8 @@ import {
18
18
  sweepStaleLaunches,
19
19
  writeLaunchMarker,
20
20
  } from './engineering-pack.mjs';
21
- import { prepareGuardrails } from './guardrails-plugin.mjs';
21
+ import { prepareCodexGuardrails, prepareGuardrails } from './guardrails-plugin.mjs';
22
+ import { CODEX_HOOKS_ENV, GUARDRAILS_SESSION_ENV, supportsCodexHooks } from './codex-hooks.mjs';
22
23
  import {
23
24
  CODEX_MCP_ENV,
24
25
  claudeDenyRules,
@@ -149,6 +150,8 @@ function childEnvironment(root) {
149
150
  delete environment[CODEX_ENGINEERING_ENV];
150
151
  delete environment[CODEX_ENGINEERING_SKILLS_ROOT_ENV];
151
152
  delete environment[CODEX_MCP_ENV];
153
+ delete environment[CODEX_HOOKS_ENV];
154
+ delete environment[GUARDRAILS_SESSION_ENV];
152
155
  environment.AIBRAIN_STATE_DIR = resolve(root);
153
156
  return environment;
154
157
  }
@@ -451,6 +454,7 @@ export async function readSessionServers(root, client, environment) {
451
454
  * @property {typeof prepareEngineeringPack} [preparePack] AISAC Engineering pack loader (E4b)
452
455
  * @property {typeof prepareGuardrails} [prepareGuardrails] Guardrail hooks plugin writer (E6a)
453
456
  * @property {typeof readSessionServers} [readSessionServers] MCP catalog servers to load (E6c)
457
+ * @property {typeof prepareCodexGuardrails} [prepareCodexGuardrails] Codex guardrail hooks (E6b)
454
458
  */
455
459
 
456
460
  /**
@@ -675,6 +679,7 @@ export async function launchClaude(config, root, args, dependencies = {}) {
675
679
  export async function launchCodex(config, root, args, dependencies = {}) {
676
680
  const preparePack = dependencies.preparePack ?? prepareEngineeringPack;
677
681
  const readServers = dependencies.readSessionServers ?? readSessionServers;
682
+ const writeCodexGuardrails = dependencies.prepareCodexGuardrails ?? prepareCodexGuardrails;
678
683
  const validated = assertConfig(config);
679
684
  assertCodexArgs(args);
680
685
  const captureLockPlatform = captureLockPlatformCheck();
@@ -744,6 +749,22 @@ export async function launchCodex(config, root, args, dependencies = {}) {
744
749
  process.stderr.write(`${line}\n`);
745
750
  if (servers !== null && servers.ready.length > 0)
746
751
  environment[CODEX_MCP_ENV] = JSON.stringify(servers.ready.map((entry) => entry.id));
752
+ // Guardrail hooks need the same license and a Codex measured with them
753
+ // (0.166.0+); like the pack they never block the launch (spec extensions FR6).
754
+ const guardrails = supportsCodexHooks(compatibility.version)
755
+ ? await writeCodexGuardrails({
756
+ root,
757
+ packageRoot,
758
+ executable,
759
+ cwd: launchPlan.effectiveCwd,
760
+ profile: launchPlan.profile,
761
+ environment,
762
+ }).catch(() => null)
763
+ : null;
764
+ if (guardrails !== null) {
765
+ environment[CODEX_HOOKS_ENV] = guardrails.value;
766
+ process.stderr.write(`${guardrails.line}\n`);
767
+ }
747
768
  }
748
769
  environment.AI_BRAIN_MCP_SERVER = MCP_ALIAS;
749
770
  environment.CODEX_BIN = executable;
package/dist/monitor.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // @ts-check
3
3
 
4
- import { access } from 'node:fs/promises';
4
+ import { access, realpath } from 'node:fs/promises';
5
5
  import { constants } from 'node:fs';
6
6
  import { spawn } from 'node:child_process';
7
7
  import { dirname, join } from 'node:path';
@@ -19,6 +19,13 @@ import {
19
19
  parseCodexEngineering,
20
20
  } from './codex-engineering.mjs';
21
21
  import { CODEX_MCP_ENV, codexMcpConfigArgs, parseCodexMcp } from './mcp-servers.mjs';
22
+ import {
23
+ CODEX_HOOKS_ENV,
24
+ GUARDRAILS_SESSION_ENV,
25
+ codexHookConfigArgs,
26
+ guardrailsSessionValue,
27
+ parseCodexHooks,
28
+ } from './codex-hooks.mjs';
22
29
 
23
30
  const root = dirname(fileURLToPath(import.meta.url));
24
31
  const endpoint = process.env.AIBRAIN_MCP_URL?.trim();
@@ -59,6 +66,35 @@ try {
59
66
  } catch {
60
67
  catalogServers = [];
61
68
  }
69
+ // Guardrail hooks (E6b) follow the pack as well, and need the state root the pack
70
+ // was checked against: their session value points at this launch's folder. The
71
+ // flags are the whole catalog with byte-stable commands (codex-hooks.mjs). A
72
+ // malformed value, or a package without the dispatcher, starts the session without
73
+ // them.
74
+ /** @type {{ args: string[], session: string } | null} */
75
+ let guardrails = null;
76
+ try {
77
+ const stateRoot = process.env.AIBRAIN_STATE_DIR;
78
+ const hooks =
79
+ engineering === null || stateRoot === undefined
80
+ ? null
81
+ : parseCodexHooks(process.env[CODEX_HOOKS_ENV]);
82
+ if (hooks !== null && engineering !== null && stateRoot !== undefined) {
83
+ // The real path, as the launcher's hooks/list probe uses (prepareCodexGuardrails).
84
+ const dispatcher = await realpath(join(root, 'guardrails', 'run.mjs'));
85
+ await access(dispatcher, constants.R_OK);
86
+ guardrails = {
87
+ args: codexHookConfigArgs({ node: process.execPath, dispatcher }),
88
+ session: guardrailsSessionValue(hooks, {
89
+ // The pack sits in <launch>/engineering/skills, inside <state>/runtime.
90
+ launchDirectory: dirname(dirname(engineering.skillsRoot)),
91
+ stateRoot,
92
+ }),
93
+ };
94
+ }
95
+ } catch {
96
+ guardrails = null;
97
+ }
62
98
  const policyText = process.env.CODEX_CONTEXT_POLICY_JSON;
63
99
  let policy;
64
100
  if (policyText === undefined) {
@@ -119,6 +155,11 @@ delete env[CODEX_APP_SERVER_PROFILE];
119
155
  delete env[CODEX_ENGINEERING_ENV];
120
156
  delete env[CODEX_ENGINEERING_SKILLS_ROOT_ENV];
121
157
  delete env[CODEX_MCP_ENV];
158
+ delete env[CODEX_HOOKS_ENV];
159
+ // Only this monitor sets the value the hook processes read; one inherited from an
160
+ // outer session never reaches the app-server.
161
+ delete env[GUARDRAILS_SESSION_ENV];
162
+ if (guardrails !== null) env[GUARDRAILS_SESSION_ENV] = guardrails.session;
122
163
  if (engineering !== null) env[CODEX_ENGINEERING_SKILLS_ROOT_ENV] = engineering.skillsRoot;
123
164
  if (launchEnvelope?.profile !== undefined) env[CODEX_APP_SERVER_PROFILE] = launchEnvelope.profile;
124
165
  if (policy !== null) env.CODEX_CONTEXT_POLICY_JSON = JSON.stringify(policy);
@@ -142,6 +183,7 @@ const child = spawn(
142
183
  ...(launchEnvelope === null ? [] : ownedAppServerConfigArgs(launchEnvelope)),
143
184
  ...(engineering === null ? [] : codexEngineeringConfigArgs(engineering)),
144
185
  ...codexMcpConfigArgs(catalogServers),
186
+ ...(guardrails === null ? [] : guardrails.args),
145
187
  ],
146
188
  { cwd: process.cwd(), env, stdio: 'inherit' },
147
189
  );
package/dist/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@brunobracaioli/aibrain",
3
- "version": "0.4.0-beta.11",
3
+ "version": "0.4.0-beta.13",
4
4
  "type": "module"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brunobracaioli/aibrain",
3
- "version": "0.4.0-beta.11",
3
+ "version": "0.4.0-beta.13",
4
4
  "private": false,
5
5
  "description": "AISAC client for Claude Code and Codex: AI Brain memory, context management and AISAC Engineering",
6
6
  "license": "UNLICENSED",