@causyne/agent 1.10.13 → 1.10.18
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/docs/ARCHITECTURE.md +118 -17
- package/docs/SECURITY_REVIEW.md +13 -1
- package/docs/THIRD_PARTY_NOTICES.md +46 -10
- package/package.json +10 -4
- package/src/agentLoop.js +123 -21
- package/src/agentProfiles.js +135 -0
- package/src/agentProfiles.ts +143 -0
- package/src/approvalGrants.js +17 -35
- package/src/approvalGrants.ts +54 -0
- package/src/auditLog.d.ts +3 -0
- package/src/browser/browserRuntime.js +88 -0
- package/src/browser/browserRuntime.ts +107 -0
- package/src/browser/causyneBrowser.js +495 -0
- package/src/browser/causyneBrowser.ts +594 -0
- package/src/browser/searchProviders.js +156 -0
- package/src/browser/searchProviders.ts +156 -0
- package/src/browser/snapshotWindow.js +28 -0
- package/src/browser/snapshotWindow.ts +40 -0
- package/src/browser/urlPolicy.js +89 -0
- package/src/browser/urlPolicy.ts +89 -0
- package/src/checkpoints.js +19 -6
- package/src/cliMode.js +13 -1
- package/src/commands.js +37 -8
- package/src/config.d.ts +2 -0
- package/src/decisionPolicy.js +68 -0
- package/src/decisionPolicy.ts +106 -0
- package/src/hooks.js +121 -107
- package/src/hooks.ts +193 -0
- package/src/implementationPlan.js +50 -0
- package/src/implementationPlan.ts +59 -0
- package/src/mcp/mcpClient.js +520 -132
- package/src/mcp/mcpClient.ts +584 -0
- package/src/mcp/mcpRegistry.js +399 -185
- package/src/mcp/mcpRegistry.ts +451 -0
- package/src/planCache.js +473 -450
- package/src/planCache.ts +840 -0
- package/src/plugins/pluginManager.js +40 -5
- package/src/plugins/pluginSkills.js +84 -0
- package/src/plugins/pluginSkills.ts +106 -0
- package/src/provisioning/capabilityProvisioner.d.ts +10 -0
- package/src/provisioning/capabilityProvisioner.js +5 -0
- package/src/provisioning/capabilityTools.js +2 -1
- package/src/sandbox.js +32 -2
- package/src/sessionJournal.js +471 -0
- package/src/sessionJournal.ts +496 -0
- package/src/sessionStore.js +3 -279
- package/src/skillsManager.js +18 -0
- package/src/subagentJournal.js +215 -0
- package/src/subagentJournal.ts +254 -0
- package/src/taskShape.js +179 -198
- package/src/taskShape.ts +259 -0
- package/src/toolPacks.js +7 -5
- package/src/tools/browserAutomator.js +45 -241
- package/src/tools/browserAutomator.ts +57 -0
- package/src/tools/manageLocalAiDependencies.js +1 -1
- package/src/tools/registry.js +4 -3
- package/src/tools/requestDecision.js +87 -0
- package/src/tools/requestDecision.ts +107 -0
- package/src/tools/subagentOrchestrator.js +73 -12
- package/src/tools/updatePlan.js +37 -84
- package/src/tools/updatePlan.ts +48 -0
- package/src/tools/webFetch.js +183 -68
- package/src/tools/webFetch.ts +185 -0
- package/src/tools/webSearch.js +50 -75
- package/src/tools/webSearch.ts +56 -0
- package/src/tui/ask.js +41 -1
- package/src/tui/chatRenderer.js +28 -0
- package/src/tui/dialogs.js +91 -0
- package/src/tui/prompt.js +32 -3
- package/src/tui/toolDisplay.js +0 -1
- package/src/types/contracts.js +8 -0
- package/src/types/contracts.ts +263 -0
- package/src/utils/paths.d.ts +3 -0
- package/src/version.d.ts +1 -0
- package/src/mcp/jsonRpc.js +0 -165
- package/src/tools/todoWrite.js +0 -75
package/src/commands.js
CHANGED
|
@@ -20,7 +20,10 @@ import {
|
|
|
20
20
|
} from './userSkills.js';
|
|
21
21
|
import { buildContextText, buildUsageText, buildCheckpointsText, buildUndoText, buildStatusText } from './reporting.js';
|
|
22
22
|
import { loadConfig, saveConfig } from './config.js';
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
serverStatus, grantServer, revokeServer, addServer, removeServer,
|
|
25
|
+
connectServer, disconnectServer, refreshServer
|
|
26
|
+
} from './mcp/mcpRegistry.js';
|
|
24
27
|
import { analyzeFiles, formatFindings } from './slopDetector.js';
|
|
25
28
|
import { suggestMovements, formatMovementBrief, DESIGN_MOVEMENTS } from './designPalettes.js';
|
|
26
29
|
import fs from 'fs';
|
|
@@ -657,10 +660,10 @@ export const COMMANDS = [
|
|
|
657
660
|
{
|
|
658
661
|
name: 'mcp',
|
|
659
662
|
scope: 'local',
|
|
660
|
-
usage: '/mcp [add <name> <cmd…> | add <name> --url <url> | remove|grant|revoke <name>]',
|
|
661
|
-
description: 'Manage
|
|
663
|
+
usage: '/mcp [add <name> <cmd…> | add <name> --url <url> | remove|grant|revoke|connect|disconnect|test|refresh <name>]',
|
|
664
|
+
description: 'Manage and diagnose MCP server connections',
|
|
662
665
|
dialogTitle: 'MCP servers',
|
|
663
|
-
run: (argv) => {
|
|
666
|
+
run: async (argv) => {
|
|
664
667
|
const [action, serverName, ...rest] = argv;
|
|
665
668
|
|
|
666
669
|
if (action === 'add') {
|
|
@@ -702,15 +705,40 @@ export const COMMANDS = [
|
|
|
702
705
|
const known = serverStatus().some(s => s.name === serverName);
|
|
703
706
|
if (!known) return fail(`No MCP server named "${serverName}" is configured.`);
|
|
704
707
|
if (action === 'grant') {
|
|
705
|
-
grantServer(serverName);
|
|
708
|
+
const granted = grantServer(serverName);
|
|
709
|
+
if (!granted.ok) return fail(granted.error);
|
|
706
710
|
return ok(`Granted "${serverName}". Its tools enter the catalogue on the next task.`);
|
|
707
711
|
}
|
|
708
712
|
revokeServer(serverName);
|
|
709
713
|
return ok(`Revoked "${serverName}". Its tools are no longer callable.`);
|
|
710
714
|
}
|
|
711
715
|
|
|
716
|
+
if (['connect', 'test', 'refresh', 'disconnect'].includes(action)) {
|
|
717
|
+
if (!serverName) return fail(`Usage: /mcp ${action} <server>`);
|
|
718
|
+
const known = serverStatus().some(server => server.name === serverName);
|
|
719
|
+
if (!known) return fail(`No MCP server named "${serverName}" is configured.`);
|
|
720
|
+
|
|
721
|
+
if (action === 'disconnect') {
|
|
722
|
+
await disconnectServer(serverName);
|
|
723
|
+
return ok(`Disconnected "${serverName}". Its grant remains in place.`);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (action === 'refresh') {
|
|
727
|
+
const tools = await refreshServer(serverName);
|
|
728
|
+
return ok(`Refreshed "${serverName}": ${tools.length} tool(s) available${tools.length ? ` — ${tools.map(tool => tool.name).join(', ')}` : ''}.`);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const client = await connectServer(serverName);
|
|
732
|
+
return ok(
|
|
733
|
+
`${action === 'test' ? 'MCP test passed' : 'Connected'}: "${serverName}"\n`
|
|
734
|
+
+ `Transport: ${client.status.transport || 'unknown'}\n`
|
|
735
|
+
+ `Server: ${client.serverInfo ? `${client.serverInfo.name} ${client.serverInfo.version}` : 'identity not provided'}\n`
|
|
736
|
+
+ `Tools: ${client.tools.length}${client.tools.length ? ` — ${client.tools.map(tool => tool.name).join(', ')}` : ''}`
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
|
|
712
740
|
if (action) {
|
|
713
|
-
return fail(`Unknown mcp action "${action}". Try: add, remove, grant, revoke, or mcp on its own to list.`);
|
|
741
|
+
return fail(`Unknown mcp action "${action}". Try: add, remove, grant, revoke, connect, disconnect, test, refresh, or mcp on its own to list.`);
|
|
714
742
|
}
|
|
715
743
|
|
|
716
744
|
const servers = serverStatus();
|
|
@@ -719,12 +747,13 @@ export const COMMANDS = [
|
|
|
719
747
|
}
|
|
720
748
|
const lines = ['MCP servers', ''];
|
|
721
749
|
for (const server of servers) {
|
|
722
|
-
lines.push(` ${server.name} [${server.transport}] ${server.granted ? 'granted' : 'NOT granted'}${server.
|
|
750
|
+
lines.push(` ${server.name} [${server.transport}] ${server.granted ? 'granted' : 'NOT granted'} ${server.status}`);
|
|
723
751
|
lines.push(` ${server.target}`);
|
|
724
752
|
if (server.tools.length > 0) lines.push(` tools: ${server.tools.join(', ')}`);
|
|
753
|
+
if (server.error) lines.push(` error: ${server.error}`);
|
|
725
754
|
}
|
|
726
755
|
lines.push('');
|
|
727
|
-
lines.push(' mcp grant <name> to enable, mcp remove <name> to delete.');
|
|
756
|
+
lines.push(' mcp grant <name> to enable, mcp test <name> to diagnose, mcp remove <name> to delete.');
|
|
728
757
|
return ok(lines.join('\n'), { effect: 'pick-mcp' });
|
|
729
758
|
}
|
|
730
759
|
},
|
package/src/config.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const ARCHITECTURE_TERMS = [
|
|
2
|
+
'architecture', 'arsitektur',
|
|
3
|
+
'framework', 'runtime', 'database', 'storage', 'authentication', 'authorization',
|
|
4
|
+
'deployment', 'hosting', 'protocol', 'plugin system', 'sandbox', 'license',
|
|
5
|
+
'typescript', 'javascript', 'python', 'rust', 'kotlin', 'swift'
|
|
6
|
+
];
|
|
7
|
+
const CHANGE_TERMS = [
|
|
8
|
+
'change', 'replace', 'switch', 'choose', 'decide', 'redesign', 'restructure',
|
|
9
|
+
'rewrite', 'rebuild', 'migration', 'migrate',
|
|
10
|
+
'ubah', 'ganti', 'pilih', 'tentukan', 'rombak', 'beralih', 'migrasi'
|
|
11
|
+
];
|
|
12
|
+
const EXPLICIT_CHOICE_PATTERNS = [
|
|
13
|
+
/\b(?:i|we)\s+(?:choose|chose|selected?|decided? (?:on|to use))\s+[a-z0-9@._+-]+/i,
|
|
14
|
+
/\b(?:must use|will use|switch to|rewrite (?:to|in)|migrate to)\s+[a-z0-9@._+-]+/i,
|
|
15
|
+
/\b(?:saya pilih|aku pilih|kita pilih|harus memakai|beralih ke|ubah ke|ganti ke)\s+[a-z0-9@._+-]+/i,
|
|
16
|
+
/^(?:please\s+)?(?:use|choose|gunakan|pilih)\s+[a-z0-9@._+-]+/i,
|
|
17
|
+
/\b(?:do not|don't|jangan)\s+(?:use|gunakan|pakai)\s+[a-z0-9@._+-]+/i
|
|
18
|
+
];
|
|
19
|
+
const MUTATING_TOOLS = new Set([
|
|
20
|
+
'fs_write', 'fs_edit', 'MultiEdit', 'fs_delete', 'fs_move', 'sysRun', 'sysRunBg',
|
|
21
|
+
'gitOp', 'manage_local_dependencies', 'plugin_manage', 'tool_forge', 'create_skill'
|
|
22
|
+
]);
|
|
23
|
+
function containsAny(text, terms) {
|
|
24
|
+
return terms.some(term => text.includes(term));
|
|
25
|
+
}
|
|
26
|
+
export function assessDecisionRequirement(prompt) {
|
|
27
|
+
const raw = String(prompt ?? '').trim();
|
|
28
|
+
const text = raw.toLowerCase();
|
|
29
|
+
const architectural = containsAny(text, ARCHITECTURE_TERMS);
|
|
30
|
+
const change = containsAny(text, CHANGE_TERMS);
|
|
31
|
+
const alternatives = /\b(?:or|versus|vs\.?|atau|dibanding(?:kan)? dengan)\b/i.test(raw);
|
|
32
|
+
const asksForChoice = /\b(?:which|what should|should we|mana|apakah sebaiknya|bagaimana sebaiknya)\b/i.test(raw);
|
|
33
|
+
const delegatesBroadRedesign = /\b(?:redesign|restructure|rombak)\s+(?:the\s+)?(?:architecture|system|runtime|database|arsitektur|sistem)\b/i.test(raw);
|
|
34
|
+
const explicit = EXPLICIT_CHOICE_PATTERNS.some(pattern => pattern.test(raw));
|
|
35
|
+
if (!architectural || explicit || (!alternatives && !asksForChoice && !(change && delegatesBroadRedesign))) {
|
|
36
|
+
return { required: false, kind: 'architecture', reason: '' };
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
required: true,
|
|
40
|
+
kind: 'architecture',
|
|
41
|
+
reason: 'The request reaches a durable architecture boundary with more than one plausible implementation.'
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function requiresImplementationPlan(prompt, archetype = '', confidence = '') {
|
|
45
|
+
const text = String(prompt ?? '').toLowerCase();
|
|
46
|
+
if (archetype === 'build_app' || archetype === 'integrate')
|
|
47
|
+
return true;
|
|
48
|
+
if (confidence === 'high' && /\b(?:implement|build|create|migrate|rewrite|refactor|integrate|buat|bangun|ubah)\b/i.test(text)) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
return containsAny(text, ARCHITECTURE_TERMS) && containsAny(text, CHANGE_TERMS);
|
|
52
|
+
}
|
|
53
|
+
export function isMutatingToolCall(toolName, args = {}) {
|
|
54
|
+
if (MUTATING_TOOLS.has(toolName)) {
|
|
55
|
+
if (toolName === 'sysRun' || toolName === 'sysRunBg') {
|
|
56
|
+
const command = String(args.command ?? '');
|
|
57
|
+
return !/^\s*(?:pwd|ls|find|rg|grep|cat|sed|head|tail|git\s+(?:status|diff|log|show)|npm\s+(?:view|audit|test)|node\s+--check)\b/i.test(command);
|
|
58
|
+
}
|
|
59
|
+
if (toolName === 'gitOp') {
|
|
60
|
+
const action = String(args.action ?? args.command ?? args.operation ?? '');
|
|
61
|
+
return !/^(?:status|diff|log|show|branch|remote)$/i.test(action.trim());
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
if (toolName.startsWith('plugin__') || toolName.startsWith('mcp__'))
|
|
66
|
+
return false;
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export type DecisionKind = 'architecture' | 'product' | 'data' | 'security' | 'workflow' | 'other';
|
|
2
|
+
|
|
3
|
+
export interface DecisionOption {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
description: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface DecisionRequest {
|
|
10
|
+
id: string;
|
|
11
|
+
kind: DecisionKind;
|
|
12
|
+
header: string;
|
|
13
|
+
question: string;
|
|
14
|
+
reason: string;
|
|
15
|
+
options: DecisionOption[];
|
|
16
|
+
allowCustom: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DecisionAnswer {
|
|
20
|
+
requestId: string;
|
|
21
|
+
optionId: string | null;
|
|
22
|
+
label: string;
|
|
23
|
+
custom: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface DecisionRequirement {
|
|
27
|
+
required: boolean;
|
|
28
|
+
kind: DecisionKind;
|
|
29
|
+
reason: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const ARCHITECTURE_TERMS = [
|
|
33
|
+
'architecture', 'arsitektur',
|
|
34
|
+
'framework', 'runtime', 'database', 'storage', 'authentication', 'authorization',
|
|
35
|
+
'deployment', 'hosting', 'protocol', 'plugin system', 'sandbox', 'license',
|
|
36
|
+
'typescript', 'javascript', 'python', 'rust', 'kotlin', 'swift'
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const CHANGE_TERMS = [
|
|
40
|
+
'change', 'replace', 'switch', 'choose', 'decide', 'redesign', 'restructure',
|
|
41
|
+
'rewrite', 'rebuild', 'migration', 'migrate',
|
|
42
|
+
'ubah', 'ganti', 'pilih', 'tentukan', 'rombak', 'beralih', 'migrasi'
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const EXPLICIT_CHOICE_PATTERNS = [
|
|
46
|
+
/\b(?:i|we)\s+(?:choose|chose|selected?|decided? (?:on|to use))\s+[a-z0-9@._+-]+/i,
|
|
47
|
+
/\b(?:must use|will use|switch to|rewrite (?:to|in)|migrate to)\s+[a-z0-9@._+-]+/i,
|
|
48
|
+
/\b(?:saya pilih|aku pilih|kita pilih|harus memakai|beralih ke|ubah ke|ganti ke)\s+[a-z0-9@._+-]+/i,
|
|
49
|
+
/^(?:please\s+)?(?:use|choose|gunakan|pilih)\s+[a-z0-9@._+-]+/i,
|
|
50
|
+
/\b(?:do not|don't|jangan)\s+(?:use|gunakan|pakai)\s+[a-z0-9@._+-]+/i
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const MUTATING_TOOLS = new Set([
|
|
54
|
+
'fs_write', 'fs_edit', 'MultiEdit', 'fs_delete', 'fs_move', 'sysRun', 'sysRunBg',
|
|
55
|
+
'gitOp', 'manage_local_dependencies', 'plugin_manage', 'tool_forge', 'create_skill'
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
function containsAny(text: string, terms: readonly string[]): boolean {
|
|
59
|
+
return terms.some(term => text.includes(term));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function assessDecisionRequirement(prompt: string): DecisionRequirement {
|
|
63
|
+
const raw = String(prompt ?? '').trim();
|
|
64
|
+
const text = raw.toLowerCase();
|
|
65
|
+
const architectural = containsAny(text, ARCHITECTURE_TERMS);
|
|
66
|
+
const change = containsAny(text, CHANGE_TERMS);
|
|
67
|
+
const alternatives = /\b(?:or|versus|vs\.?|atau|dibanding(?:kan)? dengan)\b/i.test(raw);
|
|
68
|
+
const asksForChoice = /\b(?:which|what should|should we|mana|apakah sebaiknya|bagaimana sebaiknya)\b/i.test(raw);
|
|
69
|
+
const delegatesBroadRedesign = /\b(?:redesign|restructure|rombak)\s+(?:the\s+)?(?:architecture|system|runtime|database|arsitektur|sistem)\b/i.test(raw);
|
|
70
|
+
const explicit = EXPLICIT_CHOICE_PATTERNS.some(pattern => pattern.test(raw));
|
|
71
|
+
|
|
72
|
+
if (!architectural || explicit || (!alternatives && !asksForChoice && !(change && delegatesBroadRedesign))) {
|
|
73
|
+
return { required: false, kind: 'architecture', reason: '' };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
required: true,
|
|
78
|
+
kind: 'architecture',
|
|
79
|
+
reason: 'The request reaches a durable architecture boundary with more than one plausible implementation.'
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function requiresImplementationPlan(prompt: string, archetype = '', confidence = ''): boolean {
|
|
84
|
+
const text = String(prompt ?? '').toLowerCase();
|
|
85
|
+
if (archetype === 'build_app' || archetype === 'integrate') return true;
|
|
86
|
+
if (confidence === 'high' && /\b(?:implement|build|create|migrate|rewrite|refactor|integrate|buat|bangun|ubah)\b/i.test(text)) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
return containsAny(text, ARCHITECTURE_TERMS) && containsAny(text, CHANGE_TERMS);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function isMutatingToolCall(toolName: string, args: Record<string, unknown> = {}): boolean {
|
|
93
|
+
if (MUTATING_TOOLS.has(toolName)) {
|
|
94
|
+
if (toolName === 'sysRun' || toolName === 'sysRunBg') {
|
|
95
|
+
const command = String(args.command ?? '');
|
|
96
|
+
return !/^\s*(?:pwd|ls|find|rg|grep|cat|sed|head|tail|git\s+(?:status|diff|log|show)|npm\s+(?:view|audit|test)|node\s+--check)\b/i.test(command);
|
|
97
|
+
}
|
|
98
|
+
if (toolName === 'gitOp') {
|
|
99
|
+
const action = String(args.action ?? args.command ?? args.operation ?? '');
|
|
100
|
+
return !/^(?:status|diff|log|show|branch|remote)$/i.test(action.trim());
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
if (toolName.startsWith('plugin__') || toolName.startsWith('mcp__')) return false;
|
|
105
|
+
return false;
|
|
106
|
+
}
|
package/src/hooks.js
CHANGED
|
@@ -1,95 +1,108 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
|
|
4
3
|
const MAX_HOOKS_PER_EVENT = 32;
|
|
5
4
|
const MAX_OUTPUT_BYTES = 32 * 1024;
|
|
6
5
|
const DEFAULT_TIMEOUT_MS = 5_000;
|
|
7
|
-
|
|
8
6
|
function safeObject(value) {
|
|
9
|
-
|
|
7
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
8
|
+
? value
|
|
9
|
+
: {};
|
|
10
10
|
}
|
|
11
|
-
|
|
12
11
|
function matchesTool(rule, toolName) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
const tools = Array.isArray(rule.tools)
|
|
13
|
+
? rule.tools
|
|
14
|
+
: [rule.tool == null ? '*' : rule.tool];
|
|
15
|
+
return tools.some((candidate) => candidate === '*' || candidate === toolName);
|
|
17
16
|
}
|
|
18
|
-
|
|
19
17
|
function normalizeRule(rule, event, toolName) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
18
|
+
if (rule === null || typeof rule !== 'object' || Array.isArray(rule))
|
|
19
|
+
return null;
|
|
20
|
+
const candidate = rule;
|
|
21
|
+
if (candidate.enabled === false || candidate.event !== event || !matchesTool(candidate, toolName))
|
|
22
|
+
return null;
|
|
23
|
+
if (!Array.isArray(candidate.command) || candidate.command.length === 0)
|
|
24
|
+
return null;
|
|
25
|
+
const command = candidate.command.map((part) => String(part));
|
|
26
|
+
const executable = command[0];
|
|
27
|
+
if (!executable || command.length > 64 || command.some((part) => part.length > 4096))
|
|
28
|
+
return null;
|
|
29
|
+
return {
|
|
30
|
+
name: String(candidate.name || `${event}:${toolName}`).slice(0, 128),
|
|
31
|
+
command: [executable, ...command.slice(1)],
|
|
32
|
+
timeoutMs: Number.isInteger(candidate.timeoutMs)
|
|
33
|
+
? Math.max(100, Math.min(candidate.timeoutMs, 10_000))
|
|
34
|
+
: DEFAULT_TIMEOUT_MS
|
|
35
|
+
};
|
|
31
36
|
}
|
|
32
|
-
|
|
33
37
|
export function hasMatchingHook(hooks, event, toolName) {
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
return (Array.isArray(hooks) ? hooks : [])
|
|
39
|
+
.some((rule) => normalizeRule(rule, event, toolName) !== null);
|
|
36
40
|
}
|
|
37
|
-
|
|
38
41
|
function runOneHook(rule, payload, cwd) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
const child = spawn(rule.command[0], rule.command.slice(1), {
|
|
44
|
+
cwd,
|
|
45
|
+
shell: false,
|
|
46
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
47
|
+
env: {
|
|
48
|
+
PATH: process.env.PATH || '',
|
|
49
|
+
LANG: process.env.LANG || 'C.UTF-8',
|
|
50
|
+
TMPDIR: process.env.TMPDIR || ''
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
let stdout = '';
|
|
54
|
+
let stderr = '';
|
|
55
|
+
let settled = false;
|
|
56
|
+
const finish = (value) => {
|
|
57
|
+
if (settled)
|
|
58
|
+
return;
|
|
59
|
+
settled = true;
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
resolve(value);
|
|
62
|
+
};
|
|
63
|
+
const timer = setTimeout(() => {
|
|
64
|
+
child.kill('SIGTERM');
|
|
65
|
+
finish({ exitCode: null, timedOut: true, stdout, stderr });
|
|
66
|
+
}, rule.timeoutMs);
|
|
67
|
+
timer.unref();
|
|
68
|
+
const collect = (target, chunk) => {
|
|
69
|
+
const next = target + chunk.toString();
|
|
70
|
+
if (Buffer.byteLength(next, 'utf8') > MAX_OUTPUT_BYTES) {
|
|
71
|
+
child.kill('SIGTERM');
|
|
72
|
+
finish({ exitCode: null, overflow: true, stdout, stderr });
|
|
73
|
+
return target;
|
|
74
|
+
}
|
|
75
|
+
return next;
|
|
76
|
+
};
|
|
77
|
+
child.stdout.on('data', (chunk) => { stdout = collect(stdout, chunk); });
|
|
78
|
+
child.stderr.on('data', (chunk) => { stderr = collect(stderr, chunk); });
|
|
79
|
+
child.on('error', (error) => finish({ exitCode: null, spawnError: error.message, stdout, stderr }));
|
|
80
|
+
child.on('close', (code) => finish({ exitCode: code, stdout, stderr }));
|
|
81
|
+
child.stdin.end(JSON.stringify(payload));
|
|
49
82
|
});
|
|
50
|
-
let stdout = '';
|
|
51
|
-
let stderr = '';
|
|
52
|
-
let settled = false;
|
|
53
|
-
|
|
54
|
-
const finish = (value) => {
|
|
55
|
-
if (settled) return;
|
|
56
|
-
settled = true;
|
|
57
|
-
clearTimeout(timer);
|
|
58
|
-
resolve(value);
|
|
59
|
-
};
|
|
60
|
-
const timer = setTimeout(() => {
|
|
61
|
-
child.kill('SIGTERM');
|
|
62
|
-
finish({ exitCode: null, timedOut: true, stdout, stderr });
|
|
63
|
-
}, rule.timeoutMs);
|
|
64
|
-
if (timer.unref) timer.unref();
|
|
65
|
-
|
|
66
|
-
const collect = (target, chunk) => {
|
|
67
|
-
const next = target + chunk;
|
|
68
|
-
if (Buffer.byteLength(next, 'utf8') > MAX_OUTPUT_BYTES) {
|
|
69
|
-
child.kill('SIGTERM');
|
|
70
|
-
finish({ exitCode: null, overflow: true, stdout, stderr });
|
|
71
|
-
return target;
|
|
72
|
-
}
|
|
73
|
-
return next;
|
|
74
|
-
};
|
|
75
|
-
child.stdout.on('data', (chunk) => { stdout = collect(stdout, chunk); });
|
|
76
|
-
child.stderr.on('data', (chunk) => { stderr = collect(stderr, chunk); });
|
|
77
|
-
child.on('error', (error) => finish({ exitCode: null, spawnError: error.message, stdout, stderr }));
|
|
78
|
-
child.on('close', (code) => finish({ exitCode: code, stdout, stderr }));
|
|
79
|
-
child.stdin.end(JSON.stringify(payload));
|
|
80
|
-
});
|
|
81
83
|
}
|
|
82
|
-
|
|
83
84
|
function feedbackFor(rule, result) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
85
|
+
const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
|
|
86
|
+
if (output)
|
|
87
|
+
return `${rule.name}: ${output}`.slice(0, MAX_OUTPUT_BYTES);
|
|
88
|
+
if (result.timedOut)
|
|
89
|
+
return `${rule.name}: timed out after ${rule.timeoutMs} ms`;
|
|
90
|
+
if (result.overflow)
|
|
91
|
+
return `${rule.name}: output exceeded ${MAX_OUTPUT_BYTES} bytes`;
|
|
92
|
+
if (result.spawnError)
|
|
93
|
+
return `${rule.name}: ${result.spawnError}`;
|
|
94
|
+
if (result.exitCode !== 0)
|
|
95
|
+
return `${rule.name}: exited with code ${result.exitCode}`;
|
|
96
|
+
return '';
|
|
97
|
+
}
|
|
98
|
+
function resolveWorkspaceRoots(workspacePaths) {
|
|
99
|
+
if (!Array.isArray(workspacePaths))
|
|
100
|
+
return [process.cwd()];
|
|
101
|
+
const roots = workspacePaths
|
|
102
|
+
.filter((value) => typeof value === 'string' && value.length > 0)
|
|
103
|
+
.map((value) => path.resolve(value));
|
|
104
|
+
return roots.length > 0 ? roots : [process.cwd()];
|
|
91
105
|
}
|
|
92
|
-
|
|
93
106
|
/**
|
|
94
107
|
* Run user-owned lifecycle hooks from ~/.causyne/config.json.
|
|
95
108
|
*
|
|
@@ -98,39 +111,40 @@ function feedbackFor(rule, result) {
|
|
|
98
111
|
* during startup before the user has trusted the workspace.
|
|
99
112
|
*/
|
|
100
113
|
export async function runConfiguredHooks({ hooks, event, toolName, args, result, taskId, toolCallId, workspacePaths }) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
114
|
+
const source = Array.isArray(hooks) ? hooks : [];
|
|
115
|
+
const rules = source
|
|
116
|
+
.map((rule) => normalizeRule(rule, event, toolName))
|
|
117
|
+
.filter((rule) => rule !== null)
|
|
118
|
+
.slice(0, MAX_HOOKS_PER_EVENT);
|
|
119
|
+
if (rules.length === 0)
|
|
120
|
+
return { denied: false, messages: [] };
|
|
121
|
+
const roots = resolveWorkspaceRoots(workspacePaths);
|
|
122
|
+
const primaryRoot = roots[0] ?? process.cwd();
|
|
123
|
+
const payload = {
|
|
124
|
+
event,
|
|
125
|
+
tool: toolName,
|
|
126
|
+
args: safeObject(args),
|
|
127
|
+
taskId: String(taskId || ''),
|
|
128
|
+
toolCallId: String(toolCallId || ''),
|
|
129
|
+
workspacePaths: roots
|
|
130
|
+
};
|
|
131
|
+
if (result !== null && typeof result === 'object')
|
|
132
|
+
payload.result = result;
|
|
133
|
+
const messages = [];
|
|
134
|
+
for (const rule of rules) {
|
|
135
|
+
const hookResult = await runOneHook(rule, payload, primaryRoot);
|
|
136
|
+
const feedback = feedbackFor(rule, hookResult);
|
|
137
|
+
if (feedback)
|
|
138
|
+
messages.push(feedback);
|
|
139
|
+
if (hookResult.exitCode === 2) {
|
|
140
|
+
return feedback
|
|
141
|
+
? { denied: true, messages, reason: feedback }
|
|
142
|
+
: { denied: true, messages };
|
|
143
|
+
}
|
|
144
|
+
if (event === 'PreToolUse' && hookResult.exitCode !== 0) {
|
|
145
|
+
return { denied: true, messages, reason: feedback || `${rule.name} failed closed` };
|
|
146
|
+
}
|
|
130
147
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return { denied: false, messages };
|
|
148
|
+
return { denied: false, messages };
|
|
134
149
|
}
|
|
135
|
-
|
|
136
150
|
export { MAX_HOOKS_PER_EVENT, MAX_OUTPUT_BYTES, DEFAULT_TIMEOUT_MS };
|