micro-models-agent 0.51.1 â 0.52.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/dist/cli/commands.js +162 -38
- package/dist/cli/completer.js +5 -5
- package/dist/cli/main.js +42 -54
- package/dist/cli/repl-commands.js +138 -38
- package/dist/cli/repl.js +175 -89
- package/dist/cli/run-result.js +11 -0
- package/dist/cli/security-commands.js +6 -6
- package/dist/cli/setup.js +21 -15
- package/dist/config/config.js +54 -27
- package/dist/config/defaults.js +17 -0
- package/dist/config/domains.js +179 -0
- package/dist/config/index.js +2 -1
- package/dist/config/security.js +28 -8
- package/dist/core/agent.js +162 -30
- package/dist/core/bootstrap.js +94 -17
- package/dist/core/crash-handler.js +51 -0
- package/dist/core/environment.js +199 -0
- package/dist/core/session-logger.js +60 -6
- package/dist/core/version.js +2 -0
- package/dist/i18n/en.json +120 -39
- package/dist/i18n/ru.json +91 -10
- package/dist/llm/openai-compat.js +191 -53
- package/dist/llm/orchestrator.js +5 -3
- package/dist/logger/app-logger.js +50 -4
- package/dist/main.js +1288 -635
- package/dist/modules/browser/session.js +4 -0
- package/dist/modules/certification/cli.js +58 -19
- package/dist/modules/certification/loader.js +2 -1
- package/dist/modules/certification/manifest.js +22 -14
- package/dist/modules/certification/runner.js +91 -5
- package/dist/modules/certification/scenarios.js +290 -7
- package/dist/modules/context/fact-extractor.js +6 -0
- package/dist/modules/context/manager.js +19 -2
- package/dist/modules/execution/audit-runners.js +61 -7
- package/dist/modules/execution/execution-plugin.js +219 -60
- package/dist/modules/execution/module.js +207 -18
- package/dist/modules/execution/moe-executor.js +33 -20
- package/dist/modules/execution/plan-store.js +39 -0
- package/dist/modules/execution/plan-tool.js +188 -19
- package/dist/modules/execution/planner.js +27 -23
- package/dist/modules/execution/stuck-detector.js +244 -8
- package/dist/modules/execution/tracker.js +8 -6
- package/dist/modules/execution/verifier.js +15 -2
- package/dist/modules/hallucination/detector.js +4 -0
- package/dist/modules/hallucination/factual.js +45 -5
- package/dist/modules/indexer/module.js +1 -0
- package/dist/modules/lsp/client.js +123 -12
- package/dist/modules/lsp/index.js +1 -1
- package/dist/modules/lsp/module.js +30 -2
- package/dist/modules/lsp/probe.js +11 -1
- package/dist/modules/lsp/startup-check.js +5 -2
- package/dist/modules/plugins/builtin/lint-on-write.js +144 -41
- package/dist/modules/plugins/manager.js +57 -13
- package/dist/modules/pricing/index.js +61 -0
- package/dist/modules/pricing/prices.js +129 -0
- package/dist/modules/providers/create.js +22 -0
- package/dist/modules/providers/fallback.js +79 -0
- package/dist/modules/providers/health.js +46 -0
- package/dist/modules/providers/index.js +5 -0
- package/dist/modules/providers/manager.js +161 -0
- package/dist/modules/providers/presets.js +128 -0
- package/dist/modules/providers/registry.js +22 -0
- package/dist/modules/providers/types.js +1 -0
- package/dist/modules/registry.js +1 -0
- package/dist/modules/security/command-validator.js +14 -0
- package/dist/modules/security/encryption.js +6 -6
- package/dist/modules/security/network-validator.js +17 -0
- package/dist/modules/security/path-validator.js +22 -26
- package/dist/modules/session/store.js +10 -10
- package/dist/tools/approve.js +1 -0
- package/dist/tools/attach-image.js +12 -0
- package/dist/tools/bash.js +27 -4
- package/dist/tools/browser.js +1 -0
- package/dist/tools/chunk-query.js +1 -0
- package/dist/tools/create-dir.js +1 -0
- package/dist/tools/delete-file.js +1 -0
- package/dist/tools/download-file.js +1 -0
- package/dist/tools/edit-file.js +2 -1
- package/dist/tools/enable-tools.js +1 -0
- package/dist/tools/executor.js +17 -7
- package/dist/tools/file-info.js +1 -0
- package/dist/tools/glob-tool.js +1 -0
- package/dist/tools/grep-tool.js +54 -13
- package/dist/tools/list-dir.js +1 -0
- package/dist/tools/load-skill.js +1 -0
- package/dist/tools/mcp-call.js +1 -0
- package/dist/tools/move-file.js +1 -0
- package/dist/tools/path-utils.js +51 -1
- package/dist/tools/pipeline-run.js +1 -0
- package/dist/tools/process-kill.js +11 -0
- package/dist/tools/process-list.js +1 -0
- package/dist/tools/process-log.js +9 -0
- package/dist/tools/question.js +1 -0
- package/dist/tools/read-file.js +94 -6
- package/dist/tools/recall.js +1 -0
- package/dist/tools/remember.js +1 -0
- package/dist/tools/scope-check.js +7 -5
- package/dist/tools/search-history.js +1 -0
- package/dist/tools/subagent.js +4 -4
- package/dist/tools/web-browse.js +1 -0
- package/dist/tools/web-fetch.js +27 -6
- package/dist/tools/web-search.js +70 -43
- package/dist/tools/write-file.js +1 -0
- package/dist/ui/line-editor.js +142 -23
- package/dist/ui/line-math.js +8 -4
- package/dist/ui/renderer.js +57 -7
- package/package.json +50 -48
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// src/modules/providers/presets.ts
|
|
2
|
+
import { OpenAICompatProvider } from "../../llm/openai-compat";
|
|
3
|
+
/** Build an OpenAI-compatible provider (default for most backends). */
|
|
4
|
+
function openaiCompat(opts) {
|
|
5
|
+
const retry = (opts.retry ?? {
|
|
6
|
+
maxRetries: 3,
|
|
7
|
+
baseDelay: 1000,
|
|
8
|
+
maxDelay: 30000,
|
|
9
|
+
});
|
|
10
|
+
const rateLimits = opts.rateLimits;
|
|
11
|
+
return new OpenAICompatProvider({
|
|
12
|
+
model: opts.model,
|
|
13
|
+
baseUrl: opts.baseUrl,
|
|
14
|
+
apiKey: opts.apiKey,
|
|
15
|
+
contextWindow: opts.contextWindow,
|
|
16
|
+
retry,
|
|
17
|
+
rateLimits,
|
|
18
|
+
maxCompletionTokens: opts.maxCompletionTokens,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
const OPENAI_COMPAT = {
|
|
22
|
+
type: "openai-compat",
|
|
23
|
+
label: "OpenAI-compatible (LM Studio, Ollama, vLLM, etc.)",
|
|
24
|
+
baseUrl: "",
|
|
25
|
+
capabilities: {
|
|
26
|
+
streaming: true,
|
|
27
|
+
tools: true,
|
|
28
|
+
vision: false,
|
|
29
|
+
reasoningEffort: false,
|
|
30
|
+
listModels: true,
|
|
31
|
+
requiresKey: false,
|
|
32
|
+
auth: "bearer",
|
|
33
|
+
},
|
|
34
|
+
create: openaiCompat,
|
|
35
|
+
};
|
|
36
|
+
const OPENROUTER = {
|
|
37
|
+
type: "openrouter",
|
|
38
|
+
label: "OpenRouter (openrouter.ai)",
|
|
39
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
40
|
+
capabilities: {
|
|
41
|
+
streaming: true,
|
|
42
|
+
tools: true,
|
|
43
|
+
vision: true,
|
|
44
|
+
reasoningEffort: false,
|
|
45
|
+
listModels: true,
|
|
46
|
+
requiresKey: true,
|
|
47
|
+
auth: "bearer",
|
|
48
|
+
},
|
|
49
|
+
create: openaiCompat,
|
|
50
|
+
};
|
|
51
|
+
const OPENAI = {
|
|
52
|
+
type: "openai",
|
|
53
|
+
label: "OpenAI API",
|
|
54
|
+
baseUrl: "https://api.openai.com/v1",
|
|
55
|
+
capabilities: {
|
|
56
|
+
streaming: true,
|
|
57
|
+
tools: true,
|
|
58
|
+
vision: true,
|
|
59
|
+
reasoningEffort: true,
|
|
60
|
+
listModels: true,
|
|
61
|
+
requiresKey: true,
|
|
62
|
+
auth: "bearer",
|
|
63
|
+
},
|
|
64
|
+
create: openaiCompat,
|
|
65
|
+
};
|
|
66
|
+
const ANTHROPIC = {
|
|
67
|
+
type: "anthropic",
|
|
68
|
+
label: "Anthropic Claude",
|
|
69
|
+
baseUrl: "https://api.anthropic.com",
|
|
70
|
+
capabilities: {
|
|
71
|
+
streaming: false,
|
|
72
|
+
tools: false,
|
|
73
|
+
vision: true,
|
|
74
|
+
reasoningEffort: false,
|
|
75
|
+
listModels: false,
|
|
76
|
+
requiresKey: true,
|
|
77
|
+
auth: "header",
|
|
78
|
+
},
|
|
79
|
+
// The Anthropic messages API is NOT OpenAI-compatible. This preset is a
|
|
80
|
+
// placeholder until a real AnthropicProvider lands; the factory still uses
|
|
81
|
+
// the OpenAI-compatible code path today (broken), flagged via capabilities.
|
|
82
|
+
create: openaiCompat,
|
|
83
|
+
};
|
|
84
|
+
const OPENCODE_ZEN = {
|
|
85
|
+
type: "opencode-zen",
|
|
86
|
+
label: "OpenCode Zen (opencode.ai/zen)",
|
|
87
|
+
baseUrl: "https://opencode.ai/zen/v1",
|
|
88
|
+
capabilities: {
|
|
89
|
+
streaming: true,
|
|
90
|
+
tools: true,
|
|
91
|
+
vision: false,
|
|
92
|
+
reasoningEffort: false,
|
|
93
|
+
listModels: true,
|
|
94
|
+
requiresKey: false,
|
|
95
|
+
auth: "bearer",
|
|
96
|
+
},
|
|
97
|
+
create: openaiCompat,
|
|
98
|
+
};
|
|
99
|
+
const OPENCODE_GO = {
|
|
100
|
+
type: "opencode-go",
|
|
101
|
+
label: "OpenCode Go (opencode.ai/go)",
|
|
102
|
+
baseUrl: "https://opencode.ai/zen/go/v1",
|
|
103
|
+
capabilities: {
|
|
104
|
+
streaming: true,
|
|
105
|
+
tools: true,
|
|
106
|
+
vision: false,
|
|
107
|
+
reasoningEffort: false,
|
|
108
|
+
listModels: true,
|
|
109
|
+
requiresKey: false,
|
|
110
|
+
auth: "bearer",
|
|
111
|
+
},
|
|
112
|
+
create: openaiCompat,
|
|
113
|
+
};
|
|
114
|
+
/** All builtin provider presets. */
|
|
115
|
+
export const BUILTIN_PROVIDERS = [
|
|
116
|
+
OPENAI_COMPAT,
|
|
117
|
+
OPENROUTER,
|
|
118
|
+
OPENAI,
|
|
119
|
+
ANTHROPIC,
|
|
120
|
+
OPENCODE_ZEN,
|
|
121
|
+
OPENCODE_GO,
|
|
122
|
+
];
|
|
123
|
+
/** Fixed base URLs for hosted providers (no port scan / model autodetect). */
|
|
124
|
+
export const HOSTED_BASE_URLS = {};
|
|
125
|
+
for (const spec of BUILTIN_PROVIDERS) {
|
|
126
|
+
if (spec.baseUrl)
|
|
127
|
+
HOSTED_BASE_URLS[spec.type] = spec.baseUrl;
|
|
128
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth mapping provider `type` â factory + capabilities.
|
|
3
|
+
* Adding a provider = one `register()` call; no changes to the agent loop.
|
|
4
|
+
*/
|
|
5
|
+
export class ProviderRegistry {
|
|
6
|
+
specs = new Map();
|
|
7
|
+
register(spec) {
|
|
8
|
+
this.specs.set(spec.type, spec);
|
|
9
|
+
}
|
|
10
|
+
get(type) {
|
|
11
|
+
return this.specs.get(type);
|
|
12
|
+
}
|
|
13
|
+
has(type) {
|
|
14
|
+
return this.specs.has(type);
|
|
15
|
+
}
|
|
16
|
+
list() {
|
|
17
|
+
return [...this.specs.values()];
|
|
18
|
+
}
|
|
19
|
+
types() {
|
|
20
|
+
return [...this.specs.keys()];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/modules/registry.js
CHANGED
|
@@ -115,6 +115,20 @@ export function isCommandAllowed(command, securityConfig) {
|
|
|
115
115
|
reason: `Shell wrapper "${baseForShellCheck}" is blocked â use the bash tool directly`,
|
|
116
116
|
};
|
|
117
117
|
}
|
|
118
|
+
// Interpreters with -c/-e execute arbitrary code and bypass the blacklist.
|
|
119
|
+
const INTERPRETER_FLAGS = /(?:^|\s)(?:bash|sh|zsh|dash|python|python3|node|bun|perl|ruby)\s+(?:-c|-e|--command|--eval)\b/;
|
|
120
|
+
if (INTERPRETER_FLAGS.test(trimmedCommand)) {
|
|
121
|
+
return { allowed: false, reason: "Interpreter -c/-e invocation is blocked" };
|
|
122
|
+
}
|
|
123
|
+
// Multi-line commands can hide blacklisted commands after a newline.
|
|
124
|
+
if (/\r?\n/.test(trimmedCommand)) {
|
|
125
|
+
return { allowed: false, reason: "Multi-line commands are blocked" };
|
|
126
|
+
}
|
|
127
|
+
// Chained commands bypass the base-command check (e.g. "echo hi; rm -rf /").
|
|
128
|
+
if (/[;&|]/.test(trimmedCommand) &&
|
|
129
|
+
/(^|[;&|\s])(rm|del|rd|dd|chmod|chown|wget|curl|nc|netcat|shutdown|reboot|mkfs|fdisk)\b/.test(trimmedCommand)) {
|
|
130
|
+
return { allowed: false, reason: "Command chaining to a dangerous command is blocked" };
|
|
131
|
+
}
|
|
118
132
|
// Check dangerous operators FIRST â before parsing the base command.
|
|
119
133
|
// e.g. "curl http://x | bash" must be caught even though curl isn't blacklisted.
|
|
120
134
|
if (config.blockDangerousFlags) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "node:crypto";
|
|
1
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { homedir } from "os";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import { readFileSync, writeFileSync, existsSync, copyFileSync } from "fs";
|
|
@@ -33,10 +33,10 @@ export function getOrCreateEncryptionKey(config) {
|
|
|
33
33
|
writeFileSync(keyPath, key.toString("base64"), { mode: 0o600 });
|
|
34
34
|
return key;
|
|
35
35
|
}
|
|
36
|
-
catch {
|
|
37
|
-
// If we can't
|
|
38
|
-
//
|
|
39
|
-
|
|
36
|
+
catch (e) {
|
|
37
|
+
// If we can't persist the key, encrypted data would be unrecoverable
|
|
38
|
+
// on the next run â fail loudly instead of silently degrading.
|
|
39
|
+
throw new Error(`Cannot write encryption key to ${keyPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
try {
|
|
@@ -102,7 +102,7 @@ export function decryptString(encryptedText, key) {
|
|
|
102
102
|
hmac.update(iv);
|
|
103
103
|
hmac.update(Buffer.from(encrypted, "base64"));
|
|
104
104
|
const computedTag = hmac.digest();
|
|
105
|
-
if (
|
|
105
|
+
if (computedTag.length !== authTag.length || !timingSafeEqual(computedTag, authTag)) {
|
|
106
106
|
throw new Error("Authentication failed: invalid tag");
|
|
107
107
|
}
|
|
108
108
|
// Decrypt with AES-256-CBC
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { sanitizeLogMessage } from "./data-sanitizer";
|
|
2
|
+
/**
|
|
3
|
+
* Check if a hostname is a private/link-local address (SSRF protection).
|
|
4
|
+
*/
|
|
5
|
+
export function isIpPrivate(hostname) {
|
|
6
|
+
const h = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
7
|
+
return (/^(localhost|127\.0\.0\.1|0\.0\.0\.0|10\.|192\.168\.|169\.254\.|172\.(1[6-9]|2\d|3[01])\.)/.test(h) ||
|
|
8
|
+
h === "::1" ||
|
|
9
|
+
/^(fc|fd)/.test(h) ||
|
|
10
|
+
(h.includes(":") && h.startsWith("fe80")));
|
|
11
|
+
}
|
|
2
12
|
/**
|
|
3
13
|
* Check if a URL is allowed based on security configuration
|
|
4
14
|
*/
|
|
@@ -10,6 +20,13 @@ export function isUrlAllowed(url, securityConfig) {
|
|
|
10
20
|
try {
|
|
11
21
|
const urlObj = new URL(url);
|
|
12
22
|
const domain = urlObj.hostname.toLowerCase();
|
|
23
|
+
const scheme = urlObj.protocol.toLowerCase();
|
|
24
|
+
if (scheme !== "http:" && scheme !== "https:") {
|
|
25
|
+
return { allowed: false, reason: `Scheme "${scheme}" is not allowed` };
|
|
26
|
+
}
|
|
27
|
+
if (isIpPrivate(domain)) {
|
|
28
|
+
return { allowed: false, reason: `Private address "${domain}" is blocked (SSRF)` };
|
|
29
|
+
}
|
|
13
30
|
// Check denied domains first
|
|
14
31
|
if (securityConfig.deniedDomains?.length) {
|
|
15
32
|
for (const deniedDomain of securityConfig.deniedDomains) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolve, normalize } from "path";
|
|
2
|
+
import { isInsideDir, matchesScopeEntry } from "../../tools/path-utils";
|
|
2
3
|
/**
|
|
3
4
|
* Check if a path is within the allowed scope
|
|
4
5
|
* This combines global security settings with per-tool scope
|
|
@@ -6,8 +7,9 @@ import { resolve, normalize } from "path";
|
|
|
6
7
|
export function isPathInScope(baseDir, targetPath, scope, securityConfig) {
|
|
7
8
|
const baseResolved = resolve(baseDir);
|
|
8
9
|
const targetResolved = resolve(baseDir, normalize(targetPath));
|
|
9
|
-
// First, check that path is within baseDir
|
|
10
|
-
|
|
10
|
+
// First, check that path is within baseDir (separator- and case-safe: a
|
|
11
|
+
// sibling directory sharing the prefix must not pass).
|
|
12
|
+
if (!isInsideDir(targetResolved, baseResolved)) {
|
|
11
13
|
return {
|
|
12
14
|
allowed: false,
|
|
13
15
|
reason: "Path is outside the base working directory",
|
|
@@ -17,8 +19,8 @@ export function isPathInScope(baseDir, targetPath, scope, securityConfig) {
|
|
|
17
19
|
if (!securityConfig?.enabled) {
|
|
18
20
|
// Still enforce subagent scope if present
|
|
19
21
|
if (scope) {
|
|
20
|
-
const isReadScope = scope.read_only_files.some((f) => targetResolved
|
|
21
|
-
const isWriteScope = scope.allowed_files.some((f) => targetResolved
|
|
22
|
+
const isReadScope = scope.read_only_files.some((f) => matchesScopeEntry(targetResolved, resolve(baseDir, f)));
|
|
23
|
+
const isWriteScope = scope.allowed_files.some((f) => matchesScopeEntry(targetResolved, resolve(baseDir, f)));
|
|
22
24
|
if (isWriteScope)
|
|
23
25
|
return { allowed: true };
|
|
24
26
|
if (isReadScope)
|
|
@@ -61,8 +63,8 @@ export function isPathInScope(baseDir, targetPath, scope, securityConfig) {
|
|
|
61
63
|
if (!scope) {
|
|
62
64
|
return { allowed: true };
|
|
63
65
|
}
|
|
64
|
-
const isRead = scope.read_only_files.some((f) => targetResolved
|
|
65
|
-
const isWrite = scope.allowed_files.some((f) => targetResolved
|
|
66
|
+
const isRead = scope.read_only_files.some((f) => matchesScopeEntry(targetResolved, resolve(baseDir, f)));
|
|
67
|
+
const isWrite = scope.allowed_files.some((f) => matchesScopeEntry(targetResolved, resolve(baseDir, f)));
|
|
66
68
|
if (isWrite)
|
|
67
69
|
return { allowed: true };
|
|
68
70
|
if (isRead)
|
|
@@ -78,8 +80,9 @@ export function isPathInScope(baseDir, targetPath, scope, securityConfig) {
|
|
|
78
80
|
export function isPathWritable(baseDir, targetPath, scope, securityConfig) {
|
|
79
81
|
const baseResolved = resolve(baseDir);
|
|
80
82
|
const targetResolved = resolve(baseDir, normalize(targetPath));
|
|
81
|
-
// First, check that path is within baseDir
|
|
82
|
-
|
|
83
|
+
// First, check that path is within baseDir (separator- and case-safe: a
|
|
84
|
+
// sibling directory sharing the prefix must not pass).
|
|
85
|
+
if (!isInsideDir(targetResolved, baseResolved)) {
|
|
83
86
|
return {
|
|
84
87
|
allowed: false,
|
|
85
88
|
reason: "Path is outside the base working directory",
|
|
@@ -89,7 +92,7 @@ export function isPathWritable(baseDir, targetPath, scope, securityConfig) {
|
|
|
89
92
|
if (!securityConfig?.enabled) {
|
|
90
93
|
// Still enforce subagent scope if present
|
|
91
94
|
if (scope) {
|
|
92
|
-
const isWriteScope = scope.allowed_files.some((f) => targetResolved
|
|
95
|
+
const isWriteScope = scope.allowed_files.some((f) => matchesScopeEntry(targetResolved, resolve(baseDir, f)));
|
|
93
96
|
if (isWriteScope)
|
|
94
97
|
return { allowed: true };
|
|
95
98
|
return {
|
|
@@ -130,7 +133,7 @@ export function isPathWritable(baseDir, targetPath, scope, securityConfig) {
|
|
|
130
133
|
if (!scope) {
|
|
131
134
|
return { allowed: true };
|
|
132
135
|
}
|
|
133
|
-
const isWrite = scope.allowed_files.some((f) => targetResolved
|
|
136
|
+
const isWrite = scope.allowed_files.some((f) => matchesScopeEntry(targetResolved, resolve(baseDir, f)));
|
|
134
137
|
if (isWrite)
|
|
135
138
|
return { allowed: true };
|
|
136
139
|
return {
|
|
@@ -149,16 +152,7 @@ function matchesGlobPattern(targetPath, baseDir, pattern) {
|
|
|
149
152
|
if (pattern.endsWith("/")) {
|
|
150
153
|
const dirPattern = pattern.slice(0, -1);
|
|
151
154
|
const resolvedDirPattern = resolve(baseDir, dirPattern);
|
|
152
|
-
|
|
153
|
-
if (resolvedTargetPath === resolvedDirPattern) {
|
|
154
|
-
return true;
|
|
155
|
-
}
|
|
156
|
-
// Check if target is inside the directory
|
|
157
|
-
if (resolvedTargetPath.startsWith(resolvedDirPattern + "/") ||
|
|
158
|
-
resolvedTargetPath.startsWith(resolvedDirPattern + "\\")) {
|
|
159
|
-
return true;
|
|
160
|
-
}
|
|
161
|
-
return false;
|
|
155
|
+
return isInsideDir(resolvedTargetPath, resolvedDirPattern);
|
|
162
156
|
}
|
|
163
157
|
// Handle **/* pattern (recursive)
|
|
164
158
|
if (pattern.includes("**/")) {
|
|
@@ -166,8 +160,7 @@ function matchesGlobPattern(targetPath, baseDir, pattern) {
|
|
|
166
160
|
const prefix = parts[0];
|
|
167
161
|
const suffix = parts[1];
|
|
168
162
|
const resolvedPrefix = resolve(baseDir, prefix);
|
|
169
|
-
if (!resolvedTargetPath
|
|
170
|
-
!resolvedTargetPath.startsWith(resolvedPrefix + "\\")) {
|
|
163
|
+
if (!isInsideDir(resolvedTargetPath, resolvedPrefix)) {
|
|
171
164
|
return false;
|
|
172
165
|
}
|
|
173
166
|
const remainingPath = resolvedTargetPath.slice(resolvedPrefix.length);
|
|
@@ -183,8 +176,7 @@ function matchesGlobPattern(targetPath, baseDir, pattern) {
|
|
|
183
176
|
const prefix = parts[0];
|
|
184
177
|
const suffix = parts[1];
|
|
185
178
|
const resolvedPrefix = resolve(baseDir, prefix);
|
|
186
|
-
if (!resolvedTargetPath
|
|
187
|
-
!resolvedTargetPath.startsWith(resolvedPrefix + "\\")) {
|
|
179
|
+
if (!isInsideDir(resolvedTargetPath, resolvedPrefix)) {
|
|
188
180
|
return false;
|
|
189
181
|
}
|
|
190
182
|
if (!suffix) {
|
|
@@ -202,6 +194,10 @@ function matchesGlobPattern(targetPath, baseDir, pattern) {
|
|
|
202
194
|
const relativePath = resolvedTargetPath.slice(resolvedBaseDir.length + 1).replace(/\\/g, "/");
|
|
203
195
|
return regex.test(relativePath);
|
|
204
196
|
}
|
|
205
|
-
// Exact match
|
|
206
|
-
|
|
197
|
+
// Exact match (case-insensitive on win32, where the FS is case-insensitive)
|
|
198
|
+
const exactTarget = resolve(baseDir, pattern);
|
|
199
|
+
if (process.platform === "win32") {
|
|
200
|
+
return resolvedTargetPath.toLowerCase() === exactTarget.toLowerCase();
|
|
201
|
+
}
|
|
202
|
+
return resolvedTargetPath === exactTarget;
|
|
207
203
|
}
|
|
@@ -34,7 +34,7 @@ export class SessionStore {
|
|
|
34
34
|
return this.encryptor?.isEnabled() ?? false;
|
|
35
35
|
}
|
|
36
36
|
init() {
|
|
37
|
-
mkdirSync(this.baseDir, { recursive: true });
|
|
37
|
+
mkdirSync(this.baseDir, { recursive: true, mode: 0o700 });
|
|
38
38
|
}
|
|
39
39
|
sessionDir(id) {
|
|
40
40
|
return join(this.baseDir, id);
|
|
@@ -54,13 +54,13 @@ export class SessionStore {
|
|
|
54
54
|
saveMeta(id, meta) {
|
|
55
55
|
this._metaCache.set(id, meta);
|
|
56
56
|
const dir = this.sessionDir(id);
|
|
57
|
-
mkdirSync(dir, { recursive: true });
|
|
57
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
58
58
|
const content = JSON.stringify(meta, null, 2);
|
|
59
59
|
if (this.encryptor) {
|
|
60
|
-
writeFileSync(this.metaPath(id), this.encryptor.encryptFileContent(content), "utf-8");
|
|
60
|
+
writeFileSync(this.metaPath(id), this.encryptor.encryptFileContent(content), { encoding: "utf-8", mode: 0o600 });
|
|
61
61
|
}
|
|
62
62
|
else {
|
|
63
|
-
writeFileSync(this.metaPath(id), content, "utf-8");
|
|
63
|
+
writeFileSync(this.metaPath(id), content, { encoding: "utf-8", mode: 0o600 });
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
loadMeta(id) {
|
|
@@ -83,13 +83,13 @@ export class SessionStore {
|
|
|
83
83
|
}
|
|
84
84
|
appendMessage(id, msg) {
|
|
85
85
|
const dir = this.sessionDir(id);
|
|
86
|
-
mkdirSync(dir, { recursive: true });
|
|
86
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
87
87
|
const line = JSON.stringify(msg);
|
|
88
88
|
if (this.encryptor?.isEnabled()) {
|
|
89
|
-
appendFileSync(this.historyPath(id), this.encryptor.encryptFileContent(line) + "\n", "utf-8");
|
|
89
|
+
appendFileSync(this.historyPath(id), this.encryptor.encryptFileContent(line) + "\n", { encoding: "utf-8", mode: 0o600 });
|
|
90
90
|
}
|
|
91
91
|
else {
|
|
92
|
-
appendFileSync(this.historyPath(id), line + "\n", "utf-8");
|
|
92
|
+
appendFileSync(this.historyPath(id), line + "\n", { encoding: "utf-8", mode: 0o600 });
|
|
93
93
|
}
|
|
94
94
|
const meta = this.loadMeta(id);
|
|
95
95
|
if (meta) {
|
|
@@ -135,13 +135,13 @@ export class SessionStore {
|
|
|
135
135
|
}
|
|
136
136
|
appendSessionLog(id, entry) {
|
|
137
137
|
const dir = this.sessionDir(id);
|
|
138
|
-
mkdirSync(dir, { recursive: true });
|
|
138
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
139
139
|
const line = JSON.stringify(entry);
|
|
140
140
|
if (this.encryptor?.isEnabled()) {
|
|
141
|
-
appendFileSync(this.sessionLogPath(id), this.encryptor.encryptFileContent(line) + "\n", "utf-8");
|
|
141
|
+
appendFileSync(this.sessionLogPath(id), this.encryptor.encryptFileContent(line) + "\n", { encoding: "utf-8", mode: 0o600 });
|
|
142
142
|
}
|
|
143
143
|
else {
|
|
144
|
-
appendFileSync(this.sessionLogPath(id), line + "\n", "utf-8");
|
|
144
|
+
appendFileSync(this.sessionLogPath(id), line + "\n", { encoding: "utf-8", mode: 0o600 });
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
loadSessionLog(id) {
|
package/dist/tools/approve.js
CHANGED
|
@@ -2,6 +2,7 @@ import { t } from "../i18n/index";
|
|
|
2
2
|
import { askChoice } from "./user-input";
|
|
3
3
|
export const approveTool = {
|
|
4
4
|
name: "approve",
|
|
5
|
+
icon: "đ",
|
|
5
6
|
description: "Request user approval for an action. The user picks Yes or No from a menu.",
|
|
6
7
|
tags: ["core"],
|
|
7
8
|
interactive: true,
|
|
@@ -5,6 +5,7 @@ import { loadFileAsDataUrl, loadUrlAsDataUrl, readClipboardImage, bufferToDataUr
|
|
|
5
5
|
import { logSecurityBlock } from "../modules/security/audit-log";
|
|
6
6
|
export const attachImageTool = {
|
|
7
7
|
name: "attach_image",
|
|
8
|
+
icon: "đŧī¸",
|
|
8
9
|
description: "Attach an image to the conversation from a file path, URL, or clipboard. The image will be included in the next message sent to the model. Supports PNG, JPEG, GIF, WebP.",
|
|
9
10
|
tags: ["vision", "image"],
|
|
10
11
|
parameters: {
|
|
@@ -36,6 +37,17 @@ export const attachImageTool = {
|
|
|
36
37
|
dataUrl = result.dataUrl;
|
|
37
38
|
}
|
|
38
39
|
else if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
40
|
+
// Security: validate URL against the network policy (SSRF).
|
|
41
|
+
const { isUrlAllowed } = await import("../modules/security/network-validator");
|
|
42
|
+
const { getSessionSecurityConfig } = await import("../modules/security/session-isolation");
|
|
43
|
+
const networkConfig = ctx.sessionContext
|
|
44
|
+
? getSessionSecurityConfig(ctx.config, ctx.sessionContext).network
|
|
45
|
+
: ctx.config?.security?.network;
|
|
46
|
+
const urlCheck = isUrlAllowed(source, networkConfig);
|
|
47
|
+
if (!urlCheck.allowed) {
|
|
48
|
+
logSecurityBlock(ctx.sessionId, "network_request", urlCheck.reason ?? "attach_image URL blocked", source);
|
|
49
|
+
return { success: false, output: `[SECURITY BLOCKED] ${urlCheck.reason}` };
|
|
50
|
+
}
|
|
39
51
|
const result = await loadUrlAsDataUrl(source);
|
|
40
52
|
dataUrl = result.dataUrl;
|
|
41
53
|
}
|
package/dist/tools/bash.js
CHANGED
|
@@ -298,6 +298,8 @@ export function emptyCliRunHint(command, output, code) {
|
|
|
298
298
|
* the same failing `npx`/`bunx`/`npm run` command).
|
|
299
299
|
*/
|
|
300
300
|
const NPM_EXEC_RE = /could not determine executable to run/i;
|
|
301
|
+
/** process-management tools hidden behind enable_tools (tag "shell"). */
|
|
302
|
+
const HIDDEN_PROCESS_TOOLS = ["process_list", "process_log", "process_kill"];
|
|
301
303
|
export function npmExecHint(output) {
|
|
302
304
|
if (NPM_EXEC_RE.test(output)) {
|
|
303
305
|
return `${output}\n\nHint: ${t("exec.npm_exec_hint")}`;
|
|
@@ -306,6 +308,7 @@ export function npmExecHint(output) {
|
|
|
306
308
|
}
|
|
307
309
|
export const bashTool = {
|
|
308
310
|
name: "bash",
|
|
311
|
+
icon: "đģ",
|
|
309
312
|
description: "Execute a shell command and return its output. Use for running tests, build, git, and shell operations. Commands that are still running after a few seconds are automatically moved to the background and return a process id â manage them with process_list, process_log, process_kill. Set background=true to return a process id immediately for commands you know are long-running (dev servers, watchers).\n\nWindows notes: the shell is cmd.exe â PowerShell cmdlets (Write-Host, Get-Content, Select-String, Out-File) and bash heredocs (cat << EOF) do NOT work there. Use the read_file/write_file tools instead of cat/echo redirection. Sequential commands: use && (a leading ; is auto-converted to &).",
|
|
310
313
|
tags: ["shell", "code"],
|
|
311
314
|
parameters: {
|
|
@@ -340,6 +343,19 @@ export const bashTool = {
|
|
|
340
343
|
};
|
|
341
344
|
}
|
|
342
345
|
const command = adaptCommandForWindows(originalCommand);
|
|
346
|
+
// Hidden-tool discovery (ses_mt4fn58c): with tool-set narrowing the
|
|
347
|
+
// process_* tools are invisible to the model, so running them through
|
|
348
|
+
// cmd.exe only yields "is not recognized" â three wasted attempts in
|
|
349
|
+
// that session. Point the model at enable_tools. (k=v forms are handled
|
|
350
|
+
// by the redirect above and reach the real tool.)
|
|
351
|
+
const baseCmd = originalCommand.trim().split(/\s+/)[0];
|
|
352
|
+
if (HIDDEN_PROCESS_TOOLS.includes(baseCmd) &&
|
|
353
|
+
ctx.toolExecutor?.hasTool?.(baseCmd)) {
|
|
354
|
+
return {
|
|
355
|
+
success: false,
|
|
356
|
+
output: t("exec.hidden_tool_hint", { tool: baseCmd }),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
343
359
|
// echo/printf redirection to a file is unreliable in cmd.exe (single
|
|
344
360
|
// quotes don't group, multi-line commands split, embedded double quotes
|
|
345
361
|
// break the command). Steer the model to write_file instead â it
|
|
@@ -359,7 +375,10 @@ export const bashTool = {
|
|
|
359
375
|
const fullSecurityConfig = ctx.sessionContext
|
|
360
376
|
? getSessionSecurityConfig(appConfig, ctx.sessionContext)
|
|
361
377
|
: appConfig.security || DEFAULT_SECURITY_CONFIG;
|
|
362
|
-
|
|
378
|
+
// Master switch off â all sub-modules off, even when defaults are enabled.
|
|
379
|
+
const securityConfig = fullSecurityConfig.enabled === false
|
|
380
|
+
? { ...(fullSecurityConfig.bash || DEFAULT_SECURITY_CONFIG.bash), enabled: false }
|
|
381
|
+
: fullSecurityConfig.bash || DEFAULT_SECURITY_CONFIG.bash;
|
|
363
382
|
const validation = isCommandAllowed(command, securityConfig);
|
|
364
383
|
if (!validation.allowed) {
|
|
365
384
|
// Log security block
|
|
@@ -405,11 +424,15 @@ export const bashTool = {
|
|
|
405
424
|
const testRun = detectTestResults(output);
|
|
406
425
|
if (testRun && testRun.failed > 0) {
|
|
407
426
|
output =
|
|
408
|
-
|
|
409
|
-
|
|
427
|
+
t("exec.test_runner_fail", {
|
|
428
|
+
framework: testRun.framework,
|
|
429
|
+
failed: String(testRun.failed),
|
|
430
|
+
passed: String(testRun.passed),
|
|
431
|
+
}) +
|
|
432
|
+
`\n\n${output}`;
|
|
410
433
|
}
|
|
411
434
|
else if (testRun && testRun.failed === 0 && testRun.passed > 0) {
|
|
412
|
-
output =
|
|
435
|
+
output = `${t("exec.test_runner_pass", { framework: testRun.framework, passed: String(testRun.passed) })}\n\n${output}`;
|
|
413
436
|
}
|
|
414
437
|
if (!output && code !== 0) {
|
|
415
438
|
output = `(exit code ${code})`;
|
package/dist/tools/browser.js
CHANGED
|
@@ -34,6 +34,7 @@ export function formatScreenshotResult(snapshot, screenshot, supportsVision) {
|
|
|
34
34
|
export function createBrowserTool() {
|
|
35
35
|
return {
|
|
36
36
|
name: "browser",
|
|
37
|
+
icon: "đ",
|
|
37
38
|
tags: ["browser", "vision"],
|
|
38
39
|
description: [
|
|
39
40
|
"Control a headless browser. Navigate pages, click elements, type text, scroll, take screenshots.",
|
|
@@ -5,6 +5,7 @@ import { isPathInScope } from "./scope-check";
|
|
|
5
5
|
import { runChunkQuery } from "../modules/context/chunk-query";
|
|
6
6
|
export const chunkQueryTool = {
|
|
7
7
|
name: "chunk_query",
|
|
8
|
+
icon: "đ§Š",
|
|
8
9
|
description: "Answer a query over a large text (file or inline) by splitting it into chunks and querying the model per chunk in parallel, then optionally synthesizing a final answer. Use when the input is too large for the context window.",
|
|
9
10
|
tags: ["research", "code"],
|
|
10
11
|
/** Multiple LLM calls (parallel chunk queries + synthesis) need a longer budget. */
|
package/dist/tools/create-dir.js
CHANGED
|
@@ -6,6 +6,7 @@ import { getSessionSecurityConfig } from "../modules/security/session-isolation"
|
|
|
6
6
|
import { safeResolvePath } from "./path-utils";
|
|
7
7
|
export const createDirTool = {
|
|
8
8
|
name: "create_dir",
|
|
9
|
+
icon: "đ",
|
|
9
10
|
description: "Create a directory (and any intermediate directories).",
|
|
10
11
|
tags: ["file"],
|
|
11
12
|
parameters: {
|
|
@@ -12,6 +12,7 @@ import { safeResolvePath } from "./path-utils";
|
|
|
12
12
|
export const MAX_DOWNLOAD_BYTES = 100 * 1024 * 1024;
|
|
13
13
|
export const downloadFileTool = {
|
|
14
14
|
name: "download_file",
|
|
15
|
+
icon: "âŦī¸",
|
|
15
16
|
description: "Download a file (image, archive, binary, font, etc.) from a URL and save it to disk. Returns only the path, size and content type â read the saved file with read_file/attach_image afterwards. Use this for binary files; web_fetch/web_browse return text only and cannot save bytes.",
|
|
16
17
|
tags: ["file", "research"],
|
|
17
18
|
parameters: {
|
package/dist/tools/edit-file.js
CHANGED
|
@@ -8,6 +8,7 @@ import { generateDiff } from "../ui/diff";
|
|
|
8
8
|
import { safeResolvePath } from "./path-utils";
|
|
9
9
|
export const editFileTool = {
|
|
10
10
|
name: "edit_file",
|
|
11
|
+
icon: "âī¸",
|
|
11
12
|
description: "Find and replace text in an existing file. Uses exact string match (not regex).",
|
|
12
13
|
tags: ["file", "code"],
|
|
13
14
|
parameters: {
|
|
@@ -53,7 +54,7 @@ export const editFileTool = {
|
|
|
53
54
|
if (!content.includes(oldStr)) {
|
|
54
55
|
return {
|
|
55
56
|
success: false,
|
|
56
|
-
output: t("file.string_not_found", { str: oldStr }),
|
|
57
|
+
output: t("file.string_not_found", { str: oldStr, path }),
|
|
57
58
|
};
|
|
58
59
|
}
|
|
59
60
|
const updated = content.replace(oldStr, newStr);
|
|
@@ -9,6 +9,7 @@ import { t } from "../i18n/index";
|
|
|
9
9
|
*/
|
|
10
10
|
export const enableToolsTool = {
|
|
11
11
|
name: "enable_tools",
|
|
12
|
+
icon: "đ",
|
|
12
13
|
description: "Enable hidden tool categories for the current session. Only the essential tools (file/code/shell) are active by default. Call this with the tags you need and they become available from the next turn. Available tags: file, code, shell, research, browser, vision, image, memory, core. Returns the newly available tools. To run a task in a separate context instead, use subagent with tool_tags.",
|
|
13
14
|
alwaysOn: true,
|
|
14
15
|
parameters: {
|
package/dist/tools/executor.js
CHANGED
|
@@ -114,13 +114,23 @@ export class ToolExecutor {
|
|
|
114
114
|
toolCallId: call.id,
|
|
115
115
|
};
|
|
116
116
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
117
|
+
// After an abort the tool's work is discarded (the agent loop breaks
|
|
118
|
+
// without rendering or logging it), so skip the slow post-tool plugins
|
|
119
|
+
// (LSP checks, lint, tsc) entirely. When the abort lands mid-plugin the
|
|
120
|
+
// signal in the hook context lets the plugin kill its child processes.
|
|
121
|
+
if (signal?.aborted) {
|
|
122
|
+
this.ctx.logger.debug(`Tool ${call.name}: abort, skipping onAfterTool plugins`);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
const pluginCtx = signal ? { ...this.ctx, signal } : this.ctx;
|
|
126
|
+
for (const plugin of this.pluginManager.getAllPlugins()) {
|
|
127
|
+
if (plugin.onAfterTool) {
|
|
128
|
+
try {
|
|
129
|
+
await plugin.onAfterTool(pluginCtx, call, result);
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
this.ctx.logger.warn(`Plugin onAfterTool error: ${e.message}`);
|
|
133
|
+
}
|
|
124
134
|
}
|
|
125
135
|
}
|
|
126
136
|
}
|
package/dist/tools/file-info.js
CHANGED
|
@@ -4,6 +4,7 @@ import { isPathInScope } from "../modules/security/path-validator";
|
|
|
4
4
|
import { safeResolvePath } from "./path-utils";
|
|
5
5
|
export const fileInfoTool = {
|
|
6
6
|
name: "file_info",
|
|
7
|
+
icon: "âšī¸",
|
|
7
8
|
description: "Get metadata about a file or directory (size, creation date, modification date).",
|
|
8
9
|
tags: ["file"],
|
|
9
10
|
boundedOutput: true,
|
package/dist/tools/glob-tool.js
CHANGED
|
@@ -3,6 +3,7 @@ import { t } from "../i18n/index";
|
|
|
3
3
|
import { MAX_PREVIEW_LINES } from "./preview";
|
|
4
4
|
export const globTool = {
|
|
5
5
|
name: "glob",
|
|
6
|
+
icon: "đ",
|
|
6
7
|
description: `Search for files matching a glob pattern. Shows up to ${MAX_PREVIEW_LINES} results by default. Uses standard glob syntax (e.g., **/*.ts, src/**/*.test.ts).`,
|
|
7
8
|
tags: ["file", "code", "research"],
|
|
8
9
|
parameters: {
|