@ai-sdk/harness-acp 0.0.0 → 1.0.1

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