codeep 2.18.1 → 2.20.0
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/README.md +53 -16
- package/dist/acp/commands.js +11 -55
- package/dist/acp/protocol.d.ts +34 -0
- package/dist/acp/server.d.ts +6 -1
- package/dist/acp/server.js +97 -2
- package/dist/api/index.js +9 -0
- package/dist/commands/core/index.d.ts +19 -0
- package/dist/commands/core/index.js +28 -0
- package/dist/commands/core/keysync.d.ts +2 -0
- package/dist/commands/core/keysync.js +34 -0
- package/dist/commands/core/telemetry.d.ts +2 -0
- package/dist/commands/core/telemetry.js +34 -0
- package/dist/config/index.js +2 -2
- package/dist/config/providers.js +7 -4
- package/dist/renderer/App.d.ts +9 -48
- package/dist/renderer/App.js +113 -338
- package/dist/renderer/Screen.d.ts +13 -0
- package/dist/renderer/Screen.js +22 -0
- package/dist/renderer/commands/registry.js +3 -3
- package/dist/renderer/commands.js +19 -51
- package/dist/renderer/components/CommandAutocomplete.d.ts +46 -0
- package/dist/renderer/components/CommandAutocomplete.js +103 -0
- package/dist/renderer/components/HunkPicker.d.ts +48 -0
- package/dist/renderer/components/HunkPicker.js +140 -0
- package/dist/renderer/components/MentionPicker.d.ts +60 -0
- package/dist/renderer/components/MentionPicker.js +111 -0
- package/dist/renderer/components/PasteDialog.d.ts +43 -0
- package/dist/renderer/components/PasteDialog.js +70 -0
- package/dist/renderer/layout.js +1 -0
- package/dist/renderer/main.js +15 -39
- package/dist/utils/agent.js +121 -26
- package/dist/utils/agentChat.d.ts +11 -4
- package/dist/utils/agentChat.js +53 -25
- package/dist/utils/codeepCloud.d.ts +3 -0
- package/dist/utils/codeepCloud.js +62 -7
- package/dist/utils/personalities.d.ts +63 -5
- package/dist/utils/personalities.js +583 -31
- package/dist/utils/shell.d.ts +11 -1
- package/dist/utils/shell.js +169 -82
- package/dist/utils/ssrfGuard.d.ts +18 -0
- package/dist/utils/ssrfGuard.js +83 -0
- package/dist/utils/taskPlanner.d.ts +7 -1
- package/dist/utils/taskPlanner.js +16 -7
- package/dist/utils/tokenTracker.js +5 -3
- package/dist/utils/toolExecution.d.ts +1 -0
- package/dist/utils/toolExecution.js +48 -88
- package/dist/utils/tools.d.ts +3 -3
- package/dist/utils/tools.js +18 -13
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/utils/shell.d.ts
CHANGED
|
@@ -17,7 +17,8 @@ export interface CommandOptions {
|
|
|
17
17
|
projectRoot?: string;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
|
-
* Validate if a command is safe to execute
|
|
20
|
+
* Validate if a command is safe to execute (synchronous checks).
|
|
21
|
+
* See validateCommandAsync for the DNS-resolving SSRF checks.
|
|
21
22
|
*/
|
|
22
23
|
export declare function validateCommand(command: string, args: string[], options?: CommandOptions): {
|
|
23
24
|
valid: boolean;
|
|
@@ -27,6 +28,15 @@ export declare function validateCommand(command: string, args: string[], options
|
|
|
27
28
|
* Execute a shell command with safety checks
|
|
28
29
|
*/
|
|
29
30
|
export declare function executeCommand(command: string, args?: string[], options?: CommandOptions): CommandResult;
|
|
31
|
+
/**
|
|
32
|
+
* Async validation: everything in validateCommand plus the DNS-resolving
|
|
33
|
+
* SSRF check for URL-carrying commands (curl/wget/http/https). Split from
|
|
34
|
+
* the sync part because DNS lookups can't block the event loop.
|
|
35
|
+
*/
|
|
36
|
+
export declare function validateCommandAsync(command: string, args: string[], options?: CommandOptions): Promise<{
|
|
37
|
+
valid: boolean;
|
|
38
|
+
reason?: string;
|
|
39
|
+
}>;
|
|
30
40
|
/**
|
|
31
41
|
* Execute a shell command asynchronously (non-blocking)
|
|
32
42
|
*/
|
package/dist/utils/shell.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { spawnSync, spawn } from 'child_process';
|
|
5
5
|
import { resolve, relative, isAbsolute } from 'path';
|
|
6
6
|
import { existsSync } from 'fs';
|
|
7
|
+
import { assertFetchUrlAllowed } from './ssrfGuard.js';
|
|
7
8
|
// Dangerous command patterns that should never be executed
|
|
8
9
|
const BLOCKED_COMMANDS = new Set([
|
|
9
10
|
'sudo',
|
|
@@ -68,8 +69,13 @@ const ALLOWED_COMMANDS = new Set([
|
|
|
68
69
|
// Linting/Formatting
|
|
69
70
|
'eslint', 'prettier', 'black', 'rustfmt',
|
|
70
71
|
// Other common tools
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
// NOTE: `env` deliberately NOT whitelisted — it dumps process.env to
|
|
73
|
+
// stdout, which lands in the model's context. Provider API keys ride in
|
|
74
|
+
// env vars, so a single `env` call would exfiltrate every credential the
|
|
75
|
+
// CLI holds. `printenv` is excluded for the same reason. Run these
|
|
76
|
+
// yourself outside the agent if you need environment info.
|
|
77
|
+
'echo', 'pwd', 'which', 'date', 'sleep',
|
|
78
|
+
'curl', 'wget', // allowed but patterns + SSRF-checked
|
|
73
79
|
'tar', 'unzip', 'zip',
|
|
74
80
|
// HTTP tools
|
|
75
81
|
'http', 'https',
|
|
@@ -105,8 +111,62 @@ function hasInlineEval(command, args) {
|
|
|
105
111
|
}
|
|
106
112
|
return false;
|
|
107
113
|
}
|
|
114
|
+
// Commands whose arguments carry URLs that must pass the SSRF guard
|
|
115
|
+
// (private/loopback/metadata IP check) before execution. `fetch_url` already
|
|
116
|
+
// routes through assertFetchUrlAllowed; without this list the same model-
|
|
117
|
+
// controlled URL could just be passed to curl instead.
|
|
118
|
+
const URL_CARRYING_COMMANDS = new Set(['curl', 'wget', 'http', 'https']);
|
|
119
|
+
// Heuristic: extract URL-looking arguments. curl/wget accept URLs with or
|
|
120
|
+
// without a scheme (curl example.com works), and URLs may also ride in
|
|
121
|
+
// option values (`--url=…`, `-d @url`, header values like
|
|
122
|
+
// `Host: internal.corp`). We normalize scheme-less hosts so the guard sees
|
|
123
|
+
// what curl will actually connect to.
|
|
124
|
+
function extractUrlCandidates(args) {
|
|
125
|
+
const urls = [];
|
|
126
|
+
for (const arg of args) {
|
|
127
|
+
if (arg.startsWith('-')) {
|
|
128
|
+
// Option values: --url=x, --output=y are paths not URLs, but
|
|
129
|
+
// --header="Host: x" can smuggle a host. Keep it simple: only check
|
|
130
|
+
// --url= style options that plausibly carry a URL.
|
|
131
|
+
const m = arg.match(/^--url=(.+)$/i);
|
|
132
|
+
if (m)
|
|
133
|
+
urls.push(m[1]);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (/^https?:\/\//i.test(arg)) {
|
|
137
|
+
urls.push(arg);
|
|
138
|
+
}
|
|
139
|
+
else if (
|
|
140
|
+
// scheme-less host forms curl accepts: literal IPs (with optional
|
|
141
|
+
// port/path), 'localhost', and named hosts (example.com, internal.corp).
|
|
142
|
+
// Anything else (plain filenames, package names) is left alone.
|
|
143
|
+
/^(localhost([\/?#].*)?|\d{1,3}(\.\d{1,3}){3}(:\d+)?([\/?#].*)?|[a-z0-9-]+(\.[a-z0-9-]+)+(:\d+)?([\/?#].*)?)$/i.test(arg)) {
|
|
144
|
+
// scheme-less host or host/path — what curl will connect to
|
|
145
|
+
urls.push(`http://${arg}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return urls;
|
|
149
|
+
}
|
|
150
|
+
// Exec-escapes: whitelisted utilities that can run ARBITRARY other commands
|
|
151
|
+
// as part of their arguments, silently bypassing the whitelist above.
|
|
152
|
+
// find . -exec <anything> \; → runs <anything>
|
|
153
|
+
// find . -execdir <anything> \;
|
|
154
|
+
// tar --to-command=<anything> → pipes each extracted file into it
|
|
155
|
+
// xargs <anything> → not whitelisted itself, but listed
|
|
156
|
+
// here for documentation; see note.
|
|
157
|
+
const EXEC_ESCAPE_SHORT = {
|
|
158
|
+
find: ['-exec', '-execdir', '-ok', '-okdir'],
|
|
159
|
+
tar: ['--to-command'],
|
|
160
|
+
};
|
|
161
|
+
function hasExecEscape(command, args) {
|
|
162
|
+
const flags = EXEC_ESCAPE_SHORT[command] ?? [];
|
|
163
|
+
if (flags.length === 0)
|
|
164
|
+
return false;
|
|
165
|
+
return args.some((a) => flags.includes(a) || flags.some((f) => a.startsWith(f + '=')));
|
|
166
|
+
}
|
|
108
167
|
/**
|
|
109
|
-
* Validate if a command is safe to execute
|
|
168
|
+
* Validate if a command is safe to execute (synchronous checks).
|
|
169
|
+
* See validateCommandAsync for the DNS-resolving SSRF checks.
|
|
110
170
|
*/
|
|
111
171
|
export function validateCommand(command, args, options) {
|
|
112
172
|
// Check if command is in blocked list
|
|
@@ -122,6 +182,11 @@ export function validateCommand(command, args, options) {
|
|
|
122
182
|
if (hasInlineEval(command, args)) {
|
|
123
183
|
return { valid: false, reason: `Inline code execution via '${command}' (e.g. -e/-c/--eval) is not allowed in agent mode — put the code in a file and run that, or run it yourself.` };
|
|
124
184
|
}
|
|
185
|
+
// Block whitelisted utilities whose flags spawn OTHER commands — that
|
|
186
|
+
// would bypass the whitelist entirely (find . -exec rm -rf / \;).
|
|
187
|
+
if (hasExecEscape(command, args)) {
|
|
188
|
+
return { valid: false, reason: `'${command}' with exec flags (-exec/-execdir/--to-command…) runs arbitrary commands and is not allowed in agent mode.` };
|
|
189
|
+
}
|
|
125
190
|
// Check full command string against dangerous patterns
|
|
126
191
|
const fullCommand = `${command} ${args.join(' ')}`;
|
|
127
192
|
for (const pattern of BLOCKED_PATTERNS) {
|
|
@@ -241,6 +306,25 @@ export function executeCommand(command, args = [], options) {
|
|
|
241
306
|
};
|
|
242
307
|
}
|
|
243
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* Async validation: everything in validateCommand plus the DNS-resolving
|
|
311
|
+
* SSRF check for URL-carrying commands (curl/wget/http/https). Split from
|
|
312
|
+
* the sync part because DNS lookups can't block the event loop.
|
|
313
|
+
*/
|
|
314
|
+
export async function validateCommandAsync(command, args, options) {
|
|
315
|
+
const sync = validateCommand(command, args, options);
|
|
316
|
+
if (!sync.valid)
|
|
317
|
+
return sync;
|
|
318
|
+
if (URL_CARRYING_COMMANDS.has(command)) {
|
|
319
|
+
for (const url of extractUrlCandidates(args)) {
|
|
320
|
+
const blocked = await assertFetchUrlAllowed(url);
|
|
321
|
+
if (blocked) {
|
|
322
|
+
return { valid: false, reason: `Blocked URL in ${command} arguments: ${blocked}` };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return { valid: true };
|
|
327
|
+
}
|
|
244
328
|
/**
|
|
245
329
|
* Execute a shell command asynchronously (non-blocking)
|
|
246
330
|
*/
|
|
@@ -249,88 +333,91 @@ export function executeCommandAsync(command, args = [], options) {
|
|
|
249
333
|
const startTime = Date.now();
|
|
250
334
|
const cwd = options?.cwd || process.cwd();
|
|
251
335
|
const timeout = options?.timeout || 60000;
|
|
252
|
-
// Validate command first
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
// Ensure cwd exists
|
|
267
|
-
if (!existsSync(cwd)) {
|
|
268
|
-
resolve({
|
|
269
|
-
success: false,
|
|
270
|
-
stdout: '',
|
|
271
|
-
stderr: `Working directory does not exist: ${cwd}`,
|
|
272
|
-
exitCode: -1,
|
|
273
|
-
duration: 0,
|
|
274
|
-
command,
|
|
275
|
-
args,
|
|
276
|
-
});
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
const child = spawn(command, args, {
|
|
280
|
-
cwd,
|
|
281
|
-
env: { ...process.env, ...options?.env },
|
|
282
|
-
});
|
|
283
|
-
let stdout = '';
|
|
284
|
-
let stderr = '';
|
|
285
|
-
child.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
286
|
-
child.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
287
|
-
let settled = false;
|
|
288
|
-
const timer = setTimeout(() => {
|
|
289
|
-
if (settled)
|
|
336
|
+
// Validate command first — async because URL-carrying commands get a
|
|
337
|
+
// DNS-resolving SSRF check (private/loopback/metadata IP guard) that
|
|
338
|
+
// matches the one on the fetch_url tool.
|
|
339
|
+
validateCommandAsync(command, args, options).then((validation) => {
|
|
340
|
+
if (!validation.valid) {
|
|
341
|
+
resolve({
|
|
342
|
+
success: false,
|
|
343
|
+
stdout: '',
|
|
344
|
+
stderr: validation.reason || 'Command validation failed',
|
|
345
|
+
exitCode: -1,
|
|
346
|
+
duration: 0,
|
|
347
|
+
command,
|
|
348
|
+
args,
|
|
349
|
+
});
|
|
290
350
|
return;
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}, timeout);
|
|
304
|
-
child.on('close', (code) => {
|
|
305
|
-
if (settled)
|
|
351
|
+
}
|
|
352
|
+
// Ensure cwd exists
|
|
353
|
+
if (!existsSync(cwd)) {
|
|
354
|
+
resolve({
|
|
355
|
+
success: false,
|
|
356
|
+
stdout: '',
|
|
357
|
+
stderr: `Working directory does not exist: ${cwd}`,
|
|
358
|
+
exitCode: -1,
|
|
359
|
+
duration: 0,
|
|
360
|
+
command,
|
|
361
|
+
args,
|
|
362
|
+
});
|
|
306
363
|
return;
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
success: code === 0,
|
|
312
|
-
stdout,
|
|
313
|
-
stderr,
|
|
314
|
-
exitCode: code ?? -1,
|
|
315
|
-
duration,
|
|
316
|
-
command,
|
|
317
|
-
args,
|
|
364
|
+
}
|
|
365
|
+
const child = spawn(command, args, {
|
|
366
|
+
cwd,
|
|
367
|
+
env: { ...process.env, ...options?.env },
|
|
318
368
|
});
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
settled =
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
369
|
+
let stdout = '';
|
|
370
|
+
let stderr = '';
|
|
371
|
+
child.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
372
|
+
child.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
373
|
+
let settled = false;
|
|
374
|
+
const timer = setTimeout(() => {
|
|
375
|
+
if (settled)
|
|
376
|
+
return;
|
|
377
|
+
settled = true;
|
|
378
|
+
child.kill('SIGTERM');
|
|
379
|
+
const duration = Date.now() - startTime;
|
|
380
|
+
resolve({
|
|
381
|
+
success: false,
|
|
382
|
+
stdout,
|
|
383
|
+
stderr: `Command timed out after ${timeout}ms`,
|
|
384
|
+
exitCode: -1,
|
|
385
|
+
duration,
|
|
386
|
+
command,
|
|
387
|
+
args,
|
|
388
|
+
});
|
|
389
|
+
}, timeout);
|
|
390
|
+
child.on('close', (code) => {
|
|
391
|
+
if (settled)
|
|
392
|
+
return;
|
|
393
|
+
settled = true;
|
|
394
|
+
clearTimeout(timer);
|
|
395
|
+
const duration = Date.now() - startTime;
|
|
396
|
+
resolve({
|
|
397
|
+
success: code === 0,
|
|
398
|
+
stdout,
|
|
399
|
+
stderr,
|
|
400
|
+
exitCode: code ?? -1,
|
|
401
|
+
duration,
|
|
402
|
+
command,
|
|
403
|
+
args,
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
child.on('error', (err) => {
|
|
407
|
+
if (settled)
|
|
408
|
+
return;
|
|
409
|
+
settled = true;
|
|
410
|
+
clearTimeout(timer);
|
|
411
|
+
const duration = Date.now() - startTime;
|
|
412
|
+
resolve({
|
|
413
|
+
success: false,
|
|
414
|
+
stdout: '',
|
|
415
|
+
stderr: err.message,
|
|
416
|
+
exitCode: -1,
|
|
417
|
+
duration,
|
|
418
|
+
command,
|
|
419
|
+
args,
|
|
420
|
+
});
|
|
334
421
|
});
|
|
335
422
|
});
|
|
336
423
|
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSRF (Server-Side Request Forgery) guard, shared by the agent's network-
|
|
3
|
+
* touching surfaces.
|
|
4
|
+
*
|
|
5
|
+
* Used by:
|
|
6
|
+
* - toolExecution.ts → the `fetch_url` tool
|
|
7
|
+
* - shell.ts → curl/wget/http(s) arguments in execute_command
|
|
8
|
+
*
|
|
9
|
+
* The URLs in both cases originate from model output / page content
|
|
10
|
+
* (untrusted, prompt-injectable), so the agent must not be able to reach
|
|
11
|
+
* internal services or the cloud metadata endpoint (169.254.169.254).
|
|
12
|
+
* NOTE: this deliberately does NOT apply to user-configured provider base
|
|
13
|
+
* URLs (Ollama localhost, custom vLLM/Tailscale endpoints) — those are
|
|
14
|
+
* trusted config and never routed through agent tools.
|
|
15
|
+
*/
|
|
16
|
+
export declare function isBlockedIp(ip: string): boolean;
|
|
17
|
+
/** Returns an error string if the URL must not be fetched, else null. */
|
|
18
|
+
export declare function assertFetchUrlAllowed(rawUrl: string): Promise<string | null>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSRF (Server-Side Request Forgery) guard, shared by the agent's network-
|
|
3
|
+
* touching surfaces.
|
|
4
|
+
*
|
|
5
|
+
* Used by:
|
|
6
|
+
* - toolExecution.ts → the `fetch_url` tool
|
|
7
|
+
* - shell.ts → curl/wget/http(s) arguments in execute_command
|
|
8
|
+
*
|
|
9
|
+
* The URLs in both cases originate from model output / page content
|
|
10
|
+
* (untrusted, prompt-injectable), so the agent must not be able to reach
|
|
11
|
+
* internal services or the cloud metadata endpoint (169.254.169.254).
|
|
12
|
+
* NOTE: this deliberately does NOT apply to user-configured provider base
|
|
13
|
+
* URLs (Ollama localhost, custom vLLM/Tailscale endpoints) — those are
|
|
14
|
+
* trusted config and never routed through agent tools.
|
|
15
|
+
*/
|
|
16
|
+
import { lookup as dnsLookup } from 'dns/promises';
|
|
17
|
+
export function isBlockedIp(ip) {
|
|
18
|
+
const s = ip.trim().toLowerCase();
|
|
19
|
+
if (s.includes(':')) {
|
|
20
|
+
// IPv6
|
|
21
|
+
if (s === '::1' || s === '::')
|
|
22
|
+
return true; // loopback / unspecified
|
|
23
|
+
if (s.startsWith('fe80') || s.startsWith('fc') || s.startsWith('fd'))
|
|
24
|
+
return true; // link-local / ULA
|
|
25
|
+
const mapped = s.match(/::ffff:(\d+\.\d+\.\d+\.\d+)$/); // IPv4-mapped
|
|
26
|
+
if (mapped)
|
|
27
|
+
return isBlockedIp(mapped[1]);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const parts = s.split('.').map(Number);
|
|
31
|
+
if (parts.length !== 4 || parts.some((n) => Number.isNaN(n) || n < 0 || n > 255))
|
|
32
|
+
return false;
|
|
33
|
+
const [a, b] = parts;
|
|
34
|
+
if (a === 127)
|
|
35
|
+
return true; // loopback
|
|
36
|
+
if (a === 10)
|
|
37
|
+
return true; // RFC1918
|
|
38
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
39
|
+
return true; // RFC1918
|
|
40
|
+
if (a === 192 && b === 168)
|
|
41
|
+
return true; // RFC1918
|
|
42
|
+
if (a === 169 && b === 254)
|
|
43
|
+
return true; // link-local incl. metadata 169.254.169.254
|
|
44
|
+
if (a === 0)
|
|
45
|
+
return true; // 0.0.0.0/8
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
/** Returns an error string if the URL must not be fetched, else null. */
|
|
49
|
+
export async function assertFetchUrlAllowed(rawUrl) {
|
|
50
|
+
let u;
|
|
51
|
+
try {
|
|
52
|
+
u = new URL(rawUrl);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return 'Invalid URL format';
|
|
56
|
+
}
|
|
57
|
+
if (u.protocol !== 'http:' && u.protocol !== 'https:') {
|
|
58
|
+
return `Blocked: only http/https URLs can be fetched (got "${u.protocol}")`;
|
|
59
|
+
}
|
|
60
|
+
const host = u.hostname.replace(/^\[|\]$/g, ''); // strip IPv6 brackets
|
|
61
|
+
if (host === 'localhost' || host.endsWith('.localhost')) {
|
|
62
|
+
return 'Blocked: localhost is not fetchable by the agent';
|
|
63
|
+
}
|
|
64
|
+
if (/^[0-9.]+$/.test(host) || host.includes(':')) {
|
|
65
|
+
// Literal IP — check directly.
|
|
66
|
+
if (isBlockedIp(host))
|
|
67
|
+
return `Blocked: ${host} is a private/loopback/link-local address`;
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
// Resolve and check every address (catches internal hostnames + single-record rebinding).
|
|
71
|
+
try {
|
|
72
|
+
const addrs = await dnsLookup(host, { all: true });
|
|
73
|
+
for (const a of addrs) {
|
|
74
|
+
if (isBlockedIp(a.address)) {
|
|
75
|
+
return `Blocked: ${host} resolves to a private/internal address (${a.address})`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// DNS failure — let curl attempt and fail naturally; not an SSRF risk.
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
@@ -12,6 +12,12 @@ export interface TaskPlan {
|
|
|
12
12
|
tasks: SubTask[];
|
|
13
13
|
estimatedIterations: number;
|
|
14
14
|
}
|
|
15
|
+
/** Per-run provider selection. Used by custom bots without mutating config. */
|
|
16
|
+
export interface TaskPlannerRuntime {
|
|
17
|
+
providerId?: string;
|
|
18
|
+
model?: string;
|
|
19
|
+
protocol?: 'openai' | 'anthropic';
|
|
20
|
+
}
|
|
15
21
|
/**
|
|
16
22
|
* Ask AI to break down a complex task into subtasks
|
|
17
23
|
*/
|
|
@@ -19,7 +25,7 @@ export declare function planTasks(userPrompt: string, projectContext: {
|
|
|
19
25
|
name: string;
|
|
20
26
|
type: string;
|
|
21
27
|
structure: string;
|
|
22
|
-
}): Promise<TaskPlan>;
|
|
28
|
+
}, runtime?: TaskPlannerRuntime): Promise<TaskPlan>;
|
|
23
29
|
/**
|
|
24
30
|
* Check if a task's dependencies are completed
|
|
25
31
|
*/
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* Task Planning - breaks down complex tasks into subtasks
|
|
3
3
|
*/
|
|
4
4
|
import { config, getApiKey, resolveBaseUrl } from '../config/index.js';
|
|
5
|
-
import { getProviderAuthHeader, requiresDefaultTemperature } from '../config/providers.js';
|
|
5
|
+
import { getProviderAuthHeader, isNoApiKeyProvider, requiresDefaultTemperature } from '../config/providers.js';
|
|
6
6
|
/**
|
|
7
7
|
* Ask AI to break down a complex task into subtasks
|
|
8
8
|
*/
|
|
9
|
-
export async function planTasks(userPrompt, projectContext) {
|
|
9
|
+
export async function planTasks(userPrompt, projectContext, runtime = {}) {
|
|
10
10
|
const systemPrompt = `You are a task planning expert. Break down user requests into clear, sequential subtasks.
|
|
11
11
|
|
|
12
12
|
RULES:
|
|
@@ -36,14 +36,17 @@ User Request: ${userPrompt}
|
|
|
36
36
|
|
|
37
37
|
Break this down into subtasks. Each task = one file or one logical unit. Respond with JSON only.`;
|
|
38
38
|
try {
|
|
39
|
-
const
|
|
39
|
+
const protocol = runtime.protocol ?? config.get('protocol');
|
|
40
|
+
const provider = runtime.providerId ?? config.get('provider');
|
|
41
|
+
const model = runtime.model ?? config.get('model');
|
|
42
|
+
const apiKey = getApiKey(provider) || (isNoApiKeyProvider(provider) ? 'ollama' : '');
|
|
40
43
|
if (!apiKey) {
|
|
41
44
|
throw new Error('No API key configured');
|
|
42
45
|
}
|
|
43
|
-
const protocol = config.get('protocol');
|
|
44
|
-
const provider = config.get('provider');
|
|
45
|
-
const model = config.get('model');
|
|
46
46
|
const baseUrl = resolveBaseUrl(provider, protocol);
|
|
47
|
+
if (!baseUrl) {
|
|
48
|
+
throw new Error(`No API base URL configured for ${provider}`);
|
|
49
|
+
}
|
|
47
50
|
const authHeaderType = getProviderAuthHeader(provider, protocol);
|
|
48
51
|
const messages = [
|
|
49
52
|
{ role: 'user', content: systemPrompt }
|
|
@@ -73,7 +76,13 @@ Break this down into subtasks. Each task = one file or one logical unit. Respond
|
|
|
73
76
|
else {
|
|
74
77
|
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
75
78
|
}
|
|
76
|
-
|
|
79
|
+
if (protocol === 'anthropic') {
|
|
80
|
+
headers['anthropic-version'] = '2023-06-01';
|
|
81
|
+
}
|
|
82
|
+
const endpoint = protocol === 'anthropic'
|
|
83
|
+
? `${baseUrl}/v1/messages`
|
|
84
|
+
: `${baseUrl}/chat/completions`;
|
|
85
|
+
const response = await fetch(endpoint, {
|
|
77
86
|
method: 'POST',
|
|
78
87
|
headers,
|
|
79
88
|
body: JSON.stringify(requestBody),
|
|
@@ -76,9 +76,11 @@ export function getModelContextWindow(model) {
|
|
|
76
76
|
const MODEL_PRICING = {
|
|
77
77
|
// Z.AI / ZhipuAI
|
|
78
78
|
// Coding Plan is flat-fee; these official rates apply to pay-per-use.
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
79
|
+
// GLM-5.3 reached the standalone API on 2026-08-19 and is listed at the same
|
|
80
|
+
// rate as GLM-5.2 (docs.z.ai/guides/overview/pricing). Verified there, not
|
|
81
|
+
// inferred from the match — the two being equal today is a coincidence of the
|
|
82
|
+
// price list, not a rule.
|
|
83
|
+
'glm-5.3': { inputPer1M: 1.40, outputPer1M: 4.40 },
|
|
82
84
|
'glm-5.2': { inputPer1M: 1.40, outputPer1M: 4.40 },
|
|
83
85
|
'glm-5.1': { inputPer1M: 1.40, outputPer1M: 4.40 },
|
|
84
86
|
'glm-5-turbo': { inputPer1M: 1.20, outputPer1M: 4.00 },
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* createActionLog() converts a ToolCall+ToolResult into a history ActionLog.
|
|
8
8
|
*/
|
|
9
9
|
import { ToolCall, ToolResult, ActionLog } from './tools';
|
|
10
|
+
export { isBlockedIp, assertFetchUrlAllowed } from './ssrfGuard';
|
|
10
11
|
/**
|
|
11
12
|
* Validate path is within project root.
|
|
12
13
|
* Uses realpathSync to resolve symlinks, preventing symlink traversal attacks
|