@ai-sdk/harness-codex 1.0.11 → 1.0.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.
- package/CHANGELOG.md +17 -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 +313 -28
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.js +19 -78
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/bridge/cli-relay.ts +122 -5
- package/src/bridge/host-tool-mcp.ts +7 -10
- package/src/bridge/index.ts +109 -45
- package/src/bridge/tool-relay-auth.ts +195 -0
- package/src/codex-harness.ts +19 -89
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @ai-sdk/harness-codex
|
|
2
2
|
|
|
3
|
+
## 1.0.13
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7859cea: feat(harness): add tool filtering via `activeTools` and `inactiveTools`
|
|
8
|
+
- c857346: feat(harness): add utility functions for certain duplicated layers in harnesses
|
|
9
|
+
- Updated dependencies [7859cea]
|
|
10
|
+
- Updated dependencies [c857346]
|
|
11
|
+
- @ai-sdk/harness@1.0.12
|
|
12
|
+
|
|
13
|
+
## 1.0.12
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- 2933383: fix(harness): fix harness relay auth
|
|
18
|
+
- @ai-sdk/harness@1.0.11
|
|
19
|
+
|
|
3
20
|
## 1.0.11
|
|
4
21
|
|
|
5
22
|
### 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\nfunction toZodShape(\n schema: JsonSchemaObject | undefined,\n): Record<string, z.ZodTypeAny> {\n if (!schema?.properties) return {};\n const required = new Set(schema.required ?? []);\n const shape: Record<string, z.ZodTypeAny> = {};\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\nfunction toZodType(schema: JsonSchemaObject | undefined): z.ZodTypeAny {\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: z.ZodTypeAny;\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;AAEA,SAAS,WACP,QAC8B;AAC9B,MAAI,CAAC,QAAQ,WAAY,QAAO,CAAC;AACjC,QAAM,WAAW,IAAI,IAAI,OAAO,YAAY,CAAC,CAAC;AAC9C,QAAM,QAAsC,CAAC;AAC7C,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;AAEA,SAAS,UAAU,QAAoD;AACrE,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
|
@@ -418,8 +418,7 @@ import { createServer } from "http";
|
|
|
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,11 +453,247 @@ if (!response.ok || payload.error) {
|
|
|
454
453
|
console.log(JSON.stringify(payload.result ?? payload, null, 2));
|
|
455
454
|
`;
|
|
456
455
|
}
|
|
457
|
-
function
|
|
456
|
+
function parseToolRelayCommand({
|
|
458
457
|
command,
|
|
459
458
|
cliShimPath
|
|
460
459
|
}) {
|
|
461
|
-
return command
|
|
460
|
+
return parseToolRelayCommandInternal({ command, cliShimPath, depth: 0 });
|
|
461
|
+
}
|
|
462
|
+
function parseToolRelayCommandInternal({
|
|
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,
|
|
481
|
+
cliShimPath
|
|
482
|
+
}) {
|
|
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;
|
|
462
697
|
}
|
|
463
698
|
|
|
464
699
|
// src/bridge/index.ts
|
|
@@ -499,9 +734,9 @@ async function runTurn(start, turn) {
|
|
|
499
734
|
let relay;
|
|
500
735
|
let cliShimPath;
|
|
501
736
|
if (start.tools && start.tools.length > 0) {
|
|
502
|
-
|
|
737
|
+
cliShimPath = `${cliShimDir}/${CLI_SHIM_FILENAME}`;
|
|
503
738
|
relay = await startToolRelay({
|
|
504
|
-
|
|
739
|
+
allowedScriptPaths: [cliShimPath, `${bootstrapDir}/host-tool-mcp.mjs`],
|
|
505
740
|
tools: start.tools,
|
|
506
741
|
emit,
|
|
507
742
|
requestToolResult: turn.requestToolResult
|
|
@@ -518,15 +753,13 @@ async function runTurn(start, turn) {
|
|
|
518
753
|
inputSchema: t.inputSchema
|
|
519
754
|
}))
|
|
520
755
|
),
|
|
521
|
-
TOOL_RELAY_URL: `http://127.0.0.1:${relay.port}
|
|
522
|
-
TOOL_RELAY_TOKEN: relayToken
|
|
756
|
+
TOOL_RELAY_URL: `http://127.0.0.1:${relay.port}`
|
|
523
757
|
}
|
|
524
758
|
};
|
|
525
|
-
cliShimPath = `${cliShimDir}/${CLI_SHIM_FILENAME}`;
|
|
526
759
|
await mkdir2(cliShimDir, { recursive: true });
|
|
527
760
|
await writeFile2(
|
|
528
761
|
cliShimPath,
|
|
529
|
-
buildCliShimScript({ relayPort: relay.port
|
|
762
|
+
buildCliShimScript({ relayPort: relay.port }),
|
|
530
763
|
"utf8"
|
|
531
764
|
);
|
|
532
765
|
}
|
|
@@ -589,7 +822,25 @@ async function runTurn(start, turn) {
|
|
|
589
822
|
threadState.id = event.thread_id;
|
|
590
823
|
emit({ type: "bridge-thread", threadId: event.thread_id });
|
|
591
824
|
}
|
|
592
|
-
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);
|
|
593
844
|
continue;
|
|
594
845
|
}
|
|
595
846
|
translateAndEmit(event, {
|
|
@@ -622,6 +873,18 @@ function extractMcpToolCallResult(item) {
|
|
|
622
873
|
}
|
|
623
874
|
return result.content ?? null;
|
|
624
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
|
+
}
|
|
625
888
|
function translateAndEmit(event, ctx) {
|
|
626
889
|
if (event.type === "turn.completed") {
|
|
627
890
|
if (event.usage) ctx.setTurnUsage(mapUsage(event.usage));
|
|
@@ -783,15 +1046,18 @@ function defaultUsage() {
|
|
|
783
1046
|
};
|
|
784
1047
|
}
|
|
785
1048
|
async function startToolRelay({
|
|
786
|
-
|
|
1049
|
+
allowedScriptPaths,
|
|
787
1050
|
tools,
|
|
788
1051
|
emit,
|
|
789
1052
|
requestToolResult
|
|
790
1053
|
}) {
|
|
791
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();
|
|
792
1058
|
const server = createServer(async (req, res) => {
|
|
793
1059
|
try {
|
|
794
|
-
if (req.method !== "POST" || req.url !== "/"
|
|
1060
|
+
if (req.method !== "POST" || req.url !== "/") {
|
|
795
1061
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
796
1062
|
res.end(JSON.stringify({ error: "unauthorized tool relay request" }));
|
|
797
1063
|
return;
|
|
@@ -809,21 +1075,38 @@ async function startToolRelay({
|
|
|
809
1075
|
);
|
|
810
1076
|
return;
|
|
811
1077
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
input: JSON.stringify(input ?? {}),
|
|
817
|
-
providerExecuted: false
|
|
1078
|
+
const relayCall = { toolName, input };
|
|
1079
|
+
const authorized = authorizer.consumeToolCall(relayCall) || await isToolRelayRequestFromAllowedProcess({
|
|
1080
|
+
socket: req.socket,
|
|
1081
|
+
allowedScriptPaths: allowedScriptPathSet
|
|
818
1082
|
});
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
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
|
+
}
|
|
826
1108
|
});
|
|
1109
|
+
const { output } = await result;
|
|
827
1110
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
828
1111
|
res.end(JSON.stringify({ result: output }));
|
|
829
1112
|
} catch (error) {
|
|
@@ -844,7 +1127,9 @@ async function startToolRelay({
|
|
|
844
1127
|
}
|
|
845
1128
|
return {
|
|
846
1129
|
port: address.port,
|
|
847
|
-
close: () => closeServer(server)
|
|
1130
|
+
close: () => closeServer(server),
|
|
1131
|
+
authorizeToolCall: (call) => authorizer.authorizeToolCall(call),
|
|
1132
|
+
authorizeAnyToolCall: () => authorizer.authorizeAnyToolCall()
|
|
848
1133
|
};
|
|
849
1134
|
}
|
|
850
1135
|
function closeServer(server) {
|