@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.
- package/dist/codex-spawn.d.ts +19 -3
- package/dist/codex-spawn.js +123 -18
- package/package.json +62 -62
package/dist/codex-spawn.d.ts
CHANGED
|
@@ -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
|
|
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 -->";
|
package/dist/codex-spawn.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
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.
|
|
288
|
-
// vars via bearer_token_env_var
|
|
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.
|
|
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
|
+
}
|