@getmarrow/install 0.1.57 → 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,11 @@ 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.57
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
105
109
 
106
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.
107
111
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getmarrow/install",
3
- "version": "0.1.57",
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,9 +11,9 @@ 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.88';
14
- const MCP_ADAPTER_SOURCE_SHA = '2385f0554dd9455e6a7d2c0396058710ab56611a';
15
- const MCP_ADAPTER_INTEGRITY = 'sha512-1WQtVU8oT1XLGFa9aINm9Ypn/xhNh/mPLpK2bziPIxRURsAGrs8TAfb23JL7HXDcmpXFgMLd8R6gmltVbBbHIg==';
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==';
16
17
  const SDK_ADAPTER_VERSION = '3.7.63';
17
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`;
@@ -278,10 +279,10 @@ const GROK_PRE_ACTION_GUARD_SOURCE = [
278
279
  'child.stdin.end(Buffer.concat(input));}catch{fail();}});',
279
280
  ].join('');
280
281
  const GROK_PRE_ACTION_HOOK_COMMAND = `node -e '${GROK_PRE_ACTION_GUARD_SOURCE}'`;
281
- 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_).*';
282
283
  const CODEX_NATIVE_HOOK_MATCHER = 'Bash|apply_patch|Edit|Write|MultiEdit|mcp__(?!marrow__marrow_).*|functions\\.(?!marrow_).*';
283
- const CURSOR_NATIVE_HOOK_MATCHER = 'Shell|Write|Delete|Task|MCP:(?!marrow(?:_.*|:marrow_.*)$).*';
284
- 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})$';
285
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';
286
287
  const CODEX_HOOK_TIMEOUT_SECONDS = 5;
287
288
  const CODEX_SESSION_TIMEOUT_SECONDS = 3;
@@ -710,6 +711,7 @@ function sourceClient() {
710
711
  function parseArgs(argv, env = process.env) {
711
712
  const options = {
712
713
  cwd: process.cwd(),
714
+ home: env.HOME || env.USERPROFILE || os.homedir(),
713
715
  yes: false,
714
716
  dryRun: false,
715
717
  doctor: false,
@@ -720,6 +722,7 @@ function parseArgs(argv, env = process.env) {
720
722
  agentId: env.MARROW_FLEET_AGENT_ID || env.MARROW_AGENT_ID || '',
721
723
  toolProfile: resolveToolProfile(env.MARROW_TOOL_PROFILE),
722
724
  selfTest: true,
725
+ loopGuardSelfTest: true,
723
726
  selfTestExplicitlyDisabled: false,
724
727
  json: false,
725
728
  activate: false,
@@ -873,6 +876,32 @@ function safeRead(filePath) {
873
876
  return exists(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
874
877
  }
875
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
+
876
905
  function findUp(startDir, names, maxDepth = 8) {
877
906
  let dir = path.resolve(startDir);
878
907
  for (let depth = 0; depth <= maxDepth; depth += 1) {
@@ -923,6 +952,7 @@ function detectEnvironment(cwd = process.cwd(), env = process.env) {
923
952
 
924
953
  return {
925
954
  root,
955
+ home,
926
956
  paths,
927
957
  node: exists(paths.packageJson),
928
958
  python: exists(paths.pyproject) || exists(paths.requirements) || exists(paths.setupPy),
@@ -2061,6 +2091,9 @@ function activationProfile(detection, plan, changes, client) {
2061
2091
  passive_live: false,
2062
2092
  configuration_complete: complete,
2063
2093
  complete,
2094
+ loop_guard_configured: capabilityLevel === 'native_hooks' && complete,
2095
+ loop_guard_self_tested: false,
2096
+ loop_guard_observed: false,
2064
2097
  exact_fix: exactFix,
2065
2098
  ...(client === 'cline' ? {
2066
2099
  hook_conflicts: clineConflicts,
@@ -2421,12 +2454,14 @@ function buildPlan(detection, options) {
2421
2454
  transform: (filePath) => retarget(upsertGeminiHooks(filePath)),
2422
2455
  });
2423
2456
  }
2424
- if (options.update && detection.grok && managedGrokHooksFile(detection.paths.grokHooks)) {
2457
+ if (detection.grok) {
2425
2458
  writes.push({
2426
- type: 'json-transform',
2459
+ type: 'managed-json-transform',
2427
2460
  path: detection.paths.grokHooks,
2428
- root: path.resolve(detection.paths.grokHooks, '..', '..', '..'),
2461
+ root: detection.home,
2429
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.',
2430
2465
  transform: (filePath) => retarget(upsertGrokHooks(filePath)),
2431
2466
  });
2432
2467
  }
@@ -2533,6 +2568,9 @@ function applyPlan(plan, options) {
2533
2568
  if (!Array.isArray(plan?.writes) || plan.writes.length === 0) return [];
2534
2569
  const root = path.resolve(plan.root || path.dirname(plan.writes[0].path));
2535
2570
  for (const write of plan.writes) {
2571
+ if (write.label === 'Grok native hooks') {
2572
+ assertSafeGrokHookTarget(path.resolve(write.root), write.path);
2573
+ }
2536
2574
  assertContainedManagedTarget(path.resolve(write.root || root), write.path);
2537
2575
  }
2538
2576
  const prepared = plan.writes.map((write) => {
@@ -2560,6 +2598,13 @@ function applyPlan(plan, options) {
2560
2598
  after = upsertBlock(before, write.block);
2561
2599
  } else if (write.type === 'json-transform') {
2562
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
+ }
2563
2608
  } else if (write.type === 'owned-executable') {
2564
2609
  if (fileExists && before !== write.content) {
2565
2610
  after = before;
@@ -2639,6 +2684,63 @@ async function requestJson(url, options) {
2639
2684
  return json.data || json;
2640
2685
  }
2641
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
+
2642
2744
  function isCanonicalTimestamp(value) {
2643
2745
  if (typeof value !== 'string' || value.length === 0) return false;
2644
2746
  const parsed = Date.parse(value);
@@ -3042,11 +3144,21 @@ function printReport(report) {
3042
3144
  for (const change of report.changes) {
3043
3145
  const marker = change.automatic_repair_suppressed
3044
3146
  ? 'preserved unverified-ahead surface; repair suppressed'
3147
+ : change.hook_conflict ? 'preserved unmanaged owner file; review required'
3045
3148
  : change.applied ? 'wrote' : change.changed ? 'would write' : 'unchanged';
3046
3149
  process.stdout.write(`- ${marker}: ${change.label} (${change.path})\n`);
3047
3150
  if (change.automatic_repair_suppressed && change.exact_fix) {
3048
3151
  process.stdout.write(` exact verification: ${change.exact_fix}\n`);
3049
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');
3050
3162
  }
3051
3163
 
3052
3164
  process.stdout.write('\nSelf-test:\n');
@@ -3194,6 +3306,9 @@ function printReport(report) {
3194
3306
  process.stdout.write(`- missing env: ${report.doctor.missingEnv.length ? report.doctor.missingEnv.join(', ') : 'none'}\n`);
3195
3307
  if (report.doctor.envHints.length) process.stdout.write(`- possible env files: ${report.doctor.envHints.join(', ')}\n`);
3196
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`);
3197
3312
  if (report.doctor.mcpProcesses?.available) {
3198
3313
  const processes = report.doctor.mcpProcesses;
3199
3314
  process.stdout.write(`- MCP process versions: ${processes.active_versions.length ? processes.active_versions.join(', ') : processes.active_processes ? 'unknown' : 'none'}\n`);
@@ -3244,7 +3359,11 @@ async function install(options) {
3244
3359
  options.toolProfile = resolveToolProfile(options.toolProfile === undefined
3245
3360
  ? process.env.MARROW_TOOL_PROFILE
3246
3361
  : options.toolProfile);
3247
- 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
+ });
3248
3367
  const client = detectedClient(detection);
3249
3368
  options.client = client;
3250
3369
  options.agentId = String(options.agentId || '').trim() || stableAgentId(detection.root, client);
@@ -3265,6 +3384,28 @@ async function install(options) {
3265
3384
  const writeMode = options.doctor ? 'doctor' : options.dryRun ? 'dry-run' : options.repair ? 'repair' : options.yes ? 'write' : 'dry-run';
3266
3385
  const changes = applyPlan(plan, options);
3267
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
+ };
3268
3409
  options.activation = options.activate ? {
3269
3410
  harness: client,
3270
3411
  agent_id: options.agentId,
@@ -3393,7 +3534,6 @@ async function install(options) {
3393
3534
  const changedConfig = changes.some((change) => change.applied) || configRepairs.some((repair) => repair.changed);
3394
3535
  const selfTestPassed = Boolean(!selfTest.skipped && selfTest.active && !selfTest.error);
3395
3536
  const controllerPlatform = options.controllerPlatform || process.platform;
3396
- const localControl = localControlEvidence({ apiKey: options.apiKey, home: options.controlHome });
3397
3537
  let controller = await controllerStatus({
3398
3538
  root: detection.root,
3399
3539
  agentId: options.agentId,
@@ -3478,6 +3618,10 @@ async function install(options) {
3478
3618
  receipt: selfTest.activation_receipt || null,
3479
3619
  profile,
3480
3620
  },
3621
+ loop_guard_configured: loopGuardConfigured,
3622
+ loop_guard_self_tested: loopGuardSelfTest.passed === true,
3623
+ loop_guard_observed: false,
3624
+ loop_guard: loopGuardSelfTest,
3481
3625
  harnessReload,
3482
3626
  firstCapture: firstCapturePath(detection, options.agentId),
3483
3627
  changes,
@@ -3495,6 +3639,9 @@ async function install(options) {
3495
3639
  restart_required: mcpUpdateRequired || updateAwaitingRestart,
3496
3640
  restart_instruction: mcpUpdateRequired || updateAwaitingRestart ? INSTALLER_RESTART_INSTRUCTION : null,
3497
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,
3498
3645
  recommendedFix: registryVerificationAction
3499
3646
  || (updateAwaitingRestart ? INSTALLER_RESTART_INSTRUCTION : null)
3500
3647
  || mcpProcesses.exact_fix || mcpConfigurations.exact_fix || configDiagnostics.npm_token.recommended_fix || (!options.apiKey
@@ -3550,6 +3697,7 @@ module.exports = {
3550
3697
  applyPlan,
3551
3698
  install,
3552
3699
  runSelfTest,
3700
+ runLoopGuardSelfTest,
3553
3701
  runCli,
3554
3702
  passiveRuntimeSource,
3555
3703
  inspectNpmTokenConfig,
@@ -3572,6 +3720,9 @@ module.exports = {
3572
3720
  GROK_ACTION_RESULT_HOOK_COMMAND,
3573
3721
  GROK_SESSION_END_HOOK_COMMAND,
3574
3722
  GROK_NATIVE_HOOK_MATCHER,
3723
+ NATIVE_HOOK_MATCHER,
3724
+ CURSOR_NATIVE_HOOK_MATCHER,
3725
+ GEMINI_NATIVE_HOOK_MATCHER,
3575
3726
  printReport,
3576
3727
  buildMcpToolProfileReport,
3577
3728
  resolveMcpTargetVersion,