@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/CHANGELOG.md +25 -0
- package/README.md +4 -3
- package/dist/bridge/host-tool-mcp.mjs +1 -1
- package/dist/bridge/index.mjs +408 -32
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.d.ts +27 -2
- package/dist/index.js +68 -69
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/acp-harness.ts +8 -0
- package/src/index.ts +1 -0
- package/src/v1/acp-v1-bridge-protocol.ts +4 -0
- package/src/v1/acp-v1-harness.ts +62 -19
- package/src/v1/acp-v1-settings.ts +20 -0
- package/src/v1/acp-v1-skills.ts +4 -65
- package/src/v1/bridge/acp-v1-bridge-environment.ts +10 -0
- package/src/v1/bridge/host-tool-mcp-definition.ts +69 -0
- package/src/v1/bridge/host-tool-mcp-http.ts +182 -0
- package/src/v1/bridge/host-tool-relay.ts +87 -7
- package/src/v1/bridge/index.ts +14 -21
- package/src/v1/bridge/instruction-mapping.ts +12 -0
- package/src/v1/bridge/stream-translator.ts +35 -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.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.
|
|
30
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
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": {
|
package/src/acp-harness.ts
CHANGED
|
@@ -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
|
@@ -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 = {
|
package/src/v1/acp-v1-harness.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1587
|
+
validateACPSkills({ skills });
|
|
1588
|
+
await writeSkills({
|
|
1575
1589
|
sandbox,
|
|
1576
|
-
|
|
1577
|
-
|
|
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?: {
|
package/src/v1/acp-v1-skills.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
+
}
|