@getmarrow/install 0.1.10 → 0.1.12
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 +55 -2
- package/bin/marrow-install.js +7 -2
- package/package.json +3 -1
- package/src/governed-runner.js +515 -0
- package/src/installer.js +16 -7
package/README.md
CHANGED
|
@@ -11,6 +11,52 @@ npx @getmarrow/install --repair
|
|
|
11
11
|
npx @getmarrow/install doctor
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
## What's New in v0.1.12
|
|
15
|
+
|
|
16
|
+
v0.1.12 adds the Marrow governed runner for businesses that want agent governance without replacing their existing harness.
|
|
17
|
+
|
|
18
|
+
- `npx @getmarrow/install govern` prints a setup panel for detected harnesses and recommended protected commands.
|
|
19
|
+
- `npx @getmarrow/install run --agent <agent-id> -- <command>` wraps existing agent, deploy, merge, publish, migration, and verification commands with Marrow's pre-action runtime gate.
|
|
20
|
+
- Risky actions can fail closed by default when Marrow requires owner approval, blocks an action, or requires missing proof.
|
|
21
|
+
- Successful and failed commands automatically close outcomes through `/v1/agent/commit` with a redacted proof pack.
|
|
22
|
+
- The runner sends action and command metadata only; it does not upload command stdout, stderr, full environment values, or plaintext API keys.
|
|
23
|
+
- This gives teams a thin governance path for Codex, Claude Code, OpenClaw, OpenCode, Cursor, CI scripts, and custom shell-based agents.
|
|
24
|
+
|
|
25
|
+
Business value: Marrow can sit in front of the commands that matter most, tell the agent what prior lesson or proof is required before action, and produce an audit-ready outcome trail after the command finishes.
|
|
26
|
+
|
|
27
|
+
### Governed Runner Quickstart
|
|
28
|
+
|
|
29
|
+
Preview the detected harnesses and protected command examples:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx @getmarrow/install govern
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Run a harmless command through Marrow:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
MARROW_API_KEY=mrw_live_xxx npx @getmarrow/install run --agent codex-prod --profile production -- node -e "process.exit(0)"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Gate a production action before the agent executes it:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
MARROW_API_KEY=mrw_live_xxx npx @getmarrow/install gate "deploy production worker after tests pass"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Wrap a real deploy, publish, merge, or migration command only after the agent has the required proof:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
MARROW_API_KEY=mrw_live_xxx npx @getmarrow/install run \
|
|
51
|
+
--agent deploy-agent \
|
|
52
|
+
--type deploy \
|
|
53
|
+
--profile production \
|
|
54
|
+
--policy enforce \
|
|
55
|
+
-- wrangler deploy
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Use `--policy warn` for pilot mode and `--fail-open` only for non-production local workflows where Marrow should never block execution.
|
|
59
|
+
|
|
14
60
|
## What's New in v0.1.10
|
|
15
61
|
|
|
16
62
|
- First-run output now explains the value in agent/user language: your agent is no longer starting from zero.
|
|
@@ -50,7 +96,7 @@ Expected result:
|
|
|
50
96
|
- Marrow writes the safest detected MCP/SDK/agent config.
|
|
51
97
|
- A harmless setup decision is created and its outcome is committed.
|
|
52
98
|
- `/v1/agent/status` confirms capture health and missing hooks.
|
|
53
|
-
- `/v1/agent/runtime` verifies the one-call runtime gate.
|
|
99
|
+
- `/v1/agent/runtime` verifies the one-call runtime gate and returns the before-action intervention contract.
|
|
54
100
|
- `/v1/agent/first-value` returns the five-minute proof payload used by installer, SDK, and MCP clients.
|
|
55
101
|
- The installer prints: "Your agent is no longer starting from zero."
|
|
56
102
|
- Fresh accounts get a first useful action to try immediately.
|
|
@@ -64,7 +110,7 @@ After install, ask the agent:
|
|
|
64
110
|
I am about to deploy to production. What should I check first?
|
|
65
111
|
```
|
|
66
112
|
|
|
67
|
-
Marrow should answer with
|
|
113
|
+
Marrow should answer with `proceed`, `warn`, `block`, or `owner_approval_required`, plus required proof and matching fleet lessons/playbooks before the agent acts. This is the first product moment: not just "hooks installed", but "the agent is being warned before risky work."
|
|
68
114
|
|
|
69
115
|
## What It Detects
|
|
70
116
|
|
|
@@ -132,6 +178,13 @@ npm install @getmarrow/sdk
|
|
|
132
178
|
|
|
133
179
|
The generated runtime now fails soft with an explicit warning if the SDK package is missing, so onboarding does not crash a user process.
|
|
134
180
|
|
|
181
|
+
|
|
182
|
+
## Trust and Data Boundaries
|
|
183
|
+
|
|
184
|
+
Marrow is tenant-aware by design. Private account, fleet, memory, workflow, and proof-pack data stays scoped to the authenticated account and authorized agent-bound keys. Shared/hive learning uses visibility-controlled, sanitized aggregate signals; it is not raw cross-customer decision sharing.
|
|
185
|
+
|
|
186
|
+
For business pilots, review the live trust notes before production rollout: https://getmarrow.ai/docs#trust-boundaries
|
|
187
|
+
|
|
135
188
|
## Trust Model
|
|
136
189
|
|
|
137
190
|
This package is intended to be open source and auditable. It prints every file it will touch, requires `--yes` to write, does not store API keys in project files, and supports MCP-only, SDK-only, both, and markdown-only setups.
|
package/bin/marrow-install.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const installer = require('../src/installer');
|
|
4
|
+
const governedRunner = require('../src/governed-runner');
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
const argv = process.argv.slice(2);
|
|
7
|
+
const governedCommands = new Set(['run', 'gate', 'proof', 'status', 'govern']);
|
|
8
|
+
const runCli = governedCommands.has(argv[0]) ? governedRunner.runCli : installer.runCli;
|
|
9
|
+
|
|
10
|
+
runCli(argv).catch((error) => {
|
|
6
11
|
const message = error instanceof Error ? error.message : String(error);
|
|
7
12
|
process.stderr.write(`marrow-install failed: ${message}\n`);
|
|
8
13
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getmarrow/install",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "Universal installer for Marrow passive agent setup.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"marrow-install": "bin/marrow-install.js"
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
"marrow",
|
|
16
16
|
"mcp",
|
|
17
17
|
"installer",
|
|
18
|
+
"governance",
|
|
19
|
+
"runner",
|
|
18
20
|
"passive-runtime"
|
|
19
21
|
],
|
|
20
22
|
"repository": {
|
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
const { spawn } = require('node:child_process');
|
|
2
|
+
const crypto = require('node:crypto');
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_BASE_URL = 'https://api.getmarrow.ai';
|
|
8
|
+
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;
|
|
9
|
+
function usage() {
|
|
10
|
+
return `Usage:
|
|
11
|
+
npx @getmarrow/install run --agent deploy-agent -- npm test
|
|
12
|
+
npx @getmarrow/install run --agent deploy-agent --type deploy --policy enforce -- wrangler deploy
|
|
13
|
+
npx @getmarrow/install gate "deploy production worker"
|
|
14
|
+
npx @getmarrow/install proof --decision-id <id> --success --summary "smoke passed"
|
|
15
|
+
npx @getmarrow/install status
|
|
16
|
+
npx @getmarrow/install govern
|
|
17
|
+
|
|
18
|
+
Commands:
|
|
19
|
+
run Run a command through Marrow pre-action gate and automatic outcome closure
|
|
20
|
+
gate Check Marrow runtime/gate for an action without running a command
|
|
21
|
+
proof Commit an outcome/proof for an existing decision
|
|
22
|
+
status Read /v1/agent/status
|
|
23
|
+
govern Print a TUI-style setup panel for configuring governed agent runs
|
|
24
|
+
|
|
25
|
+
Options:
|
|
26
|
+
--agent <id> Agent identity. Defaults to MARROW_FLEET_AGENT_ID, MARROW_AGENT_ID, or local user
|
|
27
|
+
--session <id> Session id. Defaults to marrow-run-<timestamp>
|
|
28
|
+
--type <type> Action type. Inferred from action/command when omitted
|
|
29
|
+
--action <text> Human-readable action. Defaults to the redacted command
|
|
30
|
+
--profile <name> Policy profile label, such as dev, staging, or production
|
|
31
|
+
--policy <mode> enforce, warn, or audit. Default: enforce
|
|
32
|
+
--fail-open If Marrow is unreachable, run anyway and mark telemetry degraded
|
|
33
|
+
--fail-closed If Marrow is unreachable, block the command
|
|
34
|
+
--owner-approved <ref> Owner approval reference for review-required gates
|
|
35
|
+
--proof-file <path> JSON proof to include on outcome commit
|
|
36
|
+
--base-url <url> Marrow API base URL
|
|
37
|
+
--key <key> Marrow API key. Prefer MARROW_API_KEY
|
|
38
|
+
--json Print machine-readable result after completion
|
|
39
|
+
`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function nowId(prefix) {
|
|
43
|
+
return `${prefix}-${new Date().toISOString().replace(/[^0-9TZ]/g, '')}-${crypto.randomBytes(4).toString('hex')}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function redact(value) {
|
|
47
|
+
let text = String(value || '');
|
|
48
|
+
text = text.replace(/\b[A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD)[A-Z0-9_]*=([^\s]+)/gi, (match, captured) => match.replace(captured, '[redacted]'));
|
|
49
|
+
text = text.replace(/\bmrw_(?:live|test)_[A-Za-z0-9._-]+/g, '[redacted]');
|
|
50
|
+
text = text.replace(/\bnpm_[A-Za-z0-9._-]+/g, '[redacted]');
|
|
51
|
+
text = text.replace(/\bgh(?:p|o|u|s|r)_[A-Za-z0-9._-]+/g, '[redacted]');
|
|
52
|
+
text = text.replace(/\bsk-[A-Za-z0-9._-]+/g, '[redacted]');
|
|
53
|
+
return text;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function shellQuote(value) {
|
|
57
|
+
const text = String(value || '');
|
|
58
|
+
return /^[A-Za-z0-9_./:@%+=,-]+$/.test(text) ? text : JSON.stringify(text);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function redactedCommand(command) {
|
|
62
|
+
return command.map((part) => shellQuote(redact(part))).join(' ');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function inferType(text) {
|
|
66
|
+
const value = String(text || '').toLowerCase();
|
|
67
|
+
if (/\b(deploy|wrangler|cloudflare|production|prod|release)\b/.test(value)) return 'deploy';
|
|
68
|
+
if (/\b(publish|npm publish)\b/.test(value)) return 'publish';
|
|
69
|
+
if (/\b(merge|gh pr merge)\b/.test(value)) return 'merge';
|
|
70
|
+
if (/\b(migration|migrate|schema|d1 execute|drop table)\b/.test(value)) return 'migration';
|
|
71
|
+
if (/\b(secret|token|key|password)\b/.test(value)) return 'security';
|
|
72
|
+
if (/\b(test|check|lint|typecheck|smoke)\b/.test(value)) return 'verification';
|
|
73
|
+
return 'general';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function inferSurfaces(text) {
|
|
77
|
+
const value = String(text || '').toLowerCase();
|
|
78
|
+
const surfaces = new Set();
|
|
79
|
+
if (/\b(git|gh|github)\b/.test(value)) surfaces.add('github');
|
|
80
|
+
if (/\b(wrangler|cloudflare|worker|d1|r2)\b/.test(value)) surfaces.add('cloudflare');
|
|
81
|
+
if (/\b(npm|pnpm|yarn|publish)\b/.test(value)) surfaces.add('npm');
|
|
82
|
+
if (/\b(sql|d1|migration|database|db)\b/.test(value)) surfaces.add('database');
|
|
83
|
+
if (/\b(curl|api|http)\b/.test(value)) surfaces.add('api');
|
|
84
|
+
if (surfaces.size === 0) surfaces.add('shell');
|
|
85
|
+
return [...surfaces];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isRisky(text, type) {
|
|
89
|
+
return HIGH_RISK_TERMS.test(`${type || ''} ${text || ''}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseBaseOptions(argv, startIndex = 0) {
|
|
93
|
+
const options = {
|
|
94
|
+
apiKey: process.env.MARROW_API_KEY || process.env.MARROW_KEY || '',
|
|
95
|
+
baseUrl: process.env.MARROW_BASE_URL || DEFAULT_BASE_URL,
|
|
96
|
+
agentId: process.env.MARROW_FLEET_AGENT_ID || process.env.MARROW_AGENT_ID || os.userInfo().username || 'agent',
|
|
97
|
+
sessionId: process.env.MARROW_SESSION_ID || '',
|
|
98
|
+
profile: process.env.MARROW_GOVERN_PROFILE || 'default',
|
|
99
|
+
policy: process.env.MARROW_GOVERN_POLICY || 'enforce',
|
|
100
|
+
failOpen: process.env.MARROW_FAIL_OPEN === 'true',
|
|
101
|
+
failClosed: process.env.MARROW_FAIL_CLOSED === 'true',
|
|
102
|
+
json: false,
|
|
103
|
+
ownerApproval: '',
|
|
104
|
+
proofFile: '',
|
|
105
|
+
type: '',
|
|
106
|
+
action: '',
|
|
107
|
+
};
|
|
108
|
+
let i = startIndex;
|
|
109
|
+
for (; i < argv.length; i += 1) {
|
|
110
|
+
const arg = argv[i];
|
|
111
|
+
if (arg === '--') break;
|
|
112
|
+
if (arg === '--agent' || arg === '--agent-id') options.agentId = argv[++i] || options.agentId;
|
|
113
|
+
else if (arg === '--session' || arg === '--session-id') options.sessionId = argv[++i] || options.sessionId;
|
|
114
|
+
else if (arg === '--type') options.type = argv[++i] || options.type;
|
|
115
|
+
else if (arg === '--action') options.action = argv[++i] || options.action;
|
|
116
|
+
else if (arg === '--profile') options.profile = argv[++i] || options.profile;
|
|
117
|
+
else if (arg === '--policy') options.policy = argv[++i] || options.policy;
|
|
118
|
+
else if (arg === '--fail-open') {
|
|
119
|
+
options.failOpen = true;
|
|
120
|
+
options.failClosed = false;
|
|
121
|
+
} else if (arg === '--fail-closed') {
|
|
122
|
+
options.failClosed = true;
|
|
123
|
+
options.failOpen = false;
|
|
124
|
+
} else if (arg === '--owner-approved') options.ownerApproval = argv[++i] || options.ownerApproval;
|
|
125
|
+
else if (arg === '--proof-file') options.proofFile = argv[++i] || options.proofFile;
|
|
126
|
+
else if (arg === '--base-url') options.baseUrl = argv[++i] || options.baseUrl;
|
|
127
|
+
else if (arg === '--key') {
|
|
128
|
+
options.apiKey = argv[++i] || options.apiKey;
|
|
129
|
+
options.keyFromArg = true;
|
|
130
|
+
} else if (arg === '--json') options.json = true;
|
|
131
|
+
else if (arg === '--help' || arg === '-h') options.help = true;
|
|
132
|
+
else if (arg.startsWith('--')) throw new Error(`Unknown option: ${arg}`);
|
|
133
|
+
else break;
|
|
134
|
+
}
|
|
135
|
+
if (!['enforce', 'warn', 'audit'].includes(options.policy)) {
|
|
136
|
+
throw new Error('--policy must be enforce, warn, or audit');
|
|
137
|
+
}
|
|
138
|
+
if (!options.sessionId) options.sessionId = nowId('marrow-run');
|
|
139
|
+
return { options, index: i };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function parseArgs(argv) {
|
|
143
|
+
const command = argv[0] || 'help';
|
|
144
|
+
if (command === '--help' || command === '-h' || command === 'help') return { command: 'help' };
|
|
145
|
+
|
|
146
|
+
if (command === 'run') {
|
|
147
|
+
const parsed = parseBaseOptions(argv, 1);
|
|
148
|
+
const separator = argv[parsed.index] === '--' ? parsed.index + 1 : parsed.index;
|
|
149
|
+
const childCommand = argv.slice(separator);
|
|
150
|
+
if (parsed.options.help) return { command: 'help' };
|
|
151
|
+
if (childCommand.length === 0) throw new Error('marrow run requires a command after --');
|
|
152
|
+
return { command, options: parsed.options, childCommand };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (command === 'gate') {
|
|
156
|
+
const parsed = parseBaseOptions(argv, 1);
|
|
157
|
+
const action = parsed.options.action || argv.slice(parsed.index).join(' ');
|
|
158
|
+
if (parsed.options.help) return { command: 'help' };
|
|
159
|
+
if (!action) throw new Error('marrow gate requires an action string');
|
|
160
|
+
return { command, options: { ...parsed.options, action } };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (command === 'proof') {
|
|
164
|
+
const parsed = parseBaseOptions(argv, 1);
|
|
165
|
+
const options = { ...parsed.options, decisionId: '', success: true, summary: '', outcome: '' };
|
|
166
|
+
for (let i = parsed.index; i < argv.length; i += 1) {
|
|
167
|
+
const arg = argv[i];
|
|
168
|
+
if (arg === '--decision-id') options.decisionId = argv[++i] || '';
|
|
169
|
+
else if (arg === '--success') options.success = true;
|
|
170
|
+
else if (arg === '--failure' || arg === '--failed') options.success = false;
|
|
171
|
+
else if (arg === '--summary') options.summary = argv[++i] || '';
|
|
172
|
+
else if (arg === '--outcome') options.outcome = argv[++i] || '';
|
|
173
|
+
else if (arg === '--help' || arg === '-h') return { command: 'help' };
|
|
174
|
+
else throw new Error(`Unknown proof option: ${arg}`);
|
|
175
|
+
}
|
|
176
|
+
if (!options.decisionId) throw new Error('marrow proof requires --decision-id');
|
|
177
|
+
return { command, options };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (command === 'status' || command === 'govern') {
|
|
181
|
+
const parsed = parseBaseOptions(argv, 1);
|
|
182
|
+
if (parsed.options.help) return { command: 'help' };
|
|
183
|
+
return { command, options: parsed.options };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
throw new Error(`Unknown command: ${command}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function headers(options) {
|
|
190
|
+
const h = {
|
|
191
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
192
|
+
'Content-Type': 'application/json',
|
|
193
|
+
'X-Marrow-Agent-Id': options.agentId,
|
|
194
|
+
'X-Marrow-Session-Id': options.sessionId,
|
|
195
|
+
'User-Agent': '@getmarrow/install governed-runner',
|
|
196
|
+
};
|
|
197
|
+
return h;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function dataOf(json) {
|
|
201
|
+
return json && typeof json === 'object' && json.data && typeof json.data === 'object' ? json.data : json;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function requestJson(options, method, route, body) {
|
|
205
|
+
if (!options.apiKey) throw new Error('MARROW_API_KEY is required. Use --fail-open only for non-production local commands.');
|
|
206
|
+
const response = await fetch(new URL(route, options.baseUrl.replace(/\/$/, '/')), {
|
|
207
|
+
method,
|
|
208
|
+
headers: headers(options),
|
|
209
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
210
|
+
});
|
|
211
|
+
const text = await response.text();
|
|
212
|
+
let json = {};
|
|
213
|
+
try { json = text ? JSON.parse(text) : {}; } catch { json = { error: text.slice(0, 500) }; }
|
|
214
|
+
if (!response.ok) {
|
|
215
|
+
const error = new Error(json.error || json.message || `Marrow ${route} returned HTTP ${response.status}`);
|
|
216
|
+
error.status = response.status;
|
|
217
|
+
error.details = json.details || json;
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
return dataOf(json);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function proofFromFile(filePath) {
|
|
224
|
+
if (!filePath) return null;
|
|
225
|
+
const raw = fs.readFileSync(path.resolve(filePath), 'utf8');
|
|
226
|
+
return JSON.parse(raw);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function defaultProof(input) {
|
|
230
|
+
const proof = proofFromFile(input.options.proofFile) || {};
|
|
231
|
+
return {
|
|
232
|
+
summary: proof.summary || `Marrow governed runner completed ${input.action}.`,
|
|
233
|
+
checks: Array.isArray(proof.checks) ? proof.checks : ['marrow runtime gate', 'command exit captured'],
|
|
234
|
+
outcome: proof.outcome || (input.success ? 'success' : 'failure'),
|
|
235
|
+
blockers: Array.isArray(proof.blockers) ? proof.blockers : [],
|
|
236
|
+
command: redactedCommand(input.childCommand || []),
|
|
237
|
+
exit_code: input.exitCode,
|
|
238
|
+
runner: '@getmarrow/install run',
|
|
239
|
+
profile: input.options.profile,
|
|
240
|
+
...(input.options.ownerApproval ? { owner_approval: { approved_by: 'owner', reference: input.options.ownerApproval } } : {}),
|
|
241
|
+
...proof,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function preflightRuntime(options, action, type, commandText) {
|
|
246
|
+
return requestJson(options, 'POST', '/v1/agent/runtime', {
|
|
247
|
+
action,
|
|
248
|
+
type,
|
|
249
|
+
surfaces: inferSurfaces(commandText || action),
|
|
250
|
+
context: {
|
|
251
|
+
runner: '@getmarrow/install run',
|
|
252
|
+
profile: options.profile,
|
|
253
|
+
command: commandText,
|
|
254
|
+
policy: options.policy,
|
|
255
|
+
governed: true,
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function gateDecision(runtime) {
|
|
261
|
+
const gate = runtime?.risk_gate || {};
|
|
262
|
+
const receipt = runtime?.gate_receipt || {};
|
|
263
|
+
return {
|
|
264
|
+
decision: gate.enforcement_decision || receipt.decision || gate.decision || 'unknown',
|
|
265
|
+
allow: gate.allow !== false,
|
|
266
|
+
required: Boolean(receipt.required || gate.gate_required),
|
|
267
|
+
ownerApprovalRequired: Boolean(receipt.owner_approval_required || gate.owner_approval_required),
|
|
268
|
+
receiptId: receipt.id || gate.gate_receipt_id || '',
|
|
269
|
+
exactNextAction: runtime?.exact_next_action || receipt.exact_fix || gate.policy?.exact_fix || '',
|
|
270
|
+
beforeYouAct: runtime?.before_you_act_injection?.message || runtime?.before_you_act || '',
|
|
271
|
+
proofPack: runtime?.proof_pack || null,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function shouldBlock(decision, options) {
|
|
276
|
+
if (options.policy === 'audit') return false;
|
|
277
|
+
if (decision.decision === 'block' || decision.allow === false) return true;
|
|
278
|
+
if (options.policy === 'warn') return false;
|
|
279
|
+
if (decision.ownerApprovalRequired && !options.ownerApproval) return true;
|
|
280
|
+
if (decision.decision === 'owner_approval_required' && !options.ownerApproval) return true;
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function printGate(decision, runtime, stream = process.stdout) {
|
|
285
|
+
stream.write(`Marrow gate: ${decision.decision}${decision.required ? ' (required)' : ''}\n`);
|
|
286
|
+
if (decision.beforeYouAct) stream.write(`Before you act: ${decision.beforeYouAct}\n`);
|
|
287
|
+
if (decision.exactNextAction) stream.write(`Next: ${decision.exactNextAction}\n`);
|
|
288
|
+
if (decision.proofPack?.required) {
|
|
289
|
+
const missing = decision.proofPack.missing?.length ? ` missing: ${decision.proofPack.missing.join(', ')}` : '';
|
|
290
|
+
stream.write(`Proof pack: required${missing}\n`);
|
|
291
|
+
}
|
|
292
|
+
if (runtime?.value_proof?.owner_summary) stream.write(`Value: ${runtime.value_proof.owner_summary}\n`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function runChild(command, env = process.env) {
|
|
296
|
+
return new Promise((resolve) => {
|
|
297
|
+
const child = spawn(command[0], command.slice(1), {
|
|
298
|
+
stdio: 'inherit',
|
|
299
|
+
shell: false,
|
|
300
|
+
env,
|
|
301
|
+
});
|
|
302
|
+
child.on('error', (error) => resolve({ exitCode: 127, error }));
|
|
303
|
+
child.on('close', (code, signal) => resolve({ exitCode: code ?? 1, signal: signal || null }));
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function createDecision(options, action, type) {
|
|
308
|
+
return requestJson(options, 'POST', '/v1/agent/think', {
|
|
309
|
+
action,
|
|
310
|
+
type,
|
|
311
|
+
context: {
|
|
312
|
+
runner: '@getmarrow/install run',
|
|
313
|
+
profile: options.profile,
|
|
314
|
+
governed: true,
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function commitOutcome(options, decisionId, success, outcome, proof, gateReceiptId) {
|
|
320
|
+
const body = {
|
|
321
|
+
decision_id: decisionId,
|
|
322
|
+
success,
|
|
323
|
+
outcome,
|
|
324
|
+
proof,
|
|
325
|
+
};
|
|
326
|
+
if (gateReceiptId) body.gate_receipt_id = gateReceiptId;
|
|
327
|
+
return requestJson(options, 'POST', '/v1/agent/commit', body);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function runGoverned(parsed) {
|
|
331
|
+
const { options, childCommand } = parsed;
|
|
332
|
+
const commandText = redactedCommand(childCommand);
|
|
333
|
+
const action = options.action ? redact(options.action) : commandText;
|
|
334
|
+
const type = options.type || inferType(`${action} ${commandText}`);
|
|
335
|
+
const risky = isRisky(`${action} ${commandText}`, type);
|
|
336
|
+
let runtime = null;
|
|
337
|
+
let decision = null;
|
|
338
|
+
let decisionId = '';
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
runtime = await preflightRuntime(options, action, type, commandText);
|
|
342
|
+
decision = gateDecision(runtime);
|
|
343
|
+
printGate(decision, runtime);
|
|
344
|
+
if (shouldBlock(decision, options)) {
|
|
345
|
+
return {
|
|
346
|
+
ok: false,
|
|
347
|
+
blocked: true,
|
|
348
|
+
exitCode: 12,
|
|
349
|
+
action,
|
|
350
|
+
type,
|
|
351
|
+
risky,
|
|
352
|
+
decision,
|
|
353
|
+
message: decision.exactNextAction || 'Marrow blocked this action before execution.',
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
const think = await createDecision(options, action, type);
|
|
357
|
+
decisionId = think.decision_id || think.id || think.decision?.id || '';
|
|
358
|
+
} catch (error) {
|
|
359
|
+
if (options.failOpen || (!risky && !options.failClosed)) {
|
|
360
|
+
process.stderr.write(`Marrow degraded: ${error.message}. Continuing because fail-open/non-risky policy allows it.\n`);
|
|
361
|
+
} else {
|
|
362
|
+
return {
|
|
363
|
+
ok: false,
|
|
364
|
+
blocked: true,
|
|
365
|
+
degraded: true,
|
|
366
|
+
exitCode: 13,
|
|
367
|
+
action,
|
|
368
|
+
type,
|
|
369
|
+
risky,
|
|
370
|
+
message: error.message,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const child = await runChild(childCommand);
|
|
376
|
+
const success = child.exitCode === 0;
|
|
377
|
+
const proof = defaultProof({ options, action, childCommand, exitCode: child.exitCode, success });
|
|
378
|
+
const outcome = success
|
|
379
|
+
? `Marrow governed command succeeded with exit code ${child.exitCode}.`
|
|
380
|
+
: `Marrow governed command failed with exit code ${child.exitCode}.`;
|
|
381
|
+
|
|
382
|
+
let commit = null;
|
|
383
|
+
if (decisionId) {
|
|
384
|
+
try {
|
|
385
|
+
commit = await commitOutcome(options, decisionId, success, outcome, proof, decision?.receiptId || '');
|
|
386
|
+
} catch (error) {
|
|
387
|
+
process.stderr.write(`Marrow outcome commit failed: ${error.message}\n`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
ok: success,
|
|
393
|
+
blocked: false,
|
|
394
|
+
exitCode: child.exitCode,
|
|
395
|
+
action,
|
|
396
|
+
type,
|
|
397
|
+
risky,
|
|
398
|
+
decision,
|
|
399
|
+
decision_id: decisionId,
|
|
400
|
+
outcome_committed: Boolean(commit),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async function gateOnly(parsed) {
|
|
405
|
+
const { options } = parsed;
|
|
406
|
+
const action = redact(options.action);
|
|
407
|
+
const type = options.type || inferType(action);
|
|
408
|
+
const runtime = await preflightRuntime(options, action, type, action);
|
|
409
|
+
const decision = gateDecision(runtime);
|
|
410
|
+
printGate(decision, runtime);
|
|
411
|
+
return { ok: !shouldBlock(decision, options), action, type, decision };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function proofOnly(parsed) {
|
|
415
|
+
const { options } = parsed;
|
|
416
|
+
const proof = defaultProof({
|
|
417
|
+
options,
|
|
418
|
+
action: options.summary || options.outcome || 'manual proof closeout',
|
|
419
|
+
childCommand: [],
|
|
420
|
+
exitCode: options.success ? 0 : 1,
|
|
421
|
+
success: options.success,
|
|
422
|
+
});
|
|
423
|
+
const result = await commitOutcome(
|
|
424
|
+
options,
|
|
425
|
+
options.decisionId,
|
|
426
|
+
options.success,
|
|
427
|
+
options.outcome || options.summary || (options.success ? 'Manual proof closeout succeeded.' : 'Manual proof closeout failed.'),
|
|
428
|
+
proof,
|
|
429
|
+
'',
|
|
430
|
+
);
|
|
431
|
+
return { ok: true, decision_id: options.decisionId, committed: true, result };
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function statusOnly(parsed) {
|
|
435
|
+
return requestJson(parsed.options, 'GET', '/v1/agent/status');
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function detectHarnesses(cwd = process.cwd()) {
|
|
439
|
+
const candidates = [
|
|
440
|
+
{ name: 'Codex', command: 'codex', detected: fs.existsSync(path.join(cwd, 'AGENTS.md')) || fs.existsSync(path.join(os.homedir(), '.codex')) },
|
|
441
|
+
{ name: 'Claude Code', command: 'claude -p', detected: fs.existsSync(path.join(cwd, 'CLAUDE.md')) || fs.existsSync(path.join(os.homedir(), '.claude.json')) },
|
|
442
|
+
{ name: 'OpenCode', command: 'opencode', detected: fs.existsSync(path.join(cwd, 'opencode.json')) || fs.existsSync(path.join(os.homedir(), '.opencode')) },
|
|
443
|
+
{ name: 'OpenClaw', command: 'openclaw agent', detected: fs.existsSync(path.join(os.homedir(), '.openclaw')) },
|
|
444
|
+
{ name: 'Custom command', command: '<your-agent-command>', detected: true },
|
|
445
|
+
];
|
|
446
|
+
return candidates;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function governPanel(options) {
|
|
450
|
+
const rows = detectHarnesses();
|
|
451
|
+
const lines = [
|
|
452
|
+
'Marrow Governed Runner',
|
|
453
|
+
'',
|
|
454
|
+
`Agent: ${options.agentId}`,
|
|
455
|
+
`Profile: ${options.profile}`,
|
|
456
|
+
`Policy: ${options.policy}`,
|
|
457
|
+
'',
|
|
458
|
+
'Choose where your agent runs. Marrow governs the action before it executes.',
|
|
459
|
+
'',
|
|
460
|
+
'Detected harnesses:',
|
|
461
|
+
...rows.map((row, index) => ` ${index + 1}. ${row.detected ? '[x]' : '[ ]'} ${row.name} ${row.command}`),
|
|
462
|
+
'',
|
|
463
|
+
'Recommended first commands:',
|
|
464
|
+
` npx @getmarrow/install run --agent ${options.agentId} --profile production --policy enforce -- codex`,
|
|
465
|
+
` npx @getmarrow/install run --agent deploy-agent --type deploy --policy enforce -- wrangler deploy`,
|
|
466
|
+
` npx @getmarrow/install gate "deploy production worker after tests pass"`,
|
|
467
|
+
'',
|
|
468
|
+
'Protected by default: deploy, merge, publish, migrations, secrets, keys, production actions.',
|
|
469
|
+
];
|
|
470
|
+
return lines.join('\n');
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async function runCli(argv) {
|
|
474
|
+
const parsed = parseArgs(argv);
|
|
475
|
+
if (parsed.command === 'help') {
|
|
476
|
+
process.stdout.write(usage());
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (parsed.options?.keyFromArg) {
|
|
481
|
+
process.stderr.write('Warning: prefer MARROW_API_KEY instead of --key because command-line args can be visible in process listings.\n');
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
let result;
|
|
485
|
+
if (parsed.command === 'run') result = await runGoverned(parsed);
|
|
486
|
+
else if (parsed.command === 'gate') result = await gateOnly(parsed);
|
|
487
|
+
else if (parsed.command === 'proof') result = await proofOnly(parsed);
|
|
488
|
+
else if (parsed.command === 'status') result = await statusOnly(parsed);
|
|
489
|
+
else if (parsed.command === 'govern') {
|
|
490
|
+
process.stdout.write(`${governPanel(parsed.options)}\n`);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (parsed.options?.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
495
|
+
else if (result?.blocked) process.stderr.write(`BLOCKED: ${result.message || 'Marrow blocked this action.'}\n`);
|
|
496
|
+
else if (parsed.command !== 'run') process.stdout.write('Marrow command completed.\n');
|
|
497
|
+
|
|
498
|
+
if (parsed.command === 'run' || result?.blocked) process.exitCode = result?.exitCode ?? (result?.ok === false ? 1 : 0);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
module.exports = {
|
|
502
|
+
parseArgs,
|
|
503
|
+
redact,
|
|
504
|
+
redactedCommand,
|
|
505
|
+
inferType,
|
|
506
|
+
inferSurfaces,
|
|
507
|
+
gateDecision,
|
|
508
|
+
shouldBlock,
|
|
509
|
+
governPanel,
|
|
510
|
+
runGoverned,
|
|
511
|
+
gateOnly,
|
|
512
|
+
proofOnly,
|
|
513
|
+
statusOnly,
|
|
514
|
+
runCli,
|
|
515
|
+
};
|
package/src/installer.js
CHANGED
|
@@ -105,6 +105,12 @@ function findUp(startDir, names, maxDepth = 8) {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
function projectRoot(startDir) {
|
|
108
|
+
const resolved = path.resolve(startDir);
|
|
109
|
+
if (path.basename(resolved) === '.marrow') return path.dirname(resolved);
|
|
110
|
+
if (path.basename(resolved) === 'env' && path.basename(path.dirname(resolved)) === '.marrow') {
|
|
111
|
+
return path.dirname(path.dirname(resolved));
|
|
112
|
+
}
|
|
113
|
+
if (exists(path.join(resolved, '.marrow'))) return resolved;
|
|
108
114
|
const marker = findUp(startDir, ['package.json', 'pyproject.toml', 'requirements.txt', '.git', 'AGENTS.md', 'CLAUDE.md']);
|
|
109
115
|
return marker ? path.dirname(marker) : path.resolve(startDir);
|
|
110
116
|
}
|
|
@@ -283,7 +289,7 @@ Marrow should run passively after install:
|
|
|
283
289
|
|
|
284
290
|
- Use MCP hooks when available: \`npx -y @getmarrow/mcp setup\`.
|
|
285
291
|
- Use SDK passive runtime in owned Node processes: \`createPassiveRuntime().install()\`.
|
|
286
|
-
- Before risky work, use Marrow's
|
|
292
|
+
- Before risky work, use Marrow's before-action intervention from \`GET /v1/agent/status\` or \`POST /v1/agent/runtime\`.
|
|
287
293
|
- After meaningful work, record the outcome so future agents learn from it.
|
|
288
294
|
- Check health with \`marrow_agent_status\` or \`GET /v1/agent/status\`.
|
|
289
295
|
|
|
@@ -640,7 +646,8 @@ async function runSelfTest(options) {
|
|
|
640
646
|
auto_outcome_closure: status.auto_outcome_closure || null,
|
|
641
647
|
runtime_active: Boolean(runtime && runtime.ok !== false),
|
|
642
648
|
runtime_exact_next_action: runtime.exact_next_action || null,
|
|
643
|
-
runtime_before_you_act: runtime.before_you_act || null,
|
|
649
|
+
runtime_before_you_act: runtime.intervention?.agent_copy || runtime.intervention?.before_action || runtime.before_you_act || null,
|
|
650
|
+
runtime_intervention: runtime.intervention || null,
|
|
644
651
|
first_value: firstValue && firstValue.ok !== false ? firstValue : null,
|
|
645
652
|
first_value_signal: firstValueSignal,
|
|
646
653
|
install_value_moment: installValueMoment,
|
|
@@ -669,7 +676,9 @@ function buildInstallValueMoment(firstValueSignal = {}, status = {}, runtime = {
|
|
|
669
676
|
|
|
670
677
|
const proof = firstValueSignal.value_proof || [];
|
|
671
678
|
const hasFleetSignal = proof.length > 0;
|
|
672
|
-
const runtimeLesson = runtime.
|
|
679
|
+
const runtimeLesson = runtime.intervention?.agent_copy
|
|
680
|
+
|| runtime.intervention?.before_action
|
|
681
|
+
|| runtime.before_you_act
|
|
673
682
|
|| runtime.before_you_act_injection?.message
|
|
674
683
|
|| runtime.exact_next_action
|
|
675
684
|
|| firstValueSignal.first_lesson;
|
|
@@ -680,14 +689,14 @@ function buildInstallValueMoment(firstValueSignal = {}, status = {}, runtime = {
|
|
|
680
689
|
'Captured this setup decision',
|
|
681
690
|
'Closed the outcome successfully',
|
|
682
691
|
'Runtime gate is ' + (firstValueSignal.active ? 'active' : 'installed'),
|
|
683
|
-
runtimeLesson ? 'Future risky work now gets a pre-action brief' : 'Future risky work now gets checked before action',
|
|
692
|
+
runtime.intervention?.must_use_before_action ? 'Before-action intervention is active for risky work' : runtimeLesson ? 'Future risky work now gets a pre-action brief' : 'Future risky work now gets checked before action',
|
|
684
693
|
],
|
|
685
694
|
fleet_signal: hasFleetSignal
|
|
686
695
|
? 'Marrow already found signal: ' + proof.join('; ') + '.'
|
|
687
696
|
: 'Fresh account: Marrow will start building fleet memory from this first captured outcome.',
|
|
688
697
|
try_this_now: 'Ask your agent: "I am about to deploy to production. What should I check first?"',
|
|
689
|
-
expected_response: 'Marrow should answer with
|
|
690
|
-
first_lesson: runtimeLesson || 'Marrow will
|
|
698
|
+
expected_response: 'Marrow should answer with proceed/warn/block, required proof, and any matching prior lesson/playbook before the agent acts.',
|
|
699
|
+
first_lesson: runtimeLesson || 'Marrow will stop agents before risky or repeated work and surface the prior lesson/playbook.',
|
|
691
700
|
};
|
|
692
701
|
}
|
|
693
702
|
|
|
@@ -704,7 +713,7 @@ function buildFirstValueSignal(status, runtime, performance, firstValue = {}) {
|
|
|
704
713
|
active: Boolean(firstValue.active),
|
|
705
714
|
headline: `Marrow active: ${(capture.surfaces || ['decisions']).join(', ')} captured.`,
|
|
706
715
|
captured: capture.surfaces || ['decisions'],
|
|
707
|
-
first_lesson: firstValue.first_value.first_lesson,
|
|
716
|
+
first_lesson: firstValue.first_value.first_lesson || runtime?.intervention?.agent_copy,
|
|
708
717
|
value_proof: proofBits,
|
|
709
718
|
next_action: firstValue.next_action?.reason || 'Keep working; Marrow will capture outcomes and reuse lessons automatically.',
|
|
710
719
|
};
|