@ai-sdk/harness-pi 1.0.35 → 1.0.37
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 +18 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +123 -25
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/pi-harness.ts +8 -0
- package/src/pi-paths.ts +42 -9
- package/src/pi-remote-ops.ts +110 -14
- package/src/pi-session.ts +16 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/harness-pi",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.37",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@earendil-works/pi-coding-agent": "^0.79.0",
|
|
30
30
|
"typebox": "^1.1.38",
|
|
31
|
-
"@ai-sdk/harness": "1.0.
|
|
32
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
31
|
+
"@ai-sdk/harness": "1.0.37",
|
|
32
|
+
"@ai-sdk/provider-utils": "5.0.11"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"zod": "^3.25.76 || ^4.1.8"
|
package/src/pi-harness.ts
CHANGED
|
@@ -33,6 +33,13 @@ export type PiHarnessSettings = {
|
|
|
33
33
|
* `thinkingLevel` option on `createAgentSession`.
|
|
34
34
|
*/
|
|
35
35
|
readonly thinkingLevel?: PiThinkingLevel;
|
|
36
|
+
/**
|
|
37
|
+
* Directory holding Pi's global agent config (auth.json, models.json,
|
|
38
|
+
* settings.json). When omitted, a per-session temp dir is used. Pass the
|
|
39
|
+
* user's agent dir (e.g. `~/.pi/agent/`) to reuse their CLI auth and
|
|
40
|
+
* model settings.
|
|
41
|
+
*/
|
|
42
|
+
readonly agentDir?: string;
|
|
36
43
|
};
|
|
37
44
|
|
|
38
45
|
const PI_BUILTIN_TOOLS = {
|
|
@@ -148,6 +155,7 @@ export function createPi(
|
|
|
148
155
|
...(startOpts.abortSignal
|
|
149
156
|
? { abortSignal: startOpts.abortSignal }
|
|
150
157
|
: {}),
|
|
158
|
+
...(settings.agentDir ? { agentDir: settings.agentDir } : {}),
|
|
151
159
|
});
|
|
152
160
|
},
|
|
153
161
|
};
|
package/src/pi-paths.ts
CHANGED
|
@@ -17,6 +17,13 @@ export interface PiPathMapper {
|
|
|
17
17
|
* allows explicitly configured sandbox roots such as `$HOME/.agents/skills`.
|
|
18
18
|
*/
|
|
19
19
|
toReadableSandboxPath(inputPath: string): string;
|
|
20
|
+
/** Verify that a sandbox-side path is still inside `sandboxWorkDir`. */
|
|
21
|
+
assertSandboxPath(inputPath: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Verify that a sandbox-side path is inside `sandboxWorkDir` or an
|
|
24
|
+
* explicitly configured readable root.
|
|
25
|
+
*/
|
|
26
|
+
assertReadableSandboxPath(inputPath: string): string;
|
|
20
27
|
/** Translate any path to its POSIX-relative form under `sandboxWorkDir`. */
|
|
21
28
|
toRelativePath(inputPath: string): string;
|
|
22
29
|
}
|
|
@@ -73,11 +80,34 @@ export function createPiPathMapper(
|
|
|
73
80
|
sandboxDir: path.posix.normalize(root.sandboxDir),
|
|
74
81
|
})) ?? [];
|
|
75
82
|
|
|
83
|
+
const assertWorkspaceSandboxPath = (inputPath: string): string => {
|
|
84
|
+
const normalizedInput = path.posix.normalize(inputPath);
|
|
85
|
+
if (!isInsidePosixPath(normalizedSandbox, normalizedInput)) {
|
|
86
|
+
throw new Error(`Pi path escapes the workspace: ${inputPath}`);
|
|
87
|
+
}
|
|
88
|
+
return normalizedInput;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const assertReadableSandboxPath = (inputPath: string): string => {
|
|
92
|
+
const normalizedInput = path.posix.normalize(inputPath);
|
|
93
|
+
if (
|
|
94
|
+
!isInsidePosixPath(normalizedSandbox, normalizedInput) &&
|
|
95
|
+
!readableRoots.some(root =>
|
|
96
|
+
isInsidePosixPath(root.sandboxDir, normalizedInput),
|
|
97
|
+
)
|
|
98
|
+
) {
|
|
99
|
+
throw new Error(`Pi path escapes the readable roots: ${inputPath}`);
|
|
100
|
+
}
|
|
101
|
+
return normalizedInput;
|
|
102
|
+
};
|
|
103
|
+
|
|
76
104
|
const toWorkspaceSandboxPath = (inputPath: string): string => {
|
|
77
105
|
if (path.posix.isAbsolute(inputPath)) {
|
|
78
106
|
const normalizedInput = path.posix.normalize(inputPath);
|
|
79
|
-
|
|
80
|
-
return normalizedInput;
|
|
107
|
+
try {
|
|
108
|
+
return assertWorkspaceSandboxPath(normalizedInput);
|
|
109
|
+
} catch {
|
|
110
|
+
// Absolute host paths are handled below.
|
|
81
111
|
}
|
|
82
112
|
}
|
|
83
113
|
|
|
@@ -110,18 +140,21 @@ export function createPiPathMapper(
|
|
|
110
140
|
toReadableSandboxPath(inputPath: string) {
|
|
111
141
|
if (path.posix.isAbsolute(inputPath)) {
|
|
112
142
|
const normalizedInput = path.posix.normalize(inputPath);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
)
|
|
118
|
-
) {
|
|
119
|
-
return normalizedInput;
|
|
143
|
+
try {
|
|
144
|
+
return assertReadableSandboxPath(normalizedInput);
|
|
145
|
+
} catch {
|
|
146
|
+
// Absolute host paths are handled by workspace mapping below.
|
|
120
147
|
}
|
|
121
148
|
}
|
|
122
149
|
|
|
123
150
|
return toWorkspaceSandboxPath(inputPath);
|
|
124
151
|
},
|
|
152
|
+
assertSandboxPath(inputPath: string) {
|
|
153
|
+
return assertWorkspaceSandboxPath(inputPath);
|
|
154
|
+
},
|
|
155
|
+
assertReadableSandboxPath(inputPath: string) {
|
|
156
|
+
return assertReadableSandboxPath(inputPath);
|
|
157
|
+
},
|
|
125
158
|
toRelativePath(inputPath: string) {
|
|
126
159
|
const sandboxPath = path.posix.isAbsolute(inputPath)
|
|
127
160
|
? path.posix.normalize(inputPath)
|
package/src/pi-remote-ops.ts
CHANGED
|
@@ -65,6 +65,10 @@ interface RunShellResult {
|
|
|
65
65
|
output: Buffer;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
function lastOutputLine(output: Buffer): string | undefined {
|
|
69
|
+
return output.toString('utf8').trim().split('\n').filter(Boolean).at(-1);
|
|
70
|
+
}
|
|
71
|
+
|
|
68
72
|
export function createPiRemoteOps(options: PiRemoteOpsOptions): PiRemoteOps {
|
|
69
73
|
const runShell = async (
|
|
70
74
|
command: string,
|
|
@@ -94,9 +98,83 @@ export function createPiRemoteOps(options: PiRemoteOpsOptions): PiRemoteOps {
|
|
|
94
98
|
};
|
|
95
99
|
};
|
|
96
100
|
|
|
101
|
+
const resolveExistingSandboxPath = async (
|
|
102
|
+
remotePath: string,
|
|
103
|
+
inputPath: string,
|
|
104
|
+
): Promise<string> => {
|
|
105
|
+
const result = await runShell(
|
|
106
|
+
[
|
|
107
|
+
`target=${shellQuote(remotePath)}`,
|
|
108
|
+
`if [ ! -e "$target" ]; then echo "__PI_REALPATH_NOT_FOUND__"; exit 2; fi`,
|
|
109
|
+
`resolved=$(realpath "$target" 2>/dev/null) || { echo "__PI_REALPATH_FAILED__"; exit 3; }`,
|
|
110
|
+
`printf '%s\\n' "$resolved"`,
|
|
111
|
+
].join('; '),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const output = result.output.toString('utf8');
|
|
115
|
+
if (output.includes('__PI_REALPATH_NOT_FOUND__')) {
|
|
116
|
+
throw new Error(`Path not found: ${inputPath}`);
|
|
117
|
+
}
|
|
118
|
+
if (output.includes('__PI_REALPATH_FAILED__') || result.exitCode !== 0) {
|
|
119
|
+
throw new Error(`Unable to resolve path: ${inputPath}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const resolvedPath = lastOutputLine(result.output);
|
|
123
|
+
if (!resolvedPath) {
|
|
124
|
+
throw new Error(`Unable to resolve path: ${inputPath}`);
|
|
125
|
+
}
|
|
126
|
+
return resolvedPath;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const resolveReadableSandboxPath = async (
|
|
130
|
+
remotePath: string,
|
|
131
|
+
inputPath: string,
|
|
132
|
+
): Promise<string> =>
|
|
133
|
+
options.paths.assertReadableSandboxPath(
|
|
134
|
+
await resolveExistingSandboxPath(remotePath, inputPath),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const resolveWritableSandboxPath = async (
|
|
138
|
+
remotePath: string,
|
|
139
|
+
inputPath: string,
|
|
140
|
+
): Promise<string> => {
|
|
141
|
+
const result = await runShell(
|
|
142
|
+
[
|
|
143
|
+
`target=${shellQuote(remotePath)}`,
|
|
144
|
+
`if [ -e "$target" ] || [ -L "$target" ]; then resolved=$(realpath "$target" 2>/dev/null) || { echo "__PI_REALPATH_FAILED__"; exit 3; }; printf '%s\\n' "$resolved"; exit 0; fi`,
|
|
145
|
+
`dir=$(dirname "$target")`,
|
|
146
|
+
`base=$(basename "$target")`,
|
|
147
|
+
`missing="$base"`,
|
|
148
|
+
`while [ ! -e "$dir" ] && [ ! -L "$dir" ]; do parent=$(dirname "$dir"); if [ "$parent" = "$dir" ]; then echo "__PI_REALPATH_NOT_FOUND__"; exit 2; fi; missing="$(basename "$dir")/$missing"; dir="$parent"; done`,
|
|
149
|
+
`resolved_dir=$(realpath "$dir" 2>/dev/null) || { echo "__PI_REALPATH_FAILED__"; exit 3; }`,
|
|
150
|
+
`printf '%s/%s\\n' "$resolved_dir" "$missing"`,
|
|
151
|
+
].join('; '),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const output = result.output.toString('utf8');
|
|
155
|
+
if (
|
|
156
|
+
output.includes('__PI_REALPATH_NOT_FOUND__') ||
|
|
157
|
+
output.includes('__PI_REALPATH_FAILED__') ||
|
|
158
|
+
result.exitCode !== 0
|
|
159
|
+
) {
|
|
160
|
+
throw new Error(`Unable to resolve path: ${inputPath}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const resolvedPath = lastOutputLine(result.output);
|
|
164
|
+
if (!resolvedPath) {
|
|
165
|
+
throw new Error(`Unable to resolve path: ${inputPath}`);
|
|
166
|
+
}
|
|
167
|
+
return options.paths.assertSandboxPath(resolvedPath);
|
|
168
|
+
};
|
|
169
|
+
|
|
97
170
|
const readBuffer = async (inputPath: string): Promise<Buffer> => {
|
|
171
|
+
const remotePath = options.paths.toReadableSandboxPath(inputPath);
|
|
172
|
+
const resolvedPath = await resolveReadableSandboxPath(
|
|
173
|
+
remotePath,
|
|
174
|
+
inputPath,
|
|
175
|
+
);
|
|
98
176
|
const bytes = await options.sandbox.readBinaryFile({
|
|
99
|
-
path:
|
|
177
|
+
path: resolvedPath,
|
|
100
178
|
});
|
|
101
179
|
if (!bytes) {
|
|
102
180
|
throw new Error(`Path not found: ${inputPath}`);
|
|
@@ -109,12 +187,18 @@ export function createPiRemoteOps(options: PiRemoteOpsOptions): PiRemoteOps {
|
|
|
109
187
|
content: string,
|
|
110
188
|
): Promise<void> => {
|
|
111
189
|
const remotePath = options.paths.toSandboxPath(inputPath);
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
190
|
+
const resolvedPath = await resolveWritableSandboxPath(
|
|
191
|
+
remotePath,
|
|
192
|
+
inputPath,
|
|
193
|
+
);
|
|
194
|
+
const previous = await options.sandbox.readBinaryFile({
|
|
195
|
+
path: resolvedPath,
|
|
196
|
+
});
|
|
197
|
+
await runShell(`mkdir -p ${shellQuote(path.posix.dirname(resolvedPath))}`);
|
|
198
|
+
await options.sandbox.writeTextFile({ path: resolvedPath, content });
|
|
115
199
|
options.onFileChange?.(
|
|
116
200
|
previous ? 'modify' : 'create',
|
|
117
|
-
options.paths.toRelativePath(
|
|
201
|
+
options.paths.toRelativePath(resolvedPath),
|
|
118
202
|
Buffer.from(content, 'utf8'),
|
|
119
203
|
);
|
|
120
204
|
};
|
|
@@ -141,11 +225,15 @@ export function createPiRemoteOps(options: PiRemoteOpsOptions): PiRemoteOps {
|
|
|
141
225
|
limit: number = 500,
|
|
142
226
|
): Promise<string[]> => {
|
|
143
227
|
const remotePath = options.paths.toReadableSandboxPath(inputPath);
|
|
228
|
+
const resolvedPath = await resolveReadableSandboxPath(
|
|
229
|
+
remotePath,
|
|
230
|
+
inputPath,
|
|
231
|
+
);
|
|
144
232
|
const result = await runShell(
|
|
145
233
|
[
|
|
146
|
-
`if [ ! -e ${shellQuote(
|
|
147
|
-
`if [ ! -d ${shellQuote(
|
|
148
|
-
`cd ${shellQuote(
|
|
234
|
+
`if [ ! -e ${shellQuote(resolvedPath)} ]; then echo "__PI_LS_NOT_FOUND__"; exit 2; fi`,
|
|
235
|
+
`if [ ! -d ${shellQuote(resolvedPath)} ]; then echo "__PI_LS_NOT_DIR__"; exit 3; fi`,
|
|
236
|
+
`cd ${shellQuote(resolvedPath)}`,
|
|
149
237
|
'ls -1Ap',
|
|
150
238
|
].join('; '),
|
|
151
239
|
);
|
|
@@ -174,10 +262,14 @@ export function createPiRemoteOps(options: PiRemoteOpsOptions): PiRemoteOps {
|
|
|
174
262
|
limit: number = 1_000,
|
|
175
263
|
): Promise<string[]> => {
|
|
176
264
|
const remotePath = options.paths.toReadableSandboxPath(inputPath);
|
|
265
|
+
const resolvedPath = await resolveReadableSandboxPath(
|
|
266
|
+
remotePath,
|
|
267
|
+
inputPath,
|
|
268
|
+
);
|
|
177
269
|
const result = await runShell(
|
|
178
270
|
[
|
|
179
|
-
`if [ ! -e ${shellQuote(
|
|
180
|
-
`if [ -d ${shellQuote(
|
|
271
|
+
`if [ ! -e ${shellQuote(resolvedPath)} ]; then echo "__PI_FIND_NOT_FOUND__"; exit 2; fi`,
|
|
272
|
+
`if [ -d ${shellQuote(resolvedPath)} ]; then find ${shellQuote(resolvedPath)} -type f -print; else printf '%s\\n' ${shellQuote(resolvedPath)}; fi`,
|
|
181
273
|
].join('; '),
|
|
182
274
|
);
|
|
183
275
|
|
|
@@ -186,7 +278,7 @@ export function createPiRemoteOps(options: PiRemoteOpsOptions): PiRemoteOps {
|
|
|
186
278
|
throw new Error(`Path not found: ${inputPath}`);
|
|
187
279
|
}
|
|
188
280
|
|
|
189
|
-
const searchRoot =
|
|
281
|
+
const searchRoot = resolvedPath;
|
|
190
282
|
return output
|
|
191
283
|
.split('\n')
|
|
192
284
|
.filter(Boolean)
|
|
@@ -218,10 +310,14 @@ export function createPiRemoteOps(options: PiRemoteOpsOptions): PiRemoteOps {
|
|
|
218
310
|
},
|
|
219
311
|
): Promise<string> => {
|
|
220
312
|
const remotePath = options.paths.toReadableSandboxPath(input.path ?? '.');
|
|
221
|
-
const
|
|
313
|
+
const resolvedPath = await resolveReadableSandboxPath(
|
|
314
|
+
remotePath,
|
|
315
|
+
input.path ?? '.',
|
|
316
|
+
);
|
|
317
|
+
const relativeTarget = options.paths.toRelativePath(resolvedPath);
|
|
222
318
|
const targetPath =
|
|
223
319
|
relativeTarget.startsWith('../') || path.posix.isAbsolute(relativeTarget)
|
|
224
|
-
?
|
|
320
|
+
? resolvedPath
|
|
225
321
|
: relativeTarget;
|
|
226
322
|
const flags = [
|
|
227
323
|
'-R',
|
|
@@ -237,7 +333,7 @@ export function createPiRemoteOps(options: PiRemoteOpsOptions): PiRemoteOps {
|
|
|
237
333
|
const limit = Math.max(1, input.limit ?? 100);
|
|
238
334
|
const result = await runShell(
|
|
239
335
|
[
|
|
240
|
-
`if [ ! -e ${shellQuote(
|
|
336
|
+
`if [ ! -e ${shellQuote(resolvedPath)} ]; then echo "__PI_GREP_NOT_FOUND__"; exit 2; fi`,
|
|
241
337
|
`cd ${shellQuote(options.paths.sandboxWorkDir)}`,
|
|
242
338
|
`grep ${flags.map(shellQuote).join(' ')} -- ${shellQuote(pattern)} ${shellQuote(targetPath)} 2>/dev/null | head -n ${limit}`,
|
|
243
339
|
].join('; '),
|
package/src/pi-session.ts
CHANGED
|
@@ -202,6 +202,13 @@ export interface CreatePiSessionInput {
|
|
|
202
202
|
readonly builtinToolFiltering?: HarnessV1BuiltinToolFiltering;
|
|
203
203
|
readonly resumeSessionFileName?: string;
|
|
204
204
|
readonly abortSignal?: AbortSignal;
|
|
205
|
+
/**
|
|
206
|
+
* Directory holding Pi's global agent config (auth.json, models.json,
|
|
207
|
+
* settings.json). When omitted, a per-session temp dir is used (the
|
|
208
|
+
* harness cannot reuse existing CLI logins). Pass the user's agent dir
|
|
209
|
+
* (e.g. `~/.pi/agent/`) to reuse their CLI auth and model settings.
|
|
210
|
+
*/
|
|
211
|
+
readonly agentDir?: string;
|
|
205
212
|
}
|
|
206
213
|
|
|
207
214
|
interface PendingToolResult {
|
|
@@ -321,12 +328,18 @@ export async function createPiSession(
|
|
|
321
328
|
|
|
322
329
|
// Pi auth + model registry are global to this Pi session. These live on the
|
|
323
330
|
// real host filesystem (`hostAgentDir`), never in the sandbox/workspace.
|
|
324
|
-
|
|
331
|
+
// When `agentDir` is provided, use it instead so the harness can reuse
|
|
332
|
+
// existing CLI logins and model/settings config.
|
|
333
|
+
const agentDir = input.agentDir ?? hostAgentDir;
|
|
334
|
+
const authStorage = AuthStorage.create(path.join(agentDir, 'auth.json'));
|
|
325
335
|
const modelRegistry = ModelRegistry.create(
|
|
326
336
|
authStorage,
|
|
327
|
-
path.join(
|
|
337
|
+
path.join(agentDir, 'models.json'),
|
|
328
338
|
);
|
|
329
|
-
const settingsManager =
|
|
339
|
+
const settingsManager =
|
|
340
|
+
input.agentDir != null
|
|
341
|
+
? SettingsManager.create(hostWorkDir, agentDir)
|
|
342
|
+
: SettingsManager.inMemory();
|
|
330
343
|
|
|
331
344
|
// Run-scoped env (for the model resolver's gateway fallback heuristic).
|
|
332
345
|
const resolverEnv = resolvePiEnv({
|