@getmarrow/install 0.1.62 → 0.1.64

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
@@ -101,7 +101,15 @@ npx @getmarrow/install controller stop
101
101
 
102
102
  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
103
 
104
- ## What's New in v0.1.62
104
+ ## What's New in v0.1.64
105
+
106
+ v0.1.64 pins MCP `3.9.96` (source `031c944936271fd6e8768ad2619ee0430b68e2c7`, packed integrity `sha512-kuBpuWaWAvusS+FbXXFw1fOEGja4T8ahj/vkmXTd/CWjz0t6teT6g9c1HfJhAK6KLzesZtcvUdZLn6pBErKP9Q==`) while keeping SDK `3.7.64` unchanged. The embedded MCP pin requires this installer patch to deliver the native pre-action hook permit fix: protected actions are no longer denied after an allowing runtime gate, and unprotected actions stop at the gate without creating a decision or permit. Policy decisions, proof requirements and fail-closed behavior are unchanged. `activate` and `doctor --self-test` now resend the identical self-test decision and commit with a stable `Idempotency-Key` after a transient 429/502/503/504 or a durable pending acknowledgement (at most three attempts, about one second apart), instead of failing with "self-test did not return decision_id"; client errors still fail immediately, and a final failure names the last state. Restart the owning harness, review hook trust and run `doctor --self-test` after updating. Package publication and registry integrity verification must precede claiming these candidate pins are publicly available.
107
+
108
+ ### Previous release: v0.1.63
109
+
110
+ v0.1.63 pins MCP `3.9.95` (source `967b17735b26534b7dc0536ec93c481a5dc07297`, packed integrity `sha512-Sw8RIyxHkjxllwwh+9za5ItoM9X0NwN4R8+Qdd7nvOuU6qAm9LotIC09GOJDim3o9jc9m8yTqxKciroRTS12EQ==`) while keeping SDK `3.7.64` unchanged. The embedded MCP pin requires this installer patch to deliver the stdin usage-loss correction and safe bounded native Codex capture. Supported transcript schema is Codex `0.157.1`; capture covers only the latest proven model-call delta. Unknown versions, missing model/turn or billing metadata, unsafe paths and unproven subagent bindings remain incomplete or unpriced. Capture does not prove a baseline, complete overhead or savings. Restart the owning harness and run `doctor --self-test` after updating. Package publication and registry integrity verification must precede claiming these candidate pins are publicly available.
111
+
112
+ ### Previous release: v0.1.62
105
113
 
106
114
  v0.1.62 delivers the model-cost capture corrections in MCP `3.9.94` (source `9456bc63de4cc92f16726d996695516be9395f26`, packed integrity `sha512-gEQejUWkcuKd1p930TLsWCSMM4rjmr/Wzz0++qa69B6hj/e64lMdT9xO+jZP8+yl4WLKgcfo0raHANllUe96/A==`) and SDK `3.7.64` (source `40b68dee609e9351fa6c79aee629fc88869a3b4f`, packed integrity `sha512-8qJj/8ouHEz1NnZkmujtFxUm/fWldqR/rHv62/sqabaxT0H90xCCHxodLOkV0SVxDscAtnahioZakqkeGNUwyA==`). Published installer0.1.61 pins older bytes and cannot deliver these fixes. Only observed usage with sufficient host, model, token and pricing evidence becomes calculated cost; unobservable host usage, unsupported streams and mixed cache TTL writes stay incomplete. No baseline or net savings is invented. Update once, restart the owning harness and run `doctor --self-test` to verify actual active versions.
107
115
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getmarrow/install",
3
- "version": "0.1.62",
3
+ "version": "0.1.64",
4
4
  "description": "Universal installer and governed runner for Marrow agent fleets.",
5
5
  "bin": {
6
6
  "marrow-install": "bin/marrow-install.js"
package/src/installer.js CHANGED
@@ -11,9 +11,9 @@ const { evidence: localControlEvidence } = require('./control-state');
11
11
  const DEFAULT_BASE_URL = 'https://api.getmarrow.ai';
12
12
  const MARROW_BLOCK_START = '<!-- marrow:passive-start -->';
13
13
  const MARROW_BLOCK_END = '<!-- marrow:passive-end -->';
14
- const MCP_ADAPTER_VERSION = '3.9.94';
15
- const MCP_ADAPTER_SOURCE_SHA = '9456bc63de4cc92f16726d996695516be9395f26';
16
- const MCP_ADAPTER_INTEGRITY = 'sha512-gEQejUWkcuKd1p930TLsWCSMM4rjmr/Wzz0++qa69B6hj/e64lMdT9xO+jZP8+yl4WLKgcfo0raHANllUe96/A==';
14
+ const MCP_ADAPTER_VERSION = '3.9.96';
15
+ const MCP_ADAPTER_SOURCE_SHA = '031c944936271fd6e8768ad2619ee0430b68e2c7';
16
+ const MCP_ADAPTER_INTEGRITY = 'sha512-kuBpuWaWAvusS+FbXXFw1fOEGja4T8ahj/vkmXTd/CWjz0t6teT6g9c1HfJhAK6KLzesZtcvUdZLn6pBErKP9Q==';
17
17
  const SDK_ADAPTER_VERSION = '3.7.64';
18
18
  const SDK_ADAPTER_INTEGRITY = 'sha512-8qJj/8ouHEz1NnZkmujtFxUm/fWldqR/rHv62/sqabaxT0H90xCCHxodLOkV0SVxDscAtnahioZakqkeGNUwyA==';
19
19
  const SDK_ADAPTER_TARBALL = `https://registry.npmjs.org/@getmarrow/sdk/-/sdk-${SDK_ADAPTER_VERSION}.tgz`;
@@ -2684,6 +2684,60 @@ async function requestJson(url, options) {
2684
2684
  return json.data || json;
2685
2685
  }
2686
2686
 
2687
+ const SELF_TEST_WRITE_ATTEMPTS = 3;
2688
+ const SELF_TEST_RETRY_DELAY_MS = 1_000;
2689
+ const SELF_TEST_MAX_RETRY_DELAY_MS = 2_000;
2690
+ const SELF_TEST_TRANSIENT_STATUSES = new Set([429, 502, 503, 504]);
2691
+
2692
+ function selfTestRetryDelayMs(response, data, override) {
2693
+ if (Number.isFinite(override) && override >= 0) return override;
2694
+ const hinted = Number(data?.retry_after_ms);
2695
+ const header = Number(response?.headers?.get?.('retry-after')) * 1000;
2696
+ const delay = hinted > 0 ? hinted : header > 0 ? header : SELF_TEST_RETRY_DELAY_MS;
2697
+ return Math.min(delay, SELF_TEST_MAX_RETRY_DELAY_MS);
2698
+ }
2699
+
2700
+ // A first write can meet a transient 5xx, or a durable "pending" acknowledgement
2701
+ // that has no decision id yet. Marrow's contract is to resend the identical request
2702
+ // with the same Idempotency-Key, so a retry can never create a second record.
2703
+ async function selfTestWrite(url, options, { idempotencyKey, complete, retryDelayMs }) {
2704
+ let lastState = 'no response';
2705
+ for (let attempt = 1; attempt <= SELF_TEST_WRITE_ATTEMPTS; attempt += 1) {
2706
+ const res = await fetch(url, { ...options, headers: { ...options.headers, 'idempotency-key': idempotencyKey } });
2707
+ const text = await res.text();
2708
+ let json = {};
2709
+ try {
2710
+ json = text ? JSON.parse(text) : {};
2711
+ } catch {
2712
+ json = { raw: text.slice(0, 500) };
2713
+ }
2714
+ const data = json.data || json;
2715
+ if (res.ok) {
2716
+ const result = complete(data);
2717
+ if (result !== undefined) return result;
2718
+ const pending = data?.retryable === true && data?.committed === false && data?.idempotency_key === idempotencyKey;
2719
+ if (!pending) return undefined;
2720
+ lastState = 'pending';
2721
+ } else if (SELF_TEST_TRANSIENT_STATUSES.has(res.status)) {
2722
+ const reason = String(json.error || json.message || '').slice(0, 200);
2723
+ lastState = reason ? `HTTP ${res.status}: ${reason}` : `HTTP ${res.status}`;
2724
+ } else {
2725
+ throw new Error(String(json.error || json.message || `HTTP ${res.status}`));
2726
+ }
2727
+ if (attempt < SELF_TEST_WRITE_ATTEMPTS) {
2728
+ await new Promise((resolve) => setTimeout(resolve, selfTestRetryDelayMs(res, data, retryDelayMs)));
2729
+ }
2730
+ }
2731
+ return { failed: lastState };
2732
+ }
2733
+
2734
+ function selfTestDecisionId(data) {
2735
+ const decisionId = data?.decision_id || data?.decisionId;
2736
+ if (typeof decisionId !== 'string' || !decisionId) return undefined;
2737
+ const pending = data.retryable === true && data.committed === false;
2738
+ return !pending || data.decision_state === 'created' ? decisionId : undefined;
2739
+ }
2740
+
2687
2741
  function runLoopGuardSelfTest(options = {}) {
2688
2742
  const target = executableMcpTarget(options);
2689
2743
  const isolatedHome = fs.mkdtempSync(path.join(os.tmpdir(), 'marrow-install-loop-guard-'));
@@ -2782,7 +2836,8 @@ async function runSelfTest(options) {
2782
2836
  if (options.agentId) headers['x-marrow-agent-id'] = options.agentId;
2783
2837
 
2784
2838
  const baseUrl = options.baseUrl.replace(/\/+$/, '');
2785
- const think = await requestJson(`${baseUrl}/v1/agent/think`, {
2839
+ const selfTestKey = `install-self-test:${crypto.randomUUID()}`;
2840
+ const think = await selfTestWrite(`${baseUrl}/v1/agent/think`, {
2786
2841
  method: 'POST',
2787
2842
  headers,
2788
2843
  body: JSON.stringify({
@@ -2794,12 +2849,18 @@ async function runSelfTest(options) {
2794
2849
  user_intent: 'operate',
2795
2850
  },
2796
2851
  }),
2852
+ }, {
2853
+ idempotencyKey: `${selfTestKey}:think`,
2854
+ complete: (data) => selfTestDecisionId(data),
2855
+ retryDelayMs: options.selfTestRetryDelayMs,
2797
2856
  });
2798
2857
 
2799
- const decisionId = think.decision_id || think.decisionId;
2800
- if (!decisionId) throw new Error('self-test did not return decision_id');
2858
+ const decisionId = typeof think === 'string' ? think : undefined;
2859
+ if (!decisionId) {
2860
+ throw new Error(`self-test did not return decision_id${think?.failed ? ` after ${SELF_TEST_WRITE_ATTEMPTS} attempts (last: ${think.failed})` : ''}`);
2861
+ }
2801
2862
 
2802
- await requestJson(`${baseUrl}/v1/agent/commit`, {
2863
+ const commit = await selfTestWrite(`${baseUrl}/v1/agent/commit`, {
2803
2864
  method: 'POST',
2804
2865
  headers,
2805
2866
  body: JSON.stringify({
@@ -2807,7 +2868,14 @@ async function runSelfTest(options) {
2807
2868
  success: true,
2808
2869
  outcome: 'Marrow passive installer self-test completed successfully',
2809
2870
  }),
2871
+ }, {
2872
+ idempotencyKey: `${selfTestKey}:commit`,
2873
+ complete: (data) => (data?.retryable === true && data?.committed === false ? undefined : data),
2874
+ retryDelayMs: options.selfTestRetryDelayMs,
2810
2875
  });
2876
+ if (commit?.failed) {
2877
+ throw new Error(`self-test commit did not complete after ${SELF_TEST_WRITE_ATTEMPTS} attempts (last: ${commit.failed})`);
2878
+ }
2811
2879
 
2812
2880
  const status = await requestJson(`${baseUrl}/v1/agent/status`, { headers });
2813
2881
  const context = await requestJson(`${baseUrl}/v1/agent/context`, { headers })