@ai-sdk/harness-acp 1.0.28 → 1.0.29

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.28",
3
+ "version": "1.0.29",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -26,7 +26,7 @@
26
26
  }
27
27
  },
28
28
  "dependencies": {
29
- "@ai-sdk/harness": "1.0.90",
29
+ "@ai-sdk/harness": "1.0.91",
30
30
  "@ai-sdk/provider-utils": "5.0.32",
31
31
  "ws": "^8.21.0"
32
32
  },
@@ -50,6 +50,7 @@ export type ACPHarnessSettings<TBuiltinTools extends ToolSet = {}> = {
50
50
  readonly authentication?: ACPV1Settings['authentication'];
51
51
  readonly providerAuthentication?: ACPV1Settings['providerAuthentication'];
52
52
  readonly modelId?: ACPV1Settings['modelId'];
53
+ readonly skillsDirectory?: ACPV1Settings['skillsDirectory'];
53
54
  readonly instructionMapping?: ACPV1Settings['instructionMapping'];
54
55
  readonly outputSchemaMapping?: ACPV1Settings['outputSchemaMapping'];
55
56
  readonly permissionModeMapping?: ACPV1Settings['permissionModeMapping'];
@@ -104,6 +105,7 @@ const acpResumeStateSchema = z.object({
104
105
  initialGuidanceApplied: z.boolean().optional(),
105
106
  skillsMaterialized: z.boolean().optional(),
106
107
  skillsFingerprint: z.string().optional(),
108
+ skillsDirectory: z.string().optional(),
107
109
  });
108
110
 
109
111
  export function createACP<TBuiltinTools extends ToolSet = {}>(
@@ -76,7 +76,6 @@ import {
76
76
  import {
77
77
  convertHarnessPromptToACPTextBlocks,
78
78
  prependACPInitialGuidance,
79
- type ACPSkillCatalogEntry,
80
79
  } from './acp-v1-prompt';
81
80
  import type {
82
81
  ACPInstructionMapping,
@@ -91,6 +90,7 @@ import {
91
90
  createACPSkillsFingerprint,
92
91
  materializeACPSkills,
93
92
  resolveACPPrivateSessionDirectory,
93
+ resolveACPSkillsDirectory,
94
94
  } from './acp-v1-skills';
95
95
 
96
96
  const HARNESS_ID_REGEXP = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
@@ -366,6 +366,14 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
366
366
  harnessId: settings.harnessId,
367
367
  sessionId: startOptions.sessionId,
368
368
  });
369
+ const skillsDirectory = resolveACPSkillsDirectory({
370
+ implementationHomeDir:
371
+ implementation.source.type === 'install-command'
372
+ ? `${resolvedImplementationDir}/home`
373
+ : sandboxHomeDir,
374
+ skillsDirectory: settings.skillsDirectory,
375
+ sessionWorkDir: workDir,
376
+ });
369
377
  const bridgeStateDir = `${privateSessionDir}/bridge`;
370
378
  const skills = startOptions.skills ?? [];
371
379
  const skillsFingerprint = createACPSkillsFingerprint({ skills });
@@ -373,21 +381,17 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
373
381
  isResume,
374
382
  lifecycleState: lifecycleData,
375
383
  skillsFingerprint,
384
+ skillsDirectory,
376
385
  });
377
- let skillCatalog: ReadonlyArray<ACPSkillCatalogEntry> = [];
378
386
  if (skills.length > 0) {
379
- skillCatalog = (
380
- await materializeACPSkills({
381
- sandbox: toolSafeSandboxSession,
382
- sandboxHomeDir,
383
- sessionWorkDir: workDir,
384
- harnessId: settings.harnessId,
385
- sessionId: startOptions.sessionId,
386
- skills,
387
- shouldMaterialize: shouldMaterializeSkills,
388
- abortSignal: startOptions.abortSignal,
389
- })
390
- ).catalog;
387
+ await materializeACPSkills({
388
+ sandbox: toolSafeSandboxSession,
389
+ rootDir: skillsDirectory,
390
+ sessionWorkDir: workDir,
391
+ skills,
392
+ shouldMaterialize: shouldMaterializeSkills,
393
+ abortSignal: startOptions.abortSignal,
394
+ });
391
395
  }
392
396
  const report = startOptions.observability?.report;
393
397
  const onDiagnostic = report
@@ -459,8 +463,8 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
459
463
  isResume: true,
460
464
  lifecycleState: lifecycleData,
461
465
  }),
462
- skillCatalog,
463
466
  skillsFingerprint,
467
+ skillsDirectory,
464
468
  acpSessionId: lifecycleData.acpSessionId,
465
469
  bridgePort: coords.port,
466
470
  bridgeToken: coords.token,
@@ -725,8 +729,8 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
725
729
  isResume,
726
730
  lifecycleState: lifecycleData,
727
731
  }),
728
- skillCatalog,
729
732
  skillsFingerprint,
733
+ skillsDirectory,
730
734
  acpSessionId: lifecycleData?.acpSessionId,
731
735
  bridgePort: boundPort,
732
736
  bridgeToken: token,
@@ -1044,8 +1048,8 @@ function createSession({
1044
1048
  mcpServers,
1045
1049
  isMcpToolCall,
1046
1050
  initialGuidanceApplied: initialGuidanceAppliedAtStart,
1047
- skillCatalog,
1048
1051
  skillsFingerprint,
1052
+ skillsDirectory,
1049
1053
  acpSessionId: acpSessionIdAtStart,
1050
1054
  bridgePort,
1051
1055
  bridgeToken,
@@ -1077,8 +1081,8 @@ function createSession({
1077
1081
  mcpServers: Record<string, unknown> | undefined;
1078
1082
  isMcpToolCall: ((toolCall: ACPToolCall) => boolean) | undefined;
1079
1083
  initialGuidanceApplied: boolean;
1080
- skillCatalog: ReadonlyArray<ACPSkillCatalogEntry>;
1081
1084
  skillsFingerprint: string;
1085
+ skillsDirectory: string;
1082
1086
  acpSessionId: string | undefined;
1083
1087
  bridgePort: number;
1084
1088
  bridgeToken: string;
@@ -1366,6 +1370,7 @@ function createSession({
1366
1370
  initialGuidanceApplied,
1367
1371
  skillsMaterialized: true,
1368
1372
  skillsFingerprint,
1373
+ skillsDirectory,
1369
1374
  });
1370
1375
 
1371
1376
  return {
@@ -1436,7 +1441,6 @@ function createSession({
1436
1441
  instructionMapping == null
1437
1442
  ? options.instructions
1438
1443
  : undefined,
1439
- skills: skillCatalog,
1440
1444
  }),
1441
1445
  ...(instructionMapping == null
1442
1446
  ? {}
@@ -8,6 +8,7 @@ export type ACPPromptGuidanceLifecycleState = {
8
8
  readonly initialGuidanceApplied?: boolean;
9
9
  readonly skillsMaterialized?: boolean;
10
10
  readonly skillsFingerprint?: string;
11
+ readonly skillsDirectory?: string;
11
12
  };
12
13
 
13
14
  export type ACPBridgeCoordinates = {
@@ -50,10 +51,12 @@ export function shouldMaterializeACPSkills({
50
51
  isResume,
51
52
  lifecycleState,
52
53
  skillsFingerprint,
54
+ skillsDirectory,
53
55
  }: {
54
56
  isResume: boolean;
55
57
  lifecycleState: ACPPromptGuidanceLifecycleState | undefined;
56
58
  skillsFingerprint: string;
59
+ skillsDirectory: string;
57
60
  }): boolean {
58
61
  if (
59
62
  isResume &&
@@ -64,7 +67,11 @@ export function shouldMaterializeACPSkills({
64
67
  'ACP lifecycle state was created with a different set of skills.',
65
68
  );
66
69
  }
67
- return !isResume || lifecycleState?.skillsMaterialized !== true;
70
+ return (
71
+ !isResume ||
72
+ lifecycleState?.skillsMaterialized !== true ||
73
+ lifecycleState.skillsDirectory !== skillsDirectory
74
+ );
68
75
  }
69
76
 
70
77
  export function validateACPLifecycleCompatibility({
@@ -11,12 +11,6 @@ export const acpTextContentBlockSchema = z.object({
11
11
 
12
12
  export type ACPTextContentBlock = z.infer<typeof acpTextContentBlockSchema>;
13
13
 
14
- export type ACPSkillCatalogEntry = {
15
- readonly name: string;
16
- readonly description: string;
17
- readonly path: string;
18
- };
19
-
20
14
  export function convertHarnessPromptToACPTextBlocks({
21
15
  prompt,
22
16
  harnessId,
@@ -75,32 +69,18 @@ export function convertHarnessPromptToACPTextBlocks({
75
69
  export function prependACPInitialGuidance({
76
70
  prompt,
77
71
  instructions,
78
- skills,
79
72
  }: {
80
73
  prompt: ReadonlyArray<ACPTextContentBlock>;
81
74
  instructions: string | undefined;
82
- skills: ReadonlyArray<ACPSkillCatalogEntry>;
83
75
  }): ACPTextContentBlock[] {
84
76
  const hasInstructions = instructions != null && instructions.length > 0;
85
- if (!hasInstructions && skills.length === 0) return [...prompt];
77
+ if (!hasInstructions) return [...prompt];
86
78
 
87
79
  const lines = [
88
80
  '<session-guidance>',
89
81
  'This block is operating guidance from the harness, not user-authored content.',
90
82
  ];
91
- if (hasInstructions) {
92
- lines.push('<instructions>', instructions, '</instructions>');
93
- }
94
- if (skills.length > 0) {
95
- lines.push(
96
- '<available-skills>',
97
- 'Load a skill only when relevant by reading its SKILL.md file at the listed absolute path.',
98
- );
99
- for (const skill of skills) {
100
- lines.push(`- ${skill.name}: ${skill.description} (${skill.path})`);
101
- }
102
- lines.push('</available-skills>');
103
- }
83
+ lines.push('<instructions>', instructions, '</instructions>');
104
84
  lines.push('</session-guidance>');
105
85
 
106
86
  return [{ type: 'text', text: lines.join('\n') }, ...prompt];
@@ -144,6 +144,11 @@ export type ACPV1Settings = {
144
144
  readonly authentication?: ACPAuthentication;
145
145
  readonly providerAuthentication?: ACPProviderAuthentication;
146
146
  readonly modelId?: string;
147
+ /**
148
+ * Native skills directory relative to the ACP implementation's home
149
+ * directory. Defaults to `.agents/skills`.
150
+ */
151
+ readonly skillsDirectory?: string;
147
152
  /**
148
153
  * Routes HarnessAgent instructions to a runtime-native system or developer
149
154
  * prompt. When omitted, instructions are prepended to the first user prompt.
@@ -3,9 +3,9 @@ import path from 'node:path';
3
3
  import type { HarnessV1Skill } from '@ai-sdk/harness';
4
4
  import { writeSkills } from '@ai-sdk/harness/utils';
5
5
  import type { Experimental_SandboxSession } from '@ai-sdk/provider-utils';
6
- import type { ACPSkillCatalogEntry } from './acp-v1-prompt';
7
6
 
8
7
  const ACP_SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
8
+ export const DEFAULT_ACP_SKILLS_DIRECTORY = '.agents/skills';
9
9
 
10
10
  export function createACPSkillsFingerprint({
11
11
  skills,
@@ -59,37 +59,21 @@ export function resolveACPPrivateSessionDirectory({
59
59
 
60
60
  export async function materializeACPSkills({
61
61
  sandbox,
62
- sandboxHomeDir,
62
+ rootDir,
63
63
  sessionWorkDir,
64
- harnessId,
65
- sessionId,
66
64
  skills,
67
65
  shouldMaterialize,
68
66
  abortSignal,
69
67
  }: {
70
68
  sandbox: Experimental_SandboxSession;
71
- sandboxHomeDir: string;
69
+ rootDir: string;
72
70
  sessionWorkDir: string;
73
- harnessId: string;
74
- sessionId: string;
75
71
  skills: ReadonlyArray<HarnessV1Skill>;
76
72
  shouldMaterialize: boolean;
77
73
  abortSignal?: AbortSignal;
78
- }): Promise<{
79
- readonly catalog: ReadonlyArray<ACPSkillCatalogEntry>;
80
- readonly rootDir: string;
81
- }> {
74
+ }): Promise<void> {
82
75
  validateACPSkills({ skills });
83
-
84
- const rootDir = path.posix.join(
85
- resolveACPPrivateSessionDirectory({
86
- sandboxHomeDir,
87
- sessionWorkDir,
88
- harnessId,
89
- sessionId,
90
- }),
91
- 'skills',
92
- );
76
+ assertOutsideSessionWorkDir({ rootDir, sessionWorkDir });
93
77
 
94
78
  if (shouldMaterialize) {
95
79
  await writeSkills({
@@ -106,15 +90,40 @@ export async function materializeACPSkills({
106
90
  )}: expected a relative POSIX path without traversal.`,
107
91
  });
108
92
  }
93
+ }
109
94
 
110
- return {
111
- rootDir,
112
- catalog: skills.map(skill => ({
113
- name: skill.name,
114
- description: skill.description,
115
- path: path.posix.join(rootDir, skill.name, 'SKILL.md'),
116
- })),
117
- };
95
+ export function resolveACPSkillsDirectory({
96
+ implementationHomeDir,
97
+ skillsDirectory = DEFAULT_ACP_SKILLS_DIRECTORY,
98
+ sessionWorkDir,
99
+ }: {
100
+ implementationHomeDir: string;
101
+ skillsDirectory?: string;
102
+ sessionWorkDir: string;
103
+ }): string {
104
+ const containsTraversal = skillsDirectory
105
+ .split(/[\\/]/)
106
+ .some(segment => segment === '..');
107
+ const normalizedDirectory = path.posix.normalize(skillsDirectory);
108
+ if (
109
+ skillsDirectory.length === 0 ||
110
+ skillsDirectory.includes('\\') ||
111
+ path.posix.isAbsolute(skillsDirectory) ||
112
+ path.win32.isAbsolute(skillsDirectory) ||
113
+ containsTraversal ||
114
+ normalizedDirectory === '.' ||
115
+ normalizedDirectory.startsWith('../') ||
116
+ normalizedDirectory.includes('/../') ||
117
+ normalizedDirectory.endsWith('/..')
118
+ ) {
119
+ throw new Error(
120
+ `ACP skillsDirectory ${JSON.stringify(skillsDirectory)} must be a relative POSIX path without traversal.`,
121
+ );
122
+ }
123
+
124
+ const rootDir = path.posix.join(implementationHomeDir, normalizedDirectory);
125
+ assertOutsideSessionWorkDir({ rootDir, sessionWorkDir });
126
+ return rootDir;
118
127
  }
119
128
 
120
129
  function validateACPSkills({