@getmarrow/install 0.1.40 → 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,7 +91,17 @@ 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.40
94
+ ## What's New in v0.1.41
95
+
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
95
105
 
96
106
  v0.1.40 binds governed runs to a privacy-safe workspace fingerprint and separates observed execution from verified completion:
97
107
 
@@ -99,7 +109,7 @@ v0.1.40 binds governed runs to a privacy-safe workspace fingerprint and separate
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.56` and SDK `3.7.55` 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;
103
113
  - governed runtime requests attach a stable privacy-safe project fingerprint and harness label without sending the raw working-directory path;
104
114
  - successful command exit remains observed execution, not verified business completion, unless a verification command or explicit proof file supplies evidence;
105
115
  - the integration matrix now reports prompt injection, pre-action, action result, closure, proof, cached brief, restart survival, evidence adapter, and safe repair separately.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getmarrow/install",
3
- "version": "0.1.40",
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"
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.56';
12
- const SDK_ADAPTER_VERSION = '3.7.55';
13
- const SDK_ADAPTER_INTEGRITY = 'sha512-aq8g3srJ9EFZVvskSQVE9MLUS8SSkvx3xY3Mr9G0ZkpKxiiDBQaa3r2pb/u74BYGVvJRP6EP/oV/D9jbKb2lBA==';
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,