@ai-sdk/harness-acp 1.0.39 → 1.0.41

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.39",
3
+ "version": "1.0.41",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -26,8 +26,8 @@
26
26
  }
27
27
  },
28
28
  "dependencies": {
29
- "@ai-sdk/harness": "1.0.101",
30
- "@ai-sdk/provider-utils": "5.0.36",
29
+ "@ai-sdk/harness": "1.0.103",
30
+ "@ai-sdk/provider-utils": "5.0.37",
31
31
  "ws": "^8.21.0"
32
32
  },
33
33
  "peerDependencies": {
@@ -74,6 +74,14 @@ export type ACPHarnessSettings<
74
74
  readonly skillsDirectory?: ACPV1Settings['skillsDirectory'];
75
75
  readonly instructionMapping?: ACPV1Settings['instructionMapping'];
76
76
  readonly outputSchemaMapping?: ACPV1Settings['outputSchemaMapping'];
77
+ /**
78
+ * Transport used for the harness-owned MCP server that exposes host tools to
79
+ * the ACP implementation. Defaults to `stdio`. Set this to `http` for
80
+ * implementations that only accept HTTP or SSE MCP servers from the client,
81
+ * which requires the implementation to advertise
82
+ * `agentCapabilities.mcpCapabilities.http`.
83
+ */
84
+ readonly hostToolMcpTransport?: ACPV1Settings['hostToolMcpTransport'];
77
85
  readonly askUserQuestions?: TAskUserQuestions;
78
86
  readonly permissionModeMapping?: ACPV1Settings['permissionModeMapping'];
79
87
  readonly session?: ACPV1Settings['session'];
package/src/index.ts CHANGED
@@ -23,6 +23,7 @@ export type {
23
23
  ACPAskUserQuestionsSettings,
24
24
  ACPCredentialBrokering,
25
25
  ACPAuthentication,
26
+ ACPHostToolMCPTransport,
26
27
  ACPInstallCommandSource,
27
28
  ACPInstructionMapping,
28
29
  ACPModelMapping,
@@ -113,6 +113,10 @@ const instructionMappingSchema: z.ZodType<ACPInstructionMapping> =
113
113
  variable: z.string().min(1),
114
114
  path: instructionMappingPathSchema,
115
115
  }),
116
+ z.object({
117
+ type: z.literal('filesystem'),
118
+ path: z.string().min(1),
119
+ }),
116
120
  ]);
117
121
 
118
122
  type ACPSerializableOutputSchemaMapping = {
@@ -32,6 +32,8 @@ import {
32
32
  warnCredentialBrokeringUnavailable,
33
33
  waitForBridgeReady,
34
34
  withBridgeToken,
35
+ writeInstructions,
36
+ writeSkills,
35
37
  } from '@ai-sdk/harness/utils';
36
38
  import {
37
39
  asSchema,
@@ -93,9 +95,11 @@ import type {
93
95
  ACPV1Settings,
94
96
  } from './acp-v1-settings';
95
97
  import {
96
- materializeACPSkills,
98
+ ACP_SKILL_NAME_PATTERN,
99
+ DEFAULT_ACP_SKILLS_DIRECTORY,
97
100
  resolveACPPrivateSessionDirectory,
98
101
  resolveACPSkillsDirectory,
102
+ validateACPSkills,
99
103
  } from './acp-v1-skills';
100
104
 
101
105
  const HARNESS_ID_REGEXP = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
@@ -324,6 +328,9 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
324
328
  const requestTransformations = settings.credentialBrokering({
325
329
  env: brokeringEnvironment,
326
330
  sandboxEnv: sandboxImplementationEnvironment,
331
+ ...(startOptions.headers == null
332
+ ? {}
333
+ : { headers: startOptions.headers }),
327
334
  });
328
335
  if (requestTransformations.length > 0) {
329
336
  await sandboxSession.addRequestTransformations(
@@ -372,17 +379,18 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
372
379
  });
373
380
  const privateSessionDir = resolveACPPrivateSessionDirectory({
374
381
  sandboxHomeDir,
375
- sessionWorkDir: workDir,
376
382
  harnessId: settings.harnessId,
377
383
  sessionId: startOptions.sessionId,
378
384
  });
385
+ const implementationHomeDir =
386
+ implementation.source.type === 'install-command'
387
+ ? `${resolvedImplementationDir}/home`
388
+ : sandboxHomeDir;
389
+ const skillsDir =
390
+ settings.skillsDirectory ?? DEFAULT_ACP_SKILLS_DIRECTORY;
379
391
  const skillsDirectory = resolveACPSkillsDirectory({
380
- implementationHomeDir:
381
- implementation.source.type === 'install-command'
382
- ? `${resolvedImplementationDir}/home`
383
- : sandboxHomeDir,
392
+ implementationHomeDir,
384
393
  skillsDirectory: settings.skillsDirectory,
385
- sessionWorkDir: workDir,
386
394
  });
387
395
  const bridgeStateDir = `${privateSessionDir}/bridge`;
388
396
  const report = startOptions.observability?.report;
@@ -459,7 +467,8 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
459
467
  }),
460
468
  instructionsFingerprint: lifecycleData.instructionsFingerprint,
461
469
  sandbox: toolSafeSandboxSession,
462
- sessionWorkDir: workDir,
470
+ homePath: implementationHomeDir,
471
+ skillsDir,
463
472
  skillsDirectory,
464
473
  acpSessionId: lifecycleData.acpSessionId,
465
474
  bridgePort: coords.port,
@@ -620,6 +629,7 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
620
629
  clientCapabilities: settings.clientCapabilities,
621
630
  askUserQuestionsRequestMethod:
622
631
  settings.askUserQuestions?.requestMethod,
632
+ hostToolMcpTransport: settings.hostToolMcpTransport,
623
633
  }),
624
634
  ...sandboxProviderAuthenticationEnvironment,
625
635
  BRIDGE_CHANNEL_TOKEN: token,
@@ -752,7 +762,8 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
752
762
  }),
753
763
  instructionsFingerprint: lifecycleData?.instructionsFingerprint,
754
764
  sandbox: toolSafeSandboxSession,
755
- sessionWorkDir: workDir,
765
+ homePath: implementationHomeDir,
766
+ skillsDir,
756
767
  skillsDirectory,
757
768
  acpSessionId: lifecycleData?.acpSessionId,
758
769
  bridgePort: boundPort,
@@ -1063,7 +1074,8 @@ function createSession({
1063
1074
  initialGuidanceApplied: initialGuidanceAppliedAtStart,
1064
1075
  instructionsFingerprint: instructionsFingerprintAtStart,
1065
1076
  sandbox,
1066
- sessionWorkDir,
1077
+ homePath,
1078
+ skillsDir,
1067
1079
  skillsDirectory,
1068
1080
  acpSessionId: acpSessionIdAtStart,
1069
1081
  bridgePort,
@@ -1100,7 +1112,8 @@ function createSession({
1100
1112
  initialGuidanceApplied: boolean;
1101
1113
  instructionsFingerprint: string | undefined;
1102
1114
  sandbox: SandboxSession;
1103
- sessionWorkDir: string;
1115
+ homePath: string;
1116
+ skillsDir: string;
1104
1117
  skillsDirectory: string;
1105
1118
  acpSessionId: string | undefined;
1106
1119
  bridgePort: number;
@@ -1571,15 +1584,41 @@ function createSession({
1571
1584
  skills: ReadonlyArray<HarnessV1Skill>;
1572
1585
  abortSignal?: AbortSignal;
1573
1586
  }): Promise<void> => {
1574
- await materializeACPSkills({
1587
+ validateACPSkills({ skills });
1588
+ await writeSkills({
1575
1589
  sandbox,
1576
- rootDir: skillsDirectory,
1577
- sessionWorkDir,
1590
+ homePath,
1591
+ skillsDir,
1578
1592
  skills,
1579
1593
  abortSignal,
1594
+ skillNamePattern: ACP_SKILL_NAME_PATTERN,
1595
+ invalidSkillNameMessage: ({ name }) =>
1596
+ `Invalid ACP skill name ${JSON.stringify(name)}: expected a kebab-case slug.`,
1597
+ invalidSkillFilePathMessage: ({ skillName, filePath }) =>
1598
+ `Invalid ACP skill file path ${JSON.stringify(filePath)} for skill ${JSON.stringify(
1599
+ skillName,
1600
+ )}: expected a relative POSIX path without traversal.`,
1580
1601
  });
1581
1602
  };
1582
1603
 
1604
+ const synchronizeInstructions = async ({
1605
+ instructions,
1606
+ abortSignal,
1607
+ }: {
1608
+ instructions: string | undefined;
1609
+ abortSignal?: AbortSignal;
1610
+ }): Promise<void> => {
1611
+ if (instructionMapping?.type === 'filesystem') {
1612
+ await writeInstructions({
1613
+ sandbox,
1614
+ homePath,
1615
+ instructionsFile: instructionMapping.path,
1616
+ instructions,
1617
+ abortSignal,
1618
+ });
1619
+ }
1620
+ };
1621
+
1583
1622
  return {
1584
1623
  sessionId,
1585
1624
  isResume,
@@ -1588,6 +1627,10 @@ function createSession({
1588
1627
  skills: options.skills,
1589
1628
  abortSignal: options.abortSignal,
1590
1629
  });
1630
+ await synchronizeInstructions({
1631
+ instructions: options.instructions,
1632
+ abortSignal: options.abortSignal,
1633
+ });
1591
1634
  if (options.responseFormat?.type === 'json') {
1592
1635
  if (options.responseFormat.schema == null) {
1593
1636
  throw unsupported({
@@ -1650,6 +1693,7 @@ function createSession({
1650
1693
  channel.send({
1651
1694
  type: 'start',
1652
1695
  prompt:
1696
+ instructionMapping?.type !== 'filesystem' &&
1653
1697
  instructionsFingerprint !== nextInstructionsFingerprint &&
1654
1698
  (instructionMapping == null || initialGuidanceApplied)
1655
1699
  ? prependACPInstructionGuidance({
@@ -1692,6 +1736,10 @@ function createSession({
1692
1736
  skills: options.skills,
1693
1737
  abortSignal: options.abortSignal,
1694
1738
  });
1739
+ await synchronizeInstructions({
1740
+ instructions: options.instructions,
1741
+ abortSignal: options.abortSignal,
1742
+ });
1695
1743
  if (options.responseFormat?.type === 'json') {
1696
1744
  if (options.responseFormat.schema == null) {
1697
1745
  throw unsupported({
@@ -1777,11 +1825,6 @@ function createSession({
1777
1825
  `${harnessId} ACP session ${sessionId} is stopped; cannot suspend.`,
1778
1826
  );
1779
1827
  }
1780
- if (!turnInFlight) {
1781
- throw new Error(
1782
- `${harnessId} ACP session ${sessionId} has no in-flight turn to suspend.`,
1783
- );
1784
- }
1785
1828
  stopped = true;
1786
1829
  const lastSeenEventId = await channel.suspend();
1787
1830
  return {
@@ -90,9 +90,11 @@ export type ACPModelMapping =
90
90
  export type ACPCredentialBrokering = ({
91
91
  env,
92
92
  sandboxEnv,
93
+ headers,
93
94
  }: {
94
95
  env: Readonly<Record<string, string>>;
95
96
  sandboxEnv?: Readonly<Record<string, string>>;
97
+ headers?: Readonly<Record<string, string>>;
96
98
  }) => ReadonlyArray<HarnessV1RequestTransformation>;
97
99
 
98
100
  export type ACPPermissionModeTarget =
@@ -124,6 +126,10 @@ export type ACPInstructionMapping =
124
126
  readonly type: 'launch-env-json';
125
127
  readonly variable: string;
126
128
  readonly path: ReadonlyArray<string>;
129
+ }
130
+ | {
131
+ readonly type: 'filesystem';
132
+ readonly path: string;
127
133
  };
128
134
 
129
135
  export type ACPOutputSchemaMapping = {
@@ -131,6 +137,12 @@ export type ACPOutputSchemaMapping = {
131
137
  readonly path: ReadonlyArray<string>;
132
138
  };
133
139
 
140
+ /**
141
+ * Transport used for the harness-owned MCP server that exposes host tools to
142
+ * an ACP implementation.
143
+ */
144
+ export type ACPHostToolMCPTransport = 'stdio' | 'http';
145
+
134
146
  export type ACPAskUserQuestionsSettings = {
135
147
  readonly requestMethod: string;
136
148
  readonly isNativeToolCall?: (options: {
@@ -200,6 +212,14 @@ export type ACPV1Settings = {
200
212
  * below the ACP session prompt's `_meta` field.
201
213
  */
202
214
  readonly outputSchemaMapping?: ACPOutputSchemaMapping;
215
+ /**
216
+ * Transport used for the harness-owned MCP server that exposes host tools to
217
+ * the ACP implementation. Defaults to `stdio`. Set this to `http` for
218
+ * implementations that only accept HTTP or SSE MCP servers from the client,
219
+ * which requires the implementation to advertise
220
+ * `agentCapabilities.mcpCapabilities.http`.
221
+ */
222
+ readonly hostToolMcpTransport?: ACPHostToolMCPTransport;
203
223
  readonly askUserQuestions?: ACPAskUserQuestionsSettings;
204
224
  readonly permissionModeMapping?: ACPPermissionModeMapping;
205
225
  readonly session?: {
@@ -1,74 +1,35 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import path from 'node:path';
3
3
  import type { HarnessV1Skill } from '@ai-sdk/harness';
4
- import { writeSkills, type WriteSkillsResult } from '@ai-sdk/harness/utils';
5
- import type { Experimental_SandboxSession } from '@ai-sdk/provider-utils';
6
4
 
7
- const ACP_SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
5
+ export const ACP_SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
8
6
  export const DEFAULT_ACP_SKILLS_DIRECTORY = '.agents/skills';
9
7
 
10
8
  export function resolveACPPrivateSessionDirectory({
11
9
  sandboxHomeDir,
12
- sessionWorkDir,
13
10
  harnessId,
14
11
  sessionId,
15
12
  }: {
16
13
  sandboxHomeDir: string;
17
- sessionWorkDir: string;
18
14
  harnessId: string;
19
15
  sessionId: string;
20
16
  }): string {
21
17
  const sessionKey = createHash('sha256').update(sessionId).digest('hex');
22
- const rootDir = path.posix.join(
18
+ return path.posix.join(
23
19
  sandboxHomeDir,
24
20
  '.ai-sdk',
25
21
  'harness-acp',
26
22
  harnessId,
27
23
  sessionKey,
28
24
  );
29
- assertOutsideSessionWorkDir({ rootDir, sessionWorkDir });
30
- return rootDir;
31
- }
32
-
33
- export async function materializeACPSkills({
34
- sandbox,
35
- rootDir,
36
- sessionWorkDir,
37
- skills,
38
- abortSignal,
39
- }: {
40
- sandbox: Experimental_SandboxSession;
41
- rootDir: string;
42
- sessionWorkDir: string;
43
- skills: ReadonlyArray<HarnessV1Skill>;
44
- abortSignal?: AbortSignal;
45
- }): Promise<WriteSkillsResult> {
46
- validateACPSkills({ skills });
47
- assertOutsideSessionWorkDir({ rootDir, sessionWorkDir });
48
-
49
- return writeSkills({
50
- sandbox,
51
- rootDir,
52
- skills,
53
- abortSignal,
54
- skillNamePattern: ACP_SKILL_NAME_PATTERN,
55
- invalidSkillNameMessage: ({ name }) =>
56
- `Invalid ACP skill name ${JSON.stringify(name)}: expected a kebab-case slug.`,
57
- invalidSkillFilePathMessage: ({ skillName, filePath }) =>
58
- `Invalid ACP skill file path ${JSON.stringify(filePath)} for skill ${JSON.stringify(
59
- skillName,
60
- )}: expected a relative POSIX path without traversal.`,
61
- });
62
25
  }
63
26
 
64
27
  export function resolveACPSkillsDirectory({
65
28
  implementationHomeDir,
66
29
  skillsDirectory = DEFAULT_ACP_SKILLS_DIRECTORY,
67
- sessionWorkDir,
68
30
  }: {
69
31
  implementationHomeDir: string;
70
32
  skillsDirectory?: string;
71
- sessionWorkDir: string;
72
33
  }): string {
73
34
  const containsTraversal = skillsDirectory
74
35
  .split(/[\\/]/)
@@ -90,12 +51,10 @@ export function resolveACPSkillsDirectory({
90
51
  );
91
52
  }
92
53
 
93
- const rootDir = path.posix.join(implementationHomeDir, normalizedDirectory);
94
- assertOutsideSessionWorkDir({ rootDir, sessionWorkDir });
95
- return rootDir;
54
+ return path.posix.join(implementationHomeDir, normalizedDirectory);
96
55
  }
97
56
 
98
- function validateACPSkills({
57
+ export function validateACPSkills({
99
58
  skills,
100
59
  }: {
101
60
  skills: ReadonlyArray<HarnessV1Skill>;
@@ -174,23 +133,3 @@ function validateACPAttachedFilePath({
174
133
  }
175
134
  return normalizedPath;
176
135
  }
177
-
178
- function assertOutsideSessionWorkDir({
179
- rootDir,
180
- sessionWorkDir,
181
- }: {
182
- rootDir: string;
183
- sessionWorkDir: string;
184
- }): void {
185
- const relative = path.posix.relative(sessionWorkDir, rootDir);
186
- const isInside =
187
- relative === '' ||
188
- (!relative.startsWith('../') && !path.posix.isAbsolute(relative));
189
- if (isInside) {
190
- throw new Error(
191
- `ACP skill directory ${JSON.stringify(rootDir)} must be outside sessionWorkDir ${JSON.stringify(
192
- sessionWorkDir,
193
- )}.`,
194
- );
195
- }
196
- }
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod/v4';
2
2
  import type {
3
3
  ACPAuthentication,
4
+ ACPHostToolMCPTransport,
4
5
  ACPProfileValue,
5
6
  ACPSerializableValue,
6
7
  } from '../acp-v1-settings';
@@ -33,6 +34,11 @@ const profileValueSchema: z.ZodType<ACPProfileValue> = z.lazy(() =>
33
34
  ]),
34
35
  );
35
36
 
37
+ const hostToolMcpTransportSchema: z.ZodType<ACPHostToolMCPTransport> = z.enum([
38
+ 'stdio',
39
+ 'http',
40
+ ]);
41
+
36
42
  const serializableRecordSchema = z.record(z.string(), serializableValueSchema);
37
43
  const profileRecordSchema = z.record(z.string(), profileValueSchema);
38
44
 
@@ -61,6 +67,7 @@ export type ACPBridgeConfiguration = {
61
67
  readonly sessionMeta?: Readonly<Record<string, ACPSerializableValue>>;
62
68
  readonly clientCapabilities?: Readonly<Record<string, ACPSerializableValue>>;
63
69
  readonly askUserQuestionsRequestMethod?: string;
70
+ readonly hostToolMcpTransport?: ACPHostToolMCPTransport;
64
71
  };
65
72
 
66
73
  const bridgeConfigurationSchema: z.ZodType<ACPBridgeConfiguration> = z.object({
@@ -70,6 +77,7 @@ const bridgeConfigurationSchema: z.ZodType<ACPBridgeConfiguration> = z.object({
70
77
  sessionMeta: serializableRecordSchema.optional(),
71
78
  clientCapabilities: serializableRecordSchema.optional(),
72
79
  askUserQuestionsRequestMethod: z.string().optional(),
80
+ hostToolMcpTransport: hostToolMcpTransportSchema.optional(),
73
81
  });
74
82
 
75
83
  export function createACPBridgeEnvironment({
@@ -79,6 +87,7 @@ export function createACPBridgeEnvironment({
79
87
  sessionMeta,
80
88
  clientCapabilities,
81
89
  askUserQuestionsRequestMethod,
90
+ hostToolMcpTransport,
82
91
  }: ACPBridgeConfiguration): Record<string, string> {
83
92
  return {
84
93
  [ACP_BRIDGE_CONFIGURATION_ENV]: JSON.stringify({
@@ -90,6 +99,7 @@ export function createACPBridgeEnvironment({
90
99
  ...(askUserQuestionsRequestMethod == null
91
100
  ? {}
92
101
  : { askUserQuestionsRequestMethod }),
102
+ ...(hostToolMcpTransport == null ? {} : { hostToolMcpTransport }),
93
103
  }),
94
104
  };
95
105
  }
@@ -0,0 +1,69 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { execPath } from 'node:process';
3
+ import type * as acp from '@agentclientprotocol/sdk';
4
+ import { HarnessBridgeCapabilityUnsupportedError } from '@ai-sdk/harness/bridge';
5
+ import type { ACPHostToolMCPTransport } from '../acp-v1-settings';
6
+ import type { HostToolRelay } from './host-tool-relay';
7
+
8
+ /*
9
+ * Builds the ACP `session/new` definition for the harness-owned MCP server
10
+ * that exposes host tools. The stdio transport launches `host-tool-mcp.mjs`,
11
+ * which bridges back to the relay over its private HTTP protocol. The http
12
+ * transport points the ACP implementation straight at the relay's own MCP
13
+ * endpoint, which is required by implementations that reject client-supplied
14
+ * stdio MCP servers.
15
+ */
16
+ export function createHostToolMcpServerDefinition({
17
+ mcpTransport,
18
+ relay,
19
+ serverName,
20
+ catalogPath,
21
+ initialization,
22
+ harnessId,
23
+ }: {
24
+ mcpTransport: ACPHostToolMCPTransport;
25
+ relay: Pick<HostToolRelay, 'url' | 'mcpUrl' | 'credential'>;
26
+ serverName: string;
27
+ catalogPath: string;
28
+ initialization: Pick<acp.InitializeResponse, 'agentCapabilities'>;
29
+ harnessId: string;
30
+ }): acp.McpServer {
31
+ if (mcpTransport === 'stdio') {
32
+ return {
33
+ name: serverName,
34
+ command: execPath,
35
+ args: [fileURLToPath(new URL('./host-tool-mcp.mjs', import.meta.url))],
36
+ env: [
37
+ {
38
+ name: 'AI_SDK_ACP_HOST_TOOLS_FILE',
39
+ value: catalogPath,
40
+ },
41
+ {
42
+ name: 'AI_SDK_ACP_HOST_TOOL_RELAY_URL',
43
+ value: relay.url,
44
+ },
45
+ {
46
+ name: 'AI_SDK_ACP_HOST_TOOL_RELAY_CREDENTIAL',
47
+ value: relay.credential,
48
+ },
49
+ ],
50
+ };
51
+ }
52
+ if (initialization.agentCapabilities?.mcpCapabilities?.http !== true) {
53
+ throw new HarnessBridgeCapabilityUnsupportedError({
54
+ harnessId,
55
+ message:
56
+ 'This harness exposes host tools through an HTTP MCP server, but ' +
57
+ 'the ACP agent does not advertise support for HTTP MCP servers.',
58
+ });
59
+ }
60
+ if (relay.mcpUrl == null) {
61
+ throw new Error('The host tool MCP HTTP endpoint is unavailable.');
62
+ }
63
+ return {
64
+ type: 'http',
65
+ name: serverName,
66
+ url: relay.mcpUrl,
67
+ headers: [{ name: 'Authorization', value: `Bearer ${relay.credential}` }],
68
+ };
69
+ }