@ai-sdk/harness-acp 0.0.0 → 1.0.0

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +13 -0
  3. package/README.md +70 -3
  4. package/dist/bridge/host-tool-mcp.mjs +302 -0
  5. package/dist/bridge/host-tool-mcp.mjs.map +1 -0
  6. package/dist/bridge/index.mjs +3278 -0
  7. package/dist/bridge/index.mjs.map +1 -0
  8. package/dist/bridge/package.json +12 -0
  9. package/dist/bridge/pnpm-lock.yaml +816 -0
  10. package/dist/index.d.ts +98 -0
  11. package/dist/index.js +2094 -0
  12. package/dist/index.js.map +1 -0
  13. package/package.json +64 -10
  14. package/src/acp-auth.ts +254 -0
  15. package/src/acp-bridge-protocol.ts +13 -0
  16. package/src/acp-harness.ts +160 -0
  17. package/src/index.ts +19 -0
  18. package/src/v1/acp-v1-bridge-protocol.ts +151 -0
  19. package/src/v1/acp-v1-harness.ts +1447 -0
  20. package/src/v1/acp-v1-lifecycle.ts +115 -0
  21. package/src/v1/acp-v1-prompt.ts +122 -0
  22. package/src/v1/acp-v1-settings.ts +98 -0
  23. package/src/v1/acp-v1-skills.ts +228 -0
  24. package/src/v1/acp-v1-turn-start-config.ts +93 -0
  25. package/src/v1/bridge/acp-diagnostics.ts +57 -0
  26. package/src/v1/bridge/acp-stream-capture.ts +152 -0
  27. package/src/v1/bridge/acp-v1-bridge-environment.ts +95 -0
  28. package/src/v1/bridge/create-emit-stream-event.ts +97 -0
  29. package/src/v1/bridge/host-tool-correlation.ts +640 -0
  30. package/src/v1/bridge/host-tool-mcp-server.ts +180 -0
  31. package/src/v1/bridge/host-tool-mcp.ts +185 -0
  32. package/src/v1/bridge/host-tool-relay-client.ts +53 -0
  33. package/src/v1/bridge/host-tool-relay.ts +510 -0
  34. package/src/v1/bridge/index.ts +729 -0
  35. package/src/v1/bridge/package.json +12 -0
  36. package/src/v1/bridge/permission-controller.ts +152 -0
  37. package/src/v1/bridge/permission-mode.ts +163 -0
  38. package/src/v1/bridge/pnpm-lock.yaml +816 -0
  39. package/src/v1/bridge/profile-values.ts +97 -0
  40. package/src/v1/bridge/protocol-configuration.ts +146 -0
  41. package/src/v1/bridge/recovered-session.ts +145 -0
  42. package/src/v1/bridge/refresh-host-tool-catalog.ts +33 -0
  43. package/src/v1/bridge/session-lifecycle.ts +83 -0
  44. package/src/v1/bridge/stream-translator.ts +901 -0
  45. package/src/v1/implementation.ts +282 -0
  46. package/src/v1/index.ts +16 -0
  47. package/src/version.ts +6 -0
  48. package/index.js +0 -1
package/dist/index.js ADDED
@@ -0,0 +1,2094 @@
1
+ // src/acp-harness.ts
2
+ import { z as z4 } from "zod/v4";
3
+
4
+ // src/acp-auth.ts
5
+ import { createHash } from "crypto";
6
+ import { getAiGatewayAuthFromEnv } from "@ai-sdk/harness/utils";
7
+ var DEFAULT_AI_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh";
8
+ function createACPAuthenticationProfileIdentity({
9
+ authentication,
10
+ providerAuthenticationCompatibility
11
+ }) {
12
+ const providerKind = providerAuthenticationCompatibility?.type ?? "direct";
13
+ const digest = createHash("sha256").update(
14
+ stableStringify({
15
+ value: {
16
+ authentication: authentication ?? null,
17
+ providerAuthentication: providerAuthenticationCompatibility ?? null
18
+ }
19
+ })
20
+ ).digest("hex");
21
+ return {
22
+ digest,
23
+ ...authentication == null ? {} : { acpMethodId: authentication.methodId },
24
+ providerKind,
25
+ ...providerAuthenticationCompatibility == null ? {} : { providerMode: providerAuthenticationCompatibility.mode },
26
+ ...providerAuthenticationCompatibility?.type === "ai-gateway" ? {
27
+ gatewayCredentialSource: providerAuthenticationCompatibility.credentialSource
28
+ } : {}
29
+ };
30
+ }
31
+ function resolveACPProviderAuthentication({
32
+ auth,
33
+ env,
34
+ compatibility
35
+ }) {
36
+ const clientAppEnv = {
37
+ AI_SDK_ACP_CLIENT_APP_NAME: auth.clientApp.name,
38
+ AI_SDK_ACP_CLIENT_APP_VERSION: auth.clientApp.version
39
+ };
40
+ const providerAuthentication = auth.providerAuthentication;
41
+ if (providerAuthentication == null) {
42
+ return { providerAuthentication: void 0, env: clientAppEnv };
43
+ }
44
+ if (compatibility?.type === "direct") {
45
+ return {
46
+ providerAuthentication: { type: "direct" },
47
+ env: clientAppEnv
48
+ };
49
+ }
50
+ const mode = auth.mode ?? "auto";
51
+ if (mode === "direct") {
52
+ return {
53
+ providerAuthentication: { type: "direct" },
54
+ env: clientAppEnv
55
+ };
56
+ }
57
+ const gateway = compatibility?.type === "ai-gateway" ? {
58
+ apiKey: resolveGatewayCredential({
59
+ env,
60
+ credentialSource: compatibility.credentialSource
61
+ }),
62
+ baseUrl: compatibility.baseUrl
63
+ } : getAiGatewayAuthFromEnv({ env });
64
+ const apiKey = gateway.apiKey;
65
+ if (compatibility == null && mode === "auto" && apiKey == null) {
66
+ return {
67
+ providerAuthentication: { type: "direct" },
68
+ env: clientAppEnv
69
+ };
70
+ }
71
+ if (apiKey == null) {
72
+ throw new Error(
73
+ "AI Gateway authentication was selected, but neither AI_GATEWAY_API_KEY nor VERCEL_OIDC_TOKEN is set."
74
+ );
75
+ }
76
+ return {
77
+ providerAuthentication: {
78
+ type: "ai-gateway",
79
+ env: compatibility?.type === "ai-gateway" ? compatibility.env : providerAuthentication.gateway.env
80
+ },
81
+ env: {
82
+ AI_SDK_ACP_GATEWAY_API_KEY: apiKey,
83
+ AI_SDK_ACP_GATEWAY_BASE_URL: gateway.baseUrl,
84
+ ...clientAppEnv
85
+ }
86
+ };
87
+ }
88
+ function resolveACPProviderAuthenticationCompatibility({
89
+ auth = "auto",
90
+ providerAuthentication,
91
+ env
92
+ }) {
93
+ if (providerAuthentication == null) return void 0;
94
+ const mode = auth;
95
+ if (mode === "direct") {
96
+ return { type: "direct", mode };
97
+ }
98
+ const credentialSource = resolveGatewayCredentialSource({
99
+ env
100
+ });
101
+ if (mode === "auto" && credentialSource == null) {
102
+ return { type: "direct", mode };
103
+ }
104
+ return {
105
+ type: "ai-gateway",
106
+ mode,
107
+ env: providerAuthentication.gateway.env,
108
+ credentialSource,
109
+ baseUrl: env.AI_GATEWAY_BASE_URL ?? DEFAULT_AI_GATEWAY_BASE_URL
110
+ };
111
+ }
112
+ function resolveGatewayCredentialSource({
113
+ env
114
+ }) {
115
+ return env.AI_GATEWAY_API_KEY ? "AI_GATEWAY_API_KEY" : env.VERCEL_OIDC_TOKEN ? "VERCEL_OIDC_TOKEN" : null;
116
+ }
117
+ function resolveGatewayCredential({
118
+ env,
119
+ credentialSource
120
+ }) {
121
+ const gatewayFromEnv = getAiGatewayAuthFromEnv({ env });
122
+ switch (credentialSource) {
123
+ case "AI_GATEWAY_API_KEY":
124
+ return env.AI_GATEWAY_API_KEY == null ? void 0 : gatewayFromEnv.apiKey;
125
+ case "VERCEL_OIDC_TOKEN":
126
+ return getAiGatewayAuthFromEnv({
127
+ env: { VERCEL_OIDC_TOKEN: env.VERCEL_OIDC_TOKEN }
128
+ }).apiKey;
129
+ case null:
130
+ return void 0;
131
+ }
132
+ }
133
+ function stableStringify({ value }) {
134
+ return JSON.stringify(sortIdentityValue({ value }));
135
+ }
136
+ function sortIdentityValue({ value }) {
137
+ if (Array.isArray(value)) {
138
+ return value.map((item) => sortIdentityValue({ value: item }));
139
+ }
140
+ if (value != null && typeof value === "object") {
141
+ return Object.fromEntries(
142
+ Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, entry]) => [key, sortIdentityValue({ value: entry })])
143
+ );
144
+ }
145
+ return value;
146
+ }
147
+
148
+ // src/v1/acp-v1-harness.ts
149
+ import { createHash as createHash5, randomBytes } from "crypto";
150
+ import { readFile } from "fs/promises";
151
+ import { posix } from "path";
152
+ import { fileURLToPath } from "url";
153
+ import {
154
+ HarnessCapabilityUnsupportedError as HarnessCapabilityUnsupportedError2,
155
+ harnessV1DiagnosticFromBridgeFrame
156
+ } from "@ai-sdk/harness";
157
+ import { HarnessBridgeCapabilityUnsupportedError } from "@ai-sdk/harness/bridge";
158
+ import {
159
+ createBridgeErrorHandler,
160
+ createBridgeStartupError,
161
+ classifyDiskLog,
162
+ drainBridgeProcessStream,
163
+ forwardBridgeProcessStream,
164
+ markBridgeStarting,
165
+ resolveSandboxHomeDir,
166
+ SandboxChannel,
167
+ shellQuote,
168
+ waitForBridgeReady
169
+ } from "@ai-sdk/harness/utils";
170
+ import {
171
+ asSchema
172
+ } from "@ai-sdk/provider-utils";
173
+ import { WebSocket } from "ws";
174
+
175
+ // src/v1/implementation.ts
176
+ import { createHash as createHash2 } from "crypto";
177
+ var EXACT_SEMVER_REGEXP = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
178
+ var PACKAGE_NAME_REGEXP = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
179
+ var EXECUTABLE_NAME_REGEXP = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
180
+ var ENVIRONMENT_VARIABLE_NAME_REGEXP = /^[A-Za-z_][A-Za-z0-9_]*$/;
181
+ function validateACPV1Implementation(implementation) {
182
+ if (isLockedImplementation(implementation)) {
183
+ if (implementation.packageJson.length === 0) {
184
+ throw new Error(
185
+ "ACP locked npm implementation packageJson must not be empty."
186
+ );
187
+ }
188
+ if (implementation.pnpmLockYaml.length === 0) {
189
+ throw new Error(
190
+ "ACP locked npm implementation pnpmLockYaml must not be empty."
191
+ );
192
+ }
193
+ } else {
194
+ validateSimpleImplementation({ implementation });
195
+ }
196
+ if (!EXECUTABLE_NAME_REGEXP.test(implementation.executable)) {
197
+ throw new Error(
198
+ `ACP executable must be a package bin name without a path; received ${JSON.stringify(implementation.executable)}.`
199
+ );
200
+ }
201
+ validateForwardEnvironment({ forwardEnv: implementation.forwardEnv });
202
+ validateEnvironment({ env: implementation.env });
203
+ const forwardedKeys = new Set(implementation.forwardEnv ?? []);
204
+ for (const key of Object.keys(implementation.env ?? {})) {
205
+ if (forwardedKeys.has(key)) {
206
+ throw new Error(
207
+ `ACP runtime environment key ${JSON.stringify(key)} cannot be configured in both forwardEnv and env.`
208
+ );
209
+ }
210
+ }
211
+ }
212
+ function createImplementationManifest({
213
+ implementation
214
+ }) {
215
+ if (isLockedImplementation(implementation)) {
216
+ return implementation.packageJson;
217
+ }
218
+ return JSON.stringify(
219
+ {
220
+ name: "harness-acp-implementation",
221
+ version: "0.0.0",
222
+ private: true,
223
+ type: "module",
224
+ dependencies: {
225
+ [implementation.packageName]: implementation.version
226
+ }
227
+ },
228
+ null,
229
+ 2
230
+ ) + "\n";
231
+ }
232
+ function getImplementationLockfile({
233
+ implementation
234
+ }) {
235
+ return isLockedImplementation(implementation) ? implementation.pnpmLockYaml : void 0;
236
+ }
237
+ function createImplementationDescriptor({
238
+ implementation,
239
+ implementationIdentity
240
+ }) {
241
+ return JSON.stringify(
242
+ {
243
+ executable: implementation.executable,
244
+ args: implementation.args ?? [],
245
+ envKeys: getImplementationEnvironmentKeys({ implementation }),
246
+ implementationIdentity
247
+ },
248
+ null,
249
+ 2
250
+ ) + "\n";
251
+ }
252
+ function createImplementationIdentity({
253
+ harnessId,
254
+ acpVersion,
255
+ implementation,
256
+ clientApp,
257
+ providerAuthentication,
258
+ permissionModeMapping
259
+ }) {
260
+ const acquisition = isLockedImplementation(implementation) ? {
261
+ mode: "locked",
262
+ packageJson: implementation.packageJson,
263
+ pnpmLockYaml: implementation.pnpmLockYaml
264
+ } : {
265
+ mode: "simple",
266
+ packageName: implementation.packageName,
267
+ version: implementation.version
268
+ };
269
+ const forwardedEnvironment = [
270
+ ...new Set(implementation.forwardEnv ?? [])
271
+ ].sort();
272
+ const literalEnvironment = Object.fromEntries(
273
+ Object.entries(implementation.env ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => [key, { value }])
274
+ );
275
+ const payload = {
276
+ harnessId,
277
+ acpVersion,
278
+ acquisition,
279
+ executable: implementation.executable,
280
+ args: implementation.args ?? [],
281
+ clientApp,
282
+ environment: {
283
+ forwarded: forwardedEnvironment,
284
+ literal: literalEnvironment
285
+ },
286
+ providerAuthentication: providerAuthentication ?? null,
287
+ permissionModeMapping: permissionModeMapping ?? null
288
+ };
289
+ return createHash2("sha256").update(stableStringify2({ value: payload })).digest("hex");
290
+ }
291
+ function createImplementationInstallCommand({
292
+ implementationDir,
293
+ storeDir,
294
+ implementation
295
+ }) {
296
+ return `pnpm --dir ${implementationDir} install` + (isLockedImplementation(implementation) ? " --frozen-lockfile" : "") + ` --prod --store-dir ${storeDir}`;
297
+ }
298
+ function resolveImplementationEnvironment({
299
+ implementation,
300
+ env
301
+ }) {
302
+ const forwardedEnvironment = {};
303
+ for (const name of implementation.forwardEnv ?? []) {
304
+ const value = env[name];
305
+ if (value != null && value.length > 0) {
306
+ forwardedEnvironment[name] = value;
307
+ }
308
+ }
309
+ return {
310
+ ...forwardedEnvironment,
311
+ ...implementation.env
312
+ };
313
+ }
314
+ function validateSimpleImplementation({
315
+ implementation
316
+ }) {
317
+ if (!PACKAGE_NAME_REGEXP.test(implementation.packageName)) {
318
+ throw new Error(
319
+ `ACP npm package name is invalid: ${JSON.stringify(implementation.packageName)}.`
320
+ );
321
+ }
322
+ if (!EXACT_SEMVER_REGEXP.test(implementation.version)) {
323
+ throw new Error(
324
+ `ACP npm implementation version must be an exact semantic version; received ${JSON.stringify(implementation.version)}.`
325
+ );
326
+ }
327
+ }
328
+ function validateEnvironment({
329
+ env
330
+ }) {
331
+ for (const [key, value] of Object.entries(env ?? {})) {
332
+ if (!ENVIRONMENT_VARIABLE_NAME_REGEXP.test(key)) {
333
+ throw new Error(
334
+ `ACP environment variable name is invalid: ${JSON.stringify(key)}.`
335
+ );
336
+ }
337
+ if (value.includes("\0")) {
338
+ throw new Error(`ACP runtime environment value for ${key} contains NUL.`);
339
+ }
340
+ }
341
+ }
342
+ function validateForwardEnvironment({
343
+ forwardEnv
344
+ }) {
345
+ for (const name of forwardEnv ?? []) {
346
+ if (!ENVIRONMENT_VARIABLE_NAME_REGEXP.test(name)) {
347
+ throw new Error(
348
+ `ACP environment variable name is invalid: ${JSON.stringify(name)}.`
349
+ );
350
+ }
351
+ }
352
+ }
353
+ function getImplementationEnvironmentKeys({
354
+ implementation
355
+ }) {
356
+ return [
357
+ .../* @__PURE__ */ new Set([
358
+ ...implementation.forwardEnv ?? [],
359
+ ...Object.keys(implementation.env ?? {})
360
+ ])
361
+ ].sort();
362
+ }
363
+ function isLockedImplementation(implementation) {
364
+ return implementation.mode === "locked";
365
+ }
366
+ function stableStringify2({ value }) {
367
+ return JSON.stringify(sortValue({ value }));
368
+ }
369
+ function sortValue({ value }) {
370
+ if (Array.isArray(value)) {
371
+ return value.map((item) => sortValue({ value: item }));
372
+ }
373
+ if (value != null && typeof value === "object") {
374
+ return Object.fromEntries(
375
+ Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, sortValue({ value: item })])
376
+ );
377
+ }
378
+ return value;
379
+ }
380
+
381
+ // src/v1/acp-v1-bridge-protocol.ts
382
+ import {
383
+ harnessV1BridgeInboundCommandSchemas,
384
+ harnessV1BridgeOutboundMessageSchema,
385
+ harnessV1BridgeReadySchema,
386
+ harnessV1BridgeStartBaseSchema,
387
+ harnessV1BridgeToolWireSchema
388
+ } from "@ai-sdk/harness";
389
+ import { z as z2 } from "zod/v4";
390
+
391
+ // src/v1/acp-v1-prompt.ts
392
+ import {
393
+ HarnessCapabilityUnsupportedError
394
+ } from "@ai-sdk/harness";
395
+ import { z } from "zod/v4";
396
+ var acpTextContentBlockSchema = z.object({
397
+ type: z.literal("text"),
398
+ text: z.string()
399
+ });
400
+ function convertHarnessPromptToACPTextBlocks({
401
+ prompt,
402
+ harnessId
403
+ }) {
404
+ if (typeof prompt === "string") {
405
+ return [{ type: "text", text: prompt }];
406
+ }
407
+ if (typeof prompt.content === "string") {
408
+ return [{ type: "text", text: prompt.content }];
409
+ }
410
+ const content = [];
411
+ for (const part of prompt.content) {
412
+ if (part.type === "text") {
413
+ content.push({ type: "text", text: part.text });
414
+ continue;
415
+ }
416
+ if (part.type === "image") {
417
+ throw unsupportedPromptContent({
418
+ harnessId,
419
+ category: "image content"
420
+ });
421
+ }
422
+ if (part.type === "file") {
423
+ const mediaCategory = part.mediaType.toLowerCase().split("/", 1)[0];
424
+ if (mediaCategory === "image") {
425
+ throw unsupportedPromptContent({
426
+ harnessId,
427
+ category: "image content"
428
+ });
429
+ }
430
+ if (mediaCategory === "audio") {
431
+ throw unsupportedPromptContent({
432
+ harnessId,
433
+ category: "audio content"
434
+ });
435
+ }
436
+ throw unsupportedPromptContent({
437
+ harnessId,
438
+ category: `embedded resource content with media type ${JSON.stringify(part.mediaType)}`
439
+ });
440
+ }
441
+ throw unsupportedPromptContent({
442
+ harnessId,
443
+ category: `content parts of type ${JSON.stringify(
444
+ part.type
445
+ )}`
446
+ });
447
+ }
448
+ return content;
449
+ }
450
+ function prependACPInitialGuidance({
451
+ prompt,
452
+ instructions,
453
+ skills
454
+ }) {
455
+ const hasInstructions = instructions != null && instructions.length > 0;
456
+ if (!hasInstructions && skills.length === 0) return [...prompt];
457
+ const lines = [
458
+ "<session-guidance>",
459
+ "This block is operating guidance from the harness, not user-authored content."
460
+ ];
461
+ if (hasInstructions) {
462
+ lines.push("<instructions>", instructions, "</instructions>");
463
+ }
464
+ if (skills.length > 0) {
465
+ lines.push(
466
+ "<available-skills>",
467
+ "Load a skill only when relevant by reading its SKILL.md file at the listed absolute path."
468
+ );
469
+ for (const skill of skills) {
470
+ lines.push(`- ${skill.name}: ${skill.description} (${skill.path})`);
471
+ }
472
+ lines.push("</available-skills>");
473
+ }
474
+ lines.push("</session-guidance>");
475
+ return [{ type: "text", text: lines.join("\n") }, ...prompt];
476
+ }
477
+ function unsupportedPromptContent({
478
+ harnessId,
479
+ category
480
+ }) {
481
+ return new HarnessCapabilityUnsupportedError({
482
+ harnessId,
483
+ message: `The ${harnessId} ACP harness supports only portable text prompts and does not support ${category}. Pass a string or a user message whose content contains only text parts.`
484
+ });
485
+ }
486
+
487
+ // src/v1/acp-v1-bridge-protocol.ts
488
+ var builtinToolSchema = z2.object({
489
+ toolName: z2.string(),
490
+ nativeName: z2.string().optional(),
491
+ inputSchema: z2.json().optional()
492
+ });
493
+ var jsonValueSchema = z2.json();
494
+ var acpSerializableToolSpecSchema = harnessV1BridgeToolWireSchema.extend({
495
+ inputSchema: z2.custom(
496
+ (value) => value != null && typeof value === "object" && !Array.isArray(value) && jsonValueSchema.safeParse(value).success
497
+ ).optional()
498
+ });
499
+ var permissionModeTargetSchema = z2.discriminatedUnion("type", [
500
+ z2.object({
501
+ type: z2.literal("session-mode"),
502
+ modeId: z2.string()
503
+ }),
504
+ z2.object({
505
+ type: z2.literal("session-config-option"),
506
+ configId: z2.string(),
507
+ value: z2.union([z2.string(), z2.boolean()])
508
+ })
509
+ ]);
510
+ var permissionModeMappingSchema = z2.object({
511
+ "allow-reads": permissionModeTargetSchema.nullable(),
512
+ "allow-edits": permissionModeTargetSchema.nullable(),
513
+ "allow-all": permissionModeTargetSchema.nullable()
514
+ });
515
+ var acpTurnStartConfigSchema = z2.object({
516
+ version: z2.literal(1),
517
+ configurationFingerprint: z2.string(),
518
+ prompt: z2.array(acpTextContentBlockSchema),
519
+ tools: z2.array(acpSerializableToolSpecSchema).default([]),
520
+ builtinTools: z2.array(builtinToolSchema).default([]),
521
+ permissionMode: harnessV1BridgeStartBaseSchema.shape.permissionMode.unwrap(),
522
+ permissionModeMapping: permissionModeMappingSchema.optional(),
523
+ debug: harnessV1BridgeStartBaseSchema.shape.debug.optional()
524
+ });
525
+ var acpColdSessionStateSchema = acpTurnStartConfigSchema.omit({ prompt: true, debug: true }).extend({
526
+ modelId: z2.string().optional()
527
+ });
528
+ var lossyRecoverySchema = z2.object({
529
+ type: z2.literal("lossy-rerun"),
530
+ acpSessionId: z2.string(),
531
+ reason: z2.string()
532
+ });
533
+ var coldRestoreSchema = z2.object({
534
+ type: z2.literal("cold-restore"),
535
+ acpSessionId: z2.string()
536
+ });
537
+ var outboundMessageSchema = harnessV1BridgeOutboundMessageSchema;
538
+ var startMessageSchema = harnessV1BridgeStartBaseSchema.extend({
539
+ prompt: z2.array(acpTextContentBlockSchema),
540
+ tools: z2.array(acpSerializableToolSpecSchema).optional(),
541
+ builtinTools: z2.array(builtinToolSchema).readonly().default([]),
542
+ permissionModeMapping: permissionModeMappingSchema.optional(),
543
+ turnStartConfig: acpTurnStartConfigSchema,
544
+ recoveryMode: z2.discriminatedUnion("type", [lossyRecoverySchema, coldRestoreSchema]).optional()
545
+ });
546
+ var inboundMessageSchema = z2.discriminatedUnion("type", [
547
+ startMessageSchema,
548
+ ...harnessV1BridgeInboundCommandSchemas
549
+ ]);
550
+
551
+ // src/v1/bridge/acp-v1-bridge-environment.ts
552
+ import { z as z3 } from "zod/v4";
553
+ var ACP_BRIDGE_CONFIGURATION_ENV = "AI_SDK_ACP_BRIDGE_CONFIGURATION";
554
+ var serializableValueSchema = z3.json();
555
+ var profileValueSchema = z3.lazy(
556
+ () => z3.union([
557
+ z3.string(),
558
+ z3.number(),
559
+ z3.boolean(),
560
+ z3.null(),
561
+ z3.object({
562
+ $source: z3.enum([
563
+ "gateway-api-key",
564
+ "gateway-base-url",
565
+ "gateway-authorization",
566
+ "client-app",
567
+ "client-app-name",
568
+ "client-app-version"
569
+ ]),
570
+ prefix: z3.string().optional(),
571
+ suffix: z3.string().optional(),
572
+ ensureSuffix: z3.string().optional()
573
+ }),
574
+ z3.array(profileValueSchema),
575
+ z3.record(z3.string(), profileValueSchema)
576
+ ])
577
+ );
578
+ var serializableRecordSchema = z3.record(z3.string(), serializableValueSchema);
579
+ var profileRecordSchema = z3.record(z3.string(), profileValueSchema);
580
+ var authenticationSchema = z3.object({
581
+ methodId: z3.string(),
582
+ meta: serializableRecordSchema.optional(),
583
+ clientCapabilities: serializableRecordSchema.optional()
584
+ });
585
+ var providerAuthenticationSchema = z3.discriminatedUnion("type", [
586
+ z3.object({ type: z3.literal("direct") }),
587
+ z3.object({
588
+ type: z3.literal("ai-gateway"),
589
+ env: profileRecordSchema
590
+ })
591
+ ]);
592
+ var bridgeConfigurationSchema = z3.object({
593
+ authentication: authenticationSchema.optional(),
594
+ providerAuthentication: providerAuthenticationSchema.optional(),
595
+ sessionMeta: serializableRecordSchema.optional()
596
+ });
597
+ function createACPBridgeEnvironment({
598
+ authentication,
599
+ providerAuthentication,
600
+ sessionMeta
601
+ }) {
602
+ return {
603
+ [ACP_BRIDGE_CONFIGURATION_ENV]: JSON.stringify({
604
+ ...authentication == null ? {} : { authentication },
605
+ ...providerAuthentication == null ? {} : { providerAuthentication },
606
+ ...sessionMeta == null ? {} : { sessionMeta }
607
+ })
608
+ };
609
+ }
610
+
611
+ // src/v1/acp-v1-turn-start-config.ts
612
+ import { createHash as createHash3 } from "crypto";
613
+ function createACPTurnStartConfig({
614
+ prompt,
615
+ tools,
616
+ builtinTools,
617
+ permissionMode,
618
+ permissionModeMapping,
619
+ debug,
620
+ authenticationProfile,
621
+ sessionMeta
622
+ }) {
623
+ return {
624
+ version: 1,
625
+ configurationFingerprint: createHash3("sha256").update(
626
+ stableStringify3({
627
+ authenticationProfile,
628
+ sessionMeta: sessionMeta ?? null,
629
+ builtinTools,
630
+ permissionModeMapping: permissionModeMapping ?? null
631
+ })
632
+ ).digest("hex"),
633
+ prompt: [...prompt],
634
+ tools: tools.map((tool) => acpSerializableToolSpecSchema.parse(tool)),
635
+ builtinTools: [...builtinTools],
636
+ permissionMode,
637
+ ...permissionModeMapping == null ? {} : { permissionModeMapping },
638
+ ...debug == null ? {} : { debug }
639
+ };
640
+ }
641
+ function createACPColdSessionState({
642
+ turnStartConfig,
643
+ modelId
644
+ }) {
645
+ return {
646
+ version: turnStartConfig.version,
647
+ configurationFingerprint: turnStartConfig.configurationFingerprint,
648
+ tools: turnStartConfig.tools,
649
+ builtinTools: turnStartConfig.builtinTools,
650
+ permissionMode: turnStartConfig.permissionMode,
651
+ ...turnStartConfig.permissionModeMapping == null ? {} : { permissionModeMapping: turnStartConfig.permissionModeMapping },
652
+ ...modelId == null ? {} : { modelId }
653
+ };
654
+ }
655
+ function stableStringify3(value) {
656
+ return JSON.stringify(sortValue2(value));
657
+ }
658
+ function sortValue2(value) {
659
+ if (Array.isArray(value)) {
660
+ return value.map((item) => sortValue2(item));
661
+ }
662
+ if (value != null && typeof value === "object") {
663
+ return Object.fromEntries(
664
+ Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, entry]) => [key, sortValue2(entry)])
665
+ );
666
+ }
667
+ return value;
668
+ }
669
+
670
+ // src/v1/acp-v1-lifecycle.ts
671
+ function resolveACPInitialGuidanceApplied({
672
+ isResume,
673
+ lifecycleState
674
+ }) {
675
+ if (!isResume) return false;
676
+ return lifecycleState?.initialGuidanceApplied ?? true;
677
+ }
678
+ function shouldMaterializeACPSkills({
679
+ isResume,
680
+ lifecycleState,
681
+ skillsFingerprint
682
+ }) {
683
+ if (isResume && lifecycleState?.skillsFingerprint != null && lifecycleState.skillsFingerprint !== skillsFingerprint) {
684
+ throw new Error(
685
+ "ACP lifecycle state was created with a different set of skills."
686
+ );
687
+ }
688
+ return !isResume || lifecycleState?.skillsMaterialized !== true;
689
+ }
690
+ function validateACPLifecycleCompatibility({
691
+ harnessId,
692
+ lifecycleHarnessId,
693
+ implementationIdentity,
694
+ authenticationProfile,
695
+ lifecycleData,
696
+ sandboxId
697
+ }) {
698
+ if (lifecycleHarnessId !== harnessId) {
699
+ throw new Error(
700
+ `ACP lifecycle state was produced by harness ${JSON.stringify(
701
+ lifecycleHarnessId
702
+ )}, but this harness is ${JSON.stringify(harnessId)}.`
703
+ );
704
+ }
705
+ if (lifecycleData.implementationIdentity !== implementationIdentity) {
706
+ throw new Error(
707
+ "ACP lifecycle state is incompatible with the configured implementation."
708
+ );
709
+ }
710
+ if (lifecycleData.authenticationProfile?.digest !== authenticationProfile.digest) {
711
+ throw new Error(
712
+ "ACP lifecycle state is incompatible with the configured authentication profile."
713
+ );
714
+ }
715
+ const expectedSandboxId = lifecycleData.bridge?.sandboxId;
716
+ if (expectedSandboxId != null && sandboxId != null && expectedSandboxId !== sandboxId) {
717
+ throw new Error(
718
+ `ACP lifecycle state belongs to sandbox ${JSON.stringify(
719
+ expectedSandboxId
720
+ )}, not ${JSON.stringify(sandboxId)}.`
721
+ );
722
+ }
723
+ }
724
+
725
+ // src/v1/acp-v1-skills.ts
726
+ import { createHash as createHash4 } from "crypto";
727
+ import path from "path";
728
+ import { writeSkills } from "@ai-sdk/harness/utils";
729
+ var ACP_SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
730
+ function createACPSkillsFingerprint({
731
+ skills
732
+ }) {
733
+ const canonicalSkills = skills.map((skill) => ({
734
+ name: skill.name,
735
+ description: skill.description,
736
+ content: skill.content,
737
+ files: [...skill.files ?? []].map((file) => ({
738
+ path: path.posix.normalize(file.path),
739
+ content: file.content
740
+ })).sort(
741
+ (left, right) => compareCanonicalStrings({ left: left.path, right: right.path })
742
+ )
743
+ })).sort(
744
+ (left, right) => compareCanonicalStrings({ left: left.name, right: right.name })
745
+ );
746
+ return createHash4("sha256").update(JSON.stringify(canonicalSkills)).digest("hex");
747
+ }
748
+ function resolveACPPrivateSessionDirectory({
749
+ sandboxHomeDir,
750
+ sessionWorkDir,
751
+ harnessId,
752
+ sessionId
753
+ }) {
754
+ const sessionKey = createHash4("sha256").update(sessionId).digest("hex");
755
+ const rootDir = path.posix.join(
756
+ sandboxHomeDir,
757
+ ".ai-sdk",
758
+ "harness-acp",
759
+ harnessId,
760
+ sessionKey
761
+ );
762
+ assertOutsideSessionWorkDir({ rootDir, sessionWorkDir });
763
+ return rootDir;
764
+ }
765
+ async function materializeACPSkills({
766
+ sandbox,
767
+ sandboxHomeDir,
768
+ sessionWorkDir,
769
+ harnessId,
770
+ sessionId,
771
+ skills,
772
+ shouldMaterialize,
773
+ abortSignal
774
+ }) {
775
+ validateACPSkills({ skills });
776
+ const rootDir = path.posix.join(
777
+ resolveACPPrivateSessionDirectory({
778
+ sandboxHomeDir,
779
+ sessionWorkDir,
780
+ harnessId,
781
+ sessionId
782
+ }),
783
+ "skills"
784
+ );
785
+ if (shouldMaterialize) {
786
+ await writeSkills({
787
+ sandbox,
788
+ rootDir,
789
+ skills,
790
+ abortSignal,
791
+ skillNamePattern: ACP_SKILL_NAME_PATTERN,
792
+ invalidSkillNameMessage: ({ name }) => `Invalid ACP skill name ${JSON.stringify(name)}: expected a kebab-case slug.`,
793
+ invalidSkillFilePathMessage: ({ skillName, filePath }) => `Invalid ACP skill file path ${JSON.stringify(filePath)} for skill ${JSON.stringify(
794
+ skillName
795
+ )}: expected a relative POSIX path without traversal.`
796
+ });
797
+ }
798
+ return {
799
+ rootDir,
800
+ catalog: skills.map((skill) => ({
801
+ name: skill.name,
802
+ description: skill.description,
803
+ path: path.posix.join(rootDir, skill.name, "SKILL.md")
804
+ }))
805
+ };
806
+ }
807
+ function validateACPSkills({
808
+ skills
809
+ }) {
810
+ const skillNames = /* @__PURE__ */ new Set();
811
+ for (const skill of skills) {
812
+ if (!ACP_SKILL_NAME_PATTERN.test(skill.name) || skill.name === "." || skill.name === "..") {
813
+ throw new Error(
814
+ `Invalid ACP skill name ${JSON.stringify(skill.name)}: expected a kebab-case slug.`
815
+ );
816
+ }
817
+ if (skillNames.has(skill.name)) {
818
+ throw new Error(
819
+ `Duplicate ACP skill name ${JSON.stringify(skill.name)}.`
820
+ );
821
+ }
822
+ skillNames.add(skill.name);
823
+ const filePaths = /* @__PURE__ */ new Set();
824
+ for (const file of skill.files ?? []) {
825
+ const normalizedPath = validateACPAttachedFilePath({
826
+ skillName: skill.name,
827
+ filePath: file.path
828
+ });
829
+ if (normalizedPath === "SKILL.md") {
830
+ throw new Error(
831
+ `Invalid ACP skill file path ${JSON.stringify(file.path)} for skill ${JSON.stringify(
832
+ skill.name
833
+ )}: SKILL.md is reserved for the skill definition.`
834
+ );
835
+ }
836
+ if (filePaths.has(normalizedPath)) {
837
+ throw new Error(
838
+ `Duplicate ACP skill file path ${JSON.stringify(file.path)} for skill ${JSON.stringify(
839
+ skill.name
840
+ )}.`
841
+ );
842
+ }
843
+ filePaths.add(normalizedPath);
844
+ }
845
+ }
846
+ }
847
+ function validateACPAttachedFilePath({
848
+ skillName,
849
+ filePath
850
+ }) {
851
+ const containsTraversal = filePath.split(/[\\/]/).some((segment) => segment === "..");
852
+ const normalizedPath = path.posix.normalize(filePath);
853
+ const invalid = filePath.length === 0 || filePath.endsWith("/") || filePath.includes("\\") || path.posix.isAbsolute(filePath) || path.win32.isAbsolute(filePath) || containsTraversal || normalizedPath === "." || normalizedPath.startsWith("../") || normalizedPath.includes("/../") || normalizedPath.endsWith("/..");
854
+ if (invalid) {
855
+ throw new Error(
856
+ `Invalid ACP skill file path ${JSON.stringify(filePath)} for skill ${JSON.stringify(
857
+ skillName
858
+ )}: expected a relative POSIX path without traversal.`
859
+ );
860
+ }
861
+ return normalizedPath;
862
+ }
863
+ function assertOutsideSessionWorkDir({
864
+ rootDir,
865
+ sessionWorkDir
866
+ }) {
867
+ const relative = path.posix.relative(sessionWorkDir, rootDir);
868
+ const isInside = relative === "" || !relative.startsWith("../") && !path.posix.isAbsolute(relative);
869
+ if (isInside) {
870
+ throw new Error(
871
+ `ACP skill directory ${JSON.stringify(rootDir)} must be outside sessionWorkDir ${JSON.stringify(
872
+ sessionWorkDir
873
+ )}.`
874
+ );
875
+ }
876
+ }
877
+ function compareCanonicalStrings({
878
+ left,
879
+ right
880
+ }) {
881
+ return left < right ? -1 : left > right ? 1 : 0;
882
+ }
883
+
884
+ // src/v1/acp-v1-harness.ts
885
+ var HARNESS_ID_REGEXP = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
886
+ function createACPV1({
887
+ settings,
888
+ builtinTools,
889
+ port: portOverride,
890
+ startupTimeoutMs,
891
+ clientApp,
892
+ implementationIdentity,
893
+ authenticationProfile,
894
+ providerAuthenticationCompatibility,
895
+ lifecycleStateSchema
896
+ }) {
897
+ if (!HARNESS_ID_REGEXP.test(settings.harnessId)) {
898
+ throw new Error(
899
+ `ACP harnessId must be a stable kebab-case identifier; received ${JSON.stringify(settings.harnessId)}.`
900
+ );
901
+ }
902
+ validateACPV1Implementation(settings.implementation);
903
+ const BOOTSTRAP_DIR = `.harness-bootstrap/${settings.harnessId}`;
904
+ let cachedBootstrap;
905
+ const permissionModeMapping = isCompletePermissionModeMapping({
906
+ value: settings.permissionModeMapping
907
+ }) ? settings.permissionModeMapping : void 0;
908
+ return {
909
+ specificationVersion: "harness-v1",
910
+ harnessId: settings.harnessId,
911
+ builtinTools,
912
+ supportsBuiltinToolApprovals: true,
913
+ supportsBuiltinToolFiltering: false,
914
+ lifecycleStateSchema,
915
+ getBootstrap: async () => {
916
+ if (cachedBootstrap != null) return cachedBootstrap;
917
+ const [bridgePackage, bridgeLock, bridge, hostToolMCP] = await Promise.all([
918
+ readBridgeAsset({ name: "package.json" }),
919
+ readBridgeAsset({ name: "pnpm-lock.yaml" }),
920
+ readBridgeAsset({ name: "index.mjs" }),
921
+ readBridgeAsset({ name: "host-tool-mcp.mjs" })
922
+ ]);
923
+ const implementationLock = getImplementationLockfile({
924
+ implementation: settings.implementation
925
+ });
926
+ cachedBootstrap = {
927
+ harnessId: settings.harnessId,
928
+ bootstrapDir: BOOTSTRAP_DIR,
929
+ files: [
930
+ {
931
+ path: `${BOOTSTRAP_DIR}/package.json`,
932
+ content: bridgePackage
933
+ },
934
+ {
935
+ path: `${BOOTSTRAP_DIR}/pnpm-lock.yaml`,
936
+ content: bridgeLock
937
+ },
938
+ { path: `${BOOTSTRAP_DIR}/bridge.mjs`, content: bridge },
939
+ {
940
+ path: `${BOOTSTRAP_DIR}/host-tool-mcp.mjs`,
941
+ content: hostToolMCP
942
+ },
943
+ {
944
+ path: `${BOOTSTRAP_DIR}/implementation/package.json`,
945
+ content: createImplementationManifest({
946
+ implementation: settings.implementation
947
+ })
948
+ },
949
+ {
950
+ path: `${BOOTSTRAP_DIR}/implementation/implementation.json`,
951
+ content: createImplementationDescriptor({
952
+ implementation: settings.implementation,
953
+ implementationIdentity
954
+ })
955
+ },
956
+ ...implementationLock == null ? [] : [
957
+ {
958
+ path: `${BOOTSTRAP_DIR}/implementation/pnpm-lock.yaml`,
959
+ content: implementationLock
960
+ }
961
+ ]
962
+ ],
963
+ commands: [
964
+ {
965
+ command: "pnpm install --frozen-lockfile --store-dir .pnpm-store"
966
+ },
967
+ {
968
+ command: createImplementationInstallCommand({
969
+ implementationDir: "implementation",
970
+ storeDir: "../.pnpm-store",
971
+ implementation: settings.implementation
972
+ })
973
+ }
974
+ ]
975
+ };
976
+ return cachedBootstrap;
977
+ },
978
+ doStart: async (startOptions) => {
979
+ if (startOptions.builtinToolFiltering != null) {
980
+ throw unsupported({
981
+ harnessId: settings.harnessId,
982
+ message: "ACP built-in tool filtering is not available in this initial ACP v1 implementation."
983
+ });
984
+ }
985
+ const permissionMode = startOptions.permissionMode ?? "allow-all";
986
+ const resolvedProviderAuthentication = resolveACPProviderAuthentication({
987
+ auth: {
988
+ mode: settings.auth,
989
+ providerAuthentication: settings.providerAuthentication,
990
+ clientApp
991
+ },
992
+ env: process.env,
993
+ compatibility: providerAuthenticationCompatibility
994
+ });
995
+ const sandboxSession = startOptions.sandboxSession;
996
+ const sandbox = sandboxSession.restricted();
997
+ const resolvedBridgeDir = posix.resolve(
998
+ sandboxSession.defaultWorkingDirectory,
999
+ BOOTSTRAP_DIR
1000
+ );
1001
+ const resolvedImplementationDir = `${resolvedBridgeDir}/implementation`;
1002
+ const workDir = startOptions.sessionWorkDir;
1003
+ const sandboxHomeDir = await resolveSandboxHomeDir({
1004
+ sandbox,
1005
+ abortSignal: startOptions.abortSignal
1006
+ });
1007
+ const privateSessionDir = resolveACPPrivateSessionDirectory({
1008
+ sandboxHomeDir,
1009
+ sessionWorkDir: workDir,
1010
+ harnessId: settings.harnessId,
1011
+ sessionId: startOptions.sessionId
1012
+ });
1013
+ const bridgeStateDir = `${privateSessionDir}/bridge`;
1014
+ const continueFrom = startOptions.continueFrom ?? startOptions.resumeFrom?.continueFrom;
1015
+ const lifecycleState = continueFrom ?? startOptions.resumeFrom;
1016
+ const isContinue = continueFrom != null;
1017
+ const isResume = lifecycleState != null;
1018
+ const lifecycleData = lifecycleState != null ? lifecycleState.data : void 0;
1019
+ if (lifecycleData != null && lifecycleState != null) {
1020
+ validateACPLifecycleCompatibility({
1021
+ harnessId: settings.harnessId,
1022
+ lifecycleHarnessId: lifecycleState.harnessId,
1023
+ implementationIdentity,
1024
+ authenticationProfile,
1025
+ lifecycleData,
1026
+ sandboxId: sandboxSession.id
1027
+ });
1028
+ }
1029
+ const skills = startOptions.skills ?? [];
1030
+ const skillsFingerprint = createACPSkillsFingerprint({ skills });
1031
+ const shouldMaterializeSkills = shouldMaterializeACPSkills({
1032
+ isResume,
1033
+ lifecycleState: lifecycleData,
1034
+ skillsFingerprint
1035
+ });
1036
+ let skillCatalog = [];
1037
+ if (skills.length > 0) {
1038
+ skillCatalog = (await materializeACPSkills({
1039
+ sandbox,
1040
+ sandboxHomeDir,
1041
+ sessionWorkDir: workDir,
1042
+ harnessId: settings.harnessId,
1043
+ sessionId: startOptions.sessionId,
1044
+ skills,
1045
+ shouldMaterialize: shouldMaterializeSkills,
1046
+ abortSignal: startOptions.abortSignal
1047
+ })).catalog;
1048
+ }
1049
+ const report = startOptions.observability?.report;
1050
+ const onDiagnostic = report ? (frame) => report(
1051
+ harnessV1DiagnosticFromBridgeFrame(frame, {
1052
+ sessionId: startOptions.sessionId,
1053
+ timestamp: Date.now()
1054
+ })
1055
+ ) : void 0;
1056
+ const onBridgeError = createBridgeErrorHandler({
1057
+ harnessId: settings.harnessId,
1058
+ sessionId: startOptions.sessionId
1059
+ });
1060
+ const builtinToolCatalog = serializeBuiltinTools({ builtinTools });
1061
+ let respawnStrategy;
1062
+ if (isResume) {
1063
+ if (lifecycleData == null) {
1064
+ throw new Error("ACP lifecycle state data is missing.");
1065
+ }
1066
+ const coords = lifecycleData.bridge;
1067
+ if (coords == null && isContinue) {
1068
+ throw unsupported({
1069
+ harnessId: settings.harnessId,
1070
+ message: "ACP continuation state does not contain bridge coordinates required for replay or process-loss rerun."
1071
+ });
1072
+ }
1073
+ if (coords != null) {
1074
+ try {
1075
+ const attachUrl = await sandboxSession.getPortUrl({
1076
+ port: coords.port,
1077
+ protocol: "ws"
1078
+ }) + `?agent_bridge_token=${encodeURIComponent(coords.token)}`;
1079
+ const attachChannel = new SandboxChannel({
1080
+ connect: () => openWebSocket({ url: attachUrl }),
1081
+ outboundSchema: outboundMessageSchema,
1082
+ initialLastSeenEventId: coords.lastSeenEventId,
1083
+ onDiagnostic,
1084
+ onBridgeError
1085
+ });
1086
+ await attachChannel.open(isContinue ? { resume: true } : void 0);
1087
+ return createSession({
1088
+ sessionId: startOptions.sessionId,
1089
+ harnessId: settings.harnessId,
1090
+ channel: attachChannel,
1091
+ proc: void 0,
1092
+ modelId: settings.modelId,
1093
+ sessionMeta: settings.session?.meta,
1094
+ debug: startOptions.observability?.debug,
1095
+ implementationIdentity,
1096
+ authenticationProfile,
1097
+ builtinTools: builtinToolCatalog,
1098
+ permissionMode,
1099
+ permissionModeMapping,
1100
+ initialGuidanceApplied: resolveACPInitialGuidanceApplied({
1101
+ isResume: true,
1102
+ lifecycleState: lifecycleData
1103
+ }),
1104
+ skillCatalog,
1105
+ skillsFingerprint,
1106
+ acpSessionId: lifecycleData.acpSessionId,
1107
+ bridgePort: coords.port,
1108
+ bridgeToken: coords.token,
1109
+ sandboxId: sandboxSession.id,
1110
+ isResume: true,
1111
+ turnInFlight: isContinue,
1112
+ bridgeStateDir,
1113
+ turnStartConfig: lifecycleData.turnStartConfig,
1114
+ recoveryStatus: lifecycleData.recovery,
1115
+ restoration: lifecycleData.restoration,
1116
+ replayOnly: false,
1117
+ lossyRerun: false
1118
+ });
1119
+ } catch (error) {
1120
+ if (isContinue) {
1121
+ const eventLog = await Promise.resolve(
1122
+ sandbox.readTextFile({
1123
+ path: `${bridgeStateDir}/event-log.ndjson`,
1124
+ abortSignal: startOptions.abortSignal
1125
+ })
1126
+ );
1127
+ const recoveryMode = await classifyDiskLog(eventLog);
1128
+ if (recoveryMode === "replay") {
1129
+ respawnStrategy = {
1130
+ mode: "disk-replay",
1131
+ reason: "completed coherent event log",
1132
+ afterSeq: coords.lastSeenEventId
1133
+ };
1134
+ } else {
1135
+ const turnStartConfig = lifecycleData.turnStartConfig;
1136
+ const acpSessionId = lifecycleData.acpSessionId;
1137
+ if (turnStartConfig == null || acpSessionId == null) {
1138
+ throw unsupported({
1139
+ harnessId: settings.harnessId,
1140
+ message: "ACP process-loss recovery is unavailable because the lifecycle state does not contain the persisted turn start configuration and ACP session identifier.",
1141
+ cause: error
1142
+ });
1143
+ }
1144
+ validateACPTurnStartConfig({
1145
+ turnStartConfig,
1146
+ authenticationProfile,
1147
+ sessionMeta: settings.session?.meta,
1148
+ builtinTools: builtinToolCatalog,
1149
+ permissionModeMapping
1150
+ });
1151
+ respawnStrategy = {
1152
+ mode: "lossy-rerun",
1153
+ reason: "event log not replayable",
1154
+ turnStartConfig,
1155
+ acpSessionId
1156
+ };
1157
+ }
1158
+ }
1159
+ }
1160
+ }
1161
+ if (!isContinue) {
1162
+ const coldSession = lifecycleData.coldSession;
1163
+ const acpSessionId = lifecycleData.acpSessionId;
1164
+ if (coldSession == null || acpSessionId == null) {
1165
+ throw unsupported({
1166
+ harnessId: settings.harnessId,
1167
+ message: "Cold ACP session restoration requires persisted cold-session configuration and an ACP session identifier."
1168
+ });
1169
+ }
1170
+ const turnStartConfig = validateACPColdSessionConfiguration({
1171
+ coldSession,
1172
+ modelId: settings.modelId,
1173
+ permissionMode,
1174
+ authenticationProfile,
1175
+ sessionMeta: settings.session?.meta,
1176
+ builtinTools: builtinToolCatalog,
1177
+ permissionModeMapping,
1178
+ debug: startOptions.observability?.debug
1179
+ });
1180
+ respawnStrategy = {
1181
+ mode: "cold-restore",
1182
+ turnStartConfig,
1183
+ acpSessionId
1184
+ };
1185
+ }
1186
+ }
1187
+ const port = resolveBridgePort({
1188
+ sandboxSession,
1189
+ override: portOverride,
1190
+ harnessId: settings.harnessId
1191
+ });
1192
+ const token = randomBytes(32).toString("hex");
1193
+ await sandbox.run({
1194
+ command: `mkdir -p ${shellQuote(workDir)} ${shellQuote(bridgeStateDir)}`,
1195
+ abortSignal: startOptions.abortSignal
1196
+ });
1197
+ await markBridgeStarting({
1198
+ sandbox,
1199
+ bridgeStateDir,
1200
+ bridgeType: settings.harnessId,
1201
+ abortSignal: startOptions.abortSignal
1202
+ });
1203
+ const proc = await sandbox.spawn({
1204
+ command: `node ${shellQuote(`${resolvedBridgeDir}/bridge.mjs`)} --workdir ${shellQuote(workDir)} --bridge-state-dir ${shellQuote(bridgeStateDir)} --implementation-dir ${shellQuote(resolvedImplementationDir)} --bridge-type ${shellQuote(settings.harnessId)}`,
1205
+ env: {
1206
+ ...resolveImplementationEnvironment({
1207
+ implementation: settings.implementation,
1208
+ env: process.env
1209
+ }),
1210
+ ...createACPBridgeEnvironment({
1211
+ authentication: settings.authentication,
1212
+ providerAuthentication: resolvedProviderAuthentication.providerAuthentication,
1213
+ sessionMeta: settings.session?.meta
1214
+ }),
1215
+ ...resolvedProviderAuthentication.env,
1216
+ BRIDGE_CHANNEL_TOKEN: token,
1217
+ BRIDGE_WS_PORT: String(port),
1218
+ ...respawnStrategy?.mode === "disk-replay" ? {
1219
+ BRIDGE_REPLAY_FROM_DISK: "1"
1220
+ } : {}
1221
+ },
1222
+ abortSignal: startOptions.abortSignal
1223
+ });
1224
+ const stderrTail = [];
1225
+ const bridgeStderrDone = forwardBridgeProcessStream({
1226
+ stream: proc.stderr,
1227
+ streamName: "stderr",
1228
+ source: settings.harnessId,
1229
+ collectTail: stderrTail
1230
+ });
1231
+ const timeoutMs = startupTimeoutMs ?? 12e4;
1232
+ const { port: boundPort } = await waitForBridgeReady({
1233
+ proc,
1234
+ sandbox,
1235
+ bridgeStateDir,
1236
+ bridgeType: settings.harnessId,
1237
+ timeoutMs,
1238
+ abortSignal: startOptions.abortSignal,
1239
+ createTimeoutError: ({ proc: proc2, stdoutTail }) => createBridgeStartupError({
1240
+ message: `${settings.harnessId} ACP bridge did not become ready in time.`,
1241
+ proc: proc2,
1242
+ stdoutTail,
1243
+ stderrTail,
1244
+ stderrDone: bridgeStderrDone
1245
+ }),
1246
+ createExitError: ({ proc: proc2, stdoutTail }) => createBridgeStartupError({
1247
+ message: `${settings.harnessId} ACP bridge exited before becoming ready.`,
1248
+ proc: proc2,
1249
+ stdoutTail,
1250
+ stderrTail,
1251
+ stderrDone: bridgeStderrDone
1252
+ })
1253
+ });
1254
+ void drainBridgeProcessStream(proc.stdout);
1255
+ const wsUrl = await sandboxSession.getPortUrl({
1256
+ port: boundPort,
1257
+ protocol: "ws"
1258
+ }) + `?agent_bridge_token=${encodeURIComponent(token)}`;
1259
+ const channel = new SandboxChannel({
1260
+ connect: () => openWebSocket({ url: wsUrl }),
1261
+ outboundSchema: outboundMessageSchema,
1262
+ ...respawnStrategy?.mode === "disk-replay" ? { initialLastSeenEventId: respawnStrategy.afterSeq } : {},
1263
+ onDiagnostic,
1264
+ onBridgeError
1265
+ });
1266
+ await channel.open(
1267
+ respawnStrategy?.mode === "disk-replay" ? { resume: true } : void 0
1268
+ );
1269
+ let coldRestoration;
1270
+ if (respawnStrategy?.mode === "cold-restore") {
1271
+ try {
1272
+ coldRestoration = await restoreColdACPSession({
1273
+ channel,
1274
+ harnessId: settings.harnessId,
1275
+ start: {
1276
+ type: "start",
1277
+ prompt: [],
1278
+ debug: startOptions.observability?.debug,
1279
+ builtinTools: builtinToolCatalog,
1280
+ permissionMode,
1281
+ permissionModeMapping,
1282
+ tools: respawnStrategy.turnStartConfig.tools,
1283
+ turnStartConfig: respawnStrategy.turnStartConfig,
1284
+ recoveryMode: {
1285
+ type: "cold-restore",
1286
+ acpSessionId: respawnStrategy.acpSessionId
1287
+ }
1288
+ }
1289
+ });
1290
+ } catch (error) {
1291
+ channel.beginClose();
1292
+ try {
1293
+ if (!channel.isClosed()) {
1294
+ channel.send({ type: "destroy" });
1295
+ }
1296
+ } catch {
1297
+ }
1298
+ try {
1299
+ await proc.kill();
1300
+ } catch {
1301
+ }
1302
+ channel.close();
1303
+ throw error;
1304
+ }
1305
+ }
1306
+ return createSession({
1307
+ sessionId: startOptions.sessionId,
1308
+ harnessId: settings.harnessId,
1309
+ channel,
1310
+ proc,
1311
+ modelId: settings.modelId,
1312
+ sessionMeta: settings.session?.meta,
1313
+ debug: startOptions.observability?.debug,
1314
+ implementationIdentity,
1315
+ authenticationProfile,
1316
+ builtinTools: builtinToolCatalog,
1317
+ permissionMode,
1318
+ permissionModeMapping,
1319
+ initialGuidanceApplied: resolveACPInitialGuidanceApplied({
1320
+ isResume,
1321
+ lifecycleState: lifecycleData
1322
+ }),
1323
+ skillCatalog,
1324
+ skillsFingerprint,
1325
+ acpSessionId: lifecycleData?.acpSessionId,
1326
+ bridgePort: boundPort,
1327
+ bridgeToken: token,
1328
+ sandboxId: sandboxSession.id,
1329
+ isResume,
1330
+ turnInFlight: respawnStrategy?.mode === "disk-replay" || respawnStrategy?.mode === "lossy-rerun",
1331
+ bridgeStateDir,
1332
+ turnStartConfig: respawnStrategy?.mode === "lossy-rerun" || respawnStrategy?.mode === "cold-restore" ? respawnStrategy.turnStartConfig : lifecycleData?.turnStartConfig,
1333
+ recoveryStatus: respawnStrategy?.mode === "disk-replay" || respawnStrategy?.mode === "lossy-rerun" ? {
1334
+ mode: respawnStrategy.mode,
1335
+ reason: respawnStrategy.reason
1336
+ } : void 0,
1337
+ restoration: coldRestoration == null ? lifecycleData?.restoration : { method: coldRestoration },
1338
+ replayOnly: respawnStrategy?.mode === "disk-replay",
1339
+ lossyRerun: respawnStrategy?.mode === "lossy-rerun"
1340
+ });
1341
+ }
1342
+ };
1343
+ }
1344
+ async function readBridgeAsset({ name }) {
1345
+ const candidates = resolveBridgeAssetCandidates({
1346
+ name,
1347
+ moduleUrl: import.meta.url
1348
+ });
1349
+ let lastError;
1350
+ for (const url of candidates) {
1351
+ try {
1352
+ return await readFile(fileURLToPath(url), "utf8");
1353
+ } catch (error) {
1354
+ if (error.code !== "ENOENT") throw error;
1355
+ lastError = error;
1356
+ }
1357
+ }
1358
+ throw lastError ?? new Error(`ACP bridge asset not found: ${name}`);
1359
+ }
1360
+ function resolveBridgeAssetCandidates({
1361
+ name,
1362
+ moduleUrl
1363
+ }) {
1364
+ return [
1365
+ new URL(`./bridge/${name}`, moduleUrl),
1366
+ new URL(`../bridge/${name}`, moduleUrl)
1367
+ ];
1368
+ }
1369
+ function resolveBridgePort({
1370
+ sandboxSession,
1371
+ override,
1372
+ harnessId
1373
+ }) {
1374
+ if (override !== void 0) return override;
1375
+ if (sandboxSession.ports.length > 0) return sandboxSession.ports[0];
1376
+ throw unsupported({
1377
+ harnessId,
1378
+ message: `The ${harnessId} ACP harness needs a TCP port exposed by the sandbox. Create the sandbox with \`ports: [<port>]\` or configure the harness \`port\`.`
1379
+ });
1380
+ }
1381
+ function openWebSocket({ url }) {
1382
+ return new Promise((resolve, reject) => {
1383
+ const ws = new WebSocket(url);
1384
+ const onOpen = () => {
1385
+ ws.off("error", onError);
1386
+ resolve(ws);
1387
+ };
1388
+ const onError = (error) => {
1389
+ ws.off("open", onOpen);
1390
+ reject(error);
1391
+ };
1392
+ ws.once("open", onOpen);
1393
+ ws.once("error", onError);
1394
+ });
1395
+ }
1396
+ function restoreColdACPSession({
1397
+ channel,
1398
+ harnessId,
1399
+ start
1400
+ }) {
1401
+ return new Promise((complete) => {
1402
+ let restorationMethod;
1403
+ let settled = false;
1404
+ const subscriptions = [];
1405
+ const cleanup = () => {
1406
+ for (const unsubscribe of subscriptions) unsubscribe();
1407
+ };
1408
+ const settle = ({
1409
+ method,
1410
+ error
1411
+ }) => {
1412
+ if (settled) return;
1413
+ settled = true;
1414
+ cleanup();
1415
+ complete({
1416
+ ...method == null ? {} : { method },
1417
+ ...error === void 0 ? {} : { error }
1418
+ });
1419
+ };
1420
+ subscriptions.push(
1421
+ channel.on("raw", (event) => {
1422
+ const rawValue = event.rawValue;
1423
+ if (isRecord(rawValue) && rawValue.type === "acp-session-restored" && (rawValue.method === "resume" || rawValue.method === "load")) {
1424
+ restorationMethod = rawValue.method;
1425
+ }
1426
+ })
1427
+ );
1428
+ subscriptions.push(
1429
+ channel.on("finish", () => {
1430
+ settle({ method: restorationMethod });
1431
+ })
1432
+ );
1433
+ subscriptions.push(
1434
+ channel.on("error", (event) => {
1435
+ settle({
1436
+ error: deserializeBridgeError({
1437
+ error: event.error,
1438
+ harnessId
1439
+ })
1440
+ });
1441
+ })
1442
+ );
1443
+ channel.onClose((_code, reason) => {
1444
+ settle({
1445
+ error: new Error(
1446
+ `${harnessId} ACP bridge closed during cold restoration: ${reason}`
1447
+ )
1448
+ });
1449
+ });
1450
+ try {
1451
+ channel.send(start);
1452
+ } catch (error) {
1453
+ settle({ error });
1454
+ }
1455
+ }).then((result) => {
1456
+ if (result.error !== void 0) throw result.error;
1457
+ if (result.method == null) {
1458
+ throw new Error(
1459
+ `${harnessId} ACP cold restoration completed without identifying the negotiated method.`
1460
+ );
1461
+ }
1462
+ return result.method;
1463
+ });
1464
+ }
1465
+ function createSession({
1466
+ sessionId,
1467
+ harnessId,
1468
+ channel,
1469
+ proc,
1470
+ modelId,
1471
+ sessionMeta,
1472
+ debug,
1473
+ implementationIdentity,
1474
+ authenticationProfile,
1475
+ builtinTools,
1476
+ permissionMode,
1477
+ permissionModeMapping,
1478
+ initialGuidanceApplied: initialGuidanceAppliedAtStart,
1479
+ skillCatalog,
1480
+ skillsFingerprint,
1481
+ acpSessionId: acpSessionIdAtStart,
1482
+ bridgePort,
1483
+ bridgeToken,
1484
+ sandboxId,
1485
+ isResume,
1486
+ turnInFlight: turnInFlightAtStart,
1487
+ bridgeStateDir,
1488
+ turnStartConfig: turnStartConfigAtStart,
1489
+ recoveryStatus,
1490
+ restoration,
1491
+ replayOnly,
1492
+ lossyRerun
1493
+ }) {
1494
+ let stopped = false;
1495
+ let turnInFlight = turnInFlightAtStart;
1496
+ let initialGuidanceApplied = initialGuidanceAppliedAtStart;
1497
+ let latestACPSessionId = acpSessionIdAtStart;
1498
+ let latestTurnStartConfig = turnStartConfigAtStart;
1499
+ const markTurnFinished = () => {
1500
+ turnInFlight = false;
1501
+ };
1502
+ channel.on("bridge-thread", (event) => {
1503
+ latestACPSessionId = event.threadId;
1504
+ });
1505
+ channel.on("finish", markTurnFinished);
1506
+ channel.on("error", markTurnFinished);
1507
+ channel.onClose(markTurnFinished);
1508
+ const wireTurn = ({
1509
+ emit,
1510
+ abortSignal,
1511
+ start
1512
+ }) => {
1513
+ let resolveDone;
1514
+ let rejectDone;
1515
+ const done = new Promise((resolve, reject) => {
1516
+ resolveDone = resolve;
1517
+ rejectDone = reject;
1518
+ });
1519
+ let settled = false;
1520
+ let abortRequested = false;
1521
+ let abortError;
1522
+ let abortListenerAttached = false;
1523
+ let openBlock;
1524
+ const subscriptions = [];
1525
+ const forward = (event) => {
1526
+ if (event.type === "text-start" || event.type === "reasoning-start") {
1527
+ openBlock = {
1528
+ type: event.type === "text-start" ? "text" : "reasoning",
1529
+ id: event.id
1530
+ };
1531
+ } else if ((event.type === "text-end" || event.type === "reasoning-end") && openBlock?.id === event.id) {
1532
+ openBlock = void 0;
1533
+ }
1534
+ try {
1535
+ emit(event);
1536
+ } catch {
1537
+ }
1538
+ };
1539
+ const closeForwardedBlock = () => {
1540
+ if (openBlock == null) return;
1541
+ const block = openBlock;
1542
+ openBlock = void 0;
1543
+ forward({
1544
+ type: block.type === "text" ? "text-end" : "reasoning-end",
1545
+ id: block.id
1546
+ });
1547
+ };
1548
+ const eventTypes = [
1549
+ "stream-start",
1550
+ "text-start",
1551
+ "text-delta",
1552
+ "text-end",
1553
+ "reasoning-start",
1554
+ "reasoning-delta",
1555
+ "reasoning-end",
1556
+ "tool-call",
1557
+ "tool-approval-request",
1558
+ "tool-result",
1559
+ "file-change",
1560
+ "finish-step",
1561
+ "raw"
1562
+ ];
1563
+ const cleanup = () => {
1564
+ for (const unsubscribe of subscriptions) unsubscribe();
1565
+ if (abortListenerAttached) {
1566
+ abortSignal?.removeEventListener("abort", onAbort);
1567
+ }
1568
+ };
1569
+ const settle = ({ error }) => {
1570
+ if (settled) return;
1571
+ settled = true;
1572
+ cleanup();
1573
+ if (error === void 0) resolveDone();
1574
+ else rejectDone(error);
1575
+ };
1576
+ const onAbort = () => {
1577
+ if (settled || abortRequested) return;
1578
+ abortRequested = true;
1579
+ abortError = abortSignal?.reason ?? new DOMException("Aborted", "AbortError");
1580
+ try {
1581
+ channel.send({ type: "abort" });
1582
+ } catch {
1583
+ settle({ error: abortError });
1584
+ }
1585
+ };
1586
+ for (const type of eventTypes) {
1587
+ subscriptions.push(channel.on(type, (event) => forward(event)));
1588
+ }
1589
+ subscriptions.push(
1590
+ channel.on("finish", (event) => {
1591
+ closeForwardedBlock();
1592
+ forward(event);
1593
+ settle(abortRequested ? { error: abortError } : {});
1594
+ })
1595
+ );
1596
+ subscriptions.push(
1597
+ channel.on("error", (event) => {
1598
+ closeForwardedBlock();
1599
+ const error = deserializeBridgeError({
1600
+ error: event.error,
1601
+ harnessId
1602
+ });
1603
+ forward({ ...event, error });
1604
+ settle({ error });
1605
+ })
1606
+ );
1607
+ channel.onClose((_code, reason) => {
1608
+ if (reason === "suspended") {
1609
+ settle({});
1610
+ return;
1611
+ }
1612
+ closeForwardedBlock();
1613
+ settle({
1614
+ error: abortRequested && abortError !== void 0 ? abortError : new Error(`${harnessId} ACP bridge closed before turn end.`)
1615
+ });
1616
+ });
1617
+ try {
1618
+ start();
1619
+ } catch (error) {
1620
+ settled = true;
1621
+ cleanup();
1622
+ throw error;
1623
+ }
1624
+ if (abortSignal?.aborted) onAbort();
1625
+ else if (abortSignal != null) {
1626
+ abortSignal.addEventListener("abort", onAbort, { once: true });
1627
+ abortListenerAttached = true;
1628
+ }
1629
+ return {
1630
+ submitToolApproval: async (input) => {
1631
+ channel.send({
1632
+ type: "tool-approval-response",
1633
+ approvalId: input.approvalId,
1634
+ approved: input.approved,
1635
+ reason: input.reason
1636
+ });
1637
+ },
1638
+ submitToolResult: async (input) => {
1639
+ channel.send({
1640
+ type: "tool-result",
1641
+ toolCallId: input.toolCallId,
1642
+ output: input.output,
1643
+ isError: input.isError
1644
+ });
1645
+ },
1646
+ done
1647
+ };
1648
+ };
1649
+ const terminateBridge = async ({
1650
+ command
1651
+ }) => {
1652
+ if (stopped) return;
1653
+ stopped = true;
1654
+ channel.beginClose();
1655
+ const channelClosed = new Promise((resolve) => {
1656
+ channel.onClose(() => resolve());
1657
+ });
1658
+ try {
1659
+ if (!channel.isClosed()) {
1660
+ channel.send({ type: command });
1661
+ }
1662
+ } catch {
1663
+ }
1664
+ let timer;
1665
+ try {
1666
+ await Promise.race([
1667
+ proc == null ? channelClosed : proc.wait(),
1668
+ new Promise((resolve) => {
1669
+ timer = setTimeout(resolve, 5e3);
1670
+ timer.unref?.();
1671
+ })
1672
+ ]);
1673
+ } finally {
1674
+ if (timer != null) clearTimeout(timer);
1675
+ try {
1676
+ await proc?.kill();
1677
+ } catch {
1678
+ }
1679
+ channel.close();
1680
+ }
1681
+ };
1682
+ const createLifecycleData = ({
1683
+ bridge,
1684
+ includeTurnStartConfig = true
1685
+ }) => ({
1686
+ implementationIdentity,
1687
+ authenticationProfile,
1688
+ ...latestACPSessionId == null ? {} : { acpSessionId: latestACPSessionId },
1689
+ ...bridge == null ? {} : { bridge },
1690
+ ...latestTurnStartConfig == null ? {} : {
1691
+ coldSession: createACPColdSessionState({
1692
+ turnStartConfig: latestTurnStartConfig,
1693
+ modelId
1694
+ })
1695
+ },
1696
+ ...!includeTurnStartConfig || latestTurnStartConfig == null ? {} : { turnStartConfig: latestTurnStartConfig },
1697
+ ...recoveryStatus == null ? {} : { recovery: recoveryStatus },
1698
+ ...restoration == null ? {} : { restoration },
1699
+ initialGuidanceApplied,
1700
+ skillsMaterialized: true,
1701
+ skillsFingerprint
1702
+ });
1703
+ return {
1704
+ sessionId,
1705
+ isResume,
1706
+ ...modelId == null ? {} : { modelId },
1707
+ doPromptTurn: async (options) => {
1708
+ if (replayOnly) {
1709
+ throw new Error(
1710
+ `${harnessId} recovered this turn through disk replay only and has no restored ACP process for a subsequent prompt.`
1711
+ );
1712
+ }
1713
+ if (options.abortSignal?.aborted) {
1714
+ throw options.abortSignal.reason ?? new DOMException("Aborted", "AbortError");
1715
+ }
1716
+ const prompt = convertHarnessPromptToACPTextBlocks({
1717
+ prompt: options.prompt,
1718
+ harnessId
1719
+ });
1720
+ const turnStartConfig = createACPTurnStartConfig({
1721
+ prompt,
1722
+ tools: options.tools ?? [],
1723
+ builtinTools,
1724
+ permissionMode,
1725
+ permissionModeMapping,
1726
+ debug,
1727
+ authenticationProfile,
1728
+ sessionMeta
1729
+ });
1730
+ const control = wireTurn({
1731
+ emit: options.emit,
1732
+ abortSignal: options.abortSignal,
1733
+ start: () => {
1734
+ if (turnInFlight) {
1735
+ throw new Error(
1736
+ `${harnessId} cannot start a new ACP prompt while a turn is in flight.`
1737
+ );
1738
+ }
1739
+ turnInFlight = true;
1740
+ channel.send({
1741
+ type: "start",
1742
+ prompt: initialGuidanceApplied ? prompt : prependACPInitialGuidance({
1743
+ prompt,
1744
+ instructions: options.instructions,
1745
+ skills: skillCatalog
1746
+ }),
1747
+ debug,
1748
+ builtinTools,
1749
+ permissionMode,
1750
+ permissionModeMapping,
1751
+ tools: options.tools == null ? void 0 : turnStartConfig.tools,
1752
+ turnStartConfig
1753
+ });
1754
+ latestTurnStartConfig = turnStartConfig;
1755
+ initialGuidanceApplied = true;
1756
+ }
1757
+ });
1758
+ return control;
1759
+ },
1760
+ doContinueTurn: async (options) => {
1761
+ if (!turnInFlight) {
1762
+ throw new Error(`${harnessId} has no in-flight ACP turn to continue.`);
1763
+ }
1764
+ if (lossyRerun) {
1765
+ if (latestTurnStartConfig == null || latestACPSessionId == null) {
1766
+ throw new Error(
1767
+ `${harnessId} cannot perform lossy ACP rerun without persisted start configuration and an ACP session identifier.`
1768
+ );
1769
+ }
1770
+ assertRecoveryToolCatalog({
1771
+ persisted: latestTurnStartConfig.tools,
1772
+ current: options.tools ?? []
1773
+ });
1774
+ }
1775
+ return wireTurn({
1776
+ emit: options.emit,
1777
+ abortSignal: options.abortSignal,
1778
+ start: () => {
1779
+ if (!lossyRerun) return;
1780
+ const turnStartConfig = latestTurnStartConfig;
1781
+ channel.send({
1782
+ type: "start",
1783
+ prompt: turnStartConfig.prompt,
1784
+ debug: turnStartConfig.debug,
1785
+ builtinTools: turnStartConfig.builtinTools,
1786
+ permissionMode: turnStartConfig.permissionMode,
1787
+ permissionModeMapping: turnStartConfig.permissionModeMapping,
1788
+ tools: turnStartConfig.tools,
1789
+ turnStartConfig,
1790
+ recoveryMode: {
1791
+ type: "lossy-rerun",
1792
+ acpSessionId: latestACPSessionId,
1793
+ reason: recoveryStatus?.reason ?? "bridge process loss"
1794
+ }
1795
+ });
1796
+ }
1797
+ });
1798
+ },
1799
+ doCompact: async () => {
1800
+ throw unsupported({
1801
+ harnessId,
1802
+ message: "ACP v1 does not define manual session compaction."
1803
+ });
1804
+ },
1805
+ doSuspendTurn: async () => {
1806
+ if (stopped) {
1807
+ throw new Error(
1808
+ `${harnessId} ACP session ${sessionId} is stopped; cannot suspend.`
1809
+ );
1810
+ }
1811
+ if (!turnInFlight) {
1812
+ throw new Error(
1813
+ `${harnessId} ACP session ${sessionId} has no in-flight turn to suspend.`
1814
+ );
1815
+ }
1816
+ stopped = true;
1817
+ const lastSeenEventId = await channel.suspend();
1818
+ return {
1819
+ type: "continue-turn",
1820
+ harnessId,
1821
+ specificationVersion: "harness-v1",
1822
+ data: createLifecycleData({
1823
+ bridge: {
1824
+ port: bridgePort,
1825
+ token: bridgeToken,
1826
+ lastSeenEventId,
1827
+ ...sandboxId == null ? {} : { sandboxId },
1828
+ stateDir: bridgeStateDir
1829
+ }
1830
+ })
1831
+ };
1832
+ },
1833
+ doDetach: async () => {
1834
+ if (stopped) {
1835
+ throw new Error(
1836
+ `${harnessId} ACP session ${sessionId} is stopped; cannot detach.`
1837
+ );
1838
+ }
1839
+ if (turnInFlight) {
1840
+ throw new Error(
1841
+ `${harnessId} ACP session ${sessionId} has an in-flight turn; suspend it instead.`
1842
+ );
1843
+ }
1844
+ stopped = true;
1845
+ const lastSeenEventId = await channel.suspend();
1846
+ return {
1847
+ type: "resume-session",
1848
+ harnessId,
1849
+ specificationVersion: "harness-v1",
1850
+ data: createLifecycleData({
1851
+ bridge: {
1852
+ port: bridgePort,
1853
+ token: bridgeToken,
1854
+ lastSeenEventId,
1855
+ ...sandboxId == null ? {} : { sandboxId },
1856
+ stateDir: bridgeStateDir
1857
+ }
1858
+ })
1859
+ };
1860
+ },
1861
+ doStop: async () => {
1862
+ const data = createLifecycleData({ includeTurnStartConfig: false });
1863
+ await terminateBridge({ command: "stop" });
1864
+ return {
1865
+ type: "resume-session",
1866
+ harnessId,
1867
+ specificationVersion: "harness-v1",
1868
+ data
1869
+ };
1870
+ },
1871
+ doDestroy: async () => {
1872
+ await terminateBridge({ command: "destroy" });
1873
+ }
1874
+ };
1875
+ }
1876
+ function isCompletePermissionModeMapping({
1877
+ value
1878
+ }) {
1879
+ return isPermissionModeMappingValue({ value: value?.["allow-reads"] }) && isPermissionModeMappingValue({ value: value?.["allow-edits"] }) && isPermissionModeMappingValue({ value: value?.["allow-all"] });
1880
+ }
1881
+ function isPermissionModeMappingValue({
1882
+ value
1883
+ }) {
1884
+ return value === null || isPermissionModeTarget({ value });
1885
+ }
1886
+ function isPermissionModeTarget({
1887
+ value
1888
+ }) {
1889
+ if (value?.type === "session-mode") {
1890
+ return typeof value.modeId === "string";
1891
+ }
1892
+ return value?.type === "session-config-option" && typeof value.configId === "string" && (typeof value.value === "string" || typeof value.value === "boolean");
1893
+ }
1894
+ function serializeBuiltinTools({
1895
+ builtinTools
1896
+ }) {
1897
+ return Object.entries(builtinTools).map(([toolName, tool]) => {
1898
+ const nativeName = tool != null && typeof tool === "object" && "nativeName" in tool && typeof tool.nativeName === "string" ? tool.nativeName : void 0;
1899
+ let inputSchema;
1900
+ if (tool != null && typeof tool === "object" && "inputSchema" in tool && tool.inputSchema != null) {
1901
+ try {
1902
+ inputSchema = asSchema(
1903
+ tool.inputSchema
1904
+ ).jsonSchema;
1905
+ } catch {
1906
+ }
1907
+ }
1908
+ return {
1909
+ toolName,
1910
+ ...nativeName == null ? {} : { nativeName },
1911
+ ...inputSchema == null ? {} : { inputSchema }
1912
+ };
1913
+ });
1914
+ }
1915
+ function validateACPTurnStartConfig({
1916
+ turnStartConfig,
1917
+ authenticationProfile,
1918
+ sessionMeta,
1919
+ builtinTools,
1920
+ permissionModeMapping
1921
+ }) {
1922
+ const current = createACPTurnStartConfig({
1923
+ prompt: turnStartConfig.prompt,
1924
+ tools: turnStartConfig.tools,
1925
+ builtinTools,
1926
+ permissionMode: turnStartConfig.permissionMode,
1927
+ permissionModeMapping,
1928
+ debug: turnStartConfig.debug,
1929
+ authenticationProfile,
1930
+ sessionMeta
1931
+ });
1932
+ if (current.configurationFingerprint !== turnStartConfig.configurationFingerprint) {
1933
+ throw new Error(
1934
+ "The persisted ACP turn start configuration is incompatible with the current non-secret start configuration."
1935
+ );
1936
+ }
1937
+ }
1938
+ function validateACPColdSessionConfiguration({
1939
+ coldSession,
1940
+ modelId,
1941
+ permissionMode,
1942
+ authenticationProfile,
1943
+ sessionMeta,
1944
+ builtinTools,
1945
+ permissionModeMapping,
1946
+ debug
1947
+ }) {
1948
+ const current = createACPTurnStartConfig({
1949
+ prompt: [],
1950
+ tools: coldSession.tools,
1951
+ builtinTools,
1952
+ permissionMode,
1953
+ permissionModeMapping,
1954
+ debug,
1955
+ authenticationProfile,
1956
+ sessionMeta
1957
+ });
1958
+ if (current.configurationFingerprint !== coldSession.configurationFingerprint || coldSession.permissionMode !== permissionMode || coldSession.modelId !== modelId) {
1959
+ throw new Error(
1960
+ "ACP cold-session state is incompatible with the current non-secret session configuration."
1961
+ );
1962
+ }
1963
+ return current;
1964
+ }
1965
+ function assertRecoveryToolCatalog({
1966
+ persisted,
1967
+ current
1968
+ }) {
1969
+ if (fingerprintValue({ value: persisted }) !== fingerprintValue({ value: current })) {
1970
+ throw new Error(
1971
+ "ACP lossy rerun requires the same active host tool catalog as the original turn."
1972
+ );
1973
+ }
1974
+ }
1975
+ function fingerprintValue({ value }) {
1976
+ return createHash5("sha256").update(JSON.stringify(value)).digest("hex");
1977
+ }
1978
+ function isRecord(value) {
1979
+ return value != null && typeof value === "object" && !Array.isArray(value);
1980
+ }
1981
+ function unsupported({
1982
+ harnessId,
1983
+ message,
1984
+ cause
1985
+ }) {
1986
+ return new HarnessCapabilityUnsupportedError2({
1987
+ harnessId,
1988
+ message,
1989
+ cause
1990
+ });
1991
+ }
1992
+ function deserializeBridgeError({
1993
+ error,
1994
+ harnessId
1995
+ }) {
1996
+ if (HarnessBridgeCapabilityUnsupportedError.isInstance(error)) {
1997
+ return unsupported({ harnessId, message: error.message });
1998
+ }
1999
+ return error;
2000
+ }
2001
+
2002
+ // src/version.ts
2003
+ var VERSION = true ? "1.0.0" : "0.0.0-test";
2004
+
2005
+ // src/acp-harness.ts
2006
+ var ACP_CLIENT_APP = {
2007
+ name: "ai-sdk/harness-acp",
2008
+ version: VERSION
2009
+ };
2010
+ var ACP_BUILTIN_TOOLS = {};
2011
+ var acpBridgeCoordsSchema = z4.object({
2012
+ port: z4.number(),
2013
+ token: z4.string(),
2014
+ lastSeenEventId: z4.number(),
2015
+ sandboxId: z4.string().optional(),
2016
+ stateDir: z4.string().optional()
2017
+ });
2018
+ var acpResumeStateSchema = z4.object({
2019
+ implementationIdentity: z4.string(),
2020
+ authenticationProfile: z4.object({
2021
+ digest: z4.string(),
2022
+ acpMethodId: z4.string().optional(),
2023
+ providerKind: z4.enum(["implementation-default", "direct", "ai-gateway"]).transform(
2024
+ (value) => value === "implementation-default" ? "direct" : value
2025
+ ),
2026
+ providerMode: z4.enum(["auto", "direct", "ai-gateway"]).optional(),
2027
+ gatewayCredentialSource: z4.enum(["AI_GATEWAY_API_KEY", "VERCEL_OIDC_TOKEN"]).nullable().optional()
2028
+ }).optional(),
2029
+ acpSessionId: z4.string().optional(),
2030
+ bridge: acpBridgeCoordsSchema.optional(),
2031
+ coldSession: acpColdSessionStateSchema.optional(),
2032
+ turnStartConfig: acpTurnStartConfigSchema.optional(),
2033
+ recovery: z4.object({
2034
+ mode: z4.enum(["disk-replay", "lossy-rerun"]),
2035
+ reason: z4.string()
2036
+ }).optional(),
2037
+ restoration: z4.object({
2038
+ method: z4.enum(["resume", "load"])
2039
+ }).optional(),
2040
+ initialGuidanceApplied: z4.boolean().optional(),
2041
+ skillsMaterialized: z4.boolean().optional(),
2042
+ skillsFingerprint: z4.string().optional()
2043
+ });
2044
+ function createACP(settings) {
2045
+ const version = settings.version ?? "v1";
2046
+ switch (version) {
2047
+ case "v1": {
2048
+ const clientApp = settings.clientApp ?? ACP_CLIENT_APP;
2049
+ const providerAuthenticationCompatibility = resolveACPProviderAuthenticationCompatibility({
2050
+ auth: settings.auth,
2051
+ providerAuthentication: settings.providerAuthentication,
2052
+ env: process.env
2053
+ });
2054
+ const implementationIdentity = createImplementationIdentity({
2055
+ harnessId: settings.harnessId,
2056
+ acpVersion: version,
2057
+ implementation: settings.implementation,
2058
+ clientApp,
2059
+ providerAuthentication: providerAuthenticationCompatibility,
2060
+ permissionModeMapping: settings.permissionModeMapping
2061
+ });
2062
+ const authenticationProfile = createACPAuthenticationProfileIdentity({
2063
+ authentication: settings.authentication,
2064
+ providerAuthenticationCompatibility
2065
+ });
2066
+ const lifecycleStateSchema = acpResumeStateSchema.extend({
2067
+ implementationIdentity: z4.literal(implementationIdentity),
2068
+ authenticationProfile: acpResumeStateSchema.shape.authenticationProfile.unwrap().extend({
2069
+ digest: z4.literal(authenticationProfile.digest)
2070
+ }).optional()
2071
+ });
2072
+ return createACPV1({
2073
+ settings,
2074
+ builtinTools: settings.builtinTools ?? ACP_BUILTIN_TOOLS,
2075
+ port: settings.port,
2076
+ startupTimeoutMs: settings.startupTimeoutMs,
2077
+ clientApp,
2078
+ implementationIdentity,
2079
+ authenticationProfile,
2080
+ providerAuthenticationCompatibility,
2081
+ lifecycleStateSchema
2082
+ });
2083
+ }
2084
+ default:
2085
+ throw new Error(
2086
+ `Unsupported ACP protocol version ${JSON.stringify(version)}. Supported versions: "v1".`
2087
+ );
2088
+ }
2089
+ }
2090
+ export {
2091
+ VERSION,
2092
+ createACP
2093
+ };
2094
+ //# sourceMappingURL=index.js.map