@getmarrow/install 0.1.52 → 0.1.54
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 +26 -5
- package/bin/marrow-install.js +2 -1
- package/package.json +1 -1
- package/src/control-state.js +116 -0
- package/src/first-hour.js +71 -7
- package/src/governed-runner.js +23 -0
- package/src/installer.js +1113 -57
package/src/installer.js
CHANGED
|
@@ -5,12 +5,14 @@ const crypto = require('node:crypto');
|
|
|
5
5
|
const { version: INSTALLER_ADAPTER_VERSION } = require('../package.json');
|
|
6
6
|
const { controllerStatus, controllerSupportedPlatform, ensureGovernanceController } = require('./controller-manager');
|
|
7
7
|
const { firstCapturePath, harnessReloadPlan } = require('./first-hour');
|
|
8
|
+
const { evidence: localControlEvidence } = require('./control-state');
|
|
8
9
|
|
|
9
10
|
const DEFAULT_BASE_URL = 'https://api.getmarrow.ai';
|
|
10
11
|
const MARROW_BLOCK_START = '<!-- marrow:passive-start -->';
|
|
11
12
|
const MARROW_BLOCK_END = '<!-- marrow:passive-end -->';
|
|
12
|
-
const MCP_ADAPTER_VERSION = '3.9.
|
|
13
|
-
const MCP_ADAPTER_SOURCE_SHA = '
|
|
13
|
+
const MCP_ADAPTER_VERSION = '3.9.77';
|
|
14
|
+
const MCP_ADAPTER_SOURCE_SHA = '1e782d8ba6bbb54bfaa322f0300565c7176f1969';
|
|
15
|
+
const MCP_ADAPTER_INTEGRITY = 'sha512-4SVvnyTaDh/mhVUry1LaGCYKXSB/mt5NCHijIOLP336jILSqe7ezjZ5/EwEOMTQ2JvLm6w0QE/jmGbcksqe86w==';
|
|
14
16
|
const SDK_ADAPTER_VERSION = '3.7.62';
|
|
15
17
|
const SDK_ADAPTER_INTEGRITY = 'sha512-n1i6Be09TpAQ9BPNRKY7aCvA2iSUPpJfw8djw2MELwpNbBCtKiZ29Jji77BK/6EFLUpSIcTW/Gmdf/ccf0JRYQ==';
|
|
16
18
|
const SDK_ADAPTER_TARBALL = `https://registry.npmjs.org/@getmarrow/sdk/-/sdk-${SDK_ADAPTER_VERSION}.tgz`;
|
|
@@ -20,8 +22,8 @@ const ADAPTER_PROVENANCE = Object.freeze({
|
|
|
20
22
|
package: '@getmarrow/mcp',
|
|
21
23
|
version: MCP_ADAPTER_VERSION,
|
|
22
24
|
source_sha: MCP_ADAPTER_SOURCE_SHA,
|
|
23
|
-
integrity:
|
|
24
|
-
integrity_state: '
|
|
25
|
+
integrity: MCP_ADAPTER_INTEGRITY,
|
|
26
|
+
integrity_state: 'sealed_local_candidate',
|
|
25
27
|
}),
|
|
26
28
|
sdk: Object.freeze({
|
|
27
29
|
package: '@getmarrow/sdk',
|
|
@@ -33,21 +35,97 @@ const MCP_CONTEXT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mc
|
|
|
33
35
|
const MCP_PRE_ACTION_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp pre-action-hook`;
|
|
34
36
|
const MCP_ACTION_RESULT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp hook`;
|
|
35
37
|
const MCP_SESSION_END_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp session-hook`;
|
|
38
|
+
const CODEX_CONTEXT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp codex-context-hook`;
|
|
39
|
+
const CODEX_PRE_ACTION_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp codex-pre-action-hook`;
|
|
40
|
+
const CODEX_ACTION_RESULT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp codex-hook`;
|
|
41
|
+
const CODEX_SESSION_END_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp codex-session-hook`;
|
|
42
|
+
const CURSOR_PRE_ACTION_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp cursor-pre-action-hook`;
|
|
43
|
+
const CURSOR_ACTION_RESULT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp cursor-hook`;
|
|
44
|
+
const CURSOR_SESSION_END_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp cursor-session-hook`;
|
|
45
|
+
const CLINE_PRE_ACTION_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp cline-pre-action-hook`;
|
|
46
|
+
const CLINE_ACTION_RESULT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp cline-hook`;
|
|
47
|
+
const CLINE_SESSION_END_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp cline-session-hook`;
|
|
48
|
+
const WINDSURF_PRE_ACTION_ENTRYPOINT = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp windsurf-pre-action-hook`;
|
|
49
|
+
const WINDSURF_ACTION_RESULT_ENTRYPOINT = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp windsurf-hook`;
|
|
50
|
+
const WINDSURF_SESSION_END_ENTRYPOINT = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp windsurf-session-hook`;
|
|
51
|
+
const WINDSURF_LAUNCH_FAILURE = 'Marrow governance adapter was unavailable; this action is blocked.';
|
|
52
|
+
const WINDSURF_PRE_ACTION_HOOK_COMMAND = `sh -c 'stderr="$(${WINDSURF_PRE_ACTION_ENTRYPOINT} 2>&1 >/dev/null)"; status=$?; if [ "$status" -eq 0 ]; then exit 0; fi; if [ "$status" -eq 2 ]; then printf "%s\\n" "$stderr" >&2; exit 2; fi; printf "%s\\n" "${WINDSURF_LAUNCH_FAILURE}" >&2; exit 2'`;
|
|
53
|
+
const WINDSURF_ACTION_RESULT_HOOK_COMMAND = `sh -c '${WINDSURF_ACTION_RESULT_ENTRYPOINT} >/dev/null 2>&1 || :; exit 0'`;
|
|
54
|
+
const WINDSURF_SESSION_END_HOOK_COMMAND = `sh -c '${WINDSURF_SESSION_END_ENTRYPOINT} >/dev/null 2>&1 || :; exit 0'`;
|
|
55
|
+
const GEMINI_PRE_ACTION_ENTRYPOINT = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp gemini-pre-action-hook`;
|
|
56
|
+
const GEMINI_ACTION_RESULT_ENTRYPOINT = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp gemini-hook`;
|
|
57
|
+
const GEMINI_SESSION_END_ENTRYPOINT = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp gemini-session-hook`;
|
|
58
|
+
const GEMINI_FIXED_DENIAL = 'Marrow blocked this action because required governance approval or proof is unavailable.';
|
|
59
|
+
const GEMINI_LAUNCH_FAILURE = 'Marrow governance adapter was unavailable; this action is blocked.';
|
|
60
|
+
const GEMINI_PRE_ACTION_HOOK_COMMAND = `sh -c 'output="$(${GEMINI_PRE_ACTION_ENTRYPOINT} 2>/dev/null)" && { case "$output" in "{\\"decision\\":\\"allow\\"}"|"{\\"decision\\":\\"deny\\",\\"reason\\":\\"${GEMINI_FIXED_DENIAL}\\"}") printf "%s\\n" "$output"; exit 0 ;; esac; }; printf "%s\\n" "${GEMINI_LAUNCH_FAILURE}" >&2; exit 2'`;
|
|
61
|
+
const GEMINI_ACTION_RESULT_HOOK_COMMAND = `sh -c '${GEMINI_ACTION_RESULT_ENTRYPOINT} >/dev/null 2>&1 || :; printf "%s\\n" "{}"; exit 0'`;
|
|
62
|
+
const GEMINI_SESSION_END_HOOK_COMMAND = `sh -c '${GEMINI_SESSION_END_ENTRYPOINT} >/dev/null 2>&1 || :; printf "%s\\n" "{}"; exit 0'`;
|
|
63
|
+
const GROK_CONTEXT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp grok-context-hook`;
|
|
64
|
+
const GROK_ACTION_RESULT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp grok-hook`;
|
|
65
|
+
const GROK_SESSION_END_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp grok-session-hook`;
|
|
66
|
+
const GROK_FIXED_DENIAL = 'Marrow blocked this protected action.';
|
|
67
|
+
const GROK_LAUNCH_FAILURE = 'Marrow governance adapter was unavailable; this action is blocked.';
|
|
68
|
+
const GROK_PRE_ACTION_GUARD_SOURCE = [
|
|
69
|
+
'const {spawn}=require("node:child_process");',
|
|
70
|
+
`const valid=new Set([${JSON.stringify('{"decision":"allow"}')},${JSON.stringify(`{"decision":"deny","reason":"${GROK_FIXED_DENIAL}"}`)}]);`,
|
|
71
|
+
'let child=null,timer=null,done=false,input=[],inputBytes=0,output="",outputBytes=0;',
|
|
72
|
+
`const fail=()=>{if(done)return;done=true;if(timer)clearTimeout(timer);if(child&&!child.killed)child.kill("SIGKILL");process.stderr.write(${JSON.stringify(`${GROK_LAUNCH_FAILURE}\n`)});process.exitCode=2;process.stdin.destroy();};`,
|
|
73
|
+
'process.stdin.on("error",fail);',
|
|
74
|
+
'process.stdin.on("data",chunk=>{const value=Buffer.from(chunk);inputBytes+=value.length;if(inputBytes>65536){fail();return;}input.push(value);});',
|
|
75
|
+
'process.stdin.on("end",()=>{if(done)return;try{',
|
|
76
|
+
`child=spawn(process.platform==="win32"?"npx.cmd":"npx",${JSON.stringify(['-y', `--package=${MCP_PACKAGE_SPEC}`, 'marrow-mcp', 'grok-pre-action-hook'])},{stdio:["pipe","pipe","ignore"]});`,
|
|
77
|
+
'timer=setTimeout(fail,5000);',
|
|
78
|
+
'child.stdout.on("data",chunk=>{if(done)return;outputBytes+=chunk.length;if(outputBytes>512){fail();return;}output+=chunk.toString("utf8");});',
|
|
79
|
+
'child.on("error",fail);child.stdin.on("error",fail);',
|
|
80
|
+
'child.on("close",code=>{if(done)return;if(code!==0||!valid.has(output)){fail();return;}done=true;if(timer)clearTimeout(timer);process.stdout.write(output);});',
|
|
81
|
+
'child.stdin.end(Buffer.concat(input));}catch{fail();}});',
|
|
82
|
+
].join('');
|
|
83
|
+
const GROK_PRE_ACTION_HOOK_COMMAND = `node -e '${GROK_PRE_ACTION_GUARD_SOURCE}'`;
|
|
36
84
|
const NATIVE_HOOK_MATCHER = 'Bash|Edit|Write|MultiEdit|mcp__(?!marrow_).*';
|
|
85
|
+
const CODEX_NATIVE_HOOK_MATCHER = 'Bash|apply_patch|Edit|Write|MultiEdit|mcp__(?!marrow__marrow_).*|functions\\.(?!marrow_).*';
|
|
86
|
+
const CURSOR_NATIVE_HOOK_MATCHER = 'Shell|Write|Delete|Task|MCP:(?!marrow(?:_.*|:marrow_.*)$).*';
|
|
87
|
+
const GEMINI_NATIVE_HOOK_MATCHER = '^(?:run_shell_command|write_file|replace|edit_file|delete_file|mcp_(?!marrow_marrow_)[A-Za-z0-9_]{1,192})$';
|
|
88
|
+
const GROK_NATIVE_HOOK_MATCHER = 'run_terminal_command|search_replace|write|spawn_subagent|use_tool|workflow|image_gen|image_edit|image_to_video|reference_to_video';
|
|
89
|
+
const CODEX_HOOK_TIMEOUT_SECONDS = 5;
|
|
90
|
+
const CODEX_SESSION_TIMEOUT_SECONDS = 3;
|
|
91
|
+
const GEMINI_HOOK_TIMEOUT_MS = 5000;
|
|
92
|
+
const GEMINI_CLOSEOUT_TIMEOUT_MS = 3000;
|
|
37
93
|
const NATIVE_EXPECTED_HOOKS = ['prompt', 'pre_action', 'action_result', 'session_end'];
|
|
38
94
|
const SOURCE_CLIENTS = new Set(['claude-code', 'cursor', 'composer', 'windsurf', 'openclaw', 'codex', 'gemini', 'grok', 'deepseek', 'qwen', 'kimi', 'minimax', 'cline', 'opencode', 'hermes', 'glm', 'mcp', 'ci', 'custom', 'unknown']);
|
|
95
|
+
const TOOL_PROFILES = new Set(['primary', 'core', 'full']);
|
|
96
|
+
const TOOL_PROFILE_EXPECTED_COUNTS = Object.freeze({ primary: 17, core: 7, full: null });
|
|
97
|
+
const TOOL_PROFILE_EXACT_FIX = 'Unset MARROW_TOOL_PROFILE to use primary, or set MARROW_TOOL_PROFILE=core or MARROW_TOOL_PROFILE=full, then restart the owning harness and run npx @getmarrow/install@latest doctor --self-test.';
|
|
98
|
+
const PRIMARY_TOOL_NAMES = Object.freeze([
|
|
99
|
+
'marrow_agent_runtime',
|
|
100
|
+
'marrow_arbitrate',
|
|
101
|
+
'marrow_coordinate',
|
|
102
|
+
'marrow_replay_compare',
|
|
103
|
+
'marrow_decision_brief',
|
|
104
|
+
'marrow_think',
|
|
105
|
+
'marrow_commit',
|
|
106
|
+
'marrow_workflow_gate',
|
|
107
|
+
'marrow_completion_contracts',
|
|
108
|
+
'marrow_evaluate_completion_contract',
|
|
109
|
+
'marrow_agent_status',
|
|
110
|
+
'marrow_value_report',
|
|
111
|
+
'marrow_buyer_proof',
|
|
112
|
+
'marrow_governance_timeline',
|
|
113
|
+
'marrow_decision_trace',
|
|
114
|
+
'marrow_fleet_lessons',
|
|
115
|
+
'marrow_model_usage',
|
|
116
|
+
]);
|
|
39
117
|
const HARNESS_CAPABILITY_REGISTRY = Object.freeze([
|
|
40
118
|
{ client: 'claude-code', capability_level: 'native_hooks', automatic: ['prompt', 'pre_action', 'action_result', 'session_end'], install_surface: 'mcp' },
|
|
41
|
-
{ client: 'cursor', capability_level: '
|
|
42
|
-
{ client: 'composer', capability_level: '
|
|
43
|
-
{ client: 'cline', capability_level: '
|
|
44
|
-
{ client: 'windsurf', capability_level: '
|
|
45
|
-
{ client: 'codex', capability_level: '
|
|
119
|
+
{ client: 'cursor', capability_level: 'native_hooks', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'mcp' },
|
|
120
|
+
{ client: 'composer', capability_level: 'native_hooks', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'mcp' },
|
|
121
|
+
{ client: 'cline', capability_level: 'native_hooks', automatic: ['pre_action', 'action_result', 'cancel_closeout'], install_surface: 'mcp' },
|
|
122
|
+
{ client: 'windsurf', capability_level: 'native_hooks', automatic: ['pre_action', 'action_result', 'response_closeout'], install_surface: 'mcp' },
|
|
123
|
+
{ client: 'codex', capability_level: 'native_hooks', automatic: ['prompt', 'pre_action', 'action_result', 'session_end'], install_surface: 'mcp' },
|
|
46
124
|
{ client: 'opencode', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
|
|
47
125
|
{ client: 'hermes', capability_level: 'event_contract', automatic: [], install_surface: 'addon' },
|
|
48
126
|
{ client: 'openclaw', capability_level: 'event_contract', automatic: [], install_surface: 'addon' },
|
|
49
|
-
{ client: 'gemini', capability_level: '
|
|
50
|
-
{ client: 'grok', capability_level: '
|
|
127
|
+
{ client: 'gemini', capability_level: 'native_hooks', automatic: ['pre_action', 'action_result', 'turn_closeout'], install_surface: 'mcp' },
|
|
128
|
+
{ client: 'grok', capability_level: 'native_hooks', automatic: ['pre_action', 'action_result', 'turn_closeout'], install_surface: 'mcp' },
|
|
51
129
|
{ client: 'deepseek', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
|
|
52
130
|
{ client: 'qwen', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
|
|
53
131
|
{ client: 'kimi', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
|
|
@@ -63,6 +141,148 @@ function explicitMcpVersion(command) {
|
|
|
63
141
|
return match ? match[1] : null;
|
|
64
142
|
}
|
|
65
143
|
|
|
144
|
+
function resolveToolProfile(value) {
|
|
145
|
+
const structured = value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
146
|
+
const candidate = structured
|
|
147
|
+
? value.configured_profile
|
|
148
|
+
: value;
|
|
149
|
+
const absent = !structured && candidate === undefined;
|
|
150
|
+
const structuredUnset = structured && candidate === 'unset';
|
|
151
|
+
const configuredProfile = absent || structuredUnset
|
|
152
|
+
? 'unset'
|
|
153
|
+
: candidate;
|
|
154
|
+
if (!absent && !structuredUnset && !TOOL_PROFILES.has(configuredProfile)) {
|
|
155
|
+
throw new Error(`Invalid MARROW_TOOL_PROFILE. ${TOOL_PROFILE_EXACT_FIX}`);
|
|
156
|
+
}
|
|
157
|
+
const effectiveProfile = configuredProfile === 'unset' ? 'primary' : configuredProfile;
|
|
158
|
+
return {
|
|
159
|
+
configured_profile: configuredProfile,
|
|
160
|
+
effective_profile: effectiveProfile,
|
|
161
|
+
expected_visible_count: TOOL_PROFILE_EXPECTED_COUNTS[effectiveProfile],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function normalizePrimaryToolAvailability(value) {
|
|
166
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || value.profile !== 'primary') return null;
|
|
167
|
+
const evidence = value.entitlement_evidence;
|
|
168
|
+
const counts = value.counts;
|
|
169
|
+
const tools = Array.isArray(value.tools) ? value.tools : [];
|
|
170
|
+
if (!evidence || typeof evidence !== 'object' || evidence.authorizing !== false) return null;
|
|
171
|
+
if (!['available', 'unavailable'].includes(evidence.state)) return null;
|
|
172
|
+
if (!counts || counts.total !== 17 || !Number.isInteger(counts.entitled) || !Number.isInteger(counts.upgrade_required)) return null;
|
|
173
|
+
if (counts.entitled + counts.upgrade_required !== counts.total || tools.length !== counts.total) return null;
|
|
174
|
+
const normalizedTools = [];
|
|
175
|
+
const seen = new Set();
|
|
176
|
+
for (const tool of tools) {
|
|
177
|
+
if (!tool || typeof tool !== 'object' || !PRIMARY_TOOL_NAMES.includes(tool.name) || seen.has(tool.name)) return null;
|
|
178
|
+
if (!['entitled', 'upgrade_required'].includes(tool.state) || typeof tool.always_available !== 'boolean') return null;
|
|
179
|
+
seen.add(tool.name);
|
|
180
|
+
normalizedTools.push({
|
|
181
|
+
name: tool.name,
|
|
182
|
+
state: tool.state,
|
|
183
|
+
always_available: tool.always_available,
|
|
184
|
+
plan_feature: typeof tool.plan_feature === 'string' ? tool.plan_feature : null,
|
|
185
|
+
minimum_plan: typeof tool.minimum_plan === 'string' ? tool.minimum_plan : null,
|
|
186
|
+
owner_management_url: typeof tool.owner_management_url === 'string' ? tool.owner_management_url : '',
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
if (PRIMARY_TOOL_NAMES.some((name) => !seen.has(name))) return null;
|
|
190
|
+
const entitled = normalizedTools.filter((tool) => tool.state === 'entitled').length;
|
|
191
|
+
const upgradeRequired = normalizedTools.filter((tool) => tool.state === 'upgrade_required').length;
|
|
192
|
+
if (entitled !== counts.entitled || upgradeRequired !== counts.upgrade_required) return null;
|
|
193
|
+
return {
|
|
194
|
+
profile: 'primary',
|
|
195
|
+
current_plan: typeof value.current_plan === 'string' ? value.current_plan : null,
|
|
196
|
+
owner_management_url: typeof value.owner_management_url === 'string' ? value.owner_management_url : '',
|
|
197
|
+
entitlement_evidence: {
|
|
198
|
+
state: evidence.state,
|
|
199
|
+
source: typeof evidence.source === 'string' ? evidence.source : 'entitlement_read_unavailable',
|
|
200
|
+
authoritative: evidence.authoritative === true,
|
|
201
|
+
authorizing: false,
|
|
202
|
+
},
|
|
203
|
+
counts: { total: 17, entitled, upgrade_required: upgradeRequired },
|
|
204
|
+
tools: normalizedTools,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function backendEntitlementProjection(statusProfile, contextProjection) {
|
|
209
|
+
const freshProjection = normalizePrimaryToolAvailability(contextProjection);
|
|
210
|
+
if (freshProjection) {
|
|
211
|
+
return {
|
|
212
|
+
evidence_state: freshProjection.entitlement_evidence.state,
|
|
213
|
+
source: 'authenticated_backend',
|
|
214
|
+
authorizes_calls: false,
|
|
215
|
+
primary_tool_availability: freshProjection,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const envelope = statusProfile?.backend_entitlement_projection;
|
|
219
|
+
const projected = normalizePrimaryToolAvailability(envelope?.primary_tool_availability);
|
|
220
|
+
const source = ['authenticated_backend', 'cached_or_stale_status', 'backend_projection_not_provided'].includes(envelope?.source)
|
|
221
|
+
? envelope.source
|
|
222
|
+
: 'backend_projection_not_provided';
|
|
223
|
+
const available = envelope?.authorizes_calls === false
|
|
224
|
+
&& envelope?.evidence_state === 'available'
|
|
225
|
+
&& source === 'authenticated_backend'
|
|
226
|
+
&& projected?.entitlement_evidence.state === 'available';
|
|
227
|
+
return {
|
|
228
|
+
evidence_state: available ? 'available' : 'unavailable',
|
|
229
|
+
source,
|
|
230
|
+
authorizes_calls: false,
|
|
231
|
+
primary_tool_availability: projected,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function buildMcpToolProfileReport(value, statusProfile = null, contextProjection = null, forceReload = false) {
|
|
236
|
+
const expected = resolveToolProfile(value);
|
|
237
|
+
const reportedNames = Array.isArray(statusProfile?.visible_tool_names)
|
|
238
|
+
? statusProfile.visible_tool_names.filter((name) => typeof name === 'string')
|
|
239
|
+
: [];
|
|
240
|
+
const reportedCount = statusProfile?.visible_tool_count;
|
|
241
|
+
const reportedConfigured = statusProfile?.configured_profile;
|
|
242
|
+
const reportedEffective = statusProfile?.effective_profile;
|
|
243
|
+
const uniqueNames = new Set(reportedNames);
|
|
244
|
+
const profileIdentityMatches = reportedConfigured === expected.configured_profile
|
|
245
|
+
&& reportedEffective === expected.effective_profile;
|
|
246
|
+
const reportedCatalogIsConsistent = Number.isInteger(reportedCount)
|
|
247
|
+
&& reportedCount >= 0
|
|
248
|
+
&& reportedNames.length === reportedCount
|
|
249
|
+
&& uniqueNames.size === reportedCount;
|
|
250
|
+
const expectedCount = expected.effective_profile === 'full'
|
|
251
|
+
&& profileIdentityMatches
|
|
252
|
+
&& reportedCatalogIsConsistent
|
|
253
|
+
? reportedCount
|
|
254
|
+
: expected.expected_visible_count;
|
|
255
|
+
const expectedPrimaryNames = expected.effective_profile !== 'primary'
|
|
256
|
+
|| (reportedNames.length === PRIMARY_TOOL_NAMES.length
|
|
257
|
+
&& PRIMARY_TOOL_NAMES.every((name) => uniqueNames.has(name)));
|
|
258
|
+
const visibilityLive = !forceReload
|
|
259
|
+
&& profileIdentityMatches
|
|
260
|
+
&& statusProfile?.local_visibility_grants_entitlement === false
|
|
261
|
+
&& reportedCatalogIsConsistent
|
|
262
|
+
&& reportedCount === expectedCount
|
|
263
|
+
&& expectedPrimaryNames;
|
|
264
|
+
return {
|
|
265
|
+
configured_profile: expected.configured_profile,
|
|
266
|
+
effective_profile: expected.effective_profile,
|
|
267
|
+
expected_visible_count: expectedCount,
|
|
268
|
+
visible_tool_count: visibilityLive ? reportedCount : null,
|
|
269
|
+
actual_visible_count: visibilityLive ? reportedCount : null,
|
|
270
|
+
visible_tool_names: visibilityLive ? reportedNames : [],
|
|
271
|
+
local_visibility_grants_entitlement: false,
|
|
272
|
+
visibility_live: visibilityLive,
|
|
273
|
+
reload_required: !visibilityLive,
|
|
274
|
+
reported_configured_profile: typeof reportedConfigured === 'string' ? reportedConfigured : null,
|
|
275
|
+
reported_effective_profile: typeof reportedEffective === 'string' ? reportedEffective : null,
|
|
276
|
+
backend_entitlement_projection: backendEntitlementProjection(statusProfile, contextProjection),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function initialToolProfileReport(value) {
|
|
281
|
+
return {
|
|
282
|
+
...buildMcpToolProfileReport(value),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
66
286
|
function readMcpPackageVersion(packageRoot) {
|
|
67
287
|
try {
|
|
68
288
|
const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
@@ -252,7 +472,7 @@ function sourceClient() {
|
|
|
252
472
|
return aliases[raw] || (SOURCE_CLIENTS.has(raw) ? raw : 'custom');
|
|
253
473
|
}
|
|
254
474
|
|
|
255
|
-
function parseArgs(argv) {
|
|
475
|
+
function parseArgs(argv, env = process.env) {
|
|
256
476
|
const options = {
|
|
257
477
|
cwd: process.cwd(),
|
|
258
478
|
yes: false,
|
|
@@ -260,50 +480,79 @@ function parseArgs(argv) {
|
|
|
260
480
|
doctor: false,
|
|
261
481
|
repair: false,
|
|
262
482
|
mode: 'auto',
|
|
263
|
-
apiKey:
|
|
264
|
-
baseUrl:
|
|
265
|
-
agentId:
|
|
483
|
+
apiKey: env.MARROW_API_KEY || '',
|
|
484
|
+
baseUrl: env.MARROW_BASE_URL || DEFAULT_BASE_URL,
|
|
485
|
+
agentId: env.MARROW_FLEET_AGENT_ID || env.MARROW_AGENT_ID || '',
|
|
486
|
+
toolProfile: resolveToolProfile(env.MARROW_TOOL_PROFILE),
|
|
266
487
|
selfTest: true,
|
|
267
488
|
selfTestExplicitlyDisabled: false,
|
|
268
489
|
json: false,
|
|
269
490
|
activate: false,
|
|
270
491
|
controller: true,
|
|
271
492
|
};
|
|
493
|
+
let explicitOperation = false;
|
|
272
494
|
|
|
273
495
|
for (let i = 0; i < argv.length; i += 1) {
|
|
274
496
|
const arg = argv[i];
|
|
275
497
|
if (arg === 'activate' || arg === '--activate') {
|
|
498
|
+
explicitOperation = true;
|
|
276
499
|
options.activate = true;
|
|
277
500
|
options.yes = true;
|
|
278
501
|
options.selfTest = true;
|
|
279
502
|
}
|
|
280
|
-
else if (arg === '--yes' || arg === '-y')
|
|
503
|
+
else if (arg === '--yes' || arg === '-y') {
|
|
504
|
+
explicitOperation = true;
|
|
505
|
+
options.yes = true;
|
|
506
|
+
}
|
|
281
507
|
else if (arg === '--repair' || arg === 'repair' || arg === 'update' || arg === '--update') {
|
|
508
|
+
explicitOperation = true;
|
|
282
509
|
options.repair = true;
|
|
283
510
|
options.yes = true;
|
|
284
511
|
options.update = true;
|
|
285
512
|
}
|
|
286
|
-
else if (arg === '--dry-run')
|
|
287
|
-
|
|
513
|
+
else if (arg === '--dry-run') {
|
|
514
|
+
explicitOperation = true;
|
|
515
|
+
options.dryRun = true;
|
|
516
|
+
}
|
|
517
|
+
else if (arg === '--doctor' || arg === 'doctor' || arg === 'check') {
|
|
518
|
+
explicitOperation = true;
|
|
519
|
+
options.doctor = true;
|
|
520
|
+
}
|
|
288
521
|
else if (arg === '--json') options.json = true;
|
|
289
522
|
else if (arg === '--no-self-test') {
|
|
523
|
+
explicitOperation = true;
|
|
290
524
|
options.selfTest = false;
|
|
291
525
|
options.selfTestExplicitlyDisabled = true;
|
|
292
526
|
}
|
|
293
527
|
else if (arg === '--no-controller') options.controller = false;
|
|
294
528
|
else if (arg === '--self-test') options.selfTest = true;
|
|
295
529
|
else if (arg === '--cwd') options.cwd = path.resolve(argv[++i] || options.cwd);
|
|
296
|
-
else if (arg === '--mode')
|
|
530
|
+
else if (arg === '--mode') {
|
|
531
|
+
explicitOperation = true;
|
|
532
|
+
options.mode = argv[++i] || options.mode;
|
|
533
|
+
}
|
|
297
534
|
else if (arg === '--key') {
|
|
298
535
|
options.apiKey = argv[++i] || options.apiKey;
|
|
299
536
|
options.keyFromArg = true;
|
|
300
537
|
}
|
|
301
538
|
else if (arg === '--base-url') options.baseUrl = argv[++i] || options.baseUrl;
|
|
302
539
|
else if (arg === '--agent-id') options.agentId = argv[++i] || options.agentId;
|
|
303
|
-
else if (arg === '--mcp')
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
540
|
+
else if (arg === '--mcp') {
|
|
541
|
+
explicitOperation = true;
|
|
542
|
+
options.mode = 'mcp';
|
|
543
|
+
}
|
|
544
|
+
else if (arg === '--sdk') {
|
|
545
|
+
explicitOperation = true;
|
|
546
|
+
options.mode = 'sdk';
|
|
547
|
+
}
|
|
548
|
+
else if (arg === '--md' || arg === '--instructions') {
|
|
549
|
+
explicitOperation = true;
|
|
550
|
+
options.mode = 'md';
|
|
551
|
+
}
|
|
552
|
+
else if (arg === '--both') {
|
|
553
|
+
explicitOperation = true;
|
|
554
|
+
options.mode = 'both';
|
|
555
|
+
}
|
|
307
556
|
else if (arg === '--help' || arg === '-h') {
|
|
308
557
|
options.help = true;
|
|
309
558
|
} else {
|
|
@@ -314,6 +563,12 @@ function parseArgs(argv) {
|
|
|
314
563
|
if (!['auto', 'mcp', 'sdk', 'both', 'md'].includes(options.mode)) {
|
|
315
564
|
throw new Error('--mode must be one of auto, mcp, sdk, both, md');
|
|
316
565
|
}
|
|
566
|
+
if (!explicitOperation) {
|
|
567
|
+
options.activate = true;
|
|
568
|
+
options.yes = true;
|
|
569
|
+
options.selfTest = true;
|
|
570
|
+
}
|
|
571
|
+
if (options.dryRun) options.selfTest = false;
|
|
317
572
|
if (options.activate && options.selfTestExplicitlyDisabled) {
|
|
318
573
|
throw new Error('activate cannot be combined with --no-self-test because server verification is required');
|
|
319
574
|
}
|
|
@@ -326,6 +581,7 @@ function parseArgs(argv) {
|
|
|
326
581
|
|
|
327
582
|
function usage() {
|
|
328
583
|
return `Usage:
|
|
584
|
+
npx @getmarrow/install
|
|
329
585
|
npx @getmarrow/install --dry-run
|
|
330
586
|
npx @getmarrow/install activate
|
|
331
587
|
npx @getmarrow/install --yes
|
|
@@ -336,6 +592,7 @@ function usage() {
|
|
|
336
592
|
npx @getmarrow/install --sdk --yes
|
|
337
593
|
|
|
338
594
|
Options:
|
|
595
|
+
(no command) Detect, install, self-test, and start the supported persistent controller
|
|
339
596
|
activate Detect, install, self-test, and return a server-confirmed activation receipt
|
|
340
597
|
--dry-run Print planned changes without writing
|
|
341
598
|
--doctor Check install health without writing
|
|
@@ -348,6 +605,10 @@ Options:
|
|
|
348
605
|
--agent-id <id> Agent/fleet id for self-test headers
|
|
349
606
|
--no-controller Do not start the local background controller during install/repair
|
|
350
607
|
--no-self-test Skip API smoke/self-test
|
|
608
|
+
|
|
609
|
+
Environment:
|
|
610
|
+
MARROW_TOOL_PROFILE Leave unset for primary (17 tools), or explicitly set primary, core, or full.
|
|
611
|
+
Visibility never grants entitlement; backend plans and permissions authorize calls.
|
|
351
612
|
`;
|
|
352
613
|
}
|
|
353
614
|
|
|
@@ -361,6 +622,9 @@ function detectedClient(detection) {
|
|
|
361
622
|
if (detection.openclaw) return 'openclaw';
|
|
362
623
|
if (detection.claudeCode) return 'claude-code';
|
|
363
624
|
if (detection.cursor) return 'cursor';
|
|
625
|
+
if (detection.cline) return 'cline';
|
|
626
|
+
if (detection.windsurf) return 'windsurf';
|
|
627
|
+
if (detection.gemini) return 'gemini';
|
|
364
628
|
if (detection.codex) return 'codex';
|
|
365
629
|
return 'custom';
|
|
366
630
|
}
|
|
@@ -402,6 +666,14 @@ function detectEnvironment(cwd = process.cwd(), env = process.env) {
|
|
|
402
666
|
requirements: path.join(root, 'requirements.txt'),
|
|
403
667
|
setupPy: path.join(root, 'setup.py'),
|
|
404
668
|
claudeSettings: path.join(root, '.claude', 'settings.json'),
|
|
669
|
+
codexHooks: path.join(root, '.codex', 'hooks.json'),
|
|
670
|
+
cursorHooks: path.join(root, '.cursor', 'hooks.json'),
|
|
671
|
+
windsurfHooks: path.join(root, '.windsurf', 'hooks.json'),
|
|
672
|
+
geminiSettings: path.join(root, '.gemini', 'settings.json'),
|
|
673
|
+
grokHooks: path.join(home, '.grok', 'hooks', 'marrow.json'),
|
|
674
|
+
clinePreToolUseHook: path.join(root, '.clinerules', 'hooks', 'PreToolUse'),
|
|
675
|
+
clinePostToolUseHook: path.join(root, '.clinerules', 'hooks', 'PostToolUse'),
|
|
676
|
+
clineTaskCancelHook: path.join(root, '.clinerules', 'hooks', 'TaskCancel'),
|
|
405
677
|
claudeMd: path.join(root, 'CLAUDE.md'),
|
|
406
678
|
agentsMd: path.join(root, 'AGENTS.md'),
|
|
407
679
|
cursorRules: path.join(root, '.cursor', 'rules', 'marrow.mdc'),
|
|
@@ -420,6 +692,10 @@ function detectEnvironment(cwd = process.cwd(), env = process.env) {
|
|
|
420
692
|
python: exists(paths.pyproject) || exists(paths.requirements) || exists(paths.setupPy),
|
|
421
693
|
claudeCode: exists(paths.claudeSettings) || exists(paths.claudeMd),
|
|
422
694
|
cursor: exists(path.join(root, '.cursor')),
|
|
695
|
+
cline: exists(path.join(root, '.clinerules')),
|
|
696
|
+
windsurf: exists(path.join(root, '.windsurf')),
|
|
697
|
+
gemini: exists(path.join(root, '.gemini')),
|
|
698
|
+
grok: exists(path.join(home, '.grok')) || exists(path.join(root, '.grok')),
|
|
423
699
|
codex: exists(paths.agentsMd) || exists(path.join(root, '.codex')),
|
|
424
700
|
mcpConfig: exists(paths.mcpJson) || exists(paths.cursorMcp) || exists(paths.claudeSettings),
|
|
425
701
|
openclaw: exists(paths.openclawJson) || Boolean(env.OPENCLAW_HOME || env.OPENCLAW_AGENT_ID),
|
|
@@ -621,13 +897,14 @@ function passiveInstructions() {
|
|
|
621
897
|
Marrow should run passively after install:
|
|
622
898
|
|
|
623
899
|
- Use MCP plus these instructions in every workspace: \`npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp setup\`.
|
|
900
|
+
- Leave \`MARROW_TOOL_PROFILE\` unset for the 17-tool primary surface. Set \`MARROW_TOOL_PROFILE=core\` or \`MARROW_TOOL_PROFILE=full\` only as an explicit opt-in; backend plans and permissions still enforce access to every visible tool.
|
|
624
901
|
- Use SDK passive runtime in owned Node processes: \`createPassiveRuntime().install()\`.
|
|
625
|
-
- Native Claude hooks install only when \`.claude\` is present. Cursor
|
|
902
|
+
- Native Claude hooks install only when \`.claude\` is present. Codex native hooks install into \`.codex/hooks.json\`; Cursor and Composer use \`.cursor/hooks.json\`; Cline uses non-overwriting executable files under \`.clinerules/hooks/\`; Windsurf uses \`.windsurf/hooks.json\`; Gemini CLI uses \`.gemini/settings.json\`; Grok uses trusted global \`~/.grok/hooks/marrow.json\`. Restart the host, enable/review hooks, disable Windsurf Restricted Mode where native hooks are required, and trust the workspace before claiming runtime coverage. The governed wrapper remains an explicit bounded fallback. Hermes, OpenClaw, and custom hosts need a bounded event adapter.
|
|
626
903
|
- Keep passive token/model usage proof enabled. Empty savings stay zero until observed model usage lands. Do not invent token, cost, or time savings.
|
|
627
904
|
- Before risky work, use Marrow's decision brief or passive prompt hook.
|
|
628
905
|
- After activate writes MCP or hooks, restart this harness, then run \`npx @getmarrow/install@latest doctor --self-test\`. Do not claim Marrow is live in a process that has not reloaded.
|
|
629
906
|
- First turn: tell the owner the gate is live and empty savings are honest. The next deploy, merge, or publish must go through Marrow.
|
|
630
|
-
- Cursor
|
|
907
|
+
- Cursor and Composer use configured native hooks after restart and \`/hooks\` trust review. Cline uses its configured PreToolUse, PostToolUse, and TaskCancel hooks only after Enable Hooks, restart, executable trust, and workspace trust; TaskComplete is not claimed as current coverage. Windsurf uses configured native pre-action, success-result, and response-closeout hooks only after restart, trust review, and leaving Restricted Mode. Gemini CLI uses configured BeforeTool, AfterTool, and AfterAgent hooks only after restart and project fingerprint review and approval in \`/hooks panel\`; explicit user disablement is preserved. Grok uses global native PreToolUse, PostToolUse/PostToolUseFailure, and one nonblocking Stop closeout only after restart and \`/hooks\` inspection; Grok hooks remain user-toggleable. MCP tools remain on demand. Codex uses configured native hooks after restart and \`/hooks\` trust review. The governed wrapper remains an explicit bounded fallback.
|
|
631
908
|
- Before the session ends, close open work with session-end auto-commit or \`marrow_commit\`. Record model usage only when the host response includes counts.
|
|
632
909
|
- After meaningful work, record the outcome so future agents learn from it.
|
|
633
910
|
- After Marrow blocks, warns, or requires review, use the decision trace receipt to tell the operator what changed and which recorded workflow or proof is required. Stay quiet for routine low-risk work.
|
|
@@ -684,6 +961,7 @@ function envExample(options = {}) {
|
|
|
684
961
|
MARROW_BASE_URL=${JSON.stringify(baseUrl)}
|
|
685
962
|
MARROW_FLEET_AGENT_ID=${JSON.stringify(agentId)}
|
|
686
963
|
MARROW_CLIENT=${JSON.stringify(client)}
|
|
964
|
+
# MARROW_TOOL_PROFILE is intentionally unset: ordinary setup uses primary. Set core or full only as an explicit opt-in.
|
|
687
965
|
MARROW_ENFORCEMENT_MODE=auto
|
|
688
966
|
MARROW_PASSIVE_BRIEF=auto
|
|
689
967
|
MARROW_PASSIVE_VALUE_REPORT=true
|
|
@@ -757,12 +1035,12 @@ function exactHookDescriptors(settings, eventName, command, matcher) {
|
|
|
757
1035
|
function marrowHookSubcommand(command) {
|
|
758
1036
|
if (typeof command !== 'string') return null;
|
|
759
1037
|
const match = command.trim().match(
|
|
760
|
-
/^npx\s+(?:-y\s+)?(?:--package=@getmarrow\/mcp(?:@[^\s]+)?\s+marrow-mcp|@getmarrow\/mcp(?:@[^\s]+)?)\s+(context-hook|pre-action-hook|hook|session-hook)$/,
|
|
1038
|
+
/^npx\s+(?:-y\s+)?(?:--package=@getmarrow\/mcp(?:@[^\s]+)?\s+marrow-mcp|@getmarrow\/mcp(?:@[^\s]+)?)\s+(?:(?:claude|codex|cursor|grok)-)?(context-hook|pre-action-hook|hook|session-hook)$/,
|
|
761
1039
|
);
|
|
762
1040
|
return match?.[1] || null;
|
|
763
1041
|
}
|
|
764
1042
|
|
|
765
|
-
function reconcileMarrowCommandHook(settings, eventName, subcommand, command, matcher) {
|
|
1043
|
+
function reconcileMarrowCommandHook(settings, eventName, subcommand, command, matcher, handlerOptions = {}) {
|
|
766
1044
|
const original = Array.isArray(settings?.hooks?.[eventName]) ? settings.hooks[eventName] : [];
|
|
767
1045
|
let preferredHandler = null;
|
|
768
1046
|
const retained = [];
|
|
@@ -786,7 +1064,7 @@ function reconcileMarrowCommandHook(settings, eventName, subcommand, command, ma
|
|
|
786
1064
|
}
|
|
787
1065
|
if (remaining.length > 0) retained.push({ ...entry, hooks: remaining });
|
|
788
1066
|
}
|
|
789
|
-
const canonical = { hooks: [{ ...(preferredHandler || {}), type: 'command', command }] };
|
|
1067
|
+
const canonical = { hooks: [{ ...(preferredHandler || {}), ...handlerOptions, type: 'command', command }] };
|
|
790
1068
|
if (matcher != null) canonical.matcher = matcher;
|
|
791
1069
|
retained.push(canonical);
|
|
792
1070
|
return retained;
|
|
@@ -882,6 +1160,439 @@ function upsertClaudeHooks(settingsPath) {
|
|
|
882
1160
|
return JSON.stringify(settings, null, 2) + '\n';
|
|
883
1161
|
}
|
|
884
1162
|
|
|
1163
|
+
function codexNativeHookFingerprint(settings) {
|
|
1164
|
+
const contract = {
|
|
1165
|
+
schema: 'marrow-codex-native-hooks.v1',
|
|
1166
|
+
adapter_version: MCP_ADAPTER_VERSION,
|
|
1167
|
+
expected_hooks: NATIVE_EXPECTED_HOOKS,
|
|
1168
|
+
configured: {
|
|
1169
|
+
prompt: exactHookConfigured(settings, 'UserPromptSubmit', CODEX_CONTEXT_HOOK_COMMAND),
|
|
1170
|
+
pre_action: exactHookConfigured(settings, 'PreToolUse', CODEX_PRE_ACTION_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER),
|
|
1171
|
+
action_result: exactHookConfigured(settings, 'PostToolUse', CODEX_ACTION_RESULT_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER),
|
|
1172
|
+
session_end: exactHookConfigured(settings, 'SessionEnd', CODEX_SESSION_END_HOOK_COMMAND),
|
|
1173
|
+
},
|
|
1174
|
+
descriptors: {
|
|
1175
|
+
prompt: exactHookDescriptors(settings, 'UserPromptSubmit', CODEX_CONTEXT_HOOK_COMMAND),
|
|
1176
|
+
pre_action: exactHookDescriptors(settings, 'PreToolUse', CODEX_PRE_ACTION_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER),
|
|
1177
|
+
action_result: exactHookDescriptors(settings, 'PostToolUse', CODEX_ACTION_RESULT_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER),
|
|
1178
|
+
session_end: exactHookDescriptors(settings, 'SessionEnd', CODEX_SESSION_END_HOOK_COMMAND),
|
|
1179
|
+
},
|
|
1180
|
+
active_marrow_handlers: {
|
|
1181
|
+
prompt: marrowHookDescriptors(settings, 'UserPromptSubmit'),
|
|
1182
|
+
pre_action: marrowHookDescriptors(settings, 'PreToolUse'),
|
|
1183
|
+
action_result: marrowHookDescriptors(settings, 'PostToolUse'),
|
|
1184
|
+
session_end: marrowHookDescriptors(settings, 'SessionEnd'),
|
|
1185
|
+
},
|
|
1186
|
+
};
|
|
1187
|
+
return crypto.createHash('sha256').update(JSON.stringify(contract)).digest('hex');
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function upsertCodexHooks(hooksPath) {
|
|
1191
|
+
const settings = parseJsonObject(hooksPath);
|
|
1192
|
+
const hooks = settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)
|
|
1193
|
+
? settings.hooks
|
|
1194
|
+
: {};
|
|
1195
|
+
settings.hooks = {
|
|
1196
|
+
...hooks,
|
|
1197
|
+
UserPromptSubmit: reconcileMarrowCommandHook(
|
|
1198
|
+
settings, 'UserPromptSubmit', 'context-hook', CODEX_CONTEXT_HOOK_COMMAND, undefined,
|
|
1199
|
+
{ timeout: CODEX_HOOK_TIMEOUT_SECONDS },
|
|
1200
|
+
),
|
|
1201
|
+
PreToolUse: reconcileMarrowCommandHook(
|
|
1202
|
+
settings, 'PreToolUse', 'pre-action-hook', CODEX_PRE_ACTION_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER,
|
|
1203
|
+
{ timeout: CODEX_HOOK_TIMEOUT_SECONDS, async: false },
|
|
1204
|
+
),
|
|
1205
|
+
PostToolUse: reconcileMarrowCommandHook(
|
|
1206
|
+
settings, 'PostToolUse', 'hook', CODEX_ACTION_RESULT_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER,
|
|
1207
|
+
{ timeout: CODEX_HOOK_TIMEOUT_SECONDS },
|
|
1208
|
+
),
|
|
1209
|
+
SessionEnd: reconcileMarrowCommandHook(
|
|
1210
|
+
settings, 'SessionEnd', 'session-hook', CODEX_SESSION_END_HOOK_COMMAND, undefined,
|
|
1211
|
+
{ timeout: CODEX_SESSION_TIMEOUT_SECONDS },
|
|
1212
|
+
),
|
|
1213
|
+
};
|
|
1214
|
+
return JSON.stringify(settings, null, 2) + '\n';
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
function reconcileCursorHook(settings, eventName, subcommand, canonical) {
|
|
1218
|
+
const original = Array.isArray(settings?.hooks?.[eventName]) ? settings.hooks[eventName] : [];
|
|
1219
|
+
const retained = original.filter((entry) => !(
|
|
1220
|
+
entry && typeof entry === 'object' && !Array.isArray(entry)
|
|
1221
|
+
&& marrowHookSubcommand(entry.command)
|
|
1222
|
+
));
|
|
1223
|
+
return [...retained, canonical];
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
function exactCursorHookConfigured(settings, eventName, command, matcher, required = {}) {
|
|
1227
|
+
const entries = settings?.hooks?.[eventName];
|
|
1228
|
+
if (!Array.isArray(entries)) return false;
|
|
1229
|
+
return entries.some((entry) => entry && typeof entry === 'object' && !Array.isArray(entry)
|
|
1230
|
+
&& entry.command === command
|
|
1231
|
+
&& (matcher === undefined ? entry.matcher === undefined : entry.matcher === matcher)
|
|
1232
|
+
&& Object.entries(required).every(([key, value]) => entry[key] === value));
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
function cursorHookDescriptors(settings, eventName) {
|
|
1236
|
+
const entries = settings?.hooks?.[eventName];
|
|
1237
|
+
if (!Array.isArray(entries)) return [];
|
|
1238
|
+
return entries.flatMap((entry) => {
|
|
1239
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry) || !marrowHookSubcommand(entry.command)) return [];
|
|
1240
|
+
return [{
|
|
1241
|
+
matcher: typeof entry.matcher === 'string' ? entry.matcher : null,
|
|
1242
|
+
command: String(entry.command).trim(),
|
|
1243
|
+
timeout: typeof entry.timeout === 'number' && Number.isFinite(entry.timeout) ? entry.timeout : null,
|
|
1244
|
+
failClosed: entry.failClosed === true,
|
|
1245
|
+
async: entry.async === false ? false : null,
|
|
1246
|
+
}];
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function cursorNativeHookFingerprint(settings) {
|
|
1251
|
+
const contract = {
|
|
1252
|
+
schema: 'marrow-cursor-native-hooks.v1',
|
|
1253
|
+
adapter_version: MCP_ADAPTER_VERSION,
|
|
1254
|
+
expected_hooks: ['pre_action', 'action_result', 'outcome_closure'],
|
|
1255
|
+
configured: {
|
|
1256
|
+
pre_action: exactCursorHookConfigured(settings, 'preToolUse', CURSOR_PRE_ACTION_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
|
|
1257
|
+
timeout: CODEX_HOOK_TIMEOUT_SECONDS,
|
|
1258
|
+
failClosed: true,
|
|
1259
|
+
async: false,
|
|
1260
|
+
}),
|
|
1261
|
+
action_result_success: exactCursorHookConfigured(settings, 'postToolUse', CURSOR_ACTION_RESULT_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
|
|
1262
|
+
timeout: CODEX_HOOK_TIMEOUT_SECONDS,
|
|
1263
|
+
}),
|
|
1264
|
+
action_result_failure: exactCursorHookConfigured(settings, 'postToolUseFailure', CURSOR_ACTION_RESULT_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
|
|
1265
|
+
timeout: CODEX_HOOK_TIMEOUT_SECONDS,
|
|
1266
|
+
}),
|
|
1267
|
+
outcome_closure: exactCursorHookConfigured(settings, 'stop', CURSOR_SESSION_END_HOOK_COMMAND, undefined, {
|
|
1268
|
+
timeout: CODEX_SESSION_TIMEOUT_SECONDS,
|
|
1269
|
+
}),
|
|
1270
|
+
},
|
|
1271
|
+
descriptors: {
|
|
1272
|
+
pre_action: cursorHookDescriptors(settings, 'preToolUse'),
|
|
1273
|
+
action_result_success: cursorHookDescriptors(settings, 'postToolUse'),
|
|
1274
|
+
action_result_failure: cursorHookDescriptors(settings, 'postToolUseFailure'),
|
|
1275
|
+
outcome_closure: cursorHookDescriptors(settings, 'stop'),
|
|
1276
|
+
},
|
|
1277
|
+
};
|
|
1278
|
+
return crypto.createHash('sha256').update(JSON.stringify(contract)).digest('hex');
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
function upsertCursorHooks(hooksPath) {
|
|
1282
|
+
const settings = parseJsonObject(hooksPath);
|
|
1283
|
+
const hooks = settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)
|
|
1284
|
+
? settings.hooks
|
|
1285
|
+
: {};
|
|
1286
|
+
settings.version = 1;
|
|
1287
|
+
settings.hooks = {
|
|
1288
|
+
...hooks,
|
|
1289
|
+
preToolUse: reconcileCursorHook(settings, 'preToolUse', 'pre-action-hook', {
|
|
1290
|
+
command: CURSOR_PRE_ACTION_HOOK_COMMAND,
|
|
1291
|
+
matcher: CURSOR_NATIVE_HOOK_MATCHER,
|
|
1292
|
+
timeout: CODEX_HOOK_TIMEOUT_SECONDS,
|
|
1293
|
+
failClosed: true,
|
|
1294
|
+
async: false,
|
|
1295
|
+
}),
|
|
1296
|
+
postToolUse: reconcileCursorHook(settings, 'postToolUse', 'hook', {
|
|
1297
|
+
command: CURSOR_ACTION_RESULT_HOOK_COMMAND,
|
|
1298
|
+
matcher: CURSOR_NATIVE_HOOK_MATCHER,
|
|
1299
|
+
timeout: CODEX_HOOK_TIMEOUT_SECONDS,
|
|
1300
|
+
}),
|
|
1301
|
+
postToolUseFailure: reconcileCursorHook(settings, 'postToolUseFailure', 'hook', {
|
|
1302
|
+
command: CURSOR_ACTION_RESULT_HOOK_COMMAND,
|
|
1303
|
+
matcher: CURSOR_NATIVE_HOOK_MATCHER,
|
|
1304
|
+
timeout: CODEX_HOOK_TIMEOUT_SECONDS,
|
|
1305
|
+
}),
|
|
1306
|
+
stop: reconcileCursorHook(settings, 'stop', 'session-hook', {
|
|
1307
|
+
command: CURSOR_SESSION_END_HOOK_COMMAND,
|
|
1308
|
+
timeout: CODEX_SESSION_TIMEOUT_SECONDS,
|
|
1309
|
+
}),
|
|
1310
|
+
};
|
|
1311
|
+
return JSON.stringify(settings, null, 2) + '\n';
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
const WINDSURF_PRE_EVENTS = ['pre_write_code', 'pre_run_command', 'pre_mcp_tool_use'];
|
|
1315
|
+
const WINDSURF_POST_EVENTS = ['post_write_code', 'post_run_command', 'post_mcp_tool_use'];
|
|
1316
|
+
|
|
1317
|
+
function windsurfMarrowHookEntrypoint(command) {
|
|
1318
|
+
if (typeof command !== 'string') return null;
|
|
1319
|
+
const match = command.match(/@getmarrow\/mcp(?:@[^\s]+)?\s+marrow-mcp\s+windsurf-(pre-action-hook|hook|session-hook)(?:\s|['"]|$)/);
|
|
1320
|
+
return match?.[1] || null;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function reconcileWindsurfHook(settings, eventName, command) {
|
|
1324
|
+
const original = Array.isArray(settings?.hooks?.[eventName]) ? settings.hooks[eventName] : [];
|
|
1325
|
+
const retained = original.filter((entry) => !(
|
|
1326
|
+
entry && typeof entry === 'object' && !Array.isArray(entry)
|
|
1327
|
+
&& windsurfMarrowHookEntrypoint(entry.command)
|
|
1328
|
+
));
|
|
1329
|
+
return [...retained, { command, show_output: false }];
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
function exactWindsurfHookConfigured(settings, eventName, command) {
|
|
1333
|
+
const entries = settings?.hooks?.[eventName];
|
|
1334
|
+
return Array.isArray(entries) && entries.some((entry) => (
|
|
1335
|
+
entry && typeof entry === 'object' && !Array.isArray(entry)
|
|
1336
|
+
&& entry.command === command
|
|
1337
|
+
&& entry.show_output === false
|
|
1338
|
+
));
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
function windsurfNativeHookFingerprint(settings) {
|
|
1342
|
+
const configured = Object.fromEntries([
|
|
1343
|
+
...WINDSURF_PRE_EVENTS.map((event) => [event, exactWindsurfHookConfigured(settings, event, WINDSURF_PRE_ACTION_HOOK_COMMAND)]),
|
|
1344
|
+
...WINDSURF_POST_EVENTS.map((event) => [event, exactWindsurfHookConfigured(settings, event, WINDSURF_ACTION_RESULT_HOOK_COMMAND)]),
|
|
1345
|
+
['post_cascade_response', exactWindsurfHookConfigured(settings, 'post_cascade_response', WINDSURF_SESSION_END_HOOK_COMMAND)],
|
|
1346
|
+
]);
|
|
1347
|
+
return crypto.createHash('sha256').update(JSON.stringify({
|
|
1348
|
+
schema: 'marrow-windsurf-native-hooks.v1',
|
|
1349
|
+
adapter_version: MCP_ADAPTER_VERSION,
|
|
1350
|
+
expected_hooks: ['pre_action', 'action_result', 'response_closeout'],
|
|
1351
|
+
restricted_mode_disables_hooks: true,
|
|
1352
|
+
configured,
|
|
1353
|
+
})).digest('hex');
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
function upsertWindsurfHooks(hooksPath) {
|
|
1357
|
+
const settings = parseJsonObject(hooksPath);
|
|
1358
|
+
const hooks = settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)
|
|
1359
|
+
? settings.hooks
|
|
1360
|
+
: {};
|
|
1361
|
+
settings.hooks = { ...hooks };
|
|
1362
|
+
for (const eventName of WINDSURF_PRE_EVENTS) {
|
|
1363
|
+
settings.hooks[eventName] = reconcileWindsurfHook(
|
|
1364
|
+
settings, eventName, WINDSURF_PRE_ACTION_HOOK_COMMAND,
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1367
|
+
for (const eventName of WINDSURF_POST_EVENTS) {
|
|
1368
|
+
settings.hooks[eventName] = reconcileWindsurfHook(
|
|
1369
|
+
settings, eventName, WINDSURF_ACTION_RESULT_HOOK_COMMAND,
|
|
1370
|
+
);
|
|
1371
|
+
}
|
|
1372
|
+
settings.hooks.post_cascade_response = reconcileWindsurfHook(
|
|
1373
|
+
settings, 'post_cascade_response', WINDSURF_SESSION_END_HOOK_COMMAND,
|
|
1374
|
+
);
|
|
1375
|
+
return JSON.stringify(settings, null, 2) + '\n';
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
function geminiMarrowHookEntrypoint(command) {
|
|
1379
|
+
if (typeof command !== 'string') return null;
|
|
1380
|
+
const match = command.match(/@getmarrow\/mcp(?:@[^\s]+)?\s+marrow-mcp\s+gemini-(pre-action-hook|hook|session-hook)(?:\s|['"]|$)/);
|
|
1381
|
+
return match?.[1] || null;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
function reconcileGeminiHook(settings, eventName, canonical) {
|
|
1385
|
+
const original = Array.isArray(settings?.hooks?.[eventName]) ? settings.hooks[eventName] : [];
|
|
1386
|
+
const retained = [];
|
|
1387
|
+
for (const group of original) {
|
|
1388
|
+
if (!group || typeof group !== 'object' || Array.isArray(group) || !Array.isArray(group.hooks)) {
|
|
1389
|
+
retained.push(group);
|
|
1390
|
+
continue;
|
|
1391
|
+
}
|
|
1392
|
+
const hooks = group.hooks.filter((handler) => !(
|
|
1393
|
+
handler && typeof handler === 'object' && !Array.isArray(handler)
|
|
1394
|
+
&& (String(handler.name || '').startsWith('marrow-') || geminiMarrowHookEntrypoint(handler.command))
|
|
1395
|
+
));
|
|
1396
|
+
if (hooks.length > 0) retained.push({ ...group, hooks });
|
|
1397
|
+
}
|
|
1398
|
+
return [...retained, canonical];
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
function exactGeminiHookConfigured(settings, eventName, name, command, matcher, timeout) {
|
|
1402
|
+
const groups = settings?.hooks?.[eventName];
|
|
1403
|
+
if (!Array.isArray(groups)) return false;
|
|
1404
|
+
return groups.some((group) => (
|
|
1405
|
+
group && typeof group === 'object' && !Array.isArray(group)
|
|
1406
|
+
&& (matcher === undefined ? group.matcher === undefined : group.matcher === matcher)
|
|
1407
|
+
&& Array.isArray(group.hooks)
|
|
1408
|
+
&& group.hooks.some((handler) => (
|
|
1409
|
+
handler && typeof handler === 'object' && !Array.isArray(handler)
|
|
1410
|
+
&& handler.name === name
|
|
1411
|
+
&& handler.type === 'command'
|
|
1412
|
+
&& handler.command === command
|
|
1413
|
+
&& handler.timeout === timeout
|
|
1414
|
+
))
|
|
1415
|
+
));
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
function geminiHooksExplicitlyDisabled(settings) {
|
|
1419
|
+
return settings?.hooksConfig?.enabled === false;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
function geminiNativeHookFingerprint(settings) {
|
|
1423
|
+
return crypto.createHash('sha256').update(JSON.stringify({
|
|
1424
|
+
schema: 'marrow-gemini-native-hooks.v1',
|
|
1425
|
+
adapter_version: MCP_ADAPTER_VERSION,
|
|
1426
|
+
expected_hooks: ['pre_action', 'action_result', 'turn_closeout'],
|
|
1427
|
+
explicitly_enabled: settings?.hooksConfig?.enabled === true,
|
|
1428
|
+
explicitly_disabled: geminiHooksExplicitlyDisabled(settings),
|
|
1429
|
+
configured: {
|
|
1430
|
+
pre_action: exactGeminiHookConfigured(
|
|
1431
|
+
settings, 'BeforeTool', 'marrow-before-tool', GEMINI_PRE_ACTION_HOOK_COMMAND,
|
|
1432
|
+
GEMINI_NATIVE_HOOK_MATCHER, GEMINI_HOOK_TIMEOUT_MS,
|
|
1433
|
+
),
|
|
1434
|
+
action_result: exactGeminiHookConfigured(
|
|
1435
|
+
settings, 'AfterTool', 'marrow-after-tool', GEMINI_ACTION_RESULT_HOOK_COMMAND,
|
|
1436
|
+
GEMINI_NATIVE_HOOK_MATCHER, GEMINI_HOOK_TIMEOUT_MS,
|
|
1437
|
+
),
|
|
1438
|
+
turn_closeout: exactGeminiHookConfigured(
|
|
1439
|
+
settings, 'AfterAgent', 'marrow-after-agent', GEMINI_SESSION_END_HOOK_COMMAND,
|
|
1440
|
+
undefined, GEMINI_CLOSEOUT_TIMEOUT_MS,
|
|
1441
|
+
),
|
|
1442
|
+
},
|
|
1443
|
+
session_end_claimed: false,
|
|
1444
|
+
})).digest('hex');
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
function exactGrokHookConfigured(settings, eventName, command, matcher, timeout) {
|
|
1448
|
+
const entries = settings?.hooks?.[eventName];
|
|
1449
|
+
if (!Array.isArray(entries)) return false;
|
|
1450
|
+
return entries.some((entry) => (
|
|
1451
|
+
entry && typeof entry === 'object' && !Array.isArray(entry)
|
|
1452
|
+
&& (matcher === undefined ? entry.matcher === undefined : entry.matcher === matcher)
|
|
1453
|
+
&& Array.isArray(entry.hooks)
|
|
1454
|
+
&& entry.hooks.some((handler) => (
|
|
1455
|
+
handler && typeof handler === 'object' && !Array.isArray(handler)
|
|
1456
|
+
&& handler.type === 'command'
|
|
1457
|
+
&& handler.command === command
|
|
1458
|
+
&& handler.timeout === timeout
|
|
1459
|
+
))
|
|
1460
|
+
));
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
function grokHasDuplicateSessionEnd(settings) {
|
|
1464
|
+
const entries = settings?.hooks?.SessionEnd;
|
|
1465
|
+
if (!Array.isArray(entries)) return false;
|
|
1466
|
+
return entries.some((entry) => Array.isArray(entry?.hooks) && entry.hooks.some((handler) => (
|
|
1467
|
+
handler && typeof handler === 'object' && !Array.isArray(handler)
|
|
1468
|
+
&& typeof handler.command === 'string'
|
|
1469
|
+
&& /marrow-mcp\s+grok-session-hook(?:\s|['"]|$)/.test(handler.command)
|
|
1470
|
+
)));
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
function grokNativeHookFingerprint(settings) {
|
|
1474
|
+
return crypto.createHash('sha256').update(JSON.stringify({
|
|
1475
|
+
schema: 'marrow-grok-native-hooks.v1',
|
|
1476
|
+
adapter_version: MCP_ADAPTER_VERSION,
|
|
1477
|
+
expected_hooks: ['pre_action', 'action_result', 'turn_closeout'],
|
|
1478
|
+
configured: {
|
|
1479
|
+
context: exactGrokHookConfigured(settings, 'UserPromptSubmit', GROK_CONTEXT_HOOK_COMMAND, undefined, 5),
|
|
1480
|
+
pre_action: exactGrokHookConfigured(settings, 'PreToolUse', GROK_PRE_ACTION_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 7),
|
|
1481
|
+
action_result_success: exactGrokHookConfigured(settings, 'PostToolUse', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5),
|
|
1482
|
+
action_result_failure: exactGrokHookConfigured(settings, 'PostToolUseFailure', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5),
|
|
1483
|
+
turn_closeout: exactGrokHookConfigured(settings, 'Stop', GROK_SESSION_END_HOOK_COMMAND, undefined, 3),
|
|
1484
|
+
duplicate_session_end: grokHasDuplicateSessionEnd(settings),
|
|
1485
|
+
},
|
|
1486
|
+
})).digest('hex');
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
function upsertGeminiHooks(settingsPath) {
|
|
1490
|
+
const settings = parseJsonObject(settingsPath);
|
|
1491
|
+
const hooks = settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)
|
|
1492
|
+
? settings.hooks
|
|
1493
|
+
: {};
|
|
1494
|
+
settings.hooks = {
|
|
1495
|
+
...hooks,
|
|
1496
|
+
BeforeTool: reconcileGeminiHook(settings, 'BeforeTool', {
|
|
1497
|
+
matcher: GEMINI_NATIVE_HOOK_MATCHER,
|
|
1498
|
+
hooks: [{
|
|
1499
|
+
name: 'marrow-before-tool',
|
|
1500
|
+
type: 'command',
|
|
1501
|
+
command: GEMINI_PRE_ACTION_HOOK_COMMAND,
|
|
1502
|
+
timeout: GEMINI_HOOK_TIMEOUT_MS,
|
|
1503
|
+
}],
|
|
1504
|
+
}),
|
|
1505
|
+
AfterTool: reconcileGeminiHook(settings, 'AfterTool', {
|
|
1506
|
+
matcher: GEMINI_NATIVE_HOOK_MATCHER,
|
|
1507
|
+
hooks: [{
|
|
1508
|
+
name: 'marrow-after-tool',
|
|
1509
|
+
type: 'command',
|
|
1510
|
+
command: GEMINI_ACTION_RESULT_HOOK_COMMAND,
|
|
1511
|
+
timeout: GEMINI_HOOK_TIMEOUT_MS,
|
|
1512
|
+
}],
|
|
1513
|
+
}),
|
|
1514
|
+
AfterAgent: reconcileGeminiHook(settings, 'AfterAgent', {
|
|
1515
|
+
hooks: [{
|
|
1516
|
+
name: 'marrow-after-agent',
|
|
1517
|
+
type: 'command',
|
|
1518
|
+
command: GEMINI_SESSION_END_HOOK_COMMAND,
|
|
1519
|
+
timeout: GEMINI_CLOSEOUT_TIMEOUT_MS,
|
|
1520
|
+
}],
|
|
1521
|
+
}),
|
|
1522
|
+
};
|
|
1523
|
+
return JSON.stringify(settings, null, 2) + '\n';
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
function clinePreToolUseHookSource() {
|
|
1527
|
+
return `#!/bin/sh
|
|
1528
|
+
output="$(npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp cline-pre-action-hook 2>/dev/null)" || output=""
|
|
1529
|
+
if [ -n "$output" ]; then
|
|
1530
|
+
validated="$(printf '%s' "$output" | NODE_OPTIONS= node -e 'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",c=>s+=c);process.stdin.on("end",()=>{try{const v=JSON.parse(s);const keys=Object.keys(v).sort();const allow=v.cancel===false&&keys.length===1&&keys[0]==="cancel";const deny=v.cancel===true&&typeof v.errorMessage==="string"&&v.errorMessage.length>0&&v.errorMessage.length<=500&&keys.length===2&&keys[0]==="cancel"&&keys[1]==="errorMessage";if(!allow&&!deny)process.exit(1);process.stdout.write(JSON.stringify(v));}catch{process.exit(1);}});' 2>/dev/null)" || validated=""
|
|
1531
|
+
if [ -n "$validated" ]; then
|
|
1532
|
+
printf '%s\n' "$validated"
|
|
1533
|
+
exit 0
|
|
1534
|
+
fi
|
|
1535
|
+
fi
|
|
1536
|
+
printf '%s\n' '{"cancel":true,"errorMessage":"Marrow governance did not return a valid decision. Restore trusted configuration and retry."}'
|
|
1537
|
+
exit 0
|
|
1538
|
+
`;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
function clineTelemetryHookSource(entrypoint) {
|
|
1542
|
+
return `#!/bin/sh
|
|
1543
|
+
npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp ${entrypoint} >/dev/null 2>&1 || :
|
|
1544
|
+
exit 0
|
|
1545
|
+
`;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
function clineHookContract(detection) {
|
|
1549
|
+
return [
|
|
1550
|
+
{
|
|
1551
|
+
stage: 'pre_action',
|
|
1552
|
+
path: detection.paths.clinePreToolUseHook,
|
|
1553
|
+
label: 'Cline PreToolUse native hook',
|
|
1554
|
+
content: clinePreToolUseHookSource(),
|
|
1555
|
+
},
|
|
1556
|
+
{
|
|
1557
|
+
stage: 'action_result',
|
|
1558
|
+
path: detection.paths.clinePostToolUseHook,
|
|
1559
|
+
label: 'Cline PostToolUse native hook',
|
|
1560
|
+
content: clineTelemetryHookSource('cline-hook'),
|
|
1561
|
+
},
|
|
1562
|
+
{
|
|
1563
|
+
stage: 'cancel_closeout',
|
|
1564
|
+
path: detection.paths.clineTaskCancelHook,
|
|
1565
|
+
label: 'Cline TaskCancel native hook',
|
|
1566
|
+
content: clineTelemetryHookSource('cline-session-hook'),
|
|
1567
|
+
},
|
|
1568
|
+
];
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
function exactExecutableFile(filePath, content) {
|
|
1572
|
+
try {
|
|
1573
|
+
const stat = fs.lstatSync(filePath);
|
|
1574
|
+
return stat.isFile() && !stat.isSymbolicLink() && (stat.mode & 0o111) !== 0 && safeRead(filePath) === content;
|
|
1575
|
+
} catch {
|
|
1576
|
+
return false;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
function clineNativeHookFingerprint(detection) {
|
|
1581
|
+
const hooks = clineHookContract(detection).map((hook) => ({
|
|
1582
|
+
stage: hook.stage,
|
|
1583
|
+
configured: exactExecutableFile(hook.path, hook.content),
|
|
1584
|
+
content_sha256: exactExecutableFile(hook.path, hook.content)
|
|
1585
|
+
? crypto.createHash('sha256').update(hook.content).digest('hex')
|
|
1586
|
+
: null,
|
|
1587
|
+
}));
|
|
1588
|
+
return crypto.createHash('sha256').update(JSON.stringify({
|
|
1589
|
+
schema: 'marrow-cline-native-hooks.v1',
|
|
1590
|
+
adapter_version: MCP_ADAPTER_VERSION,
|
|
1591
|
+
task_complete_support: 'coming_soon_not_configured',
|
|
1592
|
+
hooks,
|
|
1593
|
+
})).digest('hex');
|
|
1594
|
+
}
|
|
1595
|
+
|
|
885
1596
|
function activationProfile(detection, plan, changes, client) {
|
|
886
1597
|
const registry = HARNESS_CAPABILITY_REGISTRY.find((entry) => entry.client === client)
|
|
887
1598
|
|| HARNESS_CAPABILITY_REGISTRY.find((entry) => entry.client === 'custom');
|
|
@@ -899,15 +1610,78 @@ function activationProfile(detection, plan, changes, client) {
|
|
|
899
1610
|
: INSTALLER_ADAPTER_VERSION;
|
|
900
1611
|
const observedHooks = [];
|
|
901
1612
|
const claudeSettings = safeJsonObject(detection.paths.claudeSettings);
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1613
|
+
const codexSettings = safeJsonObject(detection.paths.codexHooks);
|
|
1614
|
+
const cursorSettings = safeJsonObject(detection.paths.cursorHooks);
|
|
1615
|
+
const windsurfSettings = safeJsonObject(detection.paths.windsurfHooks);
|
|
1616
|
+
const geminiSettings = safeJsonObject(detection.paths.geminiSettings);
|
|
1617
|
+
const grokSettings = safeJsonObject(detection.paths.grokHooks);
|
|
1618
|
+
if (client === 'codex') {
|
|
1619
|
+
if (exactHookConfigured(codexSettings, 'UserPromptSubmit', CODEX_CONTEXT_HOOK_COMMAND)) observedHooks.push('prompt');
|
|
1620
|
+
if (exactHookConfigured(codexSettings, 'PreToolUse', CODEX_PRE_ACTION_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER)) observedHooks.push('pre_action');
|
|
1621
|
+
if (exactHookConfigured(codexSettings, 'PostToolUse', CODEX_ACTION_RESULT_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER)) observedHooks.push('action_result');
|
|
1622
|
+
if (exactHookConfigured(codexSettings, 'SessionEnd', CODEX_SESSION_END_HOOK_COMMAND)) observedHooks.push('session_end');
|
|
1623
|
+
} else if (client === 'cursor' || client === 'composer') {
|
|
1624
|
+
if (exactCursorHookConfigured(cursorSettings, 'preToolUse', CURSOR_PRE_ACTION_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
|
|
1625
|
+
timeout: CODEX_HOOK_TIMEOUT_SECONDS, failClosed: true, async: false,
|
|
1626
|
+
})) observedHooks.push('pre_action');
|
|
1627
|
+
if (exactCursorHookConfigured(cursorSettings, 'postToolUse', CURSOR_ACTION_RESULT_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
|
|
1628
|
+
timeout: CODEX_HOOK_TIMEOUT_SECONDS,
|
|
1629
|
+
}) && exactCursorHookConfigured(cursorSettings, 'postToolUseFailure', CURSOR_ACTION_RESULT_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
|
|
1630
|
+
timeout: CODEX_HOOK_TIMEOUT_SECONDS,
|
|
1631
|
+
})) observedHooks.push('action_result');
|
|
1632
|
+
if (exactCursorHookConfigured(cursorSettings, 'stop', CURSOR_SESSION_END_HOOK_COMMAND, undefined, {
|
|
1633
|
+
timeout: CODEX_SESSION_TIMEOUT_SECONDS,
|
|
1634
|
+
})) observedHooks.push('outcome_closure');
|
|
1635
|
+
} else if (client === 'cline') {
|
|
1636
|
+
for (const hook of clineHookContract(detection)) {
|
|
1637
|
+
if (exactExecutableFile(hook.path, hook.content)) observedHooks.push(hook.stage);
|
|
1638
|
+
}
|
|
1639
|
+
} else if (client === 'windsurf') {
|
|
1640
|
+
if (WINDSURF_PRE_EVENTS.every((event) => exactWindsurfHookConfigured(
|
|
1641
|
+
windsurfSettings, event, WINDSURF_PRE_ACTION_HOOK_COMMAND,
|
|
1642
|
+
))) observedHooks.push('pre_action');
|
|
1643
|
+
if (WINDSURF_POST_EVENTS.every((event) => exactWindsurfHookConfigured(
|
|
1644
|
+
windsurfSettings, event, WINDSURF_ACTION_RESULT_HOOK_COMMAND,
|
|
1645
|
+
))) observedHooks.push('action_result');
|
|
1646
|
+
if (exactWindsurfHookConfigured(
|
|
1647
|
+
windsurfSettings, 'post_cascade_response', WINDSURF_SESSION_END_HOOK_COMMAND,
|
|
1648
|
+
)) observedHooks.push('response_closeout');
|
|
1649
|
+
} else if (client === 'gemini' && !geminiHooksExplicitlyDisabled(geminiSettings)) {
|
|
1650
|
+
if (exactGeminiHookConfigured(
|
|
1651
|
+
geminiSettings, 'BeforeTool', 'marrow-before-tool', GEMINI_PRE_ACTION_HOOK_COMMAND,
|
|
1652
|
+
GEMINI_NATIVE_HOOK_MATCHER, GEMINI_HOOK_TIMEOUT_MS,
|
|
1653
|
+
)) observedHooks.push('pre_action');
|
|
1654
|
+
if (exactGeminiHookConfigured(
|
|
1655
|
+
geminiSettings, 'AfterTool', 'marrow-after-tool', GEMINI_ACTION_RESULT_HOOK_COMMAND,
|
|
1656
|
+
GEMINI_NATIVE_HOOK_MATCHER, GEMINI_HOOK_TIMEOUT_MS,
|
|
1657
|
+
)) observedHooks.push('action_result');
|
|
1658
|
+
if (exactGeminiHookConfigured(
|
|
1659
|
+
geminiSettings, 'AfterAgent', 'marrow-after-agent', GEMINI_SESSION_END_HOOK_COMMAND,
|
|
1660
|
+
undefined, GEMINI_CLOSEOUT_TIMEOUT_MS,
|
|
1661
|
+
)) observedHooks.push('turn_closeout');
|
|
1662
|
+
} else if (client === 'grok') {
|
|
1663
|
+
if (exactGrokHookConfigured(
|
|
1664
|
+
grokSettings, 'PreToolUse', GROK_PRE_ACTION_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 7,
|
|
1665
|
+
)) observedHooks.push('pre_action');
|
|
1666
|
+
if (exactGrokHookConfigured(
|
|
1667
|
+
grokSettings, 'PostToolUse', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5,
|
|
1668
|
+
) && exactGrokHookConfigured(
|
|
1669
|
+
grokSettings, 'PostToolUseFailure', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5,
|
|
1670
|
+
)) observedHooks.push('action_result');
|
|
1671
|
+
if (exactGrokHookConfigured(
|
|
1672
|
+
grokSettings, 'Stop', GROK_SESSION_END_HOOK_COMMAND, undefined, 3,
|
|
1673
|
+
) && !grokHasDuplicateSessionEnd(grokSettings)) observedHooks.push('turn_closeout');
|
|
1674
|
+
} else {
|
|
1675
|
+
if (capabilityLevel === 'native_hooks'
|
|
1676
|
+
&& exactHookConfigured(claudeSettings, 'UserPromptSubmit', MCP_CONTEXT_HOOK_COMMAND)) observedHooks.push('prompt');
|
|
1677
|
+
if (capabilityLevel === 'native_hooks'
|
|
1678
|
+
&& exactHookConfigured(claudeSettings, 'PreToolUse', MCP_PRE_ACTION_HOOK_COMMAND, NATIVE_HOOK_MATCHER)) observedHooks.push('pre_action');
|
|
1679
|
+
if (capabilityLevel === 'native_hooks'
|
|
1680
|
+
&& exactHookConfigured(claudeSettings, 'PostToolUse', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER)
|
|
1681
|
+
&& exactHookConfigured(claudeSettings, 'PostToolUseFailure', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER)) observedHooks.push('action_result');
|
|
1682
|
+
if (capabilityLevel === 'native_hooks'
|
|
1683
|
+
&& exactHookConfigured(claudeSettings, 'Stop', MCP_SESSION_END_HOOK_COMMAND)) observedHooks.push('session_end');
|
|
1684
|
+
}
|
|
911
1685
|
const passiveRuntime = safeRead(detection.paths.passiveRuntime);
|
|
912
1686
|
if (capabilityLevel === 'sdk_passive_runtime'
|
|
913
1687
|
&& sdkDependency.present
|
|
@@ -930,17 +1704,40 @@ function activationProfile(detection, plan, changes, client) {
|
|
|
930
1704
|
.sort()
|
|
931
1705
|
.join('|');
|
|
932
1706
|
const configFingerprint = capabilityLevel === 'native_hooks'
|
|
933
|
-
?
|
|
1707
|
+
? client === 'codex' ? codexNativeHookFingerprint(codexSettings)
|
|
1708
|
+
: client === 'cursor' || client === 'composer' ? cursorNativeHookFingerprint(cursorSettings)
|
|
1709
|
+
: client === 'cline' ? clineNativeHookFingerprint(detection)
|
|
1710
|
+
: client === 'windsurf' ? windsurfNativeHookFingerprint(windsurfSettings)
|
|
1711
|
+
: client === 'gemini' ? geminiNativeHookFingerprint(geminiSettings)
|
|
1712
|
+
: client === 'grok' ? grokNativeHookFingerprint(grokSettings)
|
|
1713
|
+
: claudeNativeHookFingerprint(claudeSettings)
|
|
934
1714
|
: crypto.createHash('sha256')
|
|
935
1715
|
.update(`${client}:${capabilityLevel}:${expectedHooks.join(',')}:${fingerprintMaterial}`)
|
|
936
1716
|
.digest('hex');
|
|
937
1717
|
const complete = expectedHooks.length > 0 && expectedHooks.every((hook) => observedHooks.includes(hook));
|
|
1718
|
+
const clineConflicts = changes
|
|
1719
|
+
.filter((change) => change.hook_conflict)
|
|
1720
|
+
.map((change) => change.label);
|
|
938
1721
|
const exactFix = complete
|
|
939
|
-
?
|
|
1722
|
+
? client === 'cline'
|
|
1723
|
+
? 'Enable Hooks in Cline, trust the project hook executables and workspace, then restart Cline. TaskComplete remains unverified and is not configured.'
|
|
1724
|
+
: client === 'windsurf'
|
|
1725
|
+
? 'Restart Windsurf, trust the workspace hook configuration, and leave Restricted Mode before expecting hooks to run. MCP tools remain on demand.'
|
|
1726
|
+
: client === 'gemini'
|
|
1727
|
+
? 'Restart Gemini CLI, open /hooks panel, and review and approve the project hook fingerprints. MCP tools remain on demand.'
|
|
1728
|
+
: client === 'grok'
|
|
1729
|
+
? 'Restart Grok, inspect the installed global hooks with /hooks, and confirm they are enabled. Configuration remains client-self-reported and does not verify observed coverage.'
|
|
1730
|
+
: null
|
|
940
1731
|
: capabilityLevel === 'sdk_passive_runtime' && !sdkDependency.present
|
|
941
1732
|
? `${sdkDependency.install_command} && npx @getmarrow/install --repair`
|
|
942
1733
|
: capabilityLevel === 'governed_wrapper'
|
|
943
1734
|
? `npx @getmarrow/install run --agent <agent-id> -- ${client}`
|
|
1735
|
+
: client === 'cline' && clineConflicts.length > 0
|
|
1736
|
+
? 'Move or remove the conflicting owner-managed Cline hook file after owner review, then run npx @getmarrow/install --repair. Marrow will never overwrite or compose it.'
|
|
1737
|
+
: client === 'gemini' && geminiHooksExplicitlyDisabled(geminiSettings)
|
|
1738
|
+
? 'Hooks are explicitly disabled. After owner review, run /hooks enable-all, open /hooks panel, review and approve the project hook fingerprints, then restart Gemini CLI.'
|
|
1739
|
+
: client === 'grok'
|
|
1740
|
+
? `Run npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp setup, restart Grok, then inspect /hooks and confirm the global hooks are enabled.`
|
|
944
1741
|
: 'npx @getmarrow/install --repair';
|
|
945
1742
|
return {
|
|
946
1743
|
adapter_version: adapterVersion,
|
|
@@ -950,9 +1747,42 @@ function activationProfile(detection, plan, changes, client) {
|
|
|
950
1747
|
observed_hooks: observedHooks,
|
|
951
1748
|
evidence_authority: 'client_self_reported',
|
|
952
1749
|
coverage_verified: false,
|
|
1750
|
+
passive_live: false,
|
|
953
1751
|
configuration_complete: complete,
|
|
954
1752
|
complete,
|
|
955
1753
|
exact_fix: exactFix,
|
|
1754
|
+
...(client === 'cline' ? {
|
|
1755
|
+
hook_conflicts: clineConflicts,
|
|
1756
|
+
task_complete_support: 'coming_soon_not_configured',
|
|
1757
|
+
task_completion_closure_verified: false,
|
|
1758
|
+
enable_hooks_required: true,
|
|
1759
|
+
executable_trust_required: true,
|
|
1760
|
+
} : {}),
|
|
1761
|
+
...(client === 'windsurf' ? {
|
|
1762
|
+
restricted_mode_disables_hooks: true,
|
|
1763
|
+
restart_required: true,
|
|
1764
|
+
workspace_trust_required: true,
|
|
1765
|
+
mcp_tools: 'on_demand',
|
|
1766
|
+
} : {}),
|
|
1767
|
+
...(client === 'gemini' ? {
|
|
1768
|
+
hooks_enabled: !geminiHooksExplicitlyDisabled(geminiSettings),
|
|
1769
|
+
explicit_disable_preserved: geminiHooksExplicitlyDisabled(geminiSettings),
|
|
1770
|
+
trust_review_required: true,
|
|
1771
|
+
restart_required: true,
|
|
1772
|
+
mcp_tools: 'on_demand',
|
|
1773
|
+
session_end_delivery_claimed: false,
|
|
1774
|
+
deterministic_closeout: 'AfterAgent',
|
|
1775
|
+
} : {}),
|
|
1776
|
+
...(client === 'grok' ? {
|
|
1777
|
+
hooks_user_toggleable: true,
|
|
1778
|
+
hook_review_required: true,
|
|
1779
|
+
restart_required: true,
|
|
1780
|
+
global_hook_path: detection.paths.grokHooks,
|
|
1781
|
+
mcp_tools: 'on_demand',
|
|
1782
|
+
duplicate_session_end_configured: grokHasDuplicateSessionEnd(grokSettings),
|
|
1783
|
+
deterministic_closeout: 'Stop',
|
|
1784
|
+
governed_wrapper_fallback: 'explicit_bounded_only',
|
|
1785
|
+
} : {}),
|
|
956
1786
|
};
|
|
957
1787
|
}
|
|
958
1788
|
|
|
@@ -964,13 +1794,18 @@ function upsertMcpServerConfig(filePath, options = {}) {
|
|
|
964
1794
|
const servers = config.mcpServers && typeof config.mcpServers === 'object' && !Array.isArray(config.mcpServers)
|
|
965
1795
|
? config.mcpServers
|
|
966
1796
|
: {};
|
|
1797
|
+
const existingProfile = resolveToolProfile(servers.marrow?.env?.MARROW_TOOL_PROFILE).configured_profile;
|
|
1798
|
+
const requestedProfile = resolveToolProfile(options.toolProfile).configured_profile;
|
|
1799
|
+
const configuredProfile = requestedProfile === 'unset' ? existingProfile : requestedProfile;
|
|
1800
|
+
const env = {
|
|
1801
|
+
MARROW_BASE_URL: baseUrl,
|
|
1802
|
+
MARROW_FLEET_AGENT_ID: agentId,
|
|
1803
|
+
};
|
|
1804
|
+
if (configuredProfile !== 'unset') env.MARROW_TOOL_PROFILE = configuredProfile;
|
|
967
1805
|
servers.marrow = {
|
|
968
1806
|
command: 'npx',
|
|
969
1807
|
args: ['-y', `--package=${MCP_PACKAGE_SPEC}`, 'marrow-mcp'],
|
|
970
|
-
env
|
|
971
|
-
MARROW_BASE_URL: baseUrl,
|
|
972
|
-
MARROW_FLEET_AGENT_ID: agentId,
|
|
973
|
-
},
|
|
1808
|
+
env,
|
|
974
1809
|
};
|
|
975
1810
|
config.mcpServers = servers;
|
|
976
1811
|
return JSON.stringify(config, null, 2) + '\n';
|
|
@@ -1075,7 +1910,74 @@ function defaultHarnessInstallMatrix(detection = detectEnvironment(process.cwd()
|
|
|
1075
1910
|
const detected = detectedClient(detection) === entry.client
|
|
1076
1911
|
|| (entry.client === 'claude-code' && detection.claudeCode)
|
|
1077
1912
|
|| (entry.client === 'cursor' && detection.cursor)
|
|
1913
|
+
|| (entry.client === 'cline' && detection.cline)
|
|
1914
|
+
|| (entry.client === 'windsurf' && detection.windsurf)
|
|
1915
|
+
|| (entry.client === 'gemini' && detection.gemini)
|
|
1916
|
+
|| (entry.client === 'grok' && detection.grok)
|
|
1078
1917
|
|| (entry.client === 'codex' && detection.codex);
|
|
1918
|
+
const codexSettings = entry.client === 'codex' ? safeJsonObject(detection.paths.codexHooks) : null;
|
|
1919
|
+
const codexConfigured = Boolean(codexSettings
|
|
1920
|
+
&& exactHookConfigured(codexSettings, 'UserPromptSubmit', CODEX_CONTEXT_HOOK_COMMAND)
|
|
1921
|
+
&& exactHookConfigured(codexSettings, 'PreToolUse', CODEX_PRE_ACTION_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER)
|
|
1922
|
+
&& exactHookConfigured(codexSettings, 'PostToolUse', CODEX_ACTION_RESULT_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER)
|
|
1923
|
+
&& exactHookConfigured(codexSettings, 'SessionEnd', CODEX_SESSION_END_HOOK_COMMAND));
|
|
1924
|
+
const cursorSettings = ['cursor', 'composer'].includes(entry.client) ? safeJsonObject(detection.paths.cursorHooks) : null;
|
|
1925
|
+
const cursorConfigured = Boolean(cursorSettings
|
|
1926
|
+
&& exactCursorHookConfigured(cursorSettings, 'preToolUse', CURSOR_PRE_ACTION_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
|
|
1927
|
+
timeout: CODEX_HOOK_TIMEOUT_SECONDS, failClosed: true, async: false,
|
|
1928
|
+
})
|
|
1929
|
+
&& exactCursorHookConfigured(cursorSettings, 'postToolUse', CURSOR_ACTION_RESULT_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
|
|
1930
|
+
timeout: CODEX_HOOK_TIMEOUT_SECONDS,
|
|
1931
|
+
})
|
|
1932
|
+
&& exactCursorHookConfigured(cursorSettings, 'postToolUseFailure', CURSOR_ACTION_RESULT_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
|
|
1933
|
+
timeout: CODEX_HOOK_TIMEOUT_SECONDS,
|
|
1934
|
+
})
|
|
1935
|
+
&& exactCursorHookConfigured(cursorSettings, 'stop', CURSOR_SESSION_END_HOOK_COMMAND, undefined, {
|
|
1936
|
+
timeout: CODEX_SESSION_TIMEOUT_SECONDS,
|
|
1937
|
+
}));
|
|
1938
|
+
const clineConfigured = entry.client === 'cline'
|
|
1939
|
+
&& clineHookContract(detection).every((hook) => exactExecutableFile(hook.path, hook.content));
|
|
1940
|
+
const windsurfSettings = entry.client === 'windsurf' ? safeJsonObject(detection.paths.windsurfHooks) : null;
|
|
1941
|
+
const windsurfConfigured = Boolean(windsurfSettings
|
|
1942
|
+
&& WINDSURF_PRE_EVENTS.every((event) => exactWindsurfHookConfigured(
|
|
1943
|
+
windsurfSettings, event, WINDSURF_PRE_ACTION_HOOK_COMMAND,
|
|
1944
|
+
))
|
|
1945
|
+
&& WINDSURF_POST_EVENTS.every((event) => exactWindsurfHookConfigured(
|
|
1946
|
+
windsurfSettings, event, WINDSURF_ACTION_RESULT_HOOK_COMMAND,
|
|
1947
|
+
))
|
|
1948
|
+
&& exactWindsurfHookConfigured(
|
|
1949
|
+
windsurfSettings, 'post_cascade_response', WINDSURF_SESSION_END_HOOK_COMMAND,
|
|
1950
|
+
));
|
|
1951
|
+
const geminiSettings = entry.client === 'gemini' ? safeJsonObject(detection.paths.geminiSettings) : null;
|
|
1952
|
+
const geminiConfigured = Boolean(geminiSettings
|
|
1953
|
+
&& !geminiHooksExplicitlyDisabled(geminiSettings)
|
|
1954
|
+
&& exactGeminiHookConfigured(
|
|
1955
|
+
geminiSettings, 'BeforeTool', 'marrow-before-tool', GEMINI_PRE_ACTION_HOOK_COMMAND,
|
|
1956
|
+
GEMINI_NATIVE_HOOK_MATCHER, GEMINI_HOOK_TIMEOUT_MS,
|
|
1957
|
+
)
|
|
1958
|
+
&& exactGeminiHookConfigured(
|
|
1959
|
+
geminiSettings, 'AfterTool', 'marrow-after-tool', GEMINI_ACTION_RESULT_HOOK_COMMAND,
|
|
1960
|
+
GEMINI_NATIVE_HOOK_MATCHER, GEMINI_HOOK_TIMEOUT_MS,
|
|
1961
|
+
)
|
|
1962
|
+
&& exactGeminiHookConfigured(
|
|
1963
|
+
geminiSettings, 'AfterAgent', 'marrow-after-agent', GEMINI_SESSION_END_HOOK_COMMAND,
|
|
1964
|
+
undefined, GEMINI_CLOSEOUT_TIMEOUT_MS,
|
|
1965
|
+
));
|
|
1966
|
+
const grokSettings = entry.client === 'grok' ? safeJsonObject(detection.paths.grokHooks) : null;
|
|
1967
|
+
const grokConfigured = Boolean(grokSettings
|
|
1968
|
+
&& exactGrokHookConfigured(
|
|
1969
|
+
grokSettings, 'PreToolUse', GROK_PRE_ACTION_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 7,
|
|
1970
|
+
)
|
|
1971
|
+
&& exactGrokHookConfigured(
|
|
1972
|
+
grokSettings, 'PostToolUse', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5,
|
|
1973
|
+
)
|
|
1974
|
+
&& exactGrokHookConfigured(
|
|
1975
|
+
grokSettings, 'PostToolUseFailure', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5,
|
|
1976
|
+
)
|
|
1977
|
+
&& exactGrokHookConfigured(
|
|
1978
|
+
grokSettings, 'Stop', GROK_SESSION_END_HOOK_COMMAND, undefined, 3,
|
|
1979
|
+
)
|
|
1980
|
+
&& !grokHasDuplicateSessionEnd(grokSettings));
|
|
1079
1981
|
return {
|
|
1080
1982
|
client: entry.client,
|
|
1081
1983
|
capability_level: entry.capability_level,
|
|
@@ -1085,12 +1987,35 @@ function defaultHarnessInstallMatrix(detection = detectEnvironment(process.cwd()
|
|
|
1085
1987
|
mcp: true,
|
|
1086
1988
|
instructions: true,
|
|
1087
1989
|
sdk_passive_runtime: node,
|
|
1088
|
-
native_hooks: entry.capability_level === 'native_hooks' && Boolean(
|
|
1990
|
+
native_hooks: entry.capability_level === 'native_hooks' && Boolean(
|
|
1991
|
+
entry.client === 'claude-code' ? detection.claudeCode
|
|
1992
|
+
: entry.client === 'codex' ? detection.codex
|
|
1993
|
+
: ['cursor', 'composer'].includes(entry.client) ? detection.cursor
|
|
1994
|
+
: entry.client === 'cline' ? detection.cline
|
|
1995
|
+
: entry.client === 'windsurf' ? detection.windsurf
|
|
1996
|
+
: entry.client === 'gemini' ? detection.gemini
|
|
1997
|
+
: entry.client === 'grok' ? detection.grok
|
|
1998
|
+
: false,
|
|
1999
|
+
),
|
|
1089
2000
|
governed_wrapper: entry.capability_level === 'governed_wrapper',
|
|
1090
2001
|
},
|
|
1091
|
-
configured_locally:
|
|
2002
|
+
configured_locally: entry.client === 'codex' ? codexConfigured
|
|
2003
|
+
: ['cursor', 'composer'].includes(entry.client) ? cursorConfigured
|
|
2004
|
+
: entry.client === 'cline' ? clineConfigured
|
|
2005
|
+
: entry.client === 'windsurf' ? windsurfConfigured
|
|
2006
|
+
: entry.client === 'gemini' ? geminiConfigured
|
|
2007
|
+
: entry.client === 'grok' ? grokConfigured
|
|
2008
|
+
: detected && entry.automatic.length > 0,
|
|
1092
2009
|
verified_passive: false,
|
|
1093
|
-
unsupported_claim: entry.
|
|
2010
|
+
unsupported_claim: entry.client === 'cline'
|
|
2011
|
+
? 'TaskComplete is documented as coming soon and is not configured or counted as observed coverage.'
|
|
2012
|
+
: entry.client === 'windsurf'
|
|
2013
|
+
? 'Restricted Mode disables hooks; configuration requires restart and trust review and never verifies passive coverage.'
|
|
2014
|
+
: entry.client === 'gemini'
|
|
2015
|
+
? 'Project hooks require restart and project fingerprint review and approval in /hooks panel; explicit hooksConfig.enabled=false is preserved and SessionEnd delivery is not claimed.'
|
|
2016
|
+
: entry.client === 'grok'
|
|
2017
|
+
? 'Global hooks are user-toggleable; restart and /hooks inspection are required, configuration remains client-self-reported, and duplicate SessionEnd closeout is forbidden.'
|
|
2018
|
+
: entry.capability_level === 'event_contract'
|
|
1094
2019
|
? 'Needs a bounded event adapter. MCP tools remain on demand.'
|
|
1095
2020
|
: null,
|
|
1096
2021
|
};
|
|
@@ -1131,18 +2056,60 @@ function buildPlan(detection, options) {
|
|
|
1131
2056
|
transform: upsertClaudeHooks,
|
|
1132
2057
|
});
|
|
1133
2058
|
}
|
|
2059
|
+
if (detection.codex) {
|
|
2060
|
+
writes.push({
|
|
2061
|
+
type: 'json-transform',
|
|
2062
|
+
path: detection.paths.codexHooks,
|
|
2063
|
+
label: 'Codex native hooks',
|
|
2064
|
+
transform: upsertCodexHooks,
|
|
2065
|
+
});
|
|
2066
|
+
}
|
|
2067
|
+
if (detection.cline) {
|
|
2068
|
+
for (const hook of clineHookContract(detection)) {
|
|
2069
|
+
writes.push({
|
|
2070
|
+
type: 'owned-executable',
|
|
2071
|
+
path: hook.path,
|
|
2072
|
+
label: hook.label,
|
|
2073
|
+
content: hook.content,
|
|
2074
|
+
mode: 0o755,
|
|
2075
|
+
conflict_fix: 'Move or remove the existing owner-managed Cline hook after owner review, then run npx @getmarrow/install --repair.',
|
|
2076
|
+
});
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
if (detection.windsurf) {
|
|
2080
|
+
writes.push({
|
|
2081
|
+
type: 'json-transform',
|
|
2082
|
+
path: detection.paths.windsurfHooks,
|
|
2083
|
+
label: 'Windsurf native hooks',
|
|
2084
|
+
transform: upsertWindsurfHooks,
|
|
2085
|
+
});
|
|
2086
|
+
}
|
|
2087
|
+
if (detection.gemini) {
|
|
2088
|
+
writes.push({
|
|
2089
|
+
type: 'json-transform',
|
|
2090
|
+
path: detection.paths.geminiSettings,
|
|
2091
|
+
label: 'Gemini CLI native hooks',
|
|
2092
|
+
transform: upsertGeminiHooks,
|
|
2093
|
+
});
|
|
2094
|
+
}
|
|
1134
2095
|
writes.push({
|
|
1135
2096
|
type: 'json-transform',
|
|
1136
2097
|
path: detection.paths.mcpJson,
|
|
1137
2098
|
label: 'Project MCP server config',
|
|
1138
|
-
transform: (filePath) => upsertMcpServerConfig(filePath, { agentId, baseUrl }),
|
|
2099
|
+
transform: (filePath) => upsertMcpServerConfig(filePath, { agentId, baseUrl, toolProfile: options.toolProfile }),
|
|
1139
2100
|
});
|
|
1140
2101
|
if (detection.cursor) {
|
|
2102
|
+
writes.push({
|
|
2103
|
+
type: 'json-transform',
|
|
2104
|
+
path: detection.paths.cursorHooks,
|
|
2105
|
+
label: 'Cursor native hooks',
|
|
2106
|
+
transform: upsertCursorHooks,
|
|
2107
|
+
});
|
|
1141
2108
|
writes.push({
|
|
1142
2109
|
type: 'json-transform',
|
|
1143
2110
|
path: detection.paths.cursorMcp,
|
|
1144
2111
|
label: 'Cursor MCP server config',
|
|
1145
|
-
transform: (filePath) => upsertMcpServerConfig(filePath, { agentId, baseUrl }),
|
|
2112
|
+
transform: (filePath) => upsertMcpServerConfig(filePath, { agentId, baseUrl, toolProfile: options.toolProfile }),
|
|
1146
2113
|
});
|
|
1147
2114
|
}
|
|
1148
2115
|
}
|
|
@@ -1229,8 +2196,10 @@ function applyPlan(plan, options) {
|
|
|
1229
2196
|
const root = path.resolve(plan.root || path.dirname(plan.writes[0].path));
|
|
1230
2197
|
for (const write of plan.writes) assertContainedManagedTarget(root, write.path);
|
|
1231
2198
|
const prepared = plan.writes.map((write) => {
|
|
2199
|
+
const fileExists = exists(write.path);
|
|
1232
2200
|
const before = safeRead(write.path);
|
|
1233
2201
|
let after;
|
|
2202
|
+
let hookConflict = false;
|
|
1234
2203
|
if (write.type === 'file') {
|
|
1235
2204
|
if (write.overwrite === false && before) {
|
|
1236
2205
|
after = before;
|
|
@@ -1241,27 +2210,43 @@ function applyPlan(plan, options) {
|
|
|
1241
2210
|
after = upsertBlock(before, write.block);
|
|
1242
2211
|
} else if (write.type === 'json-transform') {
|
|
1243
2212
|
after = write.transform(write.path);
|
|
2213
|
+
} else if (write.type === 'owned-executable') {
|
|
2214
|
+
if (fileExists && before !== write.content) {
|
|
2215
|
+
after = before;
|
|
2216
|
+
hookConflict = true;
|
|
2217
|
+
} else {
|
|
2218
|
+
after = write.content;
|
|
2219
|
+
}
|
|
1244
2220
|
} else {
|
|
1245
2221
|
throw new Error(`Unknown write type: ${write.type}`);
|
|
1246
2222
|
}
|
|
1247
2223
|
|
|
1248
|
-
|
|
2224
|
+
const beforeMode = fileExists ? fs.lstatSync(write.path).mode & 0o777 : null;
|
|
2225
|
+
const modeChanged = !hookConflict && typeof write.mode === 'number' && beforeMode !== write.mode;
|
|
2226
|
+
return { write, before, after, hookConflict, modeChanged };
|
|
1249
2227
|
});
|
|
1250
2228
|
|
|
1251
2229
|
const changes = [];
|
|
1252
|
-
for (const { write, before, after } of prepared) {
|
|
1253
|
-
const
|
|
1254
|
-
const
|
|
2230
|
+
for (const { write, before, after, hookConflict, modeChanged } of prepared) {
|
|
2231
|
+
const contentChanged = before !== after;
|
|
2232
|
+
const changed = !hookConflict && (contentChanged || modeChanged);
|
|
2233
|
+
const writeApplied = Boolean(options.yes && !options.dryRun && !options.doctor && !hookConflict);
|
|
1255
2234
|
changes.push({
|
|
1256
2235
|
path: write.path,
|
|
1257
2236
|
label: write.label,
|
|
1258
2237
|
changed,
|
|
1259
2238
|
applied: changed && writeApplied,
|
|
1260
|
-
already_present: !changed,
|
|
2239
|
+
already_present: !changed && !hookConflict,
|
|
2240
|
+
hook_conflict: hookConflict,
|
|
2241
|
+
...(hookConflict ? { exact_fix: write.conflict_fix } : {}),
|
|
1261
2242
|
});
|
|
1262
|
-
if (
|
|
2243
|
+
if (contentChanged && writeApplied) {
|
|
1263
2244
|
atomicWriteManagedFile(root, write.path, after);
|
|
1264
2245
|
}
|
|
2246
|
+
if (modeChanged && writeApplied) {
|
|
2247
|
+
assertContainedManagedTarget(root, write.path);
|
|
2248
|
+
fs.chmodSync(write.path, write.mode);
|
|
2249
|
+
}
|
|
1265
2250
|
}
|
|
1266
2251
|
return changes;
|
|
1267
2252
|
}
|
|
@@ -1298,12 +2283,14 @@ function runtimeGateVerified(runtime) {
|
|
|
1298
2283
|
}
|
|
1299
2284
|
|
|
1300
2285
|
async function runSelfTest(options) {
|
|
1301
|
-
|
|
2286
|
+
const initialProfile = initialToolProfileReport(options.toolProfile);
|
|
2287
|
+
if (!options.selfTest) return { skipped: true, reason: 'disabled', mcp_tool_profile: initialProfile };
|
|
1302
2288
|
if (!options.apiKey) {
|
|
1303
2289
|
return {
|
|
1304
2290
|
skipped: true,
|
|
1305
2291
|
reason: 'missing MARROW_API_KEY',
|
|
1306
2292
|
exact_fix: 'export MARROW_API_KEY=mrw_live_... && npx @getmarrow/install --repair',
|
|
2293
|
+
mcp_tool_profile: initialProfile,
|
|
1307
2294
|
};
|
|
1308
2295
|
}
|
|
1309
2296
|
|
|
@@ -1349,6 +2336,14 @@ async function runSelfTest(options) {
|
|
|
1349
2336
|
});
|
|
1350
2337
|
|
|
1351
2338
|
const status = await requestJson(`${baseUrl}/v1/agent/status`, { headers });
|
|
2339
|
+
const context = await requestJson(`${baseUrl}/v1/agent/context`, { headers })
|
|
2340
|
+
.catch(() => null);
|
|
2341
|
+
const toolProfile = buildMcpToolProfileReport(
|
|
2342
|
+
options.toolProfile,
|
|
2343
|
+
status.mcp_tool_profile,
|
|
2344
|
+
context?.primary_tool_availability,
|
|
2345
|
+
Boolean(options.activation),
|
|
2346
|
+
);
|
|
1352
2347
|
const runtime = await requestJson(`${baseUrl}/v1/agent/runtime`, {
|
|
1353
2348
|
method: 'POST',
|
|
1354
2349
|
headers,
|
|
@@ -1483,6 +2478,7 @@ async function runSelfTest(options) {
|
|
|
1483
2478
|
}
|
|
1484
2479
|
return {
|
|
1485
2480
|
skipped: false,
|
|
2481
|
+
mcp_tool_profile: toolProfile,
|
|
1486
2482
|
decision_id: decisionId,
|
|
1487
2483
|
active: Boolean(status.enabled ?? status.ok),
|
|
1488
2484
|
health: status.health || null,
|
|
@@ -1677,6 +2673,24 @@ function printReport(report) {
|
|
|
1677
2673
|
}
|
|
1678
2674
|
|
|
1679
2675
|
process.stdout.write('\nSelf-test:\n');
|
|
2676
|
+
const toolProfile = report.selfTest.mcp_tool_profile || report.toolProfile;
|
|
2677
|
+
if (toolProfile) {
|
|
2678
|
+
process.stdout.write(`- configured tool profile: ${toolProfile.configured_profile}\n`);
|
|
2679
|
+
process.stdout.write(`- effective tool profile: ${toolProfile.effective_profile}\n`);
|
|
2680
|
+
process.stdout.write(`- expected visible tools: ${toolProfile.expected_visible_count == null ? 'complete catalog (awaiting reloaded MCP count)' : toolProfile.expected_visible_count}\n`);
|
|
2681
|
+
process.stdout.write(`- actual visible tools: ${toolProfile.actual_visible_count == null ? 'unavailable until process reload' : toolProfile.actual_visible_count}\n`);
|
|
2682
|
+
process.stdout.write(`- visible tool names: ${toolProfile.visibility_live ? toolProfile.visible_tool_names.join(', ') : 'unavailable until process reload'}\n`);
|
|
2683
|
+
process.stdout.write(`- profile live: ${toolProfile.visibility_live ? 'yes' : 'no'}\n`);
|
|
2684
|
+
const projection = toolProfile.backend_entitlement_projection;
|
|
2685
|
+
const availability = projection?.primary_tool_availability;
|
|
2686
|
+
if (projection?.evidence_state === 'available' && availability?.entitlement_evidence?.state === 'available') {
|
|
2687
|
+
process.stdout.write(`- backend-projected entitled tools: ${availability.counts.entitled}\n`);
|
|
2688
|
+
process.stdout.write(`- backend-projected upgrade-required tools: ${availability.counts.upgrade_required}\n`);
|
|
2689
|
+
process.stdout.write(`- backend projection source: ${projection.source}; authorizes calls: no\n`);
|
|
2690
|
+
} else {
|
|
2691
|
+
process.stdout.write(`- backend-projected entitlements: unavailable (source: ${projection?.source || 'backend_projection_not_provided'}; non-authorizing)\n`);
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
1680
2694
|
if (report.selfTest.skipped) {
|
|
1681
2695
|
process.stdout.write(`- skipped: ${report.selfTest.reason}\n`);
|
|
1682
2696
|
if (report.selfTest.exact_fix) process.stdout.write(`- exact fix: ${report.selfTest.exact_fix}\n`);
|
|
@@ -1836,9 +2850,15 @@ async function install(options) {
|
|
|
1836
2850
|
if (options.activate && options.selfTest === false) {
|
|
1837
2851
|
throw new Error('activate requires the server self-test');
|
|
1838
2852
|
}
|
|
2853
|
+
if (options.activate && !String(options.apiKey || '').trim()) {
|
|
2854
|
+
throw new Error('activation requires MARROW_API_KEY from the process environment or trusted secret storage');
|
|
2855
|
+
}
|
|
1839
2856
|
if (options.repair && options.yes !== true && !options.dryRun && !options.doctor) {
|
|
1840
2857
|
throw new Error('repair requires explicit write authorization (--yes)');
|
|
1841
2858
|
}
|
|
2859
|
+
options.toolProfile = resolveToolProfile(options.toolProfile === undefined
|
|
2860
|
+
? process.env.MARROW_TOOL_PROFILE
|
|
2861
|
+
: options.toolProfile);
|
|
1842
2862
|
const detection = detectEnvironment(options.cwd);
|
|
1843
2863
|
const client = detectedClient(detection);
|
|
1844
2864
|
options.client = client;
|
|
@@ -1860,6 +2880,7 @@ async function install(options) {
|
|
|
1860
2880
|
configuration_complete: changes.every((change) => change.applied || change.already_present),
|
|
1861
2881
|
evidence_authority: 'client_self_reported',
|
|
1862
2882
|
coverage_verified: false,
|
|
2883
|
+
passive_live: false,
|
|
1863
2884
|
adapter_version: profile.adapter_version,
|
|
1864
2885
|
capability_level: profile.capability_level,
|
|
1865
2886
|
config_fingerprint: profile.config_fingerprint,
|
|
@@ -1884,20 +2905,38 @@ async function install(options) {
|
|
|
1884
2905
|
} catch (error) {
|
|
1885
2906
|
const message = error instanceof Error ? error.message : String(error);
|
|
1886
2907
|
if (options.activate) throw new Error(`Marrow activation failed: ${message}`);
|
|
1887
|
-
selfTest = {
|
|
2908
|
+
selfTest = {
|
|
2909
|
+
skipped: false,
|
|
2910
|
+
active: false,
|
|
2911
|
+
error: message,
|
|
2912
|
+
mcp_tool_profile: initialToolProfileReport(options.toolProfile),
|
|
2913
|
+
};
|
|
1888
2914
|
}
|
|
1889
2915
|
if (options.activate && !selfTest.activation_verified) {
|
|
1890
2916
|
throw new Error('Marrow activation failed: server confirmation was not returned');
|
|
1891
2917
|
}
|
|
2918
|
+
const harnessReload = harnessReloadPlan(detection, changes);
|
|
2919
|
+
if (harnessReload.required && selfTest.mcp_tool_profile) {
|
|
2920
|
+
selfTest.mcp_tool_profile = {
|
|
2921
|
+
...selfTest.mcp_tool_profile,
|
|
2922
|
+
visible_tool_count: null,
|
|
2923
|
+
actual_visible_count: null,
|
|
2924
|
+
visible_tool_names: [],
|
|
2925
|
+
visibility_live: false,
|
|
2926
|
+
reload_required: true,
|
|
2927
|
+
};
|
|
2928
|
+
}
|
|
1892
2929
|
const changedConfig = changes.some((change) => change.applied) || configRepairs.some((repair) => repair.changed);
|
|
1893
2930
|
const selfTestPassed = Boolean(!selfTest.skipped && selfTest.active && !selfTest.error);
|
|
1894
2931
|
const controllerPlatform = options.controllerPlatform || process.platform;
|
|
2932
|
+
const localControl = localControlEvidence({ apiKey: options.apiKey, home: options.controlHome });
|
|
1895
2933
|
let controller = await controllerStatus({
|
|
1896
2934
|
root: detection.root,
|
|
1897
2935
|
agentId: options.agentId,
|
|
1898
2936
|
platform: controllerPlatform,
|
|
1899
2937
|
});
|
|
1900
2938
|
const shouldEnsureController = options.controller !== false
|
|
2939
|
+
&& localControl.enabled
|
|
1901
2940
|
&& controllerSupportedPlatform(controllerPlatform)
|
|
1902
2941
|
&& Boolean(options.apiKey)
|
|
1903
2942
|
&& selfTestPassed
|
|
@@ -1948,11 +2987,13 @@ async function install(options) {
|
|
|
1948
2987
|
adapterProvenance: ADAPTER_PROVENANCE,
|
|
1949
2988
|
mode: plan.mode,
|
|
1950
2989
|
writeMode,
|
|
2990
|
+
toolProfile: selfTest.mcp_tool_profile || initialToolProfileReport(options.toolProfile),
|
|
1951
2991
|
detected: {
|
|
1952
2992
|
node: detection.node,
|
|
1953
2993
|
python: detection.python,
|
|
1954
2994
|
claudeCode: detection.claudeCode,
|
|
1955
2995
|
cursor: detection.cursor,
|
|
2996
|
+
grok: detection.grok,
|
|
1956
2997
|
codex: detection.codex,
|
|
1957
2998
|
openclaw: detection.openclaw,
|
|
1958
2999
|
mcpConfig: detection.mcpConfig,
|
|
@@ -1969,7 +3010,7 @@ async function install(options) {
|
|
|
1969
3010
|
receipt: selfTest.activation_receipt || null,
|
|
1970
3011
|
profile,
|
|
1971
3012
|
},
|
|
1972
|
-
harnessReload
|
|
3013
|
+
harnessReload,
|
|
1973
3014
|
firstCapture: firstCapturePath(detection, options.agentId),
|
|
1974
3015
|
changes,
|
|
1975
3016
|
doctor: {
|
|
@@ -1991,6 +3032,7 @@ async function install(options) {
|
|
|
1991
3032
|
configRepairs,
|
|
1992
3033
|
sdkDependency,
|
|
1993
3034
|
controller,
|
|
3035
|
+
local_control: localControl,
|
|
1994
3036
|
selfTest,
|
|
1995
3037
|
warnings: [
|
|
1996
3038
|
...(options.keyFromArg
|
|
@@ -2038,7 +3080,21 @@ module.exports = {
|
|
|
2038
3080
|
stableAgentId,
|
|
2039
3081
|
activationProfile,
|
|
2040
3082
|
claudeNativeHookFingerprint,
|
|
3083
|
+
codexNativeHookFingerprint,
|
|
3084
|
+
cursorNativeHookFingerprint,
|
|
3085
|
+
clineNativeHookFingerprint,
|
|
3086
|
+
windsurfNativeHookFingerprint,
|
|
3087
|
+
geminiNativeHookFingerprint,
|
|
3088
|
+
grokNativeHookFingerprint,
|
|
3089
|
+
GROK_CONTEXT_HOOK_COMMAND,
|
|
3090
|
+
GROK_PRE_ACTION_HOOK_COMMAND,
|
|
3091
|
+
GROK_ACTION_RESULT_HOOK_COMMAND,
|
|
3092
|
+
GROK_SESSION_END_HOOK_COMMAND,
|
|
3093
|
+
GROK_NATIVE_HOOK_MATCHER,
|
|
2041
3094
|
printReport,
|
|
3095
|
+
buildMcpToolProfileReport,
|
|
3096
|
+
resolveToolProfile,
|
|
3097
|
+
PRIMARY_TOOL_NAMES,
|
|
2042
3098
|
ADAPTER_PROVENANCE,
|
|
2043
3099
|
HARNESS_CAPABILITY_REGISTRY,
|
|
2044
3100
|
defaultHarnessInstallMatrix,
|