@getmarrow/install 0.1.39 → 0.1.41

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
@@ -91,15 +91,28 @@ npx @getmarrow/install controller stop
91
91
 
92
92
  Persistent controller lifecycle is currently Linux-only. On macOS or Windows, activation still installs and verifies the supported hooks without starting or signaling a background process; 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.
93
93
 
94
- ## What's New in v0.1.39
94
+ ## What's New in v0.1.41
95
95
 
96
- v0.1.39 installs the bounded one-call passive read contract from day one:
96
+ v0.1.41 makes stale client detection and recovery part of normal installation health:
97
+
98
+ - `doctor` detects active stale, mixed, or version-unknown Marrow MCP processes, including direct `node_modules/.bin/marrow-mcp` launches, without exposing command lines or credentials;
99
+ - when repair is needed, `doctor` reports the executable pinned setup command, the separate owning-harness restart requirement, and the executable verification command; it does not terminate harness processes itself;
100
+ - certified hooks pin MCP `3.9.57` and SDK `3.7.56` so the installed runtime matches the advertised control contract;
101
+ - existing harnesses retain their honest coverage level: native hooks where supported, MCP calls where available, and governed wrappers or event contracts elsewhere;
102
+ - package upgrades remain operator-approved and never rotate keys or rewrite unrelated configuration.
103
+
104
+ ## Previous: v0.1.40
105
+
106
+ v0.1.40 binds governed runs to a privacy-safe workspace fingerprint and separates observed execution from verified completion:
97
107
 
98
108
  - ordinary prompts receive one compact context read; risky or mutating prompts receive one fresh runtime gate instead;
99
109
  - passive prompt telemetry is buffered locally rather than delaying the agent turn;
100
110
  - transient read failures can use clearly labeled owner-only last-known guidance, while authentication failures never use cache;
101
111
  - `doctor` prints the exact `npx -y @getmarrow/mcp@latest ping` command for measured current/p50/p99 latency, last success, and backlog health;
102
- - certified hook commands pin MCP `3.9.55` and SDK `3.7.54` so advertised behavior matches the deployed server contract.
112
+ - certified hook commands pin MCP `3.9.56` and SDK `3.7.55` so advertised behavior matches that release's deployed server contract;
113
+ - governed runtime requests attach a stable privacy-safe project fingerprint and harness label without sending the raw working-directory path;
114
+ - successful command exit remains observed execution, not verified business completion, unless a verification command or explicit proof file supplies evidence;
115
+ - the integration matrix now reports prompt injection, pre-action, action result, closure, proof, cached brief, restart survival, evidence adapter, and safe repair separately.
103
116
 
104
117
  It preserves the intervention receipts introduced in v0.1.38.
105
118
 
@@ -280,12 +293,12 @@ These are integration surfaces for one Marrow product, not separate products.
280
293
 
281
294
  Run `npx @getmarrow/install integrations --json` for the machine-readable matrix. The table below intentionally distinguishes full automatic interception from MCP-routed, wrapper-bounded, and adapter-required coverage.
282
295
 
283
- | Harnesses | Pre-action | Result | Outcome closure | Proof enforcement | Safe repair |
284
- | --- | --- | --- | --- | --- | --- |
285
- | Claude Code | Automatic native hook | Automatic native hook | Correlated when determinable | Automatic for protected actions | Managed config after activation |
286
- | Cursor, Composer, Cline, Windsurf | MCP-routed | MCP-routed | MCP-routed | Explicit or governed runner | Managed config after activation |
287
- | Codex, OpenCode, Gemini, Grok, DeepSeek, Qwen, Kimi, MiniMax, GLM | Automatic inside governed runner | Automatic inside governed runner | Automatic when result is known | Automatic for protected actions | Managed config after activation |
288
- | Hermes, OpenClaw, custom harnesses | Adapter required | Adapter required | Adapter required | Adapter required | Adapter owned |
296
+ | Harnesses | Prompt / pre-action / result | Closure and proof | Cached brief | Restart survival | Evidence adapter | Safe repair |
297
+ | --- | --- | --- | --- | --- | --- | --- |
298
+ | Claude Code | Automatic native hooks | Correlated when determinable; protected proof enforced | Owner-only bounded cache | Installed config and durable spool | Native hook evidence | Managed config after activation |
299
+ | Cursor, Composer, Cline, Windsurf | MCP-routed only | MCP-routed; explicit or governed proof | Owner-only MCP cache | MCP config and durable spool | MCP lifecycle evidence | Managed config after activation |
300
+ | Codex, OpenCode, Gemini, Grok, DeepSeek, Qwen, Kimi, MiniMax, GLM | Automatic only inside governed runner | Automatic when result is known; protected proof enforced | Runner/runtime cache | Activated controller and durable buffer | Command, test, deployment, or owner evidence | Managed config after activation |
301
+ | Hermes, OpenClaw, custom harnesses | Lifecycle adapter required | Adapter or governed runner required | Adapter dependent | Adapter dependent | Adapter supplied | Adapter owned |
289
302
 
290
303
  For native hooks, a successful tool exit is not treated as a successful business outcome when proof is missing. MCP coverage includes only actions routed through that MCP client. Governed-runner coverage includes only commands launched through the runner. Event-contract integrations must emit the documented lifecycle themselves.
291
304
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getmarrow/install",
3
- "version": "0.1.39",
3
+ "version": "0.1.41",
4
4
  "description": "Universal installer and governed runner for Marrow agent fleets.",
5
5
  "bin": {
6
6
  "marrow-install": "bin/marrow-install.js"
@@ -319,7 +319,7 @@ function detectProjectSignals(cwd = process.cwd()) {
319
319
  }
320
320
  }
321
321
 
322
- return {
322
+ const project = {
323
323
  name: packageJson?.name || path.basename(cwd),
324
324
  key: packageJson?.name || path.basename(cwd),
325
325
  type: packageJson ? 'node' : fs.existsSync(path.join(cwd, 'pyproject.toml')) ? 'python' : 'workspace',
@@ -328,6 +328,15 @@ function detectProjectSignals(cwd = process.cwd()) {
328
328
  package_scripts: packageScripts.slice(0, 30),
329
329
  config_files: configFiles.slice(0, 30),
330
330
  };
331
+ project.fingerprint = crypto.createHash('sha256').update(JSON.stringify({
332
+ name: project.name,
333
+ type: project.type,
334
+ frameworks: [...project.frameworks].sort(),
335
+ signals: [...project.signals].sort(),
336
+ package_scripts: [...project.package_scripts].sort(),
337
+ config_files: [...project.config_files].sort(),
338
+ })).digest('hex');
339
+ return project;
331
340
  }
332
341
 
333
342
  function isRisky(text, type) {
@@ -525,12 +534,25 @@ function proofFromFile(filePath) {
525
534
 
526
535
  function defaultProof(input) {
527
536
  const proof = proofFromFile(input.options.proofFile) || {};
537
+ const command = redactedCommand(input.childCommand || []);
538
+ const verificationCommand = /\b(test|smoke|check|verify|lint|typecheck|audit)\b/i.test(command);
539
+ const suppliedChecks = Array.isArray(proof.checks) && proof.checks.length > 0;
540
+ const evidenceState = typeof proof.evidence_state === 'string'
541
+ ? proof.evidence_state
542
+ : input.success && (suppliedChecks || verificationCommand)
543
+ ? 'verified'
544
+ : input.success
545
+ ? 'observed_only'
546
+ : 'failed';
528
547
  return {
529
548
  summary: proof.summary || `Marrow governed runner completed ${input.action}.`,
530
- checks: Array.isArray(proof.checks) ? proof.checks : ['marrow runtime gate', 'command exit captured'],
549
+ checks: suppliedChecks ? proof.checks : [`command_exit_code=${Number(input.exitCode)}`],
550
+ evidence_source: proof.evidence_source || (verificationCommand ? 'verification_command_result' : 'governed_command_result'),
551
+ evidence_state: evidenceState,
552
+ verified_completion: evidenceState === 'verified',
531
553
  outcome: proof.outcome || (input.success ? 'success' : 'failure'),
532
554
  blockers: Array.isArray(proof.blockers) ? proof.blockers : [],
533
- command: redactedCommand(input.childCommand || []),
555
+ command,
534
556
  exit_code: input.exitCode,
535
557
  runner: '@getmarrow/install run',
536
558
  profile: input.options.profile,
@@ -544,11 +566,14 @@ async function preflightRuntime(options, action, type, commandText) {
544
566
  const target = options.target || commandText || action;
545
567
  const surfaces = inferSurfaces(commandText || action);
546
568
  const meta = sourceMeta(options, 'runtime', { action, command: commandText, action_type: type });
569
+ const project = detectProjectSignals(options.root || process.cwd());
547
570
  return requestJson(options, 'POST', '/v1/agent/runtime', {
548
571
  action,
549
572
  type,
550
573
  target,
551
574
  surfaces,
575
+ harness: sourceClient(options.client),
576
+ project: { ...project, harness: sourceClient(options.client) },
552
577
  source_meta: meta,
553
578
  context: {
554
579
  runner: '@getmarrow/install run',
@@ -1398,6 +1423,15 @@ function integrationCoverageMatrix() {
1398
1423
  harness: entry.client,
1399
1424
  capability_level: entry.capability_level,
1400
1425
  install_surface: entry.install_surface,
1426
+ prompt_injection: native
1427
+ ? 'automatic_native_hook'
1428
+ : wrapper
1429
+ ? 'automatic_in_governed_runner'
1430
+ : sdk
1431
+ ? 'automatic_in_sdk_runtime'
1432
+ : routed
1433
+ ? 'mcp_routed'
1434
+ : 'adapter_required',
1401
1435
  pre_action: native
1402
1436
  ? 'automatic_native_hook'
1403
1437
  : wrapper
@@ -1429,6 +1463,15 @@ function integrationCoverageMatrix() {
1429
1463
  ? 'explicit_or_governed_wrapper'
1430
1464
  : 'adapter_required',
1431
1465
  automatic_repair: native || routed || sdk || wrapper ? 'installer_managed_config_only' : 'adapter_owned',
1466
+ cached_brief: native || routed ? 'mcp_local_last_known' : wrapper || sdk ? 'server_cache_only' : 'adapter_owned',
1467
+ restart_survival: native || routed || sdk || wrapper ? 'installer_managed_credentials_and_hooks' : 'adapter_owned',
1468
+ evidence_adapter: native
1469
+ ? 'tool_result_and_explicit_verification'
1470
+ : routed
1471
+ ? 'mcp_tool_and_explicit_verification'
1472
+ : wrapper || sdk
1473
+ ? 'command_exit_and_verification_command'
1474
+ : 'adapter_owned',
1432
1475
  limitation: native
1433
1476
  ? 'A successful tool exit is not treated as a successful business outcome when the result cannot be proven.'
1434
1477
  : routed
@@ -2272,6 +2315,7 @@ module.exports = {
2272
2315
  commandForSelection,
2273
2316
  buildGovernState,
2274
2317
  detectProjectSignals,
2318
+ defaultProof,
2275
2319
  recommendGovernanceMode,
2276
2320
  recordGovernanceModeSelection,
2277
2321
  gateDecision,
package/src/installer.js CHANGED
@@ -8,9 +8,9 @@ const { controllerStatus, controllerSupportedPlatform, ensureGovernanceControlle
8
8
  const DEFAULT_BASE_URL = 'https://api.getmarrow.ai';
9
9
  const MARROW_BLOCK_START = '<!-- marrow:passive-start -->';
10
10
  const MARROW_BLOCK_END = '<!-- marrow:passive-end -->';
11
- const MCP_ADAPTER_VERSION = '3.9.55';
12
- const SDK_ADAPTER_VERSION = '3.7.54';
13
- const SDK_ADAPTER_INTEGRITY = 'sha512-KMgZEZpKo0AVKimXi4/SmtyPDV3mfx9hrj5drCdZxdNP8P/FL7ciyvyAEGiiXIMemh5Z3D/PUUvVO4jhr16ozQ==';
11
+ const MCP_ADAPTER_VERSION = '3.9.57';
12
+ const SDK_ADAPTER_VERSION = '3.7.56';
13
+ const SDK_ADAPTER_INTEGRITY = 'sha512-5htliY4wfn8a1mbLT9N4OWXqhp9fWzMHuAQgUetc3RUKjOes5mWB3t91/leRKFRRIgCnEczJN6jHXg7Aw489Mw==';
14
14
  const SDK_ADAPTER_TARBALL = `https://registry.npmjs.org/@getmarrow/sdk/-/sdk-${SDK_ADAPTER_VERSION}.tgz`;
15
15
  const MCP_PACKAGE_SPEC = `@getmarrow/mcp@${MCP_ADAPTER_VERSION}`;
16
16
  const MCP_CONTEXT_HOOK_COMMAND = `npx -y ${MCP_PACKAGE_SPEC} context-hook`;
@@ -42,6 +42,116 @@ const HARNESS_CAPABILITY_REGISTRY = Object.freeze([
42
42
  { client: 'custom', capability_level: 'event_contract', automatic: [], install_surface: 'event_contract' },
43
43
  ]);
44
44
 
45
+ function explicitMcpVersion(command) {
46
+ const match = String(command || '').match(/@getmarrow\/mcp@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/);
47
+ return match ? match[1] : null;
48
+ }
49
+
50
+ function readMcpPackageVersion(packageRoot) {
51
+ try {
52
+ const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
53
+ return typeof pkg.version === 'string' && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(pkg.version)
54
+ ? pkg.version
55
+ : null;
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ function packageMcpVersion(command) {
62
+ const normalized = String(command || '').replace(/\0/g, ' ');
63
+ const packageRoots = [];
64
+ for (const match of normalized.matchAll(/(\/[^\s]+\/node_modules\/@getmarrow\/mcp)(?:\/|\s|$)/g)) {
65
+ packageRoots.push(match[1]);
66
+ }
67
+ for (const match of normalized.matchAll(/(\/[^\s]+\/node_modules\/\.bin\/marrow-mcp)(?:\s|$)/g)) {
68
+ const binPath = match[1];
69
+ packageRoots.push(path.resolve(path.dirname(binPath), '..', '@getmarrow', 'mcp'));
70
+ try {
71
+ const resolved = fs.realpathSync(binPath);
72
+ const marker = `${path.sep}node_modules${path.sep}@getmarrow${path.sep}mcp${path.sep}`;
73
+ const markerIndex = resolved.indexOf(marker);
74
+ if (markerIndex >= 0) packageRoots.push(resolved.slice(0, markerIndex + marker.length - 1));
75
+ } catch {
76
+ // The derived package root still gives a deterministic best-effort lookup.
77
+ }
78
+ }
79
+ for (const packageRoot of [...new Set(packageRoots)]) {
80
+ const version = readMcpPackageVersion(packageRoot);
81
+ if (version) return version;
82
+ }
83
+ return null;
84
+ }
85
+
86
+ function isMcpProcessCommand(command) {
87
+ const raw = String(command || '');
88
+ const args = (raw.includes('\0') ? raw.split('\0') : raw.trim().split(/\s+/)).filter(Boolean);
89
+ if (!args.length) return false;
90
+
91
+ const executable = path.basename(args[0]);
92
+ if (new Set(['bash', 'bwrap', 'dash', 'fish', 'sh', 'zsh']).has(executable)) return false;
93
+ if (executable === 'marrow-mcp') return true;
94
+
95
+ if (executable === 'node'
96
+ && args[1]
97
+ && /(?:^|\/)node_modules\/(?:@getmarrow\/mcp(?:\/|$)|\.bin\/marrow-mcp$)/.test(args[1])) {
98
+ return true;
99
+ }
100
+
101
+ const packageManagers = new Set(['bun', 'bunx', 'npm', 'npm-cli.js', 'npx', 'npx-cli.js', 'pnpm', 'pnpx', 'yarn']);
102
+ const runner = executable === 'node' && args[1] ? path.basename(args[1]) : executable;
103
+ return packageManagers.has(runner)
104
+ && args.some((arg) => /^@getmarrow\/mcp(?:@[^\s]+)?$/.test(arg));
105
+ }
106
+
107
+ function readLinuxProcessCommands(procRoot = '/proc') {
108
+ if (process.platform !== 'linux') return [];
109
+ try {
110
+ return fs.readdirSync(procRoot, { withFileTypes: true })
111
+ .filter((entry) => entry.isDirectory() && /^\d+$/.test(entry.name))
112
+ .map((entry) => {
113
+ try {
114
+ return fs.readFileSync(path.join(procRoot, entry.name, 'cmdline'), 'utf8');
115
+ } catch {
116
+ return '';
117
+ }
118
+ })
119
+ .filter(Boolean);
120
+ } catch {
121
+ return [];
122
+ }
123
+ }
124
+
125
+ function inspectMcpProcesses(options = {}) {
126
+ const commands = Array.isArray(options.commands)
127
+ ? options.commands.map(String)
128
+ : readLinuxProcessCommands(options.procRoot);
129
+ const active = commands
130
+ .filter(isMcpProcessCommand)
131
+ .map((command) => explicitMcpVersion(command) || packageMcpVersion(command) || 'unknown');
132
+ const versions = [...new Set(active.filter((version) => version !== 'unknown'))].sort();
133
+ const unknownVersionProcesses = active.filter((version) => version === 'unknown').length;
134
+ const staleVersions = versions.filter((version) => version !== MCP_ADAPTER_VERSION);
135
+ const mixedVersions = versions.length > 1 || (versions.length > 0 && unknownVersionProcesses > 0);
136
+ const stale = staleVersions.length > 0;
137
+ const needsRepair = stale || mixedVersions || unknownVersionProcesses > 0;
138
+ const repairCommand = `npx -y @getmarrow/mcp@${MCP_ADAPTER_VERSION} setup`;
139
+ return {
140
+ available: process.platform === 'linux' || Array.isArray(options.commands),
141
+ expected_version: MCP_ADAPTER_VERSION,
142
+ active_processes: active.length,
143
+ active_versions: versions,
144
+ unknown_version_processes: unknownVersionProcesses,
145
+ stale_versions: staleVersions,
146
+ mixed_versions: mixedVersions,
147
+ healthy: !needsRepair,
148
+ exact_fix: needsRepair ? repairCommand : null,
149
+ restart_required: needsRepair,
150
+ restart_instruction: needsRepair ? 'Restart every owning harness to replace its active Marrow MCP process.' : null,
151
+ verification_command: needsRepair ? 'npx -y @getmarrow/install@latest doctor' : null,
152
+ };
153
+ }
154
+
45
155
  function sourceClient() {
46
156
  const raw = String(process.env.MARROW_CLIENT || process.env.MARROW_HARNESS || process.env.MARROW_AGENT_CLIENT || '').trim().toLowerCase().replace(/\s+/g, '-').replace(/^@/, '');
47
157
  const aliases = {
@@ -1530,6 +1640,13 @@ function printReport(report) {
1530
1640
  process.stdout.write(`- missing env: ${report.doctor.missingEnv.length ? report.doctor.missingEnv.join(', ') : 'none'}\n`);
1531
1641
  if (report.doctor.envHints.length) process.stdout.write(`- possible env files: ${report.doctor.envHints.join(', ')}\n`);
1532
1642
  process.stdout.write(`- missing hooks/config: ${report.doctor.missingHooks.length ? report.doctor.missingHooks.join('; ') : 'none'}\n`);
1643
+ if (report.doctor.mcpProcesses?.available) {
1644
+ const processes = report.doctor.mcpProcesses;
1645
+ process.stdout.write(`- MCP process versions: ${processes.active_versions.length ? processes.active_versions.join(', ') : processes.active_processes ? 'unknown' : 'none'}\n`);
1646
+ process.stdout.write(`- stale/mixed/version-unknown MCP clients: ${processes.healthy ? 'no' : 'yes'}\n`);
1647
+ if (processes.restart_instruction) process.stdout.write(`- restart required: ${processes.restart_instruction}\n`);
1648
+ if (processes.verification_command) process.stdout.write(`- verify repair: ${processes.verification_command}\n`);
1649
+ }
1533
1650
  if (report.doctor.recommendedFix) process.stdout.write(`- recommended fix: ${report.doctor.recommendedFix}\n`);
1534
1651
  process.stdout.write(`- live health: ${report.doctor.healthCommand}\n`);
1535
1652
  }
@@ -1590,6 +1707,7 @@ async function install(options) {
1590
1707
  ? repairConfigDiagnostics(configDiagnostics)
1591
1708
  : [];
1592
1709
  const envHints = options.apiKey ? [] : findLikelyEnvFiles(detection);
1710
+ const mcpProcesses = inspectMcpProcesses({ commands: options.processCommands });
1593
1711
  let selfTest;
1594
1712
  try {
1595
1713
  selfTest = await runSelfTest(options);
@@ -1681,7 +1799,8 @@ async function install(options) {
1681
1799
  missingEnv: options.apiKey ? [] : ['MARROW_API_KEY'],
1682
1800
  envHints,
1683
1801
  missingHooks: changes.filter((change) => change.changed).map((change) => change.label),
1684
- recommendedFix: configDiagnostics.npm_token.recommended_fix || (!options.apiKey
1802
+ mcpProcesses,
1803
+ recommendedFix: mcpProcesses.exact_fix || configDiagnostics.npm_token.recommended_fix || (!options.apiKey
1685
1804
  ? envHints.length
1686
1805
  ? `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.`
1687
1806
  : 'Set MARROW_API_KEY, then run npx @getmarrow/install --repair.'
@@ -1694,9 +1813,14 @@ async function install(options) {
1694
1813
  sdkDependency,
1695
1814
  controller,
1696
1815
  selfTest,
1697
- warnings: options.keyFromArg
1698
- ? ['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.']
1699
- : [],
1816
+ warnings: [
1817
+ ...(options.keyFromArg
1818
+ ? ['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.']
1819
+ : []),
1820
+ ...(!mcpProcesses.healthy
1821
+ ? ['Stale, mixed, or version-unknown Marrow MCP clients are active. Run the exact repair command, then restart every owning harness.']
1822
+ : []),
1823
+ ],
1700
1824
  };
1701
1825
  }
1702
1826
 
@@ -1725,6 +1849,7 @@ module.exports = {
1725
1849
  passiveRuntimeSource,
1726
1850
  inspectNpmTokenConfig,
1727
1851
  inspectSdkDependency,
1852
+ inspectMcpProcesses,
1728
1853
  buildInstallValueMoment,
1729
1854
  buildTokenValueProof,
1730
1855
  stableAgentId,