natureco-cli 5.63.0 → 5.64.1
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 +37 -0
- package/README.md +20 -3
- package/bin/natureco.js +10 -3
- package/package.json +3 -1
- package/scripts/benchmark-startup.js +26 -0
- package/scripts/e2e-context-smoke.js +33 -0
- package/src/commands/backup.js +3 -3
- package/src/commands/code.js +49 -14
- package/src/commands/code_v5.js +25 -2
- package/src/commands/gateway-server.js +128 -24
- package/src/commands/git.js +2 -2
- package/src/commands/help.js +11 -1
- package/src/commands/pairing.js +2 -22
- package/src/commands/repl.js +8 -6
- package/src/tools/agentic-runner.js +41 -33
- package/src/tools/memory_write.js +16 -12
- package/src/tools/structural_patch.js +25 -0
- package/src/tools/workflow.js +10 -2
- package/src/utils/agent-core.js +51 -0
- package/src/utils/agent-workspace.js +24 -0
- package/src/utils/api.js +12 -8
- package/src/utils/channel-sdk.js +212 -0
- package/src/utils/code-intelligence.js +69 -0
- package/src/utils/coding-session.js +54 -0
- package/src/utils/conversation-context.js +52 -0
- package/src/utils/delivery-store.js +34 -0
- package/src/utils/i18n.js +7 -1
- package/src/utils/json-schema.js +43 -0
- package/src/utils/lsp-client.js +129 -0
- package/src/utils/memory-record.js +49 -0
- package/src/utils/pairing-store.js +55 -0
- package/src/utils/pattern-detector.js +13 -3
- package/src/utils/plugin-registry.js +3 -3
- package/src/utils/process-errors.js +14 -7
- package/src/utils/runtime-health.js +28 -0
- package/src/utils/secret-store.js +90 -0
- package/src/utils/secure-sync.js +63 -0
- package/src/utils/skill-lifecycle.js +59 -0
- package/src/utils/structural-patch.js +68 -0
- package/src/utils/sub-agent.js +13 -2
- package/src/utils/test-failure-analyzer.js +64 -0
- package/src/utils/token-budget.js +3 -0
- package/src/utils/tool-execution-gateway.js +56 -0
- package/src/utils/tool-manifest.js +31 -0
- package/src/utils/tool-path-policy.js +49 -0
- package/src/utils/tool-result.js +17 -0
- package/src/utils/tool-runner.js +81 -53
- package/src/utils/tools.js +30 -42
package/src/utils/sub-agent.js
CHANGED
|
@@ -2,6 +2,7 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const { getProviderConfig } = require('./api');
|
|
5
|
+
const { AgentWorkspaceManager } = require('./agent-workspace');
|
|
5
6
|
|
|
6
7
|
const SUB_AGENTS_FILE = path.join(os.homedir(), '.natureco', 'sub-agents.json');
|
|
7
8
|
|
|
@@ -64,12 +65,15 @@ async function spawnSubAgent(type, task, options = {}) {
|
|
|
64
65
|
saveAgents(agents);
|
|
65
66
|
|
|
66
67
|
try {
|
|
68
|
+
const workspaceManager = options.workspaceManager || new AgentWorkspaceManager();
|
|
69
|
+
const useWorkspace = options.isolatedWorktree !== false && ['general', 'debugger'].includes(type);
|
|
70
|
+
const executeAgent = async (workspace = null) => {
|
|
67
71
|
const providerConfig = getProviderConfig();
|
|
68
72
|
if (!providerConfig) {
|
|
69
73
|
throw new Error('Provider not configured. Run: natureco configure');
|
|
70
74
|
}
|
|
71
75
|
|
|
72
|
-
const systemPrompt = options.systemPrompt || SYSTEM_PROMPTS[type];
|
|
76
|
+
const systemPrompt = (options.systemPrompt || SYSTEM_PROMPTS[type]) + (workspace ? `\n\nIsolated workspace: ${workspace.cwd}\nOnly operate inside this workspace.` : '');
|
|
73
77
|
|
|
74
78
|
const baseUrl = providerConfig.url.replace(/\/+$/, '');
|
|
75
79
|
const endpoint = `${baseUrl}/chat/completions`;
|
|
@@ -109,7 +113,14 @@ async function spawnSubAgent(type, task, options = {}) {
|
|
|
109
113
|
const usage = data.usage || {};
|
|
110
114
|
const duration = new Date(entry.completedAt) - new Date(entry.startedAt);
|
|
111
115
|
|
|
112
|
-
return { result: content, usage, duration };
|
|
116
|
+
return { result: content, usage, duration, workspace: workspace?.cwd || null };
|
|
117
|
+
};
|
|
118
|
+
if (useWorkspace) {
|
|
119
|
+
const isolated = await workspaceManager.run(entry.id, executeAgent, { merge: options.mergeWorktree === true });
|
|
120
|
+
if (!isolated.ok) throw new Error(isolated.error);
|
|
121
|
+
return isolated.result;
|
|
122
|
+
}
|
|
123
|
+
return await executeAgent();
|
|
113
124
|
} catch (err) {
|
|
114
125
|
entry.status = 'failed';
|
|
115
126
|
entry.result = err.message;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
function fingerprint(text) {
|
|
6
|
+
return crypto.createHash('sha256').update(String(text || '').replace(/\d+ms|\d+\.\d+s|\d+/g, '#')).digest('hex').slice(0, 16);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function analyzeTestFailure(output, exitCode = 1) {
|
|
10
|
+
const text = String(output || '');
|
|
11
|
+
const findings = [];
|
|
12
|
+
const patterns = [
|
|
13
|
+
{ type: 'assertion', re: /(?:AssertionError|expected .* (?:to|but)|FAIL\s+)([^\n]*)/gi },
|
|
14
|
+
{ type: 'syntax', re: /(?:SyntaxError|ParseError)[:\s]+([^\n]*)/gi },
|
|
15
|
+
{ type: 'type', re: /(?:TypeError|TS\d{4}:)[:\s]*([^\n]*)/gi },
|
|
16
|
+
{ type: 'module', re: /(?:Cannot find module|ModuleNotFoundError)[:\s]*([^\n]*)/gi },
|
|
17
|
+
{ type: 'timeout', re: /(?:timed out|timeout of \d+ms exceeded|Test timeout)/gi },
|
|
18
|
+
];
|
|
19
|
+
for (const pattern of patterns) {
|
|
20
|
+
pattern.re.lastIndex = 0;
|
|
21
|
+
let match;
|
|
22
|
+
while ((match = pattern.re.exec(text)) && findings.length < 50) {
|
|
23
|
+
findings.push({ type: pattern.type, message: (match[1] || match[0]).trim().slice(0, 500) });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const locations = [...text.matchAll(/(?:\(|\s)([^\s():]+\.(?:js|ts|tsx|jsx|py|go|rs|java)):(\d+)(?::(\d+))?/g)]
|
|
27
|
+
.slice(0, 50).map(match => ({ file: match[1], line: Number(match[2]), column: match[3] ? Number(match[3]) : null }));
|
|
28
|
+
const summaryMatch = text.match(/(\d+)\s+(?:failed|failing|failures?)/i);
|
|
29
|
+
return {
|
|
30
|
+
ok: exitCode === 0,
|
|
31
|
+
exitCode,
|
|
32
|
+
framework: /vitest/i.test(text) ? 'vitest' : /jest/i.test(text) ? 'jest' : /pytest|FAILED .*\.py/i.test(text) ? 'pytest' : /TS\d{4}/.test(text) ? 'typescript' : 'unknown',
|
|
33
|
+
failedCount: summaryMatch ? Number(summaryMatch[1]) : (exitCode === 0 ? 0 : Math.max(1, findings.length)),
|
|
34
|
+
findings, locations, fingerprint: fingerprint(text), rawTail: text.slice(-6000),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class AutoFixLoop {
|
|
39
|
+
constructor(options = {}) { this.maxAttempts = options.maxAttempts || 3; }
|
|
40
|
+
|
|
41
|
+
async run({ runTests, proposeFix, onAttempt }) {
|
|
42
|
+
const attempts = [];
|
|
43
|
+
let previousFingerprint = null;
|
|
44
|
+
for (let attempt = 1; attempt <= this.maxAttempts; attempt++) {
|
|
45
|
+
const startedAt = Date.now();
|
|
46
|
+
const testResult = await runTests({ attempt });
|
|
47
|
+
const analysis = analyzeTestFailure(testResult.output, testResult.exitCode);
|
|
48
|
+
const record = { attempt, analysis, durationMs: Date.now() - startedAt };
|
|
49
|
+
attempts.push(record);
|
|
50
|
+
if (onAttempt) await onAttempt(record);
|
|
51
|
+
if (analysis.ok) return { ok: true, attempts };
|
|
52
|
+
if (analysis.fingerprint === previousFingerprint) {
|
|
53
|
+
return { ok: false, stopped: 'no-progress', attempts, analysis };
|
|
54
|
+
}
|
|
55
|
+
previousFingerprint = analysis.fingerprint;
|
|
56
|
+
const fix = await proposeFix({ attempt, analysis, attempts });
|
|
57
|
+
record.fix = fix || null;
|
|
58
|
+
if (!fix || fix.applied === false) return { ok: false, stopped: 'no-fix', attempts, analysis };
|
|
59
|
+
}
|
|
60
|
+
return { ok: false, stopped: 'max-attempts', attempts, analysis: attempts.at(-1)?.analysis };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { analyzeTestFailure, AutoFixLoop, fingerprint };
|
|
@@ -22,6 +22,7 @@ const PRESETS = {
|
|
|
22
22
|
fileContentMaxChars: 1000,
|
|
23
23
|
conversationOnDisk: 6,
|
|
24
24
|
conversationInContext: 8,
|
|
25
|
+
workflowHistoryMaxTokens: 1024,
|
|
25
26
|
autoCompact: true,
|
|
26
27
|
compactModel: null,
|
|
27
28
|
reservedTokens: 1024
|
|
@@ -42,6 +43,7 @@ const PRESETS = {
|
|
|
42
43
|
fileContentMaxChars: 2000,
|
|
43
44
|
conversationOnDisk: 10,
|
|
44
45
|
conversationInContext: 12,
|
|
46
|
+
workflowHistoryMaxTokens: 2048,
|
|
45
47
|
autoCompact: true,
|
|
46
48
|
compactModel: null,
|
|
47
49
|
reservedTokens: 2048
|
|
@@ -62,6 +64,7 @@ const PRESETS = {
|
|
|
62
64
|
fileContentMaxChars: 5000,
|
|
63
65
|
conversationOnDisk: 20,
|
|
64
66
|
conversationInContext: 25,
|
|
67
|
+
workflowHistoryMaxTokens: 8192,
|
|
65
68
|
autoCompact: false,
|
|
66
69
|
compactModel: null,
|
|
67
70
|
reservedTokens: 4096
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { assessToolPath } = require('./tool-path-policy');
|
|
4
|
+
const { validateJsonSchema } = require('./json-schema');
|
|
5
|
+
const { standardToolResult } = require('./tool-result');
|
|
6
|
+
|
|
7
|
+
// Transport-agnostic pipeline: resolve -> availability -> policy -> execute -> post-process.
|
|
8
|
+
async function executeThroughGateway(options) {
|
|
9
|
+
const {
|
|
10
|
+
toolName, args = {}, resolveTool, checkAvailability, policyChecks = [],
|
|
11
|
+
execute, postProcess, allowSensitivePaths = false, standardResult = false,
|
|
12
|
+
normalizeSuccess = result => ({ result }),
|
|
13
|
+
normalizeError = error => ({ error }),
|
|
14
|
+
} = options || {};
|
|
15
|
+
|
|
16
|
+
if (!toolName || typeof resolveTool !== 'function') {
|
|
17
|
+
return normalizeError('Geçersiz araç çalıştırma isteği');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let tool;
|
|
21
|
+
try { tool = await resolveTool(toolName); }
|
|
22
|
+
catch (error) { return normalizeError(error?.message || String(error)); }
|
|
23
|
+
|
|
24
|
+
if (!tool) return normalizeError(`Tool bulunamadı: ${toolName}`);
|
|
25
|
+
const executor = execute || tool.execute;
|
|
26
|
+
if (typeof executor !== 'function') return normalizeError(`Tool execute fonksiyonu yok: ${toolName}`);
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const schema = tool.inputSchema || tool.parameters || tool.schema?.parameters;
|
|
30
|
+
const validation = validateJsonSchema(schema, args);
|
|
31
|
+
if (!validation.valid) return normalizeError(`Geçersiz araç argümanları: ${validation.errors.join('; ')}`);
|
|
32
|
+
if (!allowSensitivePaths) {
|
|
33
|
+
const pathDecision = assessToolPath(toolName, args);
|
|
34
|
+
if (!pathDecision.allowed) return normalizeError(pathDecision.reason);
|
|
35
|
+
}
|
|
36
|
+
if (checkAvailability) {
|
|
37
|
+
const decision = await checkAvailability({ toolName, args, tool });
|
|
38
|
+
if (decision === false) return normalizeError(`${toolName} şu anda kullanılamıyor`);
|
|
39
|
+
if (decision?.allowed === false) return normalizeError(decision.reason || `${toolName} şu anda kullanılamıyor`);
|
|
40
|
+
}
|
|
41
|
+
for (const check of policyChecks) {
|
|
42
|
+
if (typeof check !== 'function') continue;
|
|
43
|
+
const decision = await check({ toolName, args, tool });
|
|
44
|
+
if (decision === false) return normalizeError('Araç çalıştırma politikası engelledi');
|
|
45
|
+
if (decision?.allowed === false) return normalizeError(decision.reason || 'Araç çalıştırma politikası engelledi');
|
|
46
|
+
}
|
|
47
|
+
let result = await executor.call(tool, args);
|
|
48
|
+
if (postProcess) result = await postProcess({ toolName, args, tool, result });
|
|
49
|
+
if (standardResult) return standardToolResult(result, { tool: toolName });
|
|
50
|
+
return normalizeSuccess(result);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return normalizeError(error?.message || String(error));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { executeThroughGateway };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
let cache = null;
|
|
7
|
+
|
|
8
|
+
function loadToolManifest({ toolsDir = path.join(__dirname, '..', 'tools'), refresh = false } = {}) {
|
|
9
|
+
if (cache && !refresh) return cache;
|
|
10
|
+
const entries = new Map();
|
|
11
|
+
if (!fs.existsSync(toolsDir)) return entries;
|
|
12
|
+
for (const file of fs.readdirSync(toolsDir).filter(name => name.endsWith('.js')).sort()) {
|
|
13
|
+
try {
|
|
14
|
+
const mod = require(path.join(toolsDir, file));
|
|
15
|
+
const name = mod.name || path.basename(file, '.js');
|
|
16
|
+
const execute = mod.execute || mod.default?.execute;
|
|
17
|
+
if (!name || typeof execute !== 'function') continue;
|
|
18
|
+
entries.set(name, {
|
|
19
|
+
name, description: mod.description || `${name} tool`,
|
|
20
|
+
inputSchema: mod.inputSchema || mod.parameters || { type: 'object', properties: {} },
|
|
21
|
+
execute, module: mod, source: file,
|
|
22
|
+
});
|
|
23
|
+
} catch { /* unavailable optional tool */ }
|
|
24
|
+
}
|
|
25
|
+
cache = entries;
|
|
26
|
+
return entries;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function clearToolManifestCache() { cache = null; }
|
|
30
|
+
|
|
31
|
+
module.exports = { loadToolManifest, clearToolManifestCache };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
|
|
6
|
+
const SENSITIVE_READ = [
|
|
7
|
+
/(^|[\\/])\.ssh[\\/]/i, /id_rsa|id_ed25519|id_ecdsa|id_dsa/i,
|
|
8
|
+
/\.pem$|\.ppk$|\.key$/i, /(^|[\\/])\.aws[\\/]/i,
|
|
9
|
+
/gcloud[\\/].*(credential|token)/i, /(^|[\\/])\.npmrc$/i,
|
|
10
|
+
/(^|[\\/])\.git-credentials$/i, /\.natureco[\\/]config\.json$/i,
|
|
11
|
+
/(^|[\\/])\.netrc$/i,
|
|
12
|
+
];
|
|
13
|
+
const SENSITIVE_WRITE = [
|
|
14
|
+
...SENSITIVE_READ,
|
|
15
|
+
/(^|[\\/])authorized_keys$/i, /System32[\\/]drivers[\\/]etc/i,
|
|
16
|
+
/\.aws[\\/]credentials/i,
|
|
17
|
+
/^[/\\](etc|usr|bin|sbin|boot|sys)[\\/]/i,
|
|
18
|
+
/(^|[\\/])etc[\\/](passwd|shadow|sudoers|hosts|crontab|ssh)/i,
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function expandPath(value) {
|
|
22
|
+
if (typeof value !== 'string' || !value.trim()) return value;
|
|
23
|
+
const trimmed = value.trim();
|
|
24
|
+
if (trimmed === '~') return os.homedir();
|
|
25
|
+
if (trimmed.startsWith('~/') || trimmed.startsWith('~\\')) {
|
|
26
|
+
return path.join(os.homedir(), trimmed.slice(2));
|
|
27
|
+
}
|
|
28
|
+
return path.resolve(trimmed);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function assessToolPath(toolName, args = {}) {
|
|
32
|
+
const rawPath = args.path || args.filePath || args.file_path;
|
|
33
|
+
if (!rawPath) return { allowed: true };
|
|
34
|
+
const targetPath = expandPath(rawPath);
|
|
35
|
+
const write = toolName === 'write_file' || toolName === 'edit_file' || toolName === 'structural_patch';
|
|
36
|
+
const read = toolName === 'read_file';
|
|
37
|
+
if (!write && !read) return { allowed: true, path: targetPath };
|
|
38
|
+
const patterns = write ? SENSITIVE_WRITE : SENSITIVE_READ;
|
|
39
|
+
if (patterns.some(pattern => pattern.test(targetPath))) {
|
|
40
|
+
return {
|
|
41
|
+
allowed: false,
|
|
42
|
+
path: targetPath,
|
|
43
|
+
reason: `Hassas dosya yolu güvenlik politikasıyla engellendi: ${targetPath}`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return { allowed: true, path: targetPath };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { assessToolPath, expandPath, SENSITIVE_READ, SENSITIVE_WRITE };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function standardToolResult(value, metrics = {}) {
|
|
4
|
+
if (value && typeof value === 'object' && typeof value.ok === 'boolean' && 'data' in value) return value;
|
|
5
|
+
const failed = value && typeof value === 'object' && (value.success === false || value.error);
|
|
6
|
+
const error = failed ? String(value.error || 'Tool execution failed') : null;
|
|
7
|
+
return {
|
|
8
|
+
ok: !failed,
|
|
9
|
+
data: failed ? null : value,
|
|
10
|
+
error,
|
|
11
|
+
warnings: Array.isArray(value?.warnings) ? value.warnings : [],
|
|
12
|
+
artifacts: Array.isArray(value?.artifacts) ? value.artifacts : [],
|
|
13
|
+
metrics: { ...metrics, ...(value?.metrics || {}) },
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = { standardToolResult };
|
package/src/utils/tool-runner.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const chalk = require('chalk');
|
|
4
|
-
const inquirer = require('./inquirer-wrapper');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const inquirer = require('./inquirer-wrapper');
|
|
5
|
+
const { executeThroughGateway } = require('./tool-execution-gateway');
|
|
6
|
+
const { checkPermission } = require('./permissions');
|
|
7
|
+
const { checkPreHooks, runPostHooks } = require('./tool-hooks');
|
|
8
|
+
const { loadToolManifest } = require('./tool-manifest');
|
|
5
9
|
|
|
6
10
|
// ── Spinner ───────────────────────────────────────────────────────────────────
|
|
7
11
|
const SPINNER_FRAMES = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
|
|
@@ -12,8 +16,8 @@ function startSpinner(label) {
|
|
|
12
16
|
process.stdout.write(`\r${chalk.cyan(SPINNER_FRAMES[i++ % SPINNER_FRAMES.length])} ${chalk.gray(label)}`);
|
|
13
17
|
}, 80);
|
|
14
18
|
return timer;
|
|
15
|
-
}
|
|
16
|
-
|
|
19
|
+
}
|
|
20
|
+
|
|
17
21
|
function stopSpinner(timer, label, success = true) {
|
|
18
22
|
clearInterval(timer);
|
|
19
23
|
if (success) {
|
|
@@ -55,23 +59,15 @@ function resetSessionStats() {
|
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
// ── Load tools ────────────────────────────────────────────────────────────────
|
|
58
|
-
function loadTools() {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const tool = require(path.join(toolsDir, file));
|
|
68
|
-
if (tool.name && tool.execute) tools[tool.name] = tool;
|
|
69
|
-
} catch (err) {
|
|
70
|
-
console.error(chalk.red(`Failed to load tool ${file}:`, err.message));
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return tools;
|
|
74
|
-
}
|
|
62
|
+
function loadTools() {
|
|
63
|
+
return Object.fromEntries([...loadToolManifest()].map(([name, entry]) => [name, {
|
|
64
|
+
...entry.module,
|
|
65
|
+
name: entry.name,
|
|
66
|
+
description: entry.description,
|
|
67
|
+
inputSchema: entry.inputSchema,
|
|
68
|
+
execute: entry.execute,
|
|
69
|
+
}]));
|
|
70
|
+
}
|
|
75
71
|
|
|
76
72
|
function getToolDefinitions() {
|
|
77
73
|
const tools = loadTools();
|
|
@@ -90,11 +86,12 @@ function getToolDefinitions() {
|
|
|
90
86
|
// v5.51.1 GÜVENLİK: edit_file eklendi — write_file diff+onay isterken, aynı riski
|
|
91
87
|
// taşıyan hedefli değişiklik (edit_file) onaysız geçiyordu (SELF.md kendini-onarma
|
|
92
88
|
// + Tek Beyin kanal erişimiyle birleşince kritik).
|
|
93
|
-
function needsConfirmation(toolName, params) {
|
|
89
|
+
function needsConfirmation(toolName, params) {
|
|
94
90
|
const p = params || {};
|
|
95
91
|
return (
|
|
96
|
-
toolName === 'write_file' ||
|
|
97
|
-
toolName === 'edit_file' ||
|
|
92
|
+
toolName === 'write_file' ||
|
|
93
|
+
toolName === 'edit_file' ||
|
|
94
|
+
toolName === 'structural_patch' ||
|
|
98
95
|
((toolName === 'bash' || toolName === 'shell_command') && /\b(rm|mv|cp|chmod|chown|dd|mkfs|truncate)\b/.test(p.command || ''))
|
|
99
96
|
);
|
|
100
97
|
}
|
|
@@ -103,18 +100,33 @@ function needsConfirmation(toolName, params) {
|
|
|
103
100
|
async function executeTool(toolName, params, opts = {}) {
|
|
104
101
|
const safeParams = params ?? {};
|
|
105
102
|
const tools = loadTools();
|
|
106
|
-
const tool = tools[toolName];
|
|
107
|
-
const agentMode = opts.agentMode || false;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
103
|
+
const tool = tools[toolName];
|
|
104
|
+
const agentMode = opts.agentMode || false;
|
|
105
|
+
if (!tool) {
|
|
106
|
+
return { success: false, error: `Tool '${toolName}' not found` };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Configured permission rules and pre-hooks are mandatory in every origin.
|
|
110
|
+
// Interactive callers may approve `ask`; API/headless/channel callers must
|
|
111
|
+
// fail closed because they cannot prove that a human approved the action.
|
|
112
|
+
const permission = checkPermission(toolName, safeParams);
|
|
113
|
+
const hook = checkPreHooks(toolName, safeParams);
|
|
114
|
+
const policy = evaluatePolicyDecision(permission, hook, opts);
|
|
115
|
+
const policyAsk = policy.needsApproval ? { reason: policy.reason } : null;
|
|
116
|
+
if (!policy.allowed) {
|
|
117
|
+
return {
|
|
118
|
+
success: false,
|
|
119
|
+
error: policy.needsApproval
|
|
120
|
+
? `Etkileşimsiz çağrıda kullanıcı onayı gerekiyor: ${policy.reason || toolName}`
|
|
121
|
+
: policy.reason || 'Araç güvenlik politikasıyla engellendi.',
|
|
122
|
+
};
|
|
123
|
+
}
|
|
112
124
|
|
|
113
125
|
const label = `${toolName}${safeParams.path ? ' — ' + safeParams.path : safeParams.command ? ' — ' + safeParams.command : ''}`;
|
|
114
126
|
|
|
115
127
|
// ── Onay mekanizması (dosya değiştiren araçlar ve tehlikeli bash) ──────────
|
|
116
|
-
if (agentMode) {
|
|
117
|
-
if (needsConfirmation(toolName, safeParams)) {
|
|
128
|
+
if (agentMode) {
|
|
129
|
+
if (policyAsk || needsConfirmation(toolName, safeParams)) {
|
|
118
130
|
if (toolName === 'write_file') {
|
|
119
131
|
// Diff göster
|
|
120
132
|
let oldContent = '';
|
|
@@ -136,8 +148,9 @@ async function executeTool(toolName, params, opts = {}) {
|
|
|
136
148
|
console.log(chalk.yellow(`\n 🖥️ Komut: ${chalk.white(safeParams.command)}\n`));
|
|
137
149
|
}
|
|
138
150
|
|
|
139
|
-
const confirmMsg =
|
|
140
|
-
|
|
151
|
+
const confirmMsg =
|
|
152
|
+
policyAsk ? `🛡️ ${policyAsk.reason || toolName + ' için izin gerekli'}` :
|
|
153
|
+
toolName === 'write_file' ? `✏️ ${safeParams.path} dosyası değiştirilecek` :
|
|
141
154
|
toolName === 'edit_file' ? `✏️ ${safeParams.path} dosyasında değişiklik yapılacak` :
|
|
142
155
|
'⚠️ Bu komut çalıştırılacak';
|
|
143
156
|
const { confirm } = await inquirer.prompt([{
|
|
@@ -155,23 +168,37 @@ async function executeTool(toolName, params, opts = {}) {
|
|
|
155
168
|
}
|
|
156
169
|
|
|
157
170
|
// ── Spinner ile çalıştır ──────────────────────────────────────────────────
|
|
158
|
-
const spinner = startSpinner(label);
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
171
|
+
const spinner = startSpinner(label);
|
|
172
|
+
const result = await executeThroughGateway({
|
|
173
|
+
toolName,
|
|
174
|
+
args: safeParams,
|
|
175
|
+
resolveTool: () => tool,
|
|
176
|
+
postProcess: ({ result: value }) => runPostHooks(toolName, safeParams, value),
|
|
177
|
+
normalizeSuccess: value => value,
|
|
178
|
+
normalizeError: error => ({ success: false, error }),
|
|
179
|
+
allowSensitivePaths: !!opts.allowSensitivePaths,
|
|
180
|
+
});
|
|
181
|
+
stopSpinner(spinner, label, result.success !== false);
|
|
182
|
+
|
|
183
|
+
// İstatistik güncelle
|
|
184
|
+
if (result.success !== false) {
|
|
185
|
+
if (toolName === 'write_file' || toolName === 'edit_file' || toolName === 'structural_patch') filesChanged++;
|
|
186
|
+
if (toolName === 'bash' || toolName === 'shell_command') commandsRun++;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function evaluatePolicyDecision(permission, hook, opts = {}) {
|
|
193
|
+
const decisions = [permission, hook].filter(Boolean);
|
|
194
|
+
const denied = decisions.find(decision => decision.action === 'deny');
|
|
195
|
+
if (denied) return { allowed: false, needsApproval: false, reason: denied.reason };
|
|
196
|
+
const asked = decisions.find(decision => decision.action === 'ask');
|
|
197
|
+
if (!asked) return { allowed: true, needsApproval: false };
|
|
198
|
+
if (opts.agentMode) return { allowed: true, needsApproval: true, reason: asked.reason };
|
|
199
|
+
if (opts.approvalMode === 'preapproved') return { allowed: true, needsApproval: false };
|
|
200
|
+
return { allowed: false, needsApproval: true, reason: asked.reason };
|
|
201
|
+
}
|
|
175
202
|
|
|
176
203
|
// ── Execute multiple tool calls (parallel for independent, sequential for others) ──
|
|
177
204
|
const PARALLEL_SAFE_TOOLS = new Set(['read_file', 'file_search', 'grep_search', 'web_search', 'web_readability', 'duckduckgo_search', 'exa_search', 'searxng_search', 'firecrawl', 'memory_search', 'memory']);
|
|
@@ -210,5 +237,6 @@ module.exports = {
|
|
|
210
237
|
executeToolCalls,
|
|
211
238
|
getSessionStats,
|
|
212
239
|
resetSessionStats,
|
|
213
|
-
needsConfirmation,
|
|
214
|
-
|
|
240
|
+
needsConfirmation,
|
|
241
|
+
evaluatePolicyDecision,
|
|
242
|
+
};
|
package/src/utils/tools.js
CHANGED
|
@@ -5,18 +5,16 @@
|
|
|
5
5
|
* v5.7.17: Emoji + toolset + check_fn + registry entegrasyonu.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const {
|
|
11
|
-
const
|
|
8
|
+
const { globalRegistry } = require('./registry');
|
|
9
|
+
const sandbox = require('./sandbox');
|
|
10
|
+
const { executeThroughGateway } = require('./tool-execution-gateway');
|
|
11
|
+
const { loadToolManifest } = require('./tool-manifest');
|
|
12
12
|
|
|
13
13
|
// Lazy config read — avoids circular deps
|
|
14
14
|
function _getSandboxLevel() {
|
|
15
15
|
try { return sandbox.getLevel(require('./config').getConfig()); } catch { return 'none'; }
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
const TOOLS_DIR = path.join(__dirname, '..', 'tools');
|
|
19
|
-
|
|
20
18
|
// ── Emoji map (central, tek kaynak) ──────────────────────────────────────
|
|
21
19
|
const EMOJI_MAP = {
|
|
22
20
|
// File operations
|
|
@@ -174,7 +172,7 @@ function getToolsForProvider(allTools, providerUrl) {
|
|
|
174
172
|
|
|
175
173
|
function loadToolDefinitions() {
|
|
176
174
|
const tools = [];
|
|
177
|
-
const
|
|
175
|
+
const manifest = loadToolManifest();
|
|
178
176
|
|
|
179
177
|
let isGroq = false;
|
|
180
178
|
try {
|
|
@@ -197,20 +195,19 @@ function loadToolDefinitions() {
|
|
|
197
195
|
'soul', 'memory', 'memory_write', 'memory_search', 'filesystem', 'grep_search'
|
|
198
196
|
]);
|
|
199
197
|
|
|
200
|
-
for (const
|
|
201
|
-
try {
|
|
202
|
-
const
|
|
203
|
-
const
|
|
204
|
-
const toolName = mod.name || path.basename(file, '.js');
|
|
198
|
+
for (const entry of manifest.values()) {
|
|
199
|
+
try {
|
|
200
|
+
const mod = entry.module;
|
|
201
|
+
const toolName = entry.name;
|
|
205
202
|
|
|
206
203
|
if (isGroq && !GROQ_ALLOWED.has(toolName)) continue;
|
|
207
204
|
if (disabledTools.has(toolName)) continue;
|
|
208
205
|
|
|
209
206
|
const meta = {
|
|
210
207
|
name: toolName,
|
|
211
|
-
description:
|
|
212
|
-
parameters:
|
|
213
|
-
execute:
|
|
208
|
+
description: entry.description,
|
|
209
|
+
parameters: entry.inputSchema,
|
|
210
|
+
execute: entry.execute,
|
|
214
211
|
emoji: EMOJI_MAP[toolName] || '',
|
|
215
212
|
toolset: TOOLSET_MAP[toolName] || 'general',
|
|
216
213
|
checkFn: CHECK_FN_MAP[toolName] || null,
|
|
@@ -290,33 +287,24 @@ function toOpenAIFormat(toolDefs) {
|
|
|
290
287
|
});
|
|
291
288
|
}
|
|
292
289
|
|
|
293
|
-
async function executeTool(toolName, args, toolDefs) {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
try {
|
|
314
|
-
const result = await tool.execute(args || {});
|
|
315
|
-
return { result };
|
|
316
|
-
} catch (e) {
|
|
317
|
-
return { error: e.message || String(e) };
|
|
318
|
-
}
|
|
319
|
-
}
|
|
290
|
+
async function executeTool(toolName, args, toolDefs) {
|
|
291
|
+
return executeThroughGateway({
|
|
292
|
+
toolName,
|
|
293
|
+
args: args || {},
|
|
294
|
+
resolveTool: name => (toolDefs || []).find(tool => tool.name === name),
|
|
295
|
+
checkAvailability: ({ tool }) => !tool.checkFn || _cachedCheckFn(tool.checkFn, tool.name)
|
|
296
|
+
? true
|
|
297
|
+
: { allowed: false, reason: `${toolName} şu anda kullanılamıyor (check_fn engelledi)` },
|
|
298
|
+
policyChecks: [({ args: safeArgs }) => {
|
|
299
|
+
if (toolName !== 'bash' && toolName !== 'shell_command') return true;
|
|
300
|
+
const level = _getSandboxLevel();
|
|
301
|
+
if (level === 'strict' && sandbox.isNetworkCommand(safeArgs.command || '')) {
|
|
302
|
+
return { allowed: false, reason: 'strict sandbox: network komutları engellendi. Daha düşük sandbox seviyesi kullanın.' };
|
|
303
|
+
}
|
|
304
|
+
return true;
|
|
305
|
+
}],
|
|
306
|
+
});
|
|
307
|
+
}
|
|
320
308
|
|
|
321
309
|
module.exports = {
|
|
322
310
|
loadToolDefinitions, toOpenAIFormat, executeTool, getToolsForProvider,
|