@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 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.56
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.56",
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.80';
14
- const MCP_ADAPTER_SOURCE_SHA = 'ee1eda3f201965a6530256accbdf175660dd8ad6';
15
- const MCP_ADAPTER_INTEGRITY = 'sha512-uou3X18pESV39EMmddDrYN7yd6MrZzosrnQ1eRtvK5j3yuBLAlupj5kQVrrKEWAL6xwuLebXq2isivK2O/2WrA==';
16
- const SDK_ADAPTER_VERSION = '3.7.62';
17
- const SDK_ADAPTER_INTEGRITY = 'sha512-n1i6Be09TpAQ9BPNRKY7aCvA2iSUPpJfw8djw2MELwpNbBCtKiZ29Jji77BK/6EFLUpSIcTW/Gmdf/ccf0JRYQ==';
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: 'sealed_local_candidate',
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__(?!marrow_).*';
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
- ? `${sdkDependency.install_command} && npx @getmarrow/install --repair`
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 declarationTrusted = typeof declaredSpec === 'string'
2187
- && declaredSpec.trim().length > 0
2188
- && /^[v0-9xX*<>=~^|.\s-]+$/.test(declaredSpec.trim());
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 ? null : `npm install @getmarrow/sdk@${SDK_ADAPTER_VERSION}`,
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 (options.update && detection.grok && managedGrokHooksFile(detection.paths.grokHooks)) {
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: path.resolve(detection.paths.grokHooks, '..', '..', '..'),
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,