@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 +95 -21
- package/package.json +1 -1
- package/src/controller-manager.js +163 -11
- package/src/enforcement-client.js +34 -13
- package/src/first-hour.js +17 -3
- package/src/governance-sidecar.js +134 -18
- package/src/governed-runner.js +430 -99
- package/src/hermes-config.js +568 -0
- package/src/installer.js +875 -385
- 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,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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
369
|
-
3. requests and verifies a single-use permit bound to the exact action, target,
|
|
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 --
|
|
378
|
-
npx @getmarrow/install permit --
|
|
379
|
-
MARROW_ACTION_PERMIT=... npx @getmarrow/install verify-permit --
|
|
380
|
-
npx @getmarrow/install
|
|
381
|
-
npx @getmarrow/install
|
|
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
|
|
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 |
|
|
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
|
-
|
|
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
|
@@ -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
|
|
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 {
|
|
@@ -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
|
-
|
|
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
|
-
|
|
237
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 = {}
|
|
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
|
}
|