@ai-sdk/harness-cline 1.0.7 → 1.0.9

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-cline",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -28,7 +28,7 @@
28
28
  "dependencies": {
29
29
  "@cline/agents": "^0.0.72",
30
30
  "@cline/core": "^0.0.72",
31
- "@ai-sdk/harness": "1.0.79",
31
+ "@ai-sdk/harness": "1.0.82",
32
32
  "@ai-sdk/provider": "4.0.7",
33
33
  "@ai-sdk/provider-utils": "5.0.28"
34
34
  },
@@ -14,6 +14,7 @@ export interface ClineRemoteOpsOptions {
14
14
  readonly sandbox: Experimental_SandboxSession;
15
15
  /** Absolute sandbox path of the session's working directory. */
16
16
  readonly workDir: string;
17
+ readonly readableRoots?: ReadonlyArray<{ readonly sandboxDir: string }>;
17
18
  }
18
19
 
19
20
  export interface ClineRemoteOps {
@@ -48,24 +49,75 @@ export function createClineRemoteOps(
48
49
  options: ClineRemoteOpsOptions,
49
50
  ): ClineRemoteOps {
50
51
  const { sandbox, workDir } = options;
52
+ const normalizedWorkDir = path.posix.normalize(workDir);
53
+ const readableRoots =
54
+ options.readableRoots?.map(root => path.posix.normalize(root.sandboxDir)) ??
55
+ [];
56
+
57
+ const isInsidePath = ({
58
+ parent,
59
+ candidate,
60
+ }: {
61
+ parent: string;
62
+ candidate: string;
63
+ }): boolean => {
64
+ const relative = path.posix.relative(parent, candidate);
65
+ return (
66
+ relative === '' ||
67
+ (relative !== '..' &&
68
+ !relative.startsWith('../') &&
69
+ !path.posix.isAbsolute(relative))
70
+ );
71
+ };
72
+
73
+ const assertWorkspacePath = (inputPath: string): string => {
74
+ const normalized = path.posix.normalize(inputPath);
75
+ if (!isInsidePath({ parent: normalizedWorkDir, candidate: normalized })) {
76
+ throw new Error(`Cline path escapes the workspace: ${inputPath}`);
77
+ }
78
+ return normalized;
79
+ };
80
+
81
+ const assertReadablePath = (inputPath: string): string => {
82
+ const normalized = path.posix.normalize(inputPath);
83
+ if (
84
+ !isInsidePath({ parent: normalizedWorkDir, candidate: normalized }) &&
85
+ !readableRoots.some(parent =>
86
+ isInsidePath({ parent, candidate: normalized }),
87
+ )
88
+ ) {
89
+ throw new Error(`Cline path escapes the readable roots: ${inputPath}`);
90
+ }
91
+ return normalized;
92
+ };
51
93
 
52
94
  const resolvePath = (inputPath: string): string => {
53
- const normalized = path.posix.isAbsolute(inputPath)
95
+ const resolved = path.posix.isAbsolute(inputPath)
54
96
  ? path.posix.normalize(inputPath)
55
- : path.posix.normalize(path.posix.join(workDir, inputPath));
56
- return normalized;
97
+ : path.posix.normalize(path.posix.join(normalizedWorkDir, inputPath));
98
+ return assertWorkspacePath(resolved);
57
99
  };
58
100
 
59
- const runShell = async (
60
- command: string,
61
- input?: { signal?: AbortSignal },
62
- ): Promise<{ exitCode: number; output: string }> => {
101
+ const resolveReadablePath = (inputPath: string): string => {
102
+ if (!path.posix.isAbsolute(inputPath)) {
103
+ return resolvePath(inputPath);
104
+ }
105
+ return assertReadablePath(inputPath);
106
+ };
107
+
108
+ const runShell = async ({
109
+ command,
110
+ signal,
111
+ }: {
112
+ command: string;
113
+ signal?: AbortSignal;
114
+ }): Promise<{ exitCode: number; output: string }> => {
63
115
  // `sandbox.run({ command })` already wraps in a shell; interpolated
64
116
  // paths/values inside `command` are quoted with `shellQuote` by callers.
65
117
  const result = await sandbox.run({
66
118
  command,
67
- workingDirectory: workDir,
68
- ...(input?.signal ? { abortSignal: input.signal } : {}),
119
+ workingDirectory: normalizedWorkDir,
120
+ ...(signal ? { abortSignal: signal } : {}),
69
121
  });
70
122
  return {
71
123
  exitCode: result.exitCode,
@@ -73,8 +125,103 @@ export function createClineRemoteOps(
73
125
  };
74
126
  };
75
127
 
128
+ const lastOutputLine = (output: string): string | undefined =>
129
+ output.trim().split('\n').filter(Boolean).at(-1);
130
+
131
+ const resolveExistingSandboxPath = async ({
132
+ remotePath,
133
+ inputPath,
134
+ missingMessage,
135
+ }: {
136
+ remotePath: string;
137
+ inputPath: string;
138
+ missingMessage?: string;
139
+ }): Promise<string> => {
140
+ const result = await runShell({
141
+ command: [
142
+ `target=${shellQuote(remotePath)}`,
143
+ 'if [ ! -e "$target" ]; then echo "__CLINE_REALPATH_NOT_FOUND__"; exit 2; fi',
144
+ 'resolved=$(realpath "$target" 2>/dev/null) || { echo "__CLINE_REALPATH_FAILED__"; exit 3; }',
145
+ 'printf \'%s\\n\' "$resolved"',
146
+ ].join('; '),
147
+ });
148
+
149
+ if (result.output.includes('__CLINE_REALPATH_NOT_FOUND__')) {
150
+ throw new Error(missingMessage ?? `Path not found: ${inputPath}`);
151
+ }
152
+ if (
153
+ result.output.includes('__CLINE_REALPATH_FAILED__') ||
154
+ result.exitCode !== 0
155
+ ) {
156
+ throw new Error(`Unable to resolve path: ${inputPath}`);
157
+ }
158
+
159
+ const resolvedPath = lastOutputLine(result.output);
160
+ if (!resolvedPath) {
161
+ throw new Error(`Unable to resolve path: ${inputPath}`);
162
+ }
163
+ return resolvedPath;
164
+ };
165
+
166
+ const resolveReadableSandboxPath = async ({
167
+ remotePath,
168
+ inputPath,
169
+ missingMessage,
170
+ }: {
171
+ remotePath: string;
172
+ inputPath: string;
173
+ missingMessage?: string;
174
+ }): Promise<string> =>
175
+ assertReadablePath(
176
+ await resolveExistingSandboxPath({
177
+ remotePath,
178
+ inputPath,
179
+ ...(missingMessage ? { missingMessage } : {}),
180
+ }),
181
+ );
182
+
183
+ const resolveWritableSandboxPath = async ({
184
+ remotePath,
185
+ inputPath,
186
+ }: {
187
+ remotePath: string;
188
+ inputPath: string;
189
+ }): Promise<string> => {
190
+ const result = await runShell({
191
+ command: [
192
+ `target=${shellQuote(remotePath)}`,
193
+ 'if [ -e "$target" ] || [ -L "$target" ]; then resolved=$(realpath "$target" 2>/dev/null) || { echo "__CLINE_REALPATH_FAILED__"; exit 3; }; printf \'%s\\n\' "$resolved"; exit 0; fi',
194
+ 'dir=$(dirname "$target")',
195
+ 'base=$(basename "$target")',
196
+ 'missing="$base"',
197
+ 'while [ ! -e "$dir" ] && [ ! -L "$dir" ]; do parent=$(dirname "$dir"); if [ "$parent" = "$dir" ]; then echo "__CLINE_REALPATH_NOT_FOUND__"; exit 2; fi; missing="$(basename "$dir")/$missing"; dir="$parent"; done',
198
+ 'resolved_dir=$(realpath "$dir" 2>/dev/null) || { echo "__CLINE_REALPATH_FAILED__"; exit 3; }',
199
+ 'printf \'%s/%s\\n\' "$resolved_dir" "$missing"',
200
+ ].join('; '),
201
+ });
202
+
203
+ if (
204
+ result.output.includes('__CLINE_REALPATH_NOT_FOUND__') ||
205
+ result.output.includes('__CLINE_REALPATH_FAILED__') ||
206
+ result.exitCode !== 0
207
+ ) {
208
+ throw new Error(`Unable to resolve path: ${inputPath}`);
209
+ }
210
+
211
+ const resolvedPath = lastOutputLine(result.output);
212
+ if (!resolvedPath) {
213
+ throw new Error(`Unable to resolve path: ${inputPath}`);
214
+ }
215
+ return assertWorkspacePath(resolvedPath);
216
+ };
217
+
76
218
  const readFile = async (inputPath: string): Promise<string> => {
77
- const resolved = resolvePath(inputPath);
219
+ const remotePath = resolveReadablePath(inputPath);
220
+ const resolved = await resolveReadableSandboxPath({
221
+ remotePath,
222
+ inputPath,
223
+ missingMessage: `File not found: ${inputPath}`,
224
+ });
78
225
  const content = await sandbox.readTextFile({ path: resolved });
79
226
  if (content == null) {
80
227
  throw new Error(`File not found: ${inputPath}`);
@@ -86,7 +233,11 @@ export function createClineRemoteOps(
86
233
  inputPath: string,
87
234
  content: string,
88
235
  ): Promise<void> => {
89
- const resolved = resolvePath(inputPath);
236
+ const remotePath = resolvePath(inputPath);
237
+ const resolved = await resolveWritableSandboxPath({
238
+ remotePath,
239
+ inputPath,
240
+ });
90
241
  // `writeTextFile` creates parent directories recursively per the
91
242
  // SandboxSession contract, so no explicit mkdir is needed.
92
243
  await sandbox.writeTextFile({ path: resolved, content });
@@ -97,7 +248,18 @@ export function createClineRemoteOps(
97
248
  oldText: string,
98
249
  newText: string,
99
250
  ): Promise<void> => {
100
- const current = await readFile(inputPath);
251
+ const remotePath = resolvePath(inputPath);
252
+ const resolved = assertWorkspacePath(
253
+ await resolveExistingSandboxPath({
254
+ remotePath,
255
+ inputPath,
256
+ missingMessage: `File not found: ${inputPath}`,
257
+ }),
258
+ );
259
+ const current = await sandbox.readTextFile({ path: resolved });
260
+ if (current == null) {
261
+ throw new Error(`File not found: ${inputPath}`);
262
+ }
101
263
  const index = current.indexOf(oldText);
102
264
  if (index === -1) {
103
265
  throw new Error(`Text to replace was not found in ${inputPath}`);
@@ -128,7 +290,10 @@ export function createClineRemoteOps(
128
290
  forwardedSignal?.addEventListener('abort', onAbort, { once: true });
129
291
 
130
292
  try {
131
- const result = await runShell(command, { signal: controller.signal });
293
+ const result = await runShell({
294
+ command,
295
+ signal: controller.signal,
296
+ });
132
297
  return { output: result.output, exitCode: result.exitCode };
133
298
  } finally {
134
299
  forwardedSignal?.removeEventListener('abort', onAbort);
@@ -139,9 +304,14 @@ export function createClineRemoteOps(
139
304
  },
140
305
 
141
306
  async grep(pattern, input = {}) {
142
- const target = resolvePath(input.path ?? '.');
307
+ const inputPath = input.path ?? '.';
308
+ const remotePath = resolveReadablePath(inputPath);
309
+ const target = await resolveReadableSandboxPath({
310
+ remotePath,
311
+ inputPath,
312
+ });
143
313
  const flags = [
144
- '-R',
314
+ '-r',
145
315
  '-n',
146
316
  '--binary-files=without-match',
147
317
  ...(input.ignoreCase ? ['-i'] : []),
@@ -152,8 +322,8 @@ export function createClineRemoteOps(
152
322
  ...(input.glob ? ['--include', input.glob] : []),
153
323
  ];
154
324
  const limit = Math.max(1, input.limit ?? 100);
155
- const result = await runShell(
156
- [
325
+ const result = await runShell({
326
+ command: [
157
327
  `if [ ! -e ${shellQuote(
158
328
  target,
159
329
  )} ]; then echo "__CLINE_GREP_NOT_FOUND__"; exit 2; fi`,
@@ -161,7 +331,7 @@ export function createClineRemoteOps(
161
331
  pattern,
162
332
  )} ${shellQuote(target)} 2>/dev/null | head -n ${limit}`,
163
333
  ].join('; '),
164
- );
334
+ });
165
335
 
166
336
  const output = result.output.trim();
167
337
  if (output.includes('__CLINE_GREP_NOT_FOUND__')) {
@@ -171,9 +341,13 @@ export function createClineRemoteOps(
171
341
  },
172
342
 
173
343
  async glob(pattern, inputPath = '.', limit = 1_000) {
174
- const target = resolvePath(inputPath);
175
- const result = await runShell(
176
- [
344
+ const remotePath = resolveReadablePath(inputPath);
345
+ const target = await resolveReadableSandboxPath({
346
+ remotePath,
347
+ inputPath,
348
+ });
349
+ const result = await runShell({
350
+ command: [
177
351
  `if [ ! -e ${shellQuote(
178
352
  target,
179
353
  )} ]; then echo "__CLINE_FIND_NOT_FOUND__"; exit 2; fi`,
@@ -181,7 +355,7 @@ export function createClineRemoteOps(
181
355
  target,
182
356
  )} -type f -print; else printf '%s\\n' ${shellQuote(target)}; fi`,
183
357
  ].join('; '),
184
- );
358
+ });
185
359
 
186
360
  const output = result.output.trim();
187
361
  if (output.includes('__CLINE_FIND_NOT_FOUND__')) {
@@ -208,9 +382,13 @@ export function createClineRemoteOps(
208
382
  },
209
383
 
210
384
  async ls(inputPath = '.', limit = 500) {
211
- const target = resolvePath(inputPath);
212
- const result = await runShell(
213
- [
385
+ const remotePath = resolveReadablePath(inputPath);
386
+ const target = await resolveReadableSandboxPath({
387
+ remotePath,
388
+ inputPath,
389
+ });
390
+ const result = await runShell({
391
+ command: [
214
392
  `if [ ! -e ${shellQuote(
215
393
  target,
216
394
  )} ]; then echo "__CLINE_LS_NOT_FOUND__"; exit 2; fi`,
@@ -220,7 +398,7 @@ export function createClineRemoteOps(
220
398
  `cd ${shellQuote(target)}`,
221
399
  'ls -1Ap',
222
400
  ].join('; '),
223
- );
401
+ });
224
402
 
225
403
  const output = result.output.trim();
226
404
  if (output.includes('__CLINE_LS_NOT_FOUND__')) {
@@ -268,16 +268,12 @@ export async function createClineSession(
268
268
  input.builtinToolFiltering,
269
269
  );
270
270
 
271
- const ops = createClineRemoteOps({
272
- sandbox: toolSafeSandboxSession,
273
- workDir: input.sessionWorkDir,
274
- });
275
-
276
271
  // Materialize harness-provided skills into sandbox HOME (not the workspace)
277
272
  // and advertise them via a system prompt section.
278
273
  let skillsSection: string | undefined;
274
+ let skillRootDir: string | undefined;
279
275
  if (input.skills.length > 0) {
280
- const skillRootDir = await writeClineSkills({
276
+ skillRootDir = await writeClineSkills({
281
277
  sandbox: toolSafeSandboxSession,
282
278
  sandboxHomeDir,
283
279
  skills: input.skills,
@@ -288,6 +284,12 @@ export async function createClineSession(
288
284
  }
289
285
  }
290
286
 
287
+ const ops = createClineRemoteOps({
288
+ sandbox: toolSafeSandboxSession,
289
+ workDir: input.sessionWorkDir,
290
+ ...(skillRootDir ? { readableRoots: [{ sandboxDir: skillRootDir }] } : {}),
291
+ });
292
+
291
293
  const baseSystemPrompt = buildSystemPrompt({
292
294
  sessionWorkDir: input.sessionWorkDir,
293
295
  sandboxDescription: toolSafeSandboxSession.description,