@ai-sdk/harness 1.0.101 → 1.0.102
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 +19 -0
- package/dist/agent/index.d.ts +65 -4
- package/dist/agent/index.js +493 -441
- package/dist/agent/index.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/utils/index.d.ts +17 -3
- package/dist/utils/index.js +321 -18
- package/dist/utils/index.js.map +1 -1
- package/package.json +3 -3
- package/src/agent/harness-agent-session.ts +5 -0
- package/src/agent/harness-agent-settings.ts +82 -3
- package/src/agent/harness-agent.ts +67 -0
- package/src/agent/internal/harness-stream-text-result.ts +33 -13
- package/src/agent/internal/run-prompt.ts +209 -87
- package/src/agent/internal/turn-telemetry.ts +293 -444
- package/src/utils/index.ts +5 -0
- package/src/utils/write-instructions.ts +373 -0
- package/src/utils/write-skills.ts +59 -2
- package/src/v1/harness-v1-session.ts +5 -0
package/src/utils/index.ts
CHANGED
|
@@ -26,6 +26,11 @@ export {
|
|
|
26
26
|
} from './sandbox-credential-brokering';
|
|
27
27
|
export { resolveSandboxHomeDir } from './sandbox-home-dir';
|
|
28
28
|
export { shellQuote } from './shell-quote';
|
|
29
|
+
export {
|
|
30
|
+
writeInstructions,
|
|
31
|
+
type WriteInstructionsOptions,
|
|
32
|
+
type WriteInstructionsResult,
|
|
33
|
+
} from './write-instructions';
|
|
29
34
|
export {
|
|
30
35
|
writeSkills,
|
|
31
36
|
type SkillFilePathMode,
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {
|
|
3
|
+
safeParseJSON,
|
|
4
|
+
type Experimental_SandboxSession,
|
|
5
|
+
} from '@ai-sdk/provider-utils';
|
|
6
|
+
import { shellQuote } from './shell-quote';
|
|
7
|
+
|
|
8
|
+
const INSTRUCTIONS_METADATA_VERSION = 1;
|
|
9
|
+
|
|
10
|
+
export type WriteInstructionsOptions = {
|
|
11
|
+
sandbox: Experimental_SandboxSession;
|
|
12
|
+
homePath: string;
|
|
13
|
+
instructionsFile: string;
|
|
14
|
+
instructions?: string;
|
|
15
|
+
abortSignal?: AbortSignal;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type WriteInstructionsResult = {
|
|
19
|
+
changed: boolean;
|
|
20
|
+
filePath: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type InstructionsMetadata = {
|
|
24
|
+
readonly version: typeof INSTRUCTIONS_METADATA_VERSION;
|
|
25
|
+
readonly originalContent: string | null;
|
|
26
|
+
readonly instructions: string;
|
|
27
|
+
readonly appliedContent: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export async function writeInstructions({
|
|
31
|
+
sandbox,
|
|
32
|
+
homePath,
|
|
33
|
+
instructionsFile,
|
|
34
|
+
instructions,
|
|
35
|
+
abortSignal,
|
|
36
|
+
}: WriteInstructionsOptions): Promise<WriteInstructionsResult> {
|
|
37
|
+
const { targetPath, metadataPath } = resolveInstructionsFilePath({
|
|
38
|
+
homePath,
|
|
39
|
+
instructionsFile,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const hasInstructions =
|
|
43
|
+
typeof instructions === 'string' && instructions.trim().length > 0;
|
|
44
|
+
const trimmedInstructions = hasInstructions ? instructions.trim() : '';
|
|
45
|
+
|
|
46
|
+
const currentDiskContent = await sandbox.readTextFile({
|
|
47
|
+
path: targetPath,
|
|
48
|
+
abortSignal,
|
|
49
|
+
});
|
|
50
|
+
const existingMetadata = await readInstructionsMetadata({
|
|
51
|
+
sandbox,
|
|
52
|
+
metadataPath,
|
|
53
|
+
abortSignal,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (hasInstructions) {
|
|
57
|
+
const originalContent = deriveOriginalContent({
|
|
58
|
+
currentDiskContent,
|
|
59
|
+
existingMetadata,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const targetContent =
|
|
63
|
+
originalContent != null && originalContent.trim().length > 0
|
|
64
|
+
? `${originalContent.replace(/\n+$/, '')}\n\n${trimmedInstructions}\n`
|
|
65
|
+
: `${trimmedInstructions}\n`;
|
|
66
|
+
|
|
67
|
+
if (
|
|
68
|
+
currentDiskContent === targetContent &&
|
|
69
|
+
existingMetadata != null &&
|
|
70
|
+
existingMetadata.instructions === trimmedInstructions &&
|
|
71
|
+
existingMetadata.originalContent === originalContent
|
|
72
|
+
) {
|
|
73
|
+
return { changed: false, filePath: targetPath };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
await sandbox.writeTextFile({
|
|
77
|
+
path: targetPath,
|
|
78
|
+
content: targetContent,
|
|
79
|
+
abortSignal,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await writeInstructionsMetadata({
|
|
83
|
+
sandbox,
|
|
84
|
+
metadataPath,
|
|
85
|
+
metadata: {
|
|
86
|
+
version: INSTRUCTIONS_METADATA_VERSION,
|
|
87
|
+
originalContent,
|
|
88
|
+
instructions: trimmedInstructions,
|
|
89
|
+
appliedContent: targetContent,
|
|
90
|
+
},
|
|
91
|
+
abortSignal,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return { changed: true, filePath: targetPath };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (existingMetadata == null) {
|
|
98
|
+
return { changed: false, filePath: targetPath };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const restoredContent = deriveRestoredContent({
|
|
102
|
+
currentDiskContent,
|
|
103
|
+
existingMetadata,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (restoredContent != null) {
|
|
107
|
+
const contentToWrite = `${restoredContent.replace(/\n+$/, '')}\n`;
|
|
108
|
+
await sandbox.writeTextFile({
|
|
109
|
+
path: targetPath,
|
|
110
|
+
content: contentToWrite,
|
|
111
|
+
abortSignal,
|
|
112
|
+
});
|
|
113
|
+
} else {
|
|
114
|
+
await removeTargetFile({
|
|
115
|
+
sandbox,
|
|
116
|
+
targetPath,
|
|
117
|
+
abortSignal,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
await removeMetadataFile({
|
|
122
|
+
sandbox,
|
|
123
|
+
metadataPath,
|
|
124
|
+
abortSignal,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return { changed: true, filePath: targetPath };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function deriveOriginalContent({
|
|
131
|
+
currentDiskContent,
|
|
132
|
+
existingMetadata,
|
|
133
|
+
}: {
|
|
134
|
+
currentDiskContent: string | null;
|
|
135
|
+
existingMetadata: InstructionsMetadata | undefined;
|
|
136
|
+
}): string | null {
|
|
137
|
+
if (existingMetadata == null) {
|
|
138
|
+
return currentDiskContent;
|
|
139
|
+
}
|
|
140
|
+
if (currentDiskContent == null) {
|
|
141
|
+
return existingMetadata.originalContent;
|
|
142
|
+
}
|
|
143
|
+
if (currentDiskContent === existingMetadata.appliedContent) {
|
|
144
|
+
return existingMetadata.originalContent;
|
|
145
|
+
}
|
|
146
|
+
const trimmedDisk = currentDiskContent.replace(/\n+$/, '');
|
|
147
|
+
const expectedSuffix = `\n\n${existingMetadata.instructions}`;
|
|
148
|
+
if (trimmedDisk.endsWith(expectedSuffix)) {
|
|
149
|
+
const userBase = trimmedDisk.slice(0, -expectedSuffix.length);
|
|
150
|
+
return userBase.length > 0 ? userBase : null;
|
|
151
|
+
}
|
|
152
|
+
if (trimmedDisk === existingMetadata.instructions) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
return existingMetadata.originalContent ?? currentDiskContent;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function deriveRestoredContent({
|
|
159
|
+
currentDiskContent,
|
|
160
|
+
existingMetadata,
|
|
161
|
+
}: {
|
|
162
|
+
currentDiskContent: string | null;
|
|
163
|
+
existingMetadata: InstructionsMetadata;
|
|
164
|
+
}): string | null {
|
|
165
|
+
if (currentDiskContent == null) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
if (currentDiskContent === existingMetadata.appliedContent) {
|
|
169
|
+
return existingMetadata.originalContent != null &&
|
|
170
|
+
existingMetadata.originalContent.trim().length > 0
|
|
171
|
+
? existingMetadata.originalContent
|
|
172
|
+
: null;
|
|
173
|
+
}
|
|
174
|
+
const trimmedDisk = currentDiskContent.replace(/\n+$/, '');
|
|
175
|
+
const expectedSuffix = `\n\n${existingMetadata.instructions}`;
|
|
176
|
+
if (trimmedDisk.endsWith(expectedSuffix)) {
|
|
177
|
+
const userBase = trimmedDisk.slice(0, -expectedSuffix.length);
|
|
178
|
+
return userBase.length > 0 ? userBase : null;
|
|
179
|
+
}
|
|
180
|
+
if (trimmedDisk === existingMetadata.instructions) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
return currentDiskContent;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function resolveInstructionsFilePath({
|
|
187
|
+
homePath,
|
|
188
|
+
instructionsFile,
|
|
189
|
+
}: {
|
|
190
|
+
homePath: string;
|
|
191
|
+
instructionsFile: string;
|
|
192
|
+
}): { targetPath: string; metadataPath: string } {
|
|
193
|
+
if (typeof homePath !== 'string' || homePath.trim().length === 0) {
|
|
194
|
+
throw new Error('Invalid homePath: expected a non-empty string.');
|
|
195
|
+
}
|
|
196
|
+
if (!path.posix.isAbsolute(homePath)) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
`Invalid homePath ${JSON.stringify(homePath)}: expected an absolute POSIX path.`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
if (
|
|
202
|
+
typeof instructionsFile !== 'string' ||
|
|
203
|
+
instructionsFile.trim().length === 0
|
|
204
|
+
) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`Invalid instructionsFile ${JSON.stringify(instructionsFile)}: expected a relative POSIX path without traversal.`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
const containsTraversal = instructionsFile
|
|
210
|
+
.split(/[\\\/]/)
|
|
211
|
+
.some(segment => segment === '..');
|
|
212
|
+
const normalizedInstructionsFile = path.posix.normalize(
|
|
213
|
+
instructionsFile.trim(),
|
|
214
|
+
);
|
|
215
|
+
const normalizedNoTrailingSlash = normalizedInstructionsFile.replace(
|
|
216
|
+
/\/+$/,
|
|
217
|
+
'',
|
|
218
|
+
);
|
|
219
|
+
if (
|
|
220
|
+
instructionsFile.includes('\\') ||
|
|
221
|
+
path.posix.isAbsolute(instructionsFile) ||
|
|
222
|
+
path.win32.isAbsolute(instructionsFile) ||
|
|
223
|
+
containsTraversal ||
|
|
224
|
+
instructionsFile.endsWith('/') ||
|
|
225
|
+
instructionsFile.endsWith('\\') ||
|
|
226
|
+
instructionsFile.endsWith('/.') ||
|
|
227
|
+
instructionsFile.endsWith('/..') ||
|
|
228
|
+
normalizedNoTrailingSlash === '' ||
|
|
229
|
+
normalizedNoTrailingSlash === '.' ||
|
|
230
|
+
normalizedInstructionsFile.startsWith('../') ||
|
|
231
|
+
normalizedInstructionsFile.includes('/../') ||
|
|
232
|
+
normalizedInstructionsFile.endsWith('/..')
|
|
233
|
+
) {
|
|
234
|
+
throw new Error(
|
|
235
|
+
`Invalid instructionsFile ${JSON.stringify(instructionsFile)}: expected a relative POSIX path without traversal.`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
const targetPath = path.posix.join(homePath, normalizedNoTrailingSlash);
|
|
239
|
+
const relative = path.posix.relative(homePath, targetPath);
|
|
240
|
+
if (
|
|
241
|
+
relative === '' ||
|
|
242
|
+
relative.startsWith('..') ||
|
|
243
|
+
path.posix.isAbsolute(relative)
|
|
244
|
+
) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
`Invalid instructionsFile ${JSON.stringify(instructionsFile)}: must be a subpath within homePath ${JSON.stringify(homePath)}.`,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const dir = path.posix.dirname(targetPath);
|
|
251
|
+
const base = path.posix.basename(targetPath);
|
|
252
|
+
const metadataPath = path.posix.join(
|
|
253
|
+
dir,
|
|
254
|
+
`.${base}.ai-sdk-harness-instructions.json`,
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
return { targetPath, metadataPath };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function readInstructionsMetadata({
|
|
261
|
+
sandbox,
|
|
262
|
+
metadataPath,
|
|
263
|
+
abortSignal,
|
|
264
|
+
}: {
|
|
265
|
+
sandbox: Experimental_SandboxSession;
|
|
266
|
+
metadataPath: string;
|
|
267
|
+
abortSignal?: AbortSignal;
|
|
268
|
+
}): Promise<InstructionsMetadata | undefined> {
|
|
269
|
+
const content = await sandbox.readTextFile({
|
|
270
|
+
path: metadataPath,
|
|
271
|
+
abortSignal,
|
|
272
|
+
});
|
|
273
|
+
if (content == null) return undefined;
|
|
274
|
+
const parsed = await safeParseJSON({ text: content });
|
|
275
|
+
if (!parsed.success || !isInstructionsMetadata(parsed.value)) {
|
|
276
|
+
throw new Error(
|
|
277
|
+
`Invalid AI SDK harness instructions metadata: ${metadataPath}`,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
return parsed.value;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function isInstructionsMetadata(value: unknown): value is InstructionsMetadata {
|
|
284
|
+
if (value == null || typeof value !== 'object' || Array.isArray(value)) {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
const candidate = value as Record<string, unknown>;
|
|
288
|
+
return (
|
|
289
|
+
candidate.version === INSTRUCTIONS_METADATA_VERSION &&
|
|
290
|
+
(candidate.originalContent === null ||
|
|
291
|
+
typeof candidate.originalContent === 'string') &&
|
|
292
|
+
typeof candidate.instructions === 'string' &&
|
|
293
|
+
typeof candidate.appliedContent === 'string'
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function writeInstructionsMetadata({
|
|
298
|
+
sandbox,
|
|
299
|
+
metadataPath,
|
|
300
|
+
metadata,
|
|
301
|
+
abortSignal,
|
|
302
|
+
}: {
|
|
303
|
+
sandbox: Experimental_SandboxSession;
|
|
304
|
+
metadataPath: string;
|
|
305
|
+
metadata: InstructionsMetadata;
|
|
306
|
+
abortSignal?: AbortSignal;
|
|
307
|
+
}): Promise<void> {
|
|
308
|
+
const temporaryPath = `${metadataPath}.tmp`;
|
|
309
|
+
await sandbox.writeTextFile({
|
|
310
|
+
path: temporaryPath,
|
|
311
|
+
content: `${JSON.stringify(metadata, null, 2)}\n`,
|
|
312
|
+
abortSignal,
|
|
313
|
+
});
|
|
314
|
+
await runSandboxCommand({
|
|
315
|
+
sandbox,
|
|
316
|
+
command: `mv -f ${shellQuote(temporaryPath)} ${shellQuote(metadataPath)}`,
|
|
317
|
+
abortSignal,
|
|
318
|
+
errorMessage: `Failed to update instructions metadata: ${metadataPath}`,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function removeMetadataFile({
|
|
323
|
+
sandbox,
|
|
324
|
+
metadataPath,
|
|
325
|
+
abortSignal,
|
|
326
|
+
}: {
|
|
327
|
+
sandbox: Experimental_SandboxSession;
|
|
328
|
+
metadataPath: string;
|
|
329
|
+
abortSignal?: AbortSignal;
|
|
330
|
+
}): Promise<void> {
|
|
331
|
+
await runSandboxCommand({
|
|
332
|
+
sandbox,
|
|
333
|
+
command: `rm -f -- ${shellQuote(metadataPath)}`,
|
|
334
|
+
abortSignal,
|
|
335
|
+
errorMessage: `Failed to remove instructions metadata: ${metadataPath}`,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function removeTargetFile({
|
|
340
|
+
sandbox,
|
|
341
|
+
targetPath,
|
|
342
|
+
abortSignal,
|
|
343
|
+
}: {
|
|
344
|
+
sandbox: Experimental_SandboxSession;
|
|
345
|
+
targetPath: string;
|
|
346
|
+
abortSignal?: AbortSignal;
|
|
347
|
+
}): Promise<void> {
|
|
348
|
+
await runSandboxCommand({
|
|
349
|
+
sandbox,
|
|
350
|
+
command: `rm -f -- ${shellQuote(targetPath)}`,
|
|
351
|
+
abortSignal,
|
|
352
|
+
errorMessage: `Failed to remove instructions file: ${targetPath}`,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function runSandboxCommand({
|
|
357
|
+
sandbox,
|
|
358
|
+
command,
|
|
359
|
+
abortSignal,
|
|
360
|
+
errorMessage,
|
|
361
|
+
}: {
|
|
362
|
+
sandbox: Experimental_SandboxSession;
|
|
363
|
+
command: string;
|
|
364
|
+
abortSignal?: AbortSignal;
|
|
365
|
+
errorMessage: string;
|
|
366
|
+
}): Promise<void> {
|
|
367
|
+
const result = await sandbox.run({ command, abortSignal });
|
|
368
|
+
if (result.exitCode !== 0) {
|
|
369
|
+
throw new Error(
|
|
370
|
+
`${errorMessage} (exit ${result.exitCode})${result.stderr ? `: ${result.stderr}` : ''}`,
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
@@ -15,7 +15,8 @@ export type SkillFilePathMode = 'relative' | 'strip-leading-slashes';
|
|
|
15
15
|
|
|
16
16
|
export type WriteSkillsOptions = {
|
|
17
17
|
sandbox: Experimental_SandboxSession;
|
|
18
|
-
|
|
18
|
+
homePath: string;
|
|
19
|
+
skillsDir: string;
|
|
19
20
|
skills: ReadonlyArray<HarnessV1Skill>;
|
|
20
21
|
abortSignal?: AbortSignal;
|
|
21
22
|
skillNamePattern?: RegExp;
|
|
@@ -57,7 +58,8 @@ type SkillsManifest = {
|
|
|
57
58
|
|
|
58
59
|
export async function writeSkills({
|
|
59
60
|
sandbox,
|
|
60
|
-
|
|
61
|
+
homePath,
|
|
62
|
+
skillsDir,
|
|
61
63
|
skills,
|
|
62
64
|
abortSignal,
|
|
63
65
|
skillNamePattern = /^[A-Za-z0-9._-]+$/,
|
|
@@ -67,6 +69,7 @@ export async function writeSkills({
|
|
|
67
69
|
`Invalid skill file path for ${skillName}: ${filePath}`,
|
|
68
70
|
trailingNewline = false,
|
|
69
71
|
}: WriteSkillsOptions): Promise<WriteSkillsResult> {
|
|
72
|
+
const rootDir = resolveSkillsRootDir({ homePath, skillsDir });
|
|
70
73
|
const projectedSkills = skills
|
|
71
74
|
.map(skill =>
|
|
72
75
|
projectSkill({
|
|
@@ -526,3 +529,57 @@ function renderSkillFile({
|
|
|
526
529
|
const content = `---\nname: ${skill.name}\ndescription: ${skill.description}\n---\n\n${skill.content}`;
|
|
527
530
|
return trailingNewline ? `${content}\n` : content;
|
|
528
531
|
}
|
|
532
|
+
|
|
533
|
+
function resolveSkillsRootDir({
|
|
534
|
+
homePath,
|
|
535
|
+
skillsDir,
|
|
536
|
+
}: {
|
|
537
|
+
homePath: string;
|
|
538
|
+
skillsDir: string;
|
|
539
|
+
}): string {
|
|
540
|
+
if (typeof homePath !== 'string' || homePath.trim().length === 0) {
|
|
541
|
+
throw new Error('Invalid homePath: expected a non-empty string.');
|
|
542
|
+
}
|
|
543
|
+
if (!path.posix.isAbsolute(homePath)) {
|
|
544
|
+
throw new Error(
|
|
545
|
+
`Invalid homePath ${JSON.stringify(homePath)}: expected an absolute POSIX path.`,
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
if (typeof skillsDir !== 'string' || skillsDir.trim().length === 0) {
|
|
549
|
+
throw new Error(
|
|
550
|
+
`Invalid skillsDir ${JSON.stringify(skillsDir)}: expected a relative POSIX path without traversal.`,
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
const containsTraversal = skillsDir
|
|
554
|
+
.split(/[\\\/]/)
|
|
555
|
+
.some(segment => segment === '..');
|
|
556
|
+
const normalizedSkillsDir = path.posix.normalize(skillsDir.trim());
|
|
557
|
+
const normalizedNoTrailingSlash = normalizedSkillsDir.replace(/\/+$/, '');
|
|
558
|
+
if (
|
|
559
|
+
skillsDir.includes('\\') ||
|
|
560
|
+
path.posix.isAbsolute(skillsDir) ||
|
|
561
|
+
path.win32.isAbsolute(skillsDir) ||
|
|
562
|
+
containsTraversal ||
|
|
563
|
+
normalizedNoTrailingSlash === '' ||
|
|
564
|
+
normalizedNoTrailingSlash === '.' ||
|
|
565
|
+
normalizedSkillsDir.startsWith('../') ||
|
|
566
|
+
normalizedSkillsDir.includes('/../') ||
|
|
567
|
+
normalizedSkillsDir.endsWith('/..')
|
|
568
|
+
) {
|
|
569
|
+
throw new Error(
|
|
570
|
+
`Invalid skillsDir ${JSON.stringify(skillsDir)}: expected a relative POSIX path without traversal.`,
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
const rootDir = path.posix.join(homePath, normalizedNoTrailingSlash);
|
|
574
|
+
const relative = path.posix.relative(homePath, rootDir);
|
|
575
|
+
if (
|
|
576
|
+
relative === '' ||
|
|
577
|
+
relative.startsWith('..') ||
|
|
578
|
+
path.posix.isAbsolute(relative)
|
|
579
|
+
) {
|
|
580
|
+
throw new Error(
|
|
581
|
+
`Invalid skillsDir ${JSON.stringify(skillsDir)}: must be a subpath within homePath ${JSON.stringify(homePath)}.`,
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
return rootDir;
|
|
585
|
+
}
|
|
@@ -21,6 +21,11 @@ import type { HarnessV1BuiltinToolFiltering } from './harness-v1-tool-filtering'
|
|
|
21
21
|
* calling the adapter, so adapters never need to derive provider-specific paths.
|
|
22
22
|
*/
|
|
23
23
|
export type HarnessV1StartOptions = {
|
|
24
|
+
/**
|
|
25
|
+
* Additional normalized HTTP headers to send with model requests.
|
|
26
|
+
*/
|
|
27
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
28
|
+
|
|
24
29
|
/**
|
|
25
30
|
* Stable identifier for this harness session. Used as the underlying
|
|
26
31
|
* resource name where the adapter has a notion of a named session
|