@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.
- package/README.md +64 -9
- package/bin/marrow-install.js +1 -1
- package/package.json +1 -1
- package/src/controller-manager.js +449 -0
- package/src/enforcement-client.js +94 -0
- package/src/governance-sidecar.js +242 -0
- package/src/governed-runner.js +404 -17
- package/src/installer.js +209 -29
package/src/governed-runner.js
CHANGED
|
@@ -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
|
|
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)
|
|
133
|
-
|
|
134
|
-
if (/\b(
|
|
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
|
|
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 === '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
560
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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' ||
|
|
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,
|