@ai-sdk/harness-codex 1.0.11 → 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 CHANGED
@@ -1,5 +1,12 @@
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
+
3
10
  ## 1.0.11
4
11
 
5
12
  ### 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// TOOL_RELAY_TOKEN bearer token required by the relay\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 || '';\nconst relayToken = process.env.TOOL_RELAY_TOKEN || '';\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 ...(relayToken ? { Authorization: `Bearer ${relayToken}` } : {}),\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":"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","names":[]}
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":"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","names":[]}
@@ -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', Authorization: 'Bearer ${relayToken}' },
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 isToolRelayCommand({
456
+ function parseToolRelayCommand({
458
457
  command,
459
458
  cliShimPath
460
459
  }) {
461
- return command.includes(cliShimPath);
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
- const relayToken = randomUUID();
737
+ cliShimPath = `${cliShimDir}/${CLI_SHIM_FILENAME}`;
503
738
  relay = await startToolRelay({
504
- relayToken,
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, relayToken }),
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" && typeof event.item.command === "string" && isToolRelayCommand({ command: event.item.command, cliShimPath })) {
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
- relayToken,
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 !== "/" || req.headers.authorization !== `Bearer ${relayToken}`) {
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
- emit({
813
- type: "tool-call",
814
- toolCallId: requestId,
815
- toolName,
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
- const { output, isError } = await requestToolResult(requestId);
820
- emit({
821
- type: "tool-result",
822
- toolCallId: requestId,
823
- toolName,
824
- result: output ?? null,
825
- isError: !!isError
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) {