@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 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
- var piResumeStateSchema = z.object({
13
- sessionFileName: z.string().optional()
14
- }).passthrough();
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 = path.join(args.hostSessionDir, args.sessionFileName);
115
+ const hostPath = resolveContainedHostPath({
116
+ baseDir: args.hostSessionDir,
117
+ sessionFileName: args.sessionFileName
118
+ });
18
119
  const content = await readFile(hostPath);
19
- const remotePath = path.posix.join(
20
- args.sessionWorkDir,
21
- PI_SESSIONS_DIR,
22
- args.sessionFileName
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 = path.posix.join(
36
- args.sessionWorkDir,
37
- PI_SESSIONS_DIR,
38
- args.sessionFileName
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 = path.join(args.hostSessionDir, args.sessionFileName);
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.object({
276
+ import { z as z2 } from "zod/v4";
277
+ var piSessionEventSchema = z2.looseObject({
176
278
  type: z2.string(),
177
- assistantMessageEvent: z2.object({
279
+ assistantMessageEvent: z2.looseObject({
178
280
  type: z2.string().optional(),
179
281
  delta: z2.string().optional()
180
- }).passthrough().optional(),
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.object({
297
+ z2.looseObject({
196
298
  errorMessage: z2.string().optional(),
197
299
  stopReason: z2.string().optional()
198
- }).passthrough()
300
+ })
199
301
  ]).optional(),
200
- message: z2.object({
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
- }).passthrough().optional()
206
- }).passthrough();
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: input.resumeSessionFileName,
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,