@ai-sdk/harness-acp 1.0.8 → 1.0.10
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 +18 -0
- package/README.md +17 -4
- package/dist/bridge/host-tool-mcp.mjs +1 -1
- package/dist/bridge/host-tool-mcp.mjs.map +1 -1
- package/dist/bridge/index.mjs +8 -13
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.d.ts +9 -2
- package/dist/index.js +349 -105
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/acp-harness.ts +10 -0
- package/src/index.ts +1 -0
- package/src/v1/acp-bootstrap.ts +119 -0
- package/src/v1/acp-v1-harness.ts +163 -118
- package/src/v1/acp-v1-settings.ts +12 -1
- package/src/v1/bridge/acp-v1-bridge-environment.ts +4 -0
- package/src/v1/bridge/index.ts +10 -8
- package/src/v1/bridge/stream-translator.ts +4 -14
- package/src/v1/implementation.ts +25 -1
- package/src/v1/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/harness-acp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"ws": "^8.21.0",
|
|
30
|
-
"@ai-sdk/harness": "1.0.
|
|
30
|
+
"@ai-sdk/harness": "1.0.72",
|
|
31
31
|
"@ai-sdk/provider-utils": "5.0.27"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
package/src/acp-harness.ts
CHANGED
|
@@ -33,6 +33,8 @@ export type ACPHarnessSettings<TBuiltinTools extends ToolSet = {}> = {
|
|
|
33
33
|
readonly executable: ACPV1Settings['executable'];
|
|
34
34
|
readonly args?: ACPV1Settings['args'];
|
|
35
35
|
readonly forwardEnv?: ACPV1Settings['forwardEnv'];
|
|
36
|
+
readonly credentialEnv?: ACPV1Settings['credentialEnv'];
|
|
37
|
+
readonly credentialBrokering?: ACPV1Settings['credentialBrokering'];
|
|
36
38
|
readonly env?: ACPV1Settings['env'];
|
|
37
39
|
readonly authentication?: ACPV1Settings['authentication'];
|
|
38
40
|
readonly providerAuthentication?: ACPV1Settings['providerAuthentication'];
|
|
@@ -94,6 +96,14 @@ const acpResumeStateSchema = z.object({
|
|
|
94
96
|
export function createACP<TBuiltinTools extends ToolSet = {}>(
|
|
95
97
|
settings: ACPHarnessSettings<TBuiltinTools>,
|
|
96
98
|
): HarnessV1<TBuiltinTools> {
|
|
99
|
+
if (
|
|
100
|
+
(settings.credentialEnv == null) !==
|
|
101
|
+
(settings.credentialBrokering == null)
|
|
102
|
+
) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
'ACP credentialEnv and credentialBrokering must be configured together.',
|
|
105
|
+
);
|
|
106
|
+
}
|
|
97
107
|
if (
|
|
98
108
|
settings.mcpServers != null &&
|
|
99
109
|
Object.prototype.hasOwnProperty.call(
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import type { HarnessV1Bootstrap } from '@ai-sdk/harness';
|
|
4
|
+
import {
|
|
5
|
+
createImplementationDescriptor,
|
|
6
|
+
createImplementationInstallCommand,
|
|
7
|
+
createImplementationManifest,
|
|
8
|
+
getImplementationLockfile,
|
|
9
|
+
type ACPNpmImplementation,
|
|
10
|
+
} from './implementation';
|
|
11
|
+
|
|
12
|
+
export function createACPBootstrap({
|
|
13
|
+
harnessId,
|
|
14
|
+
implementation,
|
|
15
|
+
}: {
|
|
16
|
+
readonly harnessId: string;
|
|
17
|
+
readonly implementation: ACPNpmImplementation;
|
|
18
|
+
}): {
|
|
19
|
+
readonly bootstrapDir: string;
|
|
20
|
+
readonly getBootstrap: () => Promise<HarnessV1Bootstrap>;
|
|
21
|
+
} {
|
|
22
|
+
const bootstrapDir = `.harness-bootstrap/${harnessId}`;
|
|
23
|
+
let cachedBootstrap: HarnessV1Bootstrap | undefined;
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
bootstrapDir,
|
|
27
|
+
getBootstrap: async () => {
|
|
28
|
+
if (cachedBootstrap != null) return cachedBootstrap;
|
|
29
|
+
const [bridgePackage, bridgeLock, bridge, hostToolMCP] =
|
|
30
|
+
await Promise.all([
|
|
31
|
+
readBridgeAsset({ name: 'package.json' }),
|
|
32
|
+
readBridgeAsset({ name: 'pnpm-lock.yaml' }),
|
|
33
|
+
readBridgeAsset({ name: 'index.mjs' }),
|
|
34
|
+
readBridgeAsset({ name: 'host-tool-mcp.mjs' }),
|
|
35
|
+
]);
|
|
36
|
+
const implementationLock = getImplementationLockfile({
|
|
37
|
+
implementation,
|
|
38
|
+
});
|
|
39
|
+
cachedBootstrap = {
|
|
40
|
+
harnessId,
|
|
41
|
+
bootstrapDir,
|
|
42
|
+
files: [
|
|
43
|
+
{
|
|
44
|
+
path: `${bootstrapDir}/package.json`,
|
|
45
|
+
content: bridgePackage,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
path: `${bootstrapDir}/pnpm-lock.yaml`,
|
|
49
|
+
content: bridgeLock,
|
|
50
|
+
},
|
|
51
|
+
{ path: `${bootstrapDir}/bridge.mjs`, content: bridge },
|
|
52
|
+
{
|
|
53
|
+
path: `${bootstrapDir}/host-tool-mcp.mjs`,
|
|
54
|
+
content: hostToolMCP,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
path: `${bootstrapDir}/implementation/package.json`,
|
|
58
|
+
content: createImplementationManifest({ implementation }),
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
path: `${bootstrapDir}/implementation/implementation.json`,
|
|
62
|
+
content: createImplementationDescriptor({ implementation }),
|
|
63
|
+
},
|
|
64
|
+
...(implementationLock == null
|
|
65
|
+
? []
|
|
66
|
+
: [
|
|
67
|
+
{
|
|
68
|
+
path: `${bootstrapDir}/implementation/pnpm-lock.yaml`,
|
|
69
|
+
content: implementationLock,
|
|
70
|
+
},
|
|
71
|
+
]),
|
|
72
|
+
],
|
|
73
|
+
commands: [
|
|
74
|
+
{
|
|
75
|
+
command: 'pnpm install --frozen-lockfile --store-dir .pnpm-store',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
command: createImplementationInstallCommand({
|
|
79
|
+
implementationDir: 'implementation',
|
|
80
|
+
storeDir: '../.pnpm-store',
|
|
81
|
+
implementation,
|
|
82
|
+
}),
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
return cachedBootstrap;
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function readBridgeAsset({ name }: { name: string }): Promise<string> {
|
|
92
|
+
const candidates = resolveBridgeAssetCandidates({
|
|
93
|
+
name,
|
|
94
|
+
moduleUrl: import.meta.url,
|
|
95
|
+
});
|
|
96
|
+
let lastError: unknown;
|
|
97
|
+
for (const url of candidates) {
|
|
98
|
+
try {
|
|
99
|
+
return await readFile(fileURLToPath(url), 'utf8');
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
102
|
+
lastError = error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
throw lastError ?? new Error(`ACP bridge asset not found: ${name}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function resolveBridgeAssetCandidates({
|
|
109
|
+
name,
|
|
110
|
+
moduleUrl,
|
|
111
|
+
}: {
|
|
112
|
+
name: string;
|
|
113
|
+
moduleUrl: string | URL;
|
|
114
|
+
}): URL[] {
|
|
115
|
+
return [
|
|
116
|
+
new URL(`./bridge/${name}`, moduleUrl),
|
|
117
|
+
new URL(`../bridge/${name}`, moduleUrl),
|
|
118
|
+
];
|
|
119
|
+
}
|
package/src/v1/acp-v1-harness.ts
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
-
import { readFile } from 'node:fs/promises';
|
|
3
2
|
import { posix } from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
3
|
import {
|
|
6
4
|
HarnessCapabilityUnsupportedError,
|
|
7
5
|
harnessV1DiagnosticFromBridgeFrame,
|
|
8
6
|
type HarnessV1,
|
|
9
|
-
type HarnessV1Bootstrap,
|
|
10
7
|
type HarnessV1DebugConfig,
|
|
11
8
|
type HarnessV1NetworkSandboxSession,
|
|
9
|
+
type HarnessV1PortEndpoint,
|
|
12
10
|
type HarnessV1PromptControl,
|
|
13
11
|
type HarnessV1Session,
|
|
14
12
|
type HarnessV1StreamPart,
|
|
@@ -22,9 +20,11 @@ import {
|
|
|
22
20
|
drainBridgeProcessStream,
|
|
23
21
|
forwardBridgeProcessStream,
|
|
24
22
|
markBridgeStarting,
|
|
23
|
+
maskSandboxCredentials,
|
|
25
24
|
resolveSandboxHomeDir,
|
|
26
25
|
SandboxChannel,
|
|
27
26
|
shellQuote,
|
|
27
|
+
warnCredentialBrokeringUnavailable,
|
|
28
28
|
waitForBridgeReady,
|
|
29
29
|
} from '@ai-sdk/harness/utils';
|
|
30
30
|
import {
|
|
@@ -43,14 +43,11 @@ import {
|
|
|
43
43
|
import type { ACPToolCall } from '../acp-tool-call';
|
|
44
44
|
import {
|
|
45
45
|
createACPV1Implementation,
|
|
46
|
-
createImplementationDescriptor,
|
|
47
46
|
createImplementationIdentity,
|
|
48
|
-
createImplementationInstallCommand,
|
|
49
|
-
createImplementationManifest,
|
|
50
|
-
getImplementationLockfile,
|
|
51
47
|
resolveImplementationEnvironment,
|
|
52
48
|
validateACPV1Implementation,
|
|
53
49
|
} from './implementation';
|
|
50
|
+
import { createACPBootstrap } from './acp-bootstrap';
|
|
54
51
|
import {
|
|
55
52
|
outboundMessageSchema,
|
|
56
53
|
type ACPBuiltinToolMapping,
|
|
@@ -61,6 +58,7 @@ import {
|
|
|
61
58
|
type StartMessage,
|
|
62
59
|
} from './acp-v1-bridge-protocol';
|
|
63
60
|
import { createACPBridgeEnvironment } from './bridge/acp-v1-bridge-environment';
|
|
61
|
+
import { resolveACPLaunchEnvironment } from './bridge/protocol-configuration';
|
|
64
62
|
import {
|
|
65
63
|
createACPColdSessionState,
|
|
66
64
|
createACPTurnStartConfig,
|
|
@@ -128,6 +126,14 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
|
|
|
128
126
|
HarnessV1<TBuiltinTools>['lifecycleStateSchema']
|
|
129
127
|
>;
|
|
130
128
|
}): HarnessV1<TBuiltinTools> {
|
|
129
|
+
if (
|
|
130
|
+
(settings.credentialEnv == null) !==
|
|
131
|
+
(settings.credentialBrokering == null)
|
|
132
|
+
) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
'ACP credentialEnv and credentialBrokering must be configured together.',
|
|
135
|
+
);
|
|
136
|
+
}
|
|
131
137
|
if (
|
|
132
138
|
settings.mcpServers != null &&
|
|
133
139
|
Object.prototype.hasOwnProperty.call(
|
|
@@ -146,10 +152,10 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
|
|
|
146
152
|
}
|
|
147
153
|
const implementation = createACPV1Implementation({ settings });
|
|
148
154
|
validateACPV1Implementation(implementation);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
155
|
+
const bootstrap = createACPBootstrap({
|
|
156
|
+
harnessId: settings.harnessId,
|
|
157
|
+
implementation,
|
|
158
|
+
});
|
|
153
159
|
const permissionModeMapping = isCompletePermissionModeMapping({
|
|
154
160
|
value: settings.permissionModeMapping,
|
|
155
161
|
})
|
|
@@ -163,71 +169,7 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
|
|
|
163
169
|
supportsBuiltinToolApprovals: true,
|
|
164
170
|
supportsBuiltinToolFiltering: false,
|
|
165
171
|
lifecycleStateSchema,
|
|
166
|
-
getBootstrap:
|
|
167
|
-
if (cachedBootstrap != null) return cachedBootstrap;
|
|
168
|
-
const [bridgePackage, bridgeLock, bridge, hostToolMCP] =
|
|
169
|
-
await Promise.all([
|
|
170
|
-
readBridgeAsset({ name: 'package.json' }),
|
|
171
|
-
readBridgeAsset({ name: 'pnpm-lock.yaml' }),
|
|
172
|
-
readBridgeAsset({ name: 'index.mjs' }),
|
|
173
|
-
readBridgeAsset({ name: 'host-tool-mcp.mjs' }),
|
|
174
|
-
]);
|
|
175
|
-
const implementationLock = getImplementationLockfile({
|
|
176
|
-
implementation,
|
|
177
|
-
});
|
|
178
|
-
cachedBootstrap = {
|
|
179
|
-
harnessId: settings.harnessId,
|
|
180
|
-
bootstrapDir: BOOTSTRAP_DIR,
|
|
181
|
-
files: [
|
|
182
|
-
{
|
|
183
|
-
path: `${BOOTSTRAP_DIR}/package.json`,
|
|
184
|
-
content: bridgePackage,
|
|
185
|
-
},
|
|
186
|
-
{
|
|
187
|
-
path: `${BOOTSTRAP_DIR}/pnpm-lock.yaml`,
|
|
188
|
-
content: bridgeLock,
|
|
189
|
-
},
|
|
190
|
-
{ path: `${BOOTSTRAP_DIR}/bridge.mjs`, content: bridge },
|
|
191
|
-
{
|
|
192
|
-
path: `${BOOTSTRAP_DIR}/host-tool-mcp.mjs`,
|
|
193
|
-
content: hostToolMCP,
|
|
194
|
-
},
|
|
195
|
-
{
|
|
196
|
-
path: `${BOOTSTRAP_DIR}/implementation/package.json`,
|
|
197
|
-
content: createImplementationManifest({
|
|
198
|
-
implementation,
|
|
199
|
-
}),
|
|
200
|
-
},
|
|
201
|
-
{
|
|
202
|
-
path: `${BOOTSTRAP_DIR}/implementation/implementation.json`,
|
|
203
|
-
content: createImplementationDescriptor({
|
|
204
|
-
implementation,
|
|
205
|
-
}),
|
|
206
|
-
},
|
|
207
|
-
...(implementationLock == null
|
|
208
|
-
? []
|
|
209
|
-
: [
|
|
210
|
-
{
|
|
211
|
-
path: `${BOOTSTRAP_DIR}/implementation/pnpm-lock.yaml`,
|
|
212
|
-
content: implementationLock,
|
|
213
|
-
},
|
|
214
|
-
]),
|
|
215
|
-
],
|
|
216
|
-
commands: [
|
|
217
|
-
{
|
|
218
|
-
command: 'pnpm install --frozen-lockfile --store-dir .pnpm-store',
|
|
219
|
-
},
|
|
220
|
-
{
|
|
221
|
-
command: createImplementationInstallCommand({
|
|
222
|
-
implementationDir: 'implementation',
|
|
223
|
-
storeDir: '../.pnpm-store',
|
|
224
|
-
implementation,
|
|
225
|
-
}),
|
|
226
|
-
},
|
|
227
|
-
],
|
|
228
|
-
};
|
|
229
|
-
return cachedBootstrap;
|
|
230
|
-
},
|
|
172
|
+
getBootstrap: bootstrap.getBootstrap,
|
|
231
173
|
doStart: async startOptions => {
|
|
232
174
|
if (startOptions.builtinToolFiltering != null) {
|
|
233
175
|
throw unsupported({
|
|
@@ -267,10 +209,75 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
|
|
|
267
209
|
compatibility: providerAuthenticationCompatibility,
|
|
268
210
|
});
|
|
269
211
|
const sandboxSession = startOptions.sandboxSession;
|
|
212
|
+
const implementationEnvironment = resolveImplementationEnvironment({
|
|
213
|
+
implementation,
|
|
214
|
+
env,
|
|
215
|
+
});
|
|
216
|
+
let sandboxImplementationEnvironment = implementationEnvironment;
|
|
217
|
+
let sandboxProviderAuthenticationEnvironment =
|
|
218
|
+
resolvedProviderAuthentication.env;
|
|
219
|
+
let sandboxProviderEnvironment: Record<string, string> | undefined;
|
|
220
|
+
|
|
221
|
+
if (
|
|
222
|
+
settings.credentialBrokering != null &&
|
|
223
|
+
sandboxSession.addRequestTransformations != null
|
|
224
|
+
) {
|
|
225
|
+
const providerEnvironment = resolveProviderEnvironment({
|
|
226
|
+
resolvedProviderAuthentication,
|
|
227
|
+
clientApp,
|
|
228
|
+
});
|
|
229
|
+
const credentialEnvironmentVariables = [
|
|
230
|
+
...new Set([
|
|
231
|
+
...(settings.credentialEnv ?? []),
|
|
232
|
+
'AI_GATEWAY_API_KEY',
|
|
233
|
+
'VERCEL_OIDC_TOKEN',
|
|
234
|
+
]),
|
|
235
|
+
];
|
|
236
|
+
const requestTransformations = settings.credentialBrokering({
|
|
237
|
+
env: {
|
|
238
|
+
...implementationEnvironment,
|
|
239
|
+
...providerEnvironment,
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
if (requestTransformations.length > 0) {
|
|
243
|
+
await sandboxSession.addRequestTransformations(
|
|
244
|
+
requestTransformations,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
sandboxImplementationEnvironment = maskSandboxCredentials({
|
|
248
|
+
environment: implementationEnvironment,
|
|
249
|
+
credentialEnvironmentVariables,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
/*
|
|
253
|
+
* Gateway profiles are resolved on the host twice: real values feed
|
|
254
|
+
* the transformation callback, while the bridge receives only a
|
|
255
|
+
* structurally equivalent environment with credential placeholders.
|
|
256
|
+
* Resolving the profile inside the sandbox would require serializing
|
|
257
|
+
* the Gateway credential into the bridge process environment.
|
|
258
|
+
*/
|
|
259
|
+
sandboxProviderEnvironment = maskSandboxCredentials({
|
|
260
|
+
environment: resolveProviderEnvironment({
|
|
261
|
+
resolvedProviderAuthentication,
|
|
262
|
+
clientApp,
|
|
263
|
+
gatewayApiKey: 'AI_GATEWAY_API_KEY',
|
|
264
|
+
}),
|
|
265
|
+
credentialEnvironmentVariables,
|
|
266
|
+
});
|
|
267
|
+
sandboxProviderAuthenticationEnvironment = Object.fromEntries(
|
|
268
|
+
Object.entries(resolvedProviderAuthentication.env).filter(
|
|
269
|
+
([key]) =>
|
|
270
|
+
key !== 'AI_SDK_ACP_GATEWAY_API_KEY' &&
|
|
271
|
+
key !== 'AI_SDK_ACP_GATEWAY_BASE_URL',
|
|
272
|
+
),
|
|
273
|
+
);
|
|
274
|
+
} else if (settings.credentialBrokering != null) {
|
|
275
|
+
warnCredentialBrokeringUnavailable();
|
|
276
|
+
}
|
|
270
277
|
const sandbox = sandboxSession.restricted();
|
|
271
278
|
const resolvedBridgeDir = posix.resolve(
|
|
272
279
|
sandboxSession.defaultWorkingDirectory,
|
|
273
|
-
|
|
280
|
+
bootstrap.bootstrapDir,
|
|
274
281
|
);
|
|
275
282
|
const resolvedImplementationDir = `${resolvedBridgeDir}/implementation`;
|
|
276
283
|
const workDir = startOptions.sessionWorkDir;
|
|
@@ -357,13 +364,16 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
|
|
|
357
364
|
}
|
|
358
365
|
if (coords != null) {
|
|
359
366
|
try {
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
367
|
+
const endpoint = await sandboxSession.getPortEndpoint({
|
|
368
|
+
port: coords.port,
|
|
369
|
+
protocol: 'ws',
|
|
370
|
+
});
|
|
371
|
+
const attachEndpoint = withBridgeToken({
|
|
372
|
+
endpoint,
|
|
373
|
+
token: coords.token,
|
|
374
|
+
});
|
|
365
375
|
const attachChannel: ACPChannel = new SandboxChannel({
|
|
366
|
-
connect: () => openWebSocket(
|
|
376
|
+
connect: () => openWebSocket(attachEndpoint),
|
|
367
377
|
outboundSchema: outboundMessageSchema,
|
|
368
378
|
initialLastSeenEventId: coords.lastSeenEventId,
|
|
369
379
|
onDiagnostic,
|
|
@@ -508,17 +518,15 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
|
|
|
508
518
|
` --implementation-dir ${shellQuote(resolvedImplementationDir)}` +
|
|
509
519
|
` --bridge-type ${shellQuote(settings.harnessId)}`,
|
|
510
520
|
env: {
|
|
511
|
-
...
|
|
512
|
-
implementation,
|
|
513
|
-
env,
|
|
514
|
-
}),
|
|
521
|
+
...sandboxImplementationEnvironment,
|
|
515
522
|
...createACPBridgeEnvironment({
|
|
516
523
|
authentication: settings.authentication,
|
|
517
524
|
providerAuthentication:
|
|
518
525
|
resolvedProviderAuthentication.providerAuthentication,
|
|
526
|
+
providerEnvironment: sandboxProviderEnvironment,
|
|
519
527
|
sessionMeta: settings.session?.meta,
|
|
520
528
|
}),
|
|
521
|
-
...
|
|
529
|
+
...sandboxProviderAuthenticationEnvironment,
|
|
522
530
|
BRIDGE_CHANNEL_TOKEN: token,
|
|
523
531
|
BRIDGE_WS_PORT: String(port),
|
|
524
532
|
...(respawnStrategy?.mode === 'disk-replay'
|
|
@@ -563,13 +571,13 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
|
|
|
563
571
|
});
|
|
564
572
|
void drainBridgeProcessStream(proc.stdout);
|
|
565
573
|
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
574
|
+
const endpoint = await sandboxSession.getPortEndpoint({
|
|
575
|
+
port: boundPort,
|
|
576
|
+
protocol: 'ws',
|
|
577
|
+
});
|
|
578
|
+
const bridgeEndpoint = withBridgeToken({ endpoint, token });
|
|
571
579
|
const channel: ACPChannel = new SandboxChannel({
|
|
572
|
-
connect: () => openWebSocket(
|
|
580
|
+
connect: () => openWebSocket(bridgeEndpoint),
|
|
573
581
|
outboundSchema: outboundMessageSchema,
|
|
574
582
|
...(respawnStrategy?.mode === 'disk-replay'
|
|
575
583
|
? { initialLastSeenEventId: respawnStrategy.afterSeq }
|
|
@@ -677,34 +685,54 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
|
|
|
677
685
|
};
|
|
678
686
|
}
|
|
679
687
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
688
|
+
function resolveProviderEnvironment({
|
|
689
|
+
resolvedProviderAuthentication,
|
|
690
|
+
clientApp,
|
|
691
|
+
gatewayApiKey,
|
|
692
|
+
}: {
|
|
693
|
+
resolvedProviderAuthentication: ReturnType<
|
|
694
|
+
typeof resolveACPProviderAuthentication
|
|
695
|
+
>;
|
|
696
|
+
clientApp: ACPClientApp;
|
|
697
|
+
gatewayApiKey?: string;
|
|
698
|
+
}): Record<string, string> {
|
|
699
|
+
if (
|
|
700
|
+
resolvedProviderAuthentication.providerAuthentication?.type !== 'ai-gateway'
|
|
701
|
+
) {
|
|
702
|
+
return {};
|
|
693
703
|
}
|
|
694
|
-
|
|
704
|
+
return resolveACPLaunchEnvironment({
|
|
705
|
+
providerAuthentication:
|
|
706
|
+
resolvedProviderAuthentication.providerAuthentication,
|
|
707
|
+
gateway: {
|
|
708
|
+
apiKey:
|
|
709
|
+
gatewayApiKey ??
|
|
710
|
+
requireResolvedEnvironmentValue({
|
|
711
|
+
environment: resolvedProviderAuthentication.env,
|
|
712
|
+
name: 'AI_SDK_ACP_GATEWAY_API_KEY',
|
|
713
|
+
}),
|
|
714
|
+
baseUrl: requireResolvedEnvironmentValue({
|
|
715
|
+
environment: resolvedProviderAuthentication.env,
|
|
716
|
+
name: 'AI_SDK_ACP_GATEWAY_BASE_URL',
|
|
717
|
+
}),
|
|
718
|
+
clientAppName: clientApp.name,
|
|
719
|
+
clientAppVersion: clientApp.version,
|
|
720
|
+
},
|
|
721
|
+
});
|
|
695
722
|
}
|
|
696
723
|
|
|
697
|
-
|
|
724
|
+
function requireResolvedEnvironmentValue({
|
|
725
|
+
environment,
|
|
698
726
|
name,
|
|
699
|
-
moduleUrl,
|
|
700
727
|
}: {
|
|
728
|
+
environment: Readonly<Record<string, string>>;
|
|
701
729
|
name: string;
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
new
|
|
706
|
-
|
|
707
|
-
|
|
730
|
+
}): string {
|
|
731
|
+
const value = environment[name];
|
|
732
|
+
if (value == null) {
|
|
733
|
+
throw new Error(`ACP resolved environment value ${name} is unavailable.`);
|
|
734
|
+
}
|
|
735
|
+
return value;
|
|
708
736
|
}
|
|
709
737
|
|
|
710
738
|
function resolveBridgePort({
|
|
@@ -726,9 +754,14 @@ function resolveBridgePort({
|
|
|
726
754
|
});
|
|
727
755
|
}
|
|
728
756
|
|
|
729
|
-
function openWebSocket({
|
|
757
|
+
function openWebSocket({
|
|
758
|
+
url,
|
|
759
|
+
headers,
|
|
760
|
+
}: HarnessV1PortEndpoint): Promise<WebSocket> {
|
|
730
761
|
return new Promise((resolve, reject) => {
|
|
731
|
-
const ws = new WebSocket(url
|
|
762
|
+
const ws = new WebSocket(url, {
|
|
763
|
+
headers: headers == null ? undefined : { ...headers },
|
|
764
|
+
});
|
|
732
765
|
const onOpen = () => {
|
|
733
766
|
ws.off('error', onError);
|
|
734
767
|
resolve(ws);
|
|
@@ -742,6 +775,18 @@ function openWebSocket({ url }: { url: string }): Promise<WebSocket> {
|
|
|
742
775
|
});
|
|
743
776
|
}
|
|
744
777
|
|
|
778
|
+
function withBridgeToken({
|
|
779
|
+
endpoint,
|
|
780
|
+
token,
|
|
781
|
+
}: {
|
|
782
|
+
endpoint: HarnessV1PortEndpoint;
|
|
783
|
+
token: string;
|
|
784
|
+
}): HarnessV1PortEndpoint {
|
|
785
|
+
const bridgeUrl = new URL(endpoint.url);
|
|
786
|
+
bridgeUrl.searchParams.set('agent_bridge_token', token);
|
|
787
|
+
return { ...endpoint, url: bridgeUrl.toString() };
|
|
788
|
+
}
|
|
789
|
+
|
|
745
790
|
function restoreColdACPSession({
|
|
746
791
|
channel,
|
|
747
792
|
harnessId,
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
HarnessV1PermissionMode,
|
|
3
|
+
HarnessV1RequestTransformation,
|
|
4
|
+
} from '@ai-sdk/harness';
|
|
2
5
|
import type { ACPToolCall } from '../acp-tool-call';
|
|
3
6
|
|
|
4
7
|
export type ACPSerializablePrimitive = string | number | boolean | null;
|
|
@@ -60,6 +63,12 @@ export type ACPProviderAuthentication = {
|
|
|
60
63
|
};
|
|
61
64
|
};
|
|
62
65
|
|
|
66
|
+
export type ACPCredentialBrokering = ({
|
|
67
|
+
env,
|
|
68
|
+
}: {
|
|
69
|
+
env: Readonly<Record<string, string>>;
|
|
70
|
+
}) => ReadonlyArray<HarnessV1RequestTransformation>;
|
|
71
|
+
|
|
63
72
|
export type ACPPermissionModeTarget =
|
|
64
73
|
| {
|
|
65
74
|
readonly type: 'session-mode';
|
|
@@ -101,6 +110,8 @@ export type ACPV1Settings = {
|
|
|
101
110
|
readonly executable: string;
|
|
102
111
|
readonly args?: ReadonlyArray<string>;
|
|
103
112
|
readonly forwardEnv?: ReadonlyArray<string>;
|
|
113
|
+
readonly credentialEnv?: ReadonlyArray<string>;
|
|
114
|
+
readonly credentialBrokering?: ACPCredentialBrokering;
|
|
104
115
|
/**
|
|
105
116
|
* Runtime environment values that are safe to persist in bootstrap and
|
|
106
117
|
* lifecycle compatibility identity.
|
|
@@ -57,24 +57,28 @@ export type ACPResolvedProviderAuthentication = z.infer<
|
|
|
57
57
|
export type ACPBridgeConfiguration = {
|
|
58
58
|
readonly authentication?: ACPAuthentication;
|
|
59
59
|
readonly providerAuthentication?: ACPResolvedProviderAuthentication;
|
|
60
|
+
readonly providerEnvironment?: Readonly<Record<string, string>>;
|
|
60
61
|
readonly sessionMeta?: Readonly<Record<string, ACPSerializableValue>>;
|
|
61
62
|
};
|
|
62
63
|
|
|
63
64
|
const bridgeConfigurationSchema: z.ZodType<ACPBridgeConfiguration> = z.object({
|
|
64
65
|
authentication: authenticationSchema.optional(),
|
|
65
66
|
providerAuthentication: providerAuthenticationSchema.optional(),
|
|
67
|
+
providerEnvironment: z.record(z.string(), z.string()).optional(),
|
|
66
68
|
sessionMeta: serializableRecordSchema.optional(),
|
|
67
69
|
});
|
|
68
70
|
|
|
69
71
|
export function createACPBridgeEnvironment({
|
|
70
72
|
authentication,
|
|
71
73
|
providerAuthentication,
|
|
74
|
+
providerEnvironment,
|
|
72
75
|
sessionMeta,
|
|
73
76
|
}: ACPBridgeConfiguration): Record<string, string> {
|
|
74
77
|
return {
|
|
75
78
|
[ACP_BRIDGE_CONFIGURATION_ENV]: JSON.stringify({
|
|
76
79
|
...(authentication == null ? {} : { authentication }),
|
|
77
80
|
...(providerAuthentication == null ? {} : { providerAuthentication }),
|
|
81
|
+
...(providerEnvironment == null ? {} : { providerEnvironment }),
|
|
78
82
|
...(sessionMeta == null ? {} : { sessionMeta }),
|
|
79
83
|
}),
|
|
80
84
|
};
|
package/src/v1/bridge/index.ts
CHANGED
|
@@ -326,6 +326,7 @@ async function ensureSession({
|
|
|
326
326
|
const fingerprint = JSON.stringify({
|
|
327
327
|
authentication: bridgeConfiguration.authentication,
|
|
328
328
|
providerAuthentication: bridgeConfiguration.providerAuthentication,
|
|
329
|
+
providerEnvironment: bridgeConfiguration.providerEnvironment,
|
|
329
330
|
sessionMeta: bridgeConfiguration.sessionMeta,
|
|
330
331
|
instructionMapping: start.instructionMapping,
|
|
331
332
|
permissionMode: start.permissionMode,
|
|
@@ -360,15 +361,16 @@ async function ensureSession({
|
|
|
360
361
|
}
|
|
361
362
|
|
|
362
363
|
const clientApp = resolveClientApp();
|
|
363
|
-
const gateway =
|
|
364
|
-
bridgeConfiguration.providerAuthentication?.type === 'ai-gateway'
|
|
365
|
-
? resolveGatewayValues({ clientApp })
|
|
366
|
-
: undefined;
|
|
367
364
|
const authentication = bridgeConfiguration.authentication;
|
|
368
|
-
const launchEnv =
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
365
|
+
const launchEnv =
|
|
366
|
+
bridgeConfiguration.providerEnvironment ??
|
|
367
|
+
resolveACPLaunchEnvironment({
|
|
368
|
+
providerAuthentication: bridgeConfiguration.providerAuthentication,
|
|
369
|
+
gateway:
|
|
370
|
+
bridgeConfiguration.providerAuthentication?.type === 'ai-gateway'
|
|
371
|
+
? resolveGatewayValues({ clientApp })
|
|
372
|
+
: undefined,
|
|
373
|
+
});
|
|
372
374
|
const instructionConfiguration = await resolveACPInstructionConfiguration({
|
|
373
375
|
instructions: start.instructions,
|
|
374
376
|
instructionMapping: start.instructionMapping,
|