@shareai-lab/kode-sdk 2.7.1 → 2.7.2

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 (97) hide show
  1. package/dist/core/agent/breakpoint-manager.js +36 -0
  2. package/dist/core/agent/message-queue.js +57 -0
  3. package/dist/core/agent/permission-manager.js +32 -0
  4. package/dist/core/agent/todo-manager.js +91 -0
  5. package/dist/core/agent/tool-runner.js +45 -0
  6. package/dist/core/agent.js +2035 -0
  7. package/dist/core/config.js +2 -0
  8. package/dist/core/context-manager.js +241 -0
  9. package/dist/core/errors.js +49 -0
  10. package/dist/core/events.js +329 -0
  11. package/dist/core/file-pool.d.ts +2 -0
  12. package/dist/core/file-pool.js +125 -0
  13. package/dist/core/hooks.js +71 -0
  14. package/dist/core/permission-modes.js +61 -0
  15. package/dist/core/pool.js +301 -0
  16. package/dist/core/room.js +57 -0
  17. package/dist/core/scheduler.js +58 -0
  18. package/dist/core/skills/index.js +20 -0
  19. package/dist/core/skills/management-manager.js +557 -0
  20. package/dist/core/skills/manager.js +243 -0
  21. package/dist/core/skills/operation-queue.js +113 -0
  22. package/dist/core/skills/sandbox-file-manager.js +183 -0
  23. package/dist/core/skills/types.js +9 -0
  24. package/dist/core/skills/xml-generator.js +70 -0
  25. package/dist/core/template.js +35 -0
  26. package/dist/core/time-bridge.js +100 -0
  27. package/dist/core/todo.js +89 -0
  28. package/dist/core/types.js +3 -0
  29. package/dist/index.js +148 -60461
  30. package/dist/infra/db/postgres/postgres-store.js +1073 -0
  31. package/dist/infra/db/sqlite/sqlite-store.js +800 -0
  32. package/dist/infra/e2b/e2b-fs.js +128 -0
  33. package/dist/infra/e2b/e2b-sandbox.js +156 -0
  34. package/dist/infra/e2b/e2b-template.js +105 -0
  35. package/dist/infra/e2b/index.js +9 -0
  36. package/dist/infra/e2b/types.js +2 -0
  37. package/dist/infra/provider.js +67 -0
  38. package/dist/infra/providers/anthropic.js +308 -0
  39. package/dist/infra/providers/core/errors.js +353 -0
  40. package/dist/infra/providers/core/fork.js +418 -0
  41. package/dist/infra/providers/core/index.js +76 -0
  42. package/dist/infra/providers/core/logger.js +191 -0
  43. package/dist/infra/providers/core/retry.js +189 -0
  44. package/dist/infra/providers/core/usage.js +376 -0
  45. package/dist/infra/providers/gemini.js +493 -0
  46. package/dist/infra/providers/index.js +83 -0
  47. package/dist/infra/providers/openai.js +662 -0
  48. package/dist/infra/providers/types.js +20 -0
  49. package/dist/infra/providers/utils.js +400 -0
  50. package/dist/infra/sandbox-factory.js +30 -0
  51. package/dist/infra/sandbox.js +243 -0
  52. package/dist/infra/store/factory.js +80 -0
  53. package/dist/infra/store/index.js +26 -0
  54. package/dist/infra/store/json-store.js +606 -0
  55. package/dist/infra/store/types.js +2 -0
  56. package/dist/infra/store.js +29 -0
  57. package/dist/tools/bash_kill/index.js +35 -0
  58. package/dist/tools/bash_kill/prompt.js +14 -0
  59. package/dist/tools/bash_logs/index.js +40 -0
  60. package/dist/tools/bash_logs/prompt.js +14 -0
  61. package/dist/tools/bash_run/index.js +61 -0
  62. package/dist/tools/bash_run/prompt.js +18 -0
  63. package/dist/tools/builtin.js +26 -0
  64. package/dist/tools/define.js +214 -0
  65. package/dist/tools/fs_edit/index.js +62 -0
  66. package/dist/tools/fs_edit/prompt.js +15 -0
  67. package/dist/tools/fs_glob/index.js +40 -0
  68. package/dist/tools/fs_glob/prompt.js +15 -0
  69. package/dist/tools/fs_grep/index.js +66 -0
  70. package/dist/tools/fs_grep/prompt.js +16 -0
  71. package/dist/tools/fs_multi_edit/index.js +106 -0
  72. package/dist/tools/fs_multi_edit/prompt.js +16 -0
  73. package/dist/tools/fs_read/index.js +40 -0
  74. package/dist/tools/fs_read/prompt.js +16 -0
  75. package/dist/tools/fs_write/index.js +40 -0
  76. package/dist/tools/fs_write/prompt.js +15 -0
  77. package/dist/tools/index.js +61 -0
  78. package/dist/tools/mcp.js +185 -0
  79. package/dist/tools/registry.js +26 -0
  80. package/dist/tools/scripts.js +205 -0
  81. package/dist/tools/skills.js +115 -0
  82. package/dist/tools/task_run/index.js +58 -0
  83. package/dist/tools/task_run/prompt.js +25 -0
  84. package/dist/tools/todo_read/index.js +29 -0
  85. package/dist/tools/todo_read/prompt.js +18 -0
  86. package/dist/tools/todo_write/index.js +42 -0
  87. package/dist/tools/todo_write/prompt.js +23 -0
  88. package/dist/tools/tool.js +211 -0
  89. package/dist/tools/toolkit.js +98 -0
  90. package/dist/tools/type-inference.js +207 -0
  91. package/dist/utils/agent-id.js +28 -0
  92. package/dist/utils/logger.js +44 -0
  93. package/dist/utils/session-id.js +64 -0
  94. package/package.json +7 -38
  95. package/dist/index.js.map +0 -7
  96. package/dist/index.mjs +0 -60385
  97. package/dist/index.mjs.map +0 -7
@@ -0,0 +1,400 @@
1
+ "use strict";
2
+ /**
3
+ * Shared utilities for provider implementations.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.AUDIO_UNSUPPORTED_TEXT = exports.IMAGE_UNSUPPORTED_TEXT = exports.FILE_UNSUPPORTED_TEXT = void 0;
7
+ exports.resolveProxyUrl = resolveProxyUrl;
8
+ exports.getProxyDispatcher = getProxyDispatcher;
9
+ exports.withProxy = withProxy;
10
+ exports.normalizeBaseUrl = normalizeBaseUrl;
11
+ exports.normalizeOpenAIBaseUrl = normalizeOpenAIBaseUrl;
12
+ exports.normalizeAnthropicBaseUrl = normalizeAnthropicBaseUrl;
13
+ exports.normalizeGeminiBaseUrl = normalizeGeminiBaseUrl;
14
+ exports.getMessageBlocks = getMessageBlocks;
15
+ exports.markTransportIfDegraded = markTransportIfDegraded;
16
+ exports.joinTextBlocks = joinTextBlocks;
17
+ exports.formatToolResult = formatToolResult;
18
+ exports.safeJsonStringify = safeJsonStringify;
19
+ exports.concatTextWithReasoning = concatTextWithReasoning;
20
+ exports.joinReasoningBlocks = joinReasoningBlocks;
21
+ exports.normalizeThinkBlocks = normalizeThinkBlocks;
22
+ exports.splitThinkText = splitThinkText;
23
+ exports.extractReasoningDetails = extractReasoningDetails;
24
+ exports.buildGeminiImagePart = buildGeminiImagePart;
25
+ exports.buildGeminiFilePart = buildGeminiFilePart;
26
+ exports.sanitizeGeminiSchema = sanitizeGeminiSchema;
27
+ exports.hasAnthropicFileBlocks = hasAnthropicFileBlocks;
28
+ exports.mergeAnthropicBetaHeader = mergeAnthropicBetaHeader;
29
+ exports.normalizeAnthropicContent = normalizeAnthropicContent;
30
+ exports.normalizeAnthropicContentBlock = normalizeAnthropicContentBlock;
31
+ exports.normalizeAnthropicDelta = normalizeAnthropicDelta;
32
+ // =============================================================================
33
+ // Proxy Handling
34
+ // =============================================================================
35
+ const proxyAgents = new Map();
36
+ function resolveProxyUrl(explicit) {
37
+ if (explicit)
38
+ return explicit;
39
+ const flag = process.env.KODE_USE_ENV_PROXY;
40
+ if (!flag || ['0', 'false', 'no'].includes(flag.toLowerCase())) {
41
+ return undefined;
42
+ }
43
+ return (process.env.HTTPS_PROXY ||
44
+ process.env.https_proxy ||
45
+ process.env.HTTP_PROXY ||
46
+ process.env.http_proxy ||
47
+ process.env.ALL_PROXY ||
48
+ process.env.all_proxy);
49
+ }
50
+ function getProxyDispatcher(proxyUrl) {
51
+ const resolved = resolveProxyUrl(proxyUrl);
52
+ if (!resolved)
53
+ return undefined;
54
+ const cached = proxyAgents.get(resolved);
55
+ if (cached)
56
+ return cached;
57
+ let ProxyAgent;
58
+ try {
59
+ ({ ProxyAgent } = require('undici'));
60
+ }
61
+ catch (error) {
62
+ throw new Error(`Proxy support requires undici. Install it to use proxyUrl (${error?.message || error}).`);
63
+ }
64
+ const agent = new ProxyAgent(resolved);
65
+ proxyAgents.set(resolved, agent);
66
+ return agent;
67
+ }
68
+ function withProxy(init, dispatcher) {
69
+ if (!dispatcher)
70
+ return init;
71
+ return { ...init, dispatcher };
72
+ }
73
+ // =============================================================================
74
+ // URL Normalization
75
+ // =============================================================================
76
+ function normalizeBaseUrl(url) {
77
+ return url.replace(/\/+$/, '');
78
+ }
79
+ function normalizeOpenAIBaseUrl(url) {
80
+ let normalized = url.replace(/\/+$/, '');
81
+ // Auto-append /v1 if not present (for OpenAI-compatible APIs)
82
+ if (!normalized.endsWith('/v1')) {
83
+ normalized += '/v1';
84
+ }
85
+ return normalized;
86
+ }
87
+ function normalizeAnthropicBaseUrl(url) {
88
+ let normalized = url.replace(/\/+$/, '');
89
+ if (normalized.endsWith('/v1')) {
90
+ normalized = normalized.slice(0, -3);
91
+ }
92
+ return normalized;
93
+ }
94
+ function normalizeGeminiBaseUrl(url) {
95
+ let normalized = url.replace(/\/+$/, '');
96
+ // Auto-append /v1beta if no version path present
97
+ if (!normalized.endsWith('/v1beta') && !normalized.endsWith('/v1')) {
98
+ normalized += '/v1beta';
99
+ }
100
+ return normalized;
101
+ }
102
+ // =============================================================================
103
+ // Content Block Utilities
104
+ // =============================================================================
105
+ function getMessageBlocks(message) {
106
+ if (message.metadata?.transport === 'omit') {
107
+ return message.content;
108
+ }
109
+ return message.metadata?.content_blocks ?? message.content;
110
+ }
111
+ function markTransportIfDegraded(message, blocks) {
112
+ if (message.metadata?.transport === 'omit') {
113
+ return;
114
+ }
115
+ if (!message.metadata) {
116
+ message.metadata = { content_blocks: blocks, transport: 'text' };
117
+ return;
118
+ }
119
+ if (!message.metadata.content_blocks) {
120
+ message.metadata.content_blocks = blocks;
121
+ }
122
+ message.metadata.transport = 'text';
123
+ }
124
+ // =============================================================================
125
+ // Text Formatting
126
+ // =============================================================================
127
+ function joinTextBlocks(blocks) {
128
+ return blocks
129
+ .filter((block) => block.type === 'text')
130
+ .map((block) => block.text)
131
+ .join('');
132
+ }
133
+ function formatToolResult(content) {
134
+ if (typeof content === 'string')
135
+ return content;
136
+ return safeJsonStringify(content);
137
+ }
138
+ function safeJsonStringify(value) {
139
+ try {
140
+ const json = JSON.stringify(value ?? {});
141
+ return json === undefined ? '{}' : json;
142
+ }
143
+ catch {
144
+ return '{}';
145
+ }
146
+ }
147
+ // =============================================================================
148
+ // Unsupported Content Messages
149
+ // =============================================================================
150
+ exports.FILE_UNSUPPORTED_TEXT = '[file unsupported] This model does not support PDF input. Please extract text or images first.';
151
+ exports.IMAGE_UNSUPPORTED_TEXT = '[image unsupported] This model does not support image URLs; please provide base64 data if supported.';
152
+ exports.AUDIO_UNSUPPORTED_TEXT = '[audio unsupported] This model does not support audio input; please provide a text transcript instead.';
153
+ // =============================================================================
154
+ // Reasoning/Thinking Utilities
155
+ // =============================================================================
156
+ function concatTextWithReasoning(blocks, reasoningTransport = 'text') {
157
+ let text = '';
158
+ for (const block of blocks) {
159
+ if (block.type === 'text') {
160
+ text += block.text;
161
+ }
162
+ else if (block.type === 'reasoning' && reasoningTransport === 'text') {
163
+ text += `<think>${block.reasoning}</think>`;
164
+ }
165
+ }
166
+ return text;
167
+ }
168
+ function joinReasoningBlocks(blocks) {
169
+ return blocks
170
+ .filter((block) => block.type === 'reasoning')
171
+ .map((block) => block.reasoning)
172
+ .join('\n');
173
+ }
174
+ /**
175
+ * Parse <think> tags in text blocks and convert to reasoning blocks.
176
+ */
177
+ function normalizeThinkBlocks(blocks, reasoningTransport = 'text') {
178
+ if (reasoningTransport !== 'text') {
179
+ return blocks;
180
+ }
181
+ const output = [];
182
+ for (const block of blocks) {
183
+ if (block.type !== 'text') {
184
+ output.push(block);
185
+ continue;
186
+ }
187
+ const parts = splitThinkText(block.text);
188
+ if (parts.length === 0) {
189
+ output.push(block);
190
+ }
191
+ else {
192
+ output.push(...parts);
193
+ }
194
+ }
195
+ return output;
196
+ }
197
+ function splitThinkText(text) {
198
+ const blocks = [];
199
+ const regex = /<think>([\s\S]*?)<\/think>/g;
200
+ let match;
201
+ let cursor = 0;
202
+ let matched = false;
203
+ while ((match = regex.exec(text)) !== null) {
204
+ matched = true;
205
+ const before = text.slice(cursor, match.index);
206
+ if (before) {
207
+ blocks.push({ type: 'text', text: before });
208
+ }
209
+ const reasoning = match[1] || '';
210
+ blocks.push({ type: 'reasoning', reasoning });
211
+ cursor = match.index + match[0].length;
212
+ }
213
+ if (!matched) {
214
+ return [];
215
+ }
216
+ const after = text.slice(cursor);
217
+ if (after) {
218
+ blocks.push({ type: 'text', text: after });
219
+ }
220
+ return blocks;
221
+ }
222
+ /**
223
+ * Extract reasoning details from OpenAI response (for reasoning models).
224
+ */
225
+ function extractReasoningDetails(message) {
226
+ const details = Array.isArray(message?.reasoning_details) ? message.reasoning_details : [];
227
+ const content = typeof message?.reasoning_content === 'string' ? message.reasoning_content : undefined;
228
+ const blocks = [];
229
+ for (const detail of details) {
230
+ if (typeof detail?.text === 'string') {
231
+ blocks.push({ type: 'reasoning', reasoning: detail.text });
232
+ }
233
+ }
234
+ if (content) {
235
+ blocks.push({ type: 'reasoning', reasoning: content });
236
+ }
237
+ return blocks;
238
+ }
239
+ // =============================================================================
240
+ // Gemini Helpers
241
+ // =============================================================================
242
+ function buildGeminiImagePart(block) {
243
+ if (block.file_id) {
244
+ return { file_data: { mime_type: block.mime_type, file_uri: block.file_id } };
245
+ }
246
+ if (block.url) {
247
+ if (block.url.startsWith('gs://')) {
248
+ return { file_data: { mime_type: block.mime_type, file_uri: block.url } };
249
+ }
250
+ return null;
251
+ }
252
+ if (block.base64 && block.mime_type) {
253
+ return { inline_data: { mime_type: block.mime_type, data: block.base64 } };
254
+ }
255
+ return null;
256
+ }
257
+ function buildGeminiFilePart(block) {
258
+ const mimeType = block.mime_type || 'application/pdf';
259
+ if (block.file_id) {
260
+ return { file_data: { mime_type: mimeType, file_uri: block.file_id } };
261
+ }
262
+ if (block.url) {
263
+ if (block.url.startsWith('gs://')) {
264
+ return { file_data: { mime_type: mimeType, file_uri: block.url } };
265
+ }
266
+ return null;
267
+ }
268
+ if (block.base64) {
269
+ return { inline_data: { mime_type: mimeType, data: block.base64 } };
270
+ }
271
+ return null;
272
+ }
273
+ function sanitizeGeminiSchema(schema) {
274
+ if (schema === null || schema === undefined)
275
+ return schema;
276
+ if (Array.isArray(schema))
277
+ return schema.map((item) => sanitizeGeminiSchema(item));
278
+ if (typeof schema !== 'object')
279
+ return schema;
280
+ const cleaned = {};
281
+ for (const [key, value] of Object.entries(schema)) {
282
+ if (key === 'additionalProperties' || key === '$schema' || key === '$defs' || key === 'definitions') {
283
+ continue;
284
+ }
285
+ cleaned[key] = sanitizeGeminiSchema(value);
286
+ }
287
+ return cleaned;
288
+ }
289
+ // =============================================================================
290
+ // Anthropic Helpers
291
+ // =============================================================================
292
+ function hasAnthropicFileBlocks(messages) {
293
+ for (const msg of messages) {
294
+ const blocks = getMessageBlocks(msg);
295
+ for (const block of blocks) {
296
+ if (block.type === 'file' && block.file_id) {
297
+ return true;
298
+ }
299
+ }
300
+ }
301
+ return false;
302
+ }
303
+ function mergeAnthropicBetaHeader(existing, entries) {
304
+ const set = new Set();
305
+ if (existing) {
306
+ for (const e of existing.split(',')) {
307
+ const trimmed = e.trim();
308
+ if (trimmed)
309
+ set.add(trimmed);
310
+ }
311
+ }
312
+ for (const e of entries) {
313
+ if (e)
314
+ set.add(e);
315
+ }
316
+ return set.size > 0 ? Array.from(set).join(',') : undefined;
317
+ }
318
+ /**
319
+ * Normalize Anthropic response content to internal format.
320
+ */
321
+ function normalizeAnthropicContent(content, reasoningTransport) {
322
+ if (!Array.isArray(content))
323
+ return [];
324
+ const blocks = [];
325
+ for (const block of content) {
326
+ const normalized = normalizeAnthropicContentBlock(block, reasoningTransport);
327
+ if (normalized)
328
+ blocks.push(normalized);
329
+ }
330
+ return blocks;
331
+ }
332
+ /**
333
+ * Normalize a single Anthropic content block.
334
+ * Handles thinking blocks with signature preservation.
335
+ */
336
+ function normalizeAnthropicContentBlock(block, reasoningTransport) {
337
+ if (!block || typeof block !== 'object')
338
+ return null;
339
+ // Handle thinking blocks - preserve signature for conversation continuity
340
+ if (block.type === 'thinking') {
341
+ if (reasoningTransport === 'text') {
342
+ return { type: 'text', text: `<think>${block.thinking ?? ''}</think>` };
343
+ }
344
+ const result = { type: 'reasoning', reasoning: block.thinking ?? '' };
345
+ // Preserve signature for multi-turn conversations (critical for Claude 4+)
346
+ if (block.signature) {
347
+ result.meta = { signature: block.signature };
348
+ }
349
+ return result;
350
+ }
351
+ if (block.type === 'text') {
352
+ return { type: 'text', text: block.text ?? '' };
353
+ }
354
+ if (block.type === 'image' && block.source?.type === 'base64') {
355
+ return {
356
+ type: 'image',
357
+ base64: block.source.data,
358
+ mime_type: block.source.media_type,
359
+ };
360
+ }
361
+ if (block.type === 'document' && block.source?.type === 'file') {
362
+ return {
363
+ type: 'file',
364
+ file_id: block.source.file_id,
365
+ mime_type: block.source.media_type,
366
+ };
367
+ }
368
+ if (block.type === 'tool_use') {
369
+ return {
370
+ type: 'tool_use',
371
+ id: block.id,
372
+ name: block.name,
373
+ input: block.input ?? {},
374
+ };
375
+ }
376
+ if (block.type === 'tool_result') {
377
+ return {
378
+ type: 'tool_result',
379
+ tool_use_id: block.tool_use_id,
380
+ content: block.content,
381
+ is_error: block.is_error,
382
+ };
383
+ }
384
+ return null;
385
+ }
386
+ /**
387
+ * Normalize Anthropic streaming delta.
388
+ */
389
+ function normalizeAnthropicDelta(delta) {
390
+ if (!delta) {
391
+ return { type: 'text_delta', text: '' };
392
+ }
393
+ if (delta.type === 'thinking_delta') {
394
+ return { type: 'reasoning_delta', text: delta.thinking ?? '' };
395
+ }
396
+ if (delta.type === 'input_json_delta') {
397
+ return { type: 'input_json_delta', partial_json: delta.partial_json ?? '' };
398
+ }
399
+ return { type: 'text_delta', text: delta.text ?? '' };
400
+ }
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SandboxFactory = void 0;
4
+ const sandbox_1 = require("./sandbox");
5
+ const e2b_sandbox_1 = require("./e2b/e2b-sandbox");
6
+ class SandboxFactory {
7
+ constructor() {
8
+ this.factories = new Map();
9
+ this.factories.set('local', (config) => new sandbox_1.LocalSandbox(config));
10
+ this.factories.set('e2b', (config) => new e2b_sandbox_1.E2BSandbox(config));
11
+ }
12
+ register(kind, factory) {
13
+ this.factories.set(kind, factory);
14
+ }
15
+ create(config) {
16
+ const factory = this.factories.get(config.kind);
17
+ if (!factory) {
18
+ throw new Error(`Sandbox factory not registered: ${config.kind}`);
19
+ }
20
+ return factory(config);
21
+ }
22
+ async createAsync(config) {
23
+ const sandbox = this.create(config);
24
+ if (config.kind === 'e2b' && sandbox instanceof e2b_sandbox_1.E2BSandbox) {
25
+ await sandbox.init();
26
+ }
27
+ return sandbox;
28
+ }
29
+ }
30
+ exports.SandboxFactory = SandboxFactory;
@@ -0,0 +1,243 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LocalSandbox = void 0;
4
+ // 危险命令模式 - 防止执行破坏性操作
5
+ const DANGEROUS_PATTERNS = [
6
+ /rm\s+-rf\s+\/($|\s)/, // rm -rf /
7
+ /sudo\s+/, // sudo 提权
8
+ /shutdown/, // 系统关机
9
+ /reboot/, // 系统重启
10
+ /mkfs\./, // 格式化文件系统
11
+ /dd\s+.*of=/, // dd 写入设备
12
+ /:\(\)\{\s*:\|\:&\s*\};:/, // fork bomb
13
+ /chmod\s+777\s+\//, // 修改根目录权限
14
+ /curl\s+.*\|\s*(bash|sh)/, // 管道执行远程脚本
15
+ /wget\s+.*\|\s*(bash|sh)/, // wget 执行远程脚本
16
+ />\s*\/dev\/sda/, // 直接写入硬盘
17
+ /mkswap/, // 创建交换分区
18
+ /swapon/, // 启用交换分区
19
+ ];
20
+ class LocalSandbox {
21
+ constructor(opts = {}) {
22
+ this.kind = 'local';
23
+ this.watchers = new Map();
24
+ const path = require('path');
25
+ this.workDir = path.resolve(opts.workDir || opts.baseDir || opts.pwd || process.cwd());
26
+ this.enforceBoundary = opts.enforceBoundary !== false;
27
+ this.allowPaths = (opts.allowPaths || []).map((p) => path.resolve(p));
28
+ this.watchEnabled = opts.watchFiles !== false; // default true
29
+ this.fs = new LocalFS(this.workDir, {
30
+ enforceBoundary: this.enforceBoundary,
31
+ allowPaths: this.allowPaths,
32
+ });
33
+ }
34
+ async exec(cmd, opts) {
35
+ // 安全检查:阻止危险命令
36
+ for (const pattern of DANGEROUS_PATTERNS) {
37
+ if (pattern.test(cmd)) {
38
+ const error = `Dangerous command blocked for security: ${cmd.slice(0, 100)}`;
39
+ return {
40
+ code: 1,
41
+ stdout: '',
42
+ stderr: error,
43
+ };
44
+ }
45
+ }
46
+ const { exec } = require('child_process');
47
+ const util = require('util');
48
+ const execPromise = util.promisify(exec);
49
+ const timeout = opts?.timeoutMs || 120000;
50
+ try {
51
+ const { stdout, stderr } = await execPromise(cmd, {
52
+ cwd: this.workDir,
53
+ timeout,
54
+ maxBuffer: 10 * 1024 * 1024,
55
+ });
56
+ return { code: 0, stdout: stdout || '', stderr: stderr || '' };
57
+ }
58
+ catch (error) {
59
+ return {
60
+ code: error.code || 1,
61
+ stdout: error.stdout || '',
62
+ stderr: error.stderr || error.message || '',
63
+ };
64
+ }
65
+ }
66
+ static local(opts) {
67
+ return new LocalSandbox(opts);
68
+ }
69
+ async watchFiles(paths, listener) {
70
+ if (!this.watchEnabled) {
71
+ return `watch-disabled-${Date.now()}`;
72
+ }
73
+ const id = `watch-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
74
+ const fs = require('fs');
75
+ const watchers = [];
76
+ for (const path of paths) {
77
+ const resolved = this.fs.resolve(path);
78
+ if (!this.fs.isInside(resolved))
79
+ continue;
80
+ const watcher = fs.watch(resolved, async () => {
81
+ try {
82
+ const stat = await this.fs.stat(resolved);
83
+ listener({ path: resolved, mtimeMs: stat.mtimeMs });
84
+ }
85
+ catch {
86
+ listener({ path: resolved, mtimeMs: Date.now() });
87
+ }
88
+ });
89
+ watchers.push(watcher);
90
+ }
91
+ this.watchers.set(id, {
92
+ paths,
93
+ close: () => watchers.forEach((w) => w.close()),
94
+ });
95
+ return id;
96
+ }
97
+ unwatchFiles(id) {
98
+ const entry = this.watchers.get(id);
99
+ if (entry) {
100
+ entry.close();
101
+ this.watchers.delete(id);
102
+ }
103
+ }
104
+ async dispose() {
105
+ for (const entry of this.watchers.values()) {
106
+ entry.close();
107
+ }
108
+ this.watchers.clear();
109
+ }
110
+ }
111
+ exports.LocalSandbox = LocalSandbox;
112
+ class LocalFS {
113
+ constructor(workDir, options) {
114
+ this.workDir = workDir;
115
+ this.options = options;
116
+ }
117
+ resolve(p) {
118
+ const path = require('path');
119
+ if (path.isAbsolute(p))
120
+ return p;
121
+ return path.resolve(this.workDir, p);
122
+ }
123
+ isInside(p) {
124
+ const path = require('path');
125
+ const resolved = path.resolve(this.resolve(p)); // resolve 去除 ..
126
+ // 1. 检查是否在 workDir 内
127
+ const relativeToWork = path.relative(this.workDir, resolved);
128
+ if (!relativeToWork.startsWith('..') && !path.isAbsolute(relativeToWork)) {
129
+ return true;
130
+ }
131
+ // 2. 如果不强制边界检查,允许所有路径
132
+ if (!this.options.enforceBoundary)
133
+ return true;
134
+ // 3. 检查白名单(先 resolve 防止绕过)
135
+ return this.options.allowPaths.some((allowed) => {
136
+ const resolvedAllowed = path.resolve(allowed); // 先 resolve
137
+ const relative = path.relative(resolvedAllowed, resolved);
138
+ return !relative.startsWith('..') && !path.isAbsolute(relative);
139
+ });
140
+ }
141
+ async read(p) {
142
+ const fs = require('fs').promises;
143
+ const resolved = this.resolve(p);
144
+ if (!this.isInside(resolved)) {
145
+ throw new Error(`Path outside sandbox: ${p}`);
146
+ }
147
+ return await fs.readFile(resolved, 'utf-8');
148
+ }
149
+ async write(p, content) {
150
+ const fs = require('fs').promises;
151
+ const path = require('path');
152
+ const resolved = this.resolve(p);
153
+ if (!this.isInside(resolved)) {
154
+ throw new Error(`Path outside sandbox: ${p}`);
155
+ }
156
+ const dir = path.dirname(resolved);
157
+ await fs.mkdir(dir, { recursive: true });
158
+ await fs.writeFile(resolved, content, 'utf-8');
159
+ }
160
+ temp(name) {
161
+ const path = require('path');
162
+ const tempName = name || `temp-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
163
+ return path.relative(this.workDir, path.join(this.workDir, '.temp', tempName));
164
+ }
165
+ async stat(p) {
166
+ const fs = require('fs').promises;
167
+ const resolved = this.resolve(p);
168
+ if (!this.isInside(resolved)) {
169
+ throw new Error(`Path outside sandbox: ${p}`);
170
+ }
171
+ const stat = await fs.stat(resolved);
172
+ return { mtimeMs: stat.mtimeMs };
173
+ }
174
+ async glob(pattern, opts) {
175
+ const path = require('path');
176
+ const cwd = opts?.cwd ? this.resolve(opts.cwd) : this.workDir;
177
+ let matches;
178
+ try {
179
+ const fg = require('fast-glob');
180
+ matches = await fg(pattern, {
181
+ cwd,
182
+ dot: opts?.dot ?? false,
183
+ absolute: true,
184
+ ignore: opts?.ignore,
185
+ });
186
+ }
187
+ catch {
188
+ matches = await this.manualGlob(pattern, { cwd, dot: opts?.dot ?? false });
189
+ }
190
+ const filtered = matches.filter((entry) => this.isInside(entry));
191
+ if (opts?.absolute) {
192
+ return filtered;
193
+ }
194
+ return filtered.map((entry) => path.relative(this.workDir, entry));
195
+ }
196
+ async manualGlob(pattern, opts) {
197
+ const fs = require('fs').promises;
198
+ const path = require('path');
199
+ const normalizedPattern = pattern.split(path.sep).join('/');
200
+ const results = [];
201
+ const walk = async (dir) => {
202
+ const entries = await fs.readdir(dir, { withFileTypes: true });
203
+ for (const entry of entries) {
204
+ if (!opts.dot && entry.name.startsWith('.'))
205
+ continue;
206
+ const full = path.join(dir, entry.name);
207
+ const rel = path.relative(opts.cwd, full).split(path.sep).join('/');
208
+ if (matchesGlob(normalizedPattern, rel)) {
209
+ results.push(full);
210
+ }
211
+ if (entry.isDirectory()) {
212
+ await walk(full);
213
+ }
214
+ }
215
+ };
216
+ await walk(opts.cwd);
217
+ return results;
218
+ }
219
+ }
220
+ function matchesGlob(pattern, target) {
221
+ const pSegs = pattern.split('/');
222
+ const tSegs = target.split('/');
223
+ return matchSegments(pSegs, tSegs);
224
+ }
225
+ function matchSegments(pattern, target) {
226
+ if (pattern.length === 0)
227
+ return target.length === 0;
228
+ const [head, ...rest] = pattern;
229
+ if (head === '**') {
230
+ return (matchSegments(rest, target) ||
231
+ (target.length > 0 && matchSegments(pattern, target.slice(1))));
232
+ }
233
+ if (target.length === 0)
234
+ return false;
235
+ if (!matchSegment(head, target[0]))
236
+ return false;
237
+ return matchSegments(rest, target.slice(1));
238
+ }
239
+ function matchSegment(pattern, target) {
240
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
241
+ const regex = escaped.replace(/\*/g, '.*').replace(/\?/g, '.');
242
+ return new RegExp(`^${regex}$`).test(target);
243
+ }