@ai-sdk/harness-pi 1.0.35 → 1.0.36
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 +9 -0
- package/dist/index.js +117 -21
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/pi-paths.ts +42 -9
- package/src/pi-remote-ops.ts +110 -14
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.36",
|
|
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.36",
|
|
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-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('; '),
|