@causyne/agent 1.10.12 → 1.10.17

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.
Files changed (68) hide show
  1. package/LICENSE +1 -1
  2. package/docs/ARCHITECTURE.md +136 -7
  3. package/docs/SECURITY_REVIEW.md +13 -1
  4. package/docs/THIRD_PARTY_NOTICES.md +43 -10
  5. package/package.json +11 -4
  6. package/src/agentLoop.js +161 -93
  7. package/src/agentProfiles.js +135 -0
  8. package/src/agentProfiles.ts +143 -0
  9. package/src/approvalGrants.js +17 -35
  10. package/src/approvalGrants.ts +54 -0
  11. package/src/auditLog.d.ts +3 -0
  12. package/src/benchmarks/planCache.js +159 -0
  13. package/src/browser/camofoxRuntime.js +93 -0
  14. package/src/browser/camofoxRuntime.ts +112 -0
  15. package/src/browser/causyneBrowser.js +495 -0
  16. package/src/browser/causyneBrowser.ts +594 -0
  17. package/src/browser/searchProviders.js +156 -0
  18. package/src/browser/searchProviders.ts +156 -0
  19. package/src/browser/snapshotWindow.js +28 -0
  20. package/src/browser/snapshotWindow.ts +40 -0
  21. package/src/browser/urlPolicy.js +89 -0
  22. package/src/browser/urlPolicy.ts +89 -0
  23. package/src/checkpoints.js +19 -6
  24. package/src/commands.js +120 -8
  25. package/src/config.d.ts +2 -0
  26. package/src/cortex/autoCondenser.js +1 -1
  27. package/src/hooks.js +150 -0
  28. package/src/hooks.ts +193 -0
  29. package/src/mcp/mcpClient.js +520 -132
  30. package/src/mcp/mcpClient.ts +584 -0
  31. package/src/mcp/mcpRegistry.js +399 -185
  32. package/src/mcp/mcpRegistry.ts +451 -0
  33. package/src/planCache.js +529 -225
  34. package/src/planCache.ts +840 -0
  35. package/src/plugins/pluginManager.js +460 -0
  36. package/src/plugins/pluginRunner.js +50 -0
  37. package/src/plugins/pluginSkills.js +84 -0
  38. package/src/plugins/pluginSkills.ts +106 -0
  39. package/src/provisioning/capabilityProvisioner.d.ts +10 -0
  40. package/src/provisioning/capabilityProvisioner.js +5 -0
  41. package/src/provisioning/capabilityTools.js +2 -1
  42. package/src/sandbox.js +53 -2
  43. package/src/sessionJournal.js +471 -0
  44. package/src/sessionJournal.ts +496 -0
  45. package/src/sessionStore.js +3 -279
  46. package/src/skillsManager.js +18 -0
  47. package/src/subagentJournal.js +215 -0
  48. package/src/subagentJournal.ts +254 -0
  49. package/src/taskShape.js +179 -198
  50. package/src/taskShape.ts +259 -0
  51. package/src/toolPacks.js +6 -3
  52. package/src/tools/browserAutomator.js +45 -241
  53. package/src/tools/browserAutomator.ts +57 -0
  54. package/src/tools/deferredMcpSearch.js +2 -2
  55. package/src/tools/manageLocalAiDependencies.js +1 -1
  56. package/src/tools/pluginManager.js +61 -0
  57. package/src/tools/registry.js +47 -53
  58. package/src/tools/subagentOrchestrator.js +73 -12
  59. package/src/tools/toolForge.js +47 -95
  60. package/src/tools/webFetch.js +183 -68
  61. package/src/tools/webFetch.ts +185 -0
  62. package/src/tools/webSearch.js +50 -75
  63. package/src/tools/webSearch.ts +56 -0
  64. package/src/types/contracts.js +8 -0
  65. package/src/types/contracts.ts +263 -0
  66. package/src/utils/paths.d.ts +3 -0
  67. package/src/version.d.ts +1 -0
  68. package/src/mcp/jsonRpc.js +0 -165
@@ -55,7 +55,7 @@ export const manageLocalDependencies = {
55
55
  type: 'object',
56
56
  properties: {
57
57
  action: { type: 'string', enum: ['setup_venv', 'pipInst', 'check_and_setupEng', 'grant_capability', 'list_capabilities', 'grant_mcp_server', 'list_mcp_servers'] },
58
- group: { type: 'string', description: 'Capability group for grant_capability: vision (browser + screen capture), ocr (read text from images), system (process and load inspection), desktop (clipboard, open, recycle bin), archive (zip and unzip).' },
58
+ group: { type: 'string', description: 'Capability group for grant_capability: browser (dedicated Camoufox web runtime), vision (screen capture), ocr (read text from images), system (process and load inspection), desktop (clipboard, open, recycle bin), archive (zip and unzip).' },
59
59
  engine: { type: 'string', enum: ['godot', 'blender'], description: 'Engine to locate when action is check_and_setupEng' },
60
60
  simulateMissing: { type: 'boolean', description: 'Simulate missing binary to verify sovereign fallback cascade' },
61
61
  packages: { type: 'array', items: { type: 'string' }, description: 'Exact PyPI pins only, for example pillow==11.3.0. URLs, paths, VCS references and pip options are rejected.' }
@@ -0,0 +1,61 @@
1
+ import {
2
+ createPlugin,
3
+ disablePlugin,
4
+ enablePlugin,
5
+ listPlugins
6
+ } from '../plugins/pluginManager.js';
7
+
8
+ export const pluginManagerTool = {
9
+ schema: {
10
+ name: 'plugin.manager',
11
+ description: 'Create and manage local Causyne plugins. Plugins package custom tools and run only after explicit enablement. Use list to inspect plugins; create writes a disabled plugin; enable trusts the current manifest digest; disable revokes it.',
12
+ parameters: {
13
+ type: 'object',
14
+ properties: {
15
+ action: { type: 'string', enum: ['list', 'create', 'enable', 'disable'] },
16
+ name: { type: 'string', description: 'Lowercase plugin name using letters, digits, and hyphens.' },
17
+ description: { type: 'string' },
18
+ toolName: { type: 'string' },
19
+ toolDescription: { type: 'string' },
20
+ parametersSchema: { type: 'string', description: 'JSON object schema for the custom tool arguments.' },
21
+ javascriptCode: { type: 'string', description: 'Body of async execute(args, context), or a complete module exporting execute.' },
22
+ capabilities: {
23
+ type: 'array',
24
+ items: { type: 'string', enum: ['workspace:read', 'workspace:write', 'network', 'subprocess'] }
25
+ }
26
+ },
27
+ required: ['action']
28
+ }
29
+ },
30
+
31
+ async execute(args) {
32
+ if (args.action === 'list') return { success: true, plugins: listPlugins() };
33
+ if (args.action === 'enable') {
34
+ const result = enablePlugin(args.name);
35
+ return result.ok ? { success: true, ...result } : { success: false, error: result.error };
36
+ }
37
+ if (args.action === 'disable') {
38
+ const result = disablePlugin(args.name);
39
+ return result.ok ? { success: true, ...result } : { success: false, error: result.error };
40
+ }
41
+ if (args.action === 'create') {
42
+ let parameters;
43
+ try {
44
+ parameters = JSON.parse(args.parametersSchema || '{}');
45
+ } catch (error) {
46
+ return { success: false, error: `parametersSchema is invalid JSON: ${error.message}` };
47
+ }
48
+ const result = createPlugin({
49
+ name: args.name,
50
+ description: args.description,
51
+ toolName: args.toolName,
52
+ toolDescription: args.toolDescription,
53
+ parameters,
54
+ code: args.javascriptCode,
55
+ capabilities: args.capabilities || []
56
+ });
57
+ return result.ok ? { success: true, ...result } : { success: false, error: result.error };
58
+ }
59
+ return { success: false, error: `Unknown plugin action: ${args.action}` };
60
+ }
61
+ };
@@ -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,4 +1,10 @@
1
1
  import { TokenTracker } from '../tokenTracker.js';
2
+ import { listAgentProfiles, loadAgentProfile } from '../agentProfiles.js';
3
+ import {
4
+ completeSubagentJournal,
5
+ failSubagentJournal,
6
+ startSubagentJournal
7
+ } from '../subagentJournal.js';
2
8
 
3
9
  // Subagent delegation.
4
10
  //
@@ -67,6 +73,8 @@ async function executeSubagent(record, parentLoop) {
67
73
  const child = new AgentLoop(transport, record.subagentId, record.prompt);
68
74
 
69
75
  // A delegated task is a bounded errand, not a second full session.
76
+ child.maxStepsCeiling = record.maxSteps;
77
+ child.taskTokenLimit = record.maxTokens;
70
78
  child.maxSteps = Math.min(child.maxSteps || 25, record.maxSteps);
71
79
  child.maxCircuitBreaker = child.maxSteps;
72
80
  child.messages = [];
@@ -78,8 +86,16 @@ async function executeSubagent(record, parentLoop) {
78
86
 
79
87
  record.loop = child;
80
88
  record.startedAtMs = Date.now();
89
+ record.journalStarted = startSubagentJournal(record);
81
90
 
82
- await child.run();
91
+ try {
92
+ await child.run();
93
+ } catch (error) {
94
+ if (record.journalStarted) {
95
+ failSubagentJournal(record.subagentId, error instanceof Error ? error.message : String(error));
96
+ }
97
+ throw error;
98
+ }
83
99
 
84
100
  const finalAssistant = [...child.messages].reverse()
85
101
  .find(m => m.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
@@ -88,7 +104,7 @@ async function executeSubagent(record, parentLoop) {
88
104
  try { return JSON.parse(m.content).success === false; } catch (e) { return false; }
89
105
  });
90
106
 
91
- return {
107
+ const result = {
92
108
  subagentId: record.subagentId,
93
109
  name: record.name,
94
110
  stepsCompleted: child.stepsCompleted,
@@ -100,6 +116,8 @@ async function executeSubagent(record, parentLoop) {
100
116
  summary: finalAssistant ? finalAssistant.content : null,
101
117
  stoppedEarly: child.stepsCompleted >= child.maxSteps
102
118
  };
119
+ if (record.journalStarted) completeSubagentJournal(record.subagentId, child.messages, result);
120
+ return result;
103
121
  }
104
122
 
105
123
  export const subagentOrchestrator = {
@@ -111,15 +129,16 @@ export const subagentOrchestrator = {
111
129
  properties: {
112
130
  action: {
113
131
  type: 'string',
114
- enum: ['spawn', 'list', 'kill', 'delegate_and_wait'],
115
- description: 'spawn (start in the background and return immediately), list (show active subagents), kill (stop one), delegate_and_wait (run it and return the result).'
132
+ enum: ['spawn', 'list', 'list_profiles', 'kill', 'delegate_and_wait'],
133
+ description: 'spawn (start in the background), list (show active subagents), list_profiles (show static user-defined agent profiles), kill (stop one), or delegate_and_wait (run and return the result).'
116
134
  },
117
135
  name: { type: 'string', description: 'A short name for the subagent, e.g. Audit_Worker.' },
118
136
  prompt: { type: 'string', description: 'The complete instruction for the subagent. It has no memory of this conversation, so it must be self-contained.' },
137
+ profile: { type: 'string', description: 'Optional profile name from ~/.causyne/agents/<name>.json. Its tool and budget limits cannot be widened by the call.' },
119
138
  subagentId: { type: 'string', description: 'Target subagent for the kill action.' },
120
139
  toolScope: { type: 'array', items: { type: 'string' }, description: 'Catalogue tool names the subagent may use. Defaults to read-only inspection tools. It can never receive the delegation tool itself.' },
121
- maxSteps: { type: 'integer', description: 'Step ceiling for the subagent. Defaults to 25.' },
122
- maxTokens: { type: 'integer', description: 'Advisory token budget for this subagent. Defaults to 50000.' },
140
+ maxSteps: { type: 'integer', description: 'Hard step ceiling for the subagent. Defaults to 25.' },
141
+ maxTokens: { type: 'integer', description: 'Hard per-task token ceiling for the subagent. Defaults to 50000.' },
123
142
  depth: { type: 'integer', description: 'Hierarchy depth. 0 = master, 1 = worker. Subagents cannot spawn subagents.' }
124
143
  },
125
144
  required: ['action']
@@ -153,6 +172,23 @@ export const subagentOrchestrator = {
153
172
  };
154
173
  }
155
174
 
175
+ if (action === 'list_profiles') {
176
+ const { profiles, problems } = listAgentProfiles();
177
+ return {
178
+ success: true,
179
+ output: JSON.stringify({
180
+ profiles: profiles.map(profile => ({
181
+ name: profile.name,
182
+ description: profile.description,
183
+ toolScope: profile.toolScope,
184
+ maxSteps: profile.maxSteps,
185
+ maxTokens: profile.maxTokens
186
+ })),
187
+ problems
188
+ }, null, 2)
189
+ };
190
+ }
191
+
156
192
  if (action === 'kill') {
157
193
  const id = args.subagentId || args.name;
158
194
  if (!id) return { success: false, error: 'subagentId or name is required to stop a subagent.' };
@@ -173,7 +209,17 @@ export const subagentOrchestrator = {
173
209
  return { success: false, error: `Unknown action: ${action}` };
174
210
  }
175
211
 
176
- const prompt = args.prompt;
212
+ let profile = null;
213
+ if (args.profile !== undefined) {
214
+ const loaded = loadAgentProfile(args.profile);
215
+ if (!loaded.ok) return { success: false, error: `Agent profile ${JSON.stringify(args.profile)}: ${loaded.error}` };
216
+ profile = loaded.profile;
217
+ }
218
+
219
+ const requestedPrompt = args.prompt;
220
+ const prompt = profile
221
+ ? `${profile.instructions}\n\nDelegated task:\n${String(requestedPrompt || '')}`
222
+ : requestedPrompt;
177
223
  if (!prompt) return { success: false, error: 'A prompt is required to delegate a task.' };
178
224
 
179
225
  const depth = args.depth !== undefined ? args.depth : 1;
@@ -195,11 +241,19 @@ export const subagentOrchestrator = {
195
241
  };
196
242
  }
197
243
 
198
- const name = args.name || `Worker_${Math.random().toString(36).slice(2, 7)}`;
244
+ const name = args.name || profile?.name || `Worker_${Math.random().toString(36).slice(2, 7)}`;
199
245
  const subagentId = `sub_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
200
- const requestedScope = Array.isArray(args.toolScope) && args.toolScope.length > 0
246
+ let requestedScope = Array.isArray(args.toolScope) && args.toolScope.length > 0
201
247
  ? args.toolScope
202
- : DEFAULT_TOOL_SCOPE;
248
+ : profile?.toolScope || DEFAULT_TOOL_SCOPE;
249
+ if (profile) {
250
+ const ceiling = new Set(profile.toolScope);
251
+ requestedScope = requestedScope.filter(tool => ceiling.has(String(tool).replace(/\./g, '_')));
252
+ if (requestedScope.length === 0) {
253
+ return { success: false, error: `Agent profile ${profile.name} permits none of the requested tools.` };
254
+ }
255
+ }
256
+ requestedScope = [...new Set(requestedScope.map(tool => String(tool).replace(/\./g, '_')))];
203
257
 
204
258
  // Reject an unknown tool name instead of silently accepting it. The old
205
259
  // implementation accepted anything, which is easy to do when the scope is
@@ -211,14 +265,21 @@ export const subagentOrchestrator = {
211
265
  return { success: false, error: `Unknown tools in toolScope: ${unknown.join(', ')}.` };
212
266
  }
213
267
 
268
+ const requestedMaxSteps = Number.isInteger(args.maxSteps)
269
+ ? Math.max(1, Math.min(args.maxSteps, 100))
270
+ : profile?.maxSteps || 25;
271
+ const requestedMaxTokens = Number.isInteger(args.maxTokens)
272
+ ? Math.max(1_000, Math.min(args.maxTokens, 200_000))
273
+ : profile?.maxTokens || 50_000;
214
274
  const record = {
215
275
  subagentId,
216
276
  name,
217
277
  prompt,
218
278
  toolScope: requestedScope,
219
279
  depth,
220
- maxSteps: args.maxSteps || 25,
221
- maxTokens: args.maxTokens || 50000,
280
+ maxSteps: Math.min(requestedMaxSteps, profile?.maxSteps || 100),
281
+ maxTokens: Math.min(requestedMaxTokens, profile?.maxTokens || 200_000),
282
+ profile: profile?.name || null,
222
283
  status: 'running',
223
284
  startTime: new Date().toISOString(),
224
285
  summary: null,
@@ -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
- };