@ai-sdk/harness-acp 1.0.24 → 1.0.28

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/harness-acp",
3
- "version": "1.0.24",
3
+ "version": "1.0.28",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -26,9 +26,9 @@
26
26
  }
27
27
  },
28
28
  "dependencies": {
29
- "ws": "^8.21.0",
30
- "@ai-sdk/harness": "1.0.86",
31
- "@ai-sdk/provider-utils": "5.0.29"
29
+ "@ai-sdk/harness": "1.0.90",
30
+ "@ai-sdk/provider-utils": "5.0.32",
31
+ "ws": "^8.21.0"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "zod": "^3.25.76 || ^4.1.8"
@@ -38,10 +38,10 @@
38
38
  "@modelcontextprotocol/sdk": "1.29.0",
39
39
  "@types/node": "22.19.19",
40
40
  "@types/ws": "^8.5.13",
41
+ "@vercel/ai-tsconfig": "0.0.0",
41
42
  "tsup": "^8.5.1",
42
43
  "typescript": "5.8.3",
43
- "zod": "3.25.76",
44
- "@vercel/ai-tsconfig": "0.0.0"
44
+ "zod": "3.25.76"
45
45
  },
46
46
  "engines": {
47
47
  "node": ">=22"
@@ -40,6 +40,12 @@ export type ACPHarnessSettings<TBuiltinTools extends ToolSet = {}> = {
40
40
  readonly forwardEnv?: ACPV1Settings['forwardEnv'];
41
41
  readonly credentialEnv?: ACPV1Settings['credentialEnv'];
42
42
  readonly credentialBrokering?: ACPV1Settings['credentialBrokering'];
43
+ /**
44
+ * Customizes each credential value before it is forwarded into a sandbox
45
+ * process. This does not restrict which credentials the harness adapter can
46
+ * discover, read, or otherwise access in the host process.
47
+ */
48
+ readonly credentialForwarding?: ACPV1Settings['credentialForwarding'];
43
49
  readonly env?: ACPV1Settings['env'];
44
50
  readonly authentication?: ACPV1Settings['authentication'];
45
51
  readonly providerAuthentication?: ACPV1Settings['providerAuthentication'];
@@ -79,6 +85,7 @@ const acpResumeStateSchema = z.object({
79
85
  .optional(),
80
86
  })
81
87
  .optional(),
88
+ sandboxCredentialEnvironment: z.record(z.string(), z.string()).optional(),
82
89
  acpSessionId: z.string().optional(),
83
90
  bridge: acpBridgeCoordsSchema.optional(),
84
91
  coldSession: acpColdSessionStateSchema.optional(),
@@ -7,6 +7,7 @@ import {
7
7
  createImplementationManifest,
8
8
  getImplementationInstallScript,
9
9
  getImplementationLockfile,
10
+ getImplementationWorkspaceFile,
10
11
  type ACPImplementation,
11
12
  } from './implementation';
12
13
 
@@ -40,6 +41,9 @@ export function createACPBootstrap({
40
41
  const implementationLock = getImplementationLockfile({
41
42
  implementation,
42
43
  });
44
+ const implementationWorkspace = getImplementationWorkspaceFile({
45
+ implementation,
46
+ });
43
47
  const implementationInstallScript = getImplementationInstallScript({
44
48
  implementation,
45
49
  });
@@ -80,6 +84,14 @@ export function createACPBootstrap({
80
84
  content: implementationLock,
81
85
  },
82
86
  ]),
87
+ ...(implementationWorkspace == null
88
+ ? []
89
+ : [
90
+ {
91
+ path: `${bootstrapDir}/implementation/pnpm-workspace.yaml`,
92
+ content: implementationWorkspace,
93
+ },
94
+ ]),
83
95
  ...(implementationInstallScript == null
84
96
  ? []
85
97
  : [
@@ -32,6 +32,18 @@ export type ACPBuiltinToolMapping = {
32
32
  */
33
33
  readonly nativeName?: HarnessV1BuiltinTool['nativeName'];
34
34
 
35
+ /**
36
+ * Stable prefix of the display title emitted by an ACP implementation.
37
+ * Used only when the implementation omits a programmatic tool name.
38
+ */
39
+ readonly title?: HarnessV1BuiltinTool['title'];
40
+
41
+ /**
42
+ * Broad capability category used to compare a built-in with the ACP tool
43
+ * kind when resolving unnamed calls.
44
+ */
45
+ readonly toolUseKind?: HarnessV1BuiltinTool['toolUseKind'];
46
+
35
47
  /**
36
48
  * JSON Schema used to identify tool calls when an ACP implementation does
37
49
  * not provide a programmatic tool name. It is optional because built-in
@@ -43,6 +55,8 @@ export type ACPBuiltinToolMapping = {
43
55
  const builtinToolSchema: z.ZodType<ACPBuiltinToolMapping> = z.object({
44
56
  toolName: z.string(),
45
57
  nativeName: z.string().optional(),
58
+ title: z.string().optional(),
59
+ toolUseKind: z.enum(['readonly', 'edit', 'bash']).optional(),
46
60
  inputSchema: z.json().optional(),
47
61
  });
48
62
 
@@ -14,14 +14,15 @@ import {
14
14
  } from '@ai-sdk/harness';
15
15
  import { HarnessBridgeCapabilityUnsupportedError } from '@ai-sdk/harness/bridge';
16
16
  import {
17
+ applyCredentialForwarding,
17
18
  createBridgeErrorHandler,
18
19
  createBridgeStartupError,
20
+ createSandboxCredentialEnvironment,
19
21
  classifyDiskLog,
20
22
  drainBridgeProcessStream,
21
23
  forwardBridgeProcessStream,
22
24
  getRestrictedSandboxSession,
23
25
  markBridgeStarting,
24
- maskSandboxCredentials,
25
26
  resolveSandboxDefaultWorkingDirectory,
26
27
  resolveSandboxHomeDir,
27
28
  SandboxChannel,
@@ -82,6 +83,7 @@ import type {
82
83
  ACPOutputSchemaMapping,
83
84
  ACPPermissionModeMapping,
84
85
  ACPPermissionModeTarget,
86
+ ACPProfileValue,
85
87
  ACPSerializableValue,
86
88
  ACPV1Settings,
87
89
  } from './acp-v1-settings';
@@ -235,6 +237,25 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
235
237
  sandboxSession,
236
238
  abortSignal: startOptions.abortSignal,
237
239
  });
240
+ const continueFrom =
241
+ startOptions.continueFrom ?? startOptions.resumeFrom?.continueFrom;
242
+ const lifecycleState = continueFrom ?? startOptions.resumeFrom;
243
+ const isContinue = continueFrom != null;
244
+ const isResume = lifecycleState != null;
245
+ const lifecycleData =
246
+ lifecycleState != null
247
+ ? (lifecycleState.data as unknown as ACPLifecycleData)
248
+ : undefined;
249
+ if (lifecycleData != null && lifecycleState != null) {
250
+ validateACPLifecycleCompatibility({
251
+ harnessId: settings.harnessId,
252
+ lifecycleHarnessId: lifecycleState.harnessId,
253
+ implementationIdentity,
254
+ authenticationProfile,
255
+ lifecycleData,
256
+ sandboxId,
257
+ });
258
+ }
238
259
  const implementationEnvironment = resolveImplementationEnvironment({
239
260
  implementation,
240
261
  env,
@@ -243,6 +264,25 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
243
264
  let sandboxProviderAuthenticationEnvironment =
244
265
  resolvedProviderAuthentication.env;
245
266
  let sandboxProviderEnvironment: Record<string, string> | undefined;
267
+ const credentialEnvironmentVariables = [
268
+ ...new Set([
269
+ ...(settings.credentialEnv ?? []),
270
+ 'AI_GATEWAY_API_KEY',
271
+ 'VERCEL_OIDC_TOKEN',
272
+ ]),
273
+ ];
274
+ const credentialForwardingEnvironmentVariables = [
275
+ ...new Set([
276
+ ...credentialEnvironmentVariables,
277
+ ...resolveACPProviderCredentialEnvironmentVariables({
278
+ providerAuthentication:
279
+ resolvedProviderAuthentication.providerAuthentication,
280
+ }),
281
+ ]),
282
+ ];
283
+ let sandboxCredentialEnvironment:
284
+ | Readonly<Record<string, string>>
285
+ | undefined;
246
286
 
247
287
  if (
248
288
  settings.credentialBrokering != null &&
@@ -253,44 +293,55 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
253
293
  resolvedProviderAuthentication,
254
294
  clientApp,
255
295
  });
256
- const credentialEnvironmentVariables = [
257
- ...new Set([
258
- ...(settings.credentialEnv ?? []),
259
- 'AI_GATEWAY_API_KEY',
260
- 'VERCEL_OIDC_TOKEN',
261
- ]),
262
- ];
296
+ const brokeringEnvironment = {
297
+ ...implementationEnvironment,
298
+ ...providerEnvironment,
299
+ };
300
+ sandboxCredentialEnvironment =
301
+ lifecycleData?.sandboxCredentialEnvironment ??
302
+ (await createSandboxCredentialEnvironment({
303
+ environment: brokeringEnvironment,
304
+ credentialEnvironmentVariables:
305
+ credentialForwardingEnvironmentVariables,
306
+ credentialForwarding: settings.credentialForwarding,
307
+ }));
308
+ sandboxImplementationEnvironment = {
309
+ ...brokeringEnvironment,
310
+ ...sandboxCredentialEnvironment,
311
+ };
263
312
  const requestTransformations = settings.credentialBrokering({
264
- env: {
265
- ...implementationEnvironment,
266
- ...providerEnvironment,
267
- },
313
+ env: brokeringEnvironment,
314
+ sandboxEnv: sandboxImplementationEnvironment,
268
315
  });
269
316
  if (requestTransformations.length > 0) {
270
317
  await sandboxSession.addRequestTransformations(
271
318
  requestTransformations,
272
319
  );
273
320
  }
274
- sandboxImplementationEnvironment = maskSandboxCredentials({
275
- environment: implementationEnvironment,
276
- credentialEnvironmentVariables,
277
- });
278
-
279
- /*
280
- * Gateway profiles are resolved on the host twice: real values feed
281
- * the transformation callback, while the bridge receives only a
282
- * structurally equivalent environment with credential placeholders.
283
- * Resolving the profile inside the sandbox would require serializing
284
- * the Gateway credential into the bridge process environment.
285
- */
286
- sandboxProviderEnvironment = maskSandboxCredentials({
287
- environment: resolveProviderEnvironment({
288
- resolvedProviderAuthentication,
289
- clientApp,
290
- gatewayApiKey: 'AI_GATEWAY_API_KEY',
291
- }),
292
- credentialEnvironmentVariables,
321
+ sandboxProviderEnvironment =
322
+ resolvedProviderAuthentication.providerAuthentication?.type ===
323
+ 'ai-gateway'
324
+ ? {}
325
+ : undefined;
326
+ } else if (settings.credentialBrokering != null) {
327
+ warnCredentialBrokeringUnavailable();
328
+ }
329
+ if (
330
+ settings.credentialForwarding != null &&
331
+ sandboxProviderEnvironment == null &&
332
+ resolvedProviderAuthentication.providerAuthentication?.type ===
333
+ 'ai-gateway'
334
+ ) {
335
+ sandboxProviderEnvironment = resolveProviderEnvironment({
336
+ resolvedProviderAuthentication,
337
+ clientApp,
293
338
  });
339
+ }
340
+ if (sandboxProviderEnvironment != null) {
341
+ sandboxImplementationEnvironment = {
342
+ ...sandboxImplementationEnvironment,
343
+ ...sandboxProviderEnvironment,
344
+ };
294
345
  sandboxProviderAuthenticationEnvironment = Object.fromEntries(
295
346
  Object.entries(resolvedProviderAuthentication.env).filter(
296
347
  ([key]) =>
@@ -298,8 +349,6 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
298
349
  key !== 'AI_SDK_ACP_GATEWAY_BASE_URL',
299
350
  ),
300
351
  );
301
- } else if (settings.credentialBrokering != null) {
302
- warnCredentialBrokeringUnavailable();
303
352
  }
304
353
  const resolvedBridgeDir = posix.resolve(
305
354
  defaultWorkingDirectory,
@@ -318,25 +367,6 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
318
367
  sessionId: startOptions.sessionId,
319
368
  });
320
369
  const bridgeStateDir = `${privateSessionDir}/bridge`;
321
- const continueFrom =
322
- startOptions.continueFrom ?? startOptions.resumeFrom?.continueFrom;
323
- const lifecycleState = continueFrom ?? startOptions.resumeFrom;
324
- const isContinue = continueFrom != null;
325
- const isResume = lifecycleState != null;
326
- const lifecycleData =
327
- lifecycleState != null
328
- ? (lifecycleState.data as unknown as ACPLifecycleData)
329
- : undefined;
330
- if (lifecycleData != null && lifecycleState != null) {
331
- validateACPLifecycleCompatibility({
332
- harnessId: settings.harnessId,
333
- lifecycleHarnessId: lifecycleState.harnessId,
334
- implementationIdentity,
335
- authenticationProfile,
336
- lifecycleData,
337
- sandboxId,
338
- });
339
- }
340
370
  const skills = startOptions.skills ?? [];
341
371
  const skillsFingerprint = createACPSkillsFingerprint({ skills });
342
372
  const shouldMaterializeSkills = shouldMaterializeACPSkills({
@@ -441,6 +471,7 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
441
471
  turnStartConfig: lifecycleData.turnStartConfig,
442
472
  recoveryStatus: lifecycleData.recovery,
443
473
  restoration: lifecycleData.restoration,
474
+ sandboxCredentialEnvironment,
444
475
  replayOnly: false,
445
476
  lossyRerun: false,
446
477
  });
@@ -521,6 +552,15 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
521
552
  }
522
553
  }
523
554
 
555
+ const forwardedImplementationEnvironment =
556
+ sandboxCredentialEnvironment == null
557
+ ? await applyCredentialForwarding({
558
+ environment: sandboxImplementationEnvironment,
559
+ credentialEnvironmentVariables:
560
+ credentialForwardingEnvironmentVariables,
561
+ credentialForwarding: settings.credentialForwarding,
562
+ })
563
+ : sandboxImplementationEnvironment;
524
564
  const port = resolveBridgePort({
525
565
  sandboxSession,
526
566
  override: portOverride,
@@ -549,12 +589,13 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
549
589
  ` --implementation-dir ${shellQuote(resolvedImplementationDir)}` +
550
590
  ` --bridge-type ${shellQuote(settings.harnessId)}`,
551
591
  env: {
552
- ...sandboxImplementationEnvironment,
592
+ ...forwardedImplementationEnvironment,
553
593
  ...createACPBridgeEnvironment({
554
594
  authentication: settings.authentication,
555
595
  providerAuthentication:
556
596
  resolvedProviderAuthentication.providerAuthentication,
557
- providerEnvironment: sandboxProviderEnvironment,
597
+ providerEnvironment:
598
+ sandboxProviderEnvironment == null ? undefined : {},
558
599
  sessionMeta: settings.session?.meta,
559
600
  }),
560
601
  ...sandboxProviderAuthenticationEnvironment,
@@ -712,6 +753,7 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
712
753
  coldRestoration == null
713
754
  ? lifecycleData?.restoration
714
755
  : { method: coldRestoration },
756
+ sandboxCredentialEnvironment,
715
757
  replayOnly: respawnStrategy?.mode === 'disk-replay',
716
758
  lossyRerun: respawnStrategy?.mode === 'lossy-rerun',
717
759
  });
@@ -722,13 +764,11 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
722
764
  function resolveProviderEnvironment({
723
765
  resolvedProviderAuthentication,
724
766
  clientApp,
725
- gatewayApiKey,
726
767
  }: {
727
768
  resolvedProviderAuthentication: ReturnType<
728
769
  typeof resolveACPProviderAuthentication
729
770
  >;
730
771
  clientApp: ACPClientApp;
731
- gatewayApiKey?: string;
732
772
  }): Record<string, string> {
733
773
  if (
734
774
  resolvedProviderAuthentication.providerAuthentication?.type !== 'ai-gateway'
@@ -739,12 +779,10 @@ function resolveProviderEnvironment({
739
779
  providerAuthentication:
740
780
  resolvedProviderAuthentication.providerAuthentication,
741
781
  gateway: {
742
- apiKey:
743
- gatewayApiKey ??
744
- requireResolvedEnvironmentValue({
745
- environment: resolvedProviderAuthentication.env,
746
- name: 'AI_SDK_ACP_GATEWAY_API_KEY',
747
- }),
782
+ apiKey: requireResolvedEnvironmentValue({
783
+ environment: resolvedProviderAuthentication.env,
784
+ name: 'AI_SDK_ACP_GATEWAY_API_KEY',
785
+ }),
748
786
  baseUrl: requireResolvedEnvironmentValue({
749
787
  environment: resolvedProviderAuthentication.env,
750
788
  name: 'AI_SDK_ACP_GATEWAY_BASE_URL',
@@ -755,6 +793,40 @@ function resolveProviderEnvironment({
755
793
  });
756
794
  }
757
795
 
796
+ function resolveACPProviderCredentialEnvironmentVariables({
797
+ providerAuthentication,
798
+ }: {
799
+ providerAuthentication: ReturnType<
800
+ typeof resolveACPProviderAuthentication
801
+ >['providerAuthentication'];
802
+ }): string[] {
803
+ if (providerAuthentication?.type !== 'ai-gateway') return [];
804
+
805
+ return Object.entries(providerAuthentication.env)
806
+ .filter(([, value]) => containsACPProviderCredential({ value }))
807
+ .map(([name]) => name);
808
+ }
809
+
810
+ function containsACPProviderCredential({
811
+ value,
812
+ }: {
813
+ value: ACPProfileValue;
814
+ }): boolean {
815
+ if (Array.isArray(value)) {
816
+ return value.some(item => containsACPProviderCredential({ value: item }));
817
+ }
818
+ if (value == null || typeof value !== 'object') return false;
819
+ if ('$source' in value) {
820
+ return (
821
+ value.$source === 'gateway-api-key' ||
822
+ value.$source === 'gateway-authorization'
823
+ );
824
+ }
825
+ return Object.values(value).some(item =>
826
+ containsACPProviderCredential({ value: item }),
827
+ );
828
+ }
829
+
758
830
  function requireResolvedEnvironmentValue({
759
831
  environment,
760
832
  name,
@@ -984,6 +1056,7 @@ function createSession({
984
1056
  turnStartConfig: turnStartConfigAtStart,
985
1057
  recoveryStatus,
986
1058
  restoration,
1059
+ sandboxCredentialEnvironment,
987
1060
  replayOnly,
988
1061
  lossyRerun,
989
1062
  }: {
@@ -1016,6 +1089,7 @@ function createSession({
1016
1089
  turnStartConfig: ACPTurnStartConfig | undefined;
1017
1090
  recoveryStatus: ACPLifecycleData['recovery'];
1018
1091
  restoration: ACPLifecycleData['restoration'];
1092
+ sandboxCredentialEnvironment: Readonly<Record<string, string>> | undefined;
1019
1093
  replayOnly: boolean;
1020
1094
  lossyRerun: boolean;
1021
1095
  }): HarnessV1Session {
@@ -1271,6 +1345,9 @@ function createSession({
1271
1345
  }): ACPLifecycleData => ({
1272
1346
  implementationIdentity,
1273
1347
  authenticationProfile,
1348
+ ...(sandboxCredentialEnvironment == null
1349
+ ? {}
1350
+ : { sandboxCredentialEnvironment }),
1274
1351
  ...(latestACPSessionId == null ? {} : { acpSessionId: latestACPSessionId }),
1275
1352
  ...(bridge == null ? {} : { bridge }),
1276
1353
  ...(latestTurnStartConfig == null
@@ -1583,6 +1660,22 @@ export function serializeBuiltinTools({
1583
1660
  typeof tool.nativeName === 'string'
1584
1661
  ? tool.nativeName
1585
1662
  : undefined;
1663
+ const title =
1664
+ tool != null &&
1665
+ typeof tool === 'object' &&
1666
+ 'title' in tool &&
1667
+ typeof tool.title === 'string'
1668
+ ? tool.title
1669
+ : undefined;
1670
+ const toolUseKind =
1671
+ tool != null &&
1672
+ typeof tool === 'object' &&
1673
+ 'toolUseKind' in tool &&
1674
+ (tool.toolUseKind === 'readonly' ||
1675
+ tool.toolUseKind === 'edit' ||
1676
+ tool.toolUseKind === 'bash')
1677
+ ? tool.toolUseKind
1678
+ : undefined;
1586
1679
  let inputSchema: ACPBuiltinToolMapping['inputSchema'];
1587
1680
  if (
1588
1681
  tool != null &&
@@ -1599,6 +1692,8 @@ export function serializeBuiltinTools({
1599
1692
  return {
1600
1693
  toolName,
1601
1694
  ...(nativeName == null ? {} : { nativeName }),
1695
+ ...(title == null ? {} : { title }),
1696
+ ...(toolUseKind == null ? {} : { toolUseKind }),
1602
1697
  ...(inputSchema == null ? {} : { inputSchema }),
1603
1698
  };
1604
1699
  });
@@ -21,6 +21,7 @@ export type ACPBridgeCoordinates = {
21
21
  export type ACPLifecycleData = ACPPromptGuidanceLifecycleState & {
22
22
  readonly implementationIdentity: string;
23
23
  readonly authenticationProfile?: ACPAuthenticationProfileIdentity;
24
+ readonly sandboxCredentialEnvironment?: Readonly<Record<string, string>>;
24
25
  readonly acpSessionId?: string;
25
26
  readonly bridge?: ACPBridgeCoordinates;
26
27
  readonly coldSession?: ACPColdSessionState;
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ HarnessV1CredentialForwarding,
2
3
  HarnessV1PermissionMode,
3
4
  HarnessV1RequestTransformation,
4
5
  } from '@ai-sdk/harness';
@@ -45,6 +46,8 @@ export type ACPNpmLockedSource = {
45
46
  readonly type: 'npm-locked';
46
47
  readonly packageJson: string;
47
48
  readonly pnpmLockYaml: string;
49
+ /** Optional pnpm workspace configuration required by the locked install. */
50
+ readonly pnpmWorkspaceYaml?: string;
48
51
  };
49
52
 
50
53
  export type ACPInstallCommandSource = {
@@ -73,8 +76,10 @@ export type ACPProviderAuthentication = {
73
76
 
74
77
  export type ACPCredentialBrokering = ({
75
78
  env,
79
+ sandboxEnv,
76
80
  }: {
77
81
  env: Readonly<Record<string, string>>;
82
+ sandboxEnv?: Readonly<Record<string, string>>;
78
83
  }) => ReadonlyArray<HarnessV1RequestTransformation>;
79
84
 
80
85
  export type ACPPermissionModeTarget =
@@ -125,6 +130,12 @@ export type ACPV1Settings = {
125
130
  readonly forwardEnv?: ReadonlyArray<string>;
126
131
  readonly credentialEnv?: ReadonlyArray<string>;
127
132
  readonly credentialBrokering?: ACPCredentialBrokering;
133
+ /**
134
+ * Customizes each credential value before it is forwarded into a sandbox
135
+ * process. This does not restrict which credentials the harness adapter can
136
+ * discover, read, or otherwise access in the host process.
137
+ */
138
+ readonly credentialForwarding?: HarnessV1CredentialForwarding;
128
139
  /**
129
140
  * Runtime environment values that are safe to persist in bootstrap and
130
141
  * lifecycle compatibility identity.
@@ -38,7 +38,10 @@ import {
38
38
  type HostToolRelay,
39
39
  type HostToolRelayTurn,
40
40
  } from './host-tool-relay';
41
- import { refreshHostToolCatalog } from './refresh-host-tool-catalog';
41
+ import {
42
+ promptAndRefreshInitialHostToolCatalog,
43
+ refreshHostToolCatalog,
44
+ } from './refresh-host-tool-catalog';
42
45
  import { createACPPermissionController } from './permission-controller';
43
46
  import { configureACPPermissionMode } from './permission-mode';
44
47
  import {
@@ -130,8 +133,12 @@ await runBridge<StartMessage>({
130
133
  });
131
134
 
132
135
  async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
136
+ let initialHostToolCatalogRefreshRequired: boolean;
133
137
  try {
134
- await ensureSession({ start, turn });
138
+ ({ initialHostToolCatalogRefreshRequired } = await ensureSession({
139
+ start,
140
+ turn,
141
+ }));
135
142
  } catch (error) {
136
143
  if (HarnessBridgeCapabilityUnsupportedError.isInstance(error)) throw error;
137
144
  throw createACPBridgeError({
@@ -149,6 +156,10 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
149
156
  'ACP session initialization did not start stderr monitoring.',
150
157
  );
151
158
  }
159
+ const activeHostToolRelay = hostToolRelay;
160
+ if (activeHostToolRelay == null) {
161
+ throw new Error('The host tool MCP relay is unavailable.');
162
+ }
152
163
  if (start.recoveryMode?.type === 'lossy-rerun') {
153
164
  const marker = {
154
165
  type: 'acp-recovery',
@@ -260,15 +271,35 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
260
271
  removeCorrelationInvocation:
261
272
  emitStreamEvent.removeHostToolCorrelationInvocation,
262
273
  };
263
- hostToolRelay?.bindTurn({ turn: relayTurn });
274
+ activeHostToolRelay.bindTurn({ turn: relayTurn });
264
275
  try {
265
276
  const promptMeta = createOutputSchemaPromptMeta({ start });
266
- void promptActiveSession({
267
- session: activeSession,
268
- agent: connection!.agent,
269
- prompt: start.prompt,
270
- meta: promptMeta,
271
- });
277
+ const startPrompt = () =>
278
+ promptActiveSession({
279
+ session: activeSession,
280
+ agent: connection!.agent,
281
+ prompt: start.prompt,
282
+ meta: promptMeta,
283
+ });
284
+ if (initialHostToolCatalogRefreshRequired) {
285
+ try {
286
+ await promptAndRefreshInitialHostToolCatalog({
287
+ startPrompt,
288
+ relay: activeHostToolRelay,
289
+ tools: start.tools ?? [],
290
+ harnessId: bridgeType,
291
+ timeoutMs: CATALOG_REFRESH_TIMEOUT_MS,
292
+ });
293
+ } catch (error) {
294
+ if (HarnessBridgeCapabilityUnsupportedError.isInstance(error)) {
295
+ catalogRefreshError = error;
296
+ }
297
+ sessionConfigurationFailure = { error };
298
+ throw error;
299
+ }
300
+ } else {
301
+ void startPrompt();
302
+ }
272
303
  if (turn.abortSignal.aborted) {
273
304
  await cancel();
274
305
  } else {
@@ -316,7 +347,7 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
316
347
  } finally {
317
348
  permissionController.cancelAll();
318
349
  activePermissionController = undefined;
319
- hostToolRelay?.unbindTurn({ turn: relayTurn });
350
+ activeHostToolRelay.unbindTurn({ turn: relayTurn });
320
351
  }
321
352
  }
322
353
 
@@ -326,7 +357,7 @@ async function ensureSession({
326
357
  }: {
327
358
  start: StartMessage;
328
359
  turn: BridgeTurn;
329
- }): Promise<void> {
360
+ }): Promise<{ initialHostToolCatalogRefreshRequired: boolean }> {
330
361
  if (sessionConfigurationFailure != null) {
331
362
  throw sessionConfigurationFailure.error;
332
363
  }
@@ -364,7 +395,7 @@ async function ensureSession({
364
395
  }
365
396
  throw error;
366
397
  }
367
- return;
398
+ return { initialHostToolCatalogRefreshRequired: false };
368
399
  }
369
400
 
370
401
  const clientApp = resolveClientApp();
@@ -558,12 +589,6 @@ async function ensureSession({
558
589
  .start();
559
590
  }
560
591
  try {
561
- await refreshHostToolCatalog({
562
- relay: hostToolRelay,
563
- tools,
564
- harnessId: bridgeType,
565
- timeoutMs: CATALOG_REFRESH_TIMEOUT_MS,
566
- });
567
592
  if (start.permissionModeMapping != null) {
568
593
  await configureACPPermissionMode({
569
594
  agent: connection.agent,
@@ -588,6 +613,7 @@ async function ensureSession({
588
613
  sessionId: createdSession.sessionId,
589
614
  });
590
615
  sessionConfigurationFingerprint = fingerprint;
616
+ return { initialHostToolCatalogRefreshRequired: tools.length > 0 };
591
617
  }
592
618
 
593
619
  function createExternalMcpServers({