@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/installer.js
CHANGED
|
@@ -3,13 +3,14 @@ const path = require('node:path');
|
|
|
3
3
|
const os = require('node:os');
|
|
4
4
|
const crypto = require('node:crypto');
|
|
5
5
|
const { version: INSTALLER_ADAPTER_VERSION } = require('../package.json');
|
|
6
|
+
const { controllerStatus, controllerSupportedPlatform, ensureGovernanceController } = require('./controller-manager');
|
|
6
7
|
|
|
7
8
|
const DEFAULT_BASE_URL = 'https://api.getmarrow.ai';
|
|
8
9
|
const MARROW_BLOCK_START = '<!-- marrow:passive-start -->';
|
|
9
10
|
const MARROW_BLOCK_END = '<!-- marrow:passive-end -->';
|
|
10
|
-
const MCP_ADAPTER_VERSION = '3.9.
|
|
11
|
-
const SDK_ADAPTER_VERSION = '3.7.
|
|
12
|
-
const SDK_ADAPTER_INTEGRITY = 'sha512-
|
|
11
|
+
const MCP_ADAPTER_VERSION = '3.9.53';
|
|
12
|
+
const SDK_ADAPTER_VERSION = '3.7.52';
|
|
13
|
+
const SDK_ADAPTER_INTEGRITY = 'sha512-dMo5rMXP5sFTRsiRI+Oe3SOWgSsi8TTt9VrYL9gC71EQFN2UTtuH914CkYEpcS627sb02jXrUDiXxznX/2fWgA==';
|
|
13
14
|
const SDK_ADAPTER_TARBALL = `https://registry.npmjs.org/@getmarrow/sdk/-/sdk-${SDK_ADAPTER_VERSION}.tgz`;
|
|
14
15
|
const MCP_PACKAGE_SPEC = `@getmarrow/mcp@${MCP_ADAPTER_VERSION}`;
|
|
15
16
|
const MCP_CONTEXT_HOOK_COMMAND = `npx -y ${MCP_PACKAGE_SPEC} context-hook`;
|
|
@@ -18,7 +19,7 @@ const MCP_ACTION_RESULT_HOOK_COMMAND = `npx -y ${MCP_PACKAGE_SPEC} hook`;
|
|
|
18
19
|
const MCP_SESSION_END_HOOK_COMMAND = `npx -y ${MCP_PACKAGE_SPEC} session-hook`;
|
|
19
20
|
const NATIVE_HOOK_MATCHER = 'Bash|Edit|Write|MultiEdit|mcp__(?!marrow_).*';
|
|
20
21
|
const NATIVE_EXPECTED_HOOKS = ['prompt', 'pre_action', 'action_result', 'session_end'];
|
|
21
|
-
const SOURCE_CLIENTS = new Set(['claude-code', 'cursor', 'composer', 'windsurf', 'openclaw', 'codex', 'gemini', 'grok', 'deepseek', 'qwen', 'kimi', 'minimax', 'cline', 'opencode', 'hermes', 'glm', 'custom', 'unknown']);
|
|
22
|
+
const SOURCE_CLIENTS = new Set(['claude-code', 'cursor', 'composer', 'windsurf', 'openclaw', 'codex', 'gemini', 'grok', 'deepseek', 'qwen', 'kimi', 'minimax', 'cline', 'opencode', 'hermes', 'glm', 'mcp', 'ci', 'custom', 'unknown']);
|
|
22
23
|
const HARNESS_CAPABILITY_REGISTRY = Object.freeze([
|
|
23
24
|
{ client: 'claude-code', capability_level: 'native_hooks', automatic: ['prompt', 'pre_action', 'action_result', 'session_end'], install_surface: 'mcp' },
|
|
24
25
|
{ client: 'cursor', capability_level: 'mcp', automatic: ['mcp_tool_calls'], install_surface: 'mcp' },
|
|
@@ -36,6 +37,8 @@ const HARNESS_CAPABILITY_REGISTRY = Object.freeze([
|
|
|
36
37
|
{ client: 'kimi', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
|
|
37
38
|
{ client: 'minimax', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
|
|
38
39
|
{ client: 'glm', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
|
|
40
|
+
{ client: 'mcp', capability_level: 'mcp', automatic: ['mcp_tool_calls'], install_surface: 'mcp' },
|
|
41
|
+
{ client: 'ci', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
|
|
39
42
|
{ client: 'custom', capability_level: 'event_contract', automatic: [], install_surface: 'event_contract' },
|
|
40
43
|
]);
|
|
41
44
|
|
|
@@ -64,6 +67,9 @@ function sourceClient() {
|
|
|
64
67
|
hermes: 'hermes',
|
|
65
68
|
'hermes-agent': 'hermes',
|
|
66
69
|
glm: 'glm',
|
|
70
|
+
mcp: 'mcp',
|
|
71
|
+
ci: 'ci',
|
|
72
|
+
'github-actions': 'ci',
|
|
67
73
|
};
|
|
68
74
|
return aliases[raw] || (SOURCE_CLIENTS.has(raw) ? raw : 'custom');
|
|
69
75
|
}
|
|
@@ -83,6 +89,7 @@ function parseArgs(argv) {
|
|
|
83
89
|
selfTestExplicitlyDisabled: false,
|
|
84
90
|
json: false,
|
|
85
91
|
activate: false,
|
|
92
|
+
controller: true,
|
|
86
93
|
};
|
|
87
94
|
|
|
88
95
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -104,6 +111,7 @@ function parseArgs(argv) {
|
|
|
104
111
|
options.selfTest = false;
|
|
105
112
|
options.selfTestExplicitlyDisabled = true;
|
|
106
113
|
}
|
|
114
|
+
else if (arg === '--no-controller') options.controller = false;
|
|
107
115
|
else if (arg === '--self-test') options.selfTest = true;
|
|
108
116
|
else if (arg === '--cwd') options.cwd = path.resolve(argv[++i] || options.cwd);
|
|
109
117
|
else if (arg === '--mode') options.mode = argv[++i] || options.mode;
|
|
@@ -157,6 +165,7 @@ Options:
|
|
|
157
165
|
--key <key> Marrow API key for self-test. Prefer MARROW_API_KEY because CLI args can appear in process listings.
|
|
158
166
|
--base-url <url> Marrow API base URL
|
|
159
167
|
--agent-id <id> Agent/fleet id for self-test headers
|
|
168
|
+
--no-controller Do not start the local background controller during install/repair
|
|
160
169
|
--no-self-test Skip API smoke/self-test
|
|
161
170
|
`;
|
|
162
171
|
}
|
|
@@ -291,6 +300,7 @@ function fingerprint(value) {
|
|
|
291
300
|
function npmTokenPaths(env = process.env) {
|
|
292
301
|
const home = env.HOME || env.USERPROFILE || os.homedir();
|
|
293
302
|
return {
|
|
303
|
+
home,
|
|
294
304
|
openclawEnv: path.join(home, '.openclaw', '.env'),
|
|
295
305
|
credentialFile: path.join(home, '.openclaw', 'credentials', 'npm-getmarrow-token.txt'),
|
|
296
306
|
npmrc: path.join(home, '.npmrc'),
|
|
@@ -301,7 +311,14 @@ function inspectNpmTokenConfig(env = process.env) {
|
|
|
301
311
|
const paths = npmTokenPaths(env);
|
|
302
312
|
const openclawToken = readEnvVar(paths.openclawEnv, 'NPM_TOKEN');
|
|
303
313
|
const credentialToken = readFirstLineSecret(paths.credentialFile);
|
|
304
|
-
|
|
314
|
+
let npmrcToken = '';
|
|
315
|
+
let unsafeNpmrcPath = false;
|
|
316
|
+
try {
|
|
317
|
+
assertDirectOwnerFile(paths.home, paths.npmrc, { allowMissing: true });
|
|
318
|
+
npmrcToken = readNpmrcToken(paths.npmrc);
|
|
319
|
+
} catch {
|
|
320
|
+
unsafeNpmrcPath = true;
|
|
321
|
+
}
|
|
305
322
|
const sourceToken = openclawToken || credentialToken;
|
|
306
323
|
const mismatch = Boolean(sourceToken && npmrcToken && fingerprint(sourceToken) !== fingerprint(npmrcToken));
|
|
307
324
|
const missingNpmrcToken = Boolean(sourceToken && !npmrcToken);
|
|
@@ -310,15 +327,18 @@ function inspectNpmTokenConfig(env = process.env) {
|
|
|
310
327
|
safe: {
|
|
311
328
|
npm_token: {
|
|
312
329
|
checked: true,
|
|
313
|
-
repairable: Boolean(sourceToken && (mismatch || missingNpmrcToken)),
|
|
330
|
+
repairable: Boolean(sourceToken && (mismatch || missingNpmrcToken) && !unsafeNpmrcPath),
|
|
314
331
|
mismatch,
|
|
315
332
|
missing_npmrc_token: missingNpmrcToken,
|
|
333
|
+
unsafe_path: unsafeNpmrcPath,
|
|
316
334
|
sources: {
|
|
317
335
|
openclaw_env: { path: paths.openclawEnv, present: Boolean(openclawToken), fingerprint: fingerprint(openclawToken) },
|
|
318
336
|
credential_file: { path: paths.credentialFile, present: Boolean(credentialToken), fingerprint: fingerprint(credentialToken) },
|
|
319
337
|
npmrc: { path: paths.npmrc, present: Boolean(npmrcToken), fingerprint: fingerprint(npmrcToken) },
|
|
320
338
|
},
|
|
321
|
-
recommended_fix:
|
|
339
|
+
recommended_fix: unsafeNpmrcPath
|
|
340
|
+
? 'Refusing automatic npm token repair because ~/.npmrc or its home directory is not a direct, regular owner path.'
|
|
341
|
+
: mismatch || missingNpmrcToken
|
|
322
342
|
? 'Run npx @getmarrow/install --repair to sync ~/.npmrc from the active OpenClaw/getmarrow npm token source.'
|
|
323
343
|
: null,
|
|
324
344
|
},
|
|
@@ -327,7 +347,49 @@ function inspectNpmTokenConfig(env = process.env) {
|
|
|
327
347
|
};
|
|
328
348
|
}
|
|
329
349
|
|
|
330
|
-
function
|
|
350
|
+
function assertDirectOwnerFile(homePath, filePath, { allowMissing = false } = {}) {
|
|
351
|
+
const home = path.resolve(homePath);
|
|
352
|
+
const target = path.resolve(filePath);
|
|
353
|
+
if (target !== path.join(home, '.npmrc')) throw new Error('npm token repair target must be the direct owner ~/.npmrc');
|
|
354
|
+
if (!fs.existsSync(home)) throw new Error('npm token repair owner home does not exist');
|
|
355
|
+
const homeStat = fs.lstatSync(home);
|
|
356
|
+
if (!homeStat.isDirectory() || homeStat.isSymbolicLink() || fs.realpathSync(home) !== home) {
|
|
357
|
+
throw new Error('npm token repair owner home must be a direct, non-symbolic directory');
|
|
358
|
+
}
|
|
359
|
+
if (!fs.existsSync(target)) {
|
|
360
|
+
if (allowMissing) return;
|
|
361
|
+
throw new Error('npm token repair target does not exist');
|
|
362
|
+
}
|
|
363
|
+
const targetStat = fs.lstatSync(target);
|
|
364
|
+
if (targetStat.isSymbolicLink() || !targetStat.isFile()) {
|
|
365
|
+
throw new Error('npm token repair target must be a regular file, not a symbolic link');
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function atomicWriteOwnerFile(homePath, filePath, contents) {
|
|
370
|
+
const home = path.resolve(homePath);
|
|
371
|
+
const target = path.resolve(filePath);
|
|
372
|
+
const allowMissing = !fs.existsSync(target);
|
|
373
|
+
assertDirectOwnerFile(home, target, { allowMissing });
|
|
374
|
+
const tempPath = path.join(home, `.npmrc.marrow-${process.pid}-${crypto.randomBytes(6).toString('hex')}.tmp`);
|
|
375
|
+
let descriptor;
|
|
376
|
+
try {
|
|
377
|
+
descriptor = fs.openSync(tempPath, 'wx', 0o600);
|
|
378
|
+
fs.writeFileSync(descriptor, contents, { encoding: 'utf8' });
|
|
379
|
+
fs.fsyncSync(descriptor);
|
|
380
|
+
fs.closeSync(descriptor);
|
|
381
|
+
descriptor = undefined;
|
|
382
|
+
assertDirectOwnerFile(home, target, { allowMissing });
|
|
383
|
+
fs.renameSync(tempPath, target);
|
|
384
|
+
fs.chmodSync(target, 0o600);
|
|
385
|
+
} finally {
|
|
386
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
387
|
+
if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function upsertNpmrcToken(homePath, filePath, token) {
|
|
392
|
+
assertDirectOwnerFile(homePath, filePath, { allowMissing: true });
|
|
331
393
|
const before = safeRead(filePath);
|
|
332
394
|
const tokenLine = `//registry.npmjs.org/:_authToken=${token}`;
|
|
333
395
|
let after;
|
|
@@ -338,14 +400,17 @@ function upsertNpmrcToken(filePath, token) {
|
|
|
338
400
|
after = `${before}${separator}${tokenLine}\n`;
|
|
339
401
|
}
|
|
340
402
|
if (before !== after) {
|
|
341
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
342
403
|
if (before) {
|
|
343
404
|
const backupPath = `${filePath}.marrow-backup`;
|
|
344
|
-
fs.
|
|
405
|
+
if (fs.existsSync(backupPath) && fs.lstatSync(backupPath).isSymbolicLink()) {
|
|
406
|
+
throw new Error('npm token repair backup must not be a symbolic link');
|
|
407
|
+
}
|
|
408
|
+
const backupTemp = path.join(path.resolve(homePath), `.npmrc.marrow-backup-${process.pid}-${crypto.randomBytes(6).toString('hex')}.tmp`);
|
|
409
|
+
fs.writeFileSync(backupTemp, before, { mode: 0o600, flag: 'wx' });
|
|
410
|
+
fs.renameSync(backupTemp, backupPath);
|
|
345
411
|
fs.chmodSync(backupPath, 0o600);
|
|
346
412
|
}
|
|
347
|
-
|
|
348
|
-
fs.chmodSync(filePath, 0o600);
|
|
413
|
+
atomicWriteOwnerFile(homePath, filePath, after);
|
|
349
414
|
}
|
|
350
415
|
return before !== after;
|
|
351
416
|
}
|
|
@@ -355,7 +420,7 @@ function repairConfigDiagnostics(diagnostics, env = process.env) {
|
|
|
355
420
|
const npm = diagnostics.npm_token;
|
|
356
421
|
const repairs = [];
|
|
357
422
|
if (npm?.repairable && inspection.raw.sourceToken) {
|
|
358
|
-
const changed = upsertNpmrcToken(inspection.raw.paths.npmrc, inspection.raw.sourceToken);
|
|
423
|
+
const changed = upsertNpmrcToken(inspection.raw.paths.home, inspection.raw.paths.npmrc, inspection.raw.sourceToken);
|
|
359
424
|
repairs.push({
|
|
360
425
|
type: 'npm_token_npmrc_sync',
|
|
361
426
|
changed,
|
|
@@ -866,10 +931,69 @@ function buildPlan(detection, options) {
|
|
|
866
931
|
});
|
|
867
932
|
}
|
|
868
933
|
|
|
869
|
-
return { mode, writes };
|
|
934
|
+
return { mode, root: detection.root, writes };
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function assertContainedManagedTarget(root, targetPath) {
|
|
938
|
+
const resolvedRoot = path.resolve(root);
|
|
939
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
940
|
+
if (resolvedTarget === resolvedRoot || !resolvedTarget.startsWith(`${resolvedRoot}${path.sep}`)) {
|
|
941
|
+
throw new Error(`Refusing installer write outside project root: ${resolvedTarget}`);
|
|
942
|
+
}
|
|
943
|
+
if (!fs.existsSync(resolvedRoot)) throw new Error(`Project root does not exist: ${resolvedRoot}`);
|
|
944
|
+
const rootStat = fs.lstatSync(resolvedRoot);
|
|
945
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
946
|
+
throw new Error(`Refusing installer write through unsafe project root: ${resolvedRoot}`);
|
|
947
|
+
}
|
|
948
|
+
const realRoot = fs.realpathSync(resolvedRoot);
|
|
949
|
+
const relativeParent = path.relative(resolvedRoot, path.dirname(resolvedTarget));
|
|
950
|
+
let current = resolvedRoot;
|
|
951
|
+
for (const segment of relativeParent.split(path.sep).filter(Boolean)) {
|
|
952
|
+
current = path.join(current, segment);
|
|
953
|
+
if (!fs.existsSync(current)) break;
|
|
954
|
+
const stat = fs.lstatSync(current);
|
|
955
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
956
|
+
throw new Error(`Refusing installer write through unsafe path component: ${current}`);
|
|
957
|
+
}
|
|
958
|
+
const realCurrent = fs.realpathSync(current);
|
|
959
|
+
if (realCurrent !== realRoot && !realCurrent.startsWith(`${realRoot}${path.sep}`)) {
|
|
960
|
+
throw new Error(`Refusing installer write outside resolved project root: ${current}`);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
if (fs.existsSync(resolvedTarget)) {
|
|
964
|
+
const targetStat = fs.lstatSync(resolvedTarget);
|
|
965
|
+
if (targetStat.isSymbolicLink() || !targetStat.isFile()) {
|
|
966
|
+
throw new Error(`Refusing installer write to unsafe managed target: ${resolvedTarget}`);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return { resolvedRoot, resolvedTarget };
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function atomicWriteManagedFile(root, targetPath, contents) {
|
|
973
|
+
const { resolvedTarget } = assertContainedManagedTarget(root, targetPath);
|
|
974
|
+
const parent = path.dirname(resolvedTarget);
|
|
975
|
+
fs.mkdirSync(parent, { recursive: true, mode: 0o700 });
|
|
976
|
+
assertContainedManagedTarget(root, resolvedTarget);
|
|
977
|
+
const existingMode = fs.existsSync(resolvedTarget)
|
|
978
|
+
? fs.lstatSync(resolvedTarget).mode & 0o777
|
|
979
|
+
: 0o600;
|
|
980
|
+
const tempPath = path.join(
|
|
981
|
+
parent,
|
|
982
|
+
`.${path.basename(resolvedTarget)}.marrow-${process.pid}-${crypto.randomBytes(6).toString('hex')}`,
|
|
983
|
+
);
|
|
984
|
+
try {
|
|
985
|
+
fs.writeFileSync(tempPath, contents, { flag: 'wx', mode: existingMode });
|
|
986
|
+
assertContainedManagedTarget(root, resolvedTarget);
|
|
987
|
+
fs.renameSync(tempPath, resolvedTarget);
|
|
988
|
+
} finally {
|
|
989
|
+
if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
|
|
990
|
+
}
|
|
870
991
|
}
|
|
871
992
|
|
|
872
993
|
function applyPlan(plan, options) {
|
|
994
|
+
if (!Array.isArray(plan?.writes) || plan.writes.length === 0) return [];
|
|
995
|
+
const root = path.resolve(plan.root || path.dirname(plan.writes[0].path));
|
|
996
|
+
for (const write of plan.writes) assertContainedManagedTarget(root, write.path);
|
|
873
997
|
const prepared = plan.writes.map((write) => {
|
|
874
998
|
const before = safeRead(write.path);
|
|
875
999
|
let after;
|
|
@@ -902,8 +1026,7 @@ function applyPlan(plan, options) {
|
|
|
902
1026
|
already_present: !changed,
|
|
903
1027
|
});
|
|
904
1028
|
if (changed && writeApplied) {
|
|
905
|
-
|
|
906
|
-
fs.writeFileSync(write.path, after);
|
|
1029
|
+
atomicWriteManagedFile(root, write.path, after);
|
|
907
1030
|
}
|
|
908
1031
|
}
|
|
909
1032
|
return changes;
|
|
@@ -1149,8 +1272,13 @@ async function runSelfTest(options) {
|
|
|
1149
1272
|
avoided_mistakes: performance.avoided_mistakes ?? performance.avoided_repeated_mistakes ?? 0,
|
|
1150
1273
|
reused_winning_decisions: performance.reused_winning_decisions ?? 0,
|
|
1151
1274
|
prevented_bad_actions: performance.prevented_bad_actions ?? 0,
|
|
1152
|
-
estimated_tokens_saved:
|
|
1153
|
-
estimated_minutes_saved:
|
|
1275
|
+
estimated_tokens_saved: tokenValueProof?.savings?.estimated_tokens_saved ?? null,
|
|
1276
|
+
estimated_minutes_saved: tokenValueProof?.savings?.estimated_minutes_saved ?? null,
|
|
1277
|
+
token_savings_available: Number(tokenValueProof?.observed?.model_calls || 0) > 0
|
|
1278
|
+
&& Number(tokenValueProof?.savings?.estimated_tokens_saved || 0) > 0,
|
|
1279
|
+
token_savings_source: 'agent_model_usage_events',
|
|
1280
|
+
token_savings_method: tokenValueProof?.savings?.method || 'warming_up',
|
|
1281
|
+
token_savings_confidence: tokenValueProof?.savings?.confidence || 'none',
|
|
1154
1282
|
reliability_score: performance.agent_reliability_score ?? null,
|
|
1155
1283
|
} : null,
|
|
1156
1284
|
};
|
|
@@ -1173,10 +1301,22 @@ function buildTokenValueProof(valueProof = {}) {
|
|
|
1173
1301
|
return modelUsage;
|
|
1174
1302
|
}
|
|
1175
1303
|
|
|
1304
|
+
function tokenValueProofLine(tokenValueProof) {
|
|
1305
|
+
const calls = Number(tokenValueProof?.observed?.model_calls || 0);
|
|
1306
|
+
const saved = Number(tokenValueProof?.savings?.estimated_tokens_saved || 0);
|
|
1307
|
+
if (calls > 0 && saved > 0) {
|
|
1308
|
+
const method = tokenValueProof.savings?.method || 'unspecified';
|
|
1309
|
+
const confidence = tokenValueProof.savings?.confidence || 'unknown';
|
|
1310
|
+
return `Marrow observed ${calls} model call${calls === 1 ? '' : 's'} and estimates ~${saved} tokens saved (${method}, ${confidence} confidence)`;
|
|
1311
|
+
}
|
|
1312
|
+
return tokenValueProof?.proof_line || null;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1176
1315
|
function buildInstallValueMoment(firstValueSignal = {}, status = {}, runtime = {}, performance = {}, firstValue = {}, tokenValueProof = null) {
|
|
1177
1316
|
if (firstValue && firstValue.ok !== false && firstValue.first_value) {
|
|
1178
1317
|
const proof = Array.isArray(firstValue.first_value.proof) ? [...firstValue.first_value.proof] : [];
|
|
1179
|
-
|
|
1318
|
+
const tokenProofLine = tokenValueProofLine(tokenValueProof);
|
|
1319
|
+
if (tokenProofLine && !proof.includes(tokenProofLine)) proof.push(tokenProofLine);
|
|
1180
1320
|
return {
|
|
1181
1321
|
headline: firstValue.headline || firstValue.first_value.headline || 'Your agent is no longer starting from zero.',
|
|
1182
1322
|
proof,
|
|
@@ -1201,7 +1341,7 @@ function buildInstallValueMoment(firstValueSignal = {}, status = {}, runtime = {
|
|
|
1201
1341
|
'Closed the outcome successfully',
|
|
1202
1342
|
'Runtime gate is ' + (firstValueSignal.active ? 'active' : 'installed'),
|
|
1203
1343
|
runtimeLesson ? 'Future risky work now gets a pre-action brief' : 'Future risky work now gets checked before action',
|
|
1204
|
-
tokenValueProof
|
|
1344
|
+
tokenValueProofLine(tokenValueProof) || 'Token usage proof is active and warming up after the first model call',
|
|
1205
1345
|
],
|
|
1206
1346
|
fleet_signal: hasFleetSignal
|
|
1207
1347
|
? 'Marrow already found signal: ' + proof.join('; ') + '.'
|
|
@@ -1220,9 +1360,8 @@ function buildFirstValueSignal(status, runtime, performance, firstValue = {}, to
|
|
|
1220
1360
|
if (Number(proof.avoided_mistakes || 0) > 0) proofBits.push(`${proof.avoided_mistakes} avoided mistake(s)`);
|
|
1221
1361
|
if (Number(proof.reused_winning_decisions || 0) > 0) proofBits.push(`${proof.reused_winning_decisions} reused winning decision(s)`);
|
|
1222
1362
|
if (Number(proof.prevented_bad_actions || 0) > 0) proofBits.push(`${proof.prevented_bad_actions} prevented risky action(s)`);
|
|
1223
|
-
|
|
1224
|
-
if (
|
|
1225
|
-
else if (tokenValueProof?.proof_line) proofBits.push(tokenValueProof.proof_line);
|
|
1363
|
+
const tokenProofLine = tokenValueProofLine(tokenValueProof);
|
|
1364
|
+
if (tokenProofLine) proofBits.push(tokenProofLine);
|
|
1226
1365
|
return {
|
|
1227
1366
|
active: Boolean(firstValue.active),
|
|
1228
1367
|
headline: `Marrow active: ${(capture.surfaces || ['decisions']).join(', ')} captured.`,
|
|
@@ -1248,10 +1387,8 @@ function buildFirstValueSignal(status, runtime, performance, firstValue = {}, to
|
|
|
1248
1387
|
if (Number(proof.avoided_mistakes || proof.avoided_repeated_mistakes || 0) > 0) proofBits.push(`${proof.avoided_mistakes || proof.avoided_repeated_mistakes} avoided mistake(s)`);
|
|
1249
1388
|
if (Number(proof.reused_winning_decisions || 0) > 0) proofBits.push(`${proof.reused_winning_decisions} reused winning decision(s)`);
|
|
1250
1389
|
if (Number(proof.prevented_bad_actions || 0) > 0) proofBits.push(`${proof.prevented_bad_actions} prevented risky action(s)`);
|
|
1251
|
-
const
|
|
1252
|
-
if (
|
|
1253
|
-
if (Number(tokenValueProof?.savings?.estimated_tokens_saved || 0) > 0) proofBits.push(`~${tokenValueProof.savings.estimated_tokens_saved} measured model tokens saved`);
|
|
1254
|
-
else if (tokenValueProof?.proof_line) proofBits.push(tokenValueProof.proof_line);
|
|
1390
|
+
const tokenProofLine = tokenValueProofLine(tokenValueProof);
|
|
1391
|
+
if (tokenProofLine) proofBits.push(tokenProofLine);
|
|
1255
1392
|
|
|
1256
1393
|
const firstLesson = runtime.before_you_act
|
|
1257
1394
|
|| runtime.before_you_act_injection?.message
|
|
@@ -1381,6 +1518,11 @@ function printReport(report) {
|
|
|
1381
1518
|
if (report.sdkDependency.warning) process.stdout.write(`- warning: ${report.sdkDependency.warning}\n`);
|
|
1382
1519
|
}
|
|
1383
1520
|
|
|
1521
|
+
process.stdout.write('\nAutomatic controller:\n');
|
|
1522
|
+
process.stdout.write(`- state: ${report.controller?.active ? 'active' : report.controller?.state || 'unavailable'}\n`);
|
|
1523
|
+
if (report.controller?.started_at) process.stdout.write(`- started: ${report.controller.started_at}\n`);
|
|
1524
|
+
if (report.controller?.exact_fix) process.stdout.write(`- exact fix: ${report.controller.exact_fix}\n`);
|
|
1525
|
+
|
|
1384
1526
|
if (report.writeMode === 'doctor') {
|
|
1385
1527
|
process.stdout.write('\nDoctor:\n');
|
|
1386
1528
|
process.stdout.write(`- Marrow active: ${report.doctor.active ? 'yes' : 'no'}\n`);
|
|
@@ -1459,6 +1601,43 @@ async function install(options) {
|
|
|
1459
1601
|
}
|
|
1460
1602
|
const changedConfig = changes.some((change) => change.applied) || configRepairs.some((repair) => repair.changed);
|
|
1461
1603
|
const selfTestPassed = Boolean(!selfTest.skipped && selfTest.active && !selfTest.error);
|
|
1604
|
+
const controllerPlatform = options.controllerPlatform || process.platform;
|
|
1605
|
+
let controller = await controllerStatus({
|
|
1606
|
+
root: detection.root,
|
|
1607
|
+
agentId: options.agentId,
|
|
1608
|
+
platform: controllerPlatform,
|
|
1609
|
+
});
|
|
1610
|
+
const shouldEnsureController = options.controller !== false
|
|
1611
|
+
&& controllerSupportedPlatform(controllerPlatform)
|
|
1612
|
+
&& Boolean(options.apiKey)
|
|
1613
|
+
&& selfTestPassed
|
|
1614
|
+
&& !options.dryRun
|
|
1615
|
+
&& !options.doctor
|
|
1616
|
+
&& (options.yes || options.activate || options.repair);
|
|
1617
|
+
if (shouldEnsureController) {
|
|
1618
|
+
try {
|
|
1619
|
+
controller = await ensureGovernanceController({
|
|
1620
|
+
apiKey: options.apiKey,
|
|
1621
|
+
baseUrl: options.baseUrl,
|
|
1622
|
+
agentId: options.agentId,
|
|
1623
|
+
client,
|
|
1624
|
+
root: detection.root,
|
|
1625
|
+
mode: plan.mode,
|
|
1626
|
+
profile: options.governanceMode || 'default',
|
|
1627
|
+
policy: options.governancePolicy || 'warn',
|
|
1628
|
+
platform: controllerPlatform,
|
|
1629
|
+
});
|
|
1630
|
+
} catch (error) {
|
|
1631
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1632
|
+
controller = {
|
|
1633
|
+
active: false,
|
|
1634
|
+
state: 'error',
|
|
1635
|
+
exact_fix: 'Run npx @getmarrow/install controller ensure after correcting the reported local controller error.',
|
|
1636
|
+
error: message,
|
|
1637
|
+
};
|
|
1638
|
+
if (options.activate) throw new Error(`Marrow activation failed: local controller did not start: ${message}`);
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1462
1641
|
const remediation = options.repair
|
|
1463
1642
|
? {
|
|
1464
1643
|
attempted: true,
|
|
@@ -1500,16 +1679,17 @@ async function install(options) {
|
|
|
1500
1679
|
missingEnv: options.apiKey ? [] : ['MARROW_API_KEY'],
|
|
1501
1680
|
envHints,
|
|
1502
1681
|
missingHooks: changes.filter((change) => change.changed).map((change) => change.label),
|
|
1503
|
-
recommendedFix: configDiagnostics.npm_token.recommended_fix ||
|
|
1682
|
+
recommendedFix: configDiagnostics.npm_token.recommended_fix || (!options.apiKey
|
|
1504
1683
|
? envHints.length
|
|
1505
1684
|
? `MARROW_API_KEY was found in a likely env file at ${envHints[0]}. Load that key from trusted secret storage, export only MARROW_API_KEY, then run npx @getmarrow/install --repair.`
|
|
1506
1685
|
: 'Set MARROW_API_KEY, then run npx @getmarrow/install --repair.'
|
|
1507
|
-
: null),
|
|
1686
|
+
: controller.exact_fix || selfTest.recommended_fix || null),
|
|
1508
1687
|
},
|
|
1509
1688
|
remediation,
|
|
1510
1689
|
configDiagnostics,
|
|
1511
1690
|
configRepairs,
|
|
1512
1691
|
sdkDependency,
|
|
1692
|
+
controller,
|
|
1513
1693
|
selfTest,
|
|
1514
1694
|
warnings: options.keyFromArg
|
|
1515
1695
|
? ['Avoid --key in shared shells because command-line arguments can be visible in process listings. Prefer MARROW_API_KEY in your environment or secret manager.']
|