@getmarrow/mcp 3.9.96 → 3.9.97

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
@@ -136,7 +136,11 @@ v3.9.87 hardens the authenticated control-path canary against single-sample tran
136
136
 
137
137
  v3.9.86 was published with the adapter version constant still at `3.9.85`, so the strict canary identity check rejected it; it is deprecated — use `3.9.87`. SDK `3.7.62` and installer `0.1.56` are unchanged; install MCP `3.9.87`, reload the host, review hook trust, and verify before claiming the updated client is active.
138
138
 
139
- ## What's New in v3.9.96
139
+ ## What's New in v3.9.97
140
+
141
+ v3.9.97 makes native hook control easier to live with. Free and starter gates are advisory again, so an advisory gate no longer hard-blocks work; a `block` decision still denies on every plan. In Claude Code's default, acceptEdits and auto permission modes, an enforced review now shows an owner-approval prompt instead of a denial. In plan, dontAsk and bypassPermissions modes, and for arbitration reviews, the action is still denied. Read-only commands such as `git show`, `git log` and `ls` no longer trigger false blocks, and denial messages now say plainly why an action was stopped and what to do next. The hook sends `protocol_version` when it verifies an action permit. When an action is denied, the hook makes a best-effort attempt, capped at 2.5 s, to close the decision with a denied outcome; if the backend does not confirm, the decision stays open. Hooks return faster because telemetry is delivered by a detached background process that honors opt-outs and never blocks the action (see `MARROW_HOOK_BACKGROUND_NUDGE` in Environment). Pending writes are retried more reliably: the client honors `retry_after_ms` and the optional `lease_remaining_ms`, and resumes the same write instead of starting a new one (see `MARROW_WRITE_RECONCILIATION_BUDGET_MS`). Hook lifecycle receipts are much less likely to be dropped when many hook processes write the spool at once: each event takes the spool lock fewer times, and the lock wait is a 10 s time budget with randomized polling. A receipt can still be lost if the lock is held for the full 10 s. Policy decisions, proof requirements and fail-closed behavior are unchanged, except that an enforced review now asks the owner (in the modes above) instead of denying. Update, reload the host and review hook trust before relying on the new hook.
142
+
143
+ ### Previous release: v3.9.96
140
144
 
141
145
  v3.9.96 fixes native pre-action hooks that denied protected actions after the runtime gate allowed them. The hook sent `source_meta` fields that Think rejects, and without a risk level the runtime answered protected actions on its low-risk fast path, whose receipt cannot back an action permit. Protected actions now request a durable gate and create their decision with accepted metadata only; unprotected actions stop at the gate without creating a decision or permit. A rejected control call now names its HTTP status and failure code without echoing service text. Policy decisions, proof requirements and fail-closed behavior are unchanged. Update, reload the host and review hook trust before relying on the new hook.
142
146
 
@@ -672,6 +676,8 @@ See the [Trust Center](https://getmarrow.ai/trust/) for implemented controls, cu
672
676
  | `MARROW_BASE_URL` | No | API base override |
673
677
  | `MARROW_AGENT_ID` | No | Bound agent identity for MCP tools |
674
678
  | `MARROW_FLEET_AGENT_ID` | No | Fleet agent identity used by passive setup |
679
+ | `MARROW_HOOK_BACKGROUND_NUDGE` | No | Default on: PostToolUse only spools the event and starts one detached background process that delivers it, so the hook adds almost no latency. Set to `false` to keep bounded inline delivery (750 ms) inside the hook. Never runs when `MARROW_AUTO_HOOK=false` or local control is disabled |
680
+ | `MARROW_WRITE_RECONCILIATION_BUDGET_MS` | No | Default `15000`, range 1000-60000: total time a pending think or commit may wait, resuming the same idempotency key and request hash after the server's `retry_after_ms` or `Retry-After`. When the next wait does not fit, the call returns the resumable pending receipt and never reports success |
675
681
 
676
682
  ## Documentation
677
683
 
package/dist/cli.js CHANGED
@@ -18,6 +18,7 @@ const hook_contract_1 = require("./hook-contract");
18
18
  const hook_context_1 = require("./hook-context");
19
19
  const hook_session_1 = require("./hook-session");
20
20
  const hook_pre_action_1 = require("./hook-pre-action");
21
+ const node_child_process_1 = require("node:child_process");
21
22
  const env_1 = require("./env");
22
23
  const lifecycle_spool_1 = require("./lifecycle-spool");
23
24
  const spool_command_1 = require("./spool-command");
@@ -61,6 +62,9 @@ function parseArgs() {
61
62
  if (args[i] === 'drain-spool' || args[i] === '--drain-spool') {
62
63
  result.drainSpool = true;
63
64
  }
65
+ if (args[i] === 'background-nudge') {
66
+ result.backgroundNudge = true;
67
+ }
64
68
  if (args[i] === 'ping' || args[i] === '--ping') {
65
69
  result.ping = true;
66
70
  }
@@ -81,6 +85,62 @@ function reportLifecycleSpool(input) {
81
85
  }
82
86
  return spool;
83
87
  }
88
+ // PostToolUse only spools. When this invocation actually spooled an event (so it passed
89
+ // MARROW_AUTO_HOOK, local-control and read-only gates), start one detached, unref'd
90
+ // process that delivers with the longer background timeout. No spool scan happens here.
91
+ function launchBackgroundLifecycleNudge() {
92
+ try {
93
+ if (!(0, hook_1.hookSpooledLifecycleEvent)() || !(0, lifecycle_spool_1.backgroundNudgeEnabled)())
94
+ return;
95
+ const identity = (0, hook_contract_2.resolveNativeHookIdentity)(process.argv[2]);
96
+ const apiKey = identity.environment.apiKey || '';
97
+ if (!apiKey)
98
+ return;
99
+ const nonce = (0, lifecycle_spool_1.claimBackgroundNudgeLock)({ apiKey, agentId: identity.agent_id });
100
+ if (!nonce)
101
+ return;
102
+ const child = (0, node_child_process_1.spawn)(process.execPath, [process.argv[1], 'background-nudge'], {
103
+ detached: true, stdio: 'ignore', windowsHide: true,
104
+ env: { ...process.env, MARROW_INTERNAL_NUDGE_LOCK: nonce },
105
+ });
106
+ child.on('error', () => (0, lifecycle_spool_1.releaseBackgroundNudgeLock)({ apiKey, agentId: identity.agent_id, nonce }));
107
+ child.unref();
108
+ }
109
+ catch { /* the event stays spooled for the next nudge */ }
110
+ }
111
+ async function runBackgroundNudge() {
112
+ const identity = (0, hook_contract_2.resolveNativeHookIdentity)(undefined);
113
+ const apiKey = identity.environment.apiKey || '';
114
+ const agentId = identity.agent_id;
115
+ let nonce = process.env.MARROW_INTERNAL_NUDGE_LOCK || '';
116
+ const release = () => { if (apiKey && nonce)
117
+ (0, lifecycle_spool_1.releaseBackgroundNudgeLock)({ apiKey, agentId, nonce }); };
118
+ // Hard lifetime cap, below the lock's stale threshold.
119
+ const watchdog = setTimeout(() => { release(); process.exit(0); }, lifecycle_spool_1.BACKGROUND_NUDGE_MAX_LIFETIME_MS);
120
+ try {
121
+ if (!apiKey || !nonce)
122
+ return;
123
+ const baseUrl = (0, index_1.validateBaseUrl)(identity.environment.baseUrl || 'https://api.getmarrow.ai');
124
+ // Deliver, then re-check once for events spooled while this process held the lock.
125
+ for (let round = 0; round < 3; round += 1) {
126
+ await (0, lifecycle_spool_1.nudgeLifecycleSpool)({ apiKey, baseUrl, agentId });
127
+ if (!(0, lifecycle_spool_1.hasDueLifecycleEvents)({ apiKey, agentId }))
128
+ break;
129
+ }
130
+ release();
131
+ const next = (0, lifecycle_spool_1.hasDueLifecycleEvents)({ apiKey, agentId }) ? (0, lifecycle_spool_1.claimBackgroundNudgeLock)({ apiKey, agentId }) : null;
132
+ if (next) {
133
+ nonce = next;
134
+ await (0, lifecycle_spool_1.nudgeLifecycleSpool)({ apiKey, baseUrl, agentId });
135
+ }
136
+ }
137
+ catch { /* best effort */ }
138
+ finally {
139
+ clearTimeout(watchdog);
140
+ release();
141
+ process.exit(0);
142
+ }
143
+ }
84
144
  async function runPingCommand() {
85
145
  if (cliArgs.apiKey) {
86
146
  process.stderr.write('Error: ping requires MARROW_API_KEY from trusted environment or owner configuration; --key is not accepted.\n');
@@ -365,7 +425,10 @@ if (process.argv[2] === 'keys') {
365
425
  // Only start MCP server if not handling a CLI command
366
426
  if (process.argv[2] !== 'keys') {
367
427
  if (cliArgs.hook) {
368
- void (0, hook_1.runHookCommand)();
428
+ void (0, hook_1.runHookCommand)().finally(launchBackgroundLifecycleNudge);
429
+ }
430
+ else if (cliArgs.backgroundNudge) {
431
+ void runBackgroundNudge();
369
432
  }
370
433
  else if (cliArgs.contextHook) {
371
434
  void (0, hook_context_1.runContextHookCommand)();