@causyne/agent 1.10.12 → 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.
@@ -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 { causyneHome } from '../utils/paths.js';
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
- const forged = await loadForgedTools();
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, ...forged, ...mcp];
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, {
@@ -1,124 +1,76 @@
1
- import fs from 'fs';
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
- const CUSTOM_TOOLS_DIR = path.join(causyneHome(), 'custom_tools');
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: 'Self-Evolution Engine: Write a new custom Javascript tool that will be permanently added to your capabilities. Use this when you lack a specific tool to accomplish a goal. The tool will be hot-reloaded instantly.',
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: 'CamelCase name of the tool (e.g. "imageResizer", "pdfExtractor"). Must be alphanumeric without spaces.',
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, so you know how to use it later.',
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. E.g. {"type":"object","properties":{"filePath":{"type":"string"}},"required":["filePath"]}',
32
+ description: 'Stringified JSON schema of the arguments the tool accepts.'
26
33
  },
27
34
  javascriptCode: {
28
35
  type: 'string',
29
- description: 'The actual Node.js code block for the execute function. MUST be an async function accepting (args, context). Must return an object like { success: true, data: ... }. DO NOT use `export` or `const toolName =`, just provide the raw function body or `async (args, context) => { ... }`.',
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
- // Ensure directory exists
87
- if (!fs.existsSync(CUSTOM_TOOLS_DIR)) {
88
- fs.mkdirSync(CUSTOM_TOOLS_DIR, { recursive: true, mode: 0o700 });
89
- }
48
+ async execute(args) {
49
+ const toolName = normaliseToolName(args.toolName);
50
+ if (!toolName) return { success: false, error: 'toolName is invalid' };
90
51
 
91
- const filePath = path.join(CUSTOM_TOOLS_DIR, `${toolName}.js`);
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
- // Construct the ES Module file
94
- const fileContent = `
95
- // AUTOGENERATED CUSTOM TOOL: ${toolName}
96
- // Generated at: ${new Date().toISOString()}
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
- export const ${toolName}Tool = {
99
- name: '${toolName}',
100
- description: ${JSON.stringify(toolDescription)},
101
- parameters: ${parametersSchema},
102
- execute: async (args, context) => {
103
- // LLM Generated Code Start
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
- };