@ai-sdk/harness-opencode 1.0.10 → 1.0.11

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-opencode
2
2
 
3
+ ## 1.0.11
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.10
4
11
 
5
12
  ### 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",
@@ -681,12 +791,10 @@ async function ensureRuntime({
681
791
  emit
682
792
  }) {
683
793
  if (runtime.client) return;
684
- let relayToken;
685
794
  if (start.tools && start.tools.length > 0) {
686
- relayToken = randomUUID();
687
795
  runtime.toolNames = new Set(start.tools.map((tool) => tool.name));
688
796
  runtime.relay = await startToolRelay({
689
- relayToken,
797
+ allowedScriptPaths: [`${bootstrapDir}/host-tool-mcp.mjs`],
690
798
  tools: start.tools,
691
799
  emit,
692
800
  requestToolResult: turn.requestToolResult
@@ -698,7 +806,6 @@ async function ensureRuntime({
698
806
  timeout: 3e4,
699
807
  config: buildOpenCodeConfig({
700
808
  start,
701
- relayToken,
702
809
  relayPort: runtime.relay?.port
703
810
  })
704
811
  });
@@ -710,7 +817,6 @@ async function ensureRuntime({
710
817
  }
711
818
  function buildOpenCodeConfig({
712
819
  start,
713
- relayToken,
714
820
  relayPort
715
821
  }) {
716
822
  const config = {
@@ -733,7 +839,7 @@ function buildOpenCodeConfig({
733
839
  if (skillsDir) config.skills = { paths: [skillsDir] };
734
840
  const provider = buildProviderConfig(start);
735
841
  if (provider) config.provider = provider;
736
- if (relayToken && relayPort && start.tools && start.tools.length > 0) {
842
+ if (relayPort && start.tools && start.tools.length > 0) {
737
843
  config.mcp = {
738
844
  "harness-tools": {
739
845
  type: "local",
@@ -747,8 +853,7 @@ function buildOpenCodeConfig({
747
853
  inputSchema: t.inputSchema
748
854
  }))
749
855
  ),
750
- TOOL_RELAY_URL: `http://127.0.0.1:${relayPort}`,
751
- TOOL_RELAY_TOKEN: relayToken
856
+ TOOL_RELAY_URL: `http://127.0.0.1:${relayPort}`
752
857
  }
753
858
  }
754
859
  };
@@ -1165,6 +1270,7 @@ function createTranslationState() {
1165
1270
  toolNames: /* @__PURE__ */ new Map(),
1166
1271
  toolCallsEmitted: /* @__PURE__ */ new Set(),
1167
1272
  toolResultsEmitted: /* @__PURE__ */ new Set(),
1273
+ hostToolCallsAuthorized: /* @__PURE__ */ new Set(),
1168
1274
  shellCommands: /* @__PURE__ */ new Map(),
1169
1275
  messageRoles: /* @__PURE__ */ new Map(),
1170
1276
  turnUsage: void 0,
@@ -1334,7 +1440,16 @@ async function translateAndEmit({
1334
1440
  const rawToolName = String(props.tool ?? "unknown");
1335
1441
  const toolName = toWireToolName(rawToolName);
1336
1442
  state.toolNames.set(callID, { rawToolName, toolName });
1337
- if (isHostTool(toolName, props.tool)) return;
1443
+ const hostToolName = getHostToolName(toolName, props.tool);
1444
+ if (hostToolName) {
1445
+ authorizeHostToolCall({
1446
+ callID,
1447
+ toolName: hostToolName,
1448
+ input: props.input ?? parseToolInput(state, props),
1449
+ state
1450
+ });
1451
+ return;
1452
+ }
1338
1453
  emit({
1339
1454
  type: "tool-call",
1340
1455
  toolCallId: callID,
@@ -1351,7 +1466,7 @@ async function translateAndEmit({
1351
1466
  const cachedTool = state.toolNames.get(callID);
1352
1467
  const rawToolName = cachedTool?.rawToolName ?? String(props.tool ?? "");
1353
1468
  const toolName = cachedTool?.toolName ?? toWireToolName(rawToolName || "unknown");
1354
- if (isHostTool(toolName, rawToolName)) return;
1469
+ if (getHostToolName(toolName, rawToolName)) return;
1355
1470
  emit({
1356
1471
  type: "tool-result",
1357
1472
  toolCallId: callID,
@@ -1514,7 +1629,18 @@ function emitLegacyToolPart({
1514
1629
  const rawToolName = toolPart.tool;
1515
1630
  const toolName = toWireToolName(rawToolName);
1516
1631
  state.toolNames.set(callID, { rawToolName, toolName });
1517
- if (isHostTool(toolName, rawToolName)) return;
1632
+ const hostToolName = getHostToolName(toolName, rawToolName);
1633
+ if (hostToolName) {
1634
+ if (status === "running") {
1635
+ authorizeHostToolCall({
1636
+ callID,
1637
+ toolName: hostToolName,
1638
+ input: legacyToolPartInput(toolPart),
1639
+ state
1640
+ });
1641
+ }
1642
+ return;
1643
+ }
1518
1644
  if (!state.toolCallsEmitted.has(callID)) {
1519
1645
  state.toolCallsEmitted.add(callID);
1520
1646
  emit({
@@ -1687,15 +1813,25 @@ function nativeNameField({
1687
1813
  if (!nativeName || nativeName === toolName || toolName === "agent") return {};
1688
1814
  return { nativeName };
1689
1815
  }
1690
- function isHostTool(toolName, rawToolName) {
1691
- if (runtime.toolNames.has(toolName)) return true;
1816
+ function getHostToolName(toolName, rawToolName) {
1817
+ if (runtime.toolNames.has(toolName)) return toolName;
1692
1818
  if (typeof rawToolName === "string" && runtime.toolNames.has(rawToolName)) {
1693
- return true;
1819
+ return rawToolName;
1694
1820
  }
1695
1821
  if (typeof rawToolName === "string" && rawToolName.startsWith("harness-tools_") && runtime.toolNames.has(rawToolName.slice("harness-tools_".length))) {
1696
- return true;
1822
+ return rawToolName.slice("harness-tools_".length);
1697
1823
  }
1698
- return false;
1824
+ return void 0;
1825
+ }
1826
+ function authorizeHostToolCall({
1827
+ callID,
1828
+ toolName,
1829
+ input,
1830
+ state
1831
+ }) {
1832
+ if (state.hostToolCallsAuthorized.has(callID)) return;
1833
+ state.hostToolCallsAuthorized.add(callID);
1834
+ runtime.relay?.authorizeToolCall({ toolName, input });
1699
1835
  }
1700
1836
  async function emitContextFallback({
1701
1837
  client,
@@ -1802,15 +1938,17 @@ function mapFinishReason(reason) {
1802
1938
  return "other";
1803
1939
  }
1804
1940
  async function startToolRelay({
1805
- relayToken,
1941
+ allowedScriptPaths,
1806
1942
  tools,
1807
1943
  emit,
1808
1944
  requestToolResult
1809
1945
  }) {
1810
1946
  const toolNames = new Set(tools.map((t) => t.name));
1947
+ const allowedScriptPathSet = new Set(allowedScriptPaths);
1948
+ const authorizer = new ToolRelayAuthorizer();
1811
1949
  const server = createServer(async (req, res) => {
1812
1950
  try {
1813
- if (req.method !== "POST" || req.url !== "/" || req.headers.authorization !== `Bearer ${relayToken}`) {
1951
+ if (req.method !== "POST" || req.url !== "/") {
1814
1952
  res.writeHead(401, { "Content-Type": "application/json" });
1815
1953
  res.end(JSON.stringify({ error: "unauthorized tool relay request" }));
1816
1954
  return;
@@ -1828,6 +1966,15 @@ async function startToolRelay({
1828
1966
  );
1829
1967
  return;
1830
1968
  }
1969
+ const authorized = authorizer.consumeToolCall({ toolName, input }) || await isToolRelayRequestFromAllowedProcess({
1970
+ socket: req.socket,
1971
+ allowedScriptPaths: allowedScriptPathSet
1972
+ });
1973
+ if (!authorized) {
1974
+ res.writeHead(401, { "Content-Type": "application/json" });
1975
+ res.end(JSON.stringify({ error: "unauthorized tool relay request" }));
1976
+ return;
1977
+ }
1831
1978
  emit({
1832
1979
  type: "tool-call",
1833
1980
  toolCallId: requestId,
@@ -1863,7 +2010,8 @@ async function startToolRelay({
1863
2010
  }
1864
2011
  return {
1865
2012
  port: address.port,
1866
- close: () => closeServer(server)
2013
+ close: () => closeServer(server),
2014
+ authorizeToolCall: (call) => authorizer.authorizeToolCall(call)
1867
2015
  };
1868
2016
  }
1869
2017
  function closeServer(server) {