@causyne/agent 1.10.11 → 1.10.13
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/LICENSE +1 -1
- package/docs/ARCHITECTURE.md +46 -1
- package/package.json +2 -1
- package/src/agentLoop.js +136 -90
- package/src/benchmarks/planCache.js +159 -0
- package/src/commands.js +83 -0
- package/src/cortex/autoCondenser.js +1 -1
- package/src/hooks.js +136 -0
- package/src/planCache.js +295 -14
- package/src/plugins/pluginManager.js +425 -0
- package/src/plugins/pluginRunner.js +50 -0
- package/src/sandbox.js +21 -0
- package/src/taskShape.js +1 -1
- package/src/toolPacks.js +6 -3
- package/src/tools/deferredMcpSearch.js +2 -2
- package/src/tools/pluginManager.js +61 -0
- package/src/tools/registry.js +47 -53
- package/src/tools/toolForge.js +47 -95
package/src/tools/registry.js
CHANGED
|
@@ -34,6 +34,7 @@ import { generateMediaViaApi } from './generateMediaViaApi.js';
|
|
|
34
34
|
import { memoryWrite } from './memoryWrite.js';
|
|
35
35
|
import { printerPrint, printerList, printerStatus } from './printer.js';
|
|
36
36
|
import { toolForgeTool } from './toolForge.js';
|
|
37
|
+
import { pluginManagerTool } from './pluginManager.js';
|
|
37
38
|
import { astryxOracle } from './astryxOracle.js';
|
|
38
39
|
import { pdfRead } from './pdfReader.js';
|
|
39
40
|
import { codeVerifySyntax, codeListSymbols } from './codeTools.js';
|
|
@@ -67,11 +68,8 @@ import { gameCanvasAudit } from './gameCanvasAudit.js';
|
|
|
67
68
|
import readNativeSkill from './readNativeSkill.js';
|
|
68
69
|
import { createSkill } from './createSkill.js';
|
|
69
70
|
import { resolveActiveTools } from '../toolPacks.js';
|
|
70
|
-
import fs from 'fs';
|
|
71
|
-
import path from 'path';
|
|
72
|
-
import os from 'os';
|
|
73
71
|
import { discoverTools, resolveMcpTool, isMcpToolName } from '../mcp/mcpRegistry.js';
|
|
74
|
-
import {
|
|
72
|
+
import { getPluginToolDefinitions, resolvePluginTool } from '../plugins/pluginManager.js';
|
|
75
73
|
|
|
76
74
|
// Phase 6 AGI Upgrade: Schema Validation Hook
|
|
77
75
|
function validateSchema(schema) {
|
|
@@ -112,7 +110,7 @@ const TOOL_MODULES = [
|
|
|
112
110
|
codebaseSearch, browserAct, viewImage, webSearch,
|
|
113
111
|
updatePlan, clipboard, screenshot, axSnapshot, desktopAct, archive,
|
|
114
112
|
checkSystemSpecs, executeProceduralMedia, manageLocalAiDependencies,
|
|
115
|
-
generateMediaViaApi, memoryWrite, printerPrint, printerList, printerStatus, toolForgeTool,
|
|
113
|
+
generateMediaViaApi, memoryWrite, printerPrint, printerList, printerStatus, toolForgeTool, pluginManagerTool,
|
|
116
114
|
astryxOracle, pdfRead, codeVerifySyntax, codeListSymbols, blenderAct, resolveAutomator, palmierAutomator,
|
|
117
115
|
codeSearchAst, codeSearcher, persistentShell, subagentOrchestrator, deferredMcpSearch, visionInspector,
|
|
118
116
|
godotAutomator, gameBuilder, gameAssetFetcher,
|
|
@@ -164,6 +162,7 @@ export function resolveTool(name) {
|
|
|
164
162
|
// MCP tools are namespaced `mcp__server__tool`, so they can never shadow a
|
|
165
163
|
// native name — the native lookup always wins above.
|
|
166
164
|
if (isMcpToolName(name)) return resolveMcpTool(name);
|
|
165
|
+
if (normalized.startsWith('plugin__')) return resolvePluginTool(normalized);
|
|
167
166
|
return null;
|
|
168
167
|
}
|
|
169
168
|
|
|
@@ -174,52 +173,6 @@ export function canonicalToolName(name) {
|
|
|
174
173
|
return TOOL_ALIASES[normalized] || normalized;
|
|
175
174
|
}
|
|
176
175
|
|
|
177
|
-
// Forged tools are hot-reloadable, but re-importing them with a cache-busting
|
|
178
|
-
// query string on every single agent step leaked one ES module per step and
|
|
179
|
-
// changed the tool ordering between requests, which silently defeats OpenAI and
|
|
180
|
-
// DeepSeek automatic prompt caching. We reload only when a file actually
|
|
181
|
-
// changes, keyed on path + mtime.
|
|
182
|
-
const forgedModuleCache = new Map();
|
|
183
|
-
|
|
184
|
-
async function loadForgedTools() {
|
|
185
|
-
const CUSTOM_TOOLS_DIR = path.join(causyneHome(), 'custom_tools');
|
|
186
|
-
if (!fs.existsSync(CUSTOM_TOOLS_DIR)) return [];
|
|
187
|
-
|
|
188
|
-
const forged = [];
|
|
189
|
-
// Sorted so the resulting schema order is identical across requests.
|
|
190
|
-
const files = fs.readdirSync(CUSTOM_TOOLS_DIR).filter(f => f.endsWith('.js')).sort();
|
|
191
|
-
|
|
192
|
-
for (const file of files) {
|
|
193
|
-
const fullPath = path.join(CUSTOM_TOOLS_DIR, file);
|
|
194
|
-
try {
|
|
195
|
-
const mtime = fs.statSync(fullPath).mtimeMs;
|
|
196
|
-
const cached = forgedModuleCache.get(fullPath);
|
|
197
|
-
let imported;
|
|
198
|
-
|
|
199
|
-
if (cached && cached.mtime === mtime) {
|
|
200
|
-
imported = cached.module;
|
|
201
|
-
} else {
|
|
202
|
-
imported = await import(`file://${fullPath}?v=${mtime}`);
|
|
203
|
-
forgedModuleCache.set(fullPath, { mtime, module: imported });
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
for (const key of Object.keys(imported)) {
|
|
207
|
-
if (imported[key] && imported[key].schema) {
|
|
208
|
-
const normalizedName = imported[key].schema.name.replace(/\./g, '_');
|
|
209
|
-
forged.push({ ...imported[key].schema, name: normalizedName });
|
|
210
|
-
if (!toolRegistry[normalizedName]) {
|
|
211
|
-
toolRegistry[normalizedName] = imported[key];
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
} catch (e) {
|
|
216
|
-
console.error(`[TOOL FORGE] Failed to load custom tool ${file}:`, e.message);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return forged;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
176
|
// Phase 6 AGI Upgrade & V5 Cortex: Dynamic Tool Culling and Hot-Reloading Engine
|
|
224
177
|
//
|
|
225
178
|
// options.unlockedPacks / options.unlockedTools drive Sovereign Progressive
|
|
@@ -227,15 +180,56 @@ async function loadForgedTools() {
|
|
|
227
180
|
// — deferred schemas simply stay out of the context window until the agent
|
|
228
181
|
// pre-warms or searches for them. options.grantAllTools restores the legacy
|
|
229
182
|
// behaviour of loading all 69 schemas up front.
|
|
183
|
+
function dedupeDefinitions(definitions) {
|
|
184
|
+
const seenNames = new Set();
|
|
185
|
+
return definitions.filter((definition) => {
|
|
186
|
+
const name = normalizeToolName(definition?.name);
|
|
187
|
+
if (!name || seenNames.has(name)) return false;
|
|
188
|
+
seenNames.add(name);
|
|
189
|
+
return true;
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function getSearchableToolDefinitions() {
|
|
194
|
+
const plugins = getPluginToolDefinitions();
|
|
195
|
+
let mcp = [];
|
|
196
|
+
try {
|
|
197
|
+
mcp = await discoverTools();
|
|
198
|
+
} catch (e) { /* a broken third-party server must not hide native tools */ }
|
|
199
|
+
return dedupeDefinitions([...TOOL_DEFINITIONS, ...plugins, ...mcp]);
|
|
200
|
+
}
|
|
201
|
+
|
|
230
202
|
export async function getDynamicTools(autonomyMode = 'C', supportsVision = true, options = {}) {
|
|
231
|
-
|
|
203
|
+
// Plugin discovery reads validated JSON manifests only. Extension JavaScript
|
|
204
|
+
// is never imported into the host process during startup or tool assembly;
|
|
205
|
+
// it runs later in a permission-restricted child process after enablement.
|
|
206
|
+
const plugins = getPluginToolDefinitions();
|
|
232
207
|
// Tools from user-configured MCP servers, appended last so the native
|
|
233
208
|
// schema order — which the provider prompt caches depend on — never shifts.
|
|
234
209
|
let mcp = [];
|
|
235
210
|
try {
|
|
236
211
|
mcp = await discoverTools();
|
|
237
212
|
} catch (e) { /* a broken third-party server must not take the catalogue down */ }
|
|
238
|
-
let tools = [...TOOL_DEFINITIONS, ...
|
|
213
|
+
let tools = [...TOOL_DEFINITIONS, ...plugins, ...mcp];
|
|
214
|
+
|
|
215
|
+
// Built-ins win name collisions, followed by the first explicitly loaded
|
|
216
|
+
// extension. This prevents an MCP server or local extension from shadowing a
|
|
217
|
+
// native safety-sensitive tool while keeping tool ordering deterministic for
|
|
218
|
+
// provider prompt caches.
|
|
219
|
+
tools = dedupeDefinitions(tools);
|
|
220
|
+
|
|
221
|
+
// Deny/allow policy is applied before the model sees the catalogue. Runtime
|
|
222
|
+
// validation still runs later; this early filter avoids wasting a model turn
|
|
223
|
+
// on a tool the harness already knows it will reject.
|
|
224
|
+
const allowedTools = Array.isArray(options.allowedTools) ? options.allowedTools : ['*'];
|
|
225
|
+
if (!allowedTools.includes('*')) {
|
|
226
|
+
const allowed = new Set(allowedTools.map(normalizeToolName));
|
|
227
|
+
tools = tools.filter((definition) => allowed.has(normalizeToolName(definition.name)));
|
|
228
|
+
}
|
|
229
|
+
if (Array.isArray(options.deniedTools) && options.deniedTools.length > 0) {
|
|
230
|
+
const denied = new Set(options.deniedTools.map(normalizeToolName));
|
|
231
|
+
tools = tools.filter((definition) => !denied.has(normalizeToolName(definition.name)));
|
|
232
|
+
}
|
|
239
233
|
|
|
240
234
|
if (!options.grantAllTools) {
|
|
241
235
|
tools = resolveActiveTools(tools, options.unlockedPacks, options.unlockedTools, options.unlockOrder, {
|
package/src/tools/toolForge.js
CHANGED
|
@@ -1,124 +1,76 @@
|
|
|
1
|
-
import
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
import { causyneHome } from '../utils/paths.js';
|
|
1
|
+
import { createPlugin } from '../plugins/pluginManager.js';
|
|
5
2
|
|
|
6
|
-
|
|
3
|
+
function normaliseToolName(value) {
|
|
4
|
+
return String(value || '')
|
|
5
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
6
|
+
.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
7
|
+
.toLowerCase()
|
|
8
|
+
.replace(/^_+|_+$/g, '');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function pluginNameFor(toolName) {
|
|
12
|
+
return `forged-${toolName.replace(/_/g, '-')}`.slice(0, 64);
|
|
13
|
+
}
|
|
7
14
|
|
|
8
15
|
export const toolForgeTool = {
|
|
9
16
|
schema: {
|
|
10
17
|
name: 'tool.forge',
|
|
11
|
-
description: '
|
|
18
|
+
description: 'Compatibility entry point for creating a local custom-tool plugin. The generated plugin is disabled until the user reviews and explicitly enables it with plugin_manager.',
|
|
12
19
|
parameters: {
|
|
13
20
|
type: 'object',
|
|
14
21
|
properties: {
|
|
15
22
|
toolName: {
|
|
16
23
|
type: 'string',
|
|
17
|
-
description: '
|
|
24
|
+
description: 'Name of the custom tool. It is normalized to lowercase letters, digits, underscores, and hyphens.'
|
|
18
25
|
},
|
|
19
26
|
toolDescription: {
|
|
20
27
|
type: 'string',
|
|
21
|
-
description: 'Detailed description of what the tool does
|
|
28
|
+
description: 'Detailed description of what the tool does and when it should be used.'
|
|
22
29
|
},
|
|
23
30
|
parametersSchema: {
|
|
24
31
|
type: 'string',
|
|
25
|
-
description: 'Stringified JSON schema of the arguments the tool accepts.
|
|
32
|
+
description: 'Stringified JSON schema of the arguments the tool accepts.'
|
|
26
33
|
},
|
|
27
34
|
javascriptCode: {
|
|
28
35
|
type: 'string',
|
|
29
|
-
description: '
|
|
36
|
+
description: 'Body of async execute(args, context), or a complete module exporting execute.'
|
|
30
37
|
},
|
|
38
|
+
capabilities: {
|
|
39
|
+
type: 'array',
|
|
40
|
+
items: { type: 'string', enum: ['workspace:read', 'workspace:write', 'network', 'subprocess'] },
|
|
41
|
+
description: 'Explicit host capabilities requested by the plugin subprocess.'
|
|
42
|
+
}
|
|
31
43
|
},
|
|
32
|
-
required: ['toolName', 'toolDescription', 'parametersSchema', 'javascriptCode']
|
|
44
|
+
required: ['toolName', 'toolDescription', 'parametersSchema', 'javascriptCode']
|
|
33
45
|
}
|
|
34
46
|
},
|
|
35
|
-
execute: async (args, context) => {
|
|
36
|
-
try {
|
|
37
|
-
const { toolName, toolDescription, parametersSchema, javascriptCode } = args;
|
|
38
|
-
|
|
39
|
-
// Validate name
|
|
40
|
-
if (!/^[a-zA-Z0-9_]+$/.test(toolName)) {
|
|
41
|
-
return { success: false, error: 'Invalid toolName. Use alphanumeric characters only.' };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Ethical Safety Gate Layer 4: Block malicious tool names and descriptions
|
|
45
|
-
const BANNED_TOOL_NAMES = [
|
|
46
|
-
/keylog/i, /phish/i, /exploit/i, /malware/i, /trojan/i,
|
|
47
|
-
/ransomware/i, /rootkit/i, /backdoor/i, /ddos/i,
|
|
48
|
-
/credential.?harvest/i, /password.?steal/i, /brute.?force/i,
|
|
49
|
-
/crypto.?mine/i, /wallet.?drain/i, /spyware/i, /surveil/i
|
|
50
|
-
];
|
|
51
|
-
|
|
52
|
-
for (const pattern of BANNED_TOOL_NAMES) {
|
|
53
|
-
if (pattern.test(toolName) || pattern.test(toolDescription)) {
|
|
54
|
-
return {
|
|
55
|
-
success: false,
|
|
56
|
-
error: 'Safety violation: Tool name or description indicates malicious intent. Causyne exists to make the world better.'
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const SUBPROCESS_MODULE = "child_process";
|
|
63
|
-
const BANNED_TOOL_PATTERNS = [
|
|
64
|
-
/process\.kill\(/,
|
|
65
|
-
new RegExp(SUBPROCESS_MODULE),
|
|
66
|
-
new RegExp('require\\s*\\(\\s*[\'"]' + SUBPROCESS_MODULE + '[\'"]\\s*\\)'),
|
|
67
|
-
/fs\.(rm|rmdir)Sync.*recursive/i,
|
|
68
|
-
/fs\.writeFileSync\s*\(\s*['"]\/etc/,
|
|
69
|
-
/fs\.writeFileSync\s*\(\s*['"]C:\\\\Windows/i,
|
|
70
|
-
/stratum\+tcp/i,
|
|
71
|
-
/cryptonight|ethash|randomx/i,
|
|
72
|
-
/iohook|keypress|keylog/i,
|
|
73
|
-
/getUserMedia|MediaRecorder.*audio|MediaRecorder.*video/i,
|
|
74
|
-
/sqlmap|nikto|metasploit/i
|
|
75
|
-
];
|
|
76
|
-
|
|
77
|
-
for (const pattern of BANNED_TOOL_PATTERNS) {
|
|
78
|
-
if (pattern.test(javascriptCode)) {
|
|
79
|
-
return {
|
|
80
|
-
success: false,
|
|
81
|
-
error: 'Safety violation: Tool code contains a banned pattern. Causyne cannot forge tools designed for harm.'
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
47
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
48
|
+
async execute(args) {
|
|
49
|
+
const toolName = normaliseToolName(args.toolName);
|
|
50
|
+
if (!toolName) return { success: false, error: 'toolName is invalid' };
|
|
90
51
|
|
|
91
|
-
|
|
52
|
+
let parameters;
|
|
53
|
+
try {
|
|
54
|
+
parameters = JSON.parse(args.parametersSchema);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return { success: false, error: `parametersSchema is invalid JSON: ${error.message}` };
|
|
57
|
+
}
|
|
92
58
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
59
|
+
const result = createPlugin({
|
|
60
|
+
name: pluginNameFor(toolName),
|
|
61
|
+
description: `Custom Causyne plugin for ${toolName}`,
|
|
62
|
+
toolName,
|
|
63
|
+
toolDescription: args.toolDescription,
|
|
64
|
+
parameters,
|
|
65
|
+
code: args.javascriptCode,
|
|
66
|
+
capabilities: args.capabilities || []
|
|
67
|
+
});
|
|
97
68
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const llmFunction = ${javascriptCode.trim().startsWith('async') ? javascriptCode : `async (args, context) => { ${javascriptCode} }`};
|
|
105
|
-
return await llmFunction(args, context);
|
|
106
|
-
// LLM Generated Code End
|
|
69
|
+
if (!result.ok) return { success: false, error: result.error };
|
|
70
|
+
return {
|
|
71
|
+
success: true,
|
|
72
|
+
...result,
|
|
73
|
+
message: `${result.message} Use plugin_manager with action "enable" and name "${result.name}" after review.`
|
|
74
|
+
};
|
|
107
75
|
}
|
|
108
76
|
};
|
|
109
|
-
`;
|
|
110
|
-
|
|
111
|
-
fs.writeFileSync(filePath, fileContent.trim());
|
|
112
|
-
|
|
113
|
-
return {
|
|
114
|
-
success: true,
|
|
115
|
-
message: `Successfully forged new tool '${toolName}'. It is saved to ${filePath}. It will be available on the next loop iteration.`
|
|
116
|
-
};
|
|
117
|
-
} catch (error) {
|
|
118
|
-
return {
|
|
119
|
-
success: false,
|
|
120
|
-
error: `Failed to forge tool: ${error.message}`
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
},
|
|
124
|
-
};
|