@ai-sdk/harness-acp 1.0.76 → 1.0.77

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,11 @@
1
1
  # @ai-sdk/harness-acp
2
2
 
3
+ ## 1.0.77
4
+
5
+ ### Patch Changes
6
+
7
+ - 58030b5: fix(harness-acp): enforce tool call match with host tool relay
8
+
3
9
  ## 1.0.76
4
10
 
5
11
  ### Patch Changes
@@ -6,7 +6,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6
6
  import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from "@modelcontextprotocol/sdk/types.js";
7
7
  import { Agent, request } from "node:http";
8
8
  //#region src/v1/bridge/host-tool-mcp-server.ts
9
- const VERSION = "1.0.76";
9
+ const VERSION = "1.0.77";
10
10
  function createHostToolMCPServer({ tools, revision = 1, invoke, onListTools }) {
11
11
  let catalog = createCatalog({
12
12
  revision,
@@ -736,7 +736,7 @@ function mergeRecords({ left, right }) {
736
736
  const result = { ...left };
737
737
  for (const [key, value] of Object.entries(right)) {
738
738
  const previous = result[key];
739
- result[key] = isRecord$6(previous) && isRecord$6(value) ? mergeRecords({
739
+ result[key] = isRecord$7(previous) && isRecord$7(value) ? mergeRecords({
740
740
  left: previous,
741
741
  right: value
742
742
  }) : value;
@@ -748,10 +748,10 @@ function requireGateway({ gateway }) {
748
748
  return gateway;
749
749
  }
750
750
  function asRecord(value) {
751
- if (!isRecord$6(value)) throw new Error("ACP profile data must resolve to an object.");
751
+ if (!isRecord$7(value)) throw new Error("ACP profile data must resolve to an object.");
752
752
  return value;
753
753
  }
754
- function isRecord$6(value) {
754
+ function isRecord$7(value) {
755
755
  return value != null && typeof value === "object" && !Array.isArray(value);
756
756
  }
757
757
  //#endregion
@@ -817,27 +817,27 @@ function captureACPStream({ stream }) {
817
817
  }
818
818
  function getRawSessionUpdate({ message }) {
819
819
  const record = message;
820
- if (record.method !== "session/update" || !isRecord$5(record.params)) return { isSessionUpdate: false };
820
+ if (record.method !== "session/update" || !isRecord$6(record.params)) return { isSessionUpdate: false };
821
821
  const params = record.params;
822
822
  return {
823
823
  isSessionUpdate: true,
824
824
  value: params.update,
825
- canFilterUnknown: typeof params.sessionId === "string" && isRecord$5(params.update)
825
+ canFilterUnknown: typeof params.sessionId === "string" && isRecord$6(params.update)
826
826
  };
827
827
  }
828
828
  function sessionUpdatesMatch({ rawUpdate, update }) {
829
- if (!isRecord$5(rawUpdate)) return false;
829
+ if (!isRecord$6(rawUpdate)) return false;
830
830
  if (rawUpdate.sessionUpdate !== update.sessionUpdate) return false;
831
831
  if (update.sessionUpdate === "tool_call" || update.sessionUpdate === "tool_call_update") return rawUpdate.toolCallId === update.toolCallId;
832
832
  if (update.sessionUpdate === "user_message_chunk" || update.sessionUpdate === "agent_message_chunk" || update.sessionUpdate === "agent_thought_chunk") return rawUpdate.messageId === update.messageId;
833
833
  return true;
834
834
  }
835
835
  function getStringProperty$1({ value, property }) {
836
- if (!isRecord$5(value)) return void 0;
836
+ if (!isRecord$6(value)) return void 0;
837
837
  const propertyValue = value[property];
838
838
  return typeof propertyValue === "string" ? propertyValue : void 0;
839
839
  }
840
- function isRecord$5(value) {
840
+ function isRecord$6(value) {
841
841
  return value != null && typeof value === "object" && !Array.isArray(value);
842
842
  }
843
843
  //#endregion
@@ -1223,10 +1223,10 @@ function resolveBuiltinTool({ programmaticName, metadata, title, kind, rawInput,
1223
1223
  });
1224
1224
  }
1225
1225
  function findMostSpecificSchemaMatch({ rawInput, schemaMatches }) {
1226
- if (!isRecord$4(rawInput)) return void 0;
1226
+ if (!isRecord$5(rawInput)) return void 0;
1227
1227
  const rawInputKeys = Object.keys(rawInput);
1228
1228
  const coverageOf = (tool) => {
1229
- const properties = isRecord$4(tool.inputSchema) ? isRecord$4(tool.inputSchema.properties) ? tool.inputSchema.properties : {} : {};
1229
+ const properties = isRecord$5(tool.inputSchema) ? isRecord$5(tool.inputSchema.properties) ? tool.inputSchema.properties : {} : {};
1230
1230
  return rawInputKeys.filter((key) => key in properties).length;
1231
1231
  };
1232
1232
  const highestCoverage = Math.max(...schemaMatches.map(coverageOf));
@@ -1275,7 +1275,7 @@ function findBuiltinToolsInMetadata({ metadata, builtinToolsByName }) {
1275
1275
  };
1276
1276
  }
1277
1277
  function matchesBuiltinToolInput({ rawInput, inputSchema }) {
1278
- if (!isRecord$4(rawInput) || !isRecord$4(inputSchema)) return false;
1278
+ if (!isRecord$5(rawInput) || !isRecord$5(inputSchema)) return false;
1279
1279
  if (inputSchema.type !== "object") return false;
1280
1280
  const required = inputSchema.required;
1281
1281
  if (!Array.isArray(required) || required.length === 0 || !required.every((property) => typeof property === "string")) return false;
@@ -1283,7 +1283,7 @@ function matchesBuiltinToolInput({ rawInput, inputSchema }) {
1283
1283
  rawInput,
1284
1284
  inputSchema
1285
1285
  })) return false;
1286
- const properties = isRecord$4(inputSchema.properties) ? inputSchema.properties : {};
1286
+ const properties = isRecord$5(inputSchema.properties) ? inputSchema.properties : {};
1287
1287
  return Object.entries(properties).filter(([, schema]) => isJSONSchemaDiscriminator({ schema })).every(([property, schema]) => {
1288
1288
  if (!Object.prototype.hasOwnProperty.call(rawInput, property)) return false;
1289
1289
  return matchesJSONSchemaValue({
@@ -1293,12 +1293,12 @@ function matchesBuiltinToolInput({ rawInput, inputSchema }) {
1293
1293
  });
1294
1294
  }
1295
1295
  function hasRequiredBuiltinToolInput({ rawInput, inputSchema }) {
1296
- if (!isRecord$4(inputSchema) || inputSchema.type !== "object") return true;
1297
- if (!isRecord$4(rawInput)) return false;
1296
+ if (!isRecord$5(inputSchema) || inputSchema.type !== "object") return true;
1297
+ if (!isRecord$5(rawInput)) return false;
1298
1298
  const required = inputSchema.required;
1299
1299
  if (!Array.isArray(required) || required.length === 0) return true;
1300
1300
  if (!required.every((property) => typeof property === "string")) return true;
1301
- const properties = isRecord$4(inputSchema.properties) ? inputSchema.properties : {};
1301
+ const properties = isRecord$5(inputSchema.properties) ? inputSchema.properties : {};
1302
1302
  return required.every((property) => {
1303
1303
  if (!Object.prototype.hasOwnProperty.call(rawInput, property)) return false;
1304
1304
  return matchesJSONSchemaValue({
@@ -1308,11 +1308,11 @@ function hasRequiredBuiltinToolInput({ rawInput, inputSchema }) {
1308
1308
  });
1309
1309
  }
1310
1310
  function isJSONSchemaDiscriminator({ schema }) {
1311
- return isRecord$4(schema) && ("const" in schema || Array.isArray(schema.enum) && schema.enum.length === 1);
1311
+ return isRecord$5(schema) && ("const" in schema || Array.isArray(schema.enum) && schema.enum.length === 1);
1312
1312
  }
1313
1313
  function matchesJSONSchemaValue({ value, schema }) {
1314
1314
  if (schema === true) return true;
1315
- if (schema === false || !isRecord$4(schema)) return false;
1315
+ if (schema === false || !isRecord$5(schema)) return false;
1316
1316
  if ("const" in schema && !Object.is(value, schema.const)) return false;
1317
1317
  if (Array.isArray(schema.enum) && !schema.enum.some((candidate) => Object.is(value, candidate))) return false;
1318
1318
  for (const alternatives of [schema.anyOf, schema.oneOf]) if (Array.isArray(alternatives) && !alternatives.some((alternative) => matchesJSONSchemaValue({
@@ -1327,7 +1327,7 @@ function matchesJSONSchemaValue({ value, schema }) {
1327
1327
  case "number": return typeof value === "number" && Number.isFinite(value);
1328
1328
  case "string": return typeof value === "string";
1329
1329
  case "array": return Array.isArray(value);
1330
- case "object": return isRecord$4(value);
1330
+ case "object": return isRecord$5(value);
1331
1331
  default: return true;
1332
1332
  }
1333
1333
  }
@@ -1373,7 +1373,7 @@ function createACPToolCall({ state }) {
1373
1373
  ...Array.isArray(state.values.locations) ? { locations: state.values.locations } : {},
1374
1374
  ...Object.prototype.hasOwnProperty.call(state.values, "rawInput") ? { rawInput: state.values.rawInput } : {},
1375
1375
  ...Object.prototype.hasOwnProperty.call(state.values, "rawOutput") ? { rawOutput: state.values.rawOutput } : {},
1376
- ...state.values._meta === null || isRecord$4(state.values._meta) ? { _meta: state.values._meta } : {}
1376
+ ...state.values._meta === null || isRecord$5(state.values._meta) ? { _meta: state.values._meta } : {}
1377
1377
  };
1378
1378
  }
1379
1379
  function isACPToolKind(value) {
@@ -1385,7 +1385,7 @@ function isACPToolCallStatus(value) {
1385
1385
  function emitFileChanges({ state, emit }) {
1386
1386
  const content = Array.isArray(state.values.content) ? state.values.content : [];
1387
1387
  for (const item of content) {
1388
- if (!isRecord$4(item) || item.type !== "diff") continue;
1388
+ if (!isRecord$5(item) || item.type !== "diff") continue;
1389
1389
  const path = getStringProperty({
1390
1390
  value: item,
1391
1391
  property: "path"
@@ -1476,11 +1476,11 @@ function toSafeJSONValue({ value, fallback, seen = /* @__PURE__ */ new Set() })
1476
1476
  return result;
1477
1477
  }
1478
1478
  function getStringProperty({ value, property }) {
1479
- if (!isRecord$4(value)) return void 0;
1479
+ if (!isRecord$5(value)) return void 0;
1480
1480
  const propertyValue = value[property];
1481
1481
  return typeof propertyValue === "string" ? propertyValue : void 0;
1482
1482
  }
1483
- function isRecord$4(value) {
1483
+ function isRecord$5(value) {
1484
1484
  return value != null && typeof value === "object" && !Array.isArray(value);
1485
1485
  }
1486
1486
  function mapACPUsage({ usage }) {
@@ -1527,6 +1527,17 @@ function unknownUsage$1() {
1527
1527
  };
1528
1528
  }
1529
1529
  //#endregion
1530
+ //#region src/v1/bridge/canonical-json-fingerprint.ts
1531
+ function canonicalFingerprint({ value }) {
1532
+ return JSON.stringify(canonicalizeJSON({ value }));
1533
+ }
1534
+ function canonicalizeJSON({ value }) {
1535
+ if (Array.isArray(value)) return value.map((item) => canonicalizeJSON({ value: item }));
1536
+ if (value == null || typeof value !== "object") return value;
1537
+ const record = value;
1538
+ return Object.fromEntries(Object.keys(record).sort().filter((key) => record[key] !== void 0).map((key) => [key, canonicalizeJSON({ value: record[key] })]));
1539
+ }
1540
+ //#endregion
1530
1541
  //#region src/v1/bridge/host-tool-correlation.ts
1531
1542
  const CORRELATION_WINDOW_MS = 1e3;
1532
1543
  const MAX_BUFFERED_UPDATES = 128;
@@ -1770,7 +1781,7 @@ function hasPortableEvidence({ candidate, invocation }) {
1770
1781
  hasServerIdentity = true;
1771
1782
  hasToolName = true;
1772
1783
  }
1773
- if (typeof deferredToolName === "string" && isRecord$3(deferredToolInput) && hasDelimitedPair({
1784
+ if (typeof deferredToolName === "string" && isRecord$4(deferredToolInput) && hasDelimitedPair({
1774
1785
  value: deferredToolName,
1775
1786
  serverName: invocation.serverName,
1776
1787
  toolName: invocation.toolName
@@ -1795,7 +1806,7 @@ function hasPortableEvidence({ candidate, invocation }) {
1795
1806
  return hasServerIdentity && hasToolName && hasInput;
1796
1807
  }
1797
1808
  function resolvePermissionHostTool({ toolCall, serverName, toolName }) {
1798
- if (!isRecord$3(toolCall.rawInput)) return void 0;
1809
+ if (!isRecord$4(toolCall.rawInput)) return void 0;
1799
1810
  const deferredToolName = getProperty({
1800
1811
  value: toolCall.rawInput,
1801
1812
  property: "tool_name"
@@ -1811,7 +1822,7 @@ function resolvePermissionHostTool({ toolCall, serverName, toolName }) {
1811
1822
  value: toolCall.rawInput,
1812
1823
  property: "tool_input"
1813
1824
  })) {
1814
- if (typeof deferredToolName !== "string" || !isRecord$3(deferredToolInput) || !hasDelimitedPair({
1825
+ if (typeof deferredToolName !== "string" || !isRecord$4(deferredToolInput) || !hasDelimitedPair({
1815
1826
  value: deferredToolName,
1816
1827
  serverName,
1817
1828
  toolName
@@ -1844,21 +1855,14 @@ function resolvePermissionHostTool({ toolCall, serverName, toolName }) {
1844
1855
  };
1845
1856
  }
1846
1857
  function containsCombinedIdentity({ value, serverName, toolName }) {
1847
- const seen = /* @__PURE__ */ new Set();
1848
- const visit = (candidate) => {
1849
- if (typeof candidate === "string") return hasDelimitedPair({
1858
+ return containsString({
1859
+ value,
1860
+ matches: (candidate) => hasDelimitedPair({
1850
1861
  value: candidate,
1851
1862
  serverName,
1852
1863
  toolName
1853
- });
1854
- if (candidate == null || typeof candidate !== "object") return false;
1855
- if (seen.has(candidate)) return false;
1856
- seen.add(candidate);
1857
- const values = Array.isArray(candidate) ? candidate : Object.values(candidate);
1858
- for (const item of values) if (visit(item)) return true;
1859
- return false;
1860
- };
1861
- return visit(value);
1864
+ })
1865
+ });
1862
1866
  }
1863
1867
  function hasDelimitedPair({ value, serverName, toolName }) {
1864
1868
  if (serverName.length === 0 || toolName.length === 0) return false;
@@ -1898,9 +1902,15 @@ function isToolUpdate(message) {
1898
1902
  return message.update.sessionUpdate === "tool_call" || message.update.sessionUpdate === "tool_call_update";
1899
1903
  }
1900
1904
  function containsExactValue({ value, target }) {
1905
+ return containsString({
1906
+ value,
1907
+ matches: (candidate) => candidate === target
1908
+ });
1909
+ }
1910
+ function containsString({ value, matches }) {
1901
1911
  const seen = /* @__PURE__ */ new Set();
1902
1912
  const visit = (candidate) => {
1903
- if (candidate === target) return true;
1913
+ if (typeof candidate === "string") return matches(candidate);
1904
1914
  if (candidate == null || typeof candidate !== "object") return false;
1905
1915
  if (seen.has(candidate)) return false;
1906
1916
  seen.add(candidate);
@@ -1924,14 +1934,6 @@ function containsFingerprint({ value, target }) {
1924
1934
  };
1925
1935
  return visit(value);
1926
1936
  }
1927
- function canonicalFingerprint({ value }) {
1928
- return JSON.stringify(canonicalizeJSON$1({ value }));
1929
- }
1930
- function canonicalizeJSON$1({ value }) {
1931
- if (Array.isArray(value)) return value.map((item) => canonicalizeJSON$1({ value: item }));
1932
- if (value == null || typeof value !== "object") return value;
1933
- return Object.fromEntries(Object.keys(value).sort().filter((key) => value[key] !== void 0).map((key) => [key, canonicalizeJSON$1({ value: value[key] })]));
1934
- }
1935
1937
  function getProperty({ value, property }) {
1936
1938
  if (value == null || typeof value !== "object" || Array.isArray(value)) return;
1937
1939
  return Reflect.get(value, property);
@@ -1939,7 +1941,7 @@ function getProperty({ value, property }) {
1939
1941
  function hasOwnProperty({ value, property }) {
1940
1942
  return Object.prototype.hasOwnProperty.call(value, property);
1941
1943
  }
1942
- function isRecord$3(value) {
1944
+ function isRecord$4(value) {
1943
1945
  return value != null && typeof value === "object" && !Array.isArray(value);
1944
1946
  }
1945
1947
  function isTerminalToolUpdate(message) {
@@ -2059,7 +2061,7 @@ function setStringAtPath({ record, path, value }) {
2059
2061
  return {
2060
2062
  ...record,
2061
2063
  [key]: remainingPath.length === 0 ? value : setStringAtPath({
2062
- record: isRecord$2(record[key]) ? record[key] : {},
2064
+ record: isRecord$3(record[key]) ? record[key] : {},
2063
2065
  path: remainingPath,
2064
2066
  value
2065
2067
  })
@@ -2068,12 +2070,12 @@ function setStringAtPath({ record, path, value }) {
2068
2070
  function assertSafePath({ path }) {
2069
2071
  if (path.length === 0 || path.some((segment) => segment.length === 0 || UNSAFE_PATH_SEGMENTS.has(segment))) throw new Error("ACP instruction mapping path must contain only safe, non-empty property names.");
2070
2072
  }
2071
- function isRecord$2(value) {
2073
+ function isRecord$3(value) {
2072
2074
  return value != null && typeof value === "object" && !Array.isArray(value);
2073
2075
  }
2074
2076
  //#endregion
2075
2077
  //#region src/v1/bridge/host-tool-mcp-server.ts
2076
- const VERSION = "1.0.76";
2078
+ const VERSION = "1.0.77";
2077
2079
  function createHostToolMCPServer({ tools, revision = 1, invoke, onListTools }) {
2078
2080
  let catalog = createCatalog({
2079
2081
  revision,
@@ -2429,7 +2431,7 @@ async function handleRequest({ request, credential, state, serverName, turn, nex
2429
2431
  });
2430
2432
  }
2431
2433
  async function handleCatalogNext({ body, state }) {
2432
- if (!isRecord$1(body) || !Number.isSafeInteger(body.afterRevision) || body.afterRevision < 0) throw new RelayRequestError({
2434
+ if (!isRecord$2(body) || !Number.isSafeInteger(body.afterRevision) || body.afterRevision < 0) throw new RelayRequestError({
2433
2435
  status: 400,
2434
2436
  message: "Invalid host tool catalog poll request."
2435
2437
  });
@@ -2445,7 +2447,7 @@ async function handleCatalogNext({ body, state }) {
2445
2447
  } : { revision: state.revision };
2446
2448
  }
2447
2449
  function handleCatalogSeen({ body, state }) {
2448
- if (!isRecord$1(body) || !Number.isSafeInteger(body.revision) || body.revision < 1 || body.revision > state.revision) throw new RelayRequestError({
2450
+ if (!isRecord$2(body) || !Number.isSafeInteger(body.revision) || body.revision < 1 || body.revision > state.revision) throw new RelayRequestError({
2449
2451
  status: 400,
2450
2452
  message: "Invalid host tool catalog acknowledgment."
2451
2453
  });
@@ -2467,7 +2469,7 @@ async function handleInvocation({ body, state, serverName, turn, nextInvocationO
2467
2469
  status: 409,
2468
2470
  message: "No ACP prompt turn is active."
2469
2471
  });
2470
- if (!isRecord$1(body) || typeof body.requestId !== "string" || typeof body.toolName !== "string" || !isRecord$1(body.input) || !Number.isSafeInteger(body.catalogRevision)) throw new RelayRequestError({
2472
+ if (!isRecord$2(body) || typeof body.requestId !== "string" || typeof body.toolName !== "string" || !isRecord$2(body.input) || !Number.isSafeInteger(body.catalogRevision)) throw new RelayRequestError({
2471
2473
  status: 400,
2472
2474
  message: "Invalid host tool relay request."
2473
2475
  });
@@ -2480,6 +2482,13 @@ async function handleInvocation({ body, state, serverName, turn, nextInvocationO
2480
2482
  status: 404,
2481
2483
  message: `Host tool ${body.toolName} is not active in catalog revision ${state.revision}.`
2482
2484
  });
2485
+ if (!await turn.waitForToolCallAuthorization({
2486
+ toolName: tool.name,
2487
+ input: body.input
2488
+ })) throw new RelayRequestError({
2489
+ status: 401,
2490
+ message: "Unauthorized host tool relay request."
2491
+ });
2483
2492
  const correlationToken = randomBytes(32).toString("hex");
2484
2493
  turn.registerCorrelationInvocation({
2485
2494
  token: correlationToken,
@@ -2556,12 +2565,7 @@ function resolveRefreshWaiters({ state, closing }) {
2556
2565
  for (const waiter of [...state.refreshWaiters]) if (closing || state.servedRevision >= waiter.revision) waiter.resolve(!closing);
2557
2566
  }
2558
2567
  function catalogFingerprint({ tools }) {
2559
- return JSON.stringify(canonicalizeJSON({ value: tools }));
2560
- }
2561
- function canonicalizeJSON({ value }) {
2562
- if (Array.isArray(value)) return value.map((item) => canonicalizeJSON({ value: item }));
2563
- if (!isRecord$1(value)) return value;
2564
- return Object.fromEntries(Object.keys(value).sort().filter((key) => value[key] !== void 0).map((key) => [key, canonicalizeJSON({ value: value[key] })]));
2568
+ return canonicalFingerprint({ value: tools });
2565
2569
  }
2566
2570
  async function readJSONBody({ request }) {
2567
2571
  const chunks = [];
@@ -2607,7 +2611,7 @@ function closeServer({ server }) {
2607
2611
  });
2608
2612
  });
2609
2613
  }
2610
- function isRecord$1(value) {
2614
+ function isRecord$2(value) {
2611
2615
  return value != null && typeof value === "object" && !Array.isArray(value);
2612
2616
  }
2613
2617
  var RelayRequestError = class extends Error {
@@ -2618,6 +2622,149 @@ var RelayRequestError = class extends Error {
2618
2622
  }
2619
2623
  };
2620
2624
  //#endregion
2625
+ //#region src/v1/bridge/host-tool-relay-authorization.ts
2626
+ const DEFAULT_AUTHORIZATION_WAIT_MS = 1e4;
2627
+ function createHostToolRelayAuthorization({ serverName, toolNames, ttlMs = DEFAULT_AUTHORIZATION_WAIT_MS }) {
2628
+ const observedCalls = /* @__PURE__ */ new Map();
2629
+ const authorizations = /* @__PURE__ */ new Map();
2630
+ const pendingRequests = [];
2631
+ let closed = false;
2632
+ const observe = ({ toolCall }) => {
2633
+ if (closed) return;
2634
+ const previous = observedCalls.get(toolCall.toolCallId);
2635
+ if (previous?.consumed || previous?.terminal) return;
2636
+ const merged = {
2637
+ ...previous?.toolCall,
2638
+ ...toolCall,
2639
+ ...toolCall.name == null && previous?.toolCall.name != null ? { name: previous.toolCall.name } : {},
2640
+ ...toolCall.rawInput === void 0 && previous?.toolCall.rawInput !== void 0 ? { rawInput: previous.toolCall.rawInput } : {},
2641
+ ...toolCall.title == null && previous?.toolCall.title != null ? { title: previous.toolCall.title } : {},
2642
+ ...toolCall._meta == null && previous?.toolCall._meta != null ? { _meta: previous.toolCall._meta } : {}
2643
+ };
2644
+ const observed = {
2645
+ toolCall: merged,
2646
+ consumed: false,
2647
+ terminal: merged.status === "completed" || merged.status === "failed"
2648
+ };
2649
+ observedCalls.set(toolCall.toolCallId, observed);
2650
+ if (observed.terminal) {
2651
+ authorizations.delete(toolCall.toolCallId);
2652
+ return;
2653
+ }
2654
+ const call = resolveHostToolCall({
2655
+ toolCall: merged,
2656
+ serverName,
2657
+ toolNames
2658
+ });
2659
+ if (call == null) {
2660
+ authorizations.delete(toolCall.toolCallId);
2661
+ return;
2662
+ }
2663
+ const key = callKey(call);
2664
+ authorizations.set(toolCall.toolCallId, key);
2665
+ const pendingIndex = pendingRequests.findIndex((request) => request.key === key);
2666
+ if (pendingIndex !== -1) {
2667
+ const [pending] = pendingRequests.splice(pendingIndex, 1);
2668
+ clearTimeout(pending.timeout);
2669
+ authorizations.delete(toolCall.toolCallId);
2670
+ observed.consumed = true;
2671
+ pending.resolve(true);
2672
+ }
2673
+ };
2674
+ return {
2675
+ observeUpdate: ({ update }) => {
2676
+ if (update.sessionUpdate === "tool_call" || update.sessionUpdate === "tool_call_update") observe({ toolCall: update });
2677
+ },
2678
+ observeAllowedPermission: ({ toolCall }) => observe({ toolCall }),
2679
+ waitForToolCallAuthorization: ({ toolName, input }) => {
2680
+ if (closed) return Promise.resolve(false);
2681
+ const key = callKey({
2682
+ toolName,
2683
+ input
2684
+ });
2685
+ for (const [toolCallId, authorizationKey] of authorizations) {
2686
+ if (authorizationKey !== key) continue;
2687
+ authorizations.delete(toolCallId);
2688
+ observedCalls.get(toolCallId).consumed = true;
2689
+ return Promise.resolve(true);
2690
+ }
2691
+ return new Promise((resolve) => {
2692
+ const pending = {
2693
+ key,
2694
+ timeout: setTimeout(() => {
2695
+ const index = pendingRequests.indexOf(pending);
2696
+ if (index !== -1) pendingRequests.splice(index, 1);
2697
+ resolve(false);
2698
+ }, ttlMs),
2699
+ resolve
2700
+ };
2701
+ pendingRequests.push(pending);
2702
+ });
2703
+ },
2704
+ close: () => {
2705
+ if (closed) return;
2706
+ closed = true;
2707
+ observedCalls.clear();
2708
+ authorizations.clear();
2709
+ for (const request of pendingRequests.splice(0)) {
2710
+ clearTimeout(request.timeout);
2711
+ request.resolve(false);
2712
+ }
2713
+ }
2714
+ };
2715
+ }
2716
+ function resolveHostToolCall({ toolCall, serverName, toolNames }) {
2717
+ const rawInput = toolCall.rawInput;
2718
+ if (!isRecord$1(rawInput)) return void 0;
2719
+ const isDeferred = "tool_name" in rawInput;
2720
+ const isProvider = "providerIdentifier" in rawInput && "toolName" in rawInput && "args" in rawInput;
2721
+ const isOrigin = "origin" in rawInput && "operation" in rawInput && "arguments" in rawInput;
2722
+ const isCodex = "server" in rawInput && "tool" in rawInput && "arguments" in rawInput;
2723
+ if ([
2724
+ isDeferred,
2725
+ isProvider,
2726
+ isOrigin,
2727
+ isCodex
2728
+ ].filter(Boolean).length > 1) return;
2729
+ const matches = [];
2730
+ for (const toolName of toolNames) {
2731
+ const qualifiedNames = [
2732
+ `mcp__${serverName}__${toolName}`,
2733
+ `${serverName}__${toolName}`,
2734
+ `mcp_${serverName}_${toolName}`
2735
+ ];
2736
+ const isQualified = (value) => typeof value === "string" && qualifiedNames.includes(value);
2737
+ const isDirect = toolCall.name === toolName && isRecord$1(toolCall._meta) && toolCall._meta.serverName === serverName;
2738
+ if (isDeferred && isQualified(rawInput.tool_name) && isRecord$1(rawInput.tool_input) && (toolCall.name == null || toolCall.name === "use_tool" || isQualified(toolCall.name))) matches.push({
2739
+ toolName,
2740
+ input: rawInput.tool_input
2741
+ });
2742
+ else if (isProvider && rawInput.providerIdentifier === serverName && rawInput.toolName === toolName && isRecord$1(rawInput.args) && (toolCall.name == null || isQualified(toolCall.name))) matches.push({
2743
+ toolName,
2744
+ input: rawInput.args
2745
+ });
2746
+ else if (isCodex && rawInput.server === serverName && rawInput.tool === toolName && isRecord$1(rawInput.arguments) && (toolCall.name == null || isQualified(toolCall.name))) matches.push({
2747
+ toolName,
2748
+ input: rawInput.arguments
2749
+ });
2750
+ else if (isOrigin && rawInput.origin === serverName && rawInput.operation === toolName && isRecord$1(rawInput.arguments) && (toolCall.name == null || toolCall.name === toolName || isQualified(toolCall.name))) matches.push({
2751
+ toolName,
2752
+ input: rawInput.arguments
2753
+ });
2754
+ else if (!isDeferred && !isProvider && !isOrigin && !isCodex && (isDirect || isQualified(toolCall.name) || toolCall.name == null && (isQualified(toolCall.title) || toolCall.title === `${serverName}-${toolName}`))) matches.push({
2755
+ toolName,
2756
+ input: rawInput
2757
+ });
2758
+ }
2759
+ return matches.length === 1 ? matches[0] : void 0;
2760
+ }
2761
+ function callKey({ toolName, input }) {
2762
+ return `${toolName}\0${canonicalFingerprint({ value: input })}`;
2763
+ }
2764
+ function isRecord$1(value) {
2765
+ return value != null && typeof value === "object" && !Array.isArray(value);
2766
+ }
2767
+ //#endregion
2621
2768
  //#region src/v1/bridge/host-tool-mcp-definition.ts
2622
2769
  function createHostToolMcpServerDefinition({ mcpTransport, relay, serverName, catalogPath, initialization, harnessId }) {
2623
2770
  if (mcpTransport === "stdio") return {
@@ -2680,7 +2827,7 @@ async function refreshHostToolCatalog({ relay, tools, harnessId, timeoutMs }) {
2680
2827
  //#endregion
2681
2828
  //#region src/v1/bridge/permission-controller.ts
2682
2829
  let permissionRequestCounter = 0;
2683
- function createACPPermissionController({ turn, sessionId, permissionMode, hasPermissionModeMapping, emitToolCall, claimHostToolPermission }) {
2830
+ function createACPPermissionController({ turn, sessionId, permissionMode, hasPermissionModeMapping, emitToolCall, claimHostToolPermission, onHostToolPermissionAllowed }) {
2684
2831
  const pendingPermissions = /* @__PURE__ */ new Map();
2685
2832
  return {
2686
2833
  async requestPermission(request) {
@@ -2695,10 +2842,13 @@ function createACPPermissionController({ turn, sessionId, permissionMode, hasPer
2695
2842
  turn.emitWarning({ message: `ACP permission request for tool call ${JSON.stringify(request.toolCall.toolCallId)} was cancelled because it did not advertise ${missing.join(" and ")}.` });
2696
2843
  return cancelled();
2697
2844
  }
2698
- if (claimHostToolPermission({ toolCall: request.toolCall })) return { outcome: {
2699
- outcome: "selected",
2700
- optionId: allowOnce.optionId
2701
- } };
2845
+ if (claimHostToolPermission({ toolCall: request.toolCall })) {
2846
+ onHostToolPermissionAllowed?.({ toolCall: request.toolCall });
2847
+ return { outcome: {
2848
+ outcome: "selected",
2849
+ optionId: allowOnce.optionId
2850
+ } };
2851
+ }
2702
2852
  if (!hasPermissionModeMapping && shouldAutoApprove({
2703
2853
  permissionMode,
2704
2854
  kind: request.toolCall.kind
@@ -3132,9 +3282,14 @@ async function runTurn(start, turn) {
3132
3282
  cancellationFailure.catch(() => {});
3133
3283
  let cancellationRequested = false;
3134
3284
  let cancellationFailureError;
3285
+ const hostToolAuthorization = createHostToolRelayAuthorization({
3286
+ serverName: HOST_TOOL_MCP_SERVER_NAME,
3287
+ toolNames: (start.tools ?? []).map((tool) => tool.name)
3288
+ });
3135
3289
  const cancel = async () => {
3136
3290
  if (cancellationRequested) return;
3137
3291
  cancellationRequested = true;
3292
+ hostToolAuthorization.close();
3138
3293
  activePermissionController?.cancelAll();
3139
3294
  try {
3140
3295
  if (connection == null) throw new Error("ACP connection closed before cancellation.");
@@ -3176,10 +3331,12 @@ async function runTurn(start, turn) {
3176
3331
  permissionMode: start.permissionMode ?? "allow-all",
3177
3332
  hasPermissionModeMapping: start.permissionModeMapping != null,
3178
3333
  emitToolCall: emitStreamEvent.permissionToolCall,
3179
- claimHostToolPermission: emitStreamEvent.claimHostToolPermission
3334
+ claimHostToolPermission: emitStreamEvent.claimHostToolPermission,
3335
+ onHostToolPermissionAllowed: hostToolAuthorization.observeAllowedPermission
3180
3336
  });
3181
3337
  activePermissionController = permissionController;
3182
3338
  const relayTurn = {
3339
+ waitForToolCallAuthorization: hostToolAuthorization.waitForToolCallAuthorization,
3183
3340
  emitToolCall: emitStreamEvent.hostToolCall,
3184
3341
  emitToolResult: emitStreamEvent.hostToolResult,
3185
3342
  requestToolResult: (toolCallId) => turn.requestToolResult(toolCallId),
@@ -3230,6 +3387,7 @@ async function runTurn(start, turn) {
3230
3387
  }
3231
3388
  if (message.kind === "session_update") {
3232
3389
  const captured = streamCapture?.takeForUpdate({ update: message.update });
3390
+ hostToolAuthorization.observeUpdate({ update: message.update });
3233
3391
  for (const rawValue of captured?.precedingRawValues ?? []) emitStreamEvent.raw({ rawValue });
3234
3392
  emitStreamEvent.message({
3235
3393
  message,
@@ -3241,6 +3399,7 @@ async function runTurn(start, turn) {
3241
3399
  if (emitStreamEvent.message({ message })) return;
3242
3400
  }
3243
3401
  } finally {
3402
+ hostToolAuthorization.close();
3244
3403
  if (activeQuestionRequest?.turn === turn) activeQuestionRequest = void 0;
3245
3404
  permissionController.cancelAll();
3246
3405
  activePermissionController = void 0;