@getmarrow/install 0.1.50 → 0.1.52

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
@@ -91,9 +91,9 @@ npx @getmarrow/install controller stop
91
91
 
92
92
  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.
93
93
 
94
- ## What's New in v0.1.50
94
+ ## What's New in v0.1.52
95
95
 
96
- v0.1.50 keeps MCP `3.9.74` and SDK `3.7.62`, and explicitly requests the expanded runtime proof required by activation self-test. Install and repair resolve one stable non-secret agent identity before writing configuration, then use that exact identity across generated MCP config, the owned-process SDK preload, doctor/self-test, and the local controller. Managed MCP config does not store an API key or shell-style credential placeholder; the owning harness inherits the key from trusted environment or secret-manager state.
96
+ v0.1.52 keeps MCP `3.9.74` and SDK `3.7.62`, and makes generated passive runtime identity follow the current process: `MARROW_FLEET_AGENT_ID` first, then `MARROW_AGENT_ID`, with the installer-captured identity retained only as a fallback. This prevents a stale installed harness identity from overriding the current Codex, Bob, or other canonical process identity. Managed MCP config does not store an API key or shell-style credential placeholder; the owning harness inherits the key from trusted environment or secret-manager state.
97
97
 
98
98
  The integration boundary is explicit: native Claude hooks are installed only where supported and remain cooperative/client-reported until authoritative server receipts exist; MCP tools are on demand; Codex, Grok, Gemini, and similar CLIs use the governed wrapper for consequential control; owned Node processes use the SDK passive runtime while installed; and custom hosts require a bounded event adapter. Exact package SHA/integrity can prove artifact provenance, not runtime coverage. Activation-profile delivery is authenticated `client_self_reported` telemetry with `certified_coverage: false`; it acknowledges delivery but cannot attest that a hook, wrapper, or adapter ran.
99
99
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getmarrow/install",
3
- "version": "0.1.50",
3
+ "version": "0.1.52",
4
4
  "description": "Universal installer and governed runner for Marrow agent fleets.",
5
5
  "bin": {
6
6
  "marrow-install": "bin/marrow-install.js"
@@ -27,6 +27,7 @@ const {
27
27
  buildPlan,
28
28
  detectEnvironment,
29
29
  } = require('./installer');
30
+ const { createHostUsageCapture } = require('./usage-telemetry');
30
31
 
31
32
  const DEFAULT_BASE_URL = 'https://api.getmarrow.ai';
32
33
  const MCP_SETUP_COMMAND = `npx -y --package=${ADAPTER_PROVENANCE.mcp.package}@${ADAPTER_PROVENANCE.mcp.version} marrow-mcp setup`;
@@ -512,11 +513,11 @@ function dataOf(json) {
512
513
  return json && typeof json === 'object' && json.data && typeof json.data === 'object' ? json.data : json;
513
514
  }
514
515
 
515
- async function requestJson(options, method, route, body) {
516
+ async function requestJson(options, method, route, body, extraHeaders = {}) {
516
517
  if (!options.apiKey) throw new Error('MARROW_API_KEY is required. Use --fail-open only for non-production local commands.');
517
518
  const response = await fetch(new URL(route, options.baseUrl.replace(/\/$/, '/')), {
518
519
  method,
519
- headers: headers(options),
520
+ headers: { ...headers(options), ...extraHeaders },
520
521
  body: body === undefined ? undefined : JSON.stringify(body),
521
522
  });
522
523
  const text = await response.text();
@@ -674,15 +675,48 @@ function printGate(decision, runtime, stream = process.stdout) {
674
675
  if (runtime?.value_proof?.owner_summary) stream.write(`Value: ${runtime.value_proof.owner_summary}\n`);
675
676
  }
676
677
 
677
- function runChild(command, env = process.env) {
678
+ function runChild(command, env = process.env, stdout = process.stdout) {
678
679
  return new Promise((resolve) => {
680
+ const usageCapture = createHostUsageCapture(command);
679
681
  const child = spawn(command[0], command.slice(1), {
680
- stdio: 'inherit',
682
+ stdio: usageCapture.supported ? ['inherit', 'pipe', 'inherit'] : 'inherit',
681
683
  shell: false,
682
684
  env,
683
685
  });
684
- child.on('error', (error) => resolve({ exitCode: 127, error }));
685
- child.on('close', (code, signal) => resolve({ exitCode: code ?? 1, signal: signal || null }));
686
+ let closed = null;
687
+ let pendingWrites = 0;
688
+ let settled = false;
689
+ const settle = () => {
690
+ if (settled || !closed || pendingWrites > 0) return;
691
+ settled = true;
692
+ resolve({
693
+ ...closed,
694
+ usageCaptureSupported: usageCapture.supported,
695
+ modelUsage: usageCapture.finish(),
696
+ });
697
+ };
698
+ if (usageCapture.supported && child.stdout) {
699
+ child.stdout.on('data', (chunk) => {
700
+ usageCapture.write(chunk);
701
+ pendingWrites += 1;
702
+ const ready = stdout.write(chunk, () => {
703
+ pendingWrites -= 1;
704
+ settle();
705
+ });
706
+ if (!ready) {
707
+ child.stdout.pause();
708
+ stdout.once('drain', () => child.stdout?.resume());
709
+ }
710
+ });
711
+ }
712
+ child.on('error', (error) => {
713
+ closed = { exitCode: 127, error };
714
+ settle();
715
+ });
716
+ child.on('close', (code, signal) => {
717
+ closed = { exitCode: code ?? 1, signal: signal || null };
718
+ settle();
719
+ });
686
720
  });
687
721
  }
688
722
 
@@ -718,7 +752,7 @@ async function createDecision(options, action, type, target, surfaces) {
718
752
  });
719
753
  }
720
754
 
721
- async function commitOutcome(options, decisionId, success, outcome, proof, gateReceiptId) {
755
+ async function commitOutcome(options, decisionId, success, outcome, proof, gateReceiptId, modelUsage = null) {
722
756
  const body = {
723
757
  decision_id: decisionId,
724
758
  success,
@@ -727,10 +761,16 @@ async function commitOutcome(options, decisionId, success, outcome, proof, gateR
727
761
  source_meta: sourceMeta(options, 'commit', { action: outcome }),
728
762
  };
729
763
  if (gateReceiptId) body.gate_receipt_id = gateReceiptId;
730
- return requestJson(options, 'POST', '/v1/agent/commit', body);
764
+ if (modelUsage) body.model_usage = modelUsage;
765
+ const commitHeaders = modelUsage
766
+ ? { 'Idempotency-Key': `marrow-run-${crypto.createHash('sha256')
767
+ .update(`${options.agentId}\u0000${options.sessionId}\u0000${decisionId}`)
768
+ .digest('hex')}` }
769
+ : {};
770
+ return requestJson(options, 'POST', '/v1/agent/commit', body, commitHeaders);
731
771
  }
732
772
 
733
- async function runGoverned(parsed) {
773
+ async function runGoverned(parsed, execution = {}) {
734
774
  const { options, childCommand } = parsed;
735
775
  const commandText = redactedCommand(childCommand);
736
776
  const action = options.action ? redact(options.action) : commandText;
@@ -816,7 +856,7 @@ async function runGoverned(parsed) {
816
856
  }
817
857
 
818
858
  const childEnv = scopedExecutionEnv(permitVerified ? actionPermit : null);
819
- const child = await runChild(childCommand, childEnv);
859
+ const child = await runChild(childCommand, childEnv, execution.stdout || process.stdout);
820
860
  const success = child.exitCode === 0;
821
861
  const proof = defaultProof({ options, action, childCommand, exitCode: child.exitCode, success });
822
862
  const outcome = success
@@ -826,7 +866,15 @@ async function runGoverned(parsed) {
826
866
  let commit = null;
827
867
  if (decisionId) {
828
868
  try {
829
- commit = await commitOutcome(options, decisionId, success, outcome, proof, decision?.receiptId || '');
869
+ commit = await commitOutcome(
870
+ options,
871
+ decisionId,
872
+ success,
873
+ outcome,
874
+ proof,
875
+ decision?.receiptId || '',
876
+ child.modelUsage,
877
+ );
830
878
  } catch (error) {
831
879
  process.stderr.write(`Marrow outcome commit failed: ${error.message}\n`);
832
880
  }
@@ -861,6 +909,16 @@ async function runGoverned(parsed) {
861
909
  permit_id: actionPermit?.permit_id || null,
862
910
  permit_verified: permitVerified,
863
911
  permit_closed: Boolean(permitClosed),
912
+ usage_capture: {
913
+ supported: child.usageCaptureSupported === true,
914
+ observed: Boolean(child.modelUsage),
915
+ source: child.modelUsage?.source || null,
916
+ evidence: child.modelUsage
917
+ ? 'accepted_host_reported_usage'
918
+ : child.usageCaptureSupported
919
+ ? 'no_valid_host_reported_usage'
920
+ : 'unsupported_child_command',
921
+ },
864
922
  };
865
923
  }
866
924
 
package/src/installer.js CHANGED
@@ -653,7 +653,7 @@ if (apiKey && !globalThis.__MARROW_PASSIVE_RUNTIME__) {
653
653
  const { MarrowClient } = await import('@getmarrow/sdk');
654
654
  const marrow = new MarrowClient(apiKey, {
655
655
  baseUrl: installedBaseUrl,
656
- agentId: installedAgentId || process.env.MARROW_FLEET_AGENT_ID || process.env.MARROW_AGENT_ID,
656
+ agentId: process.env.MARROW_FLEET_AGENT_ID || process.env.MARROW_AGENT_ID || installedAgentId,
657
657
  sessionId: process.env.MARROW_SESSION_ID,
658
658
  mode: process.env.MARROW_ENFORCEMENT_MODE || 'auto',
659
659
  });
@@ -0,0 +1,235 @@
1
+ const path = require('node:path');
2
+ const { StringDecoder } = require('node:string_decoder');
3
+
4
+ const MAX_TOKEN_COUNT = 1_000_000_000;
5
+ const MAX_USAGE_EVENT_BYTES = 4_096;
6
+ const CODEX_USAGE_KEYS = new Set([
7
+ 'cached_input_tokens',
8
+ 'input_tokens',
9
+ 'output_tokens',
10
+ ]);
11
+
12
+ function isBoundedTokenCount(value) {
13
+ return Number.isSafeInteger(value) && value >= 0 && value <= MAX_TOKEN_COUNT;
14
+ }
15
+
16
+ function exactKeys(value, allowed) {
17
+ const keys = Object.keys(value);
18
+ return keys.length === allowed.size && keys.every((key) => allowed.has(key));
19
+ }
20
+
21
+ function normalizeCodexTurnUsage(event) {
22
+ if (!event || typeof event !== 'object' || Array.isArray(event)) return null;
23
+ if (!exactKeys(event, new Set(['type', 'usage'])) || event.type !== 'turn.completed') return null;
24
+
25
+ const usage = event.usage;
26
+ if (!usage || typeof usage !== 'object' || Array.isArray(usage)) return null;
27
+ if (!exactKeys(usage, CODEX_USAGE_KEYS)) return null;
28
+
29
+ const inputTokens = usage.input_tokens;
30
+ const outputTokens = usage.output_tokens;
31
+ const cachedTokens = usage.cached_input_tokens;
32
+ if (![inputTokens, outputTokens, cachedTokens].every(isBoundedTokenCount)) return null;
33
+ if (cachedTokens > inputTokens) return null;
34
+
35
+ const totalTokens = inputTokens + outputTokens;
36
+ if (!Number.isSafeInteger(totalTokens) || totalTokens > MAX_TOKEN_COUNT) return null;
37
+
38
+ return {
39
+ provider: 'openai',
40
+ input_tokens: inputTokens,
41
+ output_tokens: outputTokens,
42
+ cached_tokens: cachedTokens,
43
+ total_tokens: totalTokens,
44
+ source: 'codex_exec_jsonl',
45
+ marrow_intervention: 'governed_runner_usage_capture',
46
+ };
47
+ }
48
+
49
+ function isCodexJsonExecution(command) {
50
+ if (!Array.isArray(command) || command.length < 3) return false;
51
+ const executable = path.basename(String(command[0] || '')).toLowerCase().replace(/\.exe$/, '');
52
+ const execArgs = command.slice(2);
53
+ const terminatorIndex = execArgs.indexOf('--');
54
+ const optionArgs = terminatorIndex === -1 ? execArgs : execArgs.slice(0, terminatorIndex);
55
+ return executable === 'codex'
56
+ && command[1] === 'exec'
57
+ && optionArgs.includes('--json');
58
+ }
59
+
60
+ function createCompletedTypeScanner() {
61
+ let depth = 0;
62
+ let inString = false;
63
+ let escaped = false;
64
+ let role = 'other';
65
+ let token = '';
66
+ let tokenInvalid = false;
67
+ let lastSignificant = '';
68
+ let expectTypeColon = false;
69
+ let expectTypeValue = false;
70
+ let completed = false;
71
+
72
+ return {
73
+ write(character) {
74
+ if (inString) {
75
+ if (escaped) {
76
+ escaped = false;
77
+ tokenInvalid = role !== 'other';
78
+ } else if (character === '\\') {
79
+ escaped = true;
80
+ } else if (character === '"') {
81
+ inString = false;
82
+ if (role === 'root_key') {
83
+ expectTypeColon = !tokenInvalid && token === 'type';
84
+ } else if (role === 'type_value') {
85
+ if (!tokenInvalid && token === 'turn.completed') completed = true;
86
+ expectTypeValue = false;
87
+ }
88
+ role = 'other';
89
+ token = '';
90
+ tokenInvalid = false;
91
+ lastSignificant = '"';
92
+ } else if (role !== 'other') {
93
+ if (token.length < 32) token += character;
94
+ else tokenInvalid = true;
95
+ }
96
+ return;
97
+ }
98
+
99
+ if (/\s/.test(character)) return;
100
+ if (character === '"') {
101
+ role = depth === 1 && (lastSignificant === '{' || lastSignificant === ',')
102
+ ? 'root_key'
103
+ : depth === 1 && expectTypeValue
104
+ ? 'type_value'
105
+ : 'other';
106
+ token = '';
107
+ tokenInvalid = false;
108
+ inString = true;
109
+ return;
110
+ }
111
+ if (character === '{' || character === '[') {
112
+ depth += 1;
113
+ lastSignificant = character;
114
+ return;
115
+ }
116
+ if (character === '}' || character === ']') {
117
+ depth = Math.max(0, depth - 1);
118
+ lastSignificant = character;
119
+ return;
120
+ }
121
+ if (depth === 1 && expectTypeColon) {
122
+ expectTypeColon = false;
123
+ expectTypeValue = character === ':';
124
+ } else if (depth === 1 && expectTypeValue) {
125
+ expectTypeValue = false;
126
+ }
127
+ if (depth === 1 && character === ',') {
128
+ expectTypeColon = false;
129
+ expectTypeValue = false;
130
+ }
131
+ lastSignificant = character;
132
+ },
133
+ completed() {
134
+ return completed;
135
+ },
136
+ };
137
+ }
138
+
139
+ function createCodexJsonlUsageCollector() {
140
+ const decoder = new StringDecoder('utf8');
141
+ let line = '';
142
+ let discardLine = false;
143
+ let acceptedUsage = null;
144
+ let usageEventCount = 0;
145
+ let invalidUsageEvent = false;
146
+ let typeScanner = createCompletedTypeScanner();
147
+
148
+ const resetLine = () => {
149
+ line = '';
150
+ discardLine = false;
151
+ typeScanner = createCompletedTypeScanner();
152
+ };
153
+
154
+ const finishLine = () => {
155
+ if (discardLine || !line.trim()) {
156
+ if (discardLine && typeScanner.completed()) invalidUsageEvent = true;
157
+ resetLine();
158
+ return;
159
+ }
160
+ let parsed = null;
161
+ try {
162
+ parsed = JSON.parse(line);
163
+ } catch {
164
+ if (typeScanner.completed()) invalidUsageEvent = true;
165
+ resetLine();
166
+ return;
167
+ }
168
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed) || parsed.type !== 'turn.completed') {
169
+ resetLine();
170
+ return;
171
+ }
172
+
173
+ usageEventCount += 1;
174
+ const normalized = normalizeCodexTurnUsage(parsed);
175
+ if (!normalized || usageEventCount !== 1) invalidUsageEvent = true;
176
+ else acceptedUsage = normalized;
177
+ resetLine();
178
+ };
179
+
180
+ const consume = (text) => {
181
+ for (const character of text) {
182
+ if (character === '\n') {
183
+ finishLine();
184
+ continue;
185
+ }
186
+ typeScanner.write(character);
187
+ if (discardLine) continue;
188
+ if (line.length >= MAX_USAGE_EVENT_BYTES) {
189
+ discardLine = true;
190
+ line = '';
191
+ continue;
192
+ }
193
+ line += character;
194
+ }
195
+ };
196
+
197
+ return {
198
+ write(chunk) {
199
+ consume(decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
200
+ },
201
+ finish() {
202
+ consume(decoder.end());
203
+ if (line || discardLine) finishLine();
204
+ return invalidUsageEvent || usageEventCount !== 1 ? null : acceptedUsage;
205
+ },
206
+ bufferedBytes() {
207
+ return Buffer.byteLength(line, 'utf8');
208
+ },
209
+ };
210
+ }
211
+
212
+ function createHostUsageCapture(command) {
213
+ if (!isCodexJsonExecution(command)) {
214
+ return {
215
+ supported: false,
216
+ write() {},
217
+ finish() { return null; },
218
+ };
219
+ }
220
+ const collector = createCodexJsonlUsageCollector();
221
+ return {
222
+ supported: true,
223
+ write: (chunk) => collector.write(chunk),
224
+ finish: () => collector.finish(),
225
+ };
226
+ }
227
+
228
+ module.exports = {
229
+ MAX_TOKEN_COUNT,
230
+ MAX_USAGE_EVENT_BYTES,
231
+ createCodexJsonlUsageCollector,
232
+ createHostUsageCapture,
233
+ isCodexJsonExecution,
234
+ normalizeCodexTurnUsage,
235
+ };