@addai/node 0.30.11 → 0.30.12

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.
@@ -25,14 +25,30 @@ export interface CodexInput {
25
25
  * Mirrors the claude --strict-mcp-config behavior. */
26
26
  mcpServers?: CodexMcpServer[];
27
27
  }
28
+ export interface CodexHomeOptions {
29
+ /** Forward a remote server's credentials as headers via env_http_headers.
30
+ * spawnCodex passes codexSupportsEnvHttpHeaders(bin), so a codex too old to
31
+ * know the key keeps exactly the config it had before. Default true. */
32
+ envHttpHeaders?: boolean;
33
+ }
28
34
  /** Build a config.toml that declares only the supplied MCP servers and
29
35
  * return the CODEX_HOME directory plus any env vars the child needs
30
- * (bearer tokens for remote servers). Caller is responsible for cleanup
31
- * (we drop it under os.tmpdir() so it gets reaped). */
32
- export declare function writeCodexHome(servers: CodexMcpServer[], workingDirectory: string): {
36
+ * (bearer tokens and header values for remote servers). Caller is
37
+ * responsible for cleanup (we drop it under os.tmpdir() so it gets reaped). */
38
+ export declare function writeCodexHome(servers: CodexMcpServer[], workingDirectory: string, opts?: CodexHomeOptions): {
33
39
  home: string;
34
40
  extraEnv: Record<string, string>;
35
41
  };
42
+ /**
43
+ * Does this codex know env_http_headers? Asked by having it parse a throwaway
44
+ * config (`codex mcp list --json`, which reads config and contacts nothing)
45
+ * and checking that the header survived. Remembered per binary. Any failure
46
+ * answers "no", which keeps the bearer-only config every install ran before.
47
+ */
48
+ export declare function codexSupportsEnvHttpHeaders(bin: string, probe?: (bin: string, codexHome: string) => string): boolean;
49
+ /** Read `codex mcp list --json` output: did the probe server's header survive?
50
+ * Exported for tests. */
51
+ export declare function codexListShowsEnvHeaders(stdout: string): boolean;
36
52
  /** First line of an AGENTS.md this daemon wrote. A project's own AGENTS.md
37
53
  * never carries it and is never touched. */
38
54
  export declare const AGENTS_MD_MARKER = "<!-- ainode: entity system prompt \u2014 rewritten before every turn; edits here do not persist -->";
@@ -42,6 +42,8 @@ var __importStar = (this && this.__importStar) || (function () {
42
42
  Object.defineProperty(exports, "__esModule", { value: true });
43
43
  exports.AGENTS_MD_MARKER = void 0;
44
44
  exports.writeCodexHome = writeCodexHome;
45
+ exports.codexSupportsEnvHttpHeaders = codexSupportsEnvHttpHeaders;
46
+ exports.codexListShowsEnvHeaders = codexListShowsEnvHeaders;
45
47
  exports.writeCodexAgentsMd = writeCodexAgentsMd;
46
48
  exports.codexEffort = codexEffort;
47
49
  exports.spawnCodex = spawnCodex;
@@ -55,6 +57,7 @@ const diskguard_1 = require("./diskguard");
55
57
  const win_1 = require("./win");
56
58
  const events_1 = require("./events");
57
59
  const prompt_transport_1 = require("./prompt-transport");
60
+ const mcp_headers_1 = require("./mcp-headers");
58
61
  /** Quote a string as a TOML basic string (double-quoted, escape backslash + quote). */
59
62
  function tomlString(s) {
60
63
  return '"' + String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n') + '"';
@@ -63,11 +66,17 @@ function tomlString(s) {
63
66
  * Only the NAME goes in config.toml — the value rides on the child env, so
64
67
  * the token never lands on disk. */
65
68
  const bearerEnvVar = (slug) => `AINODE_MCP_${slug.replace(/[^A-Za-z0-9]+/g, '_').toUpperCase()}_TOKEN`;
69
+ /** Env var name carrying one HTTP header's value into the codex process.
70
+ * config.toml references it by name via env_http_headers, so the value never
71
+ * lands on disk either. */
72
+ const headerEnvVar = (slug, header) => `AINODE_MCP_${slug.replace(/[^A-Za-z0-9]+/g, '_').toUpperCase()}` +
73
+ `_H_${header.replace(/[^A-Za-z0-9]+/g, '_').toUpperCase()}`;
66
74
  /** Build a config.toml that declares only the supplied MCP servers and
67
75
  * return the CODEX_HOME directory plus any env vars the child needs
68
- * (bearer tokens for remote servers). Caller is responsible for cleanup
69
- * (we drop it under os.tmpdir() so it gets reaped). */
70
- function writeCodexHome(servers, workingDirectory) {
76
+ * (bearer tokens and header values for remote servers). Caller is
77
+ * responsible for cleanup (we drop it under os.tmpdir() so it gets reaped). */
78
+ function writeCodexHome(servers, workingDirectory, opts = {}) {
79
+ const envHttpHeaders = opts.envHttpHeaders !== false;
71
80
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'entities-codex-'));
72
81
  const extraEnv = {};
73
82
  let toml = '# Generated by ainode — sandboxes codex against host config\n\n';
@@ -92,6 +101,8 @@ function writeCodexHome(servers, workingDirectory) {
92
101
  toml += `url = ${tomlString(url)}\n`;
93
102
  // codex reads the bearer from its OWN environment (--env is stdio-only),
94
103
  // so the value goes on the child env and only the var name in the file.
104
+ // The bearer is exactly what this adapter always sent, kept so anything
105
+ // that works on codex today keeps working.
95
106
  const creds = Object.entries(s.env || {})
96
107
  .filter(([k, v]) => typeof v === 'string' && v.length > 0 && !k.startsWith('OAUTH_REFRESH'));
97
108
  const bearer = creds.find(([k]) => k === 'OAUTH_ACCESS_TOKEN') ?? creds[0];
@@ -99,15 +110,34 @@ function writeCodexHome(servers, workingDirectory) {
99
110
  const varName = bearerEnvVar(s.slug);
100
111
  extraEnv[varName] = String(bearer[1]);
101
112
  toml += `bearer_token_env_var = ${tomlString(varName)}\n`;
102
- if (creds.length > 1) {
103
- // codex carries one bearer and no arbitrary headers. Anything that
104
- // authenticates with several custom headers is only partly wired,
105
- // and saying so beats a server that 401s for no visible reason.
106
- console.error(`[codex] MCP ${s.slug}: only ${bearer[0]} is forwarded as a bearer token; ` +
107
- `codex cannot send custom headers, so ${creds.length - 1} other credential ` +
108
- `field(s) were dropped`);
113
+ }
114
+ // The bearer alone was never enough. The servers read each credential
115
+ // from its own header (the claude path sends them all, see
116
+ // remoteMcpHeaders) and fall back to the bearer for any that is missing,
117
+ // so +Ai Drive, which carries three, used its API key as its workspace
118
+ // id and answered "Access denied to workspace" on every codex run.
119
+ // codex does send custom headers: env_http_headers maps a header name to
120
+ // the NAME of an env var, so the values stay off disk like the bearer.
121
+ const headers = (0, mcp_headers_1.remoteMcpHeaders)(s.env);
122
+ // An OAuth access token is already the bearer above.
123
+ delete headers.Authorization;
124
+ const entries = Object.entries(headers);
125
+ if (entries.length > 0 && envHttpHeaders) {
126
+ // A sub-table must follow the parent table's scalar keys.
127
+ toml += `[mcp_servers.${name}.env_http_headers]\n`;
128
+ for (const [header, value] of entries) {
129
+ const varName = headerEnvVar(s.slug, header);
130
+ extraEnv[varName] = value;
131
+ toml += `${tomlString(header)} = ${tomlString(varName)}\n`;
109
132
  }
110
133
  }
134
+ else if (creds.length > 1) {
135
+ // A codex too old to know env_http_headers carries the bearer only.
136
+ // Saying so beats a server that refuses for no visible reason.
137
+ console.error(`[codex] MCP ${s.slug}: this codex cannot send custom headers, so only ` +
138
+ `${bearer?.[0]} is forwarded and ${creds.length - 1} other credential ` +
139
+ `field(s) were dropped`);
140
+ }
111
141
  toml += '\n';
112
142
  continue;
113
143
  }
@@ -159,6 +189,76 @@ function writeCodexHome(servers, workingDirectory) {
159
189
  }
160
190
  return { home: dir, extraEnv };
161
191
  }
192
+ const PROBE_SERVER = 'ainode_probe';
193
+ const PROBE_HEADER = 'X-Ainode-Probe';
194
+ // A real answer holds until the binary might have been updated; a probe that
195
+ // could not run at all is retried sooner.
196
+ const PROBE_TTL_MS = 6 * 60 * 60 * 1000;
197
+ const PROBE_FAILED_TTL_MS = 10 * 60 * 1000;
198
+ const envHeaderSupport = new Map();
199
+ /**
200
+ * Does this codex know env_http_headers? Asked by having it parse a throwaway
201
+ * config (`codex mcp list --json`, which reads config and contacts nothing)
202
+ * and checking that the header survived. Remembered per binary. Any failure
203
+ * answers "no", which keeps the bearer-only config every install ran before.
204
+ */
205
+ function codexSupportsEnvHttpHeaders(bin, probe = codexMcpListJson) {
206
+ const hit = envHeaderSupport.get(bin);
207
+ if (hit && hit.until > Date.now())
208
+ return hit.ok;
209
+ let ok = false;
210
+ let failed = false;
211
+ let home = null;
212
+ try {
213
+ home = fs.mkdtempSync(path.join(os.tmpdir(), 'ainode-codex-probe-'));
214
+ fs.writeFileSync(path.join(home, 'config.toml'), `[mcp_servers.${PROBE_SERVER}]\n` +
215
+ `url = "http://127.0.0.1:9/mcp"\n` +
216
+ // A bearer keeps codex from looking for stored OAuth credentials.
217
+ `bearer_token_env_var = "AINODE_PROBE_TOKEN"\n` +
218
+ `[mcp_servers.${PROBE_SERVER}.env_http_headers]\n` +
219
+ `"${PROBE_HEADER}" = "AINODE_PROBE_HEADER"\n`, { mode: 0o600 });
220
+ ok = codexListShowsEnvHeaders(probe(bin, home));
221
+ }
222
+ catch {
223
+ ok = false;
224
+ failed = true;
225
+ }
226
+ finally {
227
+ if (home) {
228
+ try {
229
+ fs.rmSync(home, { recursive: true, force: true });
230
+ }
231
+ catch { /* reaped from tmp */ }
232
+ }
233
+ }
234
+ envHeaderSupport.set(bin, { ok, until: Date.now() + (failed ? PROBE_FAILED_TTL_MS : PROBE_TTL_MS) });
235
+ return ok;
236
+ }
237
+ /** Read `codex mcp list --json` output: did the probe server's header survive?
238
+ * Exported for tests. */
239
+ function codexListShowsEnvHeaders(stdout) {
240
+ const start = stdout.search(/^\s*\[/m);
241
+ if (start < 0)
242
+ return false;
243
+ let list;
244
+ try {
245
+ list = JSON.parse(stdout.slice(start));
246
+ }
247
+ catch {
248
+ return false;
249
+ }
250
+ if (!Array.isArray(list))
251
+ return false;
252
+ const entry = list.find((e) => e && e.name === PROBE_SERVER);
253
+ return entry?.transport?.env_http_headers?.[PROBE_HEADER] === 'AINODE_PROBE_HEADER';
254
+ }
255
+ function codexMcpListJson(bin, codexHome) {
256
+ const inv = (0, win_1.resolveCliInvocation)(bin, ['mcp', 'list', '--json']);
257
+ return (0, child_process_1.execFileSync)(inv.file, inv.args, {
258
+ encoding: 'utf8', timeout: 20_000, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'],
259
+ env: { ...process.env, CODEX_HOME: codexHome },
260
+ });
261
+ }
162
262
  /** First line of an AGENTS.md this daemon wrote. A project's own AGENTS.md
163
263
  * never carries it and is never touched. */
164
264
  exports.AGENTS_MD_MARKER = '<!-- ainode: entity system prompt — rewritten before every turn; edits here do not persist -->';
@@ -270,10 +370,19 @@ function spawnCodex(input) {
270
370
  const args = input.resumeThreadId
271
371
  ? ['exec', ...baseFlags, 'resume', input.resumeThreadId, input.prompt]
272
372
  : ['exec', ...baseFlags, input.prompt];
373
+ // Resolve binary path before spawn. Previously called spawn('codex', ...)
374
+ // bare, which threw ENOENT under launchd / desktop-app PATH inheritance
375
+ // and rejected `done` before any session_end event fired → request
376
+ // stuck at 'running' on the DB. Resolved first now, because the config
377
+ // below depends on what this codex understands.
378
+ const bin = (0, codex_binary_1.findCodexBinary)();
273
379
  // Sandbox codex against an isolated CODEX_HOME so the host's
274
380
  // ~/.codex/config.toml mcp_servers don't leak in. TERM=dumb keeps
275
381
  // codex's TTY heuristics happy (no fancy terminal output).
276
- const { home: codexHome, extraEnv: mcpBearerEnv } = writeCodexHome(input.mcpServers ?? [], input.workingDirectory);
382
+ const servers = input.mcpServers ?? [];
383
+ // Only a remote server that carries credentials needs the header probe.
384
+ const wantsHeaders = servers.some((s) => (0, mcp_headers_1.isRemoteMcp)(s.command) && Object.keys(s.env ?? {}).length > 0);
385
+ const { home: codexHome, extraEnv: mcpBearerEnv } = writeCodexHome(servers, input.workingDirectory, { envHttpHeaders: wantsHeaders && bin ? codexSupportsEnvHttpHeaders(bin) : false });
277
386
  // Register the scratch dir with DiskGuard: marks it active (so a
278
387
  // concurrent sweep won't evict a live run) and writes a PID lock so a
279
388
  // different runtime process can tell it's still owned. dispose(true)
@@ -284,15 +393,11 @@ function spawnCodex(input) {
284
393
  ...process.env,
285
394
  TERM: 'dumb',
286
395
  CODEX_HOME: codexHome,
287
- // Bearer tokens for remote (http/sse) MCP servers. config.toml names these
288
- // vars via bearer_token_env_var; the values only ever live here.
396
+ // Bearer tokens and header values for remote (http/sse) MCP servers.
397
+ // config.toml names these vars via bearer_token_env_var and
398
+ // env_http_headers; the values only ever live here.
289
399
  ...mcpBearerEnv,
290
400
  };
291
- // Resolve binary path before spawn. Previously called spawn('codex', ...)
292
- // bare, which threw ENOENT under launchd / desktop-app PATH inheritance
293
- // and rejected `done` before any session_end event fired → request
294
- // stuck at 'running' on the DB.
295
- const bin = (0, codex_binary_1.findCodexBinary)();
296
401
  const listeners = [];
297
402
  const emit = (e) => { for (const l of listeners)
298
403
  try {
package/package.json CHANGED
@@ -1,62 +1,62 @@
1
- {
2
- "name": "@addai/node",
3
- "version": "0.30.11",
4
- "description": "Daemon that pairs a machine with your +Ai account and runs Claude / Codex / Kimi / Gemini agents on its behalf. Reachable via Supabase from Vault, Entity Studio, or any other +Ai surface.",
5
- "license": "MIT",
6
- "keywords": [
7
- "addai",
8
- "entity-studio",
9
- "claude",
10
- "codex",
11
- "kimi",
12
- "gemini",
13
- "agent",
14
- "daemon",
15
- "supabase",
16
- "mcp"
17
- ],
18
- "repository": {
19
- "type": "git",
20
- "url": "git+https://github.com/just-AddAi/addai-entity-runtime.git"
21
- },
22
- "homepage": "https://github.com/just-AddAi/addai-entity-runtime#readme",
23
- "bugs": {
24
- "url": "https://github.com/just-AddAi/addai-entity-runtime/issues"
25
- },
26
- "type": "commonjs",
27
- "main": "dist/index.js",
28
- "bin": {
29
- "ainode": "dist/cli.js"
30
- },
31
- "publishConfig": {
32
- "access": "public"
33
- },
34
- "files": [
35
- "dist",
36
- "scripts",
37
- "README.md",
38
- "assets"
39
- ],
40
- "scripts": {
41
- "build": "tsc -p tsconfig.json && node scripts/copy-assets.js",
42
- "start": "node dist/cli.js",
43
- "dev": "tsc -p tsconfig.json && node dist/cli.js",
44
- "test": "npm run build && node --test test/*.test.mjs",
45
- "clean": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\"",
46
- "postinstall": "node scripts/fix-pty-helper.js",
47
- "prepublishOnly": "npm run build"
48
- },
49
- "engines": {
50
- "node": ">=18"
51
- },
52
- "dependencies": {
53
- "@addai/node-flows": "^1.0.0",
54
- "node-pty": "^1.1.0",
55
- "ws": "^8.21.3"
56
- },
57
- "devDependencies": {
58
- "@types/node": "^20.0.0",
59
- "@types/ws": "^8.18.1",
60
- "typescript": "^5.6.0"
61
- }
62
- }
1
+ {
2
+ "name": "@addai/node",
3
+ "version": "0.30.12",
4
+ "description": "Daemon that pairs a machine with your +Ai account and runs Claude / Codex / Kimi / Gemini agents on its behalf. Reachable via Supabase from Vault, Entity Studio, or any other +Ai surface.",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "addai",
8
+ "entity-studio",
9
+ "claude",
10
+ "codex",
11
+ "kimi",
12
+ "gemini",
13
+ "agent",
14
+ "daemon",
15
+ "supabase",
16
+ "mcp"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/just-AddAi/addai-entity-runtime.git"
21
+ },
22
+ "homepage": "https://github.com/just-AddAi/addai-entity-runtime#readme",
23
+ "bugs": {
24
+ "url": "https://github.com/just-AddAi/addai-entity-runtime/issues"
25
+ },
26
+ "type": "commonjs",
27
+ "main": "dist/index.js",
28
+ "bin": {
29
+ "ainode": "dist/cli.js"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "scripts",
37
+ "README.md",
38
+ "assets"
39
+ ],
40
+ "scripts": {
41
+ "build": "tsc -p tsconfig.json && node scripts/copy-assets.js",
42
+ "start": "node dist/cli.js",
43
+ "dev": "tsc -p tsconfig.json && node dist/cli.js",
44
+ "test": "npm run build && node --test test/*.test.mjs",
45
+ "clean": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\"",
46
+ "postinstall": "node scripts/fix-pty-helper.js",
47
+ "prepublishOnly": "npm run build"
48
+ },
49
+ "engines": {
50
+ "node": ">=18"
51
+ },
52
+ "dependencies": {
53
+ "@addai/node-flows": "^1.0.0",
54
+ "node-pty": "^1.1.0",
55
+ "ws": "^8.21.3"
56
+ },
57
+ "devDependencies": {
58
+ "@types/node": "^20.0.0",
59
+ "@types/ws": "^8.18.1",
60
+ "typescript": "^5.6.0"
61
+ }
62
+ }