natureco-cli 5.63.0 → 5.64.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 +21 -0
- package/bin/natureco.js +10 -3
- package/package.json +2 -1
- package/scripts/benchmark-startup.js +26 -0
- package/src/commands/backup.js +3 -3
- package/src/commands/code.js +49 -14
- package/src/commands/code_v5.js +15 -1
- package/src/commands/gateway-server.js +128 -24
- package/src/commands/git.js +2 -2
- 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/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/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
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
function deriveKey(syncKey, salt) { return crypto.scryptSync(String(syncKey), salt, 32); }
|
|
6
|
+
|
|
7
|
+
function encryptSyncPayload(payload, options = {}) {
|
|
8
|
+
if (!options.syncKey) throw new Error('syncKey is required');
|
|
9
|
+
if (!options.deviceId) throw new Error('deviceId is required');
|
|
10
|
+
const salt = crypto.randomBytes(16); const iv = crypto.randomBytes(12);
|
|
11
|
+
const key = deriveKey(options.syncKey, salt);
|
|
12
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
13
|
+
const header = { version: 1, deviceId: options.deviceId, revision: options.revision || 1, clock: options.clock || { [options.deviceId]: options.revision || 1 }, createdAt: new Date().toISOString() };
|
|
14
|
+
cipher.setAAD(Buffer.from(JSON.stringify(header)));
|
|
15
|
+
const encrypted = Buffer.concat([cipher.update(JSON.stringify(payload), 'utf8'), cipher.final()]);
|
|
16
|
+
return { ...header, algorithm: 'aes-256-gcm', salt: salt.toString('base64'), iv: iv.toString('base64'), tag: cipher.getAuthTag().toString('base64'), ciphertext: encrypted.toString('base64') };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function decryptSyncPayload(envelope, options = {}) {
|
|
20
|
+
if (!options.syncKey) throw new Error('syncKey is required');
|
|
21
|
+
if (!envelope || envelope.version !== 1 || envelope.algorithm !== 'aes-256-gcm') throw new Error('unsupported sync envelope');
|
|
22
|
+
const header = { version: envelope.version, deviceId: envelope.deviceId, revision: envelope.revision, clock: envelope.clock, createdAt: envelope.createdAt };
|
|
23
|
+
try {
|
|
24
|
+
const key = deriveKey(options.syncKey, Buffer.from(envelope.salt, 'base64'));
|
|
25
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(envelope.iv, 'base64'));
|
|
26
|
+
decipher.setAAD(Buffer.from(JSON.stringify(header))); decipher.setAuthTag(Buffer.from(envelope.tag, 'base64'));
|
|
27
|
+
return { header, payload: JSON.parse(Buffer.concat([decipher.update(Buffer.from(envelope.ciphertext, 'base64')), decipher.final()]).toString('utf8')) };
|
|
28
|
+
} catch { throw new Error('sync payload authentication failed'); }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function compareClocks(left = {}, right = {}) {
|
|
32
|
+
const devices = new Set([...Object.keys(left), ...Object.keys(right)]);
|
|
33
|
+
let leftAhead = false, rightAhead = false;
|
|
34
|
+
for (const device of devices) {
|
|
35
|
+
const l = left[device] || 0, r = right[device] || 0;
|
|
36
|
+
if (l > r) leftAhead = true; if (r > l) rightAhead = true;
|
|
37
|
+
}
|
|
38
|
+
if (leftAhead && rightAhead) return 'concurrent';
|
|
39
|
+
if (leftAhead) return 'after';
|
|
40
|
+
if (rightAhead) return 'before';
|
|
41
|
+
return 'equal';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function mergeSyncRecords(local = [], remote = []) {
|
|
45
|
+
const records = new Map(); const conflicts = [];
|
|
46
|
+
for (const item of [...local, ...remote]) {
|
|
47
|
+
if (!item?.id) continue;
|
|
48
|
+
const existing = records.get(item.id);
|
|
49
|
+
if (!existing) { records.set(item.id, item); continue; }
|
|
50
|
+
const relation = compareClocks(existing.clock, item.clock);
|
|
51
|
+
if (relation === 'before') records.set(item.id, item);
|
|
52
|
+
else if (relation === 'concurrent' && JSON.stringify(existing.value) !== JSON.stringify(item.value)) {
|
|
53
|
+
conflicts.push({ id: item.id, local: existing, remote: item });
|
|
54
|
+
const winner = existing.userConfirmed !== item.userConfirmed
|
|
55
|
+
? (item.userConfirmed ? item : existing)
|
|
56
|
+
: (String(item.updatedAt || '') > String(existing.updatedAt || '') ? item : existing);
|
|
57
|
+
records.set(item.id, winner);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { records: [...records.values()], conflicts };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { encryptSyncPayload, decryptSyncPayload, compareClocks, mergeSyncRecords };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const { writeFileAtomicSync, writeJsonAtomicSync, readJsonSafeSync } = require('./atomic-file');
|
|
7
|
+
|
|
8
|
+
function validSkill(content) {
|
|
9
|
+
return typeof content === 'string' && content.startsWith('---') && /\nname:\s*[^\n]+/.test(content) && /\ndescription:\s*[^\n]+/.test(content);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
class SkillLifecycle {
|
|
13
|
+
constructor(root) { this.root = root; }
|
|
14
|
+
_dir(name) { return path.join(this.root, name); }
|
|
15
|
+
_meta(name) { return path.join(this._dir(name), 'lifecycle.json'); }
|
|
16
|
+
|
|
17
|
+
stage({ name, content, source = 'pattern', evidence = {} }) {
|
|
18
|
+
if (!name || !/^[a-z0-9][a-z0-9-]*$/.test(name)) return { ok: false, error: 'invalid skill name' };
|
|
19
|
+
if (!validSkill(content)) return { ok: false, error: 'invalid SKILL.md frontmatter' };
|
|
20
|
+
const id = `candidate_${crypto.randomBytes(6).toString('hex')}`;
|
|
21
|
+
return { ok: true, candidate: { id, name, content, source, evidence, status: 'candidate', createdAt: new Date().toISOString() } };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async promote(candidate, approval, validate = async () => ({ ok: true })) {
|
|
25
|
+
if (!approval?.approved || !approval.userId) return { ok: false, error: 'explicit user approval required' };
|
|
26
|
+
const validation = await validate(candidate);
|
|
27
|
+
if (!validation?.ok) return { ok: false, error: 'skill validation failed', validation };
|
|
28
|
+
const dir = this._dir(candidate.name);
|
|
29
|
+
fs.mkdirSync(path.join(dir, 'versions'), { recursive: true });
|
|
30
|
+
const meta = readJsonSafeSync(this._meta(candidate.name), { currentVersion: 0, versions: [] });
|
|
31
|
+
const version = (meta.currentVersion || 0) + 1;
|
|
32
|
+
const currentPath = path.join(dir, 'SKILL.md');
|
|
33
|
+
if (fs.existsSync(currentPath) && meta.currentVersion) {
|
|
34
|
+
writeFileAtomicSync(path.join(dir, 'versions', `${meta.currentVersion}.md`), fs.readFileSync(currentPath));
|
|
35
|
+
}
|
|
36
|
+
writeFileAtomicSync(currentPath, candidate.content);
|
|
37
|
+
meta.currentVersion = version;
|
|
38
|
+
meta.versions.push({ version, candidateId: candidate.id, source: candidate.source, evidence: candidate.evidence, approvedBy: approval.userId, approvedAt: new Date().toISOString(), validation });
|
|
39
|
+
writeJsonAtomicSync(this._meta(candidate.name), meta, { mode: 0o600 });
|
|
40
|
+
return { ok: true, name: candidate.name, version, path: currentPath, validation };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
rollback(name, version, approval) {
|
|
44
|
+
if (!approval?.approved || !approval.userId) return { ok: false, error: 'explicit user approval required' };
|
|
45
|
+
const dir = this._dir(name);
|
|
46
|
+
const source = path.join(dir, 'versions', `${version}.md`);
|
|
47
|
+
if (!fs.existsSync(source)) return { ok: false, error: `version not found: ${version}` };
|
|
48
|
+
const current = path.join(dir, 'SKILL.md');
|
|
49
|
+
const meta = readJsonSafeSync(this._meta(name), { currentVersion: 0, versions: [] });
|
|
50
|
+
if (fs.existsSync(current) && meta.currentVersion) writeFileAtomicSync(path.join(dir, 'versions', `${meta.currentVersion}.md`), fs.readFileSync(current));
|
|
51
|
+
writeFileAtomicSync(current, fs.readFileSync(source));
|
|
52
|
+
meta.currentVersion = version;
|
|
53
|
+
meta.rollback = { toVersion: version, approvedBy: approval.userId, at: new Date().toISOString() };
|
|
54
|
+
writeJsonAtomicSync(this._meta(name), meta, { mode: 0o600 });
|
|
55
|
+
return { ok: true, name, version, path: current };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { SkillLifecycle, validSkill };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const { writeFileAtomicSync } = require('./atomic-file');
|
|
7
|
+
|
|
8
|
+
function hashContent(content) { return crypto.createHash('sha256').update(content).digest('hex'); }
|
|
9
|
+
|
|
10
|
+
class StructuralPatchEngine {
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this.history = new Map();
|
|
13
|
+
this.maxHistory = options.maxHistory || 50;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
inspect(filePath) {
|
|
17
|
+
const target = path.resolve(filePath);
|
|
18
|
+
if (!fs.existsSync(target)) return { exists: false, path: target, hash: null, content: '' };
|
|
19
|
+
const content = fs.readFileSync(target, 'utf8');
|
|
20
|
+
return { exists: true, path: target, hash: hashContent(content), content };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
preview(filePath, operations, options = {}) {
|
|
24
|
+
const state = this.inspect(filePath);
|
|
25
|
+
if (options.expectedHash && options.expectedHash !== state.hash) {
|
|
26
|
+
return { ok: false, error: 'conflict: file changed since it was read', expectedHash: options.expectedHash, actualHash: state.hash };
|
|
27
|
+
}
|
|
28
|
+
let updated = state.content;
|
|
29
|
+
const changes = [];
|
|
30
|
+
for (const [index, operation] of (operations || []).entries()) {
|
|
31
|
+
const search = operation.search;
|
|
32
|
+
const replacement = operation.replace ?? '';
|
|
33
|
+
if (typeof search !== 'string' || search.length === 0) return { ok: false, error: `operation ${index}: search is required` };
|
|
34
|
+
const occurrences = updated.split(search).length - 1;
|
|
35
|
+
if (occurrences === 0) return { ok: false, error: `operation ${index}: search text not found` };
|
|
36
|
+
if (occurrences > 1 && !operation.replaceAll) return { ok: false, error: `operation ${index}: search is ambiguous (${occurrences} matches)` };
|
|
37
|
+
updated = operation.replaceAll ? updated.split(search).join(replacement) : updated.replace(search, replacement);
|
|
38
|
+
changes.push({ index, occurrences: operation.replaceAll ? occurrences : 1, removedChars: search.length, addedChars: replacement.length });
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
ok: true, path: state.path, beforeHash: state.hash, afterHash: hashContent(updated),
|
|
42
|
+
before: state.content, after: updated, changes,
|
|
43
|
+
risk: changes.length > 10 || Math.abs(updated.length - state.content.length) > 10000 ? 'high' : changes.length > 3 ? 'medium' : 'low',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
apply(filePath, operations, options = {}) {
|
|
48
|
+
const preview = this.preview(filePath, operations, options);
|
|
49
|
+
if (!preview.ok || options.dryRun) return preview;
|
|
50
|
+
const id = `patch_${Date.now().toString(36)}_${crypto.randomBytes(3).toString('hex')}`;
|
|
51
|
+
writeFileAtomicSync(preview.path, preview.after);
|
|
52
|
+
this.history.set(id, { path: preview.path, content: preview.before, expectedHash: preview.afterHash });
|
|
53
|
+
while (this.history.size > this.maxHistory) this.history.delete(this.history.keys().next().value);
|
|
54
|
+
return { ok: true, id, path: preview.path, beforeHash: preview.beforeHash, afterHash: preview.afterHash, changes: preview.changes, risk: preview.risk };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
rollback(id) {
|
|
58
|
+
const entry = this.history.get(id);
|
|
59
|
+
if (!entry) return { ok: false, error: `unknown patch: ${id}` };
|
|
60
|
+
const current = this.inspect(entry.path);
|
|
61
|
+
if (current.hash !== entry.expectedHash) return { ok: false, error: 'rollback conflict: file changed after patch', actualHash: current.hash };
|
|
62
|
+
writeFileAtomicSync(entry.path, entry.content);
|
|
63
|
+
this.history.delete(id);
|
|
64
|
+
return { ok: true, id, path: entry.path, hash: hashContent(entry.content) };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = { StructuralPatchEngine, hashContent };
|
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 };
|
|
@@ -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
|
+
};
|