@getmarrow/install 0.1.64 → 0.1.66

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
@@ -62,7 +62,7 @@ Required secret:
62
62
  export MARROW_API_KEY=mrw_live_...
63
63
  ```
64
64
 
65
- The bare command is the default-on path: it detects and byte-idempotently writes managed configuration, runs the authenticated activation self-test, and starts the supported persistent controller. The key stays process-only and is never written to generated configuration or controller state. Use `--dry-run` for a non-writing preview, `doctor` for a read-only health check, or `--no-controller` to install and self-test without starting the controller. The explicit `activate` command remains equivalent and supported.
65
+ The bare command is the default-on path: it detects and byte-idempotently writes managed configuration, runs the authenticated activation self-test, and starts the supported persistent controller. It prints a short summary; the full report is written to `~/.marrow/logs/` (mode 600), and `--verbose` prints it instead. The key is read from `MARROW_API_KEY` or, when that is unset, from the owner-only `~/.marrow/env`. Without a key, the command prints the exact command to run and stops before writing anything. The key is never written to generated configuration, controller state or logs; the one exception is Hermes wiring, described under Trust and Data Boundaries. Use `--dry-run` for a non-writing preview, `doctor` for a read-only health check, or `--no-controller` to install and self-test without starting the controller. The explicit `activate` command remains equivalent and supported.
66
66
 
67
67
  ### MCP tool profiles
68
68
 
@@ -75,7 +75,8 @@ Tool visibility is not authorization. Every visible call still reaches Marrow's
75
75
  Marrow's hosted API, website, and dashboard update automatically; local SDK dependencies, generated runtime files, MCP hooks/configuration, and pinned package versions do not silently rewrite themselves. Keeping them current delivers new client-side features, compatibility improvements, and any published security fixes. Supported clients report their package version during authenticated status/runtime activity, and Marrow returns a `client_update` notice with the exact action when the version is behind or unknown.
76
76
 
77
77
  ```bash
78
- # When doctor reports stale or mixed MCP configuration
78
+ # One command: refreshes managed configuration, restarts an outdated controller,
79
+ # wires detected Hermes, and runs the self-test
79
80
  npx -y @getmarrow/install@latest update
80
81
 
81
82
  # Restart the detected owning harnesses once, then verify
@@ -85,7 +86,9 @@ npx -y @getmarrow/install@latest doctor --self-test
85
86
  npx -y --package=@getmarrow/mcp@latest marrow-mcp ping
86
87
  ```
87
88
 
88
- `update` resolves official npm metadata once, selects one exact verified MCP target, and synchronizes every Marrow-managed surface in the detected owning workspace while retaining unrelated user hooks and configuration. Restart the detected owning harnesses once after it completes; running processes do not change before that restart. Run the doctor verification once after restart. Do not run separate `marrow-mcp setup` and restart cycles for the same detected workspace.
89
+ `update` resolves official npm metadata once, selects one exact verified MCP target, and synchronizes every Marrow-managed surface in the detected owning workspace while retaining unrelated user hooks and configuration. It also restarts a controller that a different installer version started, and runs the self-test with a one-line summary. Restart the detected owning harnesses once after it completes; running processes do not change before that restart. Run the doctor verification once after restart. Do not run separate `marrow-mcp setup` and restart cycles for the same detected workspace.
90
+
91
+ `update` and `--repair` only refresh an existing install. Run from a directory with no Marrow-managed files while your home directory is managed, they stop without writing and print the exact `update --cwd <home>` command; to add Marrow to that project, run the install command there instead. Managed JSON that differs only in key order or formatting, for example after a harness re-saves its settings, counts as present and is not rewritten.
89
92
 
90
93
  `activate` remains available for initial activation. After explicit activation, the local controller can restore drifted Marrow-managed hooks and configuration. Package upgrades, owner policy, credentials, explicitly disabled hooks, and unrelated local files remain explicit and subject to the operator's normal change policy.
91
94
 
@@ -99,15 +102,73 @@ npx @getmarrow/install controller ensure
99
102
  npx @getmarrow/install controller stop
100
103
  ```
101
104
 
105
+ Install and update restart a controller that a different installer version started, including one left under an earlier identity directory for the same project, and report the restart in one line. The replaced controller is identified by its private state and authenticated endpoint before it is stopped. Controllers for other projects are not touched, and the owner's local control setting is not changed. `controller stop` stops every Marrow controller for the current project.
106
+
102
107
  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
108
 
104
- ## What's New in v0.1.64
109
+ The controller is not a boot service. After a host restart, or any exit that skips its shutdown handler, doctor reports it as `stale` until the next install, update, or `controller ensure`. While local control is disabled, the controller is not started, and doctor reports a stopped or stale controller as not required instead of recommending `controller ensure`; unsafe controller state or an unverified or unresponsive controller process keeps its exact fix.
110
+
111
+ ## What's New in v0.1.66
112
+
113
+ v0.1.66 keeps MCP `3.9.97` (source `be607e1dffd4d9e6a3c40f01151e509c115fba3f`, packed integrity `sha512-wab1kvgec8WhDDrpkajauLu2QdADXudIxkic0BAgzV96FZVHY0ME45hrOyQmqfkI+OtKriYdgIFp90peWGh2mA==`) and SDK `3.7.64` unchanged. It fixes the governed runner's gate and permit path, makes updating one command, and removes internal publishing tooling from the public package.
114
+
115
+ ### Update: one command
116
+
117
+ ```bash
118
+ MARROW_API_KEY=mrw_live_... npx -y @getmarrow/install@latest update
119
+ ```
120
+
121
+ First install is the same command without `update`. On its own, the command now:
122
+
123
+ - restarts a running controller that a different installer version started, so `controller stop` and `controller ensure` are no longer separate steps. Local control stays enabled or disabled as the owner left it;
124
+ - detects Hermes from `~/.hermes/config.yaml` (or `$HERMES_HOME`) or `hermes` on `PATH`, and adds or refreshes `mcp_servers.marrow` with the pinned MCP server and `MARROW_CLIENT: hermes`. The rest of the file, comments included, is kept. No copy of the file is made, because it holds other servers' credentials; the private log lists only the `mcp_servers.marrow` lines added or replaced, with values redacted, as undo steps. The edit is verified before it is written. If the file cannot be edited safely, or the existing `marrow` entry has a custom command or custom arguments, it is left untouched and the exact block to add is printed;
125
+ - needs no `MARROW_AGENT_ID`: Marrow uses the API key's bound agent or the plan's agent seat. `MARROW_AGENT_ID` still overrides;
126
+ - runs the self-test and prints a short summary: healthy with the committed decision id, or the failure (including a decision recorded without trusted closure) and one fix command, with a non-zero exit. The full report goes to `~/.marrow/logs/` (mode 600); `--verbose` prints it instead;
127
+ - reads the key from `MARROW_API_KEY`, or from the owner-only `~/.marrow/env.local` or `~/.marrow/env`, and names the file it used. It warns when those two files hold different keys. With no key it prints the command to run and stops before writing anything.
128
+
129
+ Restart the owning harness afterwards so new hooks and MCP configuration load; restart Hermes if its config changed.
105
130
 
106
- v0.1.64 pins MCP `3.9.96` (source `031c944936271fd6e8768ad2619ee0430b68e2c7`, packed integrity `sha512-kuBpuWaWAvusS+FbXXFw1fOEGja4T8ahj/vkmXTd/CWjz0t6teT6g9c1HfJhAK6KLzesZtcvUdZLn6pBErKP9Q==`) while keeping SDK `3.7.64` unchanged. The embedded MCP pin requires this installer patch to deliver the native pre-action hook permit fix: protected actions are no longer denied after an allowing runtime gate, and unprotected actions stop at the gate without creating a decision or permit. Policy decisions, proof requirements and fail-closed behavior are unchanged. `activate` and `doctor --self-test` now resend the identical self-test decision and commit with a stable `Idempotency-Key` after a transient 429/502/503/504 or a durable pending acknowledgement (at most three attempts, about one second apart), instead of failing with "self-test did not return decision_id"; client errors still fail immediately, and a final failure names the last state. Restart the owning harness, review hook trust and run `doctor --self-test` after updating. Package publication and registry integrity verification must precede claiming these candidate pins are publicly available.
131
+ ### Fixes
132
+
133
+ - **Governed runner gate and permits.** `gate`, `run` and `permit` read Marrow's real gate decision. Previously the runtime's default package response lacked the fields the runner read, so `gate` printed `unknown`, and every protected `run` sent an empty gate receipt, got HTTP 400 and exited 13.
134
+ - `gate` now prints the decision with its mode (enforced or advisory), and exits 12 when an enforced gate blocks or needs owner approval. It used to exit 0.
135
+ - A protected `run` passes the gate receipt to permit issue and closes the runtime's own decision instead of opening a second one.
136
+ - **Advisory plans.** Where the plan's gate is advisory (`enforced: false`), a protected `run` shows the warning, runs the command and records the outcome; it no longer asks for a permit the plan cannot issue. Enforced plans keep the permit requirement.
137
+ - **Retries and time limits.** The runner's runtime, think and commit calls, and the self-test's runtime and first-value calls, retry HTTP 429/502/503/504, timed-out attempts and pending answers up to three times with the same `Idempotency-Key`. Each attempt has a 10-second limit, and all attempts share a 25-second deadline. Every `run`, `gate` and `permit` uses fresh keys, so a later command in the same session is never answered with an earlier command's stored result. Client errors are not retried.
138
+ - **Self-test cleanup.** The self-test closes the decision its runtime check creates, instead of leaving it open.
139
+ - **Claude Code hook identity.** Claude Code hooks use the Claude-specific entrypoints (`claude-pre-action-hook`, `claude-hook`, `claude-context-hook`, `claude-session-hook`), the same spelling `marrow-mcp setup` writes. Hook activity is labelled `claude-code` instead of the generic `mcp-client`, and the installer and MCP setup no longer rewrite each other's entries. Existing entries are migrated in place without duplicates.
140
+ - **Agent identity.** No generated `<client>-<hash>` agent id is sent or written to MCP configuration or the SDK preload; one written by an earlier version is removed from `.mcp.json`. With an unbound key, activation reports that no single agent can be confirmed and how to bind one, instead of failing the install.
141
+ - **`run -- -- <command>`.** The documented form now runs the command; one `--` is enough.
142
+ - **Codex detection.** An `AGENTS.md` that holds only the Marrow block no longer marks a project as Codex.
143
+ - **Controller maintenance.** The five-minute maintenance pass re-applies the controller's own agent id and base URL instead of the generated id and default URL. A different value found in managed MCP configuration is reset and reported as needing attention, unless the owner allowlisted it.
144
+ - **`proof`.** `proof --decision-id` works again: the shared option parser had rejected it. It exits non-zero unless Marrow returns `committed: true`, and accepts `--gate-receipt` and `--session` for decisions created by `gate`.
145
+ - **Controller commands.** `controller ensure|stop|status` act on the controller that install and update started.
146
+ - **Removed.** `--repair` and `update` no longer read an npm token from `~/.openclaw` or write `~/.npmrc`; that was internal publishing tooling. Two internal credential-file hints were removed with it. Installer output no longer suggests invented agent ids such as `--agent hermes-prod`.
147
+
148
+ Plan semantics are unchanged: where a plan's gate is advisory, it stays advisory.
149
+
150
+ ### Previous release: v0.1.65
151
+
152
+ v0.1.65 pins MCP `3.9.97` (source `be607e1dffd4d9e6a3c40f01151e509c115fba3f`, packed integrity `sha512-wab1kvgec8WhDDrpkajauLu2QdADXudIxkic0BAgzV96FZVHY0ME45hrOyQmqfkI+OtKriYdgIFp90peWGh2mA==`) while keeping SDK `3.7.64` unchanged. Changes:
153
+
154
+ - `doctor` no longer reports hooks as missing after a harness re-saves its settings file.
155
+ - `update` refuses to write into an unrelated folder and only touches Marrow-managed roots.
156
+ - The install self-test handles pending decisions and never commits one as a success.
157
+ - Controller reporting is clearer; a disabled controller stays quiet while exact fixes for non-benign states are still shown.
158
+ - Enforcement heartbeats back off after repeated failures and retry once an hour.
159
+ - The agent id is never derived from the OS username.
160
+ - Permit verification sends `protocol_version`.
161
+ - The test suite is isolated from the real HOME and Marrow credentials.
162
+
163
+ Restart the owning harness, review hook trust and run `doctor --self-test` after updating.
164
+
165
+ ### Previous release: v0.1.64
166
+
167
+ v0.1.64 pins MCP `3.9.96` (source `031c944936271fd6e8768ad2619ee0430b68e2c7`, packed integrity `sha512-kuBpuWaWAvusS+FbXXFw1fOEGja4T8ahj/vkmXTd/CWjz0t6teT6g9c1HfJhAK6KLzesZtcvUdZLn6pBErKP9Q==`) while keeping SDK `3.7.64` unchanged. The embedded MCP pin requires this installer patch to deliver the native pre-action hook permit fix: protected actions are no longer denied after an allowing runtime gate, and unprotected actions stop at the gate without creating a decision or permit. Policy decisions, proof requirements and fail-closed behavior are unchanged. `activate` and `doctor --self-test` now resend the identical self-test decision and commit with a stable `Idempotency-Key` after a transient 429/502/503/504 or a durable pending acknowledgement (at most three attempts, about one second apart), instead of failing with "self-test did not return decision_id"; client errors still fail immediately, and a final failure names the last state. Restart the owning harness, review hook trust and run `doctor --self-test` after updating.
107
168
 
108
169
  ### Previous release: v0.1.63
109
170
 
110
- v0.1.63 pins MCP `3.9.95` (source `967b17735b26534b7dc0536ec93c481a5dc07297`, packed integrity `sha512-Sw8RIyxHkjxllwwh+9za5ItoM9X0NwN4R8+Qdd7nvOuU6qAm9LotIC09GOJDim3o9jc9m8yTqxKciroRTS12EQ==`) while keeping SDK `3.7.64` unchanged. The embedded MCP pin requires this installer patch to deliver the stdin usage-loss correction and safe bounded native Codex capture. Supported transcript schema is Codex `0.157.1`; capture covers only the latest proven model-call delta. Unknown versions, missing model/turn or billing metadata, unsafe paths and unproven subagent bindings remain incomplete or unpriced. Capture does not prove a baseline, complete overhead or savings. Restart the owning harness and run `doctor --self-test` after updating. Package publication and registry integrity verification must precede claiming these candidate pins are publicly available.
171
+ v0.1.63 pins MCP `3.9.95` (source `967b17735b26534b7dc0536ec93c481a5dc07297`, packed integrity `sha512-Sw8RIyxHkjxllwwh+9za5ItoM9X0NwN4R8+Qdd7nvOuU6qAm9LotIC09GOJDim3o9jc9m8yTqxKciroRTS12EQ==`) while keeping SDK `3.7.64` unchanged. The embedded MCP pin requires this installer patch to deliver the stdin usage-loss correction and safe bounded native Codex capture. Supported transcript schema is Codex `0.157.1`; capture covers only the latest proven model-call delta. Unknown versions, missing model/turn or billing metadata, unsafe paths and unproven subagent bindings remain incomplete or unpriced. Capture does not prove a baseline, complete overhead or savings. Restart the owning harness and run `doctor --self-test` after updating.
111
172
 
112
173
  ### Previous release: v0.1.62
113
174
 
@@ -308,7 +369,7 @@ npx @getmarrow/install activate
308
369
 
309
370
  The installer detects supported configuration and project signals for:
310
371
 
311
- - Codex, Claude Code, Cursor, Cursor Composer, Windsurf, Cline, OpenCode, Hermes, and OpenClaw;
372
+ - Codex, Claude Code, Cursor, Cursor Composer, Windsurf, Cline, OpenCode, Hermes, and OpenClaw. Hermes is detected from `~/.hermes/config.yaml` (or `$HERMES_HOME`) or `hermes` on `PATH`; an `AGENTS.md` holding only the Marrow block is not a Codex signal. `MARROW_CLIENT` overrides detection;
312
373
  - Gemini, Grok, DeepSeek, Qwen, Kimi, MiniMax, and GLM command-line or custom harness paths;
313
374
  - MCP client configuration;
314
375
  - Node.js and Python projects;
@@ -355,30 +416,36 @@ Place Marrow around an existing command without replacing the agent harness:
355
416
 
356
417
  ```bash
357
418
  npx @getmarrow/install run \
358
- --agent deploy-agent \
359
419
  --type deploy \
360
420
  --profile production \
361
421
  --policy enforce \
362
422
  -- wrangler deploy
363
423
  ```
364
424
 
425
+ The agent is the one Marrow resolves for the API key (its bound agent or the plan seat). Set `MARROW_AGENT_ID`, or pass `--agent <id>`, only for an agent already registered with Marrow. One `--` separates the command; `run -- -- <command>` also works.
426
+
365
427
  The runner:
366
428
 
367
- 1. requests the Marrow runtime gate;
368
- 2. records the governed decision against that exact gate;
369
- 3. requests and verifies a single-use permit bound to the exact action, target, and canonical action surfaces;
370
- 4. blocks protected work if policy or permit verification fails;
429
+ 1. requests the Marrow runtime gate and reads its decision, mode (enforced or advisory) and gate receipt;
430
+ 2. uses the decision the runtime created, or records one against that exact gate;
431
+ 3. where the plan enforces the gate, requests and verifies a single-use permit bound to the exact action, target, canonical action surfaces and gate receipt;
432
+ 4. blocks protected work if an enforced gate, policy or permit verification fails. Where the gate is advisory, it shows the warning and runs the command;
371
433
  5. runs the original command with the scoped permit, not the Marrow API key;
372
- 6. records success or failure, supplies every exact server-required proof field through a redacted proof pack, and closes the permit.
434
+ 6. records success or failure with the gate receipt, supplies every exact server-required proof field through a redacted proof pack, and closes the permit. It reports an outcome that Marrow did not commit as trusted instead of skipping it.
435
+
436
+ Runtime, think and commit calls retry HTTP 429/502/503/504, timed-out attempts and pending answers up to three times with the same `Idempotency-Key`, within a 25-second deadline. Each `run`, `gate` and `permit` uses its own keys. An answer in which Marrow withholds authorization (`allow: false` or observation-only) blocks the command under every plan and policy.
437
+
438
+ `gate` exits 0 when the action may proceed, 12 when an enforced gate blocks it or needs owner approval, and 13 when no gate decision is available, so `gate ... && deploy` stops on a block. When the gate creates a decision, it prints the exact `proof` command, with `--session` and `--gate-receipt`, that records the outcome afterwards. `proof` exits non-zero unless Marrow returns `committed: true`.
373
439
 
374
440
  Useful commands:
375
441
 
376
442
  ```bash
377
- npx @getmarrow/install gate --agent deploy-agent --type deploy --action "deploy production"
378
- npx @getmarrow/install permit --agent deploy-agent --type deploy --action "deploy production"
379
- MARROW_ACTION_PERMIT=... npx @getmarrow/install verify-permit --agent deploy-agent --type deploy --action "deploy production"
380
- npx @getmarrow/install coverage --agent deploy-agent
381
- npx @getmarrow/install sidecar --agent deploy-agent
443
+ npx @getmarrow/install gate --type deploy --action "deploy production"
444
+ npx @getmarrow/install permit --type deploy --action "deploy production"
445
+ MARROW_ACTION_PERMIT=... npx @getmarrow/install verify-permit --type deploy --action "deploy production"
446
+ npx @getmarrow/install proof --session <session> --decision-id <id> --gate-receipt <receipt> --success --summary "smoke passed"
447
+ npx @getmarrow/install coverage
448
+ npx @getmarrow/install sidecar
382
449
  npx @getmarrow/install controller status
383
450
  npx @getmarrow/install status
384
451
  npx @getmarrow/install doctor
@@ -417,7 +484,8 @@ Run `npx @getmarrow/install integrations --json` for the machine-readable matrix
417
484
  | 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 |
418
485
  | Codex, Gemini, Grok | Native hooks plus MCP on demand | Native pre-action/result/turn closeout; Grok Stop never blocks or retries | Owner-only MCP cache | Hook/MCP config and durable spool | Client-self-reported lifecycle evidence | Restart and host hook review required |
419
486
  | OpenCode, DeepSeek, Qwen, Kimi, MiniMax, GLM | Automatic only inside governed runner | Automatic when result is known; proof is evaluated and is advisory or enforced according to plan policy | Runner/runtime cache | Activated controller and durable buffer | Command, test, deployment, or owner evidence | Managed config after activation |
420
- | Hermes, OpenClaw, custom harnesses | Lifecycle adapter required | Adapter or governed runner required | Adapter dependent | Adapter dependent | Adapter supplied | Adapter owned |
487
+ | Hermes | MCP tools on demand after install or update adds `mcp_servers.marrow` to its config; no native pre-action hook | Explicit `marrow_commit`, or the governed runner for CLI commands | Owner-only MCP cache | Hermes config and durable spool | MCP lifecycle evidence | Install or update only; the controller does not edit the Hermes config |
488
+ | OpenClaw, custom harnesses | Lifecycle adapter required | Adapter or governed runner required | Adapter dependent | Adapter dependent | Adapter supplied | Adapter owned |
421
489
 
422
490
  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.
423
491
 
@@ -441,6 +509,9 @@ After meaningful work, supported runtime and commit responses can return observe
441
509
  - Agent-bound keys can be restricted to an allowed identity and permission set.
442
510
  - Sanitized aggregate contribution is optional and never means sharing raw prompts, code, secrets, proof packs, account identifiers, agent identifiers, or customer identities.
443
511
  - The installer diagnoses key locations without printing secret values.
512
+ - The installer never reads, reports or writes npm or other publishing tokens.
513
+ - Hermes passes only `PATH`, `HOME` and locale variables to MCP servers. When the Hermes entry and `$HERMES_HOME/.env` carry no Marrow key, install or update stores the key in the owner-only `~/.marrow/env` (mode 600) for the Marrow MCP server to read, and says so. A different key already stored there is left unchanged. The installer makes no copy of `config.yaml` or any other file that holds credentials; its undo notes contain only redacted lines.
514
+ - Managed MCP configuration always carries the configured API base and agent id. A different value found there is replaced and reported, unless the owner lists it in `MARROW_ALLOWED_BASE_URLS` or `MARROW_ALLOWED_AGENT_IDS`. Reports show only a URL's origin.
444
515
  - Marrow returns guidance and policy data. Agents must not execute returned text as shell input.
445
516
 
446
517
  See the [Trust Center](https://getmarrow.ai/trust/) for implemented controls, current limits, and roadmap status.
@@ -451,9 +522,12 @@ See the [Trust Center](https://getmarrow.ai/trust/) for implemented controls, cu
451
522
  | --- | --- | --- |
452
523
  | `MARROW_API_KEY` | Yes for live verification | Account or agent-bound API key |
453
524
  | `MARROW_BASE_URL` | No | API base override |
454
- | `MARROW_FLEET_AGENT_ID` | No | Default agent identity |
525
+ | `MARROW_FLEET_AGENT_ID`, `MARROW_AGENT_ID` | No | A registered agent id. Unset, Marrow uses the key's bound agent or the plan seat |
526
+ | `MARROW_CLIENT` | No | Harness label; overrides detection |
527
+ | `HERMES_HOME` | No | Hermes home, when not `~/.hermes` |
528
+ | `MARROW_ALLOWED_BASE_URLS`, `MARROW_ALLOWED_AGENT_IDS` | No | Comma-separated values that install, update and controller maintenance keep in managed MCP configuration instead of resetting them |
455
529
 
456
- Install and repair resolve one stable, non-secret agent identity before writing configuration. The managed MCP entry and owned-process SDK preload use that same identity and API base as doctor/self-test. The installer never copies `MARROW_API_KEY` into MCP configuration or generated runtime source; the owning harness must inherit the key from trusted environment or secret-manager configuration. Pass `--agent-id <id>` when an operator-approved identity must override the stable generated value.
530
+ The installer never generates, sends or writes an agent id of its own. Only a configured id (`MARROW_FLEET_AGENT_ID`, `MARROW_AGENT_ID` or `--agent-id`) goes into the managed MCP entry and SDK preload; without one, Marrow resolves the API key's bound agent or the plan's agent seat, and the self-test reports that server-resolved id. A key bound to no single agent still passes the self-test; activation then reports how to bind one. The installer never copies `MARROW_API_KEY` into MCP configuration or generated runtime source; the owning harness must inherit the key from trusted environment or secret-manager configuration, or from the owner-only `~/.marrow/env`.
457
531
 
458
532
  Use the host's secret manager first. The shared resolver can also check documented Marrow and project env files for owned development environments. Run `doctor` when a key or hook cannot be found.
459
533
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getmarrow/install",
3
- "version": "0.1.64",
3
+ "version": "0.1.66",
4
4
  "description": "Universal installer and governed runner for Marrow agent fleets.",
5
5
  "bin": {
6
6
  "marrow-install": "bin/marrow-install.js"
@@ -4,6 +4,8 @@ const os = require('node:os');
4
4
  const path = require('node:path');
5
5
  const { spawn } = require('node:child_process');
6
6
 
7
+ const { version: INSTALLER_VERSION } = require('../package.json');
8
+
7
9
  const START_TIMEOUT_MS = 5_000;
8
10
  const STOP_TIMEOUT_MS = 3_000;
9
11
  const HEALTH_TIMEOUT_MS = 1_000;
@@ -12,13 +14,18 @@ const LIFECYCLE_LOCK_STALE_MS = 30_000;
12
14
 
13
15
  function controllerIdentity(options = {}) {
14
16
  const root = path.resolve(options.root || process.cwd());
15
- const agentId = String(options.agentId || process.env.MARROW_FLEET_AGENT_ID || process.env.MARROW_AGENT_ID || 'agent').trim() || 'agent';
17
+ const agentId = String(options.identityAgentId || options.agentId || process.env.MARROW_FLEET_AGENT_ID || process.env.MARROW_AGENT_ID || 'agent').trim() || 'agent';
16
18
  return crypto.createHash('sha256').update(`${root}\0${agentId}`).digest('hex').slice(0, 24);
17
19
  }
18
20
 
21
+ function controllersRoot() {
22
+ return path.join(os.homedir(), '.marrow', 'controllers');
23
+ }
24
+
19
25
  function controllerDirectory(options = {}) {
20
- return process.env.MARROW_SIDECAR_STATE_DIR
21
- || path.join(os.homedir(), '.marrow', 'controllers', controllerIdentity(options));
26
+ return options.stateDirectory
27
+ || process.env.MARROW_SIDECAR_STATE_DIR
28
+ || path.join(controllersRoot(), controllerIdentity(options));
22
29
  }
23
30
 
24
31
  function stateFile(options = {}) {
@@ -71,7 +78,10 @@ function ensurePrivateDirectory(options = {}) {
71
78
  }
72
79
 
73
80
  function readState(options = {}) {
74
- const filePath = stateFile(options);
81
+ return readStateFile(stateFile(options));
82
+ }
83
+
84
+ function readStateFile(filePath) {
75
85
  if (!fs.existsSync(filePath)) return null;
76
86
  const stat = assertPrivatePath(filePath, 'state file');
77
87
  if (stat.size > MAX_STATE_BYTES) throw new Error('Controller state file is oversized.');
@@ -99,17 +109,55 @@ function controllerSupportedPlatform(platform = process.platform) {
99
109
  return platform === 'linux';
100
110
  }
101
111
 
112
+ // The bin of an @getmarrow/install package: <package>/bin/marrow-install.js with a
113
+ // package.json named @getmarrow/install. Returns the package version, or null.
114
+ function installerPackageVersionForBin(binPath) {
115
+ try {
116
+ const resolved = fs.realpathSync(binPath);
117
+ if (path.basename(resolved) !== 'marrow-install.js' || path.basename(path.dirname(resolved)) !== 'bin') return null;
118
+ const manifest = JSON.parse(fs.readFileSync(path.join(path.dirname(path.dirname(resolved)), 'package.json'), 'utf8'));
119
+ return manifest?.name === '@getmarrow/install' && typeof manifest.version === 'string' ? manifest.version : null;
120
+ } catch {
121
+ return null;
122
+ }
123
+ }
124
+
125
+ function controllerProcessBin(pid) {
126
+ const args = fs.readFileSync(`/proc/${pid}/cmdline`).toString('utf8').split('\0').filter(Boolean);
127
+ return args.length >= 3 && args[2] === 'sidecar' ? args[1] : null;
128
+ }
129
+
130
+ // A controller started by any @getmarrow/install version, including one from another npx
131
+ // cache directory, is recognized. Its identity is still proven by the private state file and
132
+ // the authenticated /health check before Marrow ever signals it.
102
133
  function isExpectedControllerProcess(pid, platform = process.platform) {
103
134
  if (!controllerSupportedPlatform(platform) || !Number.isInteger(pid) || pid <= 1) return false;
104
135
  try {
105
- const args = fs.readFileSync(`/proc/${pid}/cmdline`).toString('utf8').split('\0').filter(Boolean);
106
- if (args.length < 3 || args[2] !== 'sidecar') return false;
107
- return fs.realpathSync(args[1]) === fs.realpathSync(require.resolve('../bin/marrow-install.js'));
136
+ const binPath = controllerProcessBin(pid);
137
+ if (!binPath) return false;
138
+ return fs.realpathSync(binPath) === fs.realpathSync(require.resolve('../bin/marrow-install.js'))
139
+ || installerPackageVersionForBin(binPath) !== null;
108
140
  } catch {
109
141
  return false;
110
142
  }
111
143
  }
112
144
 
145
+ // The version a running controller loaded. Controllers from 0.1.66 on report it in /health;
146
+ // an older one does not, so its bin package version is reported with a legacy marker.
147
+ function runningControllerVersion(pid, healthBody) {
148
+ if (typeof healthBody?.installer_version === 'string' && healthBody.installer_version) {
149
+ return { version: healthBody.installer_version, reported: true };
150
+ }
151
+ let binVersion = null;
152
+ try {
153
+ const binPath = controllerProcessBin(pid);
154
+ binVersion = binPath ? installerPackageVersionForBin(binPath) : null;
155
+ } catch {
156
+ binVersion = null;
157
+ }
158
+ return { version: binVersion, reported: false };
159
+ }
160
+
113
161
  async function controllerStatus(options = {}) {
114
162
  if (!controllerSupportedPlatform(options.platform)) {
115
163
  return {
@@ -201,13 +249,20 @@ async function controllerStatus(options = {}) {
201
249
  const maintenance = body.maintenance && typeof body.maintenance === 'object'
202
250
  ? body.maintenance
203
251
  : null;
252
+ const heartbeat = body.heartbeat && typeof body.heartbeat === 'object'
253
+ ? body.heartbeat
254
+ : null;
255
+ const running = runningControllerVersion(state.pid, body);
204
256
  return {
205
257
  active: true,
206
258
  state: 'active',
207
259
  started_at: state.started_at,
208
260
  instance_id: state.instance_id,
261
+ installer_version: running.version,
262
+ installer_version_reported: running.reported,
209
263
  maintenance,
210
- exact_fix: maintenance?.exact_fix || null,
264
+ heartbeat,
265
+ exact_fix: maintenance?.exact_fix || heartbeat?.exact_fix || null,
211
266
  };
212
267
  } catch {
213
268
  return {
@@ -233,9 +288,15 @@ function cleanControllerEnv(options) {
233
288
  }
234
289
  env.MARROW_API_KEY = options.apiKey;
235
290
  env.MARROW_BASE_URL = options.baseUrl;
236
- env.MARROW_FLEET_AGENT_ID = options.agentId;
237
- env.MARROW_AGENT_ID = options.agentId;
291
+ if (options.agentId) {
292
+ env.MARROW_FLEET_AGENT_ID = options.agentId;
293
+ env.MARROW_AGENT_ID = options.agentId;
294
+ }
238
295
  env.MARROW_CLIENT = options.client;
296
+ // The owner's identity allowlist travels with the controller so maintenance honors it.
297
+ for (const name of ['MARROW_ALLOWED_BASE_URLS', 'MARROW_ALLOWED_AGENT_IDS']) {
298
+ if (process.env[name]) env[name] = process.env[name];
299
+ }
239
300
  env.MARROW_GOVERN_PROFILE = options.profile;
240
301
  env.MARROW_GOVERN_POLICY = options.policy;
241
302
  env.MARROW_CONTROLLER_PROJECT_ROOT = path.resolve(options.root || process.cwd());
@@ -370,7 +431,7 @@ async function startGovernanceController(options) {
370
431
  const args = [
371
432
  binPath,
372
433
  'sidecar',
373
- '--agent', options.agentId,
434
+ ...(options.agentId ? ['--agent', options.agentId] : []),
374
435
  '--client', options.client,
375
436
  '--profile', options.profile,
376
437
  '--policy', options.policy,
@@ -437,13 +498,104 @@ async function ensureGovernanceController(options) {
437
498
  return current.active ? { ...current, changed: false } : startGovernanceController(options);
438
499
  }
439
500
 
501
+ function controllerProjectRoot(pid, state) {
502
+ if (typeof state?.project_root === 'string' && state.project_root) return path.resolve(state.project_root);
503
+ // Controllers before 0.1.66 do not record their project in state. Their environment holds it;
504
+ // only that one variable is read, in memory, and nothing else from it is kept or printed.
505
+ try {
506
+ const entry = fs.readFileSync(`/proc/${pid}/environ`).toString('utf8').split('\0')
507
+ .find((value) => value.startsWith('MARROW_CONTROLLER_PROJECT_ROOT='));
508
+ return entry ? path.resolve(entry.slice('MARROW_CONTROLLER_PROJECT_ROOT='.length)) : null;
509
+ } catch {
510
+ return null;
511
+ }
512
+ }
513
+
514
+ // Live controllers owned by this user for one project, whatever identity directory they use.
515
+ // Identity directories changed across installer versions, so a controller can outlive the
516
+ // directory a newer installer computes for the same project.
517
+ async function projectControllers(options = {}) {
518
+ if (!controllerSupportedPlatform(options.platform)) return [];
519
+ const root = path.resolve(options.root || process.cwd());
520
+ const directories = process.env.MARROW_SIDECAR_STATE_DIR && !options.scanAll
521
+ ? [process.env.MARROW_SIDECAR_STATE_DIR]
522
+ : (() => {
523
+ try {
524
+ const base = controllersRoot();
525
+ const baseStat = fs.lstatSync(base);
526
+ if (!baseStat.isDirectory() || baseStat.isSymbolicLink()) return [];
527
+ return fs.readdirSync(base).filter((name) => /^[a-f0-9]{24}$/.test(name)).map((name) => path.join(base, name));
528
+ } catch {
529
+ return [];
530
+ }
531
+ })();
532
+ const found = [];
533
+ for (const directory of directories) {
534
+ let state;
535
+ try {
536
+ assertPrivatePath(directory, 'directory');
537
+ state = readStateFile(path.join(directory, 'active.json'));
538
+ } catch {
539
+ continue;
540
+ }
541
+ if (!state || !pidAlive(state.pid) || !isExpectedControllerProcess(state.pid, options.platform)) continue;
542
+ if (controllerProjectRoot(state.pid, state) !== root) continue;
543
+ const status = await controllerStatus({ ...options, stateDirectory: directory });
544
+ found.push({ directory, state, status });
545
+ }
546
+ return found;
547
+ }
548
+
549
+ // Keeps exactly one current controller for the project. A controller from another installer
550
+ // version, or one left in a superseded identity directory, is stopped through its verified
551
+ // endpoint and replaced. Local control state is not read or changed here; callers only ensure
552
+ // a controller while the owner has control enabled.
553
+ async function ensureCurrentGovernanceController(options) {
554
+ const currentDirectory = path.resolve(controllerDirectory(options));
555
+ const superseded = [];
556
+ for (const entry of await projectControllers(options)) {
557
+ const sameDirectory = path.resolve(entry.directory) === currentDirectory;
558
+ const current = entry.status.active && entry.status.installer_version === INSTALLER_VERSION
559
+ && entry.status.installer_version_reported === true;
560
+ if (sameDirectory && current) continue;
561
+ if (!entry.status.active && entry.status.state !== 'unreachable') continue;
562
+ await stopGovernanceController({ ...options, stateDirectory: entry.directory });
563
+ superseded.push(entry.status.installer_version
564
+ ? `${entry.status.installer_version}${entry.status.installer_version_reported ? '' : ' (legacy)'}`
565
+ : 'unknown');
566
+ }
567
+ const result = await ensureGovernanceController(options);
568
+ return {
569
+ ...result,
570
+ restarted: superseded.length > 0
571
+ ? { from_versions: superseded, to_version: INSTALLER_VERSION, stopped: superseded.length }
572
+ : null,
573
+ };
574
+ }
575
+
576
+ async function stopProjectControllers(options) {
577
+ const stopped = [];
578
+ for (const entry of await projectControllers(options)) {
579
+ await stopGovernanceController({ ...options, stateDirectory: entry.directory });
580
+ stopped.push(entry.status.installer_version || 'unknown');
581
+ }
582
+ const current = await stopGovernanceController(options);
583
+ return { ...current, changed: current.changed || stopped.length > 0, stopped: stopped.length };
584
+ }
585
+
440
586
  module.exports = {
587
+ INSTALLER_VERSION,
441
588
  controllerDirectory,
442
589
  controllerIdentity,
443
590
  controllerSupportedPlatform,
444
591
  controllerStatus,
592
+ ensureCurrentGovernanceController,
445
593
  ensureGovernanceController,
594
+ installerPackageVersionForBin,
595
+ isExpectedControllerProcess,
596
+ projectControllers,
446
597
  readState,
447
598
  startGovernanceController,
448
599
  stopGovernanceController,
600
+ stopProjectControllers,
449
601
  };
@@ -1,5 +1,10 @@
1
1
  const crypto = require('node:crypto');
2
2
 
3
+ // A background heartbeat must never hang on a stalled API response.
4
+ const HEARTBEAT_REQUEST_TIMEOUT_MS = 15_000;
5
+ // Nor may a permit call hold a protected command open indefinitely; it fails closed instead.
6
+ const PERMIT_REQUEST_TIMEOUT_MS = 15_000;
7
+
3
8
  function sha256(value) {
4
9
  return crypto.createHash('sha256').update(String(value || '')).digest('hex');
5
10
  }
@@ -17,11 +22,22 @@ function actionBinding(input) {
17
22
  };
18
23
  }
19
24
 
20
- async function enforcementRequest(requestJson, options, operation, input = {}) {
21
- return requestJson(options, 'POST', '/v1/agent/enforcement', {
22
- operation,
23
- ...input,
24
- });
25
+ // The server resolves the key-bound or seat agent when no agent id is configured.
26
+ function configuredAgentId(options) {
27
+ return String(options.agentId || '').trim() || undefined;
28
+ }
29
+
30
+ // Verify and close must declare the protocol the permit was issued under (1 or 2).
31
+ function permitProtocolVersion(input) {
32
+ const version = input.protocolVersion ?? input.protocol_version;
33
+ return version === 1 || version === 2 ? version : 1;
34
+ }
35
+
36
+ async function enforcementRequest(requestJson, options, operation, input = {}, requestOptions = undefined) {
37
+ const body = { operation, ...input };
38
+ return requestOptions
39
+ ? requestJson(options, 'POST', '/v1/agent/enforcement', body, {}, requestOptions)
40
+ : requestJson(options, 'POST', '/v1/agent/enforcement', body);
25
41
  }
26
42
 
27
43
  async function issueActionPermit(requestJson, options, input) {
@@ -29,7 +45,7 @@ async function issueActionPermit(requestJson, options, input) {
29
45
  return enforcementRequest(requestJson, options, 'issue', {
30
46
  ...binding,
31
47
  session_id: options.sessionId,
32
- agent_id: options.agentId,
48
+ agent_id: configuredAgentId(options),
33
49
  harness: options.client,
34
50
  policy_mode: options.policy,
35
51
  decision_id: input.decisionId || null,
@@ -37,7 +53,7 @@ async function issueActionPermit(requestJson, options, input) {
37
53
  owner_approval_receipt_id: input.ownerApproval || null,
38
54
  surfaces: Array.isArray(input.surfaces) ? input.surfaces : [],
39
55
  proof_requirements: Array.isArray(input.proofRequirements) ? input.proofRequirements : [],
40
- });
56
+ }, { timeoutMs: PERMIT_REQUEST_TIMEOUT_MS });
41
57
  }
42
58
 
43
59
  async function verifyActionPermit(requestJson, options, input) {
@@ -46,10 +62,11 @@ async function verifyActionPermit(requestJson, options, input) {
46
62
  ...binding,
47
63
  surfaces: Array.isArray(input.surfaces) ? input.surfaces : [],
48
64
  permit: input.permit,
65
+ protocol_version: permitProtocolVersion(input),
49
66
  session_id: options.sessionId,
50
- agent_id: options.agentId,
67
+ agent_id: configuredAgentId(options),
51
68
  harness: options.client,
52
- });
69
+ }, { timeoutMs: PERMIT_REQUEST_TIMEOUT_MS });
53
70
  }
54
71
 
55
72
  async function closeActionPermit(requestJson, options, input) {
@@ -58,22 +75,23 @@ async function closeActionPermit(requestJson, options, input) {
58
75
  permit_id: input.permitId || null,
59
76
  decision_id: input.decisionId || null,
60
77
  session_id: options.sessionId,
61
- agent_id: options.agentId,
78
+ agent_id: configuredAgentId(options),
62
79
  success: Boolean(input.success),
63
80
  evidence: input.evidence || {},
64
- });
81
+ protocol_version: permitProtocolVersion(input),
82
+ }, { timeoutMs: PERMIT_REQUEST_TIMEOUT_MS });
65
83
  }
66
84
 
67
85
  async function recordEnforcementHeartbeat(requestJson, options, input = {}) {
68
86
  return enforcementRequest(requestJson, options, 'heartbeat', {
69
87
  session_id: options.sessionId,
70
- agent_id: options.agentId,
88
+ agent_id: configuredAgentId(options),
71
89
  harness: options.client,
72
90
  sidecar_instance_id: input.sidecarInstanceId || null,
73
91
  config_fingerprint: input.configFingerprint || null,
74
92
  expected_hooks: input.expectedHooks || ['pre_action', 'action_result', 'outcome_closure'],
75
93
  observed_hooks: input.observedHooks || ['pre_action'],
76
- });
94
+ }, { timeoutMs: HEARTBEAT_REQUEST_TIMEOUT_MS });
77
95
  }
78
96
 
79
97
  async function readEnforcementCoverage(requestJson, options) {
@@ -83,8 +101,11 @@ async function readEnforcementCoverage(requestJson, options) {
83
101
  }
84
102
 
85
103
  module.exports = {
104
+ HEARTBEAT_REQUEST_TIMEOUT_MS,
105
+ PERMIT_REQUEST_TIMEOUT_MS,
86
106
  actionBinding,
87
107
  sha256,
108
+ permitProtocolVersion,
88
109
  enforcementRequest,
89
110
  issueActionPermit,
90
111
  verifyActionPermit,
package/src/first-hour.js CHANGED
@@ -49,6 +49,12 @@ function harnessReloadPlan(detection = {}, changes = []) {
49
49
  restart: 'Start a new Codex session, then review the repository hooks with /hooks before treating them as trusted or live.',
50
50
  });
51
51
  }
52
+ if (detection.hermes && (changes || []).some((change) => change?.applied && /Hermes/.test(String(change.label || '')))) {
53
+ clients.push({
54
+ client: 'hermes',
55
+ restart: 'Restart Hermes (the CLI session or `hermes gateway restart`) so it loads the Marrow MCP server from config.yaml.',
56
+ });
57
+ }
52
58
  if (clients.length === 0) {
53
59
  clients.push({
54
60
  client: 'mcp',
@@ -65,8 +71,7 @@ function harnessReloadPlan(detection = {}, changes = []) {
65
71
  };
66
72
  }
67
73
 
68
- function firstCapturePath(detection = {}, agentId) {
69
- const id = String(agentId || '').trim() || '<agent-id>';
74
+ function firstCapturePath(detection = {}) {
70
75
  if (detection.claudeCode) {
71
76
  return {
72
77
  client: 'claude-code',
@@ -123,10 +128,19 @@ function firstCapturePath(detection = {}, agentId) {
123
128
  instruction: 'After restart and /hooks inspection, Grok uses global native pre-action, result, failure-result, and one nonblocking Stop closeout hook. Hooks remain user-toggleable, MCP tools stay on demand, and configuration does not verify observed runtime coverage. The governed runner remains an explicit bounded fallback.',
124
129
  };
125
130
  }
131
+ if (detection.hermes) {
132
+ return {
133
+ client: 'hermes',
134
+ capability_level: 'mcp',
135
+ command: null,
136
+ instruction: 'After restarting Hermes, Marrow MCP tools are available on demand: call marrow_agent_runtime before deploy, merge or publish work, then marrow_commit with the returned decision and runtime authorization. Hermes has no native pre-action hook, so wrap CLI deploys with npx @getmarrow/install run -- <command>.',
137
+ };
138
+ }
126
139
  return {
127
140
  client: 'governed_wrapper',
128
141
  capability_level: 'governed_wrapper',
129
- command: `npx @getmarrow/install run --agent ${id} -- -- <command>`,
142
+ // One separator: npx forwards it to the runner, and the runner drops a doubled one.
143
+ command: 'npx @getmarrow/install run -- <command>',
130
144
  instruction: 'Wrap the next deploy, merge, or publish with the governed runner for hosts without a native gate. Before the session ends, call marrow_session_end or marrow_commit. Do not invent token counts.',
131
145
  };
132
146
  }