@ai-sdk/harness-opencode 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 CHANGED
@@ -1,5 +1,22 @@
1
1
  # @ai-sdk/harness-opencode
2
2
 
3
+ ## 1.0.12
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.11
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.10
4
21
 
5
22
  ### Patch Changes
@@ -6,7 +6,6 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
6
6
  import { z } from "zod/v4";
7
7
  var schemas = JSON.parse(process.env.TOOL_SCHEMAS || "[]");
8
8
  var relayUrl = process.env.TOOL_RELAY_URL || "";
9
- var relayToken = process.env.TOOL_RELAY_TOKEN || "";
10
9
  if (!schemas.length || !relayUrl) {
11
10
  process.stderr.write(
12
11
  "[host-tool-mcp] Missing TOOL_SCHEMAS or TOOL_RELAY_URL; exiting\n"
@@ -26,8 +25,7 @@ for (const schema of schemas) {
26
25
  const res = await fetch(relayUrl, {
27
26
  method: "POST",
28
27
  headers: {
29
- "Content-Type": "application/json",
30
- ...relayToken ? { Authorization: `Bearer ${relayToken}` } : {}
28
+ "Content-Type": "application/json"
31
29
  },
32
30
  body: JSON.stringify({ requestId, toolName: schema.name, input })
33
31
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/bridge/host-tool-mcp.ts"],"sourcesContent":["#!/usr/bin/env node\n/*\n * These bridge imports are externalized by tsup and resolved inside the\n * sandbox from src/bridge/package.json and its lockfile. Keep this file,\n * tsup.config.ts, and the bridge package dependency list in sync.\n */\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { z } from 'zod/v4';\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\ntype ZodShape = Record<string, z.ZodTypeAny>;\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\nfunction toZodShape(schema: JsonSchemaObject | undefined): ZodShape {\n if (!schema?.properties) return {};\n const required = new Set(schema.required ?? []);\n const shape: ZodShape = {};\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":"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","names":[]}
1
+ {"version":3,"sources":["../../src/bridge/host-tool-mcp.ts"],"sourcesContent":["#!/usr/bin/env node\n/*\n * These bridge imports are externalized by tsup and resolved inside the\n * sandbox from src/bridge/package.json and its lockfile. Keep this file,\n * tsup.config.ts, and the bridge package dependency list in sync.\n */\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { z } from 'zod/v4';\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\ntype ZodShape = Record<string, z.ZodTypeAny>;\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(schema: JsonSchemaObject | undefined): ZodShape {\n if (!schema?.properties) return {};\n const required = new Set(schema.required ?? []);\n const shape: ZodShape = {};\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":"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","names":[]}
@@ -608,6 +608,116 @@ function add({
608
608
  return leftNumber == null && rightNumber == null ? void 0 : (leftNumber ?? 0) + (rightNumber ?? 0);
609
609
  }
610
610
 
611
+ // src/bridge/tool-relay-auth.ts
612
+ import { readdir, readFile, readlink } from "fs/promises";
613
+ var ToolRelayAuthorizer = class {
614
+ constructor({
615
+ ttlMs = 1e4,
616
+ now = Date.now
617
+ } = {}) {
618
+ this.authorizations = [];
619
+ this.ttlMs = ttlMs;
620
+ this.now = now;
621
+ }
622
+ authorizeToolCall(call) {
623
+ this.pruneExpired();
624
+ this.authorizations.push({
625
+ key: toolRelayCallKey(call),
626
+ expiresAt: this.now() + this.ttlMs
627
+ });
628
+ }
629
+ consumeToolCall(call) {
630
+ this.pruneExpired();
631
+ const key = toolRelayCallKey(call);
632
+ const index = this.authorizations.findIndex((auth) => auth.key === key);
633
+ if (index === -1) return false;
634
+ this.authorizations.splice(index, 1);
635
+ return true;
636
+ }
637
+ pruneExpired() {
638
+ const now = this.now();
639
+ for (let i = this.authorizations.length - 1; i >= 0; i--) {
640
+ if (this.authorizations[i].expiresAt <= now) {
641
+ this.authorizations.splice(i, 1);
642
+ }
643
+ }
644
+ }
645
+ };
646
+ function toolRelayCallKey({ toolName, input }) {
647
+ return `${toolName}\0${canonicalJson(input ?? {})}`;
648
+ }
649
+ function canonicalJson(value) {
650
+ return JSON.stringify(normalizeJsonValue(value));
651
+ }
652
+ function normalizeJsonValue(value) {
653
+ if (Array.isArray(value)) {
654
+ return value.map(normalizeJsonValue);
655
+ }
656
+ if (value && typeof value === "object") {
657
+ return Object.fromEntries(
658
+ Object.entries(value).filter(([, entryValue]) => entryValue !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, entryValue]) => [key, normalizeJsonValue(entryValue)])
659
+ );
660
+ }
661
+ return value;
662
+ }
663
+ async function isToolRelayRequestFromAllowedProcess({
664
+ socket,
665
+ allowedScriptPaths
666
+ }) {
667
+ if (process.platform !== "linux") return false;
668
+ if (!socket.remotePort || !socket.localPort) return false;
669
+ const inode = await findTcpSocketInode({
670
+ clientPort: socket.remotePort,
671
+ serverPort: socket.localPort
672
+ });
673
+ if (!inode) return false;
674
+ const cmdline = await findProcessCmdlineForSocketInode({ inode });
675
+ return cmdline?.some((arg) => allowedScriptPaths.has(arg)) ?? false;
676
+ }
677
+ async function findTcpSocketInode({
678
+ clientPort,
679
+ serverPort
680
+ }) {
681
+ for (const tablePath of ["/proc/net/tcp", "/proc/net/tcp6"]) {
682
+ const table = await readFile(tablePath, "utf8").catch(() => void 0);
683
+ if (!table) continue;
684
+ for (const line of table.split("\n").slice(1)) {
685
+ const columns = line.trim().split(/\s+/);
686
+ if (columns.length < 10) continue;
687
+ const local = parseProcNetAddress(columns[1]);
688
+ const remote = parseProcNetAddress(columns[2]);
689
+ if (local?.port === clientPort && remote?.port === serverPort && columns[9] !== "0") {
690
+ return columns[9];
691
+ }
692
+ }
693
+ }
694
+ return void 0;
695
+ }
696
+ function parseProcNetAddress(value) {
697
+ const [, portHex] = value.split(":");
698
+ if (!portHex) return void 0;
699
+ return { port: Number.parseInt(portHex, 16) };
700
+ }
701
+ async function findProcessCmdlineForSocketInode({
702
+ inode
703
+ }) {
704
+ const procEntries = await readdir("/proc", { withFileTypes: true }).catch(
705
+ () => []
706
+ );
707
+ for (const entry of procEntries) {
708
+ if (!entry.isDirectory() || !/^\d+$/.test(entry.name)) continue;
709
+ const fdDir = `/proc/${entry.name}/fd`;
710
+ const fds = await readdir(fdDir).catch(() => []);
711
+ for (const fd of fds) {
712
+ const target = await readlink(`${fdDir}/${fd}`).catch(() => void 0);
713
+ if (target !== `socket:[${inode}]`) continue;
714
+ const cmdline = await readFile(`/proc/${entry.name}/cmdline`, "utf8").then((value) => value.split("\0").filter(Boolean)).catch(() => void 0);
715
+ if (cmdline) return cmdline;
716
+ }
717
+ }
718
+ return void 0;
719
+ }
720
+
611
721
  // src/bridge/index.ts
612
722
  var NATIVE_TO_COMMON = {
613
723
  view: "read",
@@ -626,6 +736,19 @@ var OPENCODE_TO_WIRE = {
626
736
  agent: "agent",
627
737
  subtask: "agent"
628
738
  };
739
+ var PUBLIC_TO_NATIVE = {
740
+ read: "view",
741
+ write: "write",
742
+ edit: "edit",
743
+ bash: "bash",
744
+ glob: "glob",
745
+ grep: "grep",
746
+ ls: "list",
747
+ webfetch: "webfetch",
748
+ skill: "skill",
749
+ todowrite: "todowrite",
750
+ agent: "agent"
751
+ };
629
752
  var TOOL_KIND = {
630
753
  read: "readonly",
631
754
  glob: "readonly",
@@ -681,12 +804,10 @@ async function ensureRuntime({
681
804
  emit
682
805
  }) {
683
806
  if (runtime.client) return;
684
- let relayToken;
685
807
  if (start.tools && start.tools.length > 0) {
686
- relayToken = randomUUID();
687
808
  runtime.toolNames = new Set(start.tools.map((tool) => tool.name));
688
809
  runtime.relay = await startToolRelay({
689
- relayToken,
810
+ allowedScriptPaths: [`${bootstrapDir}/host-tool-mcp.mjs`],
690
811
  tools: start.tools,
691
812
  emit,
692
813
  requestToolResult: turn.requestToolResult
@@ -698,7 +819,6 @@ async function ensureRuntime({
698
819
  timeout: 3e4,
699
820
  config: buildOpenCodeConfig({
700
821
  start,
701
- relayToken,
702
822
  relayPort: runtime.relay?.port
703
823
  })
704
824
  });
@@ -710,7 +830,6 @@ async function ensureRuntime({
710
830
  }
711
831
  function buildOpenCodeConfig({
712
832
  start,
713
- relayToken,
714
833
  relayPort
715
834
  }) {
716
835
  const config = {
@@ -731,9 +850,21 @@ function buildOpenCodeConfig({
731
850
  };
732
851
  if (start.model) config.model = start.model;
733
852
  if (skillsDir) config.skills = { paths: [skillsDir] };
853
+ const inactiveToolNames = resolveInactiveBuiltinToolNames(start);
854
+ const permission = config.permission;
855
+ for (const toolName of inactiveToolNames) {
856
+ const permissionName = toPermissionToolName(
857
+ PUBLIC_TO_NATIVE[toolName] ?? toolName
858
+ );
859
+ if (permissionName === "ls") {
860
+ permission.list = "ask";
861
+ } else {
862
+ permission[permissionName] = "ask";
863
+ }
864
+ }
734
865
  const provider = buildProviderConfig(start);
735
866
  if (provider) config.provider = provider;
736
- if (relayToken && relayPort && start.tools && start.tools.length > 0) {
867
+ if (relayPort && start.tools && start.tools.length > 0) {
737
868
  config.mcp = {
738
869
  "harness-tools": {
739
870
  type: "local",
@@ -747,8 +878,7 @@ function buildOpenCodeConfig({
747
878
  inputSchema: t.inputSchema
748
879
  }))
749
880
  ),
750
- TOOL_RELAY_URL: `http://127.0.0.1:${relayPort}`,
751
- TOOL_RELAY_TOKEN: relayToken
881
+ TOOL_RELAY_URL: `http://127.0.0.1:${relayPort}`
752
882
  }
753
883
  }
754
884
  };
@@ -958,6 +1088,7 @@ async function runPrompt({
958
1088
  client,
959
1089
  sessionId,
960
1090
  permissionMode: start.permissionMode,
1091
+ builtinToolFiltering: start.builtinToolFiltering,
961
1092
  turn,
962
1093
  emit: (msg) => {
963
1094
  if (msg.type === "text-delta" || msg.type === "reasoning-delta") {
@@ -1071,6 +1202,7 @@ async function runCompaction({
1071
1202
  client,
1072
1203
  sessionId,
1073
1204
  permissionMode: start.permissionMode,
1205
+ builtinToolFiltering: start.builtinToolFiltering,
1074
1206
  turn,
1075
1207
  emit: (msg) => {
1076
1208
  if (msg.type === "compaction") sawCompaction = true;
@@ -1132,6 +1264,7 @@ async function consumeEvents({
1132
1264
  client,
1133
1265
  sessionId,
1134
1266
  permissionMode,
1267
+ builtinToolFiltering,
1135
1268
  turn,
1136
1269
  emit,
1137
1270
  signal,
@@ -1150,6 +1283,7 @@ async function consumeEvents({
1150
1283
  state,
1151
1284
  sessionId,
1152
1285
  permissionMode,
1286
+ builtinToolFiltering,
1153
1287
  client,
1154
1288
  turn,
1155
1289
  emit
@@ -1165,6 +1299,7 @@ function createTranslationState() {
1165
1299
  toolNames: /* @__PURE__ */ new Map(),
1166
1300
  toolCallsEmitted: /* @__PURE__ */ new Set(),
1167
1301
  toolResultsEmitted: /* @__PURE__ */ new Set(),
1302
+ hostToolCallsAuthorized: /* @__PURE__ */ new Set(),
1168
1303
  shellCommands: /* @__PURE__ */ new Map(),
1169
1304
  messageRoles: /* @__PURE__ */ new Map(),
1170
1305
  turnUsage: void 0,
@@ -1177,6 +1312,7 @@ async function translateAndEmit({
1177
1312
  state,
1178
1313
  sessionId,
1179
1314
  permissionMode,
1315
+ builtinToolFiltering,
1180
1316
  client,
1181
1317
  turn,
1182
1318
  emit
@@ -1334,7 +1470,16 @@ async function translateAndEmit({
1334
1470
  const rawToolName = String(props.tool ?? "unknown");
1335
1471
  const toolName = toWireToolName(rawToolName);
1336
1472
  state.toolNames.set(callID, { rawToolName, toolName });
1337
- if (isHostTool(toolName, props.tool)) return;
1473
+ const hostToolName = getHostToolName(toolName, props.tool);
1474
+ if (hostToolName) {
1475
+ authorizeHostToolCall({
1476
+ callID,
1477
+ toolName: hostToolName,
1478
+ input: props.input ?? parseToolInput(state, props),
1479
+ state
1480
+ });
1481
+ return;
1482
+ }
1338
1483
  emit({
1339
1484
  type: "tool-call",
1340
1485
  toolCallId: callID,
@@ -1351,7 +1496,7 @@ async function translateAndEmit({
1351
1496
  const cachedTool = state.toolNames.get(callID);
1352
1497
  const rawToolName = cachedTool?.rawToolName ?? String(props.tool ?? "");
1353
1498
  const toolName = cachedTool?.toolName ?? toWireToolName(rawToolName || "unknown");
1354
- if (isHostTool(toolName, rawToolName)) return;
1499
+ if (getHostToolName(toolName, rawToolName)) return;
1355
1500
  emit({
1356
1501
  type: "tool-result",
1357
1502
  toolCallId: callID,
@@ -1405,6 +1550,7 @@ async function translateAndEmit({
1405
1550
  client,
1406
1551
  sessionId,
1407
1552
  permissionMode,
1553
+ builtinToolFiltering,
1408
1554
  turn,
1409
1555
  emit,
1410
1556
  event
@@ -1416,6 +1562,7 @@ async function translateAndEmit({
1416
1562
  client,
1417
1563
  sessionId,
1418
1564
  permissionMode,
1565
+ builtinToolFiltering,
1419
1566
  turn,
1420
1567
  emit,
1421
1568
  event
@@ -1514,7 +1661,18 @@ function emitLegacyToolPart({
1514
1661
  const rawToolName = toolPart.tool;
1515
1662
  const toolName = toWireToolName(rawToolName);
1516
1663
  state.toolNames.set(callID, { rawToolName, toolName });
1517
- if (isHostTool(toolName, rawToolName)) return;
1664
+ const hostToolName = getHostToolName(toolName, rawToolName);
1665
+ if (hostToolName) {
1666
+ if (status === "running") {
1667
+ authorizeHostToolCall({
1668
+ callID,
1669
+ toolName: hostToolName,
1670
+ input: legacyToolPartInput(toolPart),
1671
+ state
1672
+ });
1673
+ }
1674
+ return;
1675
+ }
1518
1676
  if (!state.toolCallsEmitted.has(callID)) {
1519
1677
  state.toolCallsEmitted.add(callID);
1520
1678
  emit({
@@ -1572,6 +1730,7 @@ async function handlePermissionV2({
1572
1730
  client,
1573
1731
  sessionId,
1574
1732
  permissionMode,
1733
+ builtinToolFiltering,
1575
1734
  turn,
1576
1735
  emit,
1577
1736
  event
@@ -1585,6 +1744,7 @@ async function handlePermissionV2({
1585
1744
  requestID,
1586
1745
  toolCallId: typeof props.source === "object" && props.source !== null && "callID" in props.source ? String(props.source.callID) : requestID,
1587
1746
  permissionMode,
1747
+ builtinToolFiltering,
1588
1748
  turn,
1589
1749
  emit
1590
1750
  });
@@ -1599,6 +1759,7 @@ async function handlePermission({
1599
1759
  client,
1600
1760
  sessionId,
1601
1761
  permissionMode,
1762
+ builtinToolFiltering,
1602
1763
  turn,
1603
1764
  emit,
1604
1765
  event
@@ -1612,6 +1773,7 @@ async function handlePermission({
1612
1773
  requestID,
1613
1774
  toolCallId: typeof props.tool === "object" && props.tool !== null && "callID" in props.tool ? String(props.tool.callID) : requestID,
1614
1775
  permissionMode,
1776
+ builtinToolFiltering,
1615
1777
  turn,
1616
1778
  emit
1617
1779
  });
@@ -1629,6 +1791,7 @@ async function selectPermissionReply({
1629
1791
  requestID,
1630
1792
  toolCallId,
1631
1793
  permissionMode,
1794
+ builtinToolFiltering,
1632
1795
  turn,
1633
1796
  emit
1634
1797
  }) {
@@ -1636,6 +1799,18 @@ async function selectPermissionReply({
1636
1799
  if (resources.some((resource) => isExternalPath(resource))) {
1637
1800
  return { reply: "reject", message: "External directory access rejected." };
1638
1801
  }
1802
+ if (isBuiltinToolInactive({ toolName, toolFiltering: builtinToolFiltering })) {
1803
+ emit({
1804
+ type: "tool-approval-request",
1805
+ approvalId: requestID,
1806
+ toolCallId
1807
+ });
1808
+ const decision2 = await turn.requestToolApproval(requestID);
1809
+ return decision2.approved ? { reply: "once" } : {
1810
+ reply: "reject",
1811
+ ...decision2.reason ? { message: decision2.reason } : {}
1812
+ };
1813
+ }
1639
1814
  if (!permissionMode || permissionMode === "allow-all") {
1640
1815
  return { reply: "always" };
1641
1816
  }
@@ -1668,6 +1843,17 @@ function toPermissionToolName(action) {
1668
1843
  if (normalized.includes("read")) return "read";
1669
1844
  return toWireToolName(normalized);
1670
1845
  }
1846
+ function resolveInactiveBuiltinToolNames(start) {
1847
+ const toolFiltering = start.builtinToolFiltering;
1848
+ if (toolFiltering == null) return [];
1849
+ return toolFiltering.mode === "allow" ? Object.keys(PUBLIC_TO_NATIVE).filter(
1850
+ (name) => !toolFiltering.toolNames.includes(name)
1851
+ ) : toolFiltering.toolNames;
1852
+ }
1853
+ function isBuiltinToolInactive(input) {
1854
+ if (input.toolFiltering == null) return false;
1855
+ return input.toolFiltering.mode === "allow" ? !input.toolFiltering.toolNames.includes(input.toolName) : input.toolFiltering.toolNames.includes(input.toolName);
1856
+ }
1671
1857
  function isExternalPath(resource) {
1672
1858
  if (!path2.isAbsolute(resource)) return false;
1673
1859
  const normalized = path2.resolve(resource);
@@ -1687,15 +1873,25 @@ function nativeNameField({
1687
1873
  if (!nativeName || nativeName === toolName || toolName === "agent") return {};
1688
1874
  return { nativeName };
1689
1875
  }
1690
- function isHostTool(toolName, rawToolName) {
1691
- if (runtime.toolNames.has(toolName)) return true;
1876
+ function getHostToolName(toolName, rawToolName) {
1877
+ if (runtime.toolNames.has(toolName)) return toolName;
1692
1878
  if (typeof rawToolName === "string" && runtime.toolNames.has(rawToolName)) {
1693
- return true;
1879
+ return rawToolName;
1694
1880
  }
1695
1881
  if (typeof rawToolName === "string" && rawToolName.startsWith("harness-tools_") && runtime.toolNames.has(rawToolName.slice("harness-tools_".length))) {
1696
- return true;
1882
+ return rawToolName.slice("harness-tools_".length);
1697
1883
  }
1698
- return false;
1884
+ return void 0;
1885
+ }
1886
+ function authorizeHostToolCall({
1887
+ callID,
1888
+ toolName,
1889
+ input,
1890
+ state
1891
+ }) {
1892
+ if (state.hostToolCallsAuthorized.has(callID)) return;
1893
+ state.hostToolCallsAuthorized.add(callID);
1894
+ runtime.relay?.authorizeToolCall({ toolName, input });
1699
1895
  }
1700
1896
  async function emitContextFallback({
1701
1897
  client,
@@ -1802,15 +1998,17 @@ function mapFinishReason(reason) {
1802
1998
  return "other";
1803
1999
  }
1804
2000
  async function startToolRelay({
1805
- relayToken,
2001
+ allowedScriptPaths,
1806
2002
  tools,
1807
2003
  emit,
1808
2004
  requestToolResult
1809
2005
  }) {
1810
2006
  const toolNames = new Set(tools.map((t) => t.name));
2007
+ const allowedScriptPathSet = new Set(allowedScriptPaths);
2008
+ const authorizer = new ToolRelayAuthorizer();
1811
2009
  const server = createServer(async (req, res) => {
1812
2010
  try {
1813
- if (req.method !== "POST" || req.url !== "/" || req.headers.authorization !== `Bearer ${relayToken}`) {
2011
+ if (req.method !== "POST" || req.url !== "/") {
1814
2012
  res.writeHead(401, { "Content-Type": "application/json" });
1815
2013
  res.end(JSON.stringify({ error: "unauthorized tool relay request" }));
1816
2014
  return;
@@ -1828,6 +2026,15 @@ async function startToolRelay({
1828
2026
  );
1829
2027
  return;
1830
2028
  }
2029
+ const authorized = authorizer.consumeToolCall({ toolName, input }) || await isToolRelayRequestFromAllowedProcess({
2030
+ socket: req.socket,
2031
+ allowedScriptPaths: allowedScriptPathSet
2032
+ });
2033
+ if (!authorized) {
2034
+ res.writeHead(401, { "Content-Type": "application/json" });
2035
+ res.end(JSON.stringify({ error: "unauthorized tool relay request" }));
2036
+ return;
2037
+ }
1831
2038
  emit({
1832
2039
  type: "tool-call",
1833
2040
  toolCallId: requestId,
@@ -1863,7 +2070,8 @@ async function startToolRelay({
1863
2070
  }
1864
2071
  return {
1865
2072
  port: address.port,
1866
- close: () => closeServer(server)
2073
+ close: () => closeServer(server),
2074
+ authorizeToolCall: (call) => authorizer.authorizeToolCall(call)
1867
2075
  };
1868
2076
  }
1869
2077
  function closeServer(server) {