@ai-sdk/harness-acp 1.0.7 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/harness-acp",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
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.70",
30
+ "@ai-sdk/harness": "1.0.72",
31
31
  "@ai-sdk/provider-utils": "5.0.27"
32
32
  },
33
33
  "peerDependencies": {
@@ -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
@@ -20,6 +20,7 @@ export type {
20
20
  ACPToolKind,
21
21
  } from './acp-tool-call';
22
22
  export type {
23
+ ACPCredentialBrokering,
23
24
  ACPAuthentication,
24
25
  ACPInstructionMapping,
25
26
  ACPNpmLockedSource,
@@ -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
+ }
@@ -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
- const BOOTSTRAP_DIR = `.harness-bootstrap/${settings.harnessId}`;
151
-
152
- let cachedBootstrap: HarnessV1Bootstrap | undefined;
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: async () => {
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
- BOOTSTRAP_DIR,
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 attachUrl =
361
- (await sandboxSession.getPortUrl({
362
- port: coords.port,
363
- protocol: 'ws',
364
- })) + `?agent_bridge_token=${encodeURIComponent(coords.token)}`;
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({ url: attachUrl }),
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
- ...resolveImplementationEnvironment({
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
- ...resolvedProviderAuthentication.env,
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 wsUrl =
567
- (await sandboxSession.getPortUrl({
568
- port: boundPort,
569
- protocol: 'ws',
570
- })) + `?agent_bridge_token=${encodeURIComponent(token)}`;
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({ url: wsUrl }),
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
- async function readBridgeAsset({ name }: { name: string }): Promise<string> {
681
- const candidates = resolveBridgeAssetCandidates({
682
- name,
683
- moduleUrl: import.meta.url,
684
- });
685
- let lastError: unknown;
686
- for (const url of candidates) {
687
- try {
688
- return await readFile(fileURLToPath(url), 'utf8');
689
- } catch (error) {
690
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
691
- lastError = error;
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
- throw lastError ?? new Error(`ACP bridge asset not found: ${name}`);
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
- export function resolveBridgeAssetCandidates({
724
+ function requireResolvedEnvironmentValue({
725
+ environment,
698
726
  name,
699
- moduleUrl,
700
727
  }: {
728
+ environment: Readonly<Record<string, string>>;
701
729
  name: string;
702
- moduleUrl: string | URL;
703
- }): URL[] {
704
- return [
705
- new URL(`./bridge/${name}`, moduleUrl),
706
- new URL(`../bridge/${name}`, moduleUrl),
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({ url }: { url: string }): Promise<WebSocket> {
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 { HarnessV1PermissionMode } from '@ai-sdk/harness';
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
  };
@@ -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 = resolveACPLaunchEnvironment({
369
- providerAuthentication: bridgeConfiguration.providerAuthentication,
370
- gateway,
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,