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.
Files changed (51) hide show
  1. package/README.md +53 -16
  2. package/dist/acp/commands.js +11 -55
  3. package/dist/acp/protocol.d.ts +34 -0
  4. package/dist/acp/server.d.ts +6 -1
  5. package/dist/acp/server.js +97 -2
  6. package/dist/api/index.js +9 -0
  7. package/dist/commands/core/index.d.ts +19 -0
  8. package/dist/commands/core/index.js +28 -0
  9. package/dist/commands/core/keysync.d.ts +2 -0
  10. package/dist/commands/core/keysync.js +34 -0
  11. package/dist/commands/core/telemetry.d.ts +2 -0
  12. package/dist/commands/core/telemetry.js +34 -0
  13. package/dist/config/index.js +2 -2
  14. package/dist/config/providers.js +7 -4
  15. package/dist/renderer/App.d.ts +9 -48
  16. package/dist/renderer/App.js +113 -338
  17. package/dist/renderer/Screen.d.ts +13 -0
  18. package/dist/renderer/Screen.js +22 -0
  19. package/dist/renderer/commands/registry.js +3 -3
  20. package/dist/renderer/commands.js +19 -51
  21. package/dist/renderer/components/CommandAutocomplete.d.ts +46 -0
  22. package/dist/renderer/components/CommandAutocomplete.js +103 -0
  23. package/dist/renderer/components/HunkPicker.d.ts +48 -0
  24. package/dist/renderer/components/HunkPicker.js +140 -0
  25. package/dist/renderer/components/MentionPicker.d.ts +60 -0
  26. package/dist/renderer/components/MentionPicker.js +111 -0
  27. package/dist/renderer/components/PasteDialog.d.ts +43 -0
  28. package/dist/renderer/components/PasteDialog.js +70 -0
  29. package/dist/renderer/layout.js +1 -0
  30. package/dist/renderer/main.js +15 -39
  31. package/dist/utils/agent.js +121 -26
  32. package/dist/utils/agentChat.d.ts +11 -4
  33. package/dist/utils/agentChat.js +53 -25
  34. package/dist/utils/codeepCloud.d.ts +3 -0
  35. package/dist/utils/codeepCloud.js +62 -7
  36. package/dist/utils/personalities.d.ts +63 -5
  37. package/dist/utils/personalities.js +583 -31
  38. package/dist/utils/shell.d.ts +11 -1
  39. package/dist/utils/shell.js +169 -82
  40. package/dist/utils/ssrfGuard.d.ts +18 -0
  41. package/dist/utils/ssrfGuard.js +83 -0
  42. package/dist/utils/taskPlanner.d.ts +7 -1
  43. package/dist/utils/taskPlanner.js +16 -7
  44. package/dist/utils/tokenTracker.js +5 -3
  45. package/dist/utils/toolExecution.d.ts +1 -0
  46. package/dist/utils/toolExecution.js +48 -88
  47. package/dist/utils/tools.d.ts +3 -3
  48. package/dist/utils/tools.js +18 -13
  49. package/dist/version.d.ts +1 -1
  50. package/dist/version.js +1 -1
  51. package/package.json +1 -1
@@ -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
  */
@@ -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
- 'echo', 'pwd', 'which', 'env', 'date', 'sleep',
72
- 'curl', 'wget', // allowed but patterns checked
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 (synchronous, fast)
253
- const validation = validateCommand(command, args, options);
254
- if (!validation.valid) {
255
- resolve({
256
- success: false,
257
- stdout: '',
258
- stderr: validation.reason || 'Command validation failed',
259
- exitCode: -1,
260
- duration: 0,
261
- command,
262
- args,
263
- });
264
- return;
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
- settled = true;
292
- child.kill('SIGTERM');
293
- const duration = Date.now() - startTime;
294
- resolve({
295
- success: false,
296
- stdout,
297
- stderr: `Command timed out after ${timeout}ms`,
298
- exitCode: -1,
299
- duration,
300
- command,
301
- args,
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
- settled = true;
308
- clearTimeout(timer);
309
- const duration = Date.now() - startTime;
310
- resolve({
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
- child.on('error', (err) => {
321
- if (settled)
322
- return;
323
- settled = true;
324
- clearTimeout(timer);
325
- const duration = Date.now() - startTime;
326
- resolve({
327
- success: false,
328
- stdout: '',
329
- stderr: err.message,
330
- exitCode: -1,
331
- duration,
332
- command,
333
- args,
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 apiKey = await getApiKey();
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
- const response = await fetch(`${baseUrl}/chat/completions`, {
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
- // `glm-5.3` is GLM Coding Plan only — Z.AI publishes no per-token rate for it
80
- // (the standalone model API is still "coming soon"), so it stays unpriced
81
- // rather than borrowing GLM-5.2's.
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