@ai-sdk/harness-pi 1.0.103 → 1.0.105

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-pi",
3
- "version": "1.0.103",
3
+ "version": "1.0.105",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -26,8 +26,8 @@
26
26
  }
27
27
  },
28
28
  "dependencies": {
29
- "@ai-sdk/harness": "1.0.101",
30
- "@ai-sdk/provider-utils": "5.0.36",
29
+ "@ai-sdk/harness": "1.0.103",
30
+ "@ai-sdk/provider-utils": "5.0.37",
31
31
  "@earendil-works/pi-ai": "0.74.2",
32
32
  "@earendil-works/pi-coding-agent": "^0.84.3",
33
33
  "pi-mcp-adapter": "2.12.1",
@@ -37,7 +37,7 @@
37
37
  "zod": "^3.25.76 || ^4.1.8"
38
38
  },
39
39
  "devDependencies": {
40
- "@ai-sdk/sandbox-just-bash": "1.0.101",
40
+ "@ai-sdk/sandbox-just-bash": "1.0.103",
41
41
  "@types/node": "22.19.19",
42
42
  "@vercel/ai-tsconfig": "0.0.0",
43
43
  "tsup": "^8.5.1",
package/src/pi-auth.ts CHANGED
@@ -174,16 +174,19 @@ function createGatewayProviderConfig({
174
174
  apiKey,
175
175
  baseUrl,
176
176
  clientApp,
177
+ headers,
177
178
  }: {
178
179
  apiKey: string;
179
180
  baseUrl: string;
180
181
  clientApp: string;
182
+ headers?: Readonly<Record<string, string>>;
181
183
  }): ProviderConfigInput {
182
184
  return {
183
185
  apiKey,
184
186
  baseUrl,
185
187
  authHeader: true,
186
188
  headers: {
189
+ ...headers,
187
190
  'User-Agent': clientApp,
188
191
  'x-client-app': clientApp,
189
192
  },
@@ -311,11 +314,13 @@ export async function registerPiProviders({
311
314
  resolvedEnv,
312
315
  registries,
313
316
  clientApp = HARNESS_CLIENT_APP,
317
+ headers,
314
318
  }: {
315
319
  options: PiAuthenticationMode | undefined;
316
320
  resolvedEnv: Record<string, string>;
317
321
  registries: PiRegistries;
318
322
  clientApp?: string;
323
+ headers?: Readonly<Record<string, string>>;
319
324
  }): Promise<void> {
320
325
  const suppliedEnvironment = isHarnessAuthenticationEnvironment(options);
321
326
  const authenticationEnvironment = suppliedEnvironment ? options : process.env;
@@ -328,6 +333,7 @@ export async function registerPiProviders({
328
333
  customEnv: { ...pickOpenAIEnv(authenticationEnvironment), ...env },
329
334
  registries,
330
335
  clientApp,
336
+ headers,
331
337
  });
332
338
  return;
333
339
  }
@@ -337,6 +343,7 @@ export async function registerPiProviders({
337
343
  customEnv: { ...pickAnthropicEnv(authenticationEnvironment), ...env },
338
344
  registries,
339
345
  clientApp,
346
+ headers,
340
347
  });
341
348
  return;
342
349
  }
@@ -347,6 +354,7 @@ export async function registerPiProviders({
347
354
  customEnv: { ...pickProviderEnv(authenticationEnvironment), ...env },
348
355
  registries,
349
356
  clientApp,
357
+ headers,
350
358
  });
351
359
  return;
352
360
  }
@@ -367,6 +375,7 @@ export async function registerPiProviders({
367
375
  apiKey: gatewayApiKey,
368
376
  baseUrl: gatewayBaseUrl,
369
377
  clientApp,
378
+ headers,
370
379
  }),
371
380
  });
372
381
  return;
@@ -391,6 +400,7 @@ export async function registerPiProviders({
391
400
  apiKey: gatewayApiKey,
392
401
  baseUrl: gatewayBaseUrl,
393
402
  clientApp,
403
+ headers,
394
404
  }),
395
405
  });
396
406
  return;
@@ -400,6 +410,7 @@ export async function registerPiProviders({
400
410
  customEnv: { ...pickProviderEnv(authenticationEnvironment), ...env },
401
411
  registries,
402
412
  clientApp,
413
+ headers,
403
414
  });
404
415
  return;
405
416
  }
@@ -455,10 +466,12 @@ async function registerCustomProviders({
455
466
  customEnv,
456
467
  registries,
457
468
  clientApp,
469
+ headers,
458
470
  }: {
459
471
  customEnv: Record<string, string>;
460
472
  registries: PiRegistries;
461
473
  clientApp: string;
474
+ headers?: Readonly<Record<string, string>>;
462
475
  }): Promise<void> {
463
476
  const gatewayKey = customEnv.AI_GATEWAY_API_KEY;
464
477
  if (gatewayKey) {
@@ -471,6 +484,7 @@ async function registerCustomProviders({
471
484
  apiKey: gatewayKey,
472
485
  baseUrl,
473
486
  clientApp,
487
+ headers,
474
488
  }),
475
489
  });
476
490
  }
@@ -485,6 +499,7 @@ async function registerCustomProviders({
485
499
  apiKey: customEnv.OPENAI_API_KEY,
486
500
  baseUrl,
487
501
  authHeader: true,
502
+ ...(headers ? { headers } : {}),
488
503
  },
489
504
  });
490
505
  }
@@ -498,10 +513,15 @@ async function registerCustomProviders({
498
513
  config: {
499
514
  apiKey: customEnv.ANTHROPIC_API_KEY,
500
515
  baseUrl,
501
- ...(customEnv.ANTHROPIC_AUTH_TOKEN
516
+ ...(headers || customEnv.ANTHROPIC_AUTH_TOKEN
502
517
  ? {
503
518
  headers: {
504
- authorization: `Bearer ${customEnv.ANTHROPIC_AUTH_TOKEN}`,
519
+ ...headers,
520
+ ...(customEnv.ANTHROPIC_AUTH_TOKEN
521
+ ? {
522
+ authorization: `Bearer ${customEnv.ANTHROPIC_AUTH_TOKEN}`,
523
+ }
524
+ : {}),
505
525
  },
506
526
  }
507
527
  : {}),
@@ -534,6 +554,7 @@ async function registerCustomProviders({
534
554
  apiKey,
535
555
  baseUrl,
536
556
  authHeader: true,
557
+ ...(headers ? { headers } : {}),
537
558
  },
538
559
  });
539
560
  }
package/src/pi-harness.ts CHANGED
@@ -161,6 +161,7 @@ export function createPi(
161
161
  ...(settings.extensionFactories
162
162
  ? { extensionFactories: settings.extensionFactories }
163
163
  : {}),
164
+ ...(startOpts.headers ? { headers: startOpts.headers } : {}),
164
165
  },
165
166
  clientApp: PI_CLIENT_APP,
166
167
  isResume: lifecycleState != null,
package/src/pi-session.ts CHANGED
@@ -11,7 +11,8 @@ import {
11
11
  type Skill,
12
12
  type ToolDefinition,
13
13
  } from '@earendil-works/pi-coding-agent';
14
- import { mkdir, rm } from 'node:fs/promises';
14
+ import { randomUUID } from 'node:crypto';
15
+ import { mkdir, rm, writeFile } from 'node:fs/promises';
15
16
  import { tmpdir } from 'node:os';
16
17
  import path from 'node:path';
17
18
  import { Type } from 'typebox';
@@ -33,6 +34,7 @@ import {
33
34
  import {
34
35
  getRestrictedSandboxSession,
35
36
  resolveSandboxHomeDir,
37
+ writeSkills,
36
38
  } from '@ai-sdk/harness/utils';
37
39
  import type { Experimental_SandboxSession as SandboxSession } from '@ai-sdk/provider-utils';
38
40
  import {
@@ -45,7 +47,7 @@ import { getPiTerminalError, parseNativeEvent } from './pi-events';
45
47
  import { createPiModelResolver } from './pi-model-resolver';
46
48
  import { createPiPathMapper } from './pi-paths';
47
49
  import { createPiRemoteOps, type PiRemoteOps } from './pi-remote-ops';
48
- import { writePiSkills } from './pi-skills';
50
+
49
51
  import {
50
52
  persistSessionFileToSandbox,
51
53
  pullSessionFileFromSandbox,
@@ -217,6 +219,7 @@ export type PiThinkingLevel =
217
219
 
218
220
  export interface PiSessionSettings {
219
221
  readonly auth?: PiAuthenticationMode;
222
+ readonly headers?: Readonly<Record<string, string>>;
220
223
  readonly model?: string;
221
224
  readonly thinkingLevel?: PiThinkingLevel;
222
225
  readonly mcpServers?: Record<string, unknown>;
@@ -280,6 +283,36 @@ interface DeferredRerunBarrier {
280
283
  readonly cancel: (reason?: unknown) => void;
281
284
  }
282
285
 
286
+ async function isWorkspaceAvailableOnHost({
287
+ sandbox,
288
+ sessionWorkDir,
289
+ }: {
290
+ sandbox: SandboxSession;
291
+ sessionWorkDir: string;
292
+ }): Promise<boolean> {
293
+ // A host path existing at the same location is not enough to prove that it
294
+ // belongs to the sandbox. Round-trip a unique marker through the sandbox
295
+ // filesystem API before using the workspace directly.
296
+ const probePath = path.join(
297
+ sessionWorkDir,
298
+ `.ai-sdk-harness-pi-${randomUUID()}`,
299
+ );
300
+ const probeContent = randomUUID();
301
+
302
+ try {
303
+ await writeFile(probePath, probeContent, { flag: 'wx' });
304
+ const sandboxContent = await sandbox.readBinaryFile({ path: probePath });
305
+ return (
306
+ sandboxContent != null &&
307
+ Buffer.from(sandboxContent).equals(Buffer.from(probeContent))
308
+ );
309
+ } catch {
310
+ return false;
311
+ } finally {
312
+ await rm(probePath, { force: true }).catch(() => {});
313
+ }
314
+ }
315
+
283
316
  export async function createPiSession(
284
317
  input: CreatePiSessionInput,
285
318
  ): Promise<HarnessV1Session> {
@@ -299,28 +332,34 @@ export async function createPiSession(
299
332
  // sub-directory tree on disk.
300
333
  const safeSessionId = input.sessionId.replace(/[\\/: ]/g, '-');
301
334
  const hostRoot = path.join(tmpdir(), 'ai-sdk-harness', 'pi', safeSessionId);
302
- const hostWorkDir = path.join(hostRoot, 'workspace');
303
335
  const hostAgentDir = path.join(hostRoot, 'agent');
304
336
  const hostSessionDir = path.join(hostRoot, 'sessions');
337
+ const toolSafeSandboxSession = getRestrictedSandboxSession(
338
+ input.sandboxSession,
339
+ );
305
340
 
306
341
  // Pi runs in this host process but must behave as though it lives in the
307
342
  // sandbox workspace: its working directory is the real `sessionWorkDir`
308
343
  // (where `setup()` clones and where the sandbox-backed tools operate), so the
309
344
  // paths Pi advertises to the model — most notably the "Current working
310
- // directory" line in its system prompt — resolve inside the sandbox. The
311
- // workspace VFS maps that sandbox path to the host-side mirror so Pi's own
312
- // `fs`-based resource loading (`.pi/`, `AGENTS.md`) still works on the host.
313
- // `sessionWorkDir` is a sandbox path (e.g. `/vercel/sandbox/...`) that does
314
- // not exist on the host, so it is a safe, collision-free VFS mount point.
345
+ // directory" line in its system prompt — resolve inside the sandbox. When
346
+ // the sandbox filesystem is remote, the workspace VFS maps that sandbox path
347
+ // to a scoped host mirror for Pi's own `fs`-based resource loading. When the
348
+ // sandbox and harness share a filesystem, Pi uses the workspace directly so
349
+ // extensions can inspect project files beyond the scoped resource paths.
315
350
  const sessionWorkDir = input.sessionWorkDir;
351
+ const workspaceAvailableOnHost = await isWorkspaceAvailableOnHost({
352
+ sandbox: toolSafeSandboxSession,
353
+ sessionWorkDir,
354
+ });
355
+ const hostWorkDir = workspaceAvailableOnHost
356
+ ? path.resolve(sessionWorkDir)
357
+ : path.join(hostRoot, 'workspace');
316
358
 
317
359
  await mkdir(hostWorkDir, { recursive: true });
318
360
  await mkdir(hostAgentDir, { recursive: true });
319
361
  await mkdir(hostSessionDir, { recursive: true });
320
362
 
321
- const toolSafeSandboxSession = getRestrictedSandboxSession(
322
- input.sandboxSession,
323
- );
324
363
  const sandboxHomeDir = await resolveSandboxHomeDir({
325
364
  sandbox: toolSafeSandboxSession,
326
365
  ...(input.abortSignal ? { abortSignal: input.abortSignal } : {}),
@@ -356,11 +395,13 @@ export async function createPiSession(
356
395
 
357
396
  // Snapshot sandbox state into the host mirror BEFORE the VFS goes live so
358
397
  // Pi sees the workspace as soon as it boots.
359
- await syncHostWorkspaceFromSandbox({
360
- sandbox: toolSafeSandboxSession,
361
- sandboxWorkDir: input.sessionWorkDir,
362
- hostWorkDir,
363
- });
398
+ if (!workspaceAvailableOnHost) {
399
+ await syncHostWorkspaceFromSandbox({
400
+ sandbox: toolSafeSandboxSession,
401
+ sandboxWorkDir: input.sessionWorkDir,
402
+ hostWorkDir,
403
+ });
404
+ }
364
405
 
365
406
  // Mount only the workspace: the model's view of the workspace lives at
366
407
  // `sessionWorkDir` and is backed by `hostWorkDir`. The agent and session
@@ -368,7 +409,9 @@ export async function createPiSession(
368
409
  // Pi state (auth, model registry, session journal) that must never surface
369
410
  // in the sandbox or the workspace mirror.
370
411
  const workspaceVfs = new PiWorkspaceVfs();
371
- workspaceVfs.mount(hostWorkDir, sessionWorkDir);
412
+ if (!workspaceAvailableOnHost) {
413
+ workspaceVfs.mount(hostWorkDir, sessionWorkDir);
414
+ }
372
415
 
373
416
  const paths = createPiPathMapper({
374
417
  hostWorkDir,
@@ -410,6 +453,7 @@ export async function createPiSession(
410
453
  modelRuntime,
411
454
  },
412
455
  clientApp: input.clientApp,
456
+ headers: input.settings.headers,
413
457
  });
414
458
  const resolveModel = createPiModelResolver({
415
459
  modelRegistry,
@@ -1008,7 +1052,7 @@ export async function createPiSession(
1008
1052
  settingsManager,
1009
1053
  resourceLoader,
1010
1054
  customTools,
1011
- ...(hasMcpServers
1055
+ ...(hasExtensionFactories
1012
1056
  ? { noTools: 'builtin' as const }
1013
1057
  : { tools: toolNames }),
1014
1058
  ...(input.settings.thinkingLevel
@@ -1071,11 +1115,15 @@ export async function createPiSession(
1071
1115
  throw new Error('Pi session has been stopped.');
1072
1116
  }
1073
1117
 
1074
- const skillWriteResult = await writePiSkills({
1118
+ const skillWriteResult = await writeSkills({
1075
1119
  sandbox: toolSafeSandboxSession,
1076
- sandboxHomeDir,
1120
+ homePath: sandboxHomeDir,
1121
+ skillsDir: '.agents/skills',
1077
1122
  skills: turnOpts.skills,
1078
1123
  abortSignal: turnOpts.abortSignal,
1124
+ invalidSkillNameMessage: ({ name }) => `Invalid Pi skill name: ${name}`,
1125
+ invalidSkillFilePathMessage: ({ skillName, filePath }) =>
1126
+ `Invalid Pi skill file path for ${skillName}: ${filePath}`,
1079
1127
  });
1080
1128
  harnessSkills = createHarnessPiSkills({
1081
1129
  skills: turnOpts.skills,
@@ -1140,11 +1188,13 @@ export async function createPiSession(
1140
1188
  await reloadResourcesOnly();
1141
1189
  turnAbortController.signal.throwIfAborted();
1142
1190
  }
1143
- await syncHostWorkspaceFromSandbox({
1144
- sandbox: toolSafeSandboxSession,
1145
- sandboxWorkDir: input.sessionWorkDir,
1146
- hostWorkDir,
1147
- });
1191
+ if (!workspaceAvailableOnHost) {
1192
+ await syncHostWorkspaceFromSandbox({
1193
+ sandbox: toolSafeSandboxSession,
1194
+ sandboxWorkDir: input.sessionWorkDir,
1195
+ hostWorkDir,
1196
+ });
1197
+ }
1148
1198
  turnAbortController.signal.throwIfAborted();
1149
1199
 
1150
1200
  // Fresh translator state for the new turn — keep the tool sets the
package/src/pi-skills.ts DELETED
@@ -1,25 +0,0 @@
1
- import path from 'node:path';
2
- import type { HarnessV1Skill } from '@ai-sdk/harness';
3
- import { writeSkills, type WriteSkillsResult } from '@ai-sdk/harness/utils';
4
- import type { Experimental_SandboxSession } from '@ai-sdk/provider-utils';
5
-
6
- /**
7
- * Materialize Pi skills as files under
8
- * `$HOME/.agents/skills/<name>/SKILL.md` inside the sandbox.
9
- */
10
- export async function writePiSkills(args: {
11
- readonly sandbox: Experimental_SandboxSession;
12
- readonly sandboxHomeDir: string;
13
- readonly skills: ReadonlyArray<HarnessV1Skill>;
14
- readonly abortSignal?: AbortSignal;
15
- }): Promise<WriteSkillsResult> {
16
- return writeSkills({
17
- sandbox: args.sandbox,
18
- rootDir: path.posix.join(args.sandboxHomeDir, '.agents', 'skills'),
19
- skills: args.skills,
20
- abortSignal: args.abortSignal,
21
- invalidSkillNameMessage: ({ name }) => `Invalid Pi skill name: ${name}`,
22
- invalidSkillFilePathMessage: ({ skillName, filePath }) =>
23
- `Invalid Pi skill file path for ${skillName}: ${filePath}`,
24
- });
25
- }