micro-models-agent 0.63.3 → 1.1.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/CHANGELOG.md +148 -1
- package/dist/cli/cache-line.js +30 -0
- package/dist/cli/command-suggest.js +38 -0
- package/dist/cli/commands.js +285 -60
- package/dist/cli/completer.js +16 -16
- package/dist/cli/json-payload.js +32 -0
- package/dist/cli/main.js +165 -77
- package/dist/cli/plugin-commands.js +5 -4
- package/dist/cli/relaunch.js +37 -0
- package/dist/cli/repl-commands.js +441 -307
- package/dist/cli/repl.js +360 -83
- package/dist/cli/run-result.js +12 -6
- package/dist/cli/security-commands.js +64 -60
- package/dist/cli/setup-order.js +57 -0
- package/dist/cli/setup-prompt.js +49 -0
- package/dist/cli/setup.js +52 -48
- package/dist/config/budget.js +48 -0
- package/dist/config/config.js +132 -70
- package/dist/config/defaults.js +37 -11
- package/dist/config/domains.js +9 -50
- package/dist/config/utils.js +56 -0
- package/dist/core/agent/audit-gate.js +49 -0
- package/dist/core/agent/compaction.js +89 -0
- package/dist/core/agent/constants.js +61 -0
- package/dist/core/agent/context-renderer.js +40 -0
- package/dist/core/agent/hallucination-gate.js +87 -0
- package/dist/core/agent/loop-state.js +53 -0
- package/dist/core/agent/prefix-monitor.js +101 -0
- package/dist/core/agent/reasoning-resolver.js +56 -0
- package/dist/core/agent/token-tracker.js +96 -0
- package/dist/core/agent/tool-batch.js +237 -0
- package/dist/core/agent/tool-output.js +62 -0
- package/dist/core/agent-moe.js +214 -69
- package/dist/core/agent.js +506 -546
- package/dist/core/bootstrap.js +297 -98
- package/dist/core/crash-handler.js +2 -1
- package/dist/core/prompt-builder.js +3 -0
- package/dist/core/prompt-overflow.js +307 -0
- package/dist/core/session-logger.js +34 -2
- package/dist/i18n/en.json +7 -4
- package/dist/i18n/ru.json +7 -4
- package/dist/index.js +5 -1
- package/dist/llm/cache-usage.js +76 -0
- package/dist/llm/image-utils.js +20 -16
- package/dist/llm/llm-errors.js +41 -0
- package/dist/llm/model-loader.js +30 -0
- package/dist/llm/openai-compat.js +287 -101
- package/dist/llm/orchestrator.js +140 -68
- package/dist/llm/provider-budget.js +68 -0
- package/dist/llm/provider.js +0 -1
- package/dist/llm/stream-state.js +26 -0
- package/dist/llm/token-counter.js +28 -0
- package/dist/logger/app-logger.js +12 -15
- package/dist/main.js +1606 -800
- package/dist/migration/detect.js +3 -1
- package/dist/modules/browser/actions.js +0 -3
- package/dist/modules/browser/bridge-client.js +2 -0
- package/dist/modules/browser/driver.js +46 -4
- package/dist/modules/certification/cli.js +85 -42
- package/dist/modules/certification/loader.js +15 -1
- package/dist/modules/certification/manifest.js +126 -15
- package/dist/modules/certification/runner.js +4 -26
- package/dist/modules/certification/scenarios.js +184 -5
- package/dist/modules/certification/syntax-scenarios.js +51 -0
- package/dist/modules/context/chunk-query.js +25 -5
- package/dist/modules/context/fact-extractor.js +6 -2
- package/dist/modules/context/manager.js +23 -7
- package/dist/modules/execution/audit-runners.js +7 -1
- package/dist/modules/execution/auditor.js +3 -3
- package/dist/modules/execution/execution-plugin.js +22 -15
- package/dist/modules/execution/input-from.js +46 -0
- package/dist/modules/execution/module.js +107 -18
- package/dist/modules/execution/moe-executor.js +166 -54
- package/dist/modules/execution/plan-actions.js +524 -0
- package/dist/modules/execution/plan-steps.js +23 -0
- package/dist/modules/execution/plan-store.js +15 -3
- package/dist/modules/execution/plan-tool.js +6 -488
- package/dist/modules/execution/plan-validator.js +24 -0
- package/dist/modules/execution/stuck-detector.js +3 -18
- package/dist/modules/execution/tracker.js +14 -5
- package/dist/modules/execution/transient-error.js +30 -0
- package/dist/modules/execution/verifier.js +94 -7
- package/dist/modules/execution/windows-commands.js +11 -0
- package/dist/modules/hallucination/confidence.js +36 -23
- package/dist/modules/hallucination/consistency.js +3 -0
- package/dist/modules/hallucination/detector.js +8 -3
- package/dist/modules/hallucination/factual.js +26 -7
- package/dist/modules/hallucination/llm-judge.js +12 -2
- package/dist/modules/indexer/map-command.js +35 -0
- package/dist/modules/indexer/map-select.js +87 -0
- package/dist/modules/indexer/module.js +34 -22
- package/dist/modules/indexer/symbols.js +189 -0
- package/dist/modules/indexer/walker.js +96 -42
- package/dist/modules/lsp/check-tool.js +2 -1
- package/dist/modules/lsp/client.js +49 -32
- package/dist/modules/lsp/config.js +55 -2
- package/dist/modules/lsp/module.js +38 -5
- package/dist/modules/lsp/probe.js +4 -3
- package/dist/modules/lsp/project-root.js +41 -1
- package/dist/modules/lsp/startup-check.js +12 -4
- package/dist/modules/mcp/client.js +153 -104
- package/dist/modules/mcp/module.js +165 -41
- package/dist/modules/memory/module.js +4 -3
- package/dist/modules/plugins/builtin/lint-on-write.js +36 -6
- package/dist/modules/plugins/manager.js +47 -84
- package/dist/modules/pricing/index.js +17 -7
- package/dist/modules/pricing/prices.js +30 -12
- package/dist/modules/processes/index.js +1 -0
- package/dist/modules/processes/kill-tree.js +56 -0
- package/dist/modules/processes/registry.js +2 -54
- package/dist/modules/providers/cache.js +23 -0
- package/dist/modules/providers/factory.js +28 -0
- package/dist/modules/providers/fallback.js +7 -5
- package/dist/modules/providers/health.js +2 -1
- package/dist/modules/providers/index.js +1 -0
- package/dist/modules/providers/manager.js +17 -2
- package/dist/modules/providers/presets.js +79 -6
- package/dist/modules/reasoning/policy.js +40 -0
- package/dist/modules/reasoning/probe.js +111 -0
- package/dist/modules/security/audit-notifier.js +42 -27
- package/dist/modules/security/command-validator.js +25 -20
- package/dist/modules/security/encryption.js +6 -12
- package/dist/modules/security/network-validator.js +76 -5
- package/dist/modules/security/path-validator.js +77 -34
- package/dist/modules/security/rate-limiter.js +11 -0
- package/dist/modules/security/security-policies.js +1 -1
- package/dist/modules/security/session-encryption.js +13 -2
- package/dist/modules/security/session-isolation.js +2 -9
- package/dist/modules/session/manager.js +11 -0
- package/dist/modules/session/module.js +11 -3
- package/dist/modules/session/store.js +41 -5
- package/dist/modules/skills/loader.js +7 -1
- package/dist/modules/skills/module.js +2 -1
- package/dist/modules/updater/changelog-reader.js +94 -0
- package/dist/modules/updater/dev-detect.js +17 -0
- package/dist/modules/updater/index.js +1 -0
- package/dist/modules/updater/module.js +14 -3
- package/dist/output/bus.js +32 -0
- package/dist/output/channel.js +233 -0
- package/dist/output/format.js +14 -0
- package/dist/output/index.js +7 -0
- package/dist/output/json-sink.js +22 -0
- package/dist/output/machine.js +8 -0
- package/dist/output/session-sink.js +27 -0
- package/dist/output/types.js +1 -0
- package/dist/tools/approve.js +6 -2
- package/dist/tools/attach-image.js +11 -11
- package/dist/tools/auto-fixer.js +198 -0
- package/dist/tools/bash.js +142 -89
- package/dist/tools/chunk-query.js +10 -6
- package/dist/tools/download-file.js +1 -1
- package/dist/tools/edit-file.js +20 -2
- package/dist/tools/executor.js +54 -9
- package/dist/tools/glob-tool.js +7 -0
- package/dist/tools/grep-tool.js +15 -1
- package/dist/tools/index.js +3 -1
- package/dist/tools/list-dir.js +3 -1
- package/dist/tools/load-skill.js +2 -1
- package/dist/tools/mcp-call.js +1 -1
- package/dist/tools/move-file.js +5 -4
- package/dist/tools/path-utils.js +7 -0
- package/dist/tools/pipeline-run.js +1 -1
- package/dist/tools/prompt-io.js +28 -0
- package/dist/tools/question.js +12 -12
- package/dist/tools/scope-request.js +91 -0
- package/dist/tools/session-info.js +44 -0
- package/dist/tools/set-thinking.js +71 -0
- package/dist/tools/subagent.js +50 -9
- package/dist/tools/syntax-validator.js +177 -0
- package/dist/tools/user-input.js +16 -9
- package/dist/tools/write-file.js +17 -1
- package/dist/ui/diff.js +10 -0
- package/dist/ui/line-editor.js +179 -26
- package/dist/ui/line-math.js +20 -3
- package/dist/ui/md-formatter.js +100 -10
- package/dist/ui/output.js +5 -4
- package/dist/ui/plan-view.js +2 -7
- package/dist/ui/renderer.js +89 -85
- package/dist/ui/spinner.js +14 -4
- package/dist/utils/error.js +4 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/retry.js +17 -0
- package/dist/utils/sleep.js +23 -0
- package/dist/utils/truncate.js +9 -0
- package/package.json +1 -1
package/dist/tools/executor.js
CHANGED
|
@@ -15,6 +15,11 @@ export class ToolExecutor {
|
|
|
15
15
|
* wiring). Used internally — e.g. bash redirects a mistaken tool-call-as-
|
|
16
16
|
* command into the real tool. Runs through the same registry and plugin
|
|
17
17
|
* hooks as a normal call.
|
|
18
|
+
*
|
|
19
|
+
* **Reentrancy warning:** swaps `this.ctx` temporarily (line 30-39). Safe
|
|
20
|
+
* for sequential callers but NOT for concurrent calls on the same executor
|
|
21
|
+
* instance — they would race on the ctx swap. Today all callers are
|
|
22
|
+
* sequential (bash redirect, single subagent tool); keep it that way.
|
|
18
23
|
*/
|
|
19
24
|
async executeByName(name, args, ctx) {
|
|
20
25
|
const prevCtx = this.ctx;
|
|
@@ -53,6 +58,7 @@ export class ToolExecutor {
|
|
|
53
58
|
const reason = typeof proceed === "string" ? proceed : undefined;
|
|
54
59
|
return {
|
|
55
60
|
success: false,
|
|
61
|
+
blocked: true,
|
|
56
62
|
output: reason
|
|
57
63
|
? t("tool.blocked_reason", { plugin: pluginName, reason })
|
|
58
64
|
: t("tool.blocked", { plugin: pluginName }),
|
|
@@ -74,11 +80,25 @@ export class ToolExecutor {
|
|
|
74
80
|
}
|
|
75
81
|
else {
|
|
76
82
|
const timeoutMs = tool.timeoutMs ?? TOOL_EXECUTION_TIMEOUT_MS;
|
|
83
|
+
const failFast = (message) => {
|
|
84
|
+
// Kill any long-running child process registered for this call.
|
|
85
|
+
killByCallId(call.id);
|
|
86
|
+
// Run tool-specific cleanup (e.g. subagent scope restore) before
|
|
87
|
+
// rejecting so shared state is restored even on abandonment.
|
|
88
|
+
const cleanup = this.ctx.cleanup;
|
|
89
|
+
if (cleanup) {
|
|
90
|
+
cleanup();
|
|
91
|
+
// Clear only if no newer call has replaced it meanwhile.
|
|
92
|
+
if (this.ctx.cleanup === cleanup)
|
|
93
|
+
this.ctx.cleanup = undefined;
|
|
94
|
+
}
|
|
95
|
+
return new Error(message);
|
|
96
|
+
};
|
|
97
|
+
let timer;
|
|
98
|
+
let onAbort;
|
|
77
99
|
const timeoutPromise = new Promise((_, reject) => {
|
|
78
|
-
setTimeout(() => {
|
|
79
|
-
|
|
80
|
-
killByCallId(call.id);
|
|
81
|
-
reject(new Error(t("tool.timeout", {
|
|
100
|
+
timer = setTimeout(() => {
|
|
101
|
+
reject(failFast(t("tool.timeout", {
|
|
82
102
|
name: call.name,
|
|
83
103
|
seconds: Math.round(timeoutMs / 1000),
|
|
84
104
|
})));
|
|
@@ -90,10 +110,7 @@ export class ToolExecutor {
|
|
|
90
110
|
];
|
|
91
111
|
if (signal) {
|
|
92
112
|
const abortPromise = new Promise((_, reject) => {
|
|
93
|
-
|
|
94
|
-
killByCallId(call.id);
|
|
95
|
-
reject(new Error(t("tool.aborted", { name: call.name })));
|
|
96
|
-
};
|
|
113
|
+
onAbort = () => reject(failFast(t("tool.aborted", { name: call.name })));
|
|
97
114
|
if (signal.aborted) {
|
|
98
115
|
onAbort();
|
|
99
116
|
}
|
|
@@ -103,7 +120,26 @@ export class ToolExecutor {
|
|
|
103
120
|
});
|
|
104
121
|
racePromises.push(abortPromise);
|
|
105
122
|
}
|
|
106
|
-
|
|
123
|
+
try {
|
|
124
|
+
result = await Promise.race(racePromises);
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
// Stop the timer and detach the abort listener once settled so they
|
|
128
|
+
// never fire against a DIFFERENT call's context/cleanup later.
|
|
129
|
+
if (timer !== undefined)
|
|
130
|
+
clearTimeout(timer);
|
|
131
|
+
if (onAbort && signal)
|
|
132
|
+
signal.removeEventListener("abort", onAbort);
|
|
133
|
+
// Losing promises have no consumer after Promise.race settled
|
|
134
|
+
// (their kill/cleanup effects were already applied in failFast).
|
|
135
|
+
// Log the rejection, then detach it so it never surfaces as an
|
|
136
|
+
// unhandled rejection.
|
|
137
|
+
for (const p of racePromises.slice(1)) {
|
|
138
|
+
p.catch((e) => {
|
|
139
|
+
this.ctx.logger.debug(`Tool ${call.name}: discarded race loser: ${e.message}`);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
107
143
|
}
|
|
108
144
|
result.toolCallId = call.id;
|
|
109
145
|
}
|
|
@@ -148,6 +184,15 @@ export class ToolExecutor {
|
|
|
148
184
|
setScope(scope) {
|
|
149
185
|
this.ctx.scope = scope;
|
|
150
186
|
}
|
|
187
|
+
setPromptIO(io) {
|
|
188
|
+
this.ctx.promptIO = io;
|
|
189
|
+
}
|
|
190
|
+
getRecursionDepth() {
|
|
191
|
+
return this.ctx.recursionDepth ?? 0;
|
|
192
|
+
}
|
|
193
|
+
setRecursionDepth(depth) {
|
|
194
|
+
this.ctx.recursionDepth = depth;
|
|
195
|
+
}
|
|
151
196
|
updateProvider(provider) {
|
|
152
197
|
this.ctx.llmProvider = provider;
|
|
153
198
|
}
|
package/dist/tools/glob-tool.js
CHANGED
|
@@ -15,6 +15,13 @@ export const globTool = {
|
|
|
15
15
|
},
|
|
16
16
|
handler: async (ctx, args) => {
|
|
17
17
|
const pattern = String(args.pattern);
|
|
18
|
+
// Security: absolute patterns (/etc/*) and ".." segments escape baseDir.
|
|
19
|
+
if (/^([a-zA-Z]:)?[\\/]/.test(pattern) || pattern.split(/[\\/]/).includes("..")) {
|
|
20
|
+
return {
|
|
21
|
+
success: false,
|
|
22
|
+
output: t("file.path_not_allowed", { path: pattern }),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
18
25
|
const results = globSync(pattern, { cwd: ctx.baseDir });
|
|
19
26
|
if (results.length === 0) {
|
|
20
27
|
return { success: true, output: t("file.no_matches") };
|
package/dist/tools/grep-tool.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
2
|
import { resolve } from "path";
|
|
3
3
|
import { t } from "../i18n/index";
|
|
4
|
-
import { logBashCommand } from "../modules/security/audit-log";
|
|
4
|
+
import { logBashCommand, logSecurityBlock } from "../modules/security/audit-log";
|
|
5
|
+
import { isPathInScope } from "../modules/security/path-validator";
|
|
6
|
+
import { DEFAULT_SECURITY_CONFIG } from "../config/security";
|
|
5
7
|
import { MAX_PREVIEW_LINES } from "./preview";
|
|
6
8
|
function truncateLines(output) {
|
|
7
9
|
const lines = output.split("\n");
|
|
@@ -74,6 +76,18 @@ export const grepTool = {
|
|
|
74
76
|
handler: async (ctx, args) => {
|
|
75
77
|
const pattern = String(args.pattern);
|
|
76
78
|
const searchPath = args.path ? resolve(ctx.baseDir, String(args.path)) : ctx.baseDir;
|
|
79
|
+
// Security: the search directory must stay inside baseDir / scope /
|
|
80
|
+
// allowed paths — `path: "../../.."` must not read outside the sandbox.
|
|
81
|
+
const scopeCheck = isPathInScope(ctx.baseDir, searchPath, ctx.scope, ctx.config?.security?.paths || DEFAULT_SECURITY_CONFIG.paths);
|
|
82
|
+
if (!scopeCheck.allowed) {
|
|
83
|
+
logSecurityBlock(ctx.sessionId || undefined, "file_read", scopeCheck.reason || "grep path not allowed", String(args.path ?? ctx.baseDir));
|
|
84
|
+
return {
|
|
85
|
+
success: false,
|
|
86
|
+
output: t("file.path_not_allowed", {
|
|
87
|
+
path: `${args.path ?? "."} — ${scopeCheck.reason}`,
|
|
88
|
+
}),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
77
91
|
// Build rg arguments as an array to avoid shell interpretation of regex
|
|
78
92
|
// metacharacters like |, (, ) — these are regex patterns, not shell operators.
|
|
79
93
|
const rgArgs = ["-n", "--with-filename", pattern, searchPath];
|
package/dist/tools/index.js
CHANGED
|
@@ -33,9 +33,10 @@ import { recallTool } from "./recall";
|
|
|
33
33
|
import { createBrowserTool } from "./browser";
|
|
34
34
|
import { attachImageTool } from "./attach-image";
|
|
35
35
|
import { enableToolsTool } from "./enable-tools";
|
|
36
|
+
import { sessionInfoTool } from "./session-info";
|
|
36
37
|
export { ToolRegistry, ToolExecutor };
|
|
37
38
|
export { filterToolsByTags } from "./filter-tools";
|
|
38
|
-
export { readFileTool, writeFileTool, editFileTool, globTool, grepTool, listDirTool, createDirTool, deleteFileTool, moveFileTool, fileInfoTool, bashTool, subagentTool, chunkQueryTool, processListTool, processLogTool, processKillTool, webSearchTool, webFetchTool, webBrowseTool, downloadFileTool, createLoadSkillTool, pipelineRunTool, mcpCallTool, searchHistoryTool, rememberTool, recallTool, createBrowserTool, attachImageTool, enableToolsTool, };
|
|
39
|
+
export { readFileTool, writeFileTool, editFileTool, globTool, grepTool, listDirTool, createDirTool, deleteFileTool, moveFileTool, fileInfoTool, bashTool, subagentTool, chunkQueryTool, processListTool, processLogTool, processKillTool, webSearchTool, webFetchTool, webBrowseTool, downloadFileTool, createLoadSkillTool, pipelineRunTool, mcpCallTool, searchHistoryTool, rememberTool, recallTool, createBrowserTool, attachImageTool, enableToolsTool, sessionInfoTool, };
|
|
39
40
|
export function registerAllTools(registry, skillsModule, opts) {
|
|
40
41
|
const tools = [
|
|
41
42
|
readFileTool,
|
|
@@ -62,6 +63,7 @@ export function registerAllTools(registry, skillsModule, opts) {
|
|
|
62
63
|
mcpCallTool,
|
|
63
64
|
searchHistoryTool,
|
|
64
65
|
attachImageTool,
|
|
66
|
+
sessionInfoTool,
|
|
65
67
|
];
|
|
66
68
|
// On-demand tool activation is the default. When explicitly disabled
|
|
67
69
|
// (enableOnDemand: false), the tool is not registered at all — combined
|
package/dist/tools/list-dir.js
CHANGED
|
@@ -4,6 +4,8 @@ import { t } from "../i18n/index";
|
|
|
4
4
|
import { isPathInScope } from "../modules/security/path-validator";
|
|
5
5
|
import { safeResolvePath } from "./path-utils";
|
|
6
6
|
import { MAX_PREVIEW_LINES } from "./preview";
|
|
7
|
+
/** Directories hidden from list_dir output (consistent with indexer walker). */
|
|
8
|
+
const IGNORED_DIRS = new Set(["node_modules", ".git", "dist", "build", "coverage", ".mma"]);
|
|
7
9
|
export const listDirTool = {
|
|
8
10
|
name: "list_dir",
|
|
9
11
|
icon: "📂",
|
|
@@ -33,7 +35,7 @@ export const listDirTool = {
|
|
|
33
35
|
if (!existsSync(resolved)) {
|
|
34
36
|
return { success: false, output: t("file.dir_notfound", { path }) };
|
|
35
37
|
}
|
|
36
|
-
const entries = readdirSync(resolved);
|
|
38
|
+
const entries = readdirSync(resolved).filter((e) => !IGNORED_DIRS.has(e));
|
|
37
39
|
const lines = entries.map((e) => {
|
|
38
40
|
const full = resolve(resolved, e);
|
|
39
41
|
return statSync(full).isDirectory() ? `${e}/` : e;
|
package/dist/tools/load-skill.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { t } from "../i18n/index";
|
|
2
|
+
import { estimateTokens } from "../llm/token-counter";
|
|
2
3
|
export function createLoadSkillTool(skillsModule) {
|
|
3
4
|
return {
|
|
4
5
|
name: "load_skill",
|
|
@@ -29,7 +30,7 @@ export function createLoadSkillTool(skillsModule) {
|
|
|
29
30
|
return { success: false, output: result.message + hint };
|
|
30
31
|
}
|
|
31
32
|
const budget = skillsModule.getBudget();
|
|
32
|
-
const tokens = result.skill ?
|
|
33
|
+
const tokens = result.skill ? estimateTokens(result.skill.content) : 0;
|
|
33
34
|
return {
|
|
34
35
|
success: true,
|
|
35
36
|
output: t("tool.skill_budget", {
|
package/dist/tools/mcp-call.js
CHANGED
|
@@ -26,7 +26,7 @@ export const mcpCallTool = {
|
|
|
26
26
|
name: "mcp_call",
|
|
27
27
|
icon: "🔗",
|
|
28
28
|
description: "Call a tool on an MCP (Model Context Protocol) server. MCP servers provide external capabilities like databases, APIs, or specialized tools.",
|
|
29
|
-
tags: ["
|
|
29
|
+
tags: ["research"],
|
|
30
30
|
parameters: {
|
|
31
31
|
type: "object",
|
|
32
32
|
properties: {
|
package/dist/tools/move-file.js
CHANGED
|
@@ -39,8 +39,9 @@ export const moveFileTool = {
|
|
|
39
39
|
output: `[SECURITY BLOCKED] Maximum file operations (${maxFileOps}) exceeded`,
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
|
-
// Check source path permissions
|
|
43
|
-
|
|
42
|
+
// Check source path permissions — against the RESOLVED path (the same
|
|
43
|
+
// one renameSync will act on), never the raw argument.
|
|
44
|
+
const fromCheck = isPathWritable(ctx.baseDir, fromResolved, ctx.scope, ctx.config.security?.paths);
|
|
44
45
|
if (!fromCheck.allowed) {
|
|
45
46
|
logSecurityBlock(ctx.sessionId, "file_write", fromCheck.reason || "Source path not allowed", fromPath);
|
|
46
47
|
return {
|
|
@@ -50,8 +51,8 @@ export const moveFileTool = {
|
|
|
50
51
|
}),
|
|
51
52
|
};
|
|
52
53
|
}
|
|
53
|
-
// Check destination path permissions
|
|
54
|
-
const toCheck = isPathWritable(ctx.baseDir,
|
|
54
|
+
// Check destination path permissions — resolved, same as above.
|
|
55
|
+
const toCheck = isPathWritable(ctx.baseDir, toResolved, ctx.scope, ctx.config.security?.paths);
|
|
55
56
|
if (!toCheck.allowed) {
|
|
56
57
|
logSecurityBlock(ctx.sessionId, "file_write", toCheck.reason || "Destination path not allowed", toPath);
|
|
57
58
|
return {
|
package/dist/tools/path-utils.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { resolve, normalize, dirname, basename, sep, relative, isAbsolute } from "path";
|
|
2
2
|
import { existsSync } from "fs";
|
|
3
|
+
/**
|
|
4
|
+
* Normalize backslashes to forward slashes for display and comparison.
|
|
5
|
+
* On POSIX this is a no-op; on Windows it converts `src\foo\bar.ts` → `src/foo/bar.ts`.
|
|
6
|
+
*/
|
|
7
|
+
export function toForwardSlash(p) {
|
|
8
|
+
return p.replace(/\\/g, "/");
|
|
9
|
+
}
|
|
3
10
|
/**
|
|
4
11
|
* True when targetPath equals dirPath or lies inside it.
|
|
5
12
|
*
|
|
@@ -55,7 +55,7 @@ export const pipelineRunTool = {
|
|
|
55
55
|
name: "pipeline_run",
|
|
56
56
|
icon: "▶️",
|
|
57
57
|
description: "Run a named pipeline with YAML definition. Creates a DAG of sub-agents that execute in dependency order.",
|
|
58
|
-
tags: ["shell"
|
|
58
|
+
tags: ["shell"],
|
|
59
59
|
parameters: {
|
|
60
60
|
type: "object",
|
|
61
61
|
properties: {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { formatMenu, choicePrompt, parseSelection, CUSTOM_INDEX, } from "./user-input";
|
|
2
|
+
import { t } from "../i18n/index";
|
|
3
|
+
/** Build a PromptIO from injectable ask/print primitives (testable, terminal-agnostic). */
|
|
4
|
+
export function createPromptIO(deps) {
|
|
5
|
+
return {
|
|
6
|
+
async askText(question) {
|
|
7
|
+
return (await deps.ask(`${question} `)).trim();
|
|
8
|
+
},
|
|
9
|
+
async askChoice(question, options, opts = {}) {
|
|
10
|
+
const multiple = opts.multiple ?? false;
|
|
11
|
+
const entryCount = options.length + (opts.allowCustom ? 1 : 0);
|
|
12
|
+
// No selectable entries (and no custom entry) — parseSelection can never
|
|
13
|
+
// succeed, so return without printing a dead menu or looping forever.
|
|
14
|
+
if (entryCount === 0)
|
|
15
|
+
return [];
|
|
16
|
+
const customEntry = options.length;
|
|
17
|
+
deps.print(formatMenu(question, options, opts));
|
|
18
|
+
for (;;) {
|
|
19
|
+
const answer = await deps.ask(choicePrompt(entryCount, multiple));
|
|
20
|
+
const parsed = parseSelection(answer, entryCount, multiple);
|
|
21
|
+
if (parsed) {
|
|
22
|
+
return parsed.map((i) => (opts.allowCustom && i === customEntry ? CUSTOM_INDEX : i));
|
|
23
|
+
}
|
|
24
|
+
deps.print(t("tool.user_input.invalid"));
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
package/dist/tools/question.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { t } from "../i18n/index";
|
|
2
2
|
import { askUser } from "./user-input";
|
|
3
|
-
const DESCRIPTION = `Use this tool when you need to ask the user questions during execution. This allows you to:
|
|
4
|
-
1. Gather user preferences or requirements
|
|
5
|
-
2. Clarify ambiguous instructions
|
|
6
|
-
3. Get decisions on implementation choices as you work
|
|
7
|
-
4. Offer choices to the user about what direction to take.
|
|
8
|
-
|
|
9
|
-
Usage notes:
|
|
10
|
-
- When "custom" is enabled (default), a "Type your own answer" option is added automatically; don't include "Other" or catch-all options
|
|
11
|
-
- Answers are returned as arrays of labels; set "multiple": true to allow selecting more than one
|
|
12
|
-
- If you recommend a specific option, make that the first option in the list and add "(Recommended)" at the end of the label
|
|
13
|
-
- Execution blocks until the user answers every question
|
|
3
|
+
const DESCRIPTION = `Use this tool when you need to ask the user questions during execution. This allows you to:
|
|
4
|
+
1. Gather user preferences or requirements
|
|
5
|
+
2. Clarify ambiguous instructions
|
|
6
|
+
3. Get decisions on implementation choices as you work
|
|
7
|
+
4. Offer choices to the user about what direction to take.
|
|
8
|
+
|
|
9
|
+
Usage notes:
|
|
10
|
+
- When "custom" is enabled (default), a "Type your own answer" option is added automatically; don't include "Other" or catch-all options
|
|
11
|
+
- Answers are returned as arrays of labels; set "multiple": true to allow selecting more than one
|
|
12
|
+
- If you recommend a specific option, make that the first option in the list and add "(Recommended)" at the end of the label
|
|
13
|
+
- Execution blocks until the user answers every question
|
|
14
14
|
- Omit "options" for a free-text question`;
|
|
15
15
|
function isOption(val) {
|
|
16
16
|
if (!val || typeof val !== "object")
|
|
@@ -128,7 +128,7 @@ export const questionTool = {
|
|
|
128
128
|
multiple: q.multiple,
|
|
129
129
|
custom: q.custom,
|
|
130
130
|
progress,
|
|
131
|
-
}));
|
|
131
|
+
}, ctx.promptIO));
|
|
132
132
|
}
|
|
133
133
|
const formatted = questions
|
|
134
134
|
.map((q, i) => `"${q.question}"="${answers[i].length ? answers[i].join(", ") : t("tool.question.unanswered")}"`)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const WRITE_TOOLS = new Set([
|
|
2
|
+
"write_file",
|
|
3
|
+
"edit_file",
|
|
4
|
+
"delete_file",
|
|
5
|
+
"create_dir",
|
|
6
|
+
"move_file",
|
|
7
|
+
"download_file",
|
|
8
|
+
]);
|
|
9
|
+
/**
|
|
10
|
+
* Scope denials funnel through i18n keys file.path_not_allowed(_short)
|
|
11
|
+
* ("Path not allowed" / "Путь недопустим") and chunk-query's "[SCOPE]" prefix.
|
|
12
|
+
*/
|
|
13
|
+
export function isPathDenialOutput(output) {
|
|
14
|
+
if (!output)
|
|
15
|
+
return false;
|
|
16
|
+
return (output.includes("Path not allowed") ||
|
|
17
|
+
output.includes("Путь недопустим") ||
|
|
18
|
+
output.startsWith("[SCOPE]"));
|
|
19
|
+
}
|
|
20
|
+
function pathArgOf(args) {
|
|
21
|
+
if (!args)
|
|
22
|
+
return "";
|
|
23
|
+
for (const key of ["path", "file", "input_path", "target", "source", "destination", "dir"]) {
|
|
24
|
+
const v = args[key];
|
|
25
|
+
if (typeof v === "string" && v.trim())
|
|
26
|
+
return v.trim();
|
|
27
|
+
}
|
|
28
|
+
for (const v of Object.values(args)) {
|
|
29
|
+
if (typeof v === "string" && /\.[\w-]+$/.test(v))
|
|
30
|
+
return v.trim();
|
|
31
|
+
}
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
/** Observes the sub-agent's tool results and records scope-denied paths. */
|
|
35
|
+
export class ScopeDenialCollector {
|
|
36
|
+
denied = new Map();
|
|
37
|
+
plugin = {
|
|
38
|
+
name: "scope-denial-collector",
|
|
39
|
+
isBuiltin: true,
|
|
40
|
+
onAfterTool: (_ctx, call, result) => {
|
|
41
|
+
const name = call?.name;
|
|
42
|
+
if (!name || !result || result.success)
|
|
43
|
+
return;
|
|
44
|
+
if (!isPathDenialOutput(typeof result.output === "string" ? result.output : undefined)) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const path = pathArgOf(call?.arguments);
|
|
48
|
+
if (!path)
|
|
49
|
+
return;
|
|
50
|
+
const write = WRITE_TOOLS.has(name);
|
|
51
|
+
this.denied.set(path, { path, write });
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
getDenied() {
|
|
55
|
+
return Array.from(this.denied.values());
|
|
56
|
+
}
|
|
57
|
+
/** read_only=true only when every denial was a pure read denial. */
|
|
58
|
+
toScopeRequest(reason = "sub-agent was denied access to files needed for the task") {
|
|
59
|
+
const all = this.getDenied();
|
|
60
|
+
if (all.length === 0)
|
|
61
|
+
return null;
|
|
62
|
+
return {
|
|
63
|
+
files: all.map((d) => d.path),
|
|
64
|
+
read_only: !all.some((d) => d.write),
|
|
65
|
+
reason,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function formatScopeRequest(req) {
|
|
70
|
+
return `<scope-request>${JSON.stringify(req)}</scope-request>`;
|
|
71
|
+
}
|
|
72
|
+
export function parseScopeRequest(text) {
|
|
73
|
+
if (!text)
|
|
74
|
+
return null;
|
|
75
|
+
const match = text.match(/<scope-request>([\s\S]*?)<\/scope-request>/);
|
|
76
|
+
if (!match)
|
|
77
|
+
return null;
|
|
78
|
+
try {
|
|
79
|
+
const parsed = JSON.parse(match[1]);
|
|
80
|
+
if (!Array.isArray(parsed.files) || parsed.files.length === 0)
|
|
81
|
+
return null;
|
|
82
|
+
return {
|
|
83
|
+
files: parsed.files.map(String),
|
|
84
|
+
read_only: !!parsed.read_only,
|
|
85
|
+
reason: String(parsed.reason || "access denied"),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { t } from "../i18n/index";
|
|
2
|
+
export const sessionInfoTool = {
|
|
3
|
+
name: "session_info",
|
|
4
|
+
icon: "ℹ️",
|
|
5
|
+
description: "Show metadata about the current session: id, name, model, provider, context window, message count, and how much of the context budget is used. Use when the user asks which session this is or how much context is left.",
|
|
6
|
+
alwaysOn: true,
|
|
7
|
+
boundedOutput: true,
|
|
8
|
+
parameters: {
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: {},
|
|
11
|
+
},
|
|
12
|
+
handler: async (ctx) => {
|
|
13
|
+
const meta = ctx.sessionManager?.getActiveMeta();
|
|
14
|
+
if (!meta) {
|
|
15
|
+
return { success: true, output: t("tool.session_info.no_active") };
|
|
16
|
+
}
|
|
17
|
+
const provider = ctx.config.provider;
|
|
18
|
+
const providerName = provider.active ?? provider.type ?? "unknown";
|
|
19
|
+
const lines = [
|
|
20
|
+
t("tool.session_info.result", {
|
|
21
|
+
id: meta.id,
|
|
22
|
+
name: meta.name,
|
|
23
|
+
model: meta.model ?? ctx.config.model ?? "unknown",
|
|
24
|
+
provider: providerName,
|
|
25
|
+
contextWindow: String(meta.contextWindow ?? ctx.config.contextWindow),
|
|
26
|
+
messages: String(meta.messageCount),
|
|
27
|
+
createdAt: meta.createdAt,
|
|
28
|
+
updatedAt: meta.updatedAt,
|
|
29
|
+
}),
|
|
30
|
+
];
|
|
31
|
+
const snapshot = ctx.contextManager?.getSnapshot();
|
|
32
|
+
if (snapshot) {
|
|
33
|
+
const used = snapshot.tokens;
|
|
34
|
+
const budget = snapshot.budget.history;
|
|
35
|
+
const percent = budget > 0 ? Math.round((used / budget) * 100) : 0;
|
|
36
|
+
lines.push(t("tool.session_info.context", {
|
|
37
|
+
used: String(used),
|
|
38
|
+
budget: String(budget),
|
|
39
|
+
percent: String(percent),
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
return { success: true, output: lines.join("\n") };
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// src/tools/set-thinking.ts
|
|
2
|
+
import { t } from "../i18n/index";
|
|
3
|
+
const VALID_LEVELS = ["default", "none", "low", "medium", "high", "max"];
|
|
4
|
+
export function createSetThinkingTool(state, config, getCurrentIteration) {
|
|
5
|
+
return {
|
|
6
|
+
name: "set_thinking",
|
|
7
|
+
description: "Set reasoning effort level for the next LLM call. Normally managed automatically by the harness. Use only to force deeper thinking for genuinely hard subproblems or shallower for mechanical batches.",
|
|
8
|
+
icon: "🧠",
|
|
9
|
+
tags: [],
|
|
10
|
+
alwaysOn: true,
|
|
11
|
+
parameters: {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
level: {
|
|
15
|
+
type: "string",
|
|
16
|
+
enum: ["none", "low", "high", "max"],
|
|
17
|
+
description: "Reasoning effort level to set",
|
|
18
|
+
},
|
|
19
|
+
reason: {
|
|
20
|
+
type: "string",
|
|
21
|
+
description: "Why changing the level",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
required: ["level"],
|
|
25
|
+
},
|
|
26
|
+
handler: async (_ctx, args) => {
|
|
27
|
+
const requested = args.level;
|
|
28
|
+
const reason = args.reason || "agent override";
|
|
29
|
+
if (!VALID_LEVELS.includes(requested) && requested !== "auto") {
|
|
30
|
+
return {
|
|
31
|
+
success: false,
|
|
32
|
+
output: t("tool.thinking_invalid_level", { level: requested }),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
// Cooldown check
|
|
36
|
+
const currentIter = getCurrentIteration();
|
|
37
|
+
const elapsed = currentIter - state.overrideIteration;
|
|
38
|
+
if (elapsed > 0 && elapsed < config.overrideCooldown) {
|
|
39
|
+
return {
|
|
40
|
+
success: true,
|
|
41
|
+
output: t("tool.thinking_cooldown", {
|
|
42
|
+
remaining: config.overrideCooldown - elapsed,
|
|
43
|
+
current: state.level,
|
|
44
|
+
}),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// Clamp to configured bounds
|
|
48
|
+
let level = requested;
|
|
49
|
+
const levelOrder = ["none", "low", "medium", "high", "max"];
|
|
50
|
+
const minIdx = levelOrder.indexOf(config.min);
|
|
51
|
+
const maxIdx = levelOrder.indexOf(config.max);
|
|
52
|
+
const reqIdx = levelOrder.indexOf(level);
|
|
53
|
+
let clamped = false;
|
|
54
|
+
if (reqIdx >= 0 && reqIdx < minIdx) {
|
|
55
|
+
level = config.min;
|
|
56
|
+
clamped = true;
|
|
57
|
+
}
|
|
58
|
+
else if (reqIdx > maxIdx) {
|
|
59
|
+
level = config.max;
|
|
60
|
+
clamped = true;
|
|
61
|
+
}
|
|
62
|
+
// Apply
|
|
63
|
+
state.level = level;
|
|
64
|
+
state.overrideIteration = currentIter;
|
|
65
|
+
const msg = clamped
|
|
66
|
+
? t("tool.thinking_set_clamped", { level, reason })
|
|
67
|
+
: t("tool.thinking_set", { level, reason });
|
|
68
|
+
return { success: true, output: msg };
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|