@ai-sdk/harness-pi 1.0.0-canary.2 → 1.0.0-canary.4

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,26 @@
1
1
  # @ai-sdk/harness-pi
2
2
 
3
+ ## 1.0.0-canary.4
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [aae0138]
8
+ - @ai-sdk/harness@1.0.0-canary.8
9
+
10
+ ## 1.0.0-canary.3
11
+
12
+ ### Patch Changes
13
+
14
+ - 1ea15a3: fix(harness): fix various bugs with harness skills not being correctly processed by the harness adapters
15
+ - e551763: fix(harness): avoid using peer dependencies for underlying harness and sandbox SDKs
16
+ - Updated dependencies [3d87086]
17
+ - Updated dependencies [aeda373]
18
+ - Updated dependencies [1ea15a3]
19
+ - Updated dependencies [375fdd7]
20
+ - Updated dependencies [b4507d5]
21
+ - @ai-sdk/harness@1.0.0-canary.7
22
+ - @ai-sdk/provider-utils@5.0.0-canary.48
23
+
3
24
  ## 1.0.0-canary.2
4
25
 
5
26
  ### Patch Changes
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  ## Setup
6
6
 
7
7
  ```bash
8
- npm i @ai-sdk/harness-pi @ai-sdk/harness @ai-sdk/sandbox-vercel @earendil-works/pi-coding-agent
8
+ npm i @ai-sdk/harness-pi @ai-sdk/harness @ai-sdk/sandbox-vercel
9
9
  ```
10
10
 
11
11
  ## Usage
package/dist/index.js CHANGED
@@ -298,27 +298,44 @@ function canonicalizeForContainment(inputPath) {
298
298
  );
299
299
  }
300
300
  }
301
- function createPiPathMapper(hostWorkDir, sandboxWorkDir) {
302
- const normalizedHost = path2.resolve(hostWorkDir);
303
- const normalizedSandbox = path2.posix.normalize(sandboxWorkDir);
301
+ function createPiPathMapper(options) {
302
+ const normalizedHost = path2.resolve(options.hostWorkDir);
303
+ const normalizedSandbox = path2.posix.normalize(options.sandboxWorkDir);
304
304
  const canonicalHost = canonicalizeForContainment(normalizedHost);
305
+ const readableRoots = options.readableRoots?.map((root) => ({
306
+ sandboxDir: path2.posix.normalize(root.sandboxDir)
307
+ })) ?? [];
308
+ const toWorkspaceSandboxPath = (inputPath) => {
309
+ if (path2.posix.isAbsolute(inputPath)) {
310
+ const normalizedInput = path2.posix.normalize(inputPath);
311
+ if (isInsidePosixPath(normalizedSandbox, normalizedInput)) {
312
+ return normalizedInput;
313
+ }
314
+ }
315
+ const resolvedHost = path2.isAbsolute(inputPath) ? path2.resolve(inputPath) : path2.resolve(normalizedHost, inputPath);
316
+ const canonicalResolvedHost = canonicalizeForContainment(resolvedHost);
317
+ if (!isInsidePath(normalizedHost, resolvedHost) || !isInsidePath(canonicalHost, canonicalResolvedHost)) {
318
+ throw new Error(`Pi path escapes the workspace: ${inputPath}`);
319
+ }
320
+ const relative = path2.relative(normalizedHost, resolvedHost).split(path2.sep).join("/");
321
+ return relative ? path2.posix.join(normalizedSandbox, relative) : normalizedSandbox;
322
+ };
305
323
  return {
306
324
  hostWorkDir: normalizedHost,
307
325
  sandboxWorkDir: normalizedSandbox,
308
326
  toSandboxPath(inputPath) {
327
+ return toWorkspaceSandboxPath(inputPath);
328
+ },
329
+ toReadableSandboxPath(inputPath) {
309
330
  if (path2.posix.isAbsolute(inputPath)) {
310
331
  const normalizedInput = path2.posix.normalize(inputPath);
311
- if (isInsidePosixPath(normalizedSandbox, normalizedInput)) {
332
+ if (isInsidePosixPath(normalizedSandbox, normalizedInput) || readableRoots.some(
333
+ (root) => isInsidePosixPath(root.sandboxDir, normalizedInput)
334
+ )) {
312
335
  return normalizedInput;
313
336
  }
314
337
  }
315
- const resolvedHost = path2.isAbsolute(inputPath) ? path2.resolve(inputPath) : path2.resolve(normalizedHost, inputPath);
316
- const canonicalResolvedHost = canonicalizeForContainment(resolvedHost);
317
- if (!isInsidePath(normalizedHost, resolvedHost) || !isInsidePath(canonicalHost, canonicalResolvedHost)) {
318
- throw new Error(`Pi path escapes the workspace: ${inputPath}`);
319
- }
320
- const relative = path2.relative(normalizedHost, resolvedHost).split(path2.sep).join("/");
321
- return relative ? path2.posix.join(normalizedSandbox, relative) : normalizedSandbox;
338
+ return toWorkspaceSandboxPath(inputPath);
322
339
  },
323
340
  toRelativePath(inputPath) {
324
341
  const sandboxPath = path2.posix.isAbsolute(inputPath) ? path2.posix.normalize(inputPath) : path2.posix.join(
@@ -416,7 +433,7 @@ function createPiRemoteOps(options) {
416
433
  };
417
434
  const readBuffer = async (inputPath) => {
418
435
  const bytes = await options.sandbox.readBinaryFile({
419
- path: options.paths.toSandboxPath(inputPath)
436
+ path: options.paths.toReadableSandboxPath(inputPath)
420
437
  });
421
438
  if (!bytes) {
422
439
  throw new Error(`Path not found: ${inputPath}`);
@@ -447,7 +464,7 @@ function createPiRemoteOps(options) {
447
464
  return updated;
448
465
  };
449
466
  const listDirectory = async (inputPath = ".", limit = 500) => {
450
- const remotePath = options.paths.toSandboxPath(inputPath);
467
+ const remotePath = options.paths.toReadableSandboxPath(inputPath);
451
468
  const result = await runShell(
452
469
  [
453
470
  `if [ ! -e ${shellQuote(remotePath)} ]; then echo "__PI_LS_NOT_FOUND__"; exit 2; fi`,
@@ -468,7 +485,7 @@ function createPiRemoteOps(options) {
468
485
  ).slice(0, limit);
469
486
  };
470
487
  const findFiles = async (pattern, inputPath = ".", limit = 1e3) => {
471
- const remotePath = options.paths.toSandboxPath(inputPath);
488
+ const remotePath = options.paths.toReadableSandboxPath(inputPath);
472
489
  const result = await runShell(
473
490
  [
474
491
  `if [ ! -e ${shellQuote(remotePath)} ]; then echo "__PI_FIND_NOT_FOUND__"; exit 2; fi`,
@@ -492,8 +509,9 @@ function createPiRemoteOps(options) {
492
509
  ).slice(0, limit);
493
510
  };
494
511
  const grepFiles = async (pattern, input) => {
495
- const remotePath = options.paths.toSandboxPath(input.path ?? ".");
512
+ const remotePath = options.paths.toReadableSandboxPath(input.path ?? ".");
496
513
  const relativeTarget = options.paths.toRelativePath(remotePath);
514
+ const targetPath = relativeTarget.startsWith("../") || path3.posix.isAbsolute(relativeTarget) ? remotePath : relativeTarget;
497
515
  const flags = [
498
516
  "-R",
499
517
  "-n",
@@ -508,7 +526,7 @@ function createPiRemoteOps(options) {
508
526
  [
509
527
  `if [ ! -e ${shellQuote(remotePath)} ]; then echo "__PI_GREP_NOT_FOUND__"; exit 2; fi`,
510
528
  `cd ${shellQuote(options.paths.sandboxWorkDir)}`,
511
- `grep ${flags.map(shellQuote).join(" ")} -- ${shellQuote(pattern)} ${shellQuote(relativeTarget)} 2>/dev/null | head -n ${limit}`
529
+ `grep ${flags.map(shellQuote).join(" ")} -- ${shellQuote(pattern)} ${shellQuote(targetPath)} 2>/dev/null | head -n ${limit}`
512
530
  ].join("; ")
513
531
  );
514
532
  const output = result.output.toString("utf8").trim();
@@ -554,22 +572,49 @@ function createPiRemoteOps(options) {
554
572
  // src/pi-skills.ts
555
573
  import path4 from "path";
556
574
  async function writePiSkills(args) {
575
+ for (const skill of args.skills) {
576
+ safePiMetadataSegment(skill.name, "skill");
577
+ for (const file of skill.files ?? []) {
578
+ safePiSkillFilePath({ skillName: skill.name, filePath: file.path });
579
+ }
580
+ }
557
581
  for (const skill of args.skills) {
558
582
  const name = safePiMetadataSegment(skill.name, "skill");
559
- const remotePath = path4.posix.join(
560
- args.sessionWorkDir,
561
- ".pi",
583
+ const sandboxSkillDir = path4.posix.join(
584
+ args.sandboxHomeDir,
585
+ ".agents",
562
586
  "skills",
563
- name,
564
- "SKILL.md"
587
+ name
565
588
  );
589
+ const content = renderPiSkillFile(skill);
566
590
  await args.sandbox.writeTextFile({
567
- path: remotePath,
568
- content: renderPiSkillFile(skill),
591
+ path: path4.posix.join(sandboxSkillDir, "SKILL.md"),
592
+ content,
569
593
  ...args.abortSignal ? { abortSignal: args.abortSignal } : {}
570
594
  });
595
+ for (const file of skill.files ?? []) {
596
+ const filePath = safePiSkillFilePath({
597
+ skillName: skill.name,
598
+ filePath: file.path
599
+ });
600
+ await args.sandbox.writeTextFile({
601
+ path: path4.posix.join(sandboxSkillDir, filePath),
602
+ content: file.content,
603
+ ...args.abortSignal ? { abortSignal: args.abortSignal } : {}
604
+ });
605
+ }
571
606
  }
572
607
  }
608
+ function safePiSkillFilePath({
609
+ skillName,
610
+ filePath
611
+ }) {
612
+ const normalized = path4.posix.normalize(filePath);
613
+ if (normalized === "." || normalized.startsWith("../") || path4.posix.isAbsolute(normalized)) {
614
+ throw new Error(`Invalid Pi skill file path for ${skillName}: ${filePath}`);
615
+ }
616
+ return normalized;
617
+ }
573
618
 
574
619
  // src/pi-translate.ts
575
620
  import { randomBytes } from "crypto";
@@ -1238,12 +1283,52 @@ async function syncHostWorkspaceFromSandbox(args) {
1238
1283
  // src/pi-session.ts
1239
1284
  var HARNESS_ID2 = "pi";
1240
1285
  var parkedPiSessions = /* @__PURE__ */ new Map();
1286
+ function isWithinDirectory(parent, child) {
1287
+ const rel = path7.relative(parent, child);
1288
+ return rel === "" || !rel.startsWith("..") && !path7.isAbsolute(rel);
1289
+ }
1241
1290
  function isWithinWorkspace(candidate, sessionWorkDir, hostWorkDir) {
1242
- const inside = (parent, child) => {
1243
- const rel = path7.relative(parent, child);
1244
- return rel === "" || !rel.startsWith("..") && !path7.isAbsolute(rel);
1245
- };
1246
- return inside(sessionWorkDir, candidate) || inside(hostWorkDir, candidate);
1291
+ return isWithinDirectory(sessionWorkDir, candidate) || isWithinDirectory(hostWorkDir, candidate);
1292
+ }
1293
+ function createHarnessPiSkills({
1294
+ skills,
1295
+ sandboxSkillRootDir
1296
+ }) {
1297
+ return skills.map((skill) => {
1298
+ const name = safePiMetadataSegment(skill.name, "skill");
1299
+ const baseDir = path7.posix.join(sandboxSkillRootDir, name);
1300
+ const filePath = path7.posix.join(baseDir, "SKILL.md");
1301
+ return {
1302
+ name: skill.name,
1303
+ description: skill.description,
1304
+ filePath,
1305
+ baseDir,
1306
+ sourceInfo: {
1307
+ path: filePath,
1308
+ source: "harness",
1309
+ scope: "temporary",
1310
+ origin: "top-level",
1311
+ baseDir
1312
+ },
1313
+ disableModelInvocation: false
1314
+ };
1315
+ });
1316
+ }
1317
+ async function resolveSandboxHomeDir({
1318
+ sandbox,
1319
+ abortSignal
1320
+ }) {
1321
+ const result = await sandbox.run({
1322
+ command: 'printf "%s" "$HOME"',
1323
+ ...abortSignal ? { abortSignal } : {}
1324
+ });
1325
+ const homeDir = result.stdout.trim();
1326
+ if (result.exitCode !== 0 || !homeDir || !path7.posix.isAbsolute(homeDir)) {
1327
+ throw new Error(
1328
+ `Unable to resolve sandbox HOME directory: ${result.stderr || result.stdout}`
1329
+ );
1330
+ }
1331
+ return homeDir;
1247
1332
  }
1248
1333
  var PI_NATIVE_BUILTIN_NAMES = [
1249
1334
  "read",
@@ -1288,10 +1373,21 @@ async function createPiSession(input) {
1288
1373
  await mkdir3(hostSessionDir, { recursive: true });
1289
1374
  const sandbox = input.sandboxSession.restricted();
1290
1375
  const permissionMode = input.permissionMode ?? "allow-all";
1376
+ let sandboxSkillRootDir;
1377
+ let harnessSkills = [];
1291
1378
  if (input.skills.length > 0) {
1379
+ const sandboxHomeDir = await resolveSandboxHomeDir({
1380
+ sandbox,
1381
+ ...input.abortSignal ? { abortSignal: input.abortSignal } : {}
1382
+ });
1383
+ sandboxSkillRootDir = path7.posix.join(sandboxHomeDir, ".agents", "skills");
1384
+ harnessSkills = createHarnessPiSkills({
1385
+ skills: input.skills,
1386
+ sandboxSkillRootDir
1387
+ });
1292
1388
  await writePiSkills({
1293
1389
  sandbox,
1294
- sessionWorkDir: input.sessionWorkDir,
1390
+ sandboxHomeDir,
1295
1391
  skills: input.skills,
1296
1392
  ...input.abortSignal ? { abortSignal: input.abortSignal } : {}
1297
1393
  });
@@ -1313,7 +1409,11 @@ async function createPiSession(input) {
1313
1409
  });
1314
1410
  const workspaceVfs = new PiWorkspaceVfs();
1315
1411
  workspaceVfs.mount(hostWorkDir, sessionWorkDir);
1316
- const paths = createPiPathMapper(hostWorkDir, sessionWorkDir);
1412
+ const paths = createPiPathMapper({
1413
+ hostWorkDir,
1414
+ sandboxWorkDir: sessionWorkDir,
1415
+ readableRoots: sandboxSkillRootDir ? [{ sandboxDir: sandboxSkillRootDir }] : []
1416
+ });
1317
1417
  const authStorage = AuthStorage.create(path7.join(hostAgentDir, "auth.json"));
1318
1418
  const modelRegistry = ModelRegistry.create(
1319
1419
  authStorage,
@@ -1337,16 +1437,19 @@ async function createPiSession(input) {
1337
1437
  // The harness does not expose extensions, themes, or prompt templates, so
1338
1438
  // disable those entirely — this also avoids loading and executing a host
1339
1439
  // developer's personal Pi extensions inside the server process. Skills are
1340
- // kept (the harness writes project skills into `.pi/skills`) but filtered
1341
- // to the workspace so host-home skills never reach the model.
1440
+ // kept but filtered to workspace project skills plus harness-provided
1441
+ // skills whose files live in sandbox HOME.
1342
1442
  noExtensions: true,
1343
1443
  noThemes: true,
1344
1444
  noPromptTemplates: true,
1345
1445
  skillsOverride: (base) => ({
1346
1446
  ...base,
1347
- skills: base.skills.filter(
1348
- (skill) => isWithinWorkspace(skill.filePath, sessionWorkDir, hostWorkDir)
1349
- )
1447
+ skills: [
1448
+ ...base.skills.filter(
1449
+ (skill) => isWithinWorkspace(skill.filePath, sessionWorkDir, hostWorkDir)
1450
+ ),
1451
+ ...harnessSkills
1452
+ ]
1350
1453
  })
1351
1454
  });
1352
1455
  await resourceLoader.reload();