@getmarrow/install 0.1.53 → 0.1.55
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 +15 -1
- package/package.json +1 -1
- package/src/installer.js +613 -80
package/README.md
CHANGED
|
@@ -64,6 +64,12 @@ export MARROW_API_KEY=mrw_live_...
|
|
|
64
64
|
|
|
65
65
|
The bare command is the default-on path: it detects and byte-idempotently writes managed configuration, runs the authenticated activation self-test, and starts the supported persistent controller. The key stays process-only and is never written to generated configuration or controller state. Use `--dry-run` for a non-writing preview, `doctor` for a read-only health check, or `--no-controller` to install and self-test without starting the controller. The explicit `activate` command remains equivalent and supported.
|
|
66
66
|
|
|
67
|
+
### MCP tool profiles
|
|
68
|
+
|
|
69
|
+
Ordinary setup leaves `MARROW_TOOL_PROFILE` unset, which selects the documented 17-tool `primary` surface. Set `MARROW_TOOL_PROFILE=core` only for the legacy seven-tool minimal surface, or `MARROW_TOOL_PROFILE=full` for the complete advanced/legacy catalog. Explicit `primary`, `core`, and `full` values are accepted; any other value fails with an exact bounded repair and never falls back to a broader profile.
|
|
70
|
+
|
|
71
|
+
Tool visibility is not authorization. Every visible call still reaches Marrow's backend authentication, tenant, key-permission, plan, proof, and policy enforcement. `doctor --self-test` reports the configured and effective profile, the expected visible count, reloaded MCP visible names/count, and non-authorizing backend entitlement/upgrade projection. Until the owning harness restarts and matching MCP status is observed, actual visibility stays unavailable and the profile remains not-live. The self-test reads backend availability status; it does not invoke paid write tools to discover access.
|
|
72
|
+
|
|
67
73
|
## Keeping Marrow Current
|
|
68
74
|
|
|
69
75
|
Marrow's hosted API, website, and dashboard update automatically; local SDK dependencies, generated runtime files, MCP hooks/configuration, and pinned package versions do not silently rewrite themselves. Keeping them current delivers new client-side features, compatibility improvements, and any published security fixes. Supported clients report their package version during authenticated status/runtime activity, and Marrow returns a `client_update` notice with the exact action when the version is behind or unknown.
|
|
@@ -93,7 +99,15 @@ npx @getmarrow/install controller stop
|
|
|
93
99
|
|
|
94
100
|
Persistent controller lifecycle is currently Linux-only. On macOS or Windows, activation still writes supported configuration and verifies one server-side install self-test without certifying that hooks continuously ran; run `npx @getmarrow/install sidecar` under an owner-managed service and pass `--no-controller`. The controller does not silently upgrade packages, change governance policy, rotate credentials, or modify unrelated project configuration.
|
|
95
101
|
|
|
96
|
-
## What's New in v0.1.
|
|
102
|
+
## What's New in v0.1.55
|
|
103
|
+
|
|
104
|
+
v0.1.55 pins sealed MCP candidate `3.9.79` from source `11f00049043d0aba90704ecbf69f32d2278a4573` for ordinary setup, generated launch configuration, and native hooks. Doctor, update, and repair retain exact-version resolution and never propagate a newer local version unless official registry metadata verifies it; offline operation preserves unverified-ahead owner surfaces and uses the sealed candidate for new managed targets.
|
|
105
|
+
|
|
106
|
+
## Previous: v0.1.54
|
|
107
|
+
|
|
108
|
+
v0.1.54 pins sealed MCP candidate `3.9.77` from source `1e782d8ba6bbb54bfaa322f0300565c7176f1969` and makes ordinary setup use the primary tool surface without writing a profile variable. Explicit `core` and `full` selections remain preserved, invalid values fail closed with a bounded repair, and human/JSON self-test output distinguishes configured/effective profile, expected visibility, actual post-reload visibility, and backend-projected entitlement state. Backend projections are status evidence only and always report `authorizes_calls: false`.
|
|
109
|
+
|
|
110
|
+
## Previous: v0.1.53
|
|
97
111
|
|
|
98
112
|
v0.1.53 pins the native-gate MCP candidate `3.9.75` and reconciles only Marrow-owned native hook surfaces. Codex uses `.codex/hooks.json`; Cursor and Composer use `.cursor/hooks.json`; Cline uses bounded executables under `.clinerules/hooks/`; Windsurf uses `.windsurf/hooks.json`; Gemini CLI receives named BeforeTool, AfterTool, and AfterAgent groups in `.gemini/settings.json`; Grok receives trusted global hooks in `~/.grok/hooks/marrow.json`. Grok PreToolUse validates strict private allow/deny JSON and fails closed with exit `2` when the child cannot provide an exact decision; PostToolUse/PostToolUseFailure emit compact results; one nonblocking Stop hook closes the turn with no duplicate SessionEnd hook. Grok hooks remain user-toggleable, so restart plus `/hooks` inspection is required and configuration never proves observed coverage.
|
|
99
113
|
|
package/package.json
CHANGED
package/src/installer.js
CHANGED
|
@@ -10,8 +10,9 @@ const { evidence: localControlEvidence } = require('./control-state');
|
|
|
10
10
|
const DEFAULT_BASE_URL = 'https://api.getmarrow.ai';
|
|
11
11
|
const MARROW_BLOCK_START = '<!-- marrow:passive-start -->';
|
|
12
12
|
const MARROW_BLOCK_END = '<!-- marrow:passive-end -->';
|
|
13
|
-
const MCP_ADAPTER_VERSION = '3.9.
|
|
14
|
-
const MCP_ADAPTER_SOURCE_SHA = '
|
|
13
|
+
const MCP_ADAPTER_VERSION = '3.9.79';
|
|
14
|
+
const MCP_ADAPTER_SOURCE_SHA = '11f00049043d0aba90704ecbf69f32d2278a4573';
|
|
15
|
+
const MCP_ADAPTER_INTEGRITY = 'sha512-LWfBWGot2PnluRbS5Hm3WA2DJCIkf2qVJfthGcCaZx8AoRXFKeg1pUO3rMVFqrOIWOB4nYZw6UgSL+7hDY1wuA==';
|
|
15
16
|
const SDK_ADAPTER_VERSION = '3.7.62';
|
|
16
17
|
const SDK_ADAPTER_INTEGRITY = 'sha512-n1i6Be09TpAQ9BPNRKY7aCvA2iSUPpJfw8djw2MELwpNbBCtKiZ29Jji77BK/6EFLUpSIcTW/Gmdf/ccf0JRYQ==';
|
|
17
18
|
const SDK_ADAPTER_TARBALL = `https://registry.npmjs.org/@getmarrow/sdk/-/sdk-${SDK_ADAPTER_VERSION}.tgz`;
|
|
@@ -21,8 +22,8 @@ const ADAPTER_PROVENANCE = Object.freeze({
|
|
|
21
22
|
package: '@getmarrow/mcp',
|
|
22
23
|
version: MCP_ADAPTER_VERSION,
|
|
23
24
|
source_sha: MCP_ADAPTER_SOURCE_SHA,
|
|
24
|
-
integrity:
|
|
25
|
-
integrity_state: '
|
|
25
|
+
integrity: MCP_ADAPTER_INTEGRITY,
|
|
26
|
+
integrity_state: 'sealed_local_candidate',
|
|
26
27
|
}),
|
|
27
28
|
sdk: Object.freeze({
|
|
28
29
|
package: '@getmarrow/sdk',
|
|
@@ -30,6 +31,184 @@ const ADAPTER_PROVENANCE = Object.freeze({
|
|
|
30
31
|
integrity: SDK_ADAPTER_INTEGRITY,
|
|
31
32
|
}),
|
|
32
33
|
});
|
|
34
|
+
const MCP_REGISTRY_LATEST_URL = 'https://registry.npmjs.org/%40getmarrow%2Fmcp/latest';
|
|
35
|
+
const MCP_REGISTRY_VERIFICATION_COMMAND = 'npm view @getmarrow/mcp@latest name version dist.integrity dist.tarball --json --registry=https://registry.npmjs.org';
|
|
36
|
+
const MCP_STABLE_VERSION_RE = /^(\d{1,6})\.(\d{1,6})\.(\d{1,9})$/;
|
|
37
|
+
const SHA512_INTEGRITY_RE = /^sha512-[A-Za-z0-9+/]+={0,2}$/;
|
|
38
|
+
const VERIFIED_MCP_EXECUTABLE_TARGET = Symbol('verified_mcp_executable_target');
|
|
39
|
+
|
|
40
|
+
function validSha512Integrity(value) {
|
|
41
|
+
if (!SHA512_INTEGRITY_RE.test(value)) return false;
|
|
42
|
+
const encoded = value.slice('sha512-'.length);
|
|
43
|
+
try {
|
|
44
|
+
const digest = Buffer.from(encoded, 'base64');
|
|
45
|
+
return digest.length === 64 && digest.toString('base64') === encoded;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parsedStableMcpVersion(value) {
|
|
52
|
+
const match = String(value || '').match(MCP_STABLE_VERSION_RE);
|
|
53
|
+
if (!match) return null;
|
|
54
|
+
const parts = match.slice(1).map(Number);
|
|
55
|
+
return parts.every(Number.isSafeInteger) ? parts : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function compareMcpVersions(left, right) {
|
|
59
|
+
const leftParts = parsedStableMcpVersion(left);
|
|
60
|
+
const rightParts = parsedStableMcpVersion(right);
|
|
61
|
+
if (!leftParts || !rightParts) return null;
|
|
62
|
+
for (let index = 0; index < leftParts.length; index += 1) {
|
|
63
|
+
if (leftParts[index] !== rightParts[index]) return leftParts[index] - rightParts[index];
|
|
64
|
+
}
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function compatibleMcpTargetVersion(value) {
|
|
69
|
+
const candidate = parsedStableMcpVersion(value);
|
|
70
|
+
const sealed = parsedStableMcpVersion(MCP_ADAPTER_VERSION);
|
|
71
|
+
return Boolean(candidate && sealed
|
|
72
|
+
&& candidate[0] === sealed[0]
|
|
73
|
+
&& candidate[1] === sealed[1]
|
|
74
|
+
&& compareMcpVersions(value, MCP_ADAPTER_VERSION) >= 0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function mcpVersionsInText(value) {
|
|
78
|
+
return [...String(value || '').matchAll(/@getmarrow\/mcp@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/g)]
|
|
79
|
+
.map((match) => match[1]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function unverifiedAheadMcpVersions(versions, targetVersion = MCP_ADAPTER_VERSION) {
|
|
83
|
+
if (!compatibleMcpTargetVersion(targetVersion)) return [];
|
|
84
|
+
return [...new Set((Array.isArray(versions) ? versions : [])
|
|
85
|
+
.filter((version) => compatibleMcpTargetVersion(version)
|
|
86
|
+
&& compareMcpVersions(version, targetVersion) > 0))].sort((left, right) => compareMcpVersions(left, right));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function mcpRegistryVerificationAction(versions) {
|
|
90
|
+
const ahead = [...new Set(Array.isArray(versions) ? versions : [])].sort((left, right) => compareMcpVersions(left, right));
|
|
91
|
+
const targets = ahead.map((version) => `@getmarrow/mcp@${version}`).join(', ');
|
|
92
|
+
return `Run ${MCP_REGISTRY_VERIFICATION_COMMAND} with official npm registry access, then rerun npx -y @getmarrow/install@latest doctor --self-test. Automatic repair is suppressed${targets ? ` for ${targets}` : ''}; preserve each existing surface until registry metadata verifies it or the owner chooses the sealed or verified version.`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function verifiedMcpRegistryMetadata(value) {
|
|
96
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
97
|
+
const version = typeof value.version === 'string' ? value.version : '';
|
|
98
|
+
const integrity = typeof value.dist?.integrity === 'string' ? value.dist.integrity : '';
|
|
99
|
+
const tarball = typeof value.dist?.tarball === 'string' ? value.dist.tarball : '';
|
|
100
|
+
const expectedTarball = `https://registry.npmjs.org/@getmarrow/mcp/-/mcp-${version}.tgz`;
|
|
101
|
+
if (value.name !== '@getmarrow/mcp'
|
|
102
|
+
|| !compatibleMcpTargetVersion(version)
|
|
103
|
+
|| !validSha512Integrity(integrity)
|
|
104
|
+
|| tarball !== expectedTarball) return null;
|
|
105
|
+
return { version, integrity, tarball };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function resolveMcpTargetVersion(options = {}) {
|
|
109
|
+
const registry = verifiedMcpRegistryMetadata(options.registryMetadata);
|
|
110
|
+
if (registry) {
|
|
111
|
+
const target = {
|
|
112
|
+
version: registry.version,
|
|
113
|
+
source: 'verified_npm_registry',
|
|
114
|
+
integrity: registry.integrity,
|
|
115
|
+
source_sha: null,
|
|
116
|
+
};
|
|
117
|
+
Object.defineProperty(target, VERIFIED_MCP_EXECUTABLE_TARGET, { value: true });
|
|
118
|
+
return target;
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
version: MCP_ADAPTER_VERSION,
|
|
122
|
+
source: 'sealed_installer',
|
|
123
|
+
integrity: MCP_ADAPTER_INTEGRITY,
|
|
124
|
+
source_sha: MCP_ADAPTER_SOURCE_SHA,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function executableMcpTarget(options = {}) {
|
|
129
|
+
const target = options.mcpTarget;
|
|
130
|
+
if (target?.[VERIFIED_MCP_EXECUTABLE_TARGET] === true
|
|
131
|
+
&& target.source === 'verified_npm_registry'
|
|
132
|
+
&& compatibleMcpTargetVersion(target.version)
|
|
133
|
+
&& validSha512Integrity(target.integrity)
|
|
134
|
+
&& target.source_sha === null) return target;
|
|
135
|
+
return resolveMcpTargetVersion();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function expectedMcpInspectionVersion(options = {}) {
|
|
139
|
+
if (options.expectedVersion === MCP_ADAPTER_VERSION) return MCP_ADAPTER_VERSION;
|
|
140
|
+
return executableMcpTarget({ mcpTarget: options.expectedTarget }).version;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function readMcpRegistryMetadata(options = {}) {
|
|
144
|
+
if (Object.prototype.hasOwnProperty.call(options, 'mcpRegistryMetadata')) {
|
|
145
|
+
return options.mcpRegistryMetadata;
|
|
146
|
+
}
|
|
147
|
+
if (options.resolveMcpRegistry !== true) return null;
|
|
148
|
+
const controller = new AbortController();
|
|
149
|
+
const timer = setTimeout(() => controller.abort(), 2500);
|
|
150
|
+
try {
|
|
151
|
+
const fetcher = typeof options.registryFetch === 'function' ? options.registryFetch : fetch;
|
|
152
|
+
const response = await fetcher(MCP_REGISTRY_LATEST_URL, {
|
|
153
|
+
headers: { accept: 'application/json' },
|
|
154
|
+
redirect: 'error',
|
|
155
|
+
signal: controller.signal,
|
|
156
|
+
});
|
|
157
|
+
if (!response.ok) return null;
|
|
158
|
+
return await response.json();
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
161
|
+
} finally {
|
|
162
|
+
clearTimeout(timer);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function adapterProvenanceForMcpTarget(target) {
|
|
167
|
+
if (!target || target.version === MCP_ADAPTER_VERSION) return ADAPTER_PROVENANCE;
|
|
168
|
+
return {
|
|
169
|
+
mcp: {
|
|
170
|
+
package: '@getmarrow/mcp',
|
|
171
|
+
version: target.version,
|
|
172
|
+
source_sha: target.source_sha,
|
|
173
|
+
integrity: target.integrity,
|
|
174
|
+
integrity_state: target.source === 'verified_npm_registry'
|
|
175
|
+
? 'verified_npm_registry_metadata'
|
|
176
|
+
: 'exact_current_configuration',
|
|
177
|
+
},
|
|
178
|
+
sdk: ADAPTER_PROVENANCE.sdk,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function retargetMcpPackageSpec(value, version) {
|
|
183
|
+
const target = compatibleMcpTargetVersion(version) ? version : MCP_ADAPTER_VERSION;
|
|
184
|
+
return String(value).split(MCP_PACKAGE_SPEC).join(`@getmarrow/mcp@${target}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function retargetMcpDowngradeRecommendation(value, targetVersion) {
|
|
188
|
+
if (typeof value !== 'string' || !compatibleMcpTargetVersion(targetVersion)) return value;
|
|
189
|
+
return value.replace(/@getmarrow\/mcp@(\d+\.\d+\.\d+)/g, (match, version) => (
|
|
190
|
+
parsedStableMcpVersion(version) && version !== targetVersion
|
|
191
|
+
? `@getmarrow/mcp@${targetVersion}`
|
|
192
|
+
: match
|
|
193
|
+
));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function alignMcpRecommendationVersions(value, targetVersion, key = '', verificationAction = null) {
|
|
197
|
+
if (Array.isArray(value)) {
|
|
198
|
+
return value.map((entry) => alignMcpRecommendationVersions(entry, targetVersion, key, verificationAction));
|
|
199
|
+
}
|
|
200
|
+
if (!value || typeof value !== 'object') {
|
|
201
|
+
return /(?:command|fix|instruction|next_action|notice)$/i.test(key)
|
|
202
|
+
? verificationAction && typeof value === 'string' && /@getmarrow\/mcp@\d+\.\d+\.\d+/.test(value)
|
|
203
|
+
? verificationAction
|
|
204
|
+
: retargetMcpDowngradeRecommendation(value, targetVersion)
|
|
205
|
+
: value;
|
|
206
|
+
}
|
|
207
|
+
return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [
|
|
208
|
+
entryKey,
|
|
209
|
+
alignMcpRecommendationVersions(entryValue, targetVersion, entryKey, verificationAction),
|
|
210
|
+
]));
|
|
211
|
+
}
|
|
33
212
|
const MCP_CONTEXT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp context-hook`;
|
|
34
213
|
const MCP_PRE_ACTION_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp pre-action-hook`;
|
|
35
214
|
const MCP_ACTION_RESULT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp hook`;
|
|
@@ -91,6 +270,28 @@ const GEMINI_HOOK_TIMEOUT_MS = 5000;
|
|
|
91
270
|
const GEMINI_CLOSEOUT_TIMEOUT_MS = 3000;
|
|
92
271
|
const NATIVE_EXPECTED_HOOKS = ['prompt', 'pre_action', 'action_result', 'session_end'];
|
|
93
272
|
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']);
|
|
273
|
+
const TOOL_PROFILES = new Set(['primary', 'core', 'full']);
|
|
274
|
+
const TOOL_PROFILE_EXPECTED_COUNTS = Object.freeze({ primary: 17, core: 7, full: null });
|
|
275
|
+
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.';
|
|
276
|
+
const PRIMARY_TOOL_NAMES = Object.freeze([
|
|
277
|
+
'marrow_agent_runtime',
|
|
278
|
+
'marrow_arbitrate',
|
|
279
|
+
'marrow_coordinate',
|
|
280
|
+
'marrow_replay_compare',
|
|
281
|
+
'marrow_decision_brief',
|
|
282
|
+
'marrow_think',
|
|
283
|
+
'marrow_commit',
|
|
284
|
+
'marrow_workflow_gate',
|
|
285
|
+
'marrow_completion_contracts',
|
|
286
|
+
'marrow_evaluate_completion_contract',
|
|
287
|
+
'marrow_agent_status',
|
|
288
|
+
'marrow_value_report',
|
|
289
|
+
'marrow_buyer_proof',
|
|
290
|
+
'marrow_governance_timeline',
|
|
291
|
+
'marrow_decision_trace',
|
|
292
|
+
'marrow_fleet_lessons',
|
|
293
|
+
'marrow_model_usage',
|
|
294
|
+
]);
|
|
94
295
|
const HARNESS_CAPABILITY_REGISTRY = Object.freeze([
|
|
95
296
|
{ client: 'claude-code', capability_level: 'native_hooks', automatic: ['prompt', 'pre_action', 'action_result', 'session_end'], install_surface: 'mcp' },
|
|
96
297
|
{ client: 'cursor', capability_level: 'native_hooks', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'mcp' },
|
|
@@ -118,6 +319,148 @@ function explicitMcpVersion(command) {
|
|
|
118
319
|
return match ? match[1] : null;
|
|
119
320
|
}
|
|
120
321
|
|
|
322
|
+
function resolveToolProfile(value) {
|
|
323
|
+
const structured = value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
324
|
+
const candidate = structured
|
|
325
|
+
? value.configured_profile
|
|
326
|
+
: value;
|
|
327
|
+
const absent = !structured && candidate === undefined;
|
|
328
|
+
const structuredUnset = structured && candidate === 'unset';
|
|
329
|
+
const configuredProfile = absent || structuredUnset
|
|
330
|
+
? 'unset'
|
|
331
|
+
: candidate;
|
|
332
|
+
if (!absent && !structuredUnset && !TOOL_PROFILES.has(configuredProfile)) {
|
|
333
|
+
throw new Error(`Invalid MARROW_TOOL_PROFILE. ${TOOL_PROFILE_EXACT_FIX}`);
|
|
334
|
+
}
|
|
335
|
+
const effectiveProfile = configuredProfile === 'unset' ? 'primary' : configuredProfile;
|
|
336
|
+
return {
|
|
337
|
+
configured_profile: configuredProfile,
|
|
338
|
+
effective_profile: effectiveProfile,
|
|
339
|
+
expected_visible_count: TOOL_PROFILE_EXPECTED_COUNTS[effectiveProfile],
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function normalizePrimaryToolAvailability(value) {
|
|
344
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || value.profile !== 'primary') return null;
|
|
345
|
+
const evidence = value.entitlement_evidence;
|
|
346
|
+
const counts = value.counts;
|
|
347
|
+
const tools = Array.isArray(value.tools) ? value.tools : [];
|
|
348
|
+
if (!evidence || typeof evidence !== 'object' || evidence.authorizing !== false) return null;
|
|
349
|
+
if (!['available', 'unavailable'].includes(evidence.state)) return null;
|
|
350
|
+
if (!counts || counts.total !== 17 || !Number.isInteger(counts.entitled) || !Number.isInteger(counts.upgrade_required)) return null;
|
|
351
|
+
if (counts.entitled + counts.upgrade_required !== counts.total || tools.length !== counts.total) return null;
|
|
352
|
+
const normalizedTools = [];
|
|
353
|
+
const seen = new Set();
|
|
354
|
+
for (const tool of tools) {
|
|
355
|
+
if (!tool || typeof tool !== 'object' || !PRIMARY_TOOL_NAMES.includes(tool.name) || seen.has(tool.name)) return null;
|
|
356
|
+
if (!['entitled', 'upgrade_required'].includes(tool.state) || typeof tool.always_available !== 'boolean') return null;
|
|
357
|
+
seen.add(tool.name);
|
|
358
|
+
normalizedTools.push({
|
|
359
|
+
name: tool.name,
|
|
360
|
+
state: tool.state,
|
|
361
|
+
always_available: tool.always_available,
|
|
362
|
+
plan_feature: typeof tool.plan_feature === 'string' ? tool.plan_feature : null,
|
|
363
|
+
minimum_plan: typeof tool.minimum_plan === 'string' ? tool.minimum_plan : null,
|
|
364
|
+
owner_management_url: typeof tool.owner_management_url === 'string' ? tool.owner_management_url : '',
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
if (PRIMARY_TOOL_NAMES.some((name) => !seen.has(name))) return null;
|
|
368
|
+
const entitled = normalizedTools.filter((tool) => tool.state === 'entitled').length;
|
|
369
|
+
const upgradeRequired = normalizedTools.filter((tool) => tool.state === 'upgrade_required').length;
|
|
370
|
+
if (entitled !== counts.entitled || upgradeRequired !== counts.upgrade_required) return null;
|
|
371
|
+
return {
|
|
372
|
+
profile: 'primary',
|
|
373
|
+
current_plan: typeof value.current_plan === 'string' ? value.current_plan : null,
|
|
374
|
+
owner_management_url: typeof value.owner_management_url === 'string' ? value.owner_management_url : '',
|
|
375
|
+
entitlement_evidence: {
|
|
376
|
+
state: evidence.state,
|
|
377
|
+
source: typeof evidence.source === 'string' ? evidence.source : 'entitlement_read_unavailable',
|
|
378
|
+
authoritative: evidence.authoritative === true,
|
|
379
|
+
authorizing: false,
|
|
380
|
+
},
|
|
381
|
+
counts: { total: 17, entitled, upgrade_required: upgradeRequired },
|
|
382
|
+
tools: normalizedTools,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function backendEntitlementProjection(statusProfile, contextProjection) {
|
|
387
|
+
const freshProjection = normalizePrimaryToolAvailability(contextProjection);
|
|
388
|
+
if (freshProjection) {
|
|
389
|
+
return {
|
|
390
|
+
evidence_state: freshProjection.entitlement_evidence.state,
|
|
391
|
+
source: 'authenticated_backend',
|
|
392
|
+
authorizes_calls: false,
|
|
393
|
+
primary_tool_availability: freshProjection,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
const envelope = statusProfile?.backend_entitlement_projection;
|
|
397
|
+
const projected = normalizePrimaryToolAvailability(envelope?.primary_tool_availability);
|
|
398
|
+
const source = ['authenticated_backend', 'cached_or_stale_status', 'backend_projection_not_provided'].includes(envelope?.source)
|
|
399
|
+
? envelope.source
|
|
400
|
+
: 'backend_projection_not_provided';
|
|
401
|
+
const available = envelope?.authorizes_calls === false
|
|
402
|
+
&& envelope?.evidence_state === 'available'
|
|
403
|
+
&& source === 'authenticated_backend'
|
|
404
|
+
&& projected?.entitlement_evidence.state === 'available';
|
|
405
|
+
return {
|
|
406
|
+
evidence_state: available ? 'available' : 'unavailable',
|
|
407
|
+
source,
|
|
408
|
+
authorizes_calls: false,
|
|
409
|
+
primary_tool_availability: projected,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function buildMcpToolProfileReport(value, statusProfile = null, contextProjection = null, forceReload = false) {
|
|
414
|
+
const expected = resolveToolProfile(value);
|
|
415
|
+
const reportedNames = Array.isArray(statusProfile?.visible_tool_names)
|
|
416
|
+
? statusProfile.visible_tool_names.filter((name) => typeof name === 'string')
|
|
417
|
+
: [];
|
|
418
|
+
const reportedCount = statusProfile?.visible_tool_count;
|
|
419
|
+
const reportedConfigured = statusProfile?.configured_profile;
|
|
420
|
+
const reportedEffective = statusProfile?.effective_profile;
|
|
421
|
+
const uniqueNames = new Set(reportedNames);
|
|
422
|
+
const profileIdentityMatches = reportedConfigured === expected.configured_profile
|
|
423
|
+
&& reportedEffective === expected.effective_profile;
|
|
424
|
+
const reportedCatalogIsConsistent = Number.isInteger(reportedCount)
|
|
425
|
+
&& reportedCount >= 0
|
|
426
|
+
&& reportedNames.length === reportedCount
|
|
427
|
+
&& uniqueNames.size === reportedCount;
|
|
428
|
+
const expectedCount = expected.effective_profile === 'full'
|
|
429
|
+
&& profileIdentityMatches
|
|
430
|
+
&& reportedCatalogIsConsistent
|
|
431
|
+
? reportedCount
|
|
432
|
+
: expected.expected_visible_count;
|
|
433
|
+
const expectedPrimaryNames = expected.effective_profile !== 'primary'
|
|
434
|
+
|| (reportedNames.length === PRIMARY_TOOL_NAMES.length
|
|
435
|
+
&& PRIMARY_TOOL_NAMES.every((name) => uniqueNames.has(name)));
|
|
436
|
+
const visibilityLive = !forceReload
|
|
437
|
+
&& profileIdentityMatches
|
|
438
|
+
&& statusProfile?.local_visibility_grants_entitlement === false
|
|
439
|
+
&& reportedCatalogIsConsistent
|
|
440
|
+
&& reportedCount === expectedCount
|
|
441
|
+
&& expectedPrimaryNames;
|
|
442
|
+
return {
|
|
443
|
+
configured_profile: expected.configured_profile,
|
|
444
|
+
effective_profile: expected.effective_profile,
|
|
445
|
+
expected_visible_count: expectedCount,
|
|
446
|
+
visible_tool_count: visibilityLive ? reportedCount : null,
|
|
447
|
+
actual_visible_count: visibilityLive ? reportedCount : null,
|
|
448
|
+
visible_tool_names: visibilityLive ? reportedNames : [],
|
|
449
|
+
local_visibility_grants_entitlement: false,
|
|
450
|
+
visibility_live: visibilityLive,
|
|
451
|
+
reload_required: !visibilityLive,
|
|
452
|
+
reported_configured_profile: typeof reportedConfigured === 'string' ? reportedConfigured : null,
|
|
453
|
+
reported_effective_profile: typeof reportedEffective === 'string' ? reportedEffective : null,
|
|
454
|
+
backend_entitlement_projection: backendEntitlementProjection(statusProfile, contextProjection),
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function initialToolProfileReport(value) {
|
|
459
|
+
return {
|
|
460
|
+
...buildMcpToolProfileReport(value),
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
121
464
|
function readMcpPackageVersion(packageRoot) {
|
|
122
465
|
try {
|
|
123
466
|
const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
@@ -201,24 +544,37 @@ function inspectMcpProcesses(options = {}) {
|
|
|
201
544
|
.filter(isMcpProcessCommand)
|
|
202
545
|
.map((command) => explicitMcpVersion(command) || packageMcpVersion(command) || 'unknown');
|
|
203
546
|
const versions = [...new Set(active.filter((version) => version !== 'unknown'))].sort();
|
|
547
|
+
const expectedVersion = expectedMcpInspectionVersion(options);
|
|
204
548
|
const unknownVersionProcesses = active.filter((version) => version === 'unknown').length;
|
|
205
|
-
const
|
|
549
|
+
const aheadUnverifiedVersions = unverifiedAheadMcpVersions(versions, expectedVersion);
|
|
550
|
+
const staleVersions = versions.filter((version) => version !== expectedVersion
|
|
551
|
+
&& !aheadUnverifiedVersions.includes(version));
|
|
206
552
|
const mixedVersions = versions.length > 1 || (versions.length > 0 && unknownVersionProcesses > 0);
|
|
207
553
|
const stale = staleVersions.length > 0;
|
|
208
|
-
const
|
|
209
|
-
const
|
|
554
|
+
const aheadUnverified = aheadUnverifiedVersions.length > 0;
|
|
555
|
+
const needsRepair = stale || mixedVersions || unknownVersionProcesses > 0 || aheadUnverified;
|
|
556
|
+
const automaticRepairSuppressed = aheadUnverified;
|
|
557
|
+
const repairCommand = `npx -y --package=@getmarrow/mcp@${expectedVersion} marrow-mcp setup`;
|
|
210
558
|
return {
|
|
211
559
|
available: process.platform === 'linux' || Array.isArray(options.commands),
|
|
212
|
-
expected_version:
|
|
560
|
+
expected_version: expectedVersion,
|
|
213
561
|
active_processes: active.length,
|
|
214
562
|
active_versions: versions,
|
|
215
563
|
unknown_version_processes: unknownVersionProcesses,
|
|
216
564
|
stale_versions: staleVersions,
|
|
565
|
+
ahead_unverified: aheadUnverified,
|
|
566
|
+
ahead_unverified_versions: aheadUnverifiedVersions,
|
|
217
567
|
mixed_versions: mixedVersions,
|
|
218
568
|
healthy: !needsRepair,
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
569
|
+
automatic_repair_suppressed: automaticRepairSuppressed,
|
|
570
|
+
registry_verification_required: aheadUnverified,
|
|
571
|
+
exact_fix: automaticRepairSuppressed
|
|
572
|
+
? mcpRegistryVerificationAction(aheadUnverifiedVersions)
|
|
573
|
+
: needsRepair ? repairCommand : null,
|
|
574
|
+
restart_required: needsRepair && !automaticRepairSuppressed,
|
|
575
|
+
restart_instruction: needsRepair && !automaticRepairSuppressed
|
|
576
|
+
? 'Restart every owning harness to replace its active Marrow MCP process.'
|
|
577
|
+
: null,
|
|
222
578
|
verification_command: needsRepair ? 'npx -y @getmarrow/install@latest doctor --self-test' : null,
|
|
223
579
|
};
|
|
224
580
|
}
|
|
@@ -257,20 +613,33 @@ function inspectMcpConfigurations(detection, options = {}) {
|
|
|
257
613
|
}
|
|
258
614
|
}
|
|
259
615
|
const configuredVersions = [...new Set(versions)].sort();
|
|
260
|
-
const
|
|
616
|
+
const expectedVersion = expectedMcpInspectionVersion(options);
|
|
617
|
+
const aheadUnverifiedVersions = unverifiedAheadMcpVersions(configuredVersions, expectedVersion);
|
|
618
|
+
const staleVersions = configuredVersions.filter((version) => version !== expectedVersion
|
|
619
|
+
&& !aheadUnverifiedVersions.includes(version));
|
|
261
620
|
const mixedVersions = configuredVersions.length > 1
|
|
262
621
|
|| (configuredVersions.length > 0 && unknownVersionConfigurations > 0);
|
|
263
|
-
const
|
|
622
|
+
const aheadUnverified = aheadUnverifiedVersions.length > 0;
|
|
623
|
+
const healthy = staleVersions.length === 0 && !mixedVersions
|
|
624
|
+
&& unknownVersionConfigurations === 0 && !aheadUnverified;
|
|
264
625
|
return {
|
|
265
|
-
expected_version:
|
|
626
|
+
expected_version: expectedVersion,
|
|
266
627
|
files_checked: filesChecked,
|
|
267
628
|
configurations_found: configurationsFound,
|
|
268
629
|
configured_versions: configuredVersions,
|
|
269
630
|
unknown_version_configurations: unknownVersionConfigurations,
|
|
270
631
|
stale_versions: staleVersions,
|
|
632
|
+
ahead_unverified: aheadUnverified,
|
|
633
|
+
ahead_unverified_versions: aheadUnverifiedVersions,
|
|
271
634
|
mixed_versions: mixedVersions,
|
|
272
635
|
healthy,
|
|
273
|
-
|
|
636
|
+
automatic_repair_suppressed: aheadUnverified,
|
|
637
|
+
registry_verification_required: aheadUnverified,
|
|
638
|
+
exact_fix: healthy
|
|
639
|
+
? null
|
|
640
|
+
: aheadUnverified
|
|
641
|
+
? mcpRegistryVerificationAction(aheadUnverifiedVersions)
|
|
642
|
+
: `Run npx -y --package=@getmarrow/mcp@${expectedVersion} marrow-mcp setup in each owning workspace, update its MCP launch to npx -y --package=@getmarrow/mcp@${expectedVersion} marrow-mcp, then restart that harness.`,
|
|
274
643
|
verification_command: healthy ? null : 'npx -y @getmarrow/install@latest doctor --self-test',
|
|
275
644
|
};
|
|
276
645
|
}
|
|
@@ -307,7 +676,7 @@ function sourceClient() {
|
|
|
307
676
|
return aliases[raw] || (SOURCE_CLIENTS.has(raw) ? raw : 'custom');
|
|
308
677
|
}
|
|
309
678
|
|
|
310
|
-
function parseArgs(argv) {
|
|
679
|
+
function parseArgs(argv, env = process.env) {
|
|
311
680
|
const options = {
|
|
312
681
|
cwd: process.cwd(),
|
|
313
682
|
yes: false,
|
|
@@ -315,9 +684,10 @@ function parseArgs(argv) {
|
|
|
315
684
|
doctor: false,
|
|
316
685
|
repair: false,
|
|
317
686
|
mode: 'auto',
|
|
318
|
-
apiKey:
|
|
319
|
-
baseUrl:
|
|
320
|
-
agentId:
|
|
687
|
+
apiKey: env.MARROW_API_KEY || '',
|
|
688
|
+
baseUrl: env.MARROW_BASE_URL || DEFAULT_BASE_URL,
|
|
689
|
+
agentId: env.MARROW_FLEET_AGENT_ID || env.MARROW_AGENT_ID || '',
|
|
690
|
+
toolProfile: resolveToolProfile(env.MARROW_TOOL_PROFILE),
|
|
321
691
|
selfTest: true,
|
|
322
692
|
selfTestExplicitlyDisabled: false,
|
|
323
693
|
json: false,
|
|
@@ -409,6 +779,7 @@ function parseArgs(argv) {
|
|
|
409
779
|
if (options.activate && options.dryRun) {
|
|
410
780
|
throw new Error('activate cannot be combined with --dry-run; use --dry-run without activate to preview changes');
|
|
411
781
|
}
|
|
782
|
+
options.resolveMcpRegistry = Boolean(options.doctor || options.repair || options.update);
|
|
412
783
|
|
|
413
784
|
return options;
|
|
414
785
|
}
|
|
@@ -439,6 +810,10 @@ Options:
|
|
|
439
810
|
--agent-id <id> Agent/fleet id for self-test headers
|
|
440
811
|
--no-controller Do not start the local background controller during install/repair
|
|
441
812
|
--no-self-test Skip API smoke/self-test
|
|
813
|
+
|
|
814
|
+
Environment:
|
|
815
|
+
MARROW_TOOL_PROFILE Leave unset for primary (17 tools), or explicitly set primary, core, or full.
|
|
816
|
+
Visibility never grants entitlement; backend plans and permissions authorize calls.
|
|
442
817
|
`;
|
|
443
818
|
}
|
|
444
819
|
|
|
@@ -727,6 +1102,7 @@ function passiveInstructions() {
|
|
|
727
1102
|
Marrow should run passively after install:
|
|
728
1103
|
|
|
729
1104
|
- Use MCP plus these instructions in every workspace: \`npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp setup\`.
|
|
1105
|
+
- 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.
|
|
730
1106
|
- Use SDK passive runtime in owned Node processes: \`createPassiveRuntime().install()\`.
|
|
731
1107
|
- 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.
|
|
732
1108
|
- Keep passive token/model usage proof enabled. Empty savings stay zero until observed model usage lands. Do not invent token, cost, or time savings.
|
|
@@ -790,6 +1166,7 @@ function envExample(options = {}) {
|
|
|
790
1166
|
MARROW_BASE_URL=${JSON.stringify(baseUrl)}
|
|
791
1167
|
MARROW_FLEET_AGENT_ID=${JSON.stringify(agentId)}
|
|
792
1168
|
MARROW_CLIENT=${JSON.stringify(client)}
|
|
1169
|
+
# MARROW_TOOL_PROFILE is intentionally unset: ordinary setup uses primary. Set core or full only as an explicit opt-in.
|
|
793
1170
|
MARROW_ENFORCEMENT_MODE=auto
|
|
794
1171
|
MARROW_PASSIVE_BRIEF=auto
|
|
795
1172
|
MARROW_PASSIVE_VALUE_REPORT=true
|
|
@@ -1431,8 +1808,12 @@ function activationProfile(detection, plan, changes, client) {
|
|
|
1431
1808
|
const expectedHooks = capabilityLevel === 'sdk_passive_runtime'
|
|
1432
1809
|
? ['pre_action', 'action_result', 'outcome_closure']
|
|
1433
1810
|
: [...registry.automatic];
|
|
1811
|
+
const mcpTargetVersion = compatibleMcpTargetVersion(plan.mcp_target_version)
|
|
1812
|
+
? plan.mcp_target_version
|
|
1813
|
+
: MCP_ADAPTER_VERSION;
|
|
1814
|
+
const targetCommand = (command) => retargetMcpPackageSpec(command, mcpTargetVersion);
|
|
1434
1815
|
const adapterVersion = capabilityLevel === 'native_hooks' || capabilityLevel === 'mcp'
|
|
1435
|
-
?
|
|
1816
|
+
? mcpTargetVersion
|
|
1436
1817
|
: capabilityLevel === 'sdk_passive_runtime'
|
|
1437
1818
|
? SDK_ADAPTER_VERSION
|
|
1438
1819
|
: INSTALLER_ADAPTER_VERSION;
|
|
@@ -1444,71 +1825,71 @@ function activationProfile(detection, plan, changes, client) {
|
|
|
1444
1825
|
const geminiSettings = safeJsonObject(detection.paths.geminiSettings);
|
|
1445
1826
|
const grokSettings = safeJsonObject(detection.paths.grokHooks);
|
|
1446
1827
|
if (client === 'codex') {
|
|
1447
|
-
if (exactHookConfigured(codexSettings, 'UserPromptSubmit', CODEX_CONTEXT_HOOK_COMMAND)) observedHooks.push('prompt');
|
|
1448
|
-
if (exactHookConfigured(codexSettings, 'PreToolUse', CODEX_PRE_ACTION_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER)) observedHooks.push('pre_action');
|
|
1449
|
-
if (exactHookConfigured(codexSettings, 'PostToolUse', CODEX_ACTION_RESULT_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER)) observedHooks.push('action_result');
|
|
1450
|
-
if (exactHookConfigured(codexSettings, 'SessionEnd', CODEX_SESSION_END_HOOK_COMMAND)) observedHooks.push('session_end');
|
|
1828
|
+
if (exactHookConfigured(codexSettings, 'UserPromptSubmit', targetCommand(CODEX_CONTEXT_HOOK_COMMAND))) observedHooks.push('prompt');
|
|
1829
|
+
if (exactHookConfigured(codexSettings, 'PreToolUse', targetCommand(CODEX_PRE_ACTION_HOOK_COMMAND), CODEX_NATIVE_HOOK_MATCHER)) observedHooks.push('pre_action');
|
|
1830
|
+
if (exactHookConfigured(codexSettings, 'PostToolUse', targetCommand(CODEX_ACTION_RESULT_HOOK_COMMAND), CODEX_NATIVE_HOOK_MATCHER)) observedHooks.push('action_result');
|
|
1831
|
+
if (exactHookConfigured(codexSettings, 'SessionEnd', targetCommand(CODEX_SESSION_END_HOOK_COMMAND))) observedHooks.push('session_end');
|
|
1451
1832
|
} else if (client === 'cursor' || client === 'composer') {
|
|
1452
|
-
if (exactCursorHookConfigured(cursorSettings, 'preToolUse', CURSOR_PRE_ACTION_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
|
|
1833
|
+
if (exactCursorHookConfigured(cursorSettings, 'preToolUse', targetCommand(CURSOR_PRE_ACTION_HOOK_COMMAND), CURSOR_NATIVE_HOOK_MATCHER, {
|
|
1453
1834
|
timeout: CODEX_HOOK_TIMEOUT_SECONDS, failClosed: true, async: false,
|
|
1454
1835
|
})) observedHooks.push('pre_action');
|
|
1455
|
-
if (exactCursorHookConfigured(cursorSettings, 'postToolUse', CURSOR_ACTION_RESULT_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
|
|
1836
|
+
if (exactCursorHookConfigured(cursorSettings, 'postToolUse', targetCommand(CURSOR_ACTION_RESULT_HOOK_COMMAND), CURSOR_NATIVE_HOOK_MATCHER, {
|
|
1456
1837
|
timeout: CODEX_HOOK_TIMEOUT_SECONDS,
|
|
1457
|
-
}) && exactCursorHookConfigured(cursorSettings, 'postToolUseFailure', CURSOR_ACTION_RESULT_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
|
|
1838
|
+
}) && exactCursorHookConfigured(cursorSettings, 'postToolUseFailure', targetCommand(CURSOR_ACTION_RESULT_HOOK_COMMAND), CURSOR_NATIVE_HOOK_MATCHER, {
|
|
1458
1839
|
timeout: CODEX_HOOK_TIMEOUT_SECONDS,
|
|
1459
1840
|
})) observedHooks.push('action_result');
|
|
1460
|
-
if (exactCursorHookConfigured(cursorSettings, 'stop', CURSOR_SESSION_END_HOOK_COMMAND, undefined, {
|
|
1841
|
+
if (exactCursorHookConfigured(cursorSettings, 'stop', targetCommand(CURSOR_SESSION_END_HOOK_COMMAND), undefined, {
|
|
1461
1842
|
timeout: CODEX_SESSION_TIMEOUT_SECONDS,
|
|
1462
1843
|
})) observedHooks.push('outcome_closure');
|
|
1463
1844
|
} else if (client === 'cline') {
|
|
1464
1845
|
for (const hook of clineHookContract(detection)) {
|
|
1465
|
-
if (exactExecutableFile(hook.path, hook.content)) observedHooks.push(hook.stage);
|
|
1846
|
+
if (exactExecutableFile(hook.path, targetCommand(hook.content))) observedHooks.push(hook.stage);
|
|
1466
1847
|
}
|
|
1467
1848
|
} else if (client === 'windsurf') {
|
|
1468
1849
|
if (WINDSURF_PRE_EVENTS.every((event) => exactWindsurfHookConfigured(
|
|
1469
|
-
windsurfSettings, event, WINDSURF_PRE_ACTION_HOOK_COMMAND,
|
|
1850
|
+
windsurfSettings, event, targetCommand(WINDSURF_PRE_ACTION_HOOK_COMMAND),
|
|
1470
1851
|
))) observedHooks.push('pre_action');
|
|
1471
1852
|
if (WINDSURF_POST_EVENTS.every((event) => exactWindsurfHookConfigured(
|
|
1472
|
-
windsurfSettings, event, WINDSURF_ACTION_RESULT_HOOK_COMMAND,
|
|
1853
|
+
windsurfSettings, event, targetCommand(WINDSURF_ACTION_RESULT_HOOK_COMMAND),
|
|
1473
1854
|
))) observedHooks.push('action_result');
|
|
1474
1855
|
if (exactWindsurfHookConfigured(
|
|
1475
|
-
windsurfSettings, 'post_cascade_response', WINDSURF_SESSION_END_HOOK_COMMAND,
|
|
1856
|
+
windsurfSettings, 'post_cascade_response', targetCommand(WINDSURF_SESSION_END_HOOK_COMMAND),
|
|
1476
1857
|
)) observedHooks.push('response_closeout');
|
|
1477
1858
|
} else if (client === 'gemini' && !geminiHooksExplicitlyDisabled(geminiSettings)) {
|
|
1478
1859
|
if (exactGeminiHookConfigured(
|
|
1479
|
-
geminiSettings, 'BeforeTool', 'marrow-before-tool', GEMINI_PRE_ACTION_HOOK_COMMAND,
|
|
1860
|
+
geminiSettings, 'BeforeTool', 'marrow-before-tool', targetCommand(GEMINI_PRE_ACTION_HOOK_COMMAND),
|
|
1480
1861
|
GEMINI_NATIVE_HOOK_MATCHER, GEMINI_HOOK_TIMEOUT_MS,
|
|
1481
1862
|
)) observedHooks.push('pre_action');
|
|
1482
1863
|
if (exactGeminiHookConfigured(
|
|
1483
|
-
geminiSettings, 'AfterTool', 'marrow-after-tool', GEMINI_ACTION_RESULT_HOOK_COMMAND,
|
|
1864
|
+
geminiSettings, 'AfterTool', 'marrow-after-tool', targetCommand(GEMINI_ACTION_RESULT_HOOK_COMMAND),
|
|
1484
1865
|
GEMINI_NATIVE_HOOK_MATCHER, GEMINI_HOOK_TIMEOUT_MS,
|
|
1485
1866
|
)) observedHooks.push('action_result');
|
|
1486
1867
|
if (exactGeminiHookConfigured(
|
|
1487
|
-
geminiSettings, 'AfterAgent', 'marrow-after-agent', GEMINI_SESSION_END_HOOK_COMMAND,
|
|
1868
|
+
geminiSettings, 'AfterAgent', 'marrow-after-agent', targetCommand(GEMINI_SESSION_END_HOOK_COMMAND),
|
|
1488
1869
|
undefined, GEMINI_CLOSEOUT_TIMEOUT_MS,
|
|
1489
1870
|
)) observedHooks.push('turn_closeout');
|
|
1490
1871
|
} else if (client === 'grok') {
|
|
1491
1872
|
if (exactGrokHookConfigured(
|
|
1492
|
-
grokSettings, 'PreToolUse', GROK_PRE_ACTION_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 7,
|
|
1873
|
+
grokSettings, 'PreToolUse', targetCommand(GROK_PRE_ACTION_HOOK_COMMAND), GROK_NATIVE_HOOK_MATCHER, 7,
|
|
1493
1874
|
)) observedHooks.push('pre_action');
|
|
1494
1875
|
if (exactGrokHookConfigured(
|
|
1495
|
-
grokSettings, 'PostToolUse', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5,
|
|
1876
|
+
grokSettings, 'PostToolUse', targetCommand(GROK_ACTION_RESULT_HOOK_COMMAND), GROK_NATIVE_HOOK_MATCHER, 5,
|
|
1496
1877
|
) && exactGrokHookConfigured(
|
|
1497
|
-
grokSettings, 'PostToolUseFailure', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5,
|
|
1878
|
+
grokSettings, 'PostToolUseFailure', targetCommand(GROK_ACTION_RESULT_HOOK_COMMAND), GROK_NATIVE_HOOK_MATCHER, 5,
|
|
1498
1879
|
)) observedHooks.push('action_result');
|
|
1499
1880
|
if (exactGrokHookConfigured(
|
|
1500
|
-
grokSettings, 'Stop', GROK_SESSION_END_HOOK_COMMAND, undefined, 3,
|
|
1881
|
+
grokSettings, 'Stop', targetCommand(GROK_SESSION_END_HOOK_COMMAND), undefined, 3,
|
|
1501
1882
|
) && !grokHasDuplicateSessionEnd(grokSettings)) observedHooks.push('turn_closeout');
|
|
1502
1883
|
} else {
|
|
1503
1884
|
if (capabilityLevel === 'native_hooks'
|
|
1504
|
-
&& exactHookConfigured(claudeSettings, 'UserPromptSubmit', MCP_CONTEXT_HOOK_COMMAND)) observedHooks.push('prompt');
|
|
1885
|
+
&& exactHookConfigured(claudeSettings, 'UserPromptSubmit', targetCommand(MCP_CONTEXT_HOOK_COMMAND))) observedHooks.push('prompt');
|
|
1505
1886
|
if (capabilityLevel === 'native_hooks'
|
|
1506
|
-
&& exactHookConfigured(claudeSettings, 'PreToolUse', MCP_PRE_ACTION_HOOK_COMMAND, NATIVE_HOOK_MATCHER)) observedHooks.push('pre_action');
|
|
1887
|
+
&& exactHookConfigured(claudeSettings, 'PreToolUse', targetCommand(MCP_PRE_ACTION_HOOK_COMMAND), NATIVE_HOOK_MATCHER)) observedHooks.push('pre_action');
|
|
1507
1888
|
if (capabilityLevel === 'native_hooks'
|
|
1508
|
-
&& exactHookConfigured(claudeSettings, 'PostToolUse', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER)
|
|
1509
|
-
&& exactHookConfigured(claudeSettings, 'PostToolUseFailure', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER)) observedHooks.push('action_result');
|
|
1889
|
+
&& exactHookConfigured(claudeSettings, 'PostToolUse', targetCommand(MCP_ACTION_RESULT_HOOK_COMMAND), NATIVE_HOOK_MATCHER)
|
|
1890
|
+
&& exactHookConfigured(claudeSettings, 'PostToolUseFailure', targetCommand(MCP_ACTION_RESULT_HOOK_COMMAND), NATIVE_HOOK_MATCHER)) observedHooks.push('action_result');
|
|
1510
1891
|
if (capabilityLevel === 'native_hooks'
|
|
1511
|
-
&& exactHookConfigured(claudeSettings, 'Stop', MCP_SESSION_END_HOOK_COMMAND)) observedHooks.push('session_end');
|
|
1892
|
+
&& exactHookConfigured(claudeSettings, 'Stop', targetCommand(MCP_SESSION_END_HOOK_COMMAND))) observedHooks.push('session_end');
|
|
1512
1893
|
}
|
|
1513
1894
|
const passiveRuntime = safeRead(detection.paths.passiveRuntime);
|
|
1514
1895
|
if (capabilityLevel === 'sdk_passive_runtime'
|
|
@@ -1524,7 +1905,7 @@ function activationProfile(detection, plan, changes, client) {
|
|
|
1524
1905
|
if (capabilityLevel === 'mcp' && mcpConfigs.some((config) => (
|
|
1525
1906
|
config?.mcpServers?.marrow?.command === 'npx'
|
|
1526
1907
|
&& Array.isArray(config.mcpServers.marrow.args)
|
|
1527
|
-
&& config.mcpServers.marrow.args.join(' ') === `-y --package
|
|
1908
|
+
&& config.mcpServers.marrow.args.join(' ') === `-y --package=@getmarrow/mcp@${mcpTargetVersion} marrow-mcp`
|
|
1528
1909
|
))) observedHooks.push('mcp_tool_calls');
|
|
1529
1910
|
const fingerprintMaterial = changes
|
|
1530
1911
|
.filter((change) => change.applied || change.already_present)
|
|
@@ -1565,7 +1946,7 @@ function activationProfile(detection, plan, changes, client) {
|
|
|
1565
1946
|
: client === 'gemini' && geminiHooksExplicitlyDisabled(geminiSettings)
|
|
1566
1947
|
? '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.'
|
|
1567
1948
|
: client === 'grok'
|
|
1568
|
-
? `Run npx -y --package
|
|
1949
|
+
? `Run npx -y --package=@getmarrow/mcp@${mcpTargetVersion} marrow-mcp setup, restart Grok, then inspect /hooks and confirm the global hooks are enabled.`
|
|
1569
1950
|
: 'npx @getmarrow/install --repair';
|
|
1570
1951
|
return {
|
|
1571
1952
|
adapter_version: adapterVersion,
|
|
@@ -1622,13 +2003,18 @@ function upsertMcpServerConfig(filePath, options = {}) {
|
|
|
1622
2003
|
const servers = config.mcpServers && typeof config.mcpServers === 'object' && !Array.isArray(config.mcpServers)
|
|
1623
2004
|
? config.mcpServers
|
|
1624
2005
|
: {};
|
|
2006
|
+
const existingProfile = resolveToolProfile(servers.marrow?.env?.MARROW_TOOL_PROFILE).configured_profile;
|
|
2007
|
+
const requestedProfile = resolveToolProfile(options.toolProfile).configured_profile;
|
|
2008
|
+
const configuredProfile = requestedProfile === 'unset' ? existingProfile : requestedProfile;
|
|
2009
|
+
const env = {
|
|
2010
|
+
MARROW_BASE_URL: baseUrl,
|
|
2011
|
+
MARROW_FLEET_AGENT_ID: agentId,
|
|
2012
|
+
};
|
|
2013
|
+
if (configuredProfile !== 'unset') env.MARROW_TOOL_PROFILE = configuredProfile;
|
|
1625
2014
|
servers.marrow = {
|
|
1626
2015
|
command: 'npx',
|
|
1627
2016
|
args: ['-y', `--package=${MCP_PACKAGE_SPEC}`, 'marrow-mcp'],
|
|
1628
|
-
env
|
|
1629
|
-
MARROW_BASE_URL: baseUrl,
|
|
1630
|
-
MARROW_FLEET_AGENT_ID: agentId,
|
|
1631
|
-
},
|
|
2017
|
+
env,
|
|
1632
2018
|
};
|
|
1633
2019
|
config.mcpServers = servers;
|
|
1634
2020
|
return JSON.stringify(config, null, 2) + '\n';
|
|
@@ -1849,6 +2235,8 @@ function buildPlan(detection, options) {
|
|
|
1849
2235
|
const client = options.client || detectedClient(detection);
|
|
1850
2236
|
const agentId = String(options.agentId || '').trim() || stableAgentId(detection.root, client);
|
|
1851
2237
|
const baseUrl = String(options.baseUrl || DEFAULT_BASE_URL).trim() || DEFAULT_BASE_URL;
|
|
2238
|
+
const mcpTargetVersion = executableMcpTarget(options).version;
|
|
2239
|
+
const retarget = (value) => retargetMcpPackageSpec(value, mcpTargetVersion);
|
|
1852
2240
|
const mode = options.mode === 'auto'
|
|
1853
2241
|
? detection.node ? 'both' : 'mcp'
|
|
1854
2242
|
: options.mode;
|
|
@@ -1876,7 +2264,7 @@ function buildPlan(detection, options) {
|
|
|
1876
2264
|
type: 'json-transform',
|
|
1877
2265
|
path: detection.paths.claudeSettings,
|
|
1878
2266
|
label: 'Claude Code MCP passive hooks',
|
|
1879
|
-
transform: upsertClaudeHooks,
|
|
2267
|
+
transform: (filePath) => retarget(upsertClaudeHooks(filePath)),
|
|
1880
2268
|
});
|
|
1881
2269
|
}
|
|
1882
2270
|
if (detection.codex) {
|
|
@@ -1884,7 +2272,7 @@ function buildPlan(detection, options) {
|
|
|
1884
2272
|
type: 'json-transform',
|
|
1885
2273
|
path: detection.paths.codexHooks,
|
|
1886
2274
|
label: 'Codex native hooks',
|
|
1887
|
-
transform: upsertCodexHooks,
|
|
2275
|
+
transform: (filePath) => retarget(upsertCodexHooks(filePath)),
|
|
1888
2276
|
});
|
|
1889
2277
|
}
|
|
1890
2278
|
if (detection.cline) {
|
|
@@ -1893,7 +2281,7 @@ function buildPlan(detection, options) {
|
|
|
1893
2281
|
type: 'owned-executable',
|
|
1894
2282
|
path: hook.path,
|
|
1895
2283
|
label: hook.label,
|
|
1896
|
-
content: hook.content,
|
|
2284
|
+
content: retarget(hook.content),
|
|
1897
2285
|
mode: 0o755,
|
|
1898
2286
|
conflict_fix: 'Move or remove the existing owner-managed Cline hook after owner review, then run npx @getmarrow/install --repair.',
|
|
1899
2287
|
});
|
|
@@ -1904,7 +2292,7 @@ function buildPlan(detection, options) {
|
|
|
1904
2292
|
type: 'json-transform',
|
|
1905
2293
|
path: detection.paths.windsurfHooks,
|
|
1906
2294
|
label: 'Windsurf native hooks',
|
|
1907
|
-
transform: upsertWindsurfHooks,
|
|
2295
|
+
transform: (filePath) => retarget(upsertWindsurfHooks(filePath)),
|
|
1908
2296
|
});
|
|
1909
2297
|
}
|
|
1910
2298
|
if (detection.gemini) {
|
|
@@ -1912,27 +2300,27 @@ function buildPlan(detection, options) {
|
|
|
1912
2300
|
type: 'json-transform',
|
|
1913
2301
|
path: detection.paths.geminiSettings,
|
|
1914
2302
|
label: 'Gemini CLI native hooks',
|
|
1915
|
-
transform: upsertGeminiHooks,
|
|
2303
|
+
transform: (filePath) => retarget(upsertGeminiHooks(filePath)),
|
|
1916
2304
|
});
|
|
1917
2305
|
}
|
|
1918
2306
|
writes.push({
|
|
1919
2307
|
type: 'json-transform',
|
|
1920
2308
|
path: detection.paths.mcpJson,
|
|
1921
2309
|
label: 'Project MCP server config',
|
|
1922
|
-
transform: (filePath) => upsertMcpServerConfig(filePath, { agentId, baseUrl }),
|
|
2310
|
+
transform: (filePath) => retarget(upsertMcpServerConfig(filePath, { agentId, baseUrl, toolProfile: options.toolProfile })),
|
|
1923
2311
|
});
|
|
1924
2312
|
if (detection.cursor) {
|
|
1925
2313
|
writes.push({
|
|
1926
2314
|
type: 'json-transform',
|
|
1927
2315
|
path: detection.paths.cursorHooks,
|
|
1928
2316
|
label: 'Cursor native hooks',
|
|
1929
|
-
transform: upsertCursorHooks,
|
|
2317
|
+
transform: (filePath) => retarget(upsertCursorHooks(filePath)),
|
|
1930
2318
|
});
|
|
1931
2319
|
writes.push({
|
|
1932
2320
|
type: 'json-transform',
|
|
1933
2321
|
path: detection.paths.cursorMcp,
|
|
1934
2322
|
label: 'Cursor MCP server config',
|
|
1935
|
-
transform: (filePath) => upsertMcpServerConfig(filePath, { agentId, baseUrl }),
|
|
2323
|
+
transform: (filePath) => retarget(upsertMcpServerConfig(filePath, { agentId, baseUrl, toolProfile: options.toolProfile })),
|
|
1936
2324
|
});
|
|
1937
2325
|
}
|
|
1938
2326
|
}
|
|
@@ -1942,7 +2330,7 @@ function buildPlan(detection, options) {
|
|
|
1942
2330
|
type: 'md-block',
|
|
1943
2331
|
path: detection.paths.agentsMd,
|
|
1944
2332
|
label: 'Agent instructions',
|
|
1945
|
-
block: passiveInstructions(),
|
|
2333
|
+
block: retarget(passiveInstructions()),
|
|
1946
2334
|
});
|
|
1947
2335
|
}
|
|
1948
2336
|
|
|
@@ -1951,11 +2339,11 @@ function buildPlan(detection, options) {
|
|
|
1951
2339
|
type: 'file',
|
|
1952
2340
|
path: detection.paths.cursorRules,
|
|
1953
2341
|
label: 'Cursor Marrow rule',
|
|
1954
|
-
content: passiveInstructions().replace(/<!--[^>]+-->/g, '').trim() + '\n',
|
|
2342
|
+
content: retarget(passiveInstructions()).replace(/<!--[^>]+-->/g, '').trim() + '\n',
|
|
1955
2343
|
});
|
|
1956
2344
|
}
|
|
1957
2345
|
|
|
1958
|
-
return { mode, root: detection.root, writes };
|
|
2346
|
+
return { mode, root: detection.root, writes, mcp_target_version: mcpTargetVersion };
|
|
1959
2347
|
}
|
|
1960
2348
|
|
|
1961
2349
|
function assertContainedManagedTarget(root, targetPath) {
|
|
@@ -2021,9 +2409,16 @@ function applyPlan(plan, options) {
|
|
|
2021
2409
|
const prepared = plan.writes.map((write) => {
|
|
2022
2410
|
const fileExists = exists(write.path);
|
|
2023
2411
|
const before = safeRead(write.path);
|
|
2412
|
+
const aheadUnverifiedVersions = unverifiedAheadMcpVersions(
|
|
2413
|
+
mcpVersionsInText(before),
|
|
2414
|
+
plan.mcp_target_version,
|
|
2415
|
+
);
|
|
2416
|
+
const automaticRepairSuppressed = aheadUnverifiedVersions.length > 0;
|
|
2024
2417
|
let after;
|
|
2025
2418
|
let hookConflict = false;
|
|
2026
|
-
if (
|
|
2419
|
+
if (automaticRepairSuppressed) {
|
|
2420
|
+
after = before;
|
|
2421
|
+
} else if (write.type === 'file') {
|
|
2027
2422
|
if (write.overwrite === false && before) {
|
|
2028
2423
|
after = before;
|
|
2029
2424
|
} else {
|
|
@@ -2046,21 +2441,43 @@ function applyPlan(plan, options) {
|
|
|
2046
2441
|
|
|
2047
2442
|
const beforeMode = fileExists ? fs.lstatSync(write.path).mode & 0o777 : null;
|
|
2048
2443
|
const modeChanged = !hookConflict && typeof write.mode === 'number' && beforeMode !== write.mode;
|
|
2049
|
-
return {
|
|
2444
|
+
return {
|
|
2445
|
+
write,
|
|
2446
|
+
before,
|
|
2447
|
+
after,
|
|
2448
|
+
hookConflict,
|
|
2449
|
+
modeChanged,
|
|
2450
|
+
automaticRepairSuppressed,
|
|
2451
|
+
aheadUnverifiedVersions,
|
|
2452
|
+
};
|
|
2050
2453
|
});
|
|
2051
2454
|
|
|
2052
2455
|
const changes = [];
|
|
2053
|
-
for (const {
|
|
2456
|
+
for (const {
|
|
2457
|
+
write,
|
|
2458
|
+
before,
|
|
2459
|
+
after,
|
|
2460
|
+
hookConflict,
|
|
2461
|
+
modeChanged,
|
|
2462
|
+
automaticRepairSuppressed,
|
|
2463
|
+
aheadUnverifiedVersions,
|
|
2464
|
+
} of prepared) {
|
|
2054
2465
|
const contentChanged = before !== after;
|
|
2055
|
-
const changed = !hookConflict && (contentChanged || modeChanged);
|
|
2056
|
-
const writeApplied = Boolean(options.yes && !options.dryRun && !options.doctor
|
|
2466
|
+
const changed = !automaticRepairSuppressed && !hookConflict && (contentChanged || modeChanged);
|
|
2467
|
+
const writeApplied = Boolean(options.yes && !options.dryRun && !options.doctor
|
|
2468
|
+
&& !hookConflict && !automaticRepairSuppressed);
|
|
2057
2469
|
changes.push({
|
|
2058
2470
|
path: write.path,
|
|
2059
2471
|
label: write.label,
|
|
2060
2472
|
changed,
|
|
2061
2473
|
applied: changed && writeApplied,
|
|
2062
|
-
already_present: !changed && !hookConflict,
|
|
2474
|
+
already_present: !changed && !hookConflict && !automaticRepairSuppressed,
|
|
2063
2475
|
hook_conflict: hookConflict,
|
|
2476
|
+
automatic_repair_suppressed: automaticRepairSuppressed,
|
|
2477
|
+
...(automaticRepairSuppressed ? {
|
|
2478
|
+
ahead_unverified_versions: aheadUnverifiedVersions,
|
|
2479
|
+
exact_fix: mcpRegistryVerificationAction(aheadUnverifiedVersions),
|
|
2480
|
+
} : {}),
|
|
2064
2481
|
...(hookConflict ? { exact_fix: write.conflict_fix } : {}),
|
|
2065
2482
|
});
|
|
2066
2483
|
if (contentChanged && writeApplied) {
|
|
@@ -2106,12 +2523,14 @@ function runtimeGateVerified(runtime) {
|
|
|
2106
2523
|
}
|
|
2107
2524
|
|
|
2108
2525
|
async function runSelfTest(options) {
|
|
2109
|
-
|
|
2526
|
+
const initialProfile = initialToolProfileReport(options.toolProfile);
|
|
2527
|
+
if (!options.selfTest) return { skipped: true, reason: 'disabled', mcp_tool_profile: initialProfile };
|
|
2110
2528
|
if (!options.apiKey) {
|
|
2111
2529
|
return {
|
|
2112
2530
|
skipped: true,
|
|
2113
2531
|
reason: 'missing MARROW_API_KEY',
|
|
2114
2532
|
exact_fix: 'export MARROW_API_KEY=mrw_live_... && npx @getmarrow/install --repair',
|
|
2533
|
+
mcp_tool_profile: initialProfile,
|
|
2115
2534
|
};
|
|
2116
2535
|
}
|
|
2117
2536
|
|
|
@@ -2124,7 +2543,7 @@ async function runSelfTest(options) {
|
|
|
2124
2543
|
'x-marrow-package-version': INSTALLER_ADAPTER_VERSION,
|
|
2125
2544
|
'x-marrow-install-version': INSTALLER_ADAPTER_VERSION,
|
|
2126
2545
|
'x-marrow-sdk-version': SDK_ADAPTER_VERSION,
|
|
2127
|
-
'x-marrow-mcp-version':
|
|
2546
|
+
'x-marrow-mcp-version': executableMcpTarget(options).version,
|
|
2128
2547
|
};
|
|
2129
2548
|
if (options.agentId) headers['x-marrow-agent-id'] = options.agentId;
|
|
2130
2549
|
|
|
@@ -2157,6 +2576,14 @@ async function runSelfTest(options) {
|
|
|
2157
2576
|
});
|
|
2158
2577
|
|
|
2159
2578
|
const status = await requestJson(`${baseUrl}/v1/agent/status`, { headers });
|
|
2579
|
+
const context = await requestJson(`${baseUrl}/v1/agent/context`, { headers })
|
|
2580
|
+
.catch(() => null);
|
|
2581
|
+
const toolProfile = buildMcpToolProfileReport(
|
|
2582
|
+
options.toolProfile,
|
|
2583
|
+
status.mcp_tool_profile,
|
|
2584
|
+
context?.primary_tool_availability,
|
|
2585
|
+
Boolean(options.activation),
|
|
2586
|
+
);
|
|
2160
2587
|
const runtime = await requestJson(`${baseUrl}/v1/agent/runtime`, {
|
|
2161
2588
|
method: 'POST',
|
|
2162
2589
|
headers,
|
|
@@ -2291,6 +2718,7 @@ async function runSelfTest(options) {
|
|
|
2291
2718
|
}
|
|
2292
2719
|
return {
|
|
2293
2720
|
skipped: false,
|
|
2721
|
+
mcp_tool_profile: toolProfile,
|
|
2294
2722
|
decision_id: decisionId,
|
|
2295
2723
|
active: Boolean(status.enabled ?? status.ok),
|
|
2296
2724
|
health: status.health || null,
|
|
@@ -2480,11 +2908,34 @@ function printReport(report) {
|
|
|
2480
2908
|
|
|
2481
2909
|
process.stdout.write('\nPlanned changes:\n');
|
|
2482
2910
|
for (const change of report.changes) {
|
|
2483
|
-
const marker = change.
|
|
2911
|
+
const marker = change.automatic_repair_suppressed
|
|
2912
|
+
? 'preserved unverified-ahead surface; repair suppressed'
|
|
2913
|
+
: change.applied ? 'wrote' : change.changed ? 'would write' : 'unchanged';
|
|
2484
2914
|
process.stdout.write(`- ${marker}: ${change.label} (${change.path})\n`);
|
|
2915
|
+
if (change.automatic_repair_suppressed && change.exact_fix) {
|
|
2916
|
+
process.stdout.write(` exact verification: ${change.exact_fix}\n`);
|
|
2917
|
+
}
|
|
2485
2918
|
}
|
|
2486
2919
|
|
|
2487
2920
|
process.stdout.write('\nSelf-test:\n');
|
|
2921
|
+
const toolProfile = report.selfTest.mcp_tool_profile || report.toolProfile;
|
|
2922
|
+
if (toolProfile) {
|
|
2923
|
+
process.stdout.write(`- configured tool profile: ${toolProfile.configured_profile}\n`);
|
|
2924
|
+
process.stdout.write(`- effective tool profile: ${toolProfile.effective_profile}\n`);
|
|
2925
|
+
process.stdout.write(`- expected visible tools: ${toolProfile.expected_visible_count == null ? 'complete catalog (awaiting reloaded MCP count)' : toolProfile.expected_visible_count}\n`);
|
|
2926
|
+
process.stdout.write(`- actual visible tools: ${toolProfile.actual_visible_count == null ? 'unavailable until process reload' : toolProfile.actual_visible_count}\n`);
|
|
2927
|
+
process.stdout.write(`- visible tool names: ${toolProfile.visibility_live ? toolProfile.visible_tool_names.join(', ') : 'unavailable until process reload'}\n`);
|
|
2928
|
+
process.stdout.write(`- profile live: ${toolProfile.visibility_live ? 'yes' : 'no'}\n`);
|
|
2929
|
+
const projection = toolProfile.backend_entitlement_projection;
|
|
2930
|
+
const availability = projection?.primary_tool_availability;
|
|
2931
|
+
if (projection?.evidence_state === 'available' && availability?.entitlement_evidence?.state === 'available') {
|
|
2932
|
+
process.stdout.write(`- backend-projected entitled tools: ${availability.counts.entitled}\n`);
|
|
2933
|
+
process.stdout.write(`- backend-projected upgrade-required tools: ${availability.counts.upgrade_required}\n`);
|
|
2934
|
+
process.stdout.write(`- backend projection source: ${projection.source}; authorizes calls: no\n`);
|
|
2935
|
+
} else {
|
|
2936
|
+
process.stdout.write(`- backend-projected entitlements: unavailable (source: ${projection?.source || 'backend_projection_not_provided'}; non-authorizing)\n`);
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2488
2939
|
if (report.selfTest.skipped) {
|
|
2489
2940
|
process.stdout.write(`- skipped: ${report.selfTest.reason}\n`);
|
|
2490
2941
|
if (report.selfTest.exact_fix) process.stdout.write(`- exact fix: ${report.selfTest.exact_fix}\n`);
|
|
@@ -2613,6 +3064,8 @@ function printReport(report) {
|
|
|
2613
3064
|
const processes = report.doctor.mcpProcesses;
|
|
2614
3065
|
process.stdout.write(`- MCP process versions: ${processes.active_versions.length ? processes.active_versions.join(', ') : processes.active_processes ? 'unknown' : 'none'}\n`);
|
|
2615
3066
|
process.stdout.write(`- stale/mixed/version-unknown MCP clients: ${processes.healthy ? 'no' : 'yes'}\n`);
|
|
3067
|
+
if (processes.ahead_unverified_versions.length) process.stdout.write(`- unverified-ahead MCP clients: ${processes.ahead_unverified_versions.join(', ')}\n`);
|
|
3068
|
+
if (processes.automatic_repair_suppressed) process.stdout.write('- automatic MCP process repair: suppressed pending official registry verification\n');
|
|
2616
3069
|
if (processes.restart_instruction) process.stdout.write(`- restart required: ${processes.restart_instruction}\n`);
|
|
2617
3070
|
if (processes.verification_command) process.stdout.write(`- verify repair: ${processes.verification_command}\n`);
|
|
2618
3071
|
}
|
|
@@ -2620,6 +3073,8 @@ function printReport(report) {
|
|
|
2620
3073
|
const configurations = report.doctor.mcpConfigurations;
|
|
2621
3074
|
process.stdout.write(`- configured MCP versions: ${configurations.configured_versions.length ? configurations.configured_versions.join(', ') : 'none pinned'}\n`);
|
|
2622
3075
|
process.stdout.write(`- stale/mixed/version-unknown MCP configuration: ${configurations.healthy ? 'no' : 'yes'}\n`);
|
|
3076
|
+
if (configurations.ahead_unverified_versions.length) process.stdout.write(`- unverified-ahead MCP configuration: ${configurations.ahead_unverified_versions.join(', ')}\n`);
|
|
3077
|
+
if (configurations.automatic_repair_suppressed) process.stdout.write('- automatic MCP configuration repair: suppressed pending official registry verification\n');
|
|
2623
3078
|
}
|
|
2624
3079
|
if (report.doctor.recommendedFix) process.stdout.write(`- recommended fix: ${report.doctor.recommendedFix}\n`);
|
|
2625
3080
|
process.stdout.write(`- live health: ${report.doctor.healthCommand}\n`);
|
|
@@ -2650,14 +3105,30 @@ async function install(options) {
|
|
|
2650
3105
|
if (options.repair && options.yes !== true && !options.dryRun && !options.doctor) {
|
|
2651
3106
|
throw new Error('repair requires explicit write authorization (--yes)');
|
|
2652
3107
|
}
|
|
3108
|
+
options.toolProfile = resolveToolProfile(options.toolProfile === undefined
|
|
3109
|
+
? process.env.MARROW_TOOL_PROFILE
|
|
3110
|
+
: options.toolProfile);
|
|
2653
3111
|
const detection = detectEnvironment(options.cwd);
|
|
2654
3112
|
const client = detectedClient(detection);
|
|
2655
3113
|
options.client = client;
|
|
2656
3114
|
options.agentId = String(options.agentId || '').trim() || stableAgentId(detection.root, client);
|
|
3115
|
+
const observedMcpProcesses = inspectMcpProcesses({ commands: options.processCommands });
|
|
3116
|
+
const observedMcpConfigurations = inspectMcpConfigurations(detection, { paths: options.mcpConfigPaths });
|
|
3117
|
+
const registryMetadata = await readMcpRegistryMetadata(options);
|
|
3118
|
+
const latestTargetOperation = Boolean(options.doctor || options.repair || options.update);
|
|
3119
|
+
const mcpTarget = resolveMcpTargetVersion({
|
|
3120
|
+
currentVersions: latestTargetOperation ? [
|
|
3121
|
+
...observedMcpProcesses.active_versions,
|
|
3122
|
+
...observedMcpConfigurations.configured_versions,
|
|
3123
|
+
] : [],
|
|
3124
|
+
registryMetadata: latestTargetOperation ? registryMetadata : null,
|
|
3125
|
+
});
|
|
3126
|
+
options.mcpTarget = mcpTarget;
|
|
3127
|
+
options.mcpTargetVersion = mcpTarget.version;
|
|
2657
3128
|
const plan = buildPlan(detection, options);
|
|
2658
3129
|
const writeMode = options.doctor ? 'doctor' : options.dryRun ? 'dry-run' : options.repair ? 'repair' : options.yes ? 'write' : 'dry-run';
|
|
2659
3130
|
const changes = applyPlan(plan, options);
|
|
2660
|
-
|
|
3131
|
+
let profile = activationProfile(detection, plan, changes, client);
|
|
2661
3132
|
options.activation = options.activate ? {
|
|
2662
3133
|
harness: client,
|
|
2663
3134
|
agent_id: options.agentId,
|
|
@@ -2688,19 +3159,66 @@ async function install(options) {
|
|
|
2688
3159
|
? repairConfigDiagnostics(configDiagnostics)
|
|
2689
3160
|
: [];
|
|
2690
3161
|
const envHints = options.apiKey ? [] : findLikelyEnvFiles(detection);
|
|
2691
|
-
const mcpProcesses = inspectMcpProcesses({
|
|
2692
|
-
|
|
3162
|
+
const mcpProcesses = inspectMcpProcesses({
|
|
3163
|
+
commands: options.processCommands,
|
|
3164
|
+
expectedTarget: mcpTarget,
|
|
3165
|
+
});
|
|
3166
|
+
const mcpConfigurations = inspectMcpConfigurations(detection, {
|
|
3167
|
+
paths: options.mcpConfigPaths,
|
|
3168
|
+
expectedTarget: mcpTarget,
|
|
3169
|
+
});
|
|
3170
|
+
const aheadUnverifiedVersions = [...new Set([
|
|
3171
|
+
...mcpProcesses.ahead_unverified_versions,
|
|
3172
|
+
...mcpConfigurations.ahead_unverified_versions,
|
|
3173
|
+
...changes.flatMap((change) => change.ahead_unverified_versions || []),
|
|
3174
|
+
])].sort((left, right) => compareMcpVersions(left, right));
|
|
3175
|
+
const automaticMcpRepairSuppressed = aheadUnverifiedVersions.length > 0;
|
|
3176
|
+
const registryVerificationAction = automaticMcpRepairSuppressed
|
|
3177
|
+
? mcpRegistryVerificationAction(aheadUnverifiedVersions)
|
|
3178
|
+
: null;
|
|
3179
|
+
if (automaticMcpRepairSuppressed) {
|
|
3180
|
+
profile = {
|
|
3181
|
+
...profile,
|
|
3182
|
+
configuration_complete: false,
|
|
3183
|
+
complete: false,
|
|
3184
|
+
automatic_repair_suppressed: true,
|
|
3185
|
+
ahead_unverified_versions: aheadUnverifiedVersions,
|
|
3186
|
+
exact_fix: registryVerificationAction,
|
|
3187
|
+
};
|
|
3188
|
+
}
|
|
2693
3189
|
let selfTest;
|
|
2694
3190
|
try {
|
|
2695
3191
|
selfTest = await runSelfTest(options);
|
|
2696
3192
|
} catch (error) {
|
|
2697
3193
|
const message = error instanceof Error ? error.message : String(error);
|
|
2698
3194
|
if (options.activate) throw new Error(`Marrow activation failed: ${message}`);
|
|
2699
|
-
selfTest = {
|
|
3195
|
+
selfTest = {
|
|
3196
|
+
skipped: false,
|
|
3197
|
+
active: false,
|
|
3198
|
+
error: message,
|
|
3199
|
+
mcp_tool_profile: initialToolProfileReport(options.toolProfile),
|
|
3200
|
+
};
|
|
2700
3201
|
}
|
|
3202
|
+
selfTest = alignMcpRecommendationVersions(
|
|
3203
|
+
selfTest,
|
|
3204
|
+
mcpTarget.version,
|
|
3205
|
+
'',
|
|
3206
|
+
registryVerificationAction,
|
|
3207
|
+
);
|
|
2701
3208
|
if (options.activate && !selfTest.activation_verified) {
|
|
2702
3209
|
throw new Error('Marrow activation failed: server confirmation was not returned');
|
|
2703
3210
|
}
|
|
3211
|
+
const harnessReload = harnessReloadPlan(detection, changes);
|
|
3212
|
+
if (harnessReload.required && selfTest.mcp_tool_profile) {
|
|
3213
|
+
selfTest.mcp_tool_profile = {
|
|
3214
|
+
...selfTest.mcp_tool_profile,
|
|
3215
|
+
visible_tool_count: null,
|
|
3216
|
+
actual_visible_count: null,
|
|
3217
|
+
visible_tool_names: [],
|
|
3218
|
+
visibility_live: false,
|
|
3219
|
+
reload_required: true,
|
|
3220
|
+
};
|
|
3221
|
+
}
|
|
2704
3222
|
const changedConfig = changes.some((change) => change.applied) || configRepairs.some((repair) => repair.changed);
|
|
2705
3223
|
const selfTestPassed = Boolean(!selfTest.skipped && selfTest.active && !selfTest.error);
|
|
2706
3224
|
const controllerPlatform = options.controllerPlatform || process.platform;
|
|
@@ -2747,7 +3265,10 @@ async function install(options) {
|
|
|
2747
3265
|
attempted: true,
|
|
2748
3266
|
fixedConfig: changedConfig,
|
|
2749
3267
|
selfTestPassed,
|
|
2750
|
-
|
|
3268
|
+
automaticMcpRepairSuppressed,
|
|
3269
|
+
message: automaticMcpRepairSuppressed
|
|
3270
|
+
? registryVerificationAction
|
|
3271
|
+
: selfTestPassed
|
|
2751
3272
|
? selfTest.health === 'healthy'
|
|
2752
3273
|
? 'I fixed Marrow passive config, one-call runtime is active, and self-test passed.'
|
|
2753
3274
|
: `I fixed Marrow passive config and self-test passed; status is ${selfTest.health || 'unknown'}${selfTest.next_action ? `. Next action: ${selfTest.next_action}` : ''}.`
|
|
@@ -2759,9 +3280,10 @@ async function install(options) {
|
|
|
2759
3280
|
|
|
2760
3281
|
return {
|
|
2761
3282
|
root: detection.root,
|
|
2762
|
-
adapterProvenance:
|
|
3283
|
+
adapterProvenance: adapterProvenanceForMcpTarget(mcpTarget),
|
|
2763
3284
|
mode: plan.mode,
|
|
2764
3285
|
writeMode,
|
|
3286
|
+
toolProfile: selfTest.mcp_tool_profile || initialToolProfileReport(options.toolProfile),
|
|
2765
3287
|
detected: {
|
|
2766
3288
|
node: detection.node,
|
|
2767
3289
|
python: detection.python,
|
|
@@ -2784,7 +3306,7 @@ async function install(options) {
|
|
|
2784
3306
|
receipt: selfTest.activation_receipt || null,
|
|
2785
3307
|
profile,
|
|
2786
3308
|
},
|
|
2787
|
-
harnessReload
|
|
3309
|
+
harnessReload,
|
|
2788
3310
|
firstCapture: firstCapturePath(detection, options.agentId),
|
|
2789
3311
|
changes,
|
|
2790
3312
|
doctor: {
|
|
@@ -2794,7 +3316,10 @@ async function install(options) {
|
|
|
2794
3316
|
missingHooks: changes.filter((change) => change.changed).map((change) => change.label),
|
|
2795
3317
|
mcpProcesses,
|
|
2796
3318
|
mcpConfigurations,
|
|
2797
|
-
|
|
3319
|
+
ahead_unverified_versions: aheadUnverifiedVersions,
|
|
3320
|
+
automatic_mcp_repair_suppressed: automaticMcpRepairSuppressed,
|
|
3321
|
+
registry_verification_action: registryVerificationAction,
|
|
3322
|
+
recommendedFix: registryVerificationAction || mcpProcesses.exact_fix || mcpConfigurations.exact_fix || configDiagnostics.npm_token.recommended_fix || (!options.apiKey
|
|
2798
3323
|
? envHints.length
|
|
2799
3324
|
? `MARROW_API_KEY was found in a likely env file at ${envHints[0]}. Load that key from trusted secret storage, export only MARROW_API_KEY, then run npx @getmarrow/install --repair.`
|
|
2800
3325
|
: 'Set MARROW_API_KEY, then run npx @getmarrow/install --repair.'
|
|
@@ -2812,10 +3337,14 @@ async function install(options) {
|
|
|
2812
3337
|
...(options.keyFromArg
|
|
2813
3338
|
? ['Avoid --key in shared shells because command-line arguments can be visible in process listings. Prefer MARROW_API_KEY in your environment or secret manager.']
|
|
2814
3339
|
: []),
|
|
2815
|
-
...(
|
|
3340
|
+
...(mcpProcesses.ahead_unverified
|
|
3341
|
+
? ['Registry-unverified ahead Marrow MCP clients are active. Automatic repair is suppressed; run the exact registry verification action before owner-directed replacement.']
|
|
3342
|
+
: !mcpProcesses.healthy
|
|
2816
3343
|
? ['Stale, mixed, or version-unknown Marrow MCP clients are active. Run the exact repair command, then restart every owning harness.']
|
|
2817
3344
|
: []),
|
|
2818
|
-
...(
|
|
3345
|
+
...(mcpConfigurations.ahead_unverified || changes.some((change) => change.automatic_repair_suppressed)
|
|
3346
|
+
? ['Registry-unverified ahead Marrow MCP configuration was preserved. It was not copied into other managed targets, and automatic repair is suppressed for that surface.']
|
|
3347
|
+
: !mcpConfigurations.healthy
|
|
2819
3348
|
? ['Stale, mixed, or version-unknown Marrow MCP versions remain in owner configuration. Repair each owning workspace and restart its harness.']
|
|
2820
3349
|
: []),
|
|
2821
3350
|
],
|
|
@@ -2866,6 +3395,10 @@ module.exports = {
|
|
|
2866
3395
|
GROK_SESSION_END_HOOK_COMMAND,
|
|
2867
3396
|
GROK_NATIVE_HOOK_MATCHER,
|
|
2868
3397
|
printReport,
|
|
3398
|
+
buildMcpToolProfileReport,
|
|
3399
|
+
resolveMcpTargetVersion,
|
|
3400
|
+
resolveToolProfile,
|
|
3401
|
+
PRIMARY_TOOL_NAMES,
|
|
2869
3402
|
ADAPTER_PROVENANCE,
|
|
2870
3403
|
HARNESS_CAPABILITY_REGISTRY,
|
|
2871
3404
|
defaultHarnessInstallMatrix,
|