@getmarrow/install 0.1.56 → 0.1.58
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 +9 -1
- package/package.json +2 -1
- package/src/installer.js +196 -20
package/README.md
CHANGED
|
@@ -101,7 +101,15 @@ npx @getmarrow/install controller stop
|
|
|
101
101
|
|
|
102
102
|
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.
|
|
103
103
|
|
|
104
|
-
## What's New in v0.1.
|
|
104
|
+
## What's New in v0.1.58
|
|
105
|
+
|
|
106
|
+
v0.1.58 pins MCP `3.9.89` from source `ff229e17419f65aeebd7fa7754dd61cbda61900d` with packed integrity `sha512-KC/P4dStzOOfZKxSwigQE4TWCt1TBgYINQzlXrnA5jpYHRrT2Jo73vaV4FiucVPOT+xyU1uCci+bmJoC35aZ0g==`; SDK remains `3.7.63`. Supported native hooks now include their host read, search, and status surfaces so MCP's private local session loop guard can stop unchanged successful checks, polls, and failed retries without adding routine backend writes. Install and update run `marrow-mcp loop-guard-self-test` against isolated temporary state and report configuration, isolated proof, and live host observation separately. Missing local control state stays enabled by default; an explicit owner disable is preserved byte-for-byte and prevents the controller from starting. Grok's trusted global hook file is created only at its direct owner-safe path, managed files are reconciled, and unmanaged files are preserved for owner review. Restart and complete each host's hook trust or review flow before relying on live enforcement.
|
|
107
|
+
|
|
108
|
+
### Previous release: v0.1.57
|
|
109
|
+
|
|
110
|
+
v0.1.57 pins the npm-verified MCP `3.9.88` and SDK `3.7.63` packages. Doctor now recognizes an installed and lockfile-verified SDK `3.7.63` instead of suggesting `3.7.62`. If a workspace has a newer stable SDK version, doctor preserves it and asks for official registry verification before replacement. The generated MCP setup, hooks, and update instructions target `3.9.88`; restart the owning harness and run `npx @getmarrow/install@latest doctor --self-test` after activation.
|
|
111
|
+
|
|
112
|
+
## Previous: v0.1.56
|
|
105
113
|
|
|
106
114
|
v0.1.56 pins the officially verified MCP `3.9.80` release from source `ee1eda3f201965a6530256accbdf175660dd8ad6` with registry integrity `sha512-uou3X18pESV39EMmddDrYN7yd6MrZzosrnQ1eRtvK5j3yuBLAlupj5kQVrrKEWAL6xwuLebXq2isivK2O/2WrA==`; SDK remains `3.7.62`. Doctor now directs stale, mixed, or version-unknown MCP installations through one `npx -y @getmarrow/install@latest update`, followed by one owning-harness restart and one `doctor --self-test` verification. The update resolves one official target and applies it consistently across detected Marrow-managed instructions, MCP launch configuration, and supported native hooks without claiming that already-running processes changed.
|
|
107
115
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getmarrow/install",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.58",
|
|
4
4
|
"description": "Universal installer and governed runner for Marrow agent fleets.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"marrow-install": "bin/marrow-install.js"
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"mcp",
|
|
17
17
|
"installer",
|
|
18
18
|
"passive-runtime",
|
|
19
|
+
"loop-guard",
|
|
19
20
|
"governance",
|
|
20
21
|
"proof-packs",
|
|
21
22
|
"agent-control-plane"
|
package/src/installer.js
CHANGED
|
@@ -2,6 +2,7 @@ const fs = require('node:fs');
|
|
|
2
2
|
const path = require('node:path');
|
|
3
3
|
const os = require('node:os');
|
|
4
4
|
const crypto = require('node:crypto');
|
|
5
|
+
const { spawnSync } = require('node:child_process');
|
|
5
6
|
const { version: INSTALLER_ADAPTER_VERSION } = require('../package.json');
|
|
6
7
|
const { controllerStatus, controllerSupportedPlatform, ensureGovernanceController } = require('./controller-manager');
|
|
7
8
|
const { firstCapturePath, harnessReloadPlan } = require('./first-hour');
|
|
@@ -10,11 +11,11 @@ const { evidence: localControlEvidence } = require('./control-state');
|
|
|
10
11
|
const DEFAULT_BASE_URL = 'https://api.getmarrow.ai';
|
|
11
12
|
const MARROW_BLOCK_START = '<!-- marrow:passive-start -->';
|
|
12
13
|
const MARROW_BLOCK_END = '<!-- marrow:passive-end -->';
|
|
13
|
-
const MCP_ADAPTER_VERSION = '3.9.
|
|
14
|
-
const MCP_ADAPTER_SOURCE_SHA = '
|
|
15
|
-
const MCP_ADAPTER_INTEGRITY = 'sha512-
|
|
16
|
-
const SDK_ADAPTER_VERSION = '3.7.
|
|
17
|
-
const SDK_ADAPTER_INTEGRITY = 'sha512-
|
|
14
|
+
const MCP_ADAPTER_VERSION = '3.9.89';
|
|
15
|
+
const MCP_ADAPTER_SOURCE_SHA = 'ff229e17419f65aeebd7fa7754dd61cbda61900d';
|
|
16
|
+
const MCP_ADAPTER_INTEGRITY = 'sha512-KC/P4dStzOOfZKxSwigQE4TWCt1TBgYINQzlXrnA5jpYHRrT2Jo73vaV4FiucVPOT+xyU1uCci+bmJoC35aZ0g==';
|
|
17
|
+
const SDK_ADAPTER_VERSION = '3.7.63';
|
|
18
|
+
const SDK_ADAPTER_INTEGRITY = 'sha512-5BiV1P0J1NMVdgjqxqULfM+zGrTjHBuWm1amKZWba4yW6C3t10VlhIlj/A+4YKbKqvyZe8gXgUwW+VrYDbVQrQ==';
|
|
18
19
|
const SDK_ADAPTER_TARBALL = `https://registry.npmjs.org/@getmarrow/sdk/-/sdk-${SDK_ADAPTER_VERSION}.tgz`;
|
|
19
20
|
const MCP_PACKAGE_SPEC = `@getmarrow/mcp@${MCP_ADAPTER_VERSION}`;
|
|
20
21
|
const ADAPTER_PROVENANCE = Object.freeze({
|
|
@@ -23,7 +24,7 @@ const ADAPTER_PROVENANCE = Object.freeze({
|
|
|
23
24
|
version: MCP_ADAPTER_VERSION,
|
|
24
25
|
source_sha: MCP_ADAPTER_SOURCE_SHA,
|
|
25
26
|
integrity: MCP_ADAPTER_INTEGRITY,
|
|
26
|
-
integrity_state: '
|
|
27
|
+
integrity_state: 'verified_npm_registry_metadata',
|
|
27
28
|
}),
|
|
28
29
|
sdk: Object.freeze({
|
|
29
30
|
package: '@getmarrow/sdk',
|
|
@@ -68,6 +69,13 @@ function compareMcpVersions(left, right) {
|
|
|
68
69
|
return 0;
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
function declaredStableSdkLowerBound(spec) {
|
|
73
|
+
if (typeof spec !== 'string') return null;
|
|
74
|
+
const match = spec.trim().match(/^(v|\^|~|>=|>)?(\d{1,6}\.\d{1,6}\.\d{1,9})$/);
|
|
75
|
+
if (!match || !parsedStableMcpVersion(match[2])) return null;
|
|
76
|
+
return { version: match[2], exclusive: match[1] === '>' };
|
|
77
|
+
}
|
|
78
|
+
|
|
71
79
|
function compatibleMcpTargetVersion(value) {
|
|
72
80
|
const candidate = parsedStableMcpVersion(value);
|
|
73
81
|
const sealed = parsedStableMcpVersion(MCP_ADAPTER_VERSION);
|
|
@@ -271,10 +279,10 @@ const GROK_PRE_ACTION_GUARD_SOURCE = [
|
|
|
271
279
|
'child.stdin.end(Buffer.concat(input));}catch{fail();}});',
|
|
272
280
|
].join('');
|
|
273
281
|
const GROK_PRE_ACTION_HOOK_COMMAND = `node -e '${GROK_PRE_ACTION_GUARD_SOURCE}'`;
|
|
274
|
-
const NATIVE_HOOK_MATCHER = 'Bash|Edit|Write|MultiEdit|mcp__(?!
|
|
282
|
+
const NATIVE_HOOK_MATCHER = 'Bash|Edit|Write|MultiEdit|Read|Glob|Grep|Search|WebSearch|Task|functions\\.(?!mcp__marrow__marrow_).*|mcp__(?!marrow__marrow_).*';
|
|
275
283
|
const CODEX_NATIVE_HOOK_MATCHER = 'Bash|apply_patch|Edit|Write|MultiEdit|mcp__(?!marrow__marrow_).*|functions\\.(?!marrow_).*';
|
|
276
|
-
const CURSOR_NATIVE_HOOK_MATCHER = 'Shell|Write|Delete|Task|MCP:(?!marrow(?:_.*|:marrow_.*)$).*';
|
|
277
|
-
const GEMINI_NATIVE_HOOK_MATCHER = '^(?:run_shell_command|write_file|replace|edit_file|delete_file|mcp_(?!marrow_marrow_)[A-Za-z0-9_]{1,192})$';
|
|
284
|
+
const CURSOR_NATIVE_HOOK_MATCHER = 'Shell|Write|Delete|Task|Read|Glob|Grep|Search|WebSearch|List|MCP:(?!marrow(?:_.*|:marrow_.*)$).*';
|
|
285
|
+
const GEMINI_NATIVE_HOOK_MATCHER = '^(?:run_shell_command|write_file|replace|edit_file|delete_file|read_file|read_many_files|glob|grep_search|list_directory|get_file_info|web_search|google_web_search|mcp_(?!marrow_marrow_)[A-Za-z0-9_]{1,192})$';
|
|
278
286
|
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';
|
|
279
287
|
const CODEX_HOOK_TIMEOUT_SECONDS = 5;
|
|
280
288
|
const CODEX_SESSION_TIMEOUT_SECONDS = 3;
|
|
@@ -703,6 +711,7 @@ function sourceClient() {
|
|
|
703
711
|
function parseArgs(argv, env = process.env) {
|
|
704
712
|
const options = {
|
|
705
713
|
cwd: process.cwd(),
|
|
714
|
+
home: env.HOME || env.USERPROFILE || os.homedir(),
|
|
706
715
|
yes: false,
|
|
707
716
|
dryRun: false,
|
|
708
717
|
doctor: false,
|
|
@@ -713,6 +722,7 @@ function parseArgs(argv, env = process.env) {
|
|
|
713
722
|
agentId: env.MARROW_FLEET_AGENT_ID || env.MARROW_AGENT_ID || '',
|
|
714
723
|
toolProfile: resolveToolProfile(env.MARROW_TOOL_PROFILE),
|
|
715
724
|
selfTest: true,
|
|
725
|
+
loopGuardSelfTest: true,
|
|
716
726
|
selfTestExplicitlyDisabled: false,
|
|
717
727
|
json: false,
|
|
718
728
|
activate: false,
|
|
@@ -866,6 +876,32 @@ function safeRead(filePath) {
|
|
|
866
876
|
return exists(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
|
|
867
877
|
}
|
|
868
878
|
|
|
879
|
+
function assertSafeGrokHookTarget(homePath, targetPath) {
|
|
880
|
+
const home = path.resolve(homePath);
|
|
881
|
+
const target = path.resolve(targetPath);
|
|
882
|
+
const expected = path.join(home, '.grok', 'hooks', 'marrow.json');
|
|
883
|
+
if (target !== expected) throw new Error('Refusing Grok hook write outside the direct owner hook path');
|
|
884
|
+
const uid = typeof process.getuid === 'function' ? process.getuid() : null;
|
|
885
|
+
const checkDirectory = (directory) => {
|
|
886
|
+
if (!exists(directory)) return;
|
|
887
|
+
const stat = fs.lstatSync(directory);
|
|
888
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()
|
|
889
|
+
|| (uid !== null && stat.uid !== uid) || (stat.mode & 0o022) !== 0) {
|
|
890
|
+
throw new Error(`Refusing Grok hook write through unsafe owner path: ${directory}`);
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
checkDirectory(home);
|
|
894
|
+
checkDirectory(path.join(home, '.grok'));
|
|
895
|
+
checkDirectory(path.join(home, '.grok', 'hooks'));
|
|
896
|
+
if (exists(target)) {
|
|
897
|
+
const stat = fs.lstatSync(target);
|
|
898
|
+
if (!stat.isFile() || stat.isSymbolicLink()
|
|
899
|
+
|| (uid !== null && stat.uid !== uid) || (stat.mode & 0o022) !== 0) {
|
|
900
|
+
throw new Error('Refusing Grok hook write to unsafe owner file');
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
869
905
|
function findUp(startDir, names, maxDepth = 8) {
|
|
870
906
|
let dir = path.resolve(startDir);
|
|
871
907
|
for (let depth = 0; depth <= maxDepth; depth += 1) {
|
|
@@ -916,6 +952,7 @@ function detectEnvironment(cwd = process.cwd(), env = process.env) {
|
|
|
916
952
|
|
|
917
953
|
return {
|
|
918
954
|
root,
|
|
955
|
+
home,
|
|
919
956
|
paths,
|
|
920
957
|
node: exists(paths.packageJson),
|
|
921
958
|
python: exists(paths.pyproject) || exists(paths.requirements) || exists(paths.setupPy),
|
|
@@ -2031,7 +2068,9 @@ function activationProfile(detection, plan, changes, client) {
|
|
|
2031
2068
|
? '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.'
|
|
2032
2069
|
: null
|
|
2033
2070
|
: capabilityLevel === 'sdk_passive_runtime' && !sdkDependency.present
|
|
2034
|
-
?
|
|
2071
|
+
? sdkDependency.install_command
|
|
2072
|
+
? `${sdkDependency.install_command} && npx @getmarrow/install --repair`
|
|
2073
|
+
: sdkDependency.warning
|
|
2035
2074
|
: capabilityLevel === 'governed_wrapper'
|
|
2036
2075
|
? `npx @getmarrow/install run --agent <agent-id> -- ${client}`
|
|
2037
2076
|
: client === 'cline' && clineConflicts.length > 0
|
|
@@ -2052,6 +2091,9 @@ function activationProfile(detection, plan, changes, client) {
|
|
|
2052
2091
|
passive_live: false,
|
|
2053
2092
|
configuration_complete: complete,
|
|
2054
2093
|
complete,
|
|
2094
|
+
loop_guard_configured: capabilityLevel === 'native_hooks' && complete,
|
|
2095
|
+
loop_guard_self_tested: false,
|
|
2096
|
+
loop_guard_observed: false,
|
|
2055
2097
|
exact_fix: exactFix,
|
|
2056
2098
|
...(client === 'cline' ? {
|
|
2057
2099
|
hook_conflicts: clineConflicts,
|
|
@@ -2150,10 +2192,12 @@ function inspectSdkDependency(detection) {
|
|
|
2150
2192
|
|| objectTargetsSdk(packageJson.resolutions)
|
|
2151
2193
|
|| objectTargetsSdk(packageJson.pnpm?.overrides);
|
|
2152
2194
|
let lockVerified = false;
|
|
2195
|
+
let lockedVersion = null;
|
|
2153
2196
|
try {
|
|
2154
2197
|
const lock = JSON.parse(safeRead(path.join(detection.root, 'package-lock.json')) || '{}');
|
|
2155
2198
|
const rootLock = lock?.packages?.[''];
|
|
2156
2199
|
const lockedSdk = lock?.packages?.['node_modules/@getmarrow/sdk'];
|
|
2200
|
+
lockedVersion = typeof lockedSdk?.version === 'string' ? lockedSdk.version : null;
|
|
2157
2201
|
const lockedDeclaration = [
|
|
2158
2202
|
rootLock?.dependencies,
|
|
2159
2203
|
rootLock?.devDependencies,
|
|
@@ -2183,9 +2227,16 @@ function inspectSdkDependency(detection) {
|
|
|
2183
2227
|
installedVersion = null;
|
|
2184
2228
|
installedName = null;
|
|
2185
2229
|
}
|
|
2186
|
-
const
|
|
2187
|
-
|
|
2188
|
-
|
|
2230
|
+
const declaredLowerBound = declaredStableSdkLowerBound(declaredSpec);
|
|
2231
|
+
const declarationTrusted = declaredLowerBound !== null;
|
|
2232
|
+
const unsupportedDeclaration = declaredSpec !== null && !declarationTrusted;
|
|
2233
|
+
const declaredComparison = declaredLowerBound
|
|
2234
|
+
? compareMcpVersions(declaredLowerBound.version, SDK_ADAPTER_VERSION) : null;
|
|
2235
|
+
const declaredAhead = declaredComparison !== null
|
|
2236
|
+
&& (declaredComparison > 0 || (declaredLowerBound.exclusive && declaredComparison === 0));
|
|
2237
|
+
const aheadVersions = [installedVersion, lockedVersion]
|
|
2238
|
+
.filter((version) => compareMcpVersions(version, SDK_ADAPTER_VERSION) > 0);
|
|
2239
|
+
const aheadUnverified = aheadVersions.length > 0 || declaredAhead;
|
|
2189
2240
|
const present = declarationTrusted
|
|
2190
2241
|
&& !overrideDetected
|
|
2191
2242
|
&& lockVerified
|
|
@@ -2201,8 +2252,15 @@ function inspectSdkDependency(detection) {
|
|
|
2201
2252
|
lock_verified: lockVerified,
|
|
2202
2253
|
installed_name: installedName,
|
|
2203
2254
|
installed_version: installedVersion,
|
|
2255
|
+
ahead_unverified: aheadUnverified,
|
|
2204
2256
|
expected_version: SDK_ADAPTER_VERSION,
|
|
2205
|
-
install_command: present
|
|
2257
|
+
install_command: present || aheadUnverified || unsupportedDeclaration
|
|
2258
|
+
? null : `npm install @getmarrow/sdk@${SDK_ADAPTER_VERSION}`,
|
|
2259
|
+
...(aheadUnverified ? {
|
|
2260
|
+
warning: 'A newer SDK version is configured or installed. Preserve it and verify its exact version and integrity against the official npm registry before changing it.',
|
|
2261
|
+
} : unsupportedDeclaration ? {
|
|
2262
|
+
warning: 'The configured SDK version range is unsupported or ambiguous. Preserve it and verify the intended version against the official npm registry before changing it.',
|
|
2263
|
+
} : {}),
|
|
2206
2264
|
};
|
|
2207
2265
|
}
|
|
2208
2266
|
|
|
@@ -2396,12 +2454,14 @@ function buildPlan(detection, options) {
|
|
|
2396
2454
|
transform: (filePath) => retarget(upsertGeminiHooks(filePath)),
|
|
2397
2455
|
});
|
|
2398
2456
|
}
|
|
2399
|
-
if (
|
|
2457
|
+
if (detection.grok) {
|
|
2400
2458
|
writes.push({
|
|
2401
|
-
type: 'json-transform',
|
|
2459
|
+
type: 'managed-json-transform',
|
|
2402
2460
|
path: detection.paths.grokHooks,
|
|
2403
|
-
root:
|
|
2461
|
+
root: detection.home,
|
|
2404
2462
|
label: 'Grok native hooks',
|
|
2463
|
+
isManaged: managedGrokHooksFile,
|
|
2464
|
+
conflict_fix: 'Preserve or move the unmanaged owner Grok hook file after owner review, then rerun npx @getmarrow/install update.',
|
|
2405
2465
|
transform: (filePath) => retarget(upsertGrokHooks(filePath)),
|
|
2406
2466
|
});
|
|
2407
2467
|
}
|
|
@@ -2508,6 +2568,9 @@ function applyPlan(plan, options) {
|
|
|
2508
2568
|
if (!Array.isArray(plan?.writes) || plan.writes.length === 0) return [];
|
|
2509
2569
|
const root = path.resolve(plan.root || path.dirname(plan.writes[0].path));
|
|
2510
2570
|
for (const write of plan.writes) {
|
|
2571
|
+
if (write.label === 'Grok native hooks') {
|
|
2572
|
+
assertSafeGrokHookTarget(path.resolve(write.root), write.path);
|
|
2573
|
+
}
|
|
2511
2574
|
assertContainedManagedTarget(path.resolve(write.root || root), write.path);
|
|
2512
2575
|
}
|
|
2513
2576
|
const prepared = plan.writes.map((write) => {
|
|
@@ -2535,6 +2598,13 @@ function applyPlan(plan, options) {
|
|
|
2535
2598
|
after = upsertBlock(before, write.block);
|
|
2536
2599
|
} else if (write.type === 'json-transform') {
|
|
2537
2600
|
after = write.transform(write.path);
|
|
2601
|
+
} else if (write.type === 'managed-json-transform') {
|
|
2602
|
+
if (fileExists && !write.isManaged(write.path)) {
|
|
2603
|
+
after = before;
|
|
2604
|
+
hookConflict = true;
|
|
2605
|
+
} else {
|
|
2606
|
+
after = write.transform(write.path);
|
|
2607
|
+
}
|
|
2538
2608
|
} else if (write.type === 'owned-executable') {
|
|
2539
2609
|
if (fileExists && before !== write.content) {
|
|
2540
2610
|
after = before;
|
|
@@ -2614,6 +2684,63 @@ async function requestJson(url, options) {
|
|
|
2614
2684
|
return json.data || json;
|
|
2615
2685
|
}
|
|
2616
2686
|
|
|
2687
|
+
function runLoopGuardSelfTest(options = {}) {
|
|
2688
|
+
const target = executableMcpTarget(options);
|
|
2689
|
+
const isolatedHome = fs.mkdtempSync(path.join(os.tmpdir(), 'marrow-install-loop-guard-'));
|
|
2690
|
+
const command = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
2691
|
+
const args = ['-y', `--package=@getmarrow/mcp@${target.version}`, 'marrow-mcp', 'loop-guard-self-test'];
|
|
2692
|
+
try {
|
|
2693
|
+
const invocation = {
|
|
2694
|
+
command,
|
|
2695
|
+
args,
|
|
2696
|
+
cwd: isolatedHome,
|
|
2697
|
+
env: {
|
|
2698
|
+
...process.env,
|
|
2699
|
+
HOME: isolatedHome,
|
|
2700
|
+
USERPROFILE: isolatedHome,
|
|
2701
|
+
MARROW_API_KEY: '',
|
|
2702
|
+
MARROW_KEY: '',
|
|
2703
|
+
},
|
|
2704
|
+
encoding: 'utf8',
|
|
2705
|
+
timeout: 15_000,
|
|
2706
|
+
maxBuffer: 16 * 1024,
|
|
2707
|
+
};
|
|
2708
|
+
const result = typeof options.loopGuardSelfTestRunner === 'function'
|
|
2709
|
+
? options.loopGuardSelfTestRunner(invocation)
|
|
2710
|
+
: spawnSync(command, args, invocation);
|
|
2711
|
+
if (!result || result.error || result.status !== 0) {
|
|
2712
|
+
const reason = result?.error?.message || String(result?.stderr || '').trim() || `exit ${result?.status ?? 'unknown'}`;
|
|
2713
|
+
throw new Error(`MCP ${target.version} isolated loop-guard self-test failed: ${reason.slice(0, 300)}`);
|
|
2714
|
+
}
|
|
2715
|
+
let proof;
|
|
2716
|
+
try {
|
|
2717
|
+
proof = JSON.parse(String(result.stdout || ''));
|
|
2718
|
+
} catch {
|
|
2719
|
+
throw new Error(`MCP ${target.version} isolated loop-guard self-test returned invalid JSON`);
|
|
2720
|
+
}
|
|
2721
|
+
const valid = proof && !Array.isArray(proof)
|
|
2722
|
+
&& proof.pass === true
|
|
2723
|
+
&& proof.isolated === true
|
|
2724
|
+
&& proof.live_hook_observed === false
|
|
2725
|
+
&& proof.repeat_denied === true
|
|
2726
|
+
&& proof.mutation_reset === true
|
|
2727
|
+
&& proof.owner_disabled_bypass === true;
|
|
2728
|
+
if (!valid) throw new Error(`MCP ${target.version} isolated loop-guard self-test returned incomplete proof`);
|
|
2729
|
+
return {
|
|
2730
|
+
attempted: true,
|
|
2731
|
+
passed: true,
|
|
2732
|
+
isolated: true,
|
|
2733
|
+
mcp_version: target.version,
|
|
2734
|
+
repeat_denied: true,
|
|
2735
|
+
mutation_reset: true,
|
|
2736
|
+
owner_disabled_bypass: true,
|
|
2737
|
+
live_hook_observed: false,
|
|
2738
|
+
};
|
|
2739
|
+
} finally {
|
|
2740
|
+
fs.rmSync(isolatedHome, { recursive: true, force: true });
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2617
2744
|
function isCanonicalTimestamp(value) {
|
|
2618
2745
|
if (typeof value !== 'string' || value.length === 0) return false;
|
|
2619
2746
|
const parsed = Date.parse(value);
|
|
@@ -3017,11 +3144,21 @@ function printReport(report) {
|
|
|
3017
3144
|
for (const change of report.changes) {
|
|
3018
3145
|
const marker = change.automatic_repair_suppressed
|
|
3019
3146
|
? 'preserved unverified-ahead surface; repair suppressed'
|
|
3147
|
+
: change.hook_conflict ? 'preserved unmanaged owner file; review required'
|
|
3020
3148
|
: change.applied ? 'wrote' : change.changed ? 'would write' : 'unchanged';
|
|
3021
3149
|
process.stdout.write(`- ${marker}: ${change.label} (${change.path})\n`);
|
|
3022
3150
|
if (change.automatic_repair_suppressed && change.exact_fix) {
|
|
3023
3151
|
process.stdout.write(` exact verification: ${change.exact_fix}\n`);
|
|
3024
3152
|
}
|
|
3153
|
+
if (change.hook_conflict && change.exact_fix) process.stdout.write(` exact fix: ${change.exact_fix}\n`);
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
process.stdout.write('\nLocal session loop guard:\n');
|
|
3157
|
+
process.stdout.write(`- configured: ${report.loop_guard_configured ? 'yes' : 'no'}\n`);
|
|
3158
|
+
process.stdout.write(`- isolated self-test passed: ${report.loop_guard_self_tested ? 'yes' : 'no'}\n`);
|
|
3159
|
+
process.stdout.write(`- observed in a reloaded host: ${report.loop_guard_observed ? 'yes' : 'no'}\n`);
|
|
3160
|
+
if (!report.loop_guard_observed) {
|
|
3161
|
+
process.stdout.write('- next: restart the host and complete its hook trust/review step before relying on live enforcement\n');
|
|
3025
3162
|
}
|
|
3026
3163
|
|
|
3027
3164
|
process.stdout.write('\nSelf-test:\n');
|
|
@@ -3145,7 +3282,7 @@ function printReport(report) {
|
|
|
3145
3282
|
|
|
3146
3283
|
if (report.sdkDependency?.required) {
|
|
3147
3284
|
process.stdout.write('\nSDK dependency:\n');
|
|
3148
|
-
process.stdout.write(`- @getmarrow/sdk: ${report.sdkDependency.present ? 'present' : 'missing'}\n`);
|
|
3285
|
+
process.stdout.write(`- @getmarrow/sdk: ${report.sdkDependency.present ? 'present' : report.sdkDependency.ahead_unverified ? 'newer version requires verification' : 'missing'}\n`);
|
|
3149
3286
|
if (report.sdkDependency.install_command) process.stdout.write(`- exact fix: ${report.sdkDependency.install_command}\n`);
|
|
3150
3287
|
if (report.sdkDependency.warning) process.stdout.write(`- warning: ${report.sdkDependency.warning}\n`);
|
|
3151
3288
|
}
|
|
@@ -3169,6 +3306,9 @@ function printReport(report) {
|
|
|
3169
3306
|
process.stdout.write(`- missing env: ${report.doctor.missingEnv.length ? report.doctor.missingEnv.join(', ') : 'none'}\n`);
|
|
3170
3307
|
if (report.doctor.envHints.length) process.stdout.write(`- possible env files: ${report.doctor.envHints.join(', ')}\n`);
|
|
3171
3308
|
process.stdout.write(`- missing hooks/config: ${report.doctor.missingHooks.length ? report.doctor.missingHooks.join('; ') : 'none'}\n`);
|
|
3309
|
+
process.stdout.write(`- loop guard configured: ${report.doctor.loop_guard_configured ? 'yes' : 'no'}\n`);
|
|
3310
|
+
process.stdout.write(`- loop guard isolated self-test: ${report.doctor.loop_guard_self_tested ? 'passed' : 'not passed'}\n`);
|
|
3311
|
+
process.stdout.write(`- loop guard observed after reload/trust: ${report.doctor.loop_guard_observed ? 'yes' : 'no'}\n`);
|
|
3172
3312
|
if (report.doctor.mcpProcesses?.available) {
|
|
3173
3313
|
const processes = report.doctor.mcpProcesses;
|
|
3174
3314
|
process.stdout.write(`- MCP process versions: ${processes.active_versions.length ? processes.active_versions.join(', ') : processes.active_processes ? 'unknown' : 'none'}\n`);
|
|
@@ -3219,7 +3359,11 @@ async function install(options) {
|
|
|
3219
3359
|
options.toolProfile = resolveToolProfile(options.toolProfile === undefined
|
|
3220
3360
|
? process.env.MARROW_TOOL_PROFILE
|
|
3221
3361
|
: options.toolProfile);
|
|
3222
|
-
const detection = detectEnvironment(options.cwd
|
|
3362
|
+
const detection = detectEnvironment(options.cwd, {
|
|
3363
|
+
...process.env,
|
|
3364
|
+
HOME: options.home || options.cwd,
|
|
3365
|
+
USERPROFILE: options.home || options.cwd,
|
|
3366
|
+
});
|
|
3223
3367
|
const client = detectedClient(detection);
|
|
3224
3368
|
options.client = client;
|
|
3225
3369
|
options.agentId = String(options.agentId || '').trim() || stableAgentId(detection.root, client);
|
|
@@ -3240,6 +3384,28 @@ async function install(options) {
|
|
|
3240
3384
|
const writeMode = options.doctor ? 'doctor' : options.dryRun ? 'dry-run' : options.repair ? 'repair' : options.yes ? 'write' : 'dry-run';
|
|
3241
3385
|
const changes = applyPlan(plan, options);
|
|
3242
3386
|
let profile = activationProfile(detection, plan, changes, client);
|
|
3387
|
+
const localControl = localControlEvidence({
|
|
3388
|
+
apiKey: options.apiKey,
|
|
3389
|
+
home: options.controlHome || options.home || detection.home,
|
|
3390
|
+
});
|
|
3391
|
+
const loopGuardConfigured = profile.capability_level === 'native_hooks'
|
|
3392
|
+
&& profile.configuration_complete === true;
|
|
3393
|
+
const loopGuardSelfTest = options.loopGuardSelfTest === true && !options.dryRun
|
|
3394
|
+
? runLoopGuardSelfTest(options)
|
|
3395
|
+
: {
|
|
3396
|
+
attempted: false,
|
|
3397
|
+
passed: false,
|
|
3398
|
+
isolated: true,
|
|
3399
|
+
mcp_version: mcpTarget.version,
|
|
3400
|
+
live_hook_observed: false,
|
|
3401
|
+
};
|
|
3402
|
+
profile = {
|
|
3403
|
+
...profile,
|
|
3404
|
+
loop_guard_configured: loopGuardConfigured,
|
|
3405
|
+
loop_guard_self_tested: loopGuardSelfTest.passed === true,
|
|
3406
|
+
loop_guard_observed: false,
|
|
3407
|
+
loop_guard_enabled: localControl.enabled === true,
|
|
3408
|
+
};
|
|
3243
3409
|
options.activation = options.activate ? {
|
|
3244
3410
|
harness: client,
|
|
3245
3411
|
agent_id: options.agentId,
|
|
@@ -3368,7 +3534,6 @@ async function install(options) {
|
|
|
3368
3534
|
const changedConfig = changes.some((change) => change.applied) || configRepairs.some((repair) => repair.changed);
|
|
3369
3535
|
const selfTestPassed = Boolean(!selfTest.skipped && selfTest.active && !selfTest.error);
|
|
3370
3536
|
const controllerPlatform = options.controllerPlatform || process.platform;
|
|
3371
|
-
const localControl = localControlEvidence({ apiKey: options.apiKey, home: options.controlHome });
|
|
3372
3537
|
let controller = await controllerStatus({
|
|
3373
3538
|
root: detection.root,
|
|
3374
3539
|
agentId: options.agentId,
|
|
@@ -3453,6 +3618,10 @@ async function install(options) {
|
|
|
3453
3618
|
receipt: selfTest.activation_receipt || null,
|
|
3454
3619
|
profile,
|
|
3455
3620
|
},
|
|
3621
|
+
loop_guard_configured: loopGuardConfigured,
|
|
3622
|
+
loop_guard_self_tested: loopGuardSelfTest.passed === true,
|
|
3623
|
+
loop_guard_observed: false,
|
|
3624
|
+
loop_guard: loopGuardSelfTest,
|
|
3456
3625
|
harnessReload,
|
|
3457
3626
|
firstCapture: firstCapturePath(detection, options.agentId),
|
|
3458
3627
|
changes,
|
|
@@ -3470,6 +3639,9 @@ async function install(options) {
|
|
|
3470
3639
|
restart_required: mcpUpdateRequired || updateAwaitingRestart,
|
|
3471
3640
|
restart_instruction: mcpUpdateRequired || updateAwaitingRestart ? INSTALLER_RESTART_INSTRUCTION : null,
|
|
3472
3641
|
verification_command: mcpUpdateRequired || updateAwaitingRestart ? INSTALLER_DOCTOR_COMMAND : null,
|
|
3642
|
+
loop_guard_configured: loopGuardConfigured,
|
|
3643
|
+
loop_guard_self_tested: loopGuardSelfTest.passed === true,
|
|
3644
|
+
loop_guard_observed: false,
|
|
3473
3645
|
recommendedFix: registryVerificationAction
|
|
3474
3646
|
|| (updateAwaitingRestart ? INSTALLER_RESTART_INSTRUCTION : null)
|
|
3475
3647
|
|| mcpProcesses.exact_fix || mcpConfigurations.exact_fix || configDiagnostics.npm_token.recommended_fix || (!options.apiKey
|
|
@@ -3525,6 +3697,7 @@ module.exports = {
|
|
|
3525
3697
|
applyPlan,
|
|
3526
3698
|
install,
|
|
3527
3699
|
runSelfTest,
|
|
3700
|
+
runLoopGuardSelfTest,
|
|
3528
3701
|
runCli,
|
|
3529
3702
|
passiveRuntimeSource,
|
|
3530
3703
|
inspectNpmTokenConfig,
|
|
@@ -3547,6 +3720,9 @@ module.exports = {
|
|
|
3547
3720
|
GROK_ACTION_RESULT_HOOK_COMMAND,
|
|
3548
3721
|
GROK_SESSION_END_HOOK_COMMAND,
|
|
3549
3722
|
GROK_NATIVE_HOOK_MATCHER,
|
|
3723
|
+
NATIVE_HOOK_MATCHER,
|
|
3724
|
+
CURSOR_NATIVE_HOOK_MATCHER,
|
|
3725
|
+
GEMINI_NATIVE_HOOK_MATCHER,
|
|
3550
3726
|
printReport,
|
|
3551
3727
|
buildMcpToolProfileReport,
|
|
3552
3728
|
resolveMcpTargetVersion,
|