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
@@ -6,8 +6,8 @@
6
6
  * listDirectory() and htmlToText() are private helpers.
7
7
  * createActionLog() converts a ToolCall+ToolResult into a history ActionLog.
8
8
  */
9
- import { existsSync, readdirSync, statSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, rmSync, realpathSync } from 'fs';
10
- import { join, dirname, relative, resolve, isAbsolute } from 'path';
9
+ import { existsSync, readdirSync, statSync, lstatSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, rmSync, realpathSync } from 'fs';
10
+ import { join, dirname, relative, resolve, isAbsolute, sep } from 'path';
11
11
  import { executeCommandAsync } from './shell.js';
12
12
  import { recordWrite, recordEdit, recordDelete, recordMkdir, recordCommand } from './history.js';
13
13
  import { loadIgnoreRules, isIgnored } from './gitignore.js';
@@ -15,83 +15,13 @@ import { normalizeToolName } from './toolParsing.js';
15
15
  import { getZaiMcpConfig, getZaiVisionConfig, getMinimaxMcpConfig, callZaiMcp, callZaiVisionApi, callMinimaxApi } from './mcpIntegration.js';
16
16
  import { logger } from './logger.js';
17
17
  import { runHook } from './hooks.js';
18
+ import { checkCommandRateLimit } from './ratelimit.js';
18
19
  import { isMcpToolName, callSessionTool, isVirtualMcpToolName, callSessionVirtualTool } from './mcpRegistry.js';
19
- import { lookup as dnsLookup } from 'dns/promises';
20
- /**
21
- * SSRF guard for the agent's `fetch_url` tool. The URL there comes from model
22
- * output / page content (untrusted, prompt-injectable), so the agent must not
23
- * be able to reach internal services or the cloud metadata endpoint
24
- * (169.254.169.254). NOTE: this does NOT apply to user-configured provider
25
- * base URLs (Ollama localhost, custom vLLM/Tailscale endpoints) — those are
26
- * trusted config and never routed through fetch_url.
27
- */
28
- function isBlockedIp(ip) {
29
- const s = ip.trim().toLowerCase();
30
- if (s.includes(':')) {
31
- // IPv6
32
- if (s === '::1' || s === '::')
33
- return true; // loopback / unspecified
34
- if (s.startsWith('fe80') || s.startsWith('fc') || s.startsWith('fd'))
35
- return true; // link-local / ULA
36
- const mapped = s.match(/::ffff:(\d+\.\d+\.\d+\.\d+)$/); // IPv4-mapped
37
- if (mapped)
38
- return isBlockedIp(mapped[1]);
39
- return false;
40
- }
41
- const parts = s.split('.').map(Number);
42
- if (parts.length !== 4 || parts.some((n) => Number.isNaN(n) || n < 0 || n > 255))
43
- return false;
44
- const [a, b] = parts;
45
- if (a === 127)
46
- return true; // loopback
47
- if (a === 10)
48
- return true; // RFC1918
49
- if (a === 172 && b >= 16 && b <= 31)
50
- return true; // RFC1918
51
- if (a === 192 && b === 168)
52
- return true; // RFC1918
53
- if (a === 169 && b === 254)
54
- return true; // link-local incl. metadata 169.254.169.254
55
- if (a === 0)
56
- return true; // 0.0.0.0/8
57
- return false;
58
- }
59
- /** Returns an error string if the URL must not be fetched, else null. */
60
- async function assertFetchUrlAllowed(rawUrl) {
61
- let u;
62
- try {
63
- u = new URL(rawUrl);
64
- }
65
- catch {
66
- return 'Invalid URL format';
67
- }
68
- if (u.protocol !== 'http:' && u.protocol !== 'https:') {
69
- return `Blocked: only http/https URLs can be fetched (got "${u.protocol}")`;
70
- }
71
- const host = u.hostname.replace(/^\[|\]$/g, ''); // strip IPv6 brackets
72
- if (host === 'localhost' || host.endsWith('.localhost')) {
73
- return 'Blocked: localhost is not fetchable by the agent';
74
- }
75
- if (/^[0-9.]+$/.test(host) || host.includes(':')) {
76
- // Literal IP — check directly.
77
- if (isBlockedIp(host))
78
- return `Blocked: ${host} is a private/loopback/link-local address`;
79
- return null;
80
- }
81
- // Resolve and check every address (catches internal hostnames + single-record rebinding).
82
- try {
83
- const addrs = await dnsLookup(host, { all: true });
84
- for (const a of addrs) {
85
- if (isBlockedIp(a.address)) {
86
- return `Blocked: ${host} resolves to a private/internal address (${a.address})`;
87
- }
88
- }
89
- }
90
- catch {
91
- // DNS failure — let curl attempt and fail naturally; not an SSRF risk.
92
- }
93
- return null;
94
- }
20
+ // SSRF guard (isBlockedIp / assertFetchUrlAllowed) moved to ./ssrfGuard —
21
+ // shared with shell.ts for curl/wget URL checks. Re-exported here so the
22
+ // existing tests that import it from toolExecution keep working.
23
+ export { isBlockedIp, assertFetchUrlAllowed } from './ssrfGuard.js';
24
+ import { assertFetchUrlAllowed } from './ssrfGuard.js';
95
25
  const debug = (...args) => {
96
26
  if (process.env.CODEEP_DEBUG === '1') {
97
27
  logger.debug(args.map(String).join(' '));
@@ -115,20 +45,37 @@ export function validatePath(path, projectRoot) {
115
45
  if (relativePath.startsWith('..')) {
116
46
  return { valid: false, absolutePath, error: `Path '${path}' is outside project directory` };
117
47
  }
118
- // Resolve symlinks to prevent traversal attacks (only if path exists)
119
- if (existsSync(absolutePath)) {
120
- try {
121
- const realPath = realpathSync(absolutePath);
122
- const realRoot = realpathSync(projectRoot);
123
- if (!realPath.startsWith(realRoot + '/') && realPath !== realRoot) {
124
- return { valid: false, absolutePath, error: `Path '${path}' resolves outside project directory (symlink traversal)` };
48
+ // Resolve the deepest existing ancestor, not only the complete target. A
49
+ // write to `project/link-to-outside/new.txt` has a non-existent leaf, but
50
+ // still follows the existing symlinked parent. lstat is intentional: unlike
51
+ // existsSync it also sees a broken symlink, which must fail closed rather
52
+ // than be followed by writeFileSync.
53
+ try {
54
+ const realRoot = realpathSync(projectRoot);
55
+ let existingAncestor = absolutePath;
56
+ while (true) {
57
+ try {
58
+ lstatSync(existingAncestor);
59
+ break;
60
+ }
61
+ catch {
62
+ const parent = dirname(existingAncestor);
63
+ if (parent === existingAncestor) {
64
+ return { valid: false, absolutePath, error: `Path '${path}' could not be resolved` };
65
+ }
66
+ existingAncestor = parent;
125
67
  }
126
68
  }
127
- catch {
128
- // realpathSync can fail on broken symlinks — treat as invalid
129
- return { valid: false, absolutePath, error: `Path '${path}' could not be resolved` };
69
+ const realAncestor = realpathSync(existingAncestor);
70
+ const ancestorRelative = relative(realRoot, realAncestor);
71
+ if (ancestorRelative === '..' || ancestorRelative.startsWith(`..${sep}`) || isAbsolute(ancestorRelative)) {
72
+ return { valid: false, absolutePath, error: `Path '${path}' resolves outside project directory (symlink traversal)` };
130
73
  }
131
74
  }
75
+ catch {
76
+ // realpathSync fails for broken symlinks and inaccessible ancestors.
77
+ return { valid: false, absolutePath, error: `Path '${path}' could not be resolved` };
78
+ }
132
79
  return { valid: true, absolutePath };
133
80
  }
134
81
  /**
@@ -145,6 +92,11 @@ function listDirectory(dir, projectRoot, recursive, prefix = '', ignoreRules, vi
145
92
  continue;
146
93
  if (entry.isDirectory() || entry.isSymbolicLink()) {
147
94
  try {
95
+ // Recursive listing must not follow an in-workspace symlink into an
96
+ // external directory. Top-level paths already pass validatePath, but
97
+ // each discovered symlink needs the same boundary check.
98
+ if (entry.isSymbolicLink() && !validatePath(fullPath, projectRoot).valid)
99
+ continue;
148
100
  const st = statSync(fullPath); // follows symlinks
149
101
  if (st.isDirectory()) {
150
102
  if (visitedInodes.has(st.ino))
@@ -533,6 +485,14 @@ async function dispatchTool(tool, parameters, projectRoot, fs, toolCall) {
533
485
  const args = parameters.args || [];
534
486
  if (!command)
535
487
  return { success: false, output: '', error: 'Missing required parameter: command', tool, parameters };
488
+ // Command throttle — guards against agent loops that spawn commands
489
+ // every iteration (each can be up to 2 minutes of subprocess time).
490
+ // Rate-limited *after* permission resolution: an allowed command
491
+ // consumes budget, a denied one never reaches here.
492
+ const cmdRate = checkCommandRateLimit();
493
+ if (!cmdRate.allowed) {
494
+ return { success: false, output: '', error: cmdRate.message || 'Command rate limit exceeded', tool, parameters };
495
+ }
536
496
  recordCommand(command, args);
537
497
  const result = await executeCommandAsync(command, args, {
538
498
  cwd: projectRoot,
@@ -328,16 +328,16 @@ export interface AdditionalToolDef {
328
328
  * Optionally appends an "Additional tools" section listing tools from
329
329
  * sources outside the built-in catalog (currently MCP servers).
330
330
  */
331
- export declare function formatToolDefinitions(additionalTools?: AdditionalToolDef[]): string;
331
+ export declare function formatToolDefinitions(additionalTools?: AdditionalToolDef[], allowedToolNames?: ReadonlySet<string>): string;
332
332
  /**
333
333
  * Get tools in OpenAI Function Calling format.
334
334
  * Additional tools (e.g. MCP) are appended with their JSON-schema as-is.
335
335
  */
336
- export declare function getOpenAITools(additionalTools?: AdditionalToolDef[]): OpenAITool[];
336
+ export declare function getOpenAITools(additionalTools?: AdditionalToolDef[], allowedToolNames?: ReadonlySet<string>): OpenAITool[];
337
337
  /**
338
338
  * Get tools in Anthropic Tool Use format.
339
339
  * Additional tools (e.g. MCP) are appended with their JSON-schema as-is.
340
340
  */
341
- export declare function getAnthropicTools(additionalTools?: AdditionalToolDef[]): AnthropicTool[];
341
+ export declare function getAnthropicTools(additionalTools?: AdditionalToolDef[], allowedToolNames?: ReadonlySet<string>): AnthropicTool[];
342
342
  export { normalizeToolName, parseOpenAIToolCalls, parseAnthropicToolCalls, parseToolCalls } from './toolParsing';
343
343
  export { executeTool, validatePath, createActionLog } from './toolExecution';
@@ -140,11 +140,13 @@ export const AGENT_TOOLS = {
140
140
  /**
141
141
  * Get filtered tool entries (excludes provider-specific tools when API key not available)
142
142
  */
143
- function getFilteredToolEntries() {
143
+ function getFilteredToolEntries(allowedToolNames) {
144
144
  const hasMcp = hasZaiMcpAccess();
145
145
  const hasZaiVision = hasZaiVisionAccess();
146
146
  const hasMinimaxMcp = hasMinimaxMcpAccess();
147
147
  return Object.entries(AGENT_TOOLS).filter(([name]) => {
148
+ if (allowedToolNames && !allowedToolNames.has(name))
149
+ return false;
148
150
  if (ZAI_MCP_TOOLS.includes(name))
149
151
  return hasMcp;
150
152
  if (ZAI_VISION_TOOLS.includes(name))
@@ -159,9 +161,9 @@ function getFilteredToolEntries() {
159
161
  * Optionally appends an "Additional tools" section listing tools from
160
162
  * sources outside the built-in catalog (currently MCP servers).
161
163
  */
162
- export function formatToolDefinitions(additionalTools) {
164
+ export function formatToolDefinitions(additionalTools, allowedToolNames) {
163
165
  const lines = [];
164
- for (const [name, tool] of getFilteredToolEntries()) {
166
+ for (const [name, tool] of getFilteredToolEntries(allowedToolNames)) {
165
167
  lines.push(`### ${name}`);
166
168
  lines.push(tool.description);
167
169
  lines.push('Parameters:');
@@ -171,7 +173,8 @@ export function formatToolDefinitions(additionalTools) {
171
173
  }
172
174
  lines.push('');
173
175
  }
174
- if (additionalTools?.length) {
176
+ const allowedAdditionalTools = additionalTools?.filter(tool => !allowedToolNames || allowedToolNames.has(tool.name));
177
+ if (allowedAdditionalTools?.length) {
175
178
  // Token budget caps. MCP servers can return tools with absurdly verbose
176
179
  // JSON Schema definitions (multi-KB per tool). Without caps a 30-tool
177
180
  // server eats ~10K tokens of every prompt. Per-tool 2KB + total 16KB
@@ -183,7 +186,7 @@ export function formatToolDefinitions(additionalTools) {
183
186
  let skipped = 0;
184
187
  lines.push('## Additional tools (from MCP servers)');
185
188
  lines.push('');
186
- for (const tool of additionalTools) {
189
+ for (const tool of allowedAdditionalTools) {
187
190
  if (budget <= 0) {
188
191
  skipped++;
189
192
  continue;
@@ -227,8 +230,8 @@ function additionalToolSchema(tool) {
227
230
  * Get tools in OpenAI Function Calling format.
228
231
  * Additional tools (e.g. MCP) are appended with their JSON-schema as-is.
229
232
  */
230
- export function getOpenAITools(additionalTools) {
231
- const builtin = getFilteredToolEntries().map(([name, tool]) => {
233
+ export function getOpenAITools(additionalTools, allowedToolNames) {
234
+ const builtin = getFilteredToolEntries(allowedToolNames).map(([name, tool]) => {
232
235
  const properties = {};
233
236
  const required = [];
234
237
  for (const [param, info] of Object.entries(tool.parameters)) {
@@ -251,9 +254,10 @@ export function getOpenAITools(additionalTools) {
251
254
  },
252
255
  };
253
256
  });
254
- if (!additionalTools?.length)
257
+ const allowedAdditionalTools = additionalTools?.filter(tool => !allowedToolNames || allowedToolNames.has(tool.name));
258
+ if (!allowedAdditionalTools?.length)
255
259
  return builtin;
256
- const extra = additionalTools.map(t => ({
260
+ const extra = allowedAdditionalTools.map(t => ({
257
261
  type: 'function',
258
262
  function: {
259
263
  name: t.name,
@@ -269,8 +273,8 @@ export function getOpenAITools(additionalTools) {
269
273
  * Get tools in Anthropic Tool Use format.
270
274
  * Additional tools (e.g. MCP) are appended with their JSON-schema as-is.
271
275
  */
272
- export function getAnthropicTools(additionalTools) {
273
- const builtin = getFilteredToolEntries().map(([name, tool]) => {
276
+ export function getAnthropicTools(additionalTools, allowedToolNames) {
277
+ const builtin = getFilteredToolEntries(allowedToolNames).map(([name, tool]) => {
274
278
  const properties = {};
275
279
  const required = [];
276
280
  for (const [param, info] of Object.entries(tool.parameters)) {
@@ -290,9 +294,10 @@ export function getAnthropicTools(additionalTools) {
290
294
  input_schema: { type: 'object', properties, required },
291
295
  };
292
296
  });
293
- if (!additionalTools?.length)
297
+ const allowedAdditionalTools = additionalTools?.filter(tool => !allowedToolNames || allowedToolNames.has(tool.name));
298
+ if (!allowedAdditionalTools?.length)
294
299
  return builtin;
295
- const extra = additionalTools.map(t => ({
300
+ const extra = allowedAdditionalTools.map(t => ({
296
301
  name: t.name,
297
302
  description: t.description ?? `External tool: ${t.name}`,
298
303
  input_schema: additionalToolSchema(t),
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "2.18.1";
1
+ export declare const VERSION = "2.20.0";
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  // AUTO-GENERATED by scripts/gen-version.js — do not edit by hand.
2
2
  // Baked from package.json at build time so the bun-compiled binary reports
3
3
  // the right version (it has no package.json on disk to read at runtime).
4
- export const VERSION = '2.18.1';
4
+ export const VERSION = '2.20.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeep",
3
- "version": "2.18.1",
3
+ "version": "2.20.0",
4
4
  "description": "AI-powered coding assistant built for the terminal. Multiple LLM providers, project-aware context, and a seamless development workflow.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",