@getmarrow/install 0.1.35 → 0.1.37

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.
@@ -5,9 +5,84 @@ const os = require('node:os');
5
5
  const path = require('node:path');
6
6
  const readline = require('node:readline');
7
7
  const { version: INSTALLER_PACKAGE_VERSION } = require('../package.json');
8
+ const {
9
+ actionBinding,
10
+ closeActionPermit,
11
+ issueActionPermit,
12
+ readEnforcementCoverage,
13
+ recordEnforcementHeartbeat,
14
+ verifyActionPermit,
15
+ } = require('./enforcement-client');
16
+ const { startGovernanceSidecar } = require('./governance-sidecar');
17
+ const {
18
+ controllerStatus,
19
+ ensureGovernanceController,
20
+ startGovernanceController,
21
+ stopGovernanceController,
22
+ } = require('./controller-manager');
23
+ const {
24
+ HARNESS_CAPABILITY_REGISTRY,
25
+ applyPlan,
26
+ buildPlan,
27
+ detectEnvironment,
28
+ } = require('./installer');
8
29
 
9
30
  const DEFAULT_BASE_URL = 'https://api.getmarrow.ai';
10
31
  const HIGH_RISK_TERMS = /\b(deploy|prod|production|publish|release|merge|migration|migrate|secret|token|key|cloudflare|wrangler|npm publish|gh pr merge|git push|terraform apply|kubectl apply|delete|destroy|drop)\b/i;
32
+ const PROTECTED_COMMAND_PATTERNS = [
33
+ /\b(?:npm|pnpm|yarn)(?:\s+npm)?\b[\s\S]{0,8192}\b(?:publish|unpublish|deprecate|access|owner|team|token|login|logout|profile\s+(?:set|enable-2fa|disable-2fa)|dist-tag|tag\s+(?:add|remove))\b/i,
34
+ /\b(?:cargo\s+(?:publish|yank|owner)|twine\s+upload|gem\s+(?:push|yank|owner)|(?:dotnet\s+nuget|nuget)\s+(?:push|delete))\b/i,
35
+ /\bgit\b[\s\S]{0,8192}\b(?:push|commit|merge|rebase|reset|tag|clean|rm|cherry-pick|revert|worktree\s+(?:add|move|remove|prune|repair|lock|unlock)|branch\s+(?:-[dDmM]|--delete|--move)|remote\s+(?:add|remove|rename|set-url|set-head|prune|update)|checkout\s+-[bB]|switch\s+-[cC])\b/i,
36
+ /\bgh\b[\s\S]{0,8192}\b(?:auth\s+logout|pr\s+(?:merge|close|reopen|edit|review|comment)|issue\s+(?:create|close|reopen|edit|comment)|run\s+(?:cancel|delete|rerun)|release\s+(?:create|delete|edit|upload)|repo\s+(?:archive|delete|edit|fork|rename)|workflow\s+run|secret\s+(?:set|delete)|variable\s+(?:set|delete))\b/i,
37
+ /\bgh\s+api\b[\s\S]{0,8192}(?:(?:--method|-X)(?:=|\s+)(?:POST|PUT|PATCH|DELETE)\b|(?:-f|-F|--field|--raw-field|--input)(?:=|\s+))/i,
38
+ /\b(?:kubectl|oc)\b[\s\S]{0,8192}\b(?:apply|create|delete|edit|patch|replace|rollout|scale|set|drain|cordon|uncordon|taint|exec|cp|run|expose|autoscale|label|annotate|reconcile|certificate\s+(?:approve|deny))\b/i,
39
+ /\b(?:terraform|terragrunt|tofu)\b[\s\S]{0,8192}\b(?:apply|destroy|import|taint|untaint|force-unlock|state\s+(?:mv|rm|push|replace-provider)|workspace\s+(?:new|delete))\b/i,
40
+ /\bpulumi\b[\s\S]{0,8192}\b(?:up|destroy|import|refresh|stack\s+rm|config\s+(?:set|rm))\b/i,
41
+ /\bhelm\b[\s\S]{0,8192}\b(?:install|upgrade|uninstall|rollback|push)\b/i,
42
+ /\bflux\b[\s\S]{0,8192}\b(?:bootstrap|create|delete|install|reconcile|resume|suspend|tag|uninstall)\b/i,
43
+ /\bnomad\b[\s\S]{0,8192}\b(?:job\s+(?:dispatch|plan|promote|run|scale|stop)|alloc\s+stop|deployment\s+(?:fail|promote)|acl\s+(?:bootstrap|policy|role|token))\b/i,
44
+ /\bcdk\b[\s\S]{0,8192}\b(?:bootstrap|deploy|destroy|import|rollback)\b/i,
45
+ /\bansible-playbook\b/i,
46
+ /\b(?:docker|podman)\b[\s\S]{0,8192}\b(?:push|buildx\s+build\b[\s\S]*--push)\b/i,
47
+ /\bwrangler\b[\s\S]{0,8192}\b(?:deploy|delete|rollback|execute|apply|put|bulk|secret|publish)\b/i,
48
+ /\bcurl\b[\s\S]{0,8192}(?:(?:-X\s*|--request(?:=|\s+))(?:POST|PUT|PATCH|DELETE)\b|--(?:json|data(?:-ascii|-raw|-binary|-urlencode)?)(?:=|\s+)|-[dF](?:\s+|[^A-Za-z])|--form(?:-string)?(?:=|\s+)|(?:-T|--upload-file)(?:=|\s+))/i,
49
+ /\b(?:http|xh)\b[\s\S]{0,8192}(?:\b(?:POST|PUT|PATCH|DELETE)\b|(?:--form|--raw|-f)\b|\s[^\s=:@]+(?::=|=|@))/i,
50
+ /\bwget\b[\s\S]{0,8192}(?:--post-data|--post-file|--body-data|--body-file|--method(?:=|\s+)(?:POST|PUT|PATCH|DELETE))\b/i,
51
+ /\b(?:psql|mysql|sqlite3|duckdb)\b[\s\S]{0,8192}(?:\b(?:drop|delete|update|insert|alter|truncate|create|grant|revoke|call|do)\b|(?:-f|--file|\.read|source)(?:=|\s+)|\s<\s*[^\s])/i,
52
+ /\bredis-cli\b[\s\S]{0,8192}\b(?:set|setex|psetex|mset|del|unlink|getdel|incr|decr|append|expire|persist|rename|move|flushall|flushdb|shutdown|eval|evalsha|fcall|fcall_ro|function|script\s+(?:load|flush|kill)|config\s+set|acl\s+setuser|hset|hdel|lpush|rpush|lpop|rpop|sadd|srem|zadd|zrem|xadd|xdel|publish|restore|migrate)\b/i,
53
+ /\baws\b[\s\S]{0,8192}\b(?:create|update|delete|put|attach|detach|associate|disassociate|terminate|stop|start|reboot|modify|restore|rotate|tag|untag|deploy|sync|s3\s+(?:cp|mv|rm)|s3api\s+put-object|ssm\s+(?:put-parameter|delete-parameter|delete-parameters))\b/i,
54
+ /\bgcloud\b[\s\S]{0,8192}\b(?:create|update|delete|deploy|add|remove|set|destroy|disable|restore|storage\s+(?:cp|mv|rm|rsync)|pubsub\s+(?:topics|subscriptions)\s+(?:create|delete|update))\b/i,
55
+ /\baz\b[\s\S]{0,8192}\b(?:create|update|delete|set|deploy|start|stop|restart|restore|storage\s+blob\s+(?:upload|delete|copy)|group\s+(?:create|delete|update))\b/i,
56
+ /\brclone\b[\s\S]{0,8192}\b(?:copy|copyto|sync|move|moveto|delete|deletefile|purge|mkdir|rmdir|bisync)\b/i,
57
+ /\bgsutil\b[\s\S]{0,8192}\b(?:cp|mv|rm|rsync|setacl|setmeta|web)\b/i,
58
+ /(?:^|[;&|]\s*|\bsudo\s+|\benv\s+)mc\b[\s\S]{0,8192}\b(?:cp|mv|rm|mirror|mb|rb|anonymous|admin)\b/i,
59
+ /\boci\b[\s\S]{0,8192}\bos\b[\s\S]{0,8192}\b(?:put|upload|bulk-upload|delete|rename|restore|reencrypt)\b/i,
60
+ /\b(?:vault|op)\b[\s\S]{0,8192}\b(?:write|put|patch|delete|edit|create|move|rotate|revoke|destroy|share)\b/i,
61
+ /\bpass\b[\s\S]{0,8192}\b(?:insert|edit|generate|rm|remove|mv|cp|init|git)\b/i,
62
+ /(?:^|[\s;&|]|\bsudo\s+|\benv\s+)(?:(?:\/[^\s/]+)*\/)?(?:rm\b|unlink\b|shred\b|truncate\b|dd\b[\s\S]{0,8192}\bof=|find\b[\s\S]{0,8192}\s-delete\b|xargs\b[\s\S]{0,8192}(?:(?:\/[^\s/]+)*\/)?rm\b)/i,
63
+ ];
64
+ const PROTECTED_ACTION_TYPES = new Set([
65
+ 'credential',
66
+ 'credentials',
67
+ 'deploy',
68
+ 'financial',
69
+ 'merge',
70
+ 'migration',
71
+ 'production',
72
+ 'publish',
73
+ 'release',
74
+ 'secret',
75
+ 'security',
76
+ ]);
77
+ const SAFE_MARROW_CHILD_METADATA = new Set([
78
+ 'MARROW_AGENT_ID',
79
+ 'MARROW_AGENT_CLIENT',
80
+ 'MARROW_CLIENT',
81
+ 'MARROW_FLEET_AGENT_ID',
82
+ 'MARROW_GOVERN_PROFILE',
83
+ 'MARROW_HARNESS',
84
+ 'MARROW_SESSION_ID',
85
+ ]);
11
86
  const GOVERN_TUI_ROW_COUNT = 7;
12
87
  const FLEET_TUI_ROW_COUNT = 12;
13
88
  function usage() {
@@ -17,6 +92,10 @@ function usage() {
17
92
  npx @getmarrow/install gate "deploy production worker"
18
93
  npx @getmarrow/install proof --decision-id <id> --success --summary "smoke passed"
19
94
  npx @getmarrow/install status
95
+ npx @getmarrow/install permit --action "deploy production" --type deploy
96
+ MARROW_ACTION_PERMIT=... npx @getmarrow/install verify-permit --action "deploy production" --type deploy
97
+ npx @getmarrow/install coverage
98
+ npx @getmarrow/install sidecar
20
99
  npx @getmarrow/install govern
21
100
  npx @getmarrow/install govern --no-interactive
22
101
  npx @getmarrow/install fleet
@@ -29,6 +108,12 @@ Commands:
29
108
  gate Check Marrow runtime/gate for an action without running a command
30
109
  proof Commit an outcome/proof for an existing decision
31
110
  status Read /v1/agent/status
111
+ permit Issue a short-lived action-bound permit after Marrow policy evaluation
112
+ verify-permit Verify a permit before CI, deploy, publish, merge, migration, or credential access
113
+ coverage Show enforcement, hook-health, closure, and bypass coverage
114
+ sidecar Run the loopback-only Marrow governance sidecar
115
+ controller <ensure|start|status|stop>
116
+ Keep the loopback governance controller active across agent sessions
32
117
  govern Interactive setup TUI when run in a terminal; text panel in CI/non-TTY
33
118
  fleet Fleet operator TUI for live agents, workflows, gates, proof debt, and exact fixes
34
119
  integrations List Marrow-supported harness add-ons
@@ -42,9 +127,12 @@ Options:
42
127
  --action <text> Human-readable action. Defaults to the redacted command
43
128
  --profile <name> Policy profile label, such as dev, staging, or production
44
129
  --policy <mode> enforce, warn, or audit. Default: enforce
45
- --fail-open If Marrow is unreachable, run anyway and mark telemetry degraded
130
+ --fail-open For non-protected, low-risk actions only, run if Marrow is unreachable
46
131
  --fail-closed If Marrow is unreachable, block the command
47
132
  --owner-approved <ref> Owner approval reference for review-required gates
133
+ --permit <token> Short-lived action permit. Prefer MARROW_ACTION_PERMIT
134
+ --target <text> Protected target binding, such as repository/environment
135
+ --sidecar-port <port> Loopback sidecar port. Default: ephemeral
48
136
  --proof-file <path> JSON proof to include on outcome commit
49
137
  --client <label> Harness/client label. Defaults to MARROW_CLIENT, MARROW_HARNESS, or MARROW_AGENT_CLIENT
50
138
  --base-url <url> Marrow API base URL
@@ -79,6 +167,14 @@ function redactedCommand(command) {
79
167
  return command.map((part) => shellQuote(redact(part))).join(' ');
80
168
  }
81
169
 
170
+ function isProtectedCommand(text) {
171
+ const raw = String(text || '');
172
+ if (raw.length > 8192) return true;
173
+ const value = raw.slice(0, 8192);
174
+ return HIGH_RISK_TERMS.test(value)
175
+ || PROTECTED_COMMAND_PATTERNS.some((pattern) => pattern.test(value));
176
+ }
177
+
82
178
  function normalizeClientLabel(value) {
83
179
  const raw = String(value || '').trim().toLowerCase();
84
180
  if (!raw) return '';
@@ -129,9 +225,11 @@ function shellQuoteDisplay(value) {
129
225
 
130
226
  function inferType(text) {
131
227
  const value = String(text || '').toLowerCase();
132
- if (/\b(deploy|wrangler|cloudflare|production|prod|release)\b/.test(value)) return 'deploy';
133
- if (/\b(publish|npm publish)\b/.test(value)) return 'publish';
134
- if (/\b(merge|gh pr merge)\b/.test(value)) return 'merge';
228
+ if (/\b(deploy|wrangler|cloudflare|production|prod|release)\b/.test(value)
229
+ || /\b(?:kubectl|terraform|pulumi|helm)\b/.test(value) && isProtectedCommand(value)) return 'deploy';
230
+ if (/\b(publish|unpublish|deprecate|npm publish)\b/.test(value)) return 'publish';
231
+ if (/\b(merge|gh pr merge)\b/.test(value)
232
+ || /\bgit\b[^\n;&|]{0,240}\bpush\b/.test(value)) return 'merge';
135
233
  if (/\b(migration|migrate|schema|d1 execute|drop table)\b/.test(value)) return 'migration';
136
234
  if (/\b(secret|token|key|password)\b/.test(value)) return 'security';
137
235
  if (/\b(test|check|lint|typecheck|smoke)\b/.test(value)) return 'verification';
@@ -143,6 +241,7 @@ function inferSurfaces(text) {
143
241
  const surfaces = new Set();
144
242
  if (/\b(git|gh|github)\b/.test(value)) surfaces.add('github');
145
243
  if (/\b(wrangler|cloudflare|worker|d1|r2)\b/.test(value)) surfaces.add('cloudflare');
244
+ if (/\b(kubectl|terraform|pulumi|helm|production|prod)\b/.test(value)) surfaces.add('production');
146
245
  if (/\b(npm|pnpm|yarn|publish)\b/.test(value)) surfaces.add('npm');
147
246
  if (/\b(sql|d1|migration|database|db)\b/.test(value)) surfaces.add('database');
148
247
  if (/\b(curl|api|http)\b/.test(value)) surfaces.add('api');
@@ -232,7 +331,8 @@ function detectProjectSignals(cwd = process.cwd()) {
232
331
  }
233
332
 
234
333
  function isRisky(text, type) {
235
- return HIGH_RISK_TERMS.test(`${type || ''} ${text || ''}`);
334
+ return PROTECTED_ACTION_TYPES.has(String(type || '').trim().toLowerCase())
335
+ || isProtectedCommand(`${type || ''} ${text || ''}`);
236
336
  }
237
337
 
238
338
  function parseBaseOptions(argv, startIndex = 0) {
@@ -252,6 +352,9 @@ function parseBaseOptions(argv, startIndex = 0) {
252
352
  action: '',
253
353
  client: sourceClient(),
254
354
  interactive: null,
355
+ permit: process.env.MARROW_ACTION_PERMIT || '',
356
+ target: process.env.MARROW_ACTION_TARGET || '',
357
+ sidecarPort: process.env.MARROW_SIDECAR_PORT || '0',
255
358
  };
256
359
  let i = startIndex;
257
360
  for (; i < argv.length; i += 1) {
@@ -270,6 +373,9 @@ function parseBaseOptions(argv, startIndex = 0) {
270
373
  options.failClosed = true;
271
374
  options.failOpen = false;
272
375
  } else if (arg === '--owner-approved') options.ownerApproval = argv[++i] || options.ownerApproval;
376
+ else if (arg === '--permit') options.permit = argv[++i] || options.permit;
377
+ else if (arg === '--target') options.target = argv[++i] || options.target;
378
+ else if (arg === '--sidecar-port') options.sidecarPort = argv[++i] || options.sidecarPort;
273
379
  else if (arg === '--proof-file') options.proofFile = argv[++i] || options.proofFile;
274
380
  else if (arg === '--client' || arg === '--harness') options.client = sourceClient(argv[++i] || options.client);
275
381
  else if (arg === '--base-url') options.baseUrl = argv[++i] || options.baseUrl;
@@ -311,6 +417,17 @@ function parseArgs(argv) {
311
417
  return { command, options: { ...parsed.options, action } };
312
418
  }
313
419
 
420
+ if (command === 'permit' || command === 'verify-permit') {
421
+ const parsed = parseBaseOptions(argv, 1);
422
+ const action = parsed.options.action || argv.slice(parsed.index).join(' ');
423
+ if (parsed.options.help) return { command: 'help' };
424
+ if (!action) throw new Error(`${command} requires --action or an action string`);
425
+ if (command === 'verify-permit' && !parsed.options.permit) {
426
+ throw new Error('verify-permit requires MARROW_ACTION_PERMIT or --permit');
427
+ }
428
+ return { command, options: { ...parsed.options, action } };
429
+ }
430
+
314
431
  if (command === 'proof') {
315
432
  const parsed = parseBaseOptions(argv, 1);
316
433
  const options = { ...parsed.options, decisionId: '', success: true, summary: '', outcome: '' };
@@ -328,7 +445,17 @@ function parseArgs(argv) {
328
445
  return { command, options };
329
446
  }
330
447
 
331
- if (command === 'status' || command === 'govern' || command === 'fleet' || command === 'hermes' || command === 'openclaw' || command === 'integrations') {
448
+ if (command === 'controller') {
449
+ const action = argv[1] && !argv[1].startsWith('--') ? argv[1] : 'status';
450
+ if (!['ensure', 'start', 'status', 'stop'].includes(action)) {
451
+ throw new Error('controller action must be ensure, start, status, or stop');
452
+ }
453
+ const parsed = parseBaseOptions(argv, action === 'status' && argv[1] !== 'status' ? 1 : 2);
454
+ if (parsed.options.help) return { command: 'help' };
455
+ return { command, action, options: parsed.options };
456
+ }
457
+
458
+ if (command === 'status' || command === 'govern' || command === 'fleet' || command === 'hermes' || command === 'openclaw' || command === 'integrations' || command === 'coverage' || command === 'sidecar') {
332
459
  const parsed = parseBaseOptions(argv, 1);
333
460
  if (parsed.options.help) return { command: 'help' };
334
461
  return { command, options: parsed.options };
@@ -414,11 +541,14 @@ function defaultProof(input) {
414
541
  }
415
542
 
416
543
  async function preflightRuntime(options, action, type, commandText) {
544
+ const target = options.target || commandText || action;
545
+ const surfaces = inferSurfaces(commandText || action);
417
546
  const meta = sourceMeta(options, 'runtime', { action, command: commandText, action_type: type });
418
547
  return requestJson(options, 'POST', '/v1/agent/runtime', {
419
548
  action,
420
549
  type,
421
- surfaces: inferSurfaces(commandText || action),
550
+ target,
551
+ surfaces,
422
552
  source_meta: meta,
423
553
  context: {
424
554
  runner: '@getmarrow/install run',
@@ -484,6 +614,7 @@ function gateDecision(runtime) {
484
614
  return {
485
615
  decision: gate.enforcement_decision || receipt.decision || gate.decision || 'unknown',
486
616
  allow: gate.allow !== false,
617
+ riskLevel: String(gate.risk_level || receipt.risk_level || runtime?.risk_level || '').trim().toLowerCase(),
487
618
  required: Boolean(receipt.required || gate.gate_required),
488
619
  ownerApprovalRequired: Boolean(receipt.owner_approval_required || gate.owner_approval_required),
489
620
  receiptId: receipt.id || gate.gate_receipt_id || '',
@@ -525,11 +656,28 @@ function runChild(command, env = process.env) {
525
656
  });
526
657
  }
527
658
 
528
- async function createDecision(options, action, type) {
659
+ function scopedExecutionEnv(permit) {
660
+ const env = {};
661
+ for (const [name, value] of Object.entries(process.env)) {
662
+ if (name.startsWith('MARROW_') && !SAFE_MARROW_CHILD_METADATA.has(name)) continue;
663
+ if (name.startsWith('ACTION_PERMIT_')) continue;
664
+ env[name] = value;
665
+ }
666
+ if (permit?.permit) {
667
+ env.MARROW_ACTION_PERMIT = permit.permit;
668
+ env.MARROW_ACTION_PERMIT_ID = String(permit.permit_id || '');
669
+ env.MARROW_GOVERNANCE_VERIFIED = 'true';
670
+ }
671
+ return env;
672
+ }
673
+
674
+ async function createDecision(options, action, type, target, surfaces) {
529
675
  const meta = sourceMeta(options, 'think', { action, action_type: type });
530
676
  return requestJson(options, 'POST', '/v1/agent/think', {
531
677
  action,
532
678
  type,
679
+ target,
680
+ surfaces,
533
681
  source_meta: meta,
534
682
  context: {
535
683
  runner: '@getmarrow/install run',
@@ -556,15 +704,25 @@ async function runGoverned(parsed) {
556
704
  const { options, childCommand } = parsed;
557
705
  const commandText = redactedCommand(childCommand);
558
706
  const action = options.action ? redact(options.action) : commandText;
559
- const type = options.type || inferType(`${action} ${commandText}`);
560
- const risky = isRisky(`${action} ${commandText}`, type);
707
+ const riskText = `${action} ${commandText} ${options.target || ''}`;
708
+ const type = options.type || inferType(riskText);
709
+ const risky = isRisky(riskText, type);
561
710
  let runtime = null;
562
711
  let decision = null;
563
712
  let decisionId = '';
713
+ let actionPermit = null;
714
+ let permitVerified = false;
715
+ let degraded = false;
716
+ let protectedAction = risky;
717
+ const surfaces = inferSurfaces(commandText || action);
564
718
 
565
719
  try {
566
720
  runtime = await preflightRuntime(options, action, type, commandText);
567
721
  decision = gateDecision(runtime);
722
+ protectedAction = risky
723
+ || decision?.required === true
724
+ || decision?.riskLevel === 'high'
725
+ || decision?.riskLevel === 'critical';
568
726
  printGate(decision, runtime);
569
727
  if (shouldBlock(decision, options)) {
570
728
  return {
@@ -578,10 +736,40 @@ async function runGoverned(parsed) {
578
736
  message: decision.exactNextAction || 'Marrow blocked this action before execution.',
579
737
  };
580
738
  }
581
- const think = await createDecision(options, action, type);
739
+ const target = options.target || commandText;
740
+ const think = await createDecision(options, action, type, target, surfaces);
582
741
  decisionId = think.decision_id || think.id || think.decision?.id || '';
742
+ if (protectedAction) {
743
+ actionPermit = await issueActionPermit(requestJson, options, {
744
+ action,
745
+ type,
746
+ target,
747
+ surfaces,
748
+ decisionId,
749
+ gateReceiptId: decision?.receiptId || '',
750
+ ownerApproval: options.ownerApproval,
751
+ proofRequirements: decision?.proofPack?.required_fields || decision?.proofPack?.missing || [],
752
+ });
753
+ if (!actionPermit?.permit || !actionPermit?.permit_id) {
754
+ throw new Error('Marrow did not issue a valid action permit.');
755
+ }
756
+ const verified = await verifyActionPermit(requestJson, options, {
757
+ action,
758
+ type,
759
+ target,
760
+ surfaces,
761
+ permit: actionPermit.permit,
762
+ });
763
+ if (verified?.verified !== true) throw new Error('Marrow action permit verification failed.');
764
+ permitVerified = true;
765
+ }
583
766
  } catch (error) {
584
- if (options.failOpen || (!risky && !options.failClosed)) {
767
+ const canDegrade = !protectedAction
768
+ && (options.failOpen || !options.failClosed);
769
+ if (canDegrade) {
770
+ actionPermit = null;
771
+ permitVerified = false;
772
+ degraded = true;
585
773
  process.stderr.write(`Marrow degraded: ${error.message}. Continuing because fail-open/non-risky policy allows it.\n`);
586
774
  } else {
587
775
  return {
@@ -597,7 +785,8 @@ async function runGoverned(parsed) {
597
785
  }
598
786
  }
599
787
 
600
- const child = await runChild(childCommand);
788
+ const childEnv = scopedExecutionEnv(permitVerified ? actionPermit : null);
789
+ const child = await runChild(childCommand, childEnv);
601
790
  const success = child.exitCode === 0;
602
791
  const proof = defaultProof({ options, action, childCommand, exitCode: child.exitCode, success });
603
792
  const outcome = success
@@ -613,6 +802,21 @@ async function runGoverned(parsed) {
613
802
  }
614
803
  }
615
804
 
805
+ let permitClosed = null;
806
+ if (actionPermit?.permit) {
807
+ try {
808
+ permitClosed = await closeActionPermit(requestJson, options, {
809
+ permit: actionPermit.permit,
810
+ permitId: actionPermit.permit_id,
811
+ decisionId,
812
+ success,
813
+ evidence: proof,
814
+ });
815
+ } catch (error) {
816
+ process.stderr.write(`Marrow permit close failed: ${error.message}\n`);
817
+ }
818
+ }
819
+
616
820
  return {
617
821
  ok: success,
618
822
  blocked: false,
@@ -620,10 +824,110 @@ async function runGoverned(parsed) {
620
824
  action,
621
825
  type,
622
826
  risky,
827
+ degraded,
623
828
  decision,
624
829
  decision_id: decisionId,
625
830
  outcome_committed: Boolean(commit),
831
+ permit_id: actionPermit?.permit_id || null,
832
+ permit_verified: permitVerified,
833
+ permit_closed: Boolean(permitClosed),
834
+ };
835
+ }
836
+
837
+ async function permitOnly(parsed) {
838
+ const { options } = parsed;
839
+ const action = redact(options.action);
840
+ const type = options.type || inferType(action);
841
+ const target = options.target || action;
842
+ const surfaces = inferSurfaces(target);
843
+ const runtime = await preflightRuntime(options, action, type, target);
844
+ const decision = gateDecision(runtime);
845
+ if (shouldBlock(decision, options)) {
846
+ return { ok: false, blocked: true, exitCode: 12, decision, message: decision.exactNextAction };
847
+ }
848
+ const think = await createDecision(options, action, type, target, surfaces);
849
+ const decisionId = think.decision_id || think.id || think.decision?.id || '';
850
+ const result = await issueActionPermit(requestJson, options, {
851
+ action,
852
+ type,
853
+ target,
854
+ surfaces,
855
+ decisionId,
856
+ gateReceiptId: decision.receiptId,
857
+ ownerApproval: options.ownerApproval,
858
+ proofRequirements: decision?.proofPack?.required_fields || decision?.proofPack?.missing || [],
859
+ });
860
+ return { ok: true, decision_id: decisionId, ...result };
861
+ }
862
+
863
+ async function verifyPermitOnly(parsed) {
864
+ const { options } = parsed;
865
+ const action = redact(options.action);
866
+ const type = options.type || inferType(action);
867
+ const surfaces = inferSurfaces(options.target || action);
868
+ const result = await verifyActionPermit(requestJson, options, {
869
+ action,
870
+ type,
871
+ target: options.target || action,
872
+ surfaces,
873
+ permit: options.permit,
874
+ });
875
+ const verified = result?.verified === true;
876
+ return { ...result, ok: verified, exitCode: verified ? 0 : 14 };
877
+ }
878
+
879
+ async function coverageOnly(parsed) {
880
+ return readEnforcementCoverage(requestJson, parsed.options);
881
+ }
882
+
883
+ async function sidecarOnly(parsed) {
884
+ const options = parsed.options;
885
+ const root = path.resolve(process.env.MARROW_CONTROLLER_PROJECT_ROOT || process.cwd());
886
+ const managedMode = process.env.MARROW_CONTROLLER_MANAGED_MODE || 'auto';
887
+ let lastMaintenanceAt = 0;
888
+ let lastMaintenance = null;
889
+ const maintain = async () => {
890
+ if (lastMaintenance && Date.now() - lastMaintenanceAt < 5 * 60_000) return lastMaintenance;
891
+ const detection = detectEnvironment(root, process.env);
892
+ const plan = buildPlan(detection, { mode: managedMode });
893
+ const changes = applyPlan(plan, { yes: true, dryRun: false, doctor: false });
894
+ const repaired = changes.filter((change) => change.applied).map((change) => change.label);
895
+ const remaining = changes.filter((change) => change.changed && !change.applied).map((change) => change.label);
896
+ lastMaintenanceAt = Date.now();
897
+ lastMaintenance = {
898
+ state: remaining.length > 0 ? 'attention_required' : repaired.length > 0 ? 'repaired' : 'clear',
899
+ checked_at: new Date(lastMaintenanceAt).toISOString(),
900
+ repaired,
901
+ exact_fix: remaining.length > 0 ? 'Run npx @getmarrow/install --repair in the managed project.' : null,
902
+ };
903
+ return lastMaintenance;
626
904
  };
905
+ const sidecar = await startGovernanceSidecar(options, {
906
+ permit: (input) => issueActionPermit(requestJson, options, input),
907
+ verify: (input) => verifyActionPermit(requestJson, options, input),
908
+ close: (input) => closeActionPermit(requestJson, options, input),
909
+ coverage: () => readEnforcementCoverage(requestJson, options),
910
+ heartbeat: (input) => recordEnforcementHeartbeat(requestJson, options, input),
911
+ maintain,
912
+ });
913
+ process.stdout.write(`Marrow governance sidecar active on 127.0.0.1:${sidecar.port}. Press Ctrl+C to stop.\n`);
914
+ await new Promise((resolve) => sidecar.server.once('close', resolve));
915
+ return { ok: true };
916
+ }
917
+
918
+ async function controllerOnly(parsed) {
919
+ const options = { ...parsed.options, root: process.cwd(), mode: 'auto' };
920
+ let result;
921
+ if (parsed.action === 'ensure') result = await ensureGovernanceController(options);
922
+ else if (parsed.action === 'start') result = await startGovernanceController(options);
923
+ else if (parsed.action === 'stop') result = await stopGovernanceController(options);
924
+ else result = await controllerStatus(options);
925
+ if (!options.json) {
926
+ process.stdout.write(`Marrow controller: ${result.active ? 'active' : result.state}.\n`);
927
+ if (result.started_at) process.stdout.write(`Started: ${result.started_at}\n`);
928
+ if (result.exact_fix) process.stdout.write(`Next: ${result.exact_fix}\n`);
929
+ }
930
+ return { ok: result.active || parsed.action === 'stop', controller: result };
627
931
  }
628
932
 
629
933
  async function gateOnly(parsed) {
@@ -1056,7 +1360,7 @@ function fleetPanel(snapshot) {
1056
1360
  const GENERIC_GOVERNED_COMMAND = 'MARROW_API_KEY=mrw_live_xxx npx @getmarrow/install run --agent <agent-id> --profile production --policy warn -- <harness-command>';
1057
1361
 
1058
1362
  function localSupportedHarnesses() {
1059
- return [
1363
+ const harnesses = [
1060
1364
  { display_name: 'OpenAI Codex', client_label: 'codex', category: 'agent_harness', support_level: 'governed_runner', install_command: 'MARROW_API_KEY=mrw_live_xxx npx @getmarrow/install run --agent codex-prod -- codex' },
1061
1365
  { display_name: 'Claude Code', client_label: 'claude-code', category: 'agent_harness', support_level: 'native_mcp_or_sdk', install_command: 'MARROW_API_KEY=mrw_live_xxx npx @getmarrow/mcp setup' },
1062
1366
  { display_name: 'Cursor', client_label: 'cursor', category: 'ide_agent', support_level: 'native_mcp_or_sdk', install_command: 'MARROW_API_KEY=mrw_live_xxx npx @getmarrow/mcp setup' },
@@ -1077,6 +1381,63 @@ function localSupportedHarnesses() {
1077
1381
  { display_name: 'CI scripts and deploy runners', client_label: 'ci', category: 'ci_runner', support_level: 'governed_runner', install_command: 'MARROW_API_KEY=mrw_live_xxx npx @getmarrow/install run --agent ci-release --profile production --policy enforce -- <ci-or-deploy-command>' },
1078
1382
  { display_name: 'Custom shell/API harness', client_label: 'custom', category: 'custom_runner', support_level: 'event_contract', install_command: 'POST /v1/agent/integrations/events with harness, event_type, agent_id, and action' },
1079
1383
  ];
1384
+ return harnesses.map((harness) => {
1385
+ const capability = HARNESS_CAPABILITY_REGISTRY.find((entry) => entry.client === harness.client_label)
1386
+ || HARNESS_CAPABILITY_REGISTRY.find((entry) => entry.client === 'custom');
1387
+ return { ...harness, capability_level: capability.capability_level };
1388
+ });
1389
+ }
1390
+
1391
+ function integrationCoverageMatrix() {
1392
+ return HARNESS_CAPABILITY_REGISTRY.map((entry) => {
1393
+ const native = entry.capability_level === 'native_hooks';
1394
+ const wrapper = entry.capability_level === 'governed_wrapper';
1395
+ const routed = entry.capability_level === 'mcp';
1396
+ const sdk = entry.capability_level === 'sdk_passive_runtime';
1397
+ return {
1398
+ harness: entry.client,
1399
+ capability_level: entry.capability_level,
1400
+ install_surface: entry.install_surface,
1401
+ pre_action: native
1402
+ ? 'automatic_native_hook'
1403
+ : wrapper
1404
+ ? 'automatic_in_governed_runner'
1405
+ : sdk
1406
+ ? 'automatic_in_sdk_runtime'
1407
+ : routed
1408
+ ? 'mcp_routed'
1409
+ : 'adapter_required',
1410
+ action_result: native
1411
+ ? 'automatic_native_hook'
1412
+ : wrapper
1413
+ ? 'automatic_in_governed_runner'
1414
+ : sdk
1415
+ ? 'automatic_in_sdk_runtime'
1416
+ : routed
1417
+ ? 'mcp_routed'
1418
+ : 'adapter_required',
1419
+ outcome_closure: wrapper || sdk
1420
+ ? 'automatic_when_result_is_known'
1421
+ : native
1422
+ ? 'correlated_when_determinable'
1423
+ : routed
1424
+ ? 'mcp_routed'
1425
+ : 'adapter_required',
1426
+ proof_enforcement: native || wrapper || sdk
1427
+ ? 'automatic_for_protected_actions'
1428
+ : routed
1429
+ ? 'explicit_or_governed_wrapper'
1430
+ : 'adapter_required',
1431
+ automatic_repair: native || routed || sdk || wrapper ? 'installer_managed_config_only' : 'adapter_owned',
1432
+ limitation: native
1433
+ ? 'A successful tool exit is not treated as a successful business outcome when the result cannot be proven.'
1434
+ : routed
1435
+ ? 'Only actions routed through the MCP client are visible automatically.'
1436
+ : wrapper
1437
+ ? 'Only commands launched through the governed wrapper receive full automatic coverage.'
1438
+ : 'The harness must emit the documented lifecycle event contract.',
1439
+ };
1440
+ });
1080
1441
  }
1081
1442
 
1082
1443
  function detectHarnesses(cwd = process.cwd()) {
@@ -1209,16 +1570,22 @@ async function integrationOnly(parsed, name) {
1209
1570
 
1210
1571
  async function integrationsOnly(parsed) {
1211
1572
  const local = {
1212
- integration_registry_version: 'local.broad-harness-support-v2',
1573
+ integration_registry_version: 'local.automatic-control-v3',
1213
1574
  first_class_addons: ['hermes', 'openclaw'].map((name) => localIntegrationManifest(name)),
1214
1575
  supported_harnesses: localSupportedHarnesses(),
1576
+ integration_coverage: integrationCoverageMatrix(),
1215
1577
  exact_next_action: 'Pick the harness your team already uses. Use the install_command shown here, or send compact events to /v1/agent/integrations/events.',
1216
1578
  };
1217
1579
  let registry = local;
1218
1580
  let source = 'local';
1219
1581
  if (parsed.options.apiKey) {
1220
1582
  try {
1221
- registry = await requestJson(parsed.options, 'GET', '/v1/agent/integrations');
1583
+ const remote = await requestJson(parsed.options, 'GET', '/v1/agent/integrations');
1584
+ registry = {
1585
+ ...local,
1586
+ ...remote,
1587
+ integration_coverage: local.integration_coverage,
1588
+ };
1222
1589
  source = 'api';
1223
1590
  } catch (error) {
1224
1591
  registry = { ...local, api_warning: error instanceof Error ? error.message : String(error) };
@@ -1226,6 +1593,7 @@ async function integrationsOnly(parsed) {
1226
1593
  }
1227
1594
  if (parsed.options.json) return { ok: true, source, registry };
1228
1595
  const harnesses = Array.isArray(registry.supported_harnesses) ? registry.supported_harnesses : [];
1596
+ const coverage = Array.isArray(registry.integration_coverage) ? registry.integration_coverage : [];
1229
1597
  process.stdout.write([
1230
1598
  'Marrow Harness Integrations',
1231
1599
  '',
@@ -1233,6 +1601,9 @@ async function integrationsOnly(parsed) {
1233
1601
  'Supported harnesses and model CLIs:',
1234
1602
  ...harnesses.map((harness) => ` - ${displayText(harness.display_name || harness.client_label || harness.integration || harness.title, 40)} [${displayText(harness.support_level || 'supported', 24)}] ${displayText(harness.install_command || harness.install_commands?.[0] || '', 120)}`),
1235
1603
  '',
1604
+ 'Automatic lifecycle coverage:',
1605
+ ...coverage.map((entry) => ` - ${displayText(entry.harness, 24)}: pre=${entry.pre_action}; result=${entry.action_result}; closure=${entry.outcome_closure}; proof=${entry.proof_enforcement}; repair=${entry.automatic_repair}`),
1606
+ '',
1236
1607
  'First-class add-on guides: hermes, openclaw',
1237
1608
  `Next: ${displayText(registry.exact_next_action, 140)}`,
1238
1609
  registry.api_warning ? `API warning: ${displayText(registry.api_warning, 120)}` : '',
@@ -1859,6 +2230,11 @@ async function runCli(argv) {
1859
2230
  else if (parsed.command === 'gate') result = await gateOnly(parsed);
1860
2231
  else if (parsed.command === 'proof') result = await proofOnly(parsed);
1861
2232
  else if (parsed.command === 'status') result = await statusOnly(parsed);
2233
+ else if (parsed.command === 'permit') result = await permitOnly(parsed);
2234
+ else if (parsed.command === 'verify-permit') result = await verifyPermitOnly(parsed);
2235
+ else if (parsed.command === 'coverage') result = await coverageOnly(parsed);
2236
+ else if (parsed.command === 'sidecar') result = await sidecarOnly(parsed);
2237
+ else if (parsed.command === 'controller') result = await controllerOnly(parsed);
1862
2238
  else if (parsed.command === 'govern') {
1863
2239
  await runGovernInteractive(parsed.options);
1864
2240
  return;
@@ -1877,7 +2253,9 @@ async function runCli(argv) {
1877
2253
  else if (parsed.command === 'status') process.stdout.write(`${statusPanel(result)}\n`);
1878
2254
  else if (!['run', 'fleet', 'hermes', 'openclaw', 'integrations'].includes(parsed.command)) process.stdout.write('Marrow command completed.\n');
1879
2255
 
1880
- if (parsed.command === 'run' || result?.blocked) process.exitCode = result?.exitCode ?? (result?.ok === false ? 1 : 0);
2256
+ if (parsed.command === 'run' || parsed.command === 'verify-permit' || result?.blocked) {
2257
+ process.exitCode = result?.exitCode ?? (result?.ok === false ? 1 : 0);
2258
+ }
1881
2259
  }
1882
2260
 
1883
2261
  module.exports = {
@@ -1890,6 +2268,7 @@ module.exports = {
1890
2268
  headers,
1891
2269
  inferType,
1892
2270
  inferSurfaces,
2271
+ isRisky,
1893
2272
  commandForSelection,
1894
2273
  buildGovernState,
1895
2274
  detectProjectSignals,
@@ -1901,6 +2280,13 @@ module.exports = {
1901
2280
  renderGovernTui,
1902
2281
  canUseInteractive,
1903
2282
  runGoverned,
2283
+ scopedExecutionEnv,
2284
+ permitOnly,
2285
+ verifyPermitOnly,
2286
+ coverageOnly,
2287
+ sidecarOnly,
2288
+ controllerOnly,
2289
+ actionBinding,
1904
2290
  gateOnly,
1905
2291
  proofOnly,
1906
2292
  statusOnly,
@@ -1916,6 +2302,7 @@ module.exports = {
1916
2302
  renderFleetTui,
1917
2303
  runFleetInteractive,
1918
2304
  localSupportedHarnesses,
2305
+ integrationCoverageMatrix,
1919
2306
  localIntegrationManifest,
1920
2307
  renderIntegrationPanel,
1921
2308
  integrationOnly,