slaunt 0.1.17 → 0.1.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/README.md +17 -122
- package/bin/slaunt-hooks.js +96 -0
- package/package.json +9 -59
- package/release/SlauntKillBar.zip +0 -0
- package/release/manifest.json +7 -0
- package/scripts/killbar-portability-check.js +67 -0
- package/scripts/package-killbar-macos.sh +38 -0
- package/src/approval-manager.js +53 -0
- package/src/cli-auth.js +230 -0
- package/src/compact-wordmark.js +13 -0
- package/src/desktop/broker-daemon.js +18 -0
- package/src/desktop/broker-grants.js +37 -0
- package/src/desktop/broker-hook-command.js +23 -0
- package/src/desktop/broker-keychain.js +29 -0
- package/src/desktop/broker-launch-agent.js +34 -0
- package/src/desktop/broker-service.js +40 -0
- package/src/desktop/chat-capture-command.js +7 -0
- package/src/desktop/chat-hook-capture.js +445 -0
- package/src/desktop/claude-lifecycle.js +143 -0
- package/src/desktop/client-identity.js +19 -0
- package/src/desktop/native-claude-broker.js +34 -0
- package/src/desktop/native-claude-connection.js +42 -0
- package/src/desktop/tool-guard-command.js +13 -0
- package/src/desktop/tool-guard.js +225 -0
- package/src/discovery/catalog.js +734 -0
- package/src/full-screen.js +88 -0
- package/src/helper-release.js +27 -0
- package/src/hidden-oauth.js +70 -0
- package/src/independent-grants.js +38 -0
- package/src/install-flow.js +71 -0
- package/src/killbar-background-install.js +47 -0
- package/src/killbar-enrollment.js +74 -0
- package/src/killbar-launch-agent.js +10 -0
- package/src/mcp-identity.js +6 -0
- package/src/native-config.js +150 -0
- package/src/native-oauth.js +33 -0
- package/src/oauth-cohort.js +43 -0
- package/src/pty-wrapper.py +57 -0
- package/src/read-json.js +8 -0
- package/src/setup/client-installer.js +93 -0
- package/src/setup/codex-guard-supervisor.js +34 -0
- package/src/setup/hook-installer.js +496 -0
- package/src/setup/managed-policy.js +26 -0
- package/src/setup/mcp-config-installer.js +204 -0
- package/src/setup-flow.js +66 -0
- package/src/tui.js +205 -0
- package/test/approval-manager.test.js +27 -0
- package/test/broker-grants.test.js +14 -0
- package/test/broker-keychain.test.js +14 -0
- package/test/broker-launch-agent.test.js +15 -0
- package/test/broker-service.test.js +42 -0
- package/test/cli-auth-recovery.test.js +15 -0
- package/test/codex-guard-supervisor.test.js +25 -0
- package/test/full-screen.test.js +49 -0
- package/test/helper-release.test.js +22 -0
- package/test/hidden-oauth.test.js +44 -0
- package/test/hook-installer.test.js +232 -0
- package/test/independent-grants.test.js +16 -0
- package/test/install-flow.test.js +24 -0
- package/test/killbar-background-install.test.js +15 -0
- package/test/killbar-enrollment.test.js +115 -0
- package/test/killbar-launch-agent.test.js +12 -0
- package/test/killbar-portability.test.js +17 -0
- package/test/managed-policy.test.js +18 -0
- package/test/native-claude-broker.test.js +19 -0
- package/test/native-claude-connection.test.js +17 -0
- package/test/native-claude-hooks.test.js +9 -0
- package/test/native-config.test.js +3 -0
- package/test/native-integration.cjs +29 -0
- package/test/native-oauth.test.js +23 -0
- package/test/oauth-cohort.test.js +26 -0
- package/test/tui.test.js +100 -0
- package/LICENSE +0 -201
- package/dist/audit/risk-engine.d.ts +0 -4
- package/dist/audit/risk-engine.d.ts.map +0 -1
- package/dist/audit/risk-engine.js +0 -174
- package/dist/audit/risk-engine.js.map +0 -1
- package/dist/audit/run.d.ts +0 -18
- package/dist/audit/run.d.ts.map +0 -1
- package/dist/audit/run.js +0 -68
- package/dist/audit/run.js.map +0 -1
- package/dist/classification/builtin-catalog.d.ts +0 -3
- package/dist/classification/builtin-catalog.d.ts.map +0 -1
- package/dist/classification/builtin-catalog.js +0 -283
- package/dist/classification/builtin-catalog.js.map +0 -1
- package/dist/classification/catalog.d.ts +0 -10
- package/dist/classification/catalog.d.ts.map +0 -1
- package/dist/classification/catalog.js +0 -194
- package/dist/classification/catalog.js.map +0 -1
- package/dist/classification/classifier.d.ts +0 -3
- package/dist/classification/classifier.d.ts.map +0 -1
- package/dist/classification/classifier.js +0 -64
- package/dist/classification/classifier.js.map +0 -1
- package/dist/classification/public-catalog-config.d.ts +0 -3
- package/dist/classification/public-catalog-config.d.ts.map +0 -1
- package/dist/classification/public-catalog-config.js +0 -5
- package/dist/classification/public-catalog-config.js.map +0 -1
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -239
- package/dist/cli.js.map +0 -1
- package/dist/config/discover.d.ts +0 -8
- package/dist/config/discover.d.ts.map +0 -1
- package/dist/config/discover.js +0 -163
- package/dist/config/discover.js.map +0 -1
- package/dist/config/install.d.ts +0 -10
- package/dist/config/install.d.ts.map +0 -1
- package/dist/config/install.js +0 -143
- package/dist/config/install.js.map +0 -1
- package/dist/config/paths.d.ts +0 -24
- package/dist/config/paths.d.ts.map +0 -1
- package/dist/config/paths.js +0 -90
- package/dist/config/paths.js.map +0 -1
- package/dist/demo.d.ts +0 -4
- package/dist/demo.d.ts.map +0 -1
- package/dist/demo.js +0 -99
- package/dist/demo.js.map +0 -1
- package/dist/explanation/local.d.ts +0 -24
- package/dist/explanation/local.d.ts.map +0 -1
- package/dist/explanation/local.js +0 -696
- package/dist/explanation/local.js.map +0 -1
- package/dist/mcp/introspect.d.ts +0 -18
- package/dist/mcp/introspect.d.ts.map +0 -1
- package/dist/mcp/introspect.js +0 -236
- package/dist/mcp/introspect.js.map +0 -1
- package/dist/mcp/oauth.d.ts +0 -14
- package/dist/mcp/oauth.d.ts.map +0 -1
- package/dist/mcp/oauth.js +0 -257
- package/dist/mcp/oauth.js.map +0 -1
- package/dist/presentation/plain-language.d.ts +0 -12
- package/dist/presentation/plain-language.d.ts.map +0 -1
- package/dist/presentation/plain-language.js +0 -120
- package/dist/presentation/plain-language.js.map +0 -1
- package/dist/privacy/redaction.d.ts +0 -2
- package/dist/privacy/redaction.d.ts.map +0 -1
- package/dist/privacy/redaction.js +0 -50
- package/dist/privacy/redaction.js.map +0 -1
- package/dist/report/html.d.ts +0 -4
- package/dist/report/html.d.ts.map +0 -1
- package/dist/report/html.js +0 -101
- package/dist/report/html.js.map +0 -1
- package/dist/tui/analysis-path.d.ts +0 -17
- package/dist/tui/analysis-path.d.ts.map +0 -1
- package/dist/tui/analysis-path.js +0 -112
- package/dist/tui/analysis-path.js.map +0 -1
- package/dist/tui/render.d.ts +0 -4
- package/dist/tui/render.d.ts.map +0 -1
- package/dist/tui/render.js +0 -262
- package/dist/tui/render.js.map +0 -1
- package/dist/tui/splash.d.ts +0 -15
- package/dist/tui/splash.d.ts.map +0 -1
- package/dist/tui/splash.js +0 -130
- package/dist/tui/splash.js.map +0 -1
- package/dist/tui/stream.d.ts +0 -6
- package/dist/tui/stream.d.ts.map +0 -1
- package/dist/tui/stream.js +0 -56
- package/dist/tui/stream.js.map +0 -1
- package/dist/types.d.ts +0 -132
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/version.d.ts +0 -2
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js +0 -8
- package/dist/version.js.map +0 -1
- package/models/APACHE-2.0.txt +0 -202
- package/models/MODEL.json +0 -29
- package/models/NOTICE.md +0 -23
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { spawnSync } = require('node:child_process');
|
|
7
|
+
|
|
8
|
+
const { catalog } = require('../discovery/catalog');
|
|
9
|
+
|
|
10
|
+
const { SLAUNT_SERVER_NAME } = require('../mcp-identity');
|
|
11
|
+
const { DENIAL } = require('../desktop/tool-guard');
|
|
12
|
+
const GUARD_MARKER = ': slaunt-pretooluse-v1; ';
|
|
13
|
+
const GUARD_STATUS = 'Requesting Slaunt tool authorization';
|
|
14
|
+
const CAPTURE_MARKER = ': slaunt-chat-capture-v1; ';
|
|
15
|
+
const CAPTURE_STATUS = 'Capturing visible Slaunt messages';
|
|
16
|
+
const NATIVE_LIFECYCLE_MARKER = ': slaunt-native-lifecycle-v1; ';
|
|
17
|
+
const PREVIOUS_SERVER_NAMES = new Set([SLAUNT_SERVER_NAME, 'Slaunt', 'slaunt-2']);
|
|
18
|
+
const MANAGED_TOOL_NAMES = new Set(['aki_workspace_context', 'codex_hook_event', 'slaunt_hook_event', 'tool_authorize_codex']);
|
|
19
|
+
const MAX_CONFIG_BYTES = 2 * 1024 * 1024;
|
|
20
|
+
const HOOK_SETUP_TARGETS = Object.freeze({
|
|
21
|
+
'claude-code': 'claude-mcp-tool',
|
|
22
|
+
'codex-cli': 'codex-mcp-tool'
|
|
23
|
+
});
|
|
24
|
+
// Hook installation uses only documented, fixed per-client config paths. Do
|
|
25
|
+
// not search for arbitrary files or accept a target path from renderer input.
|
|
26
|
+
const CLIENT_HOOK_TARGETS = Object.freeze({
|
|
27
|
+
'codex-mcp-tool': Object.freeze({ directory: '.codex', file: 'hooks.json' }),
|
|
28
|
+
'claude-mcp-tool': Object.freeze({ directory: '.claude', file: 'settings.json' })
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
function makeHook(input, options = {}) {
|
|
32
|
+
const hook = {
|
|
33
|
+
type: 'mcp_tool',
|
|
34
|
+
server: SLAUNT_SERVER_NAME,
|
|
35
|
+
tool: options.tool || 'codex_hook_event',
|
|
36
|
+
input,
|
|
37
|
+
timeout: options.timeout || 15,
|
|
38
|
+
statusMessage: options.statusMessage
|
|
39
|
+
};
|
|
40
|
+
return hook;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function makeGuardHook(broker, clientId) {
|
|
44
|
+
const quote = value => `'${value.replace(/'/g, `'\\''`)}'`;
|
|
45
|
+
const fallback = `printf '%s\\n' ${quote(JSON.stringify(DENIAL))}`;
|
|
46
|
+
let command = fallback;
|
|
47
|
+
if (broker) {
|
|
48
|
+
if (!path.isAbsolute(broker.command || '') || !Array.isArray(broker.args)
|
|
49
|
+
|| broker.args.length > 4 || [broker.command, ...broker.args].some(value =>
|
|
50
|
+
typeof value !== 'string' || !value || value.length > 4096 || /[\x00-\x1f\x7f]/.test(value))
|
|
51
|
+
|| broker.args.filter(value => value === '--slaunt-mcp-stdio').length !== 1
|
|
52
|
+
|| broker.args.some(value => value.startsWith('--slaunt-client='))) throw new Error('invalid-guard-broker');
|
|
53
|
+
const script = path.resolve(__dirname, '../desktop/tool-guard-command.js');
|
|
54
|
+
command = `/usr/bin/env -u NODE_OPTIONS -u NODE_PATH ELECTRON_RUN_AS_NODE=1 ${[
|
|
55
|
+
broker.command, script, '--slaunt-pretooluse', `--slaunt-client=${clientId}`
|
|
56
|
+
].map(quote).join(' ')} 2>/dev/null || { ${fallback}; }`;
|
|
57
|
+
}
|
|
58
|
+
return { type: 'command', command: GUARD_MARKER + command, timeout: 20, statusMessage: GUARD_STATUS };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function makeCaptureHook(broker) {
|
|
62
|
+
// Reuse the fixed-executable validation; no transcript path or content is
|
|
63
|
+
// interpolated into a shell command. Native hook JSON arrives only on stdin.
|
|
64
|
+
makeGuardHook(broker, 'codex-cli');
|
|
65
|
+
const quote = value => `'${value.replace(/'/g, `'\\''`)}'`;
|
|
66
|
+
const command = broker
|
|
67
|
+
? `/usr/bin/env -u NODE_OPTIONS -u NODE_PATH ELECTRON_RUN_AS_NODE=1 ${[
|
|
68
|
+
broker.command, path.resolve(__dirname, '../desktop/chat-capture-command.js')
|
|
69
|
+
].map(quote).join(' ')}`
|
|
70
|
+
: `printf '%s\\n' 'Slaunt visible chat capture unavailable (capture-broker-unavailable).' >&2; printf '%s\\n' '{}'`;
|
|
71
|
+
return { type: 'command', command: CAPTURE_MARKER + command, timeout: 10, statusMessage: CAPTURE_STATUS };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function createSlauntHooks({ broker, captureVisibleMessages = false } = {}) {
|
|
75
|
+
const config = {
|
|
76
|
+
hooks: {
|
|
77
|
+
SessionStart: [{
|
|
78
|
+
matcher: '^(startup|resume|clear|compact)$',
|
|
79
|
+
hooks: [makeHook({
|
|
80
|
+
event_name: 'SessionStart',
|
|
81
|
+
session_id: '${session_id}',
|
|
82
|
+
model: '${model}'
|
|
83
|
+
}, {
|
|
84
|
+
timeout: 30,
|
|
85
|
+
statusMessage: 'Registering Slaunt session'
|
|
86
|
+
})]
|
|
87
|
+
}],
|
|
88
|
+
UserPromptSubmit: [{
|
|
89
|
+
hooks: [makeHook({
|
|
90
|
+
event_name: 'UserPromptSubmit',
|
|
91
|
+
session_id: '${session_id}',
|
|
92
|
+
turn_id: '${turn_id}',
|
|
93
|
+
model: '${model}',
|
|
94
|
+
permission_mode: '${permission_mode}'
|
|
95
|
+
}, { statusMessage: 'Refreshing Slaunt context and starting thread logging' })]
|
|
96
|
+
}],
|
|
97
|
+
PreToolUse: [{
|
|
98
|
+
matcher: '*',
|
|
99
|
+
hooks: [makeHook({event_name:'PreToolUse',session_id:'${session_id}',turn_id:'${turn_id}',tool_name:'${tool_name}',tool_use_id:'${tool_use_id}'}, {tool:'tool_authorize_codex',timeout:10,statusMessage:GUARD_STATUS})]
|
|
100
|
+
}],
|
|
101
|
+
PostToolUse: [{
|
|
102
|
+
matcher: '*',
|
|
103
|
+
hooks: [makeHook({
|
|
104
|
+
event_name: 'PostToolUse',
|
|
105
|
+
session_id: '${session_id}',
|
|
106
|
+
turn_id: '${turn_id}',
|
|
107
|
+
tool_name: '${tool_name}',
|
|
108
|
+
tool_use_id: '${tool_use_id}',
|
|
109
|
+
model: '${model}',
|
|
110
|
+
permission_mode: '${permission_mode}'
|
|
111
|
+
}, { timeout: 15, statusMessage: 'Logging tool completion' })]
|
|
112
|
+
}],
|
|
113
|
+
SubagentStart: [{
|
|
114
|
+
hooks: [makeHook({
|
|
115
|
+
event_name: 'SubagentStart',
|
|
116
|
+
session_id: '${session_id}',
|
|
117
|
+
turn_id: '${turn_id}',
|
|
118
|
+
agent_type: '${agent_type}',
|
|
119
|
+
agent_id: '${agent_id}'
|
|
120
|
+
}, { timeout: 15, statusMessage: 'Logging subagent start' })]
|
|
121
|
+
}],
|
|
122
|
+
SubagentStop: [{
|
|
123
|
+
hooks: [makeHook({
|
|
124
|
+
event_name: 'SubagentStop',
|
|
125
|
+
session_id: '${session_id}',
|
|
126
|
+
turn_id: '${turn_id}',
|
|
127
|
+
agent_type: '${agent_type}',
|
|
128
|
+
agent_id: '${agent_id}'
|
|
129
|
+
}, { timeout: 15, statusMessage: 'Logging subagent completion' })]
|
|
130
|
+
}],
|
|
131
|
+
PreCompact: [{
|
|
132
|
+
matcher: '^(manual|auto)$',
|
|
133
|
+
hooks: [makeHook({
|
|
134
|
+
event_name: 'PreCompact',
|
|
135
|
+
session_id: '${session_id}',
|
|
136
|
+
turn_id: '${turn_id}',
|
|
137
|
+
trigger: '${trigger}'
|
|
138
|
+
}, { timeout: 15, statusMessage: 'Logging compaction start' })]
|
|
139
|
+
}],
|
|
140
|
+
PostCompact: [{
|
|
141
|
+
matcher: '^(manual|auto)$',
|
|
142
|
+
hooks: [makeHook({
|
|
143
|
+
event_name: 'PostCompact',
|
|
144
|
+
session_id: '${session_id}',
|
|
145
|
+
turn_id: '${turn_id}',
|
|
146
|
+
trigger: '${trigger}'
|
|
147
|
+
}, { timeout: 15, statusMessage: 'Logging compaction completion' })]
|
|
148
|
+
}],
|
|
149
|
+
Stop: [{
|
|
150
|
+
hooks: [makeHook({
|
|
151
|
+
event_name: 'Stop',
|
|
152
|
+
session_id: '${session_id}',
|
|
153
|
+
turn_id: '${turn_id}'
|
|
154
|
+
}, { timeout: 15, statusMessage: 'Closing Slaunt run' })]
|
|
155
|
+
}],
|
|
156
|
+
Interrupt: [{
|
|
157
|
+
hooks: [makeHook({
|
|
158
|
+
event_name: 'Interrupt',
|
|
159
|
+
session_id: '${session_id}',
|
|
160
|
+
turn_id: '${turn_id}'
|
|
161
|
+
}, { timeout: 3, statusMessage: 'Closing interrupted Slaunt run' })]
|
|
162
|
+
}]
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
if (captureVisibleMessages) {
|
|
166
|
+
for (const event of ['UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'Stop', 'Interrupt']) {
|
|
167
|
+
config.hooks[event][0].hooks.push(makeCaptureHook(broker));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return config;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function createClaudeCodeHooks({ broker, nativeBroker = false } = {}) {
|
|
174
|
+
if(nativeBroker){
|
|
175
|
+
if(!broker)throw new Error('invalid-guard-broker');
|
|
176
|
+
const quote=value=>`'${value.replace(/'/g, `'\\''`)}'`;
|
|
177
|
+
const guard=makeGuardHook(broker,'claude-code');
|
|
178
|
+
const lifecycle=`/usr/bin/env -u NODE_OPTIONS -u NODE_PATH ${[broker.command,path.resolve(__dirname,'../desktop/broker-hook-command.js')].map(quote).join(' ')}`;
|
|
179
|
+
const eventHook=()=>({type:'command',command:NATIVE_LIFECYCLE_MARKER+lifecycle,timeout:20,onFailure:'block',statusMessage:'Connecting Slaunt run'});
|
|
180
|
+
return {hooks:{SessionStart:[{hooks:[eventHook()]}],UserPromptSubmit:[{hooks:[eventHook()]}],PreToolUse:[{matcher:'*',hooks:[{...guard,onFailure:'block'}]}],Stop:[{hooks:[eventHook()]}]}};
|
|
181
|
+
}
|
|
182
|
+
const hooks = createSlauntHooks({ broker, captureVisibleMessages: false }).hooks;
|
|
183
|
+
hooks.PreToolUse[0].hooks = [{ ...makeGuardHook(broker, 'claude-code'), onFailure: 'block' }];
|
|
184
|
+
delete hooks.Interrupt;
|
|
185
|
+
|
|
186
|
+
// Claude Code runs SessionStart before MCP servers are available on initial
|
|
187
|
+
// startup/resume. Only refresh after clear/compaction, when MCP is available.
|
|
188
|
+
hooks.SessionStart[0].matcher = '^(clear|compact)$';
|
|
189
|
+
|
|
190
|
+
hooks.UserPromptSubmit[0].hooks[0].input = {
|
|
191
|
+
event_name: 'UserPromptSubmit',
|
|
192
|
+
session_id: '${session_id}',
|
|
193
|
+
prompt_id: '${prompt_id}',
|
|
194
|
+
prompt: '${prompt}',
|
|
195
|
+
agent_id: '${agent_id}'
|
|
196
|
+
};
|
|
197
|
+
hooks.UserPromptSubmit[0].hooks[0].statusMessage = 'Saving Claude chat and refreshing Slaunt context';
|
|
198
|
+
hooks.PostToolUse[0].hooks[0].input = {
|
|
199
|
+
event_name: 'PostToolUse',
|
|
200
|
+
session_id: '${session_id}',
|
|
201
|
+
tool_name: '${tool_name}',
|
|
202
|
+
tool_use_id: '${tool_use_id}',
|
|
203
|
+
permission_mode: '${permission_mode}'
|
|
204
|
+
};
|
|
205
|
+
for (const eventName of ['SubagentStart', 'SubagentStop']) {
|
|
206
|
+
hooks[eventName][0].hooks[0].input = {
|
|
207
|
+
event_name: eventName,
|
|
208
|
+
session_id: '${session_id}',
|
|
209
|
+
agent_type: '${agent_type}',
|
|
210
|
+
agent_id: '${agent_id}'
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
for (const eventName of ['PreCompact', 'PostCompact']) {
|
|
214
|
+
hooks[eventName][0].hooks[0].input = {
|
|
215
|
+
event_name: eventName,
|
|
216
|
+
session_id: '${session_id}',
|
|
217
|
+
trigger: '${trigger}'
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
hooks.Stop[0].hooks[0].input = {
|
|
221
|
+
event_name: 'Stop',
|
|
222
|
+
session_id: '${session_id}',
|
|
223
|
+
prompt_id: '${prompt_id}',
|
|
224
|
+
last_assistant_message: '${last_assistant_message}'
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
for (const groups of Object.values(hooks)) {
|
|
228
|
+
for (const group of groups) for (const hook of group.hooks) {
|
|
229
|
+
if (hook.type === 'mcp_tool') hook.tool = 'slaunt_hook_event';
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
hooks.SessionStart[0].hooks[0].input = { event_name: 'SessionStart', session_id: '${session_id}' };
|
|
233
|
+
return { hooks };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function isManagedSlauntHook(hook) {
|
|
237
|
+
return Boolean(hook?.type === 'command' && typeof hook.command==='string' && hook.command.startsWith(NATIVE_LIFECYCLE_MARKER)) || Boolean(hook?.type === 'command' && hook.statusMessage === CAPTURE_STATUS
|
|
238
|
+
&& typeof hook.command === 'string' && hook.command.startsWith(CAPTURE_MARKER)) || Boolean(hook?.type === 'command' && hook.statusMessage === GUARD_STATUS
|
|
239
|
+
&& typeof hook.command === 'string' && hook.command.startsWith(GUARD_MARKER)) || Boolean(hook
|
|
240
|
+
&& hook.type === 'mcp_tool'
|
|
241
|
+
&& PREVIOUS_SERVER_NAMES.has(hook.server)
|
|
242
|
+
&& MANAGED_TOOL_NAMES.has(hook.tool));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function mergeSlauntHooks(existingConfig, hookConfig = createSlauntHooks()) {
|
|
246
|
+
if (!existingConfig || typeof existingConfig !== 'object' || Array.isArray(existingConfig)) {
|
|
247
|
+
throw new Error('invalid-config-root');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const merged = { ...existingConfig };
|
|
251
|
+
if (merged.hooks === undefined) merged.hooks = {};
|
|
252
|
+
if (!merged.hooks || typeof merged.hooks !== 'object' || Array.isArray(merged.hooks)) {
|
|
253
|
+
throw new Error('invalid-hooks-section');
|
|
254
|
+
}
|
|
255
|
+
merged.hooks = { ...merged.hooks };
|
|
256
|
+
|
|
257
|
+
const managed = hookConfig.hooks;
|
|
258
|
+
for (const [eventName, groups] of Object.entries(managed)) {
|
|
259
|
+
const existingGroups = merged.hooks[eventName] || [];
|
|
260
|
+
if (!Array.isArray(existingGroups)) throw new Error('invalid-event-hook-list');
|
|
261
|
+
|
|
262
|
+
const retainedGroups = existingGroups
|
|
263
|
+
.map((group) => {
|
|
264
|
+
if (!group || typeof group !== 'object' || Array.isArray(group)) return group;
|
|
265
|
+
if (!Array.isArray(group.hooks)) return group;
|
|
266
|
+
const retainedHooks = group.hooks.filter((hook) => !isManagedSlauntHook(hook)
|
|
267
|
+
// The collector owns final assistant capture for Codex. Keep existing
|
|
268
|
+
// prompt/tool capture hooks, but avoid a second Stop final event.
|
|
269
|
+
&& !(eventName === 'Stop' && groups.some(g => g.hooks.some(h => h.command?.startsWith(CAPTURE_MARKER)))
|
|
270
|
+
&& hook.type === 'mcp_tool' && PREVIOUS_SERVER_NAMES.has(hook.server)
|
|
271
|
+
&& hook.tool === 'chat_hook_event'));
|
|
272
|
+
if (retainedHooks.length === group.hooks.length) return group;
|
|
273
|
+
return retainedHooks.length > 0 ? { ...group, hooks: retainedHooks } : null;
|
|
274
|
+
})
|
|
275
|
+
.filter(Boolean);
|
|
276
|
+
|
|
277
|
+
merged.hooks[eventName] = [...retainedGroups, ...groups];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return merged;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function ensureConfigDirectory(homeDir, directoryName, fileSystem) {
|
|
284
|
+
if (!/^\.[a-z0-9-]+$/.test(directoryName)) throw new Error('unsafe-config-directory');
|
|
285
|
+
const configDirectory = path.join(homeDir, directoryName);
|
|
286
|
+
try {
|
|
287
|
+
const stats = fileSystem.lstatSync(configDirectory);
|
|
288
|
+
if (stats.isSymbolicLink() || !stats.isDirectory()) throw new Error('unsafe-config-directory');
|
|
289
|
+
} catch (error) {
|
|
290
|
+
if (error.code !== 'ENOENT') throw error;
|
|
291
|
+
fileSystem.mkdirSync(configDirectory, { mode: 0o700 });
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const stats = fileSystem.lstatSync(configDirectory);
|
|
295
|
+
if (stats.isSymbolicLink() || !stats.isDirectory()) throw new Error('unsafe-config-directory');
|
|
296
|
+
return configDirectory;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function readExistingConfig(configPath, fileSystem) {
|
|
300
|
+
let stats;
|
|
301
|
+
try {
|
|
302
|
+
stats = fileSystem.lstatSync(configPath);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (error.code === 'ENOENT') return {};
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (stats.isSymbolicLink() || !stats.isFile()) throw new Error('unsafe-config-file');
|
|
309
|
+
if (stats.size > MAX_CONFIG_BYTES) throw new Error('config-file-too-large');
|
|
310
|
+
try {
|
|
311
|
+
const parsed = JSON.parse(fileSystem.readFileSync(configPath, 'utf8'));
|
|
312
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('invalid-config-root');
|
|
313
|
+
return parsed;
|
|
314
|
+
} catch (error) {
|
|
315
|
+
if (error instanceof SyntaxError) throw new Error('invalid-config-json');
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function writeConfigAtomically(configPath, configDirectory, serialized, fileSystem) {
|
|
321
|
+
const temporaryPath = path.join(
|
|
322
|
+
configDirectory,
|
|
323
|
+
`.hooks.json.slaunt-${process.pid}-${crypto.randomBytes(8).toString('hex')}.tmp`
|
|
324
|
+
);
|
|
325
|
+
let fileDescriptor;
|
|
326
|
+
try {
|
|
327
|
+
fileDescriptor = fileSystem.openSync(temporaryPath, 'wx', 0o600);
|
|
328
|
+
fileSystem.writeFileSync(fileDescriptor, serialized, 'utf8');
|
|
329
|
+
fileSystem.fsyncSync(fileDescriptor);
|
|
330
|
+
fileSystem.closeSync(fileDescriptor);
|
|
331
|
+
fileDescriptor = undefined;
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
const current = fileSystem.lstatSync(configPath);
|
|
335
|
+
if (current.isSymbolicLink() || !current.isFile()) throw new Error('unsafe-config-file');
|
|
336
|
+
} catch (error) {
|
|
337
|
+
if (error.code !== 'ENOENT') throw error;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
fileSystem.renameSync(temporaryPath, configPath);
|
|
341
|
+
fileSystem.chmodSync(configPath, 0o600);
|
|
342
|
+
} finally {
|
|
343
|
+
if (fileDescriptor !== undefined) fileSystem.closeSync(fileDescriptor);
|
|
344
|
+
try {
|
|
345
|
+
fileSystem.unlinkSync(temporaryPath);
|
|
346
|
+
} catch (error) {
|
|
347
|
+
if (error.code !== 'ENOENT') throw error;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function installHooksFile(homeDir, directoryName, fileName, hookConfig, fileSystem = fs) {
|
|
353
|
+
const configDirectory = ensureConfigDirectory(homeDir, directoryName, fileSystem);
|
|
354
|
+
const configPath = path.join(configDirectory, fileName);
|
|
355
|
+
const existing = readExistingConfig(configPath, fileSystem);
|
|
356
|
+
const merged = mergeSlauntHooks(existing, hookConfig);
|
|
357
|
+
const serialized = `${JSON.stringify(merged, null, 2)}\n`;
|
|
358
|
+
const previousSerialized = `${JSON.stringify(existing, null, 2)}\n`;
|
|
359
|
+
|
|
360
|
+
if (serialized === previousSerialized) return { status: 'unchanged' };
|
|
361
|
+
writeConfigAtomically(configPath, configDirectory, serialized, fileSystem);
|
|
362
|
+
return { status: 'installed' };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function installCodexHooks(homeDir, fileSystem = fs, options = {}) {
|
|
366
|
+
const target = CLIENT_HOOK_TARGETS['codex-mcp-tool'];
|
|
367
|
+
return installHooksFile(homeDir, target.directory, target.file, createSlauntHooks(options), fileSystem);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function claudeSupportsBlocking(versionOutput) {
|
|
371
|
+
const match = /^\s*(\d+)\.(\d+)\.(\d+)(?:\s|$)/.exec(versionOutput || '');
|
|
372
|
+
if (!match) return false;
|
|
373
|
+
const parts=match.slice(1).map(Number), minimum=[2,1,295];
|
|
374
|
+
for(let i=0;i<3;i++){if(parts[i]>minimum[i])return true;if(parts[i]<minimum[i])return false;}
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function installClaudeCodeHooks(homeDir, fileSystem = fs, options = {}) {
|
|
379
|
+
const probe=(options.versionProbe || (()=>spawnSync('claude',['--version'],{encoding:'utf8',timeout:10000,stdio:['ignore','pipe','ignore']})))();
|
|
380
|
+
if (probe?.error || probe?.status !== 0 || !claudeSupportsBlocking(probe.stdout)) throw new Error('claude-version-unsupported');
|
|
381
|
+
const target = CLIENT_HOOK_TARGETS['claude-mcp-tool'];
|
|
382
|
+
const result = installHooksFile(homeDir, target.directory, target.file, createClaudeCodeHooks(options), fileSystem);
|
|
383
|
+
return {
|
|
384
|
+
status: 'partial',
|
|
385
|
+
reason: result.status === 'installed' || result.status === 'unchanged'
|
|
386
|
+
? 'claude-lifecycle-differences'
|
|
387
|
+
: null
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function safeClientResult(client, status, reason = null) {
|
|
392
|
+
return {
|
|
393
|
+
id: client.id,
|
|
394
|
+
surface: client.surface || 'unknown',
|
|
395
|
+
status,
|
|
396
|
+
reason
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function safeInstallErrorReason(error) {
|
|
401
|
+
const knownReasons = new Set([
|
|
402
|
+
'invalid-config-root',
|
|
403
|
+
'invalid-hooks-section',
|
|
404
|
+
'invalid-event-hook-list',
|
|
405
|
+
'invalid-config-json',
|
|
406
|
+
'unsafe-config-directory',
|
|
407
|
+
'unsafe-config-file',
|
|
408
|
+
'config-file-too-large'
|
|
409
|
+
,'claude-version-unsupported'
|
|
410
|
+
]);
|
|
411
|
+
if (knownReasons.has(error?.message)) return error.message;
|
|
412
|
+
if (error?.code === 'EACCES' || error?.code === 'EPERM') return 'permission-denied';
|
|
413
|
+
if (error?.code === 'ENOSPC') return 'insufficient-storage';
|
|
414
|
+
return 'config-write-failed';
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function installHooksForDetectedClients({ clients = [], selectedClientIds, homeDir, broker, nativeBroker = false, fileSystem = fs, versionProbe } = {}) {
|
|
418
|
+
const byId = new Map(catalog.map((entry) => [entry.id, entry]));
|
|
419
|
+
const selected = Array.isArray(selectedClientIds) ? new Set(selectedClientIds) : null;
|
|
420
|
+
const detectedById = new Map();
|
|
421
|
+
clients.filter((client) => {
|
|
422
|
+
const entry = byId.get(client?.id);
|
|
423
|
+
return entry
|
|
424
|
+
&& entry.localDetection
|
|
425
|
+
&& entry.displayInOnboarding
|
|
426
|
+
&& entry.nativeMcpClient
|
|
427
|
+
&& (!selected || selected.has(entry.id))
|
|
428
|
+
&& HOOK_SETUP_TARGETS[entry.id] === entry.hookAdapter;
|
|
429
|
+
}).forEach((client) => {
|
|
430
|
+
if (!detectedById.has(client.id)) detectedById.set(client.id, client);
|
|
431
|
+
});
|
|
432
|
+
const detectedClients = [...detectedById.values()];
|
|
433
|
+
const codexClients = detectedClients.filter((client) => byId.get(client.id).hookAdapter === 'codex-mcp-tool');
|
|
434
|
+
const claudeClients = detectedClients.filter((client) => byId.get(client.id).hookAdapter === 'claude-mcp-tool');
|
|
435
|
+
|
|
436
|
+
let codexInstallResult = null;
|
|
437
|
+
if (codexClients.length > 0) {
|
|
438
|
+
try {
|
|
439
|
+
codexInstallResult = installCodexHooks(homeDir, fileSystem, { broker });
|
|
440
|
+
} catch (error) {
|
|
441
|
+
codexInstallResult = {
|
|
442
|
+
status: 'error',
|
|
443
|
+
reason: safeInstallErrorReason(error)
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
let claudeInstallResult = null;
|
|
449
|
+
if (claudeClients.length > 0) {
|
|
450
|
+
try {
|
|
451
|
+
claudeInstallResult = installClaudeCodeHooks(homeDir, fileSystem, { broker, nativeBroker, versionProbe });
|
|
452
|
+
} catch (error) {
|
|
453
|
+
claudeInstallResult = {
|
|
454
|
+
status: 'error',
|
|
455
|
+
reason: safeInstallErrorReason(error)
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const results = detectedClients.map((client) => {
|
|
461
|
+
const entry = byId.get(client.id);
|
|
462
|
+
if (entry.hookAdapter === 'codex-mcp-tool') {
|
|
463
|
+
return safeClientResult(client, codexInstallResult.status, codexInstallResult.reason || null);
|
|
464
|
+
}
|
|
465
|
+
if (entry.hookAdapter === 'claude-mcp-tool') {
|
|
466
|
+
return safeClientResult(client, claudeInstallResult.status, claudeInstallResult.reason || null);
|
|
467
|
+
}
|
|
468
|
+
return null;
|
|
469
|
+
});
|
|
470
|
+
const targetResults = results.filter(Boolean);
|
|
471
|
+
|
|
472
|
+
return {
|
|
473
|
+
serverName: SLAUNT_SERVER_NAME,
|
|
474
|
+
results: targetResults,
|
|
475
|
+
summary: {
|
|
476
|
+
detected: targetResults.length,
|
|
477
|
+
installed: targetResults.filter((result) => result.status === 'installed').length,
|
|
478
|
+
unchanged: targetResults.filter((result) => result.status === 'unchanged').length,
|
|
479
|
+
partial: targetResults.filter((result) => result.status === 'partial').length,
|
|
480
|
+
unsupported: 0,
|
|
481
|
+
failed: targetResults.filter((result) => result.status === 'error').length
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
module.exports = {
|
|
487
|
+
SLAUNT_SERVER_NAME,
|
|
488
|
+
CLIENT_HOOK_TARGETS,
|
|
489
|
+
createSlauntHooks,
|
|
490
|
+
createClaudeCodeHooks,
|
|
491
|
+
claudeSupportsBlocking,
|
|
492
|
+
mergeSlauntHooks,
|
|
493
|
+
installCodexHooks,
|
|
494
|
+
installClaudeCodeHooks,
|
|
495
|
+
installHooksForDetectedClients
|
|
496
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { stringify } = require('smol-toml');
|
|
5
|
+
|
|
6
|
+
function createCodexManagedPolicy() {
|
|
7
|
+
return stringify({features:{hooks:true},hooks:{PreToolUse:[{matcher:'.*',hooks:[{type:'mcp_tool',server:'slaunt',tool:'tool_authorize_codex',input:{event_name:'PreToolUse',session_id:'${session_id}',turn_id:'${turn_id}',tool_name:'${tool_name}',tool_use_id:'${tool_use_id}'},timeout:10,statusMessage:'Requesting Slaunt tool authorization'}]}]}});
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Generate policy only; the administrator must deploy the guard executable in
|
|
11
|
+
// a root-owned directory and install the output through their management system.
|
|
12
|
+
// A managed hook cannot make Codex itself fail closed when the hook fails.
|
|
13
|
+
function createManagedPolicies({ guardPath, managedDir }) {
|
|
14
|
+
if (typeof guardPath !== 'string' || typeof managedDir !== 'string' ||
|
|
15
|
+
!path.posix.isAbsolute(guardPath) || !path.posix.isAbsolute(managedDir) ||
|
|
16
|
+
/[\x00-\x1f\x7f]/.test(guardPath + managedDir) ||
|
|
17
|
+
path.posix.normalize(guardPath) !== guardPath || path.posix.normalize(managedDir) !== managedDir ||
|
|
18
|
+
!guardPath.startsWith(managedDir.replace(/\/$/, '') + '/')) throw new Error('invalid-managed-guard-path');
|
|
19
|
+
const quote = value => `'${value.replace(/'/g, `'\\''`)}'`;
|
|
20
|
+
const command = quote(guardPath);
|
|
21
|
+
return {
|
|
22
|
+
codex: createCodexManagedPolicy(),
|
|
23
|
+
claude: {hooks:{PreToolUse:[{hooks:[{type:'command',command,onFailure:'block',timeout:20,statusMessage:'Requesting Slaunt tool authorization'}]}]}}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
module.exports = { createManagedPolicies, createCodexManagedPolicy };
|