@ai-sdk/harness-pi 1.0.3 → 1.0.5
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 +17 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +134 -92
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
- package/src/pi-events.ts +41 -47
- package/src/pi-harness.ts +1 -1
- package/src/pi-resume-state.ts +78 -19
- package/src/pi-session.ts +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @ai-sdk/harness-pi
|
|
2
2
|
|
|
3
|
+
## 1.0.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- b6d0025: fix(harness-pi): fix potential path traversal issues in Pi harness adapter
|
|
8
|
+
- 43a8c68: fix(harness): use `shellQuote` where appropriate in harness adapters
|
|
9
|
+
- ba6d510: chore: fix deprecated use of zod `.passthrough()`
|
|
10
|
+
- @ai-sdk/harness@1.0.5
|
|
11
|
+
|
|
12
|
+
## 1.0.4
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- c493634: fix(harness): fix harness Zod usage to be v3/v4 compatible
|
|
17
|
+
- Updated dependencies [c493634]
|
|
18
|
+
- @ai-sdk/harness@1.0.4
|
|
19
|
+
|
|
3
20
|
## 1.0.3
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -65,8 +65,8 @@ declare const PI_BUILTIN_TOOLS: {
|
|
|
65
65
|
}, unknown>;
|
|
66
66
|
readonly grep: HarnessV1BuiltinTool<{
|
|
67
67
|
pattern: string;
|
|
68
|
-
glob?: string | undefined;
|
|
69
68
|
path?: string | undefined;
|
|
69
|
+
glob?: string | undefined;
|
|
70
70
|
ignoreCase?: boolean | undefined;
|
|
71
71
|
literal?: boolean | undefined;
|
|
72
72
|
context?: number | undefined;
|
|
@@ -104,8 +104,8 @@ declare const pi: _ai_sdk_harness.HarnessV1<{
|
|
|
104
104
|
}, unknown>;
|
|
105
105
|
readonly grep: _ai_sdk_harness.HarnessV1BuiltinTool<{
|
|
106
106
|
pattern: string;
|
|
107
|
-
glob?: string | undefined;
|
|
108
107
|
path?: string | undefined;
|
|
108
|
+
glob?: string | undefined;
|
|
109
109
|
ignoreCase?: boolean | undefined;
|
|
110
110
|
literal?: boolean | undefined;
|
|
111
111
|
context?: number | undefined;
|
package/dist/index.js
CHANGED
|
@@ -3,26 +3,126 @@ import {
|
|
|
3
3
|
commonTool
|
|
4
4
|
} from "@ai-sdk/harness";
|
|
5
5
|
import { tool } from "@ai-sdk/provider-utils";
|
|
6
|
-
import { z as z3 } from "zod";
|
|
6
|
+
import { z as z3 } from "zod/v4";
|
|
7
7
|
|
|
8
8
|
// src/pi-resume-state.ts
|
|
9
9
|
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
10
10
|
import path from "path";
|
|
11
|
-
import { z } from "zod";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
import { z } from "zod/v4";
|
|
12
|
+
|
|
13
|
+
// src/pi-utils.ts
|
|
14
|
+
import {
|
|
15
|
+
HarnessCapabilityUnsupportedError
|
|
16
|
+
} from "@ai-sdk/harness";
|
|
17
|
+
var HARNESS_ID = "pi";
|
|
18
|
+
function extractUserText(prompt) {
|
|
19
|
+
if (typeof prompt === "string") {
|
|
20
|
+
return prompt;
|
|
21
|
+
}
|
|
22
|
+
const { content } = prompt;
|
|
23
|
+
if (typeof content === "string") {
|
|
24
|
+
return content;
|
|
25
|
+
}
|
|
26
|
+
const parts = [];
|
|
27
|
+
for (const part of content) {
|
|
28
|
+
if (part.type !== "text") {
|
|
29
|
+
throw new HarnessCapabilityUnsupportedError({
|
|
30
|
+
message: `pi: only text user-message parts are supported; got '${part.type}'.`,
|
|
31
|
+
harnessId: HARNESS_ID
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
parts.push(part.text);
|
|
35
|
+
}
|
|
36
|
+
return parts.join("\n\n");
|
|
37
|
+
}
|
|
38
|
+
function frameInstructions(instructions, userText) {
|
|
39
|
+
return `<session-instructions>
|
|
40
|
+
The block below is operating guidance from the system, not a message from the user \u2014 follow it, but do not mention it or attribute it to the user.
|
|
41
|
+
|
|
42
|
+
${instructions}
|
|
43
|
+
</session-instructions>
|
|
44
|
+
|
|
45
|
+
<user-message>
|
|
46
|
+
${userText}
|
|
47
|
+
</user-message>`;
|
|
48
|
+
}
|
|
49
|
+
function shellQuote(value) {
|
|
50
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
51
|
+
}
|
|
52
|
+
function serializeToolOutput(output) {
|
|
53
|
+
if (typeof output === "string") {
|
|
54
|
+
return output;
|
|
55
|
+
}
|
|
56
|
+
const serialized = JSON.stringify(output);
|
|
57
|
+
return serialized ?? "null";
|
|
58
|
+
}
|
|
59
|
+
function safePiMetadataSegment(name, label) {
|
|
60
|
+
if (!/^[A-Za-z0-9._-]+$/.test(name) || name === "." || name === "..") {
|
|
61
|
+
throw new Error(`Invalid Pi ${label} name: ${name}`);
|
|
62
|
+
}
|
|
63
|
+
return name;
|
|
64
|
+
}
|
|
65
|
+
function renderPiSkillFile(skill) {
|
|
66
|
+
return `---
|
|
67
|
+
name: ${skill.name}
|
|
68
|
+
description: ${skill.description}
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
${skill.content}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/pi-resume-state.ts
|
|
75
|
+
var PI_SESSION_FILE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*\.jsonl?$/;
|
|
76
|
+
function safePiSessionFileName(sessionFileName) {
|
|
77
|
+
if (!PI_SESSION_FILE_NAME_PATTERN.test(sessionFileName)) {
|
|
78
|
+
throw new Error(`Invalid Pi session file name: ${sessionFileName}`);
|
|
79
|
+
}
|
|
80
|
+
return sessionFileName;
|
|
81
|
+
}
|
|
82
|
+
var piSessionFileNameSchema = z.string().refine(
|
|
83
|
+
(sessionFileName) => PI_SESSION_FILE_NAME_PATTERN.test(sessionFileName),
|
|
84
|
+
"Pi sessionFileName must be a safe .jsonl or .json basename."
|
|
85
|
+
);
|
|
86
|
+
var piResumeStateSchema = z.looseObject({
|
|
87
|
+
sessionFileName: piSessionFileNameSchema.optional()
|
|
88
|
+
});
|
|
15
89
|
var PI_SESSIONS_DIR = ".pi-sessions";
|
|
90
|
+
function resolveContainedHostPath(input) {
|
|
91
|
+
const baseDir = path.resolve(input.baseDir);
|
|
92
|
+
const filePath = path.resolve(
|
|
93
|
+
baseDir,
|
|
94
|
+
safePiSessionFileName(input.sessionFileName)
|
|
95
|
+
);
|
|
96
|
+
const relativePath = path.relative(baseDir, filePath);
|
|
97
|
+
if (relativePath === "" || relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
|
|
98
|
+
throw new Error(`Invalid Pi session file name: ${input.sessionFileName}`);
|
|
99
|
+
}
|
|
100
|
+
return filePath;
|
|
101
|
+
}
|
|
102
|
+
function resolveContainedSandboxPath(input) {
|
|
103
|
+
const sessionDir = path.posix.resolve(input.sessionWorkDir, PI_SESSIONS_DIR);
|
|
104
|
+
const filePath = path.posix.resolve(
|
|
105
|
+
sessionDir,
|
|
106
|
+
safePiSessionFileName(input.sessionFileName)
|
|
107
|
+
);
|
|
108
|
+
const relativePath = path.posix.relative(sessionDir, filePath);
|
|
109
|
+
if (relativePath === "" || relativePath.startsWith("..") || path.posix.isAbsolute(relativePath)) {
|
|
110
|
+
throw new Error(`Invalid Pi session file name: ${input.sessionFileName}`);
|
|
111
|
+
}
|
|
112
|
+
return filePath;
|
|
113
|
+
}
|
|
16
114
|
async function persistSessionFileToSandbox(args) {
|
|
17
|
-
const hostPath =
|
|
115
|
+
const hostPath = resolveContainedHostPath({
|
|
116
|
+
baseDir: args.hostSessionDir,
|
|
117
|
+
sessionFileName: args.sessionFileName
|
|
118
|
+
});
|
|
18
119
|
const content = await readFile(hostPath);
|
|
19
|
-
const remotePath =
|
|
20
|
-
args.sessionWorkDir,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
);
|
|
120
|
+
const remotePath = resolveContainedSandboxPath({
|
|
121
|
+
sessionWorkDir: args.sessionWorkDir,
|
|
122
|
+
sessionFileName: args.sessionFileName
|
|
123
|
+
});
|
|
24
124
|
await args.sandbox.run({
|
|
25
|
-
command: `mkdir -p ${path.posix.dirname(remotePath)}`,
|
|
125
|
+
command: `mkdir -p ${shellQuote(path.posix.dirname(remotePath))}`,
|
|
26
126
|
...args.abortSignal ? { abortSignal: args.abortSignal } : {}
|
|
27
127
|
});
|
|
28
128
|
await args.sandbox.writeBinaryFile({
|
|
@@ -32,18 +132,20 @@ async function persistSessionFileToSandbox(args) {
|
|
|
32
132
|
});
|
|
33
133
|
}
|
|
34
134
|
async function pullSessionFileFromSandbox(args) {
|
|
35
|
-
const remotePath =
|
|
36
|
-
args.sessionWorkDir,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
);
|
|
135
|
+
const remotePath = resolveContainedSandboxPath({
|
|
136
|
+
sessionWorkDir: args.sessionWorkDir,
|
|
137
|
+
sessionFileName: args.sessionFileName
|
|
138
|
+
});
|
|
40
139
|
const bytes = await args.sandbox.readBinaryFile({
|
|
41
140
|
path: remotePath,
|
|
42
141
|
...args.abortSignal ? { abortSignal: args.abortSignal } : {}
|
|
43
142
|
});
|
|
44
143
|
if (!bytes) return void 0;
|
|
45
144
|
await mkdir(args.hostSessionDir, { recursive: true });
|
|
46
|
-
const hostPath =
|
|
145
|
+
const hostPath = resolveContainedHostPath({
|
|
146
|
+
baseDir: args.hostSessionDir,
|
|
147
|
+
sessionFileName: args.sessionFileName
|
|
148
|
+
});
|
|
47
149
|
await writeFile(hostPath, bytes);
|
|
48
150
|
return hostPath;
|
|
49
151
|
}
|
|
@@ -171,13 +273,13 @@ function applyCustomEnv(customEnv, registries) {
|
|
|
171
273
|
}
|
|
172
274
|
|
|
173
275
|
// src/pi-events.ts
|
|
174
|
-
import { z as z2 } from "zod";
|
|
175
|
-
var piSessionEventSchema = z2.
|
|
276
|
+
import { z as z2 } from "zod/v4";
|
|
277
|
+
var piSessionEventSchema = z2.looseObject({
|
|
176
278
|
type: z2.string(),
|
|
177
|
-
assistantMessageEvent: z2.
|
|
279
|
+
assistantMessageEvent: z2.looseObject({
|
|
178
280
|
type: z2.string().optional(),
|
|
179
281
|
delta: z2.string().optional()
|
|
180
|
-
}).
|
|
282
|
+
}).optional(),
|
|
181
283
|
toolCallId: z2.string().optional(),
|
|
182
284
|
toolName: z2.string().optional(),
|
|
183
285
|
args: z2.unknown().optional(),
|
|
@@ -192,18 +294,18 @@ var piSessionEventSchema = z2.object({
|
|
|
192
294
|
aborted: z2.boolean().optional(),
|
|
193
295
|
error: z2.union([
|
|
194
296
|
z2.string(),
|
|
195
|
-
z2.
|
|
297
|
+
z2.looseObject({
|
|
196
298
|
errorMessage: z2.string().optional(),
|
|
197
299
|
stopReason: z2.string().optional()
|
|
198
|
-
})
|
|
300
|
+
})
|
|
199
301
|
]).optional(),
|
|
200
|
-
message: z2.
|
|
302
|
+
message: z2.looseObject({
|
|
201
303
|
role: z2.string().optional(),
|
|
202
304
|
content: z2.unknown().optional(),
|
|
203
305
|
stopReason: z2.string().optional(),
|
|
204
306
|
errorMessage: z2.string().optional()
|
|
205
|
-
}).
|
|
206
|
-
})
|
|
307
|
+
}).optional()
|
|
308
|
+
});
|
|
207
309
|
function parseNativeEvent(raw) {
|
|
208
310
|
const parsed = piSessionEventSchema.safeParse(raw);
|
|
209
311
|
return parsed.success ? parsed.data : void 0;
|
|
@@ -358,69 +460,6 @@ function createPiPathMapper(options) {
|
|
|
358
460
|
|
|
359
461
|
// src/pi-remote-ops.ts
|
|
360
462
|
import path3 from "path";
|
|
361
|
-
|
|
362
|
-
// src/pi-utils.ts
|
|
363
|
-
import {
|
|
364
|
-
HarnessCapabilityUnsupportedError
|
|
365
|
-
} from "@ai-sdk/harness";
|
|
366
|
-
var HARNESS_ID = "pi";
|
|
367
|
-
function extractUserText(prompt) {
|
|
368
|
-
if (typeof prompt === "string") {
|
|
369
|
-
return prompt;
|
|
370
|
-
}
|
|
371
|
-
const { content } = prompt;
|
|
372
|
-
if (typeof content === "string") {
|
|
373
|
-
return content;
|
|
374
|
-
}
|
|
375
|
-
const parts = [];
|
|
376
|
-
for (const part of content) {
|
|
377
|
-
if (part.type !== "text") {
|
|
378
|
-
throw new HarnessCapabilityUnsupportedError({
|
|
379
|
-
message: `pi: only text user-message parts are supported; got '${part.type}'.`,
|
|
380
|
-
harnessId: HARNESS_ID
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
parts.push(part.text);
|
|
384
|
-
}
|
|
385
|
-
return parts.join("\n\n");
|
|
386
|
-
}
|
|
387
|
-
function frameInstructions(instructions, userText) {
|
|
388
|
-
return `<session-instructions>
|
|
389
|
-
The block below is operating guidance from the system, not a message from the user \u2014 follow it, but do not mention it or attribute it to the user.
|
|
390
|
-
|
|
391
|
-
${instructions}
|
|
392
|
-
</session-instructions>
|
|
393
|
-
|
|
394
|
-
<user-message>
|
|
395
|
-
${userText}
|
|
396
|
-
</user-message>`;
|
|
397
|
-
}
|
|
398
|
-
function shellQuote(value) {
|
|
399
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
400
|
-
}
|
|
401
|
-
function serializeToolOutput(output) {
|
|
402
|
-
if (typeof output === "string") {
|
|
403
|
-
return output;
|
|
404
|
-
}
|
|
405
|
-
const serialized = JSON.stringify(output);
|
|
406
|
-
return serialized ?? "null";
|
|
407
|
-
}
|
|
408
|
-
function safePiMetadataSegment(name, label) {
|
|
409
|
-
if (!/^[A-Za-z0-9._-]+$/.test(name) || name === "." || name === "..") {
|
|
410
|
-
throw new Error(`Invalid Pi ${label} name: ${name}`);
|
|
411
|
-
}
|
|
412
|
-
return name;
|
|
413
|
-
}
|
|
414
|
-
function renderPiSkillFile(skill) {
|
|
415
|
-
return `---
|
|
416
|
-
name: ${skill.name}
|
|
417
|
-
description: ${skill.description}
|
|
418
|
-
---
|
|
419
|
-
|
|
420
|
-
${skill.content}`;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// src/pi-remote-ops.ts
|
|
424
463
|
function createPiRemoteOps(options) {
|
|
425
464
|
const runShell = async (command, input = {}) => {
|
|
426
465
|
const result = await options.sandbox.run({
|
|
@@ -1402,11 +1441,14 @@ async function createPiSession(input) {
|
|
|
1402
1441
|
}
|
|
1403
1442
|
let resumeSessionFilePath;
|
|
1404
1443
|
if (input.isResume && input.resumeSessionFileName) {
|
|
1444
|
+
const resumeSessionFileName = safePiSessionFileName(
|
|
1445
|
+
input.resumeSessionFileName
|
|
1446
|
+
);
|
|
1405
1447
|
resumeSessionFilePath = await pullSessionFileFromSandbox({
|
|
1406
1448
|
sandbox,
|
|
1407
1449
|
sessionWorkDir: input.sessionWorkDir,
|
|
1408
1450
|
hostSessionDir,
|
|
1409
|
-
sessionFileName:
|
|
1451
|
+
sessionFileName: resumeSessionFileName,
|
|
1410
1452
|
...input.abortSignal ? { abortSignal: input.abortSignal } : {}
|
|
1411
1453
|
});
|
|
1412
1454
|
}
|
|
@@ -1612,7 +1654,7 @@ async function createPiSession(input) {
|
|
|
1612
1654
|
piSession = session;
|
|
1613
1655
|
const candidatePath = sessionManager.getSessionFile();
|
|
1614
1656
|
if (candidatePath) {
|
|
1615
|
-
sessionFileName = path7.basename(candidatePath);
|
|
1657
|
+
sessionFileName = safePiSessionFileName(path7.basename(candidatePath));
|
|
1616
1658
|
}
|
|
1617
1659
|
translatorState = createPiTranslatorState({
|
|
1618
1660
|
builtinToolNames: builtinNames,
|