@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.
- package/CHANGELOG.md +13 -0
- package/LICENSE +13 -0
- package/README.md +70 -3
- package/dist/bridge/host-tool-mcp.mjs +302 -0
- package/dist/bridge/host-tool-mcp.mjs.map +1 -0
- package/dist/bridge/index.mjs +3278 -0
- package/dist/bridge/index.mjs.map +1 -0
- package/dist/bridge/package.json +12 -0
- package/dist/bridge/pnpm-lock.yaml +816 -0
- package/dist/index.d.ts +98 -0
- package/dist/index.js +2094 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -10
- package/src/acp-auth.ts +254 -0
- package/src/acp-bridge-protocol.ts +13 -0
- package/src/acp-harness.ts +160 -0
- package/src/index.ts +19 -0
- package/src/v1/acp-v1-bridge-protocol.ts +151 -0
- package/src/v1/acp-v1-harness.ts +1447 -0
- package/src/v1/acp-v1-lifecycle.ts +115 -0
- package/src/v1/acp-v1-prompt.ts +122 -0
- package/src/v1/acp-v1-settings.ts +98 -0
- package/src/v1/acp-v1-skills.ts +228 -0
- package/src/v1/acp-v1-turn-start-config.ts +93 -0
- package/src/v1/bridge/acp-diagnostics.ts +57 -0
- package/src/v1/bridge/acp-stream-capture.ts +152 -0
- package/src/v1/bridge/acp-v1-bridge-environment.ts +95 -0
- package/src/v1/bridge/create-emit-stream-event.ts +97 -0
- package/src/v1/bridge/host-tool-correlation.ts +640 -0
- package/src/v1/bridge/host-tool-mcp-server.ts +180 -0
- package/src/v1/bridge/host-tool-mcp.ts +185 -0
- package/src/v1/bridge/host-tool-relay-client.ts +53 -0
- package/src/v1/bridge/host-tool-relay.ts +510 -0
- package/src/v1/bridge/index.ts +729 -0
- package/src/v1/bridge/package.json +12 -0
- package/src/v1/bridge/permission-controller.ts +152 -0
- package/src/v1/bridge/permission-mode.ts +163 -0
- package/src/v1/bridge/pnpm-lock.yaml +816 -0
- package/src/v1/bridge/profile-values.ts +97 -0
- package/src/v1/bridge/protocol-configuration.ts +146 -0
- package/src/v1/bridge/recovered-session.ts +145 -0
- package/src/v1/bridge/refresh-host-tool-catalog.ts +33 -0
- package/src/v1/bridge/session-lifecycle.ts +83 -0
- package/src/v1/bridge/stream-translator.ts +901 -0
- package/src/v1/implementation.ts +282 -0
- package/src/v1/index.ts +16 -0
- package/src/version.ts +6 -0
- 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
|