@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 +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 +227 -19
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.js +20 -78
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/bridge/host-tool-mcp.ts +0 -2
- package/src/bridge/index.ts +170 -27
- package/src/bridge/tool-relay-auth.ts +151 -0
- package/src/opencode-harness.ts +19 -85
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 || '';\
|
|
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":";;;AAMA,SAAS,iBAAiB;AAC1B,SAAS,4BAA4B;AACrC,SAAS,SAAS;AAwBlB,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,WAAW,QAAgD;AAClE,MAAI,CAAC,QAAQ,WAAY,QAAO,CAAC;AACjC,QAAM,WAAW,IAAI,IAAI,OAAO,YAAY,CAAC,CAAC;AAC9C,QAAM,QAAkB,CAAC;AACzB,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
|
@@ -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
|
-
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
1691
|
-
if (runtime.toolNames.has(toolName)) return
|
|
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
|
|
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
|
|
1882
|
+
return rawToolName.slice("harness-tools_".length);
|
|
1697
1883
|
}
|
|
1698
|
-
return
|
|
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
|
-
|
|
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 !== "/"
|
|
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) {
|