@getmarrow/install 0.1.65 → 0.1.67
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 +81 -22
- package/package.json +1 -1
- package/src/controller-manager.js +152 -6
- package/src/enforcement-client.js +6 -3
- package/src/first-hour.js +17 -3
- package/src/governance-sidecar.js +6 -0
- package/src/governed-runner.js +413 -90
- package/src/hermes-config.js +568 -0
- package/src/installer.js +767 -376
- package/src/owner-env.js +134 -0
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
|
|
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
|
-
#
|
|
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,7 @@ 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.
|
|
89
90
|
|
|
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.
|
|
91
92
|
|
|
@@ -101,11 +102,56 @@ npx @getmarrow/install controller ensure
|
|
|
101
102
|
npx @getmarrow/install controller stop
|
|
102
103
|
```
|
|
103
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
|
+
|
|
104
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.
|
|
105
108
|
|
|
106
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.
|
|
107
110
|
|
|
108
|
-
## What's New in v0.1.
|
|
111
|
+
## What's New in v0.1.67
|
|
112
|
+
|
|
113
|
+
v0.1.67 installs MCP `3.9.98` (source `e40d3cb40479456fd937bce0b9488eb0c3f10863`, packed integrity `sha512-AmDT3afwdm7+Dc555zDs+yGIG4RyC/YbaQm+9O1mThlC6g/9EujTr7y7UvRMEtYDvVWAAaG6CFM7/u/ytjKhwQ==`), which makes `npx @getmarrow/mcp ...` commands work again. SDK `3.7.64` and everything else are unchanged.
|
|
114
|
+
|
|
115
|
+
### Previous release: v0.1.66
|
|
116
|
+
|
|
117
|
+
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.
|
|
118
|
+
|
|
119
|
+
#### Update: one command
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
MARROW_API_KEY=mrw_live_... npx -y @getmarrow/install@latest update
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
First install is the same command without `update`. On its own, the command now:
|
|
126
|
+
|
|
127
|
+
- 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;
|
|
128
|
+
- 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;
|
|
129
|
+
- needs no `MARROW_AGENT_ID`: Marrow uses the API key's bound agent or the plan's agent seat. `MARROW_AGENT_ID` still overrides;
|
|
130
|
+
- 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;
|
|
131
|
+
- 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.
|
|
132
|
+
|
|
133
|
+
Restart the owning harness afterwards so new hooks and MCP configuration load; restart Hermes if its config changed.
|
|
134
|
+
|
|
135
|
+
#### Fixes
|
|
136
|
+
|
|
137
|
+
- **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.
|
|
138
|
+
- `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.
|
|
139
|
+
- A protected `run` passes the gate receipt to permit issue and closes the runtime's own decision instead of opening a second one.
|
|
140
|
+
- **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.
|
|
141
|
+
- **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.
|
|
142
|
+
- **Self-test cleanup.** The self-test closes the decision its runtime check creates, instead of leaving it open.
|
|
143
|
+
- **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.
|
|
144
|
+
- **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.
|
|
145
|
+
- **`run -- -- <command>`.** The documented form now runs the command; one `--` is enough.
|
|
146
|
+
- **Codex detection.** An `AGENTS.md` that holds only the Marrow block no longer marks a project as Codex.
|
|
147
|
+
- **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.
|
|
148
|
+
- **`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`.
|
|
149
|
+
- **Controller commands.** `controller ensure|stop|status` act on the controller that install and update started.
|
|
150
|
+
- **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`.
|
|
151
|
+
|
|
152
|
+
Plan semantics are unchanged: where a plan's gate is advisory, it stays advisory.
|
|
153
|
+
|
|
154
|
+
### Previous release: v0.1.65
|
|
109
155
|
|
|
110
156
|
v0.1.65 pins MCP `3.9.97` (source `be607e1dffd4d9e6a3c40f01151e509c115fba3f`, packed integrity `sha512-wab1kvgec8WhDDrpkajauLu2QdADXudIxkic0BAgzV96FZVHY0ME45hrOyQmqfkI+OtKriYdgIFp90peWGh2mA==`) while keeping SDK `3.7.64` unchanged. Changes:
|
|
111
157
|
|
|
@@ -118,15 +164,15 @@ v0.1.65 pins MCP `3.9.97` (source `be607e1dffd4d9e6a3c40f01151e509c115fba3f`, pa
|
|
|
118
164
|
- Permit verification sends `protocol_version`.
|
|
119
165
|
- The test suite is isolated from the real HOME and Marrow credentials.
|
|
120
166
|
|
|
121
|
-
Restart the owning harness, review hook trust and run `doctor --self-test` after updating.
|
|
167
|
+
Restart the owning harness, review hook trust and run `doctor --self-test` after updating.
|
|
122
168
|
|
|
123
169
|
### Previous release: v0.1.64
|
|
124
170
|
|
|
125
|
-
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.
|
|
171
|
+
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.
|
|
126
172
|
|
|
127
173
|
### Previous release: v0.1.63
|
|
128
174
|
|
|
129
|
-
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.
|
|
175
|
+
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.
|
|
130
176
|
|
|
131
177
|
### Previous release: v0.1.62
|
|
132
178
|
|
|
@@ -327,7 +373,7 @@ npx @getmarrow/install activate
|
|
|
327
373
|
|
|
328
374
|
The installer detects supported configuration and project signals for:
|
|
329
375
|
|
|
330
|
-
- Codex, Claude Code, Cursor, Cursor Composer, Windsurf, Cline, OpenCode, Hermes, and OpenClaw;
|
|
376
|
+
- 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;
|
|
331
377
|
- Gemini, Grok, DeepSeek, Qwen, Kimi, MiniMax, and GLM command-line or custom harness paths;
|
|
332
378
|
- MCP client configuration;
|
|
333
379
|
- Node.js and Python projects;
|
|
@@ -374,30 +420,36 @@ Place Marrow around an existing command without replacing the agent harness:
|
|
|
374
420
|
|
|
375
421
|
```bash
|
|
376
422
|
npx @getmarrow/install run \
|
|
377
|
-
--agent deploy-agent \
|
|
378
423
|
--type deploy \
|
|
379
424
|
--profile production \
|
|
380
425
|
--policy enforce \
|
|
381
426
|
-- wrangler deploy
|
|
382
427
|
```
|
|
383
428
|
|
|
429
|
+
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.
|
|
430
|
+
|
|
384
431
|
The runner:
|
|
385
432
|
|
|
386
|
-
1. requests the Marrow runtime gate;
|
|
387
|
-
2.
|
|
388
|
-
3. requests and verifies a single-use permit bound to the exact action, target,
|
|
389
|
-
4. blocks protected work if policy or permit verification fails;
|
|
433
|
+
1. requests the Marrow runtime gate and reads its decision, mode (enforced or advisory) and gate receipt;
|
|
434
|
+
2. uses the decision the runtime created, or records one against that exact gate;
|
|
435
|
+
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;
|
|
436
|
+
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;
|
|
390
437
|
5. runs the original command with the scoped permit, not the Marrow API key;
|
|
391
|
-
6. records success or failure, supplies every exact server-required proof field through a redacted proof pack, and closes the permit.
|
|
438
|
+
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.
|
|
439
|
+
|
|
440
|
+
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.
|
|
441
|
+
|
|
442
|
+
`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`.
|
|
392
443
|
|
|
393
444
|
Useful commands:
|
|
394
445
|
|
|
395
446
|
```bash
|
|
396
|
-
npx @getmarrow/install gate --
|
|
397
|
-
npx @getmarrow/install permit --
|
|
398
|
-
MARROW_ACTION_PERMIT=... npx @getmarrow/install verify-permit --
|
|
399
|
-
npx @getmarrow/install
|
|
400
|
-
npx @getmarrow/install
|
|
447
|
+
npx @getmarrow/install gate --type deploy --action "deploy production"
|
|
448
|
+
npx @getmarrow/install permit --type deploy --action "deploy production"
|
|
449
|
+
MARROW_ACTION_PERMIT=... npx @getmarrow/install verify-permit --type deploy --action "deploy production"
|
|
450
|
+
npx @getmarrow/install proof --session <session> --decision-id <id> --gate-receipt <receipt> --success --summary "smoke passed"
|
|
451
|
+
npx @getmarrow/install coverage
|
|
452
|
+
npx @getmarrow/install sidecar
|
|
401
453
|
npx @getmarrow/install controller status
|
|
402
454
|
npx @getmarrow/install status
|
|
403
455
|
npx @getmarrow/install doctor
|
|
@@ -436,7 +488,8 @@ Run `npx @getmarrow/install integrations --json` for the machine-readable matrix
|
|
|
436
488
|
| 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 |
|
|
437
489
|
| 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 |
|
|
438
490
|
| 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 |
|
|
439
|
-
| Hermes
|
|
491
|
+
| 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 |
|
|
492
|
+
| OpenClaw, custom harnesses | Lifecycle adapter required | Adapter or governed runner required | Adapter dependent | Adapter dependent | Adapter supplied | Adapter owned |
|
|
440
493
|
|
|
441
494
|
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.
|
|
442
495
|
|
|
@@ -460,6 +513,9 @@ After meaningful work, supported runtime and commit responses can return observe
|
|
|
460
513
|
- Agent-bound keys can be restricted to an allowed identity and permission set.
|
|
461
514
|
- Sanitized aggregate contribution is optional and never means sharing raw prompts, code, secrets, proof packs, account identifiers, agent identifiers, or customer identities.
|
|
462
515
|
- The installer diagnoses key locations without printing secret values.
|
|
516
|
+
- The installer never reads, reports or writes npm or other publishing tokens.
|
|
517
|
+
- 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.
|
|
518
|
+
- 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.
|
|
463
519
|
- Marrow returns guidance and policy data. Agents must not execute returned text as shell input.
|
|
464
520
|
|
|
465
521
|
See the [Trust Center](https://getmarrow.ai/trust/) for implemented controls, current limits, and roadmap status.
|
|
@@ -470,9 +526,12 @@ See the [Trust Center](https://getmarrow.ai/trust/) for implemented controls, cu
|
|
|
470
526
|
| --- | --- | --- |
|
|
471
527
|
| `MARROW_API_KEY` | Yes for live verification | Account or agent-bound API key |
|
|
472
528
|
| `MARROW_BASE_URL` | No | API base override |
|
|
473
|
-
| `MARROW_FLEET_AGENT_ID` | No |
|
|
529
|
+
| `MARROW_FLEET_AGENT_ID`, `MARROW_AGENT_ID` | No | A registered agent id. Unset, Marrow uses the key's bound agent or the plan seat |
|
|
530
|
+
| `MARROW_CLIENT` | No | Harness label; overrides detection |
|
|
531
|
+
| `HERMES_HOME` | No | Hermes home, when not `~/.hermes` |
|
|
532
|
+
| `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 |
|
|
474
533
|
|
|
475
|
-
|
|
534
|
+
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`.
|
|
476
535
|
|
|
477
536
|
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.
|
|
478
537
|
|
package/package.json
CHANGED
|
@@ -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;
|
|
@@ -16,9 +18,14 @@ function controllerIdentity(options = {}) {
|
|
|
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
|
|
21
|
-
||
|
|
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
|
-
|
|
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
|
|
106
|
-
if (
|
|
107
|
-
return fs.realpathSync(
|
|
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 {
|
|
@@ -204,11 +252,14 @@ async function controllerStatus(options = {}) {
|
|
|
204
252
|
const heartbeat = body.heartbeat && typeof body.heartbeat === 'object'
|
|
205
253
|
? body.heartbeat
|
|
206
254
|
: null;
|
|
255
|
+
const running = runningControllerVersion(state.pid, body);
|
|
207
256
|
return {
|
|
208
257
|
active: true,
|
|
209
258
|
state: 'active',
|
|
210
259
|
started_at: state.started_at,
|
|
211
260
|
instance_id: state.instance_id,
|
|
261
|
+
installer_version: running.version,
|
|
262
|
+
installer_version_reported: running.reported,
|
|
212
263
|
maintenance,
|
|
213
264
|
heartbeat,
|
|
214
265
|
exact_fix: maintenance?.exact_fix || heartbeat?.exact_fix || null,
|
|
@@ -242,6 +293,10 @@ function cleanControllerEnv(options) {
|
|
|
242
293
|
env.MARROW_AGENT_ID = options.agentId;
|
|
243
294
|
}
|
|
244
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
|
+
}
|
|
245
300
|
env.MARROW_GOVERN_PROFILE = options.profile;
|
|
246
301
|
env.MARROW_GOVERN_POLICY = options.policy;
|
|
247
302
|
env.MARROW_CONTROLLER_PROJECT_ROOT = path.resolve(options.root || process.cwd());
|
|
@@ -443,13 +498,104 @@ async function ensureGovernanceController(options) {
|
|
|
443
498
|
return current.active ? { ...current, changed: false } : startGovernanceController(options);
|
|
444
499
|
}
|
|
445
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
|
+
|
|
446
586
|
module.exports = {
|
|
587
|
+
INSTALLER_VERSION,
|
|
447
588
|
controllerDirectory,
|
|
448
589
|
controllerIdentity,
|
|
449
590
|
controllerSupportedPlatform,
|
|
450
591
|
controllerStatus,
|
|
592
|
+
ensureCurrentGovernanceController,
|
|
451
593
|
ensureGovernanceController,
|
|
594
|
+
installerPackageVersionForBin,
|
|
595
|
+
isExpectedControllerProcess,
|
|
596
|
+
projectControllers,
|
|
452
597
|
readState,
|
|
453
598
|
startGovernanceController,
|
|
454
599
|
stopGovernanceController,
|
|
600
|
+
stopProjectControllers,
|
|
455
601
|
};
|
|
@@ -2,6 +2,8 @@ const crypto = require('node:crypto');
|
|
|
2
2
|
|
|
3
3
|
// A background heartbeat must never hang on a stalled API response.
|
|
4
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;
|
|
5
7
|
|
|
6
8
|
function sha256(value) {
|
|
7
9
|
return crypto.createHash('sha256').update(String(value || '')).digest('hex');
|
|
@@ -51,7 +53,7 @@ async function issueActionPermit(requestJson, options, input) {
|
|
|
51
53
|
owner_approval_receipt_id: input.ownerApproval || null,
|
|
52
54
|
surfaces: Array.isArray(input.surfaces) ? input.surfaces : [],
|
|
53
55
|
proof_requirements: Array.isArray(input.proofRequirements) ? input.proofRequirements : [],
|
|
54
|
-
});
|
|
56
|
+
}, { timeoutMs: PERMIT_REQUEST_TIMEOUT_MS });
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
async function verifyActionPermit(requestJson, options, input) {
|
|
@@ -64,7 +66,7 @@ async function verifyActionPermit(requestJson, options, input) {
|
|
|
64
66
|
session_id: options.sessionId,
|
|
65
67
|
agent_id: configuredAgentId(options),
|
|
66
68
|
harness: options.client,
|
|
67
|
-
});
|
|
69
|
+
}, { timeoutMs: PERMIT_REQUEST_TIMEOUT_MS });
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
async function closeActionPermit(requestJson, options, input) {
|
|
@@ -77,7 +79,7 @@ async function closeActionPermit(requestJson, options, input) {
|
|
|
77
79
|
success: Boolean(input.success),
|
|
78
80
|
evidence: input.evidence || {},
|
|
79
81
|
protocol_version: permitProtocolVersion(input),
|
|
80
|
-
});
|
|
82
|
+
}, { timeoutMs: PERMIT_REQUEST_TIMEOUT_MS });
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
async function recordEnforcementHeartbeat(requestJson, options, input = {}) {
|
|
@@ -100,6 +102,7 @@ async function readEnforcementCoverage(requestJson, options) {
|
|
|
100
102
|
|
|
101
103
|
module.exports = {
|
|
102
104
|
HEARTBEAT_REQUEST_TIMEOUT_MS,
|
|
105
|
+
PERMIT_REQUEST_TIMEOUT_MS,
|
|
103
106
|
actionBinding,
|
|
104
107
|
sha256,
|
|
105
108
|
permitProtocolVersion,
|
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 = {}
|
|
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
|
-
|
|
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
|
}
|
|
@@ -3,6 +3,7 @@ const fs = require('node:fs');
|
|
|
3
3
|
const http = require('node:http');
|
|
4
4
|
const os = require('node:os');
|
|
5
5
|
const path = require('node:path');
|
|
6
|
+
const { version: INSTALLER_VERSION } = require('../package.json');
|
|
6
7
|
|
|
7
8
|
const MAX_BODY_BYTES = 64 * 1024;
|
|
8
9
|
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
@@ -240,6 +241,7 @@ async function startGovernanceSidecar(options, handlers) {
|
|
|
240
241
|
instance_id: instanceId,
|
|
241
242
|
pid: process.pid,
|
|
242
243
|
started_at: startedAt,
|
|
244
|
+
installer_version: INSTALLER_VERSION,
|
|
243
245
|
maintenance: latestMaintenance,
|
|
244
246
|
heartbeat: latestHeartbeat,
|
|
245
247
|
});
|
|
@@ -277,6 +279,10 @@ async function startGovernanceSidecar(options, handlers) {
|
|
|
277
279
|
port: boundPort,
|
|
278
280
|
token: authToken,
|
|
279
281
|
started_at: startedAt,
|
|
282
|
+
installer_version: INSTALLER_VERSION,
|
|
283
|
+
...(process.env.MARROW_CONTROLLER_PROJECT_ROOT
|
|
284
|
+
? { project_root: path.resolve(process.env.MARROW_CONTROLLER_PROJECT_ROOT) }
|
|
285
|
+
: {}),
|
|
280
286
|
});
|
|
281
287
|
} catch (error) {
|
|
282
288
|
await new Promise((resolve) => server.close(resolve));
|