@ai-sdk/harness-codex 1.0.10 → 1.0.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/CHANGELOG.md +13 -0
- package/dist/bridge/host-tool-mcp.mjs +1 -3
- package/dist/bridge/host-tool-mcp.mjs.map +1 -1
- package/dist/bridge/index.mjs +333 -89
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.js +76 -10
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/bridge/cli-relay.ts +125 -41
- package/src/bridge/host-tool-mcp.ts +1 -3
- package/src/bridge/index.ts +141 -101
- package/src/bridge/tool-relay-auth.ts +195 -0
- package/src/codex-bridge-protocol.ts +0 -1
- package/src/codex-harness.ts +102 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @ai-sdk/harness-codex
|
|
2
2
|
|
|
3
|
+
## 1.0.12
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 2933383: fix(harness): fix harness relay auth
|
|
8
|
+
- @ai-sdk/harness@1.0.11
|
|
9
|
+
|
|
10
|
+
## 1.0.11
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- 0d1cb8e: fix(harness-codex): improve Codex prompt framing and avoid placing relay shim file in session workdir
|
|
15
|
+
|
|
3
16
|
## 1.0.10
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
|
@@ -8,7 +8,6 @@ var { McpServer } = mcpServerModule;
|
|
|
8
8
|
var { StdioServerTransport } = mcpStdioModule;
|
|
9
9
|
var schemas = JSON.parse(process.env.TOOL_SCHEMAS || "[]");
|
|
10
10
|
var relayUrl = process.env.TOOL_RELAY_URL || "";
|
|
11
|
-
var relayToken = process.env.TOOL_RELAY_TOKEN || "";
|
|
12
11
|
if (!schemas.length || !relayUrl) {
|
|
13
12
|
process.stderr.write(
|
|
14
13
|
"[host-tool-mcp] Missing TOOL_SCHEMAS or TOOL_RELAY_URL; exiting\n"
|
|
@@ -28,8 +27,7 @@ for (const schema of schemas) {
|
|
|
28
27
|
const res = await fetch(relayUrl, {
|
|
29
28
|
method: "POST",
|
|
30
29
|
headers: {
|
|
31
|
-
"Content-Type": "application/json"
|
|
32
|
-
...relayToken ? { Authorization: `Bearer ${relayToken}` } : {}
|
|
30
|
+
"Content-Type": "application/json"
|
|
33
31
|
},
|
|
34
32
|
body: JSON.stringify({ requestId, toolName: schema.name, input })
|
|
35
33
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/bridge/host-tool-mcp.ts"],"sourcesContent":["#!/usr/bin/env node\n// MCP-stdio tool server spawned by the codex CLI when\n// `mcp_servers.harness-tools` is configured. Exposes host-defined tools\n// over MCP-stdio and round-trips each call to the bridge's HTTP relay.\n//\n// Env vars (set by the bridge when starting a turn):\n// TOOL_SCHEMAS — JSON array of { name, description, inputSchema }\n// TOOL_RELAY_URL — http://127.0.0.1:<port> of the bridge relay server\n//
|
|
1
|
+
{"version":3,"sources":["../../src/bridge/host-tool-mcp.ts"],"sourcesContent":["#!/usr/bin/env node\n// MCP-stdio tool server spawned by the codex CLI when\n// `mcp_servers.harness-tools` is configured. Exposes host-defined tools\n// over MCP-stdio and round-trips each call to the bridge's HTTP relay.\n//\n// Env vars (set by the bridge when starting a turn):\n// TOOL_SCHEMAS — JSON array of { name, description, inputSchema }\n// TOOL_RELAY_URL — http://127.0.0.1:<port> of the bridge relay server\n// Relay authorization is issued by bridge runtime events, not an env token.\n\n/*\n * CONSTRAINT — the third-party imports below are NEVER bundled into the\n * compiled `bridge/host-tool-mcp.mjs`. They are declared `external` in\n * tsup.config.ts and resolved at runtime from the node_modules that the\n * bridge installs *inside the sandbox* from `src/bridge/package.json` (and\n * its pinned `pnpm-lock.yaml`). That bridge package.json — NOT this host\n * package — is the single source of truth for these packages and their\n * versions; the published `@ai-sdk/harness-codex` package does not provide\n * them at runtime.\n *\n * When adding or changing a third-party import here you MUST keep all three\n * in sync, or this server will either get the dependency bundled in or fail\n * to resolve it in the sandbox:\n * 1. the import statement below,\n * 2. the `external` array in tsup.config.ts, and\n * 3. the dependency entry in `src/bridge/package.json`.\n */\nimport * as mcpServerModule from '@modelcontextprotocol/sdk/server/mcp.js';\nimport * as mcpStdioModule from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { z } from 'zod/v4';\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst { McpServer } = mcpServerModule as any;\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst { StdioServerTransport } = mcpStdioModule as any;\n\ntype ToolSchema = {\n name: string;\n description?: string;\n inputSchema?: JsonSchemaObject;\n};\n\ntype JsonSchemaObject = {\n type?: string | string[];\n description?: string;\n properties?: Record<string, JsonSchemaObject>;\n required?: string[];\n items?: JsonSchemaObject;\n enum?: unknown[];\n const?: unknown;\n oneOf?: JsonSchemaObject[];\n anyOf?: JsonSchemaObject[];\n additionalProperties?: boolean | JsonSchemaObject;\n nullable?: boolean;\n};\n\nconst schemas: ToolSchema[] = JSON.parse(process.env.TOOL_SCHEMAS || '[]');\nconst relayUrl = process.env.TOOL_RELAY_URL || '';\n\nif (!schemas.length || !relayUrl) {\n process.stderr.write(\n '[host-tool-mcp] Missing TOOL_SCHEMAS or TOOL_RELAY_URL; exiting\\n',\n );\n process.exit(0);\n}\n\nconst server = new McpServer({ name: 'harness-tools', version: '1.0.0' });\n\nfor (const schema of schemas) {\n const shape = toZodShape(schema.inputSchema);\n server.tool(\n schema.name,\n schema.description ?? '',\n shape,\n async (input: Record<string, unknown>) => {\n const requestId = crypto.randomUUID();\n try {\n const res = await fetch(relayUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ requestId, toolName: schema.name, input }),\n });\n if (!res.ok) {\n const body = await res.text();\n throw new Error(\n `Tool relay ${schema.name} failed with ${res.status}: ${body.slice(0, 500)}`,\n );\n }\n const data = (await res.json()) as { result?: unknown };\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify(data.result ?? null),\n },\n ],\n };\n } catch (err) {\n return {\n content: [{ type: 'text' as const, text: `Error: ${String(err)}` }],\n isError: true,\n };\n }\n },\n );\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction toZodShape(schema: JsonSchemaObject | undefined): Record<string, any> {\n if (!schema?.properties) return {};\n const required = new Set(schema.required ?? []);\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const shape: Record<string, any> = {};\n for (const [key, propSchema] of Object.entries(schema.properties)) {\n const propType = toZodType(propSchema);\n shape[key] = required.has(key) ? propType : propType.optional();\n }\n return shape;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction toZodType(schema: JsonSchemaObject | undefined): any {\n if (!schema) return z.any();\n const types = Array.isArray(schema.type)\n ? schema.type.filter((t): t is string => t !== 'null')\n : ([schema.type].filter(Boolean) as string[]);\n let zType;\n switch (types[0]) {\n case 'string':\n zType = z.string();\n break;\n case 'number':\n zType = z.number();\n break;\n case 'integer':\n zType = z.number().int();\n break;\n case 'boolean':\n zType = z.boolean();\n break;\n case 'array':\n zType = z.array(toZodType(schema.items));\n break;\n case 'object':\n zType = z.object(toZodShape(schema));\n break;\n case 'null':\n zType = z.null();\n break;\n default:\n zType = z.any();\n }\n if (schema.description) zType = zType.describe(schema.description);\n if (schema.nullable) zType = zType.nullable();\n return zType;\n}\n\nconst transport = new StdioServerTransport();\nawait server.connect(transport);\n"],"mappings":";;;AA2BA,YAAY,qBAAqB;AACjC,YAAY,oBAAoB;AAChC,SAAS,SAAS;AAGlB,IAAM,EAAE,UAAU,IAAI;AAEtB,IAAM,EAAE,qBAAqB,IAAI;AAsBjC,IAAM,UAAwB,KAAK,MAAM,QAAQ,IAAI,gBAAgB,IAAI;AACzE,IAAM,WAAW,QAAQ,IAAI,kBAAkB;AAE/C,IAAI,CAAC,QAAQ,UAAU,CAAC,UAAU;AAChC,UAAQ,OAAO;AAAA,IACb;AAAA,EACF;AACA,UAAQ,KAAK,CAAC;AAChB;AAEA,IAAM,SAAS,IAAI,UAAU,EAAE,MAAM,iBAAiB,SAAS,QAAQ,CAAC;AAExE,WAAW,UAAU,SAAS;AAC5B,QAAM,QAAQ,WAAW,OAAO,WAAW;AAC3C,SAAO;AAAA,IACL,OAAO;AAAA,IACP,OAAO,eAAe;AAAA,IACtB;AAAA,IACA,OAAO,UAAmC;AACxC,YAAM,YAAY,OAAO,WAAW;AACpC,UAAI;AACF,cAAM,MAAM,MAAM,MAAM,UAAU;AAAA,UAChC,QAAQ;AAAA,UACR,SAAS;AAAA,YACP,gBAAgB;AAAA,UAClB;AAAA,UACA,MAAM,KAAK,UAAU,EAAE,WAAW,UAAU,OAAO,MAAM,MAAM,CAAC;AAAA,QAClE,CAAC;AACD,YAAI,CAAC,IAAI,IAAI;AACX,gBAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,gBAAM,IAAI;AAAA,YACR,cAAc,OAAO,IAAI,gBAAgB,IAAI,MAAM,KAAK,KAAK,MAAM,GAAG,GAAG,CAAC;AAAA,UAC5E;AAAA,QACF;AACA,cAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,eAAO;AAAA,UACL,SAAS;AAAA,YACP;AAAA,cACE,MAAM;AAAA,cACN,MAAM,KAAK,UAAU,KAAK,UAAU,IAAI;AAAA,YAC1C;AAAA,UACF;AAAA,QACF;AAAA,MACF,SAAS,KAAK;AACZ,eAAO;AAAA,UACL,SAAS,CAAC,EAAE,MAAM,QAAiB,MAAM,UAAU,OAAO,GAAG,CAAC,GAAG,CAAC;AAAA,UAClE,SAAS;AAAA,QACX;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAGA,SAAS,WAAW,QAA2D;AAC7E,MAAI,CAAC,QAAQ,WAAY,QAAO,CAAC;AACjC,QAAM,WAAW,IAAI,IAAI,OAAO,YAAY,CAAC,CAAC;AAE9C,QAAM,QAA6B,CAAC;AACpC,aAAW,CAAC,KAAK,UAAU,KAAK,OAAO,QAAQ,OAAO,UAAU,GAAG;AACjE,UAAM,WAAW,UAAU,UAAU;AACrC,UAAM,GAAG,IAAI,SAAS,IAAI,GAAG,IAAI,WAAW,SAAS,SAAS;AAAA,EAChE;AACA,SAAO;AACT;AAGA,SAAS,UAAU,QAA2C;AAC5D,MAAI,CAAC,OAAQ,QAAO,EAAE,IAAI;AAC1B,QAAM,QAAQ,MAAM,QAAQ,OAAO,IAAI,IACnC,OAAO,KAAK,OAAO,CAAC,MAAmB,MAAM,MAAM,IAClD,CAAC,OAAO,IAAI,EAAE,OAAO,OAAO;AACjC,MAAI;AACJ,UAAQ,MAAM,CAAC,GAAG;AAAA,IAChB,KAAK;AACH,cAAQ,EAAE,OAAO;AACjB;AAAA,IACF,KAAK;AACH,cAAQ,EAAE,OAAO;AACjB;AAAA,IACF,KAAK;AACH,cAAQ,EAAE,OAAO,EAAE,IAAI;AACvB;AAAA,IACF,KAAK;AACH,cAAQ,EAAE,QAAQ;AAClB;AAAA,IACF,KAAK;AACH,cAAQ,EAAE,MAAM,UAAU,OAAO,KAAK,CAAC;AACvC;AAAA,IACF,KAAK;AACH,cAAQ,EAAE,OAAO,WAAW,MAAM,CAAC;AACnC;AAAA,IACF,KAAK;AACH,cAAQ,EAAE,KAAK;AACf;AAAA,IACF;AACE,cAAQ,EAAE,IAAI;AAAA,EAClB;AACA,MAAI,OAAO,YAAa,SAAQ,MAAM,SAAS,OAAO,WAAW;AACjE,MAAI,OAAO,SAAU,SAAQ,MAAM,SAAS;AAC5C,SAAO;AACT;AAEA,IAAM,YAAY,IAAI,qBAAqB;AAC3C,MAAM,OAAO,QAAQ,SAAS;","names":[]}
|
package/dist/bridge/index.mjs
CHANGED
|
@@ -412,14 +412,13 @@ function serialiseError(err) {
|
|
|
412
412
|
|
|
413
413
|
// src/bridge/index.ts
|
|
414
414
|
import { randomUUID } from "crypto";
|
|
415
|
-
import { writeFile as writeFile2 } from "fs/promises";
|
|
415
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
416
416
|
import { createServer } from "http";
|
|
417
417
|
|
|
418
418
|
// src/bridge/cli-relay.ts
|
|
419
419
|
var CLI_SHIM_FILENAME = "harness-tool.mjs";
|
|
420
420
|
function buildCliShimScript({
|
|
421
|
-
relayPort
|
|
422
|
-
relayToken
|
|
421
|
+
relayPort
|
|
423
422
|
}) {
|
|
424
423
|
return `#!/usr/bin/env node
|
|
425
424
|
const [toolName, inputJson = '{}'] = process.argv.slice(2);
|
|
@@ -437,7 +436,7 @@ try {
|
|
|
437
436
|
const requestId = 'cli-' + Date.now() + '-' + Math.random().toString(16).slice(2);
|
|
438
437
|
const response = await fetch('http://127.0.0.1:${relayPort}', {
|
|
439
438
|
method: 'POST',
|
|
440
|
-
headers: { 'Content-Type': 'application/json'
|
|
439
|
+
headers: { 'Content-Type': 'application/json' },
|
|
441
440
|
body: JSON.stringify({ requestId, toolName, input }),
|
|
442
441
|
});
|
|
443
442
|
const text = await response.text();
|
|
@@ -454,36 +453,247 @@ if (!response.ok || payload.error) {
|
|
|
454
453
|
console.log(JSON.stringify(payload.result ?? payload, null, 2));
|
|
455
454
|
`;
|
|
456
455
|
}
|
|
457
|
-
function
|
|
458
|
-
|
|
456
|
+
function parseToolRelayCommand({
|
|
457
|
+
command,
|
|
459
458
|
cliShimPath
|
|
460
459
|
}) {
|
|
461
|
-
|
|
462
|
-
"## Host tools",
|
|
463
|
-
"",
|
|
464
|
-
"You have access to the following host-provided tools. To use one, run the following command via your built-in `bash` tool:",
|
|
465
|
-
"",
|
|
466
|
-
` node ${cliShimPath} <toolName> '<jsonInput>'`,
|
|
467
|
-
"",
|
|
468
|
-
"The script prints the JSON result to stdout. Do not invent another way to call these tools \u2014 only this CLI invocation will work. Pass the JSON input as a single-quoted argument.",
|
|
469
|
-
"For every user request that depends on a host-provided tool, run a separate CLI invocation for each needed tool call in the current turn before answering. Do not reuse previous tool results, and do not say you used a host tool unless the command has completed in the current turn.",
|
|
470
|
-
""
|
|
471
|
-
];
|
|
472
|
-
for (const tool of tools) {
|
|
473
|
-
lines.push(`### ${tool.name}`);
|
|
474
|
-
if (tool.description) lines.push(tool.description);
|
|
475
|
-
lines.push(
|
|
476
|
-
`Input schema: \`${JSON.stringify(tool.inputSchema ?? {})}\``,
|
|
477
|
-
""
|
|
478
|
-
);
|
|
479
|
-
}
|
|
480
|
-
return lines.join("\n");
|
|
460
|
+
return parseToolRelayCommandInternal({ command, cliShimPath, depth: 0 });
|
|
481
461
|
}
|
|
482
|
-
function
|
|
462
|
+
function parseToolRelayCommandInternal({
|
|
483
463
|
command,
|
|
464
|
+
cliShimPath,
|
|
465
|
+
depth
|
|
466
|
+
}) {
|
|
467
|
+
const argv2 = parseShellWords(command);
|
|
468
|
+
if (!argv2) return void 0;
|
|
469
|
+
const relayCall = parseDirectToolRelayArgv({ argv: argv2, cliShimPath });
|
|
470
|
+
if (relayCall) return relayCall;
|
|
471
|
+
const innerCommand = extractShellEvalCommand(argv2);
|
|
472
|
+
if (!innerCommand || depth >= 2) return void 0;
|
|
473
|
+
return parseToolRelayCommandInternal({
|
|
474
|
+
command: innerCommand,
|
|
475
|
+
cliShimPath,
|
|
476
|
+
depth: depth + 1
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
function parseDirectToolRelayArgv({
|
|
480
|
+
argv: argv2,
|
|
484
481
|
cliShimPath
|
|
485
482
|
}) {
|
|
486
|
-
|
|
483
|
+
if (argv2.length < 3 || argv2.length > 4) return void 0;
|
|
484
|
+
if (argv2[0] !== "node" || argv2[1] !== cliShimPath) return void 0;
|
|
485
|
+
const toolName = argv2[2];
|
|
486
|
+
if (!toolName) return void 0;
|
|
487
|
+
try {
|
|
488
|
+
return { toolName, input: JSON.parse(argv2[3] ?? "{}") };
|
|
489
|
+
} catch {
|
|
490
|
+
return void 0;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
function extractShellEvalCommand(argv2) {
|
|
494
|
+
if (argv2.length !== 3) return void 0;
|
|
495
|
+
const shellName = argv2[0].split("/").at(-1);
|
|
496
|
+
if (shellName !== "bash" && shellName !== "sh" && shellName !== "zsh") {
|
|
497
|
+
return void 0;
|
|
498
|
+
}
|
|
499
|
+
if (argv2[1] !== "-c" && argv2[1] !== "-lc") return void 0;
|
|
500
|
+
return argv2[2];
|
|
501
|
+
}
|
|
502
|
+
function parseShellWords(command) {
|
|
503
|
+
const words = [];
|
|
504
|
+
let current = "";
|
|
505
|
+
let quote;
|
|
506
|
+
let hasCurrent = false;
|
|
507
|
+
const pushCurrent = () => {
|
|
508
|
+
if (!hasCurrent) return;
|
|
509
|
+
words.push(current);
|
|
510
|
+
current = "";
|
|
511
|
+
hasCurrent = false;
|
|
512
|
+
};
|
|
513
|
+
for (let i = 0; i < command.length; i++) {
|
|
514
|
+
const char = command[i];
|
|
515
|
+
if (quote === "'") {
|
|
516
|
+
if (char === "'") {
|
|
517
|
+
quote = void 0;
|
|
518
|
+
} else {
|
|
519
|
+
current += char;
|
|
520
|
+
}
|
|
521
|
+
hasCurrent = true;
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
if (quote === '"') {
|
|
525
|
+
if (char === '"') {
|
|
526
|
+
quote = void 0;
|
|
527
|
+
} else if (char === "\\" && i + 1 < command.length) {
|
|
528
|
+
current += command[++i];
|
|
529
|
+
} else {
|
|
530
|
+
current += char;
|
|
531
|
+
}
|
|
532
|
+
hasCurrent = true;
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (/\s/.test(char)) {
|
|
536
|
+
pushCurrent();
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
if (char === '"' || char === "'") {
|
|
540
|
+
quote = char;
|
|
541
|
+
hasCurrent = true;
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
if (char === "\\" && i + 1 < command.length) {
|
|
545
|
+
current += command[++i];
|
|
546
|
+
hasCurrent = true;
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
if (/[;&|<>()`$]/.test(char)) return void 0;
|
|
550
|
+
current += char;
|
|
551
|
+
hasCurrent = true;
|
|
552
|
+
}
|
|
553
|
+
if (quote) return void 0;
|
|
554
|
+
pushCurrent();
|
|
555
|
+
return words;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// src/bridge/tool-relay-auth.ts
|
|
559
|
+
import { readdir, readFile, readlink } from "fs/promises";
|
|
560
|
+
var ToolRelayAuthorizer = class {
|
|
561
|
+
constructor({
|
|
562
|
+
ttlMs = 1e4,
|
|
563
|
+
now = Date.now
|
|
564
|
+
} = {}) {
|
|
565
|
+
this.authorizations = [];
|
|
566
|
+
this.ttlMs = ttlMs;
|
|
567
|
+
this.now = now;
|
|
568
|
+
}
|
|
569
|
+
authorizeToolCall(call) {
|
|
570
|
+
this.pruneExpired();
|
|
571
|
+
this.authorizations.push({
|
|
572
|
+
key: toolRelayCallKey(call),
|
|
573
|
+
expiresAt: this.now() + this.ttlMs
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
authorizeAnyToolCall() {
|
|
577
|
+
this.pruneExpired();
|
|
578
|
+
this.authorizations.push({
|
|
579
|
+
expiresAt: this.now() + this.ttlMs
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
consumeToolCall(call) {
|
|
583
|
+
this.pruneExpired();
|
|
584
|
+
const key = toolRelayCallKey(call);
|
|
585
|
+
let index = this.authorizations.findIndex((auth) => auth.key === key);
|
|
586
|
+
if (index === -1) {
|
|
587
|
+
index = this.authorizations.findIndex((auth) => auth.key === void 0);
|
|
588
|
+
}
|
|
589
|
+
if (index === -1) return false;
|
|
590
|
+
this.authorizations.splice(index, 1);
|
|
591
|
+
return true;
|
|
592
|
+
}
|
|
593
|
+
pruneExpired() {
|
|
594
|
+
const now = this.now();
|
|
595
|
+
for (let i = this.authorizations.length - 1; i >= 0; i--) {
|
|
596
|
+
if (this.authorizations[i].expiresAt <= now) {
|
|
597
|
+
this.authorizations.splice(i, 1);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
var ToolRelayPendingCalls = class {
|
|
603
|
+
constructor() {
|
|
604
|
+
this.calls = /* @__PURE__ */ new Map();
|
|
605
|
+
}
|
|
606
|
+
begin({
|
|
607
|
+
call,
|
|
608
|
+
run
|
|
609
|
+
}) {
|
|
610
|
+
const key = toolRelayCallKey(call);
|
|
611
|
+
const existing = this.calls.get(key);
|
|
612
|
+
if (existing) return { result: existing, isNew: false };
|
|
613
|
+
const result = run();
|
|
614
|
+
this.calls.set(key, result);
|
|
615
|
+
void result.finally(() => {
|
|
616
|
+
if (this.calls.get(key) === result) {
|
|
617
|
+
this.calls.delete(key);
|
|
618
|
+
}
|
|
619
|
+
}).catch(() => {
|
|
620
|
+
});
|
|
621
|
+
return { result, isNew: true };
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
function toolRelayCallKey({ toolName, input }) {
|
|
625
|
+
return `${toolName}\0${canonicalJson(input ?? {})}`;
|
|
626
|
+
}
|
|
627
|
+
function canonicalJson(value) {
|
|
628
|
+
return JSON.stringify(normalizeJsonValue(value));
|
|
629
|
+
}
|
|
630
|
+
function normalizeJsonValue(value) {
|
|
631
|
+
if (Array.isArray(value)) {
|
|
632
|
+
return value.map(normalizeJsonValue);
|
|
633
|
+
}
|
|
634
|
+
if (value && typeof value === "object") {
|
|
635
|
+
return Object.fromEntries(
|
|
636
|
+
Object.entries(value).filter(([, entryValue]) => entryValue !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, entryValue]) => [key, normalizeJsonValue(entryValue)])
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
return value;
|
|
640
|
+
}
|
|
641
|
+
async function isToolRelayRequestFromAllowedProcess({
|
|
642
|
+
socket,
|
|
643
|
+
allowedScriptPaths
|
|
644
|
+
}) {
|
|
645
|
+
if (process.platform !== "linux") return false;
|
|
646
|
+
if (!socket.remotePort || !socket.localPort) return false;
|
|
647
|
+
const inode = await findTcpSocketInode({
|
|
648
|
+
clientPort: socket.remotePort,
|
|
649
|
+
serverPort: socket.localPort
|
|
650
|
+
});
|
|
651
|
+
if (!inode) return false;
|
|
652
|
+
const cmdline = await findProcessCmdlineForSocketInode({ inode });
|
|
653
|
+
return cmdline?.some((arg) => allowedScriptPaths.has(arg)) ?? false;
|
|
654
|
+
}
|
|
655
|
+
async function findTcpSocketInode({
|
|
656
|
+
clientPort,
|
|
657
|
+
serverPort
|
|
658
|
+
}) {
|
|
659
|
+
for (const tablePath of ["/proc/net/tcp", "/proc/net/tcp6"]) {
|
|
660
|
+
const table = await readFile(tablePath, "utf8").catch(() => void 0);
|
|
661
|
+
if (!table) continue;
|
|
662
|
+
for (const line of table.split("\n").slice(1)) {
|
|
663
|
+
const columns = line.trim().split(/\s+/);
|
|
664
|
+
if (columns.length < 10) continue;
|
|
665
|
+
const local = parseProcNetAddress(columns[1]);
|
|
666
|
+
const remote = parseProcNetAddress(columns[2]);
|
|
667
|
+
if (local?.port === clientPort && remote?.port === serverPort && columns[9] !== "0") {
|
|
668
|
+
return columns[9];
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return void 0;
|
|
673
|
+
}
|
|
674
|
+
function parseProcNetAddress(value) {
|
|
675
|
+
const [, portHex] = value.split(":");
|
|
676
|
+
if (!portHex) return void 0;
|
|
677
|
+
return { port: Number.parseInt(portHex, 16) };
|
|
678
|
+
}
|
|
679
|
+
async function findProcessCmdlineForSocketInode({
|
|
680
|
+
inode
|
|
681
|
+
}) {
|
|
682
|
+
const procEntries = await readdir("/proc", { withFileTypes: true }).catch(
|
|
683
|
+
() => []
|
|
684
|
+
);
|
|
685
|
+
for (const entry of procEntries) {
|
|
686
|
+
if (!entry.isDirectory() || !/^\d+$/.test(entry.name)) continue;
|
|
687
|
+
const fdDir = `/proc/${entry.name}/fd`;
|
|
688
|
+
const fds = await readdir(fdDir).catch(() => []);
|
|
689
|
+
for (const fd of fds) {
|
|
690
|
+
const target = await readlink(`${fdDir}/${fd}`).catch(() => void 0);
|
|
691
|
+
if (target !== `socket:[${inode}]`) continue;
|
|
692
|
+
const cmdline = await readFile(`/proc/${entry.name}/cmdline`, "utf8").then((value) => value.split("\0").filter(Boolean)).catch(() => void 0);
|
|
693
|
+
if (cmdline) return cmdline;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return void 0;
|
|
487
697
|
}
|
|
488
698
|
|
|
489
699
|
// src/bridge/index.ts
|
|
@@ -497,14 +707,15 @@ function toCommonName(nativeName) {
|
|
|
497
707
|
return NATIVE_TO_COMMON[nativeName] ?? nativeName;
|
|
498
708
|
}
|
|
499
709
|
var args = parseArgs(argv.slice(2));
|
|
500
|
-
var workdir = args.workdir;
|
|
501
|
-
var bridgeStateDir =
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
710
|
+
var workdir = requireArg({ value: args.workdir, name: "--workdir" });
|
|
711
|
+
var bridgeStateDir = requireArg({
|
|
712
|
+
value: args.bridgeStateDir,
|
|
713
|
+
name: "--bridge-state-dir"
|
|
714
|
+
});
|
|
715
|
+
var cliShimDir = requireArg({
|
|
716
|
+
value: args.cliShimDir,
|
|
717
|
+
name: "--cli-shim-dir"
|
|
718
|
+
});
|
|
508
719
|
var bootstrapDir = args.bootstrapDir ?? workdir;
|
|
509
720
|
var codexSdk = codexSdkModule;
|
|
510
721
|
var threadState = { id: void 0 };
|
|
@@ -523,9 +734,9 @@ async function runTurn(start, turn) {
|
|
|
523
734
|
let relay;
|
|
524
735
|
let cliShimPath;
|
|
525
736
|
if (start.tools && start.tools.length > 0) {
|
|
526
|
-
|
|
737
|
+
cliShimPath = `${cliShimDir}/${CLI_SHIM_FILENAME}`;
|
|
527
738
|
relay = await startToolRelay({
|
|
528
|
-
|
|
739
|
+
allowedScriptPaths: [cliShimPath, `${bootstrapDir}/host-tool-mcp.mjs`],
|
|
529
740
|
tools: start.tools,
|
|
530
741
|
emit,
|
|
531
742
|
requestToolResult: turn.requestToolResult
|
|
@@ -542,14 +753,13 @@ async function runTurn(start, turn) {
|
|
|
542
753
|
inputSchema: t.inputSchema
|
|
543
754
|
}))
|
|
544
755
|
),
|
|
545
|
-
TOOL_RELAY_URL: `http://127.0.0.1:${relay.port}
|
|
546
|
-
TOOL_RELAY_TOKEN: relayToken
|
|
756
|
+
TOOL_RELAY_URL: `http://127.0.0.1:${relay.port}`
|
|
547
757
|
}
|
|
548
758
|
};
|
|
549
|
-
|
|
759
|
+
await mkdir2(cliShimDir, { recursive: true });
|
|
550
760
|
await writeFile2(
|
|
551
761
|
cliShimPath,
|
|
552
|
-
buildCliShimScript({ relayPort: relay.port
|
|
762
|
+
buildCliShimScript({ relayPort: relay.port }),
|
|
553
763
|
"utf8"
|
|
554
764
|
);
|
|
555
765
|
}
|
|
@@ -598,15 +808,7 @@ async function runTurn(start, turn) {
|
|
|
598
808
|
};
|
|
599
809
|
const thread = threadState.id ? codex.resumeThread(threadState.id, threadOptions) : codex.startThread(threadOptions);
|
|
600
810
|
emit({ type: "stream-start" });
|
|
601
|
-
const userMessage =
|
|
602
|
-
text: start.prompt,
|
|
603
|
-
instructions: start.instructions,
|
|
604
|
-
// Temporary workaround for upstream codex MCP-tool bug — see ./cli-relay.ts
|
|
605
|
-
toolUsageBlock: cliShimPath && start.tools && start.tools.length > 0 ? composeToolUsageInstructions({
|
|
606
|
-
tools: start.tools,
|
|
607
|
-
cliShimPath
|
|
608
|
-
}) : void 0
|
|
609
|
-
});
|
|
811
|
+
const userMessage = start.prompt;
|
|
610
812
|
let turnUsage;
|
|
611
813
|
const textByItem = /* @__PURE__ */ new Map();
|
|
612
814
|
const reasoningByItem = /* @__PURE__ */ new Map();
|
|
@@ -620,7 +822,25 @@ async function runTurn(start, turn) {
|
|
|
620
822
|
threadState.id = event.thread_id;
|
|
621
823
|
emit({ type: "bridge-thread", threadId: event.thread_id });
|
|
622
824
|
}
|
|
623
|
-
if (cliShimPath && event.item?.type === "command_execution"
|
|
825
|
+
if (cliShimPath && event.item?.type === "command_execution") {
|
|
826
|
+
const relayCall = typeof event.item.command === "string" ? parseToolRelayCommand({
|
|
827
|
+
command: event.item.command,
|
|
828
|
+
cliShimPath
|
|
829
|
+
}) : void 0;
|
|
830
|
+
if (event.type === "item.started" && relay) {
|
|
831
|
+
if (relayCall) {
|
|
832
|
+
relay.authorizeToolCall(relayCall);
|
|
833
|
+
} else if (typeof event.item.command !== "string") {
|
|
834
|
+
relay.authorizeAnyToolCall();
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
if (relayCall) {
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
if (relay && isHostMcpToolEvent(event)) {
|
|
842
|
+
const relayCall = relayCallFromCodexMcpEvent(event);
|
|
843
|
+
if (relayCall) relay.authorizeToolCall(relayCall);
|
|
624
844
|
continue;
|
|
625
845
|
}
|
|
626
846
|
translateAndEmit(event, {
|
|
@@ -653,6 +873,18 @@ function extractMcpToolCallResult(item) {
|
|
|
653
873
|
}
|
|
654
874
|
return result.content ?? null;
|
|
655
875
|
}
|
|
876
|
+
function isHostMcpToolEvent(event) {
|
|
877
|
+
return event.item?.type === "mcp_tool_call" && event.item.server === "harness-tools";
|
|
878
|
+
}
|
|
879
|
+
function relayCallFromCodexMcpEvent(event) {
|
|
880
|
+
if (event.type !== "item.started") return void 0;
|
|
881
|
+
const toolName = event.item?.tool;
|
|
882
|
+
if (!toolName) return void 0;
|
|
883
|
+
return {
|
|
884
|
+
toolName,
|
|
885
|
+
input: event.item?.arguments ?? {}
|
|
886
|
+
};
|
|
887
|
+
}
|
|
656
888
|
function translateAndEmit(event, ctx) {
|
|
657
889
|
if (event.type === "turn.completed") {
|
|
658
890
|
if (event.usage) ctx.setTurnUsage(mapUsage(event.usage));
|
|
@@ -813,37 +1045,19 @@ function defaultUsage() {
|
|
|
813
1045
|
outputTokens: { total: 0, text: 0 }
|
|
814
1046
|
};
|
|
815
1047
|
}
|
|
816
|
-
function composeUserMessage({
|
|
817
|
-
text,
|
|
818
|
-
instructions,
|
|
819
|
-
toolUsageBlock
|
|
820
|
-
}) {
|
|
821
|
-
const blocks = [];
|
|
822
|
-
if (instructions) {
|
|
823
|
-
blocks.push(
|
|
824
|
-
`<session-instructions>
|
|
825
|
-
The block below is operating guidance from the system, not a message from the user \u2014 follow it, but do not mention it or attribute it to the user.
|
|
826
|
-
|
|
827
|
-
${instructions}
|
|
828
|
-
</session-instructions>`
|
|
829
|
-
);
|
|
830
|
-
}
|
|
831
|
-
if (toolUsageBlock) blocks.push(toolUsageBlock);
|
|
832
|
-
blocks.push(instructions ? `<user-message>
|
|
833
|
-
${text}
|
|
834
|
-
</user-message>` : text);
|
|
835
|
-
return blocks.join("\n\n");
|
|
836
|
-
}
|
|
837
1048
|
async function startToolRelay({
|
|
838
|
-
|
|
1049
|
+
allowedScriptPaths,
|
|
839
1050
|
tools,
|
|
840
1051
|
emit,
|
|
841
1052
|
requestToolResult
|
|
842
1053
|
}) {
|
|
843
1054
|
const toolNames = new Set(tools.map((t) => t.name));
|
|
1055
|
+
const allowedScriptPathSet = new Set(allowedScriptPaths);
|
|
1056
|
+
const authorizer = new ToolRelayAuthorizer();
|
|
1057
|
+
const pendingCalls = new ToolRelayPendingCalls();
|
|
844
1058
|
const server = createServer(async (req, res) => {
|
|
845
1059
|
try {
|
|
846
|
-
if (req.method !== "POST" || req.url !== "/"
|
|
1060
|
+
if (req.method !== "POST" || req.url !== "/") {
|
|
847
1061
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
848
1062
|
res.end(JSON.stringify({ error: "unauthorized tool relay request" }));
|
|
849
1063
|
return;
|
|
@@ -861,21 +1075,38 @@ async function startToolRelay({
|
|
|
861
1075
|
);
|
|
862
1076
|
return;
|
|
863
1077
|
}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
input: JSON.stringify(input ?? {}),
|
|
869
|
-
providerExecuted: false
|
|
1078
|
+
const relayCall = { toolName, input };
|
|
1079
|
+
const authorized = authorizer.consumeToolCall(relayCall) || await isToolRelayRequestFromAllowedProcess({
|
|
1080
|
+
socket: req.socket,
|
|
1081
|
+
allowedScriptPaths: allowedScriptPathSet
|
|
870
1082
|
});
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
1083
|
+
if (!authorized) {
|
|
1084
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1085
|
+
res.end(JSON.stringify({ error: "unauthorized tool relay request" }));
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
const { result } = pendingCalls.begin({
|
|
1089
|
+
call: relayCall,
|
|
1090
|
+
run: async () => {
|
|
1091
|
+
emit({
|
|
1092
|
+
type: "tool-call",
|
|
1093
|
+
toolCallId: requestId,
|
|
1094
|
+
toolName,
|
|
1095
|
+
input: JSON.stringify(input ?? {}),
|
|
1096
|
+
providerExecuted: false
|
|
1097
|
+
});
|
|
1098
|
+
const toolResult = await requestToolResult(requestId);
|
|
1099
|
+
emit({
|
|
1100
|
+
type: "tool-result",
|
|
1101
|
+
toolCallId: requestId,
|
|
1102
|
+
toolName,
|
|
1103
|
+
result: toolResult.output ?? null,
|
|
1104
|
+
isError: !!toolResult.isError
|
|
1105
|
+
});
|
|
1106
|
+
return toolResult;
|
|
1107
|
+
}
|
|
878
1108
|
});
|
|
1109
|
+
const { output } = await result;
|
|
879
1110
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
880
1111
|
res.end(JSON.stringify({ result: output }));
|
|
881
1112
|
} catch (error) {
|
|
@@ -896,7 +1127,9 @@ async function startToolRelay({
|
|
|
896
1127
|
}
|
|
897
1128
|
return {
|
|
898
1129
|
port: address.port,
|
|
899
|
-
close: () => closeServer(server)
|
|
1130
|
+
close: () => closeServer(server),
|
|
1131
|
+
authorizeToolCall: (call) => authorizer.authorizeToolCall(call),
|
|
1132
|
+
authorizeAnyToolCall: () => authorizer.authorizeAnyToolCall()
|
|
900
1133
|
};
|
|
901
1134
|
}
|
|
902
1135
|
function closeServer(server) {
|
|
@@ -914,6 +1147,8 @@ function parseArgs(args2) {
|
|
|
914
1147
|
out.bridgeStateDir = args2[++i];
|
|
915
1148
|
} else if (args2[i] === "--bootstrap-dir" && i + 1 < args2.length) {
|
|
916
1149
|
out.bootstrapDir = args2[++i];
|
|
1150
|
+
} else if (args2[i] === "--cli-shim-dir" && i + 1 < args2.length) {
|
|
1151
|
+
out.cliShimDir = args2[++i];
|
|
917
1152
|
}
|
|
918
1153
|
}
|
|
919
1154
|
return out;
|
|
@@ -928,4 +1163,13 @@ function emitFatal(message) {
|
|
|
928
1163
|
stdout2.write(JSON.stringify({ type: "bridge-fatal", message }) + "\n");
|
|
929
1164
|
process.exit(1);
|
|
930
1165
|
}
|
|
1166
|
+
function requireArg({
|
|
1167
|
+
value,
|
|
1168
|
+
name
|
|
1169
|
+
}) {
|
|
1170
|
+
if (!value) {
|
|
1171
|
+
emitFatal(`Missing ${name} argument.`);
|
|
1172
|
+
}
|
|
1173
|
+
return value;
|
|
1174
|
+
}
|
|
931
1175
|
//# sourceMappingURL=index.mjs.map
|