@getmarrow/install 0.1.33 → 0.1.34
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 +17 -4
- package/package.json +1 -1
- package/src/governed-runner.js +66 -6
- package/src/installer.js +413 -30
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ Verify current claims through the [public evidence manifest](https://getmarrow.a
|
|
|
53
53
|
## Install
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
|
-
npx @getmarrow/install
|
|
56
|
+
npx @getmarrow/install activate
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
Required secret:
|
|
@@ -62,9 +62,19 @@ Required secret:
|
|
|
62
62
|
export MARROW_API_KEY=mrw_live_...
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
## What's New in v0.1.
|
|
65
|
+
## What's New in v0.1.34
|
|
66
|
+
|
|
67
|
+
v0.1.34 verifies whether passive governance is actually active after install. Activation now registers a bounded capability profile, a one-way configuration fingerprint, expected and observed hook surfaces, and a server-accepted lifecycle receipt. The Fleet Operator shows activation state, capture coverage, outcome closure, intervention follow-through, drift, and the exact repair:
|
|
66
68
|
|
|
67
|
-
|
|
69
|
+
- Claude Code installation includes exact `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, and `Stop` hooks;
|
|
70
|
+
- matching pre-action/result receipts use one tool correlation, and activation fingerprints the exact hook contract without uploading configuration contents;
|
|
71
|
+
- the capability registry distinguishes native hooks, MCP, SDK passive runtime, governed wrappers, and custom event contracts;
|
|
72
|
+
- `activate` fails when the local integration is incomplete or the server does not accept the exact activation profile;
|
|
73
|
+
- `doctor` and `--repair` use the same configuration evidence without exposing configuration contents;
|
|
74
|
+
- the harness certification suite prevents support claims from overstating what is automatic;
|
|
75
|
+
- unavailable coverage remains “insufficient data” instead of a synthetic percentage.
|
|
76
|
+
|
|
77
|
+
It preserves agent-disagreement visibility from v0.1.33 and the server-verified first-run activation introduced in v0.1.29:
|
|
68
78
|
|
|
69
79
|
- GitHub and npm now advertise separate signed discovery placements;
|
|
70
80
|
- package metadata identifies the installer as agent governance rather than a general memory utility;
|
|
@@ -104,10 +114,13 @@ With a valid key, `activate`:
|
|
|
104
114
|
4. closes its outcome;
|
|
105
115
|
5. sends the exact self-test decision ID to Marrow for server-side verification;
|
|
106
116
|
6. reads agent status and the one-call runtime;
|
|
107
|
-
7.
|
|
117
|
+
7. registers the detected capability, expected hooks, and one-way configuration fingerprint;
|
|
118
|
+
8. returns an activation receipt with capture, intervention, closure, first-value state, and the exact next action.
|
|
108
119
|
|
|
109
120
|
Healthy output confirms the exact decision outcome exists under the authenticated account and agent. A local file write or client-supplied `verified: true` value cannot produce an active receipt.
|
|
110
121
|
|
|
122
|
+
The installer does not claim identical automation for every harness. Native hooks provide the broadest automatic coverage; MCP covers MCP-routed actions; the SDK covers owned Node processes; the governed runner covers commands launched through it; custom harnesses must map their own lifecycle events.
|
|
123
|
+
|
|
111
124
|
## Govern TUI
|
|
112
125
|
|
|
113
126
|
Open the interactive setup panel:
|
package/package.json
CHANGED
package/src/governed-runner.js
CHANGED
|
@@ -806,6 +806,9 @@ function normalizeFixCommands(status, capacity) {
|
|
|
806
806
|
add(status.route_contract?.exact_fix);
|
|
807
807
|
add(status.auto_outcome_closure?.exact_fix);
|
|
808
808
|
add(status.capture_coverage?.exact_fix);
|
|
809
|
+
add(status.activation_coverage?.exact_fix);
|
|
810
|
+
add(status.activation_coverage?.drift?.repair_command, true);
|
|
811
|
+
add(status.passive_activation?.exact_fix);
|
|
809
812
|
add(capacity.exact_next_action, true);
|
|
810
813
|
add(capacity.next_action, true);
|
|
811
814
|
if (listValue(status.missed_hooks, status.degraded_hooks).length) add('npx @getmarrow/install --repair');
|
|
@@ -813,6 +816,52 @@ function normalizeFixCommands(status, capacity) {
|
|
|
813
816
|
return commands.slice(0, 6);
|
|
814
817
|
}
|
|
815
818
|
|
|
819
|
+
function normalizeActivationCoverage(status, report, fleet) {
|
|
820
|
+
const source = firstDefined(
|
|
821
|
+
status.activation_coverage,
|
|
822
|
+
status.passive_activation,
|
|
823
|
+
report.activation_coverage,
|
|
824
|
+
report.passive_activation,
|
|
825
|
+
fleet.activation_coverage,
|
|
826
|
+
{},
|
|
827
|
+
) || {};
|
|
828
|
+
const activation = source.activation || {};
|
|
829
|
+
const capture = source.capture_coverage || source.capture || source.coverage || {};
|
|
830
|
+
const closure = source.outcome_closure || source.closure || {};
|
|
831
|
+
const effectiveness = source.intervention_effectiveness || source.effectiveness || {};
|
|
832
|
+
const drift = source.drift && typeof source.drift === 'object' ? source.drift : {};
|
|
833
|
+
const available = source.available === true || capture.available === true;
|
|
834
|
+
const driftAvailable = available && drift.available === true && typeof drift.detected === 'boolean';
|
|
835
|
+
const percent = (value) => {
|
|
836
|
+
const number = Number(value);
|
|
837
|
+
if (!Number.isFinite(number)) return null;
|
|
838
|
+
return Math.max(0, Math.min(100, number));
|
|
839
|
+
};
|
|
840
|
+
const ratio = (value) => {
|
|
841
|
+
const number = Number(value);
|
|
842
|
+
if (!Number.isFinite(number)) return null;
|
|
843
|
+
return Math.max(0, Math.min(100, number * 100));
|
|
844
|
+
};
|
|
845
|
+
const metricPercent = (explicitPercent, rate, fallbackPercent) => {
|
|
846
|
+
const explicit = firstDefined(explicitPercent, fallbackPercent);
|
|
847
|
+
return explicit == null ? ratio(rate) : percent(explicit);
|
|
848
|
+
};
|
|
849
|
+
return {
|
|
850
|
+
available,
|
|
851
|
+
state: statusLabel(firstDefined(source.state, source.status), available ? 'active' : 'warming_up'),
|
|
852
|
+
capability_level: displayText(firstDefined(activation.capability_level, source.capability_level, source.capability, 'unknown'), 40),
|
|
853
|
+
capture_percent: metricPercent(capture.percent, capture.rate, source.capture_percent),
|
|
854
|
+
closure_percent: metricPercent(closure.percent, closure.rate, source.closure_percent),
|
|
855
|
+
effectiveness_percent: metricPercent(
|
|
856
|
+
effectiveness.followed_percent,
|
|
857
|
+
firstDefined(effectiveness.follow_through_rate, effectiveness.rate),
|
|
858
|
+
source.effectiveness_percent,
|
|
859
|
+
),
|
|
860
|
+
drift: driftAvailable ? drift.detected : null,
|
|
861
|
+
exact_fix: displayText(firstDefined(drift.repair_command, source.exact_fix, source.repair_command, ''), 180),
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
|
|
816
865
|
function normalizeFleetSnapshot(raw, options) {
|
|
817
866
|
const status = raw.status?.data || {};
|
|
818
867
|
const capacity = raw.capacity?.data || {};
|
|
@@ -873,6 +922,7 @@ function normalizeFleetSnapshot(raw, options) {
|
|
|
873
922
|
const recentDecisions = normalizeRecentDecisions(status, report, fleet);
|
|
874
923
|
const gates = normalizeGates(status, report);
|
|
875
924
|
const arbitrations = normalizeArbitrations(status, report, fleet);
|
|
925
|
+
const activationCoverage = normalizeActivationCoverage(status, report, fleet);
|
|
876
926
|
const fixCommands = normalizeFixCommands(status, capacity);
|
|
877
927
|
const errors = Object.entries(raw)
|
|
878
928
|
.filter(([, value]) => value && value.ok === false)
|
|
@@ -893,6 +943,7 @@ function normalizeFleetSnapshot(raw, options) {
|
|
|
893
943
|
degraded_hooks: missedHooks,
|
|
894
944
|
gates,
|
|
895
945
|
arbitrations,
|
|
946
|
+
activation_coverage: activationCoverage,
|
|
896
947
|
agents,
|
|
897
948
|
fix_commands: fixCommands.length ? fixCommands : ['npx @getmarrow/install doctor'],
|
|
898
949
|
source_errors: errors,
|
|
@@ -944,6 +995,8 @@ function fleetPanel(snapshot) {
|
|
|
944
995
|
'',
|
|
945
996
|
`Live agents: ${snapshot.live_agents ?? 'unknown'}`,
|
|
946
997
|
`Active workflows: ${snapshot.active_workflows ?? 'unknown'}`,
|
|
998
|
+
`Passive activation: ${snapshot.activation_coverage.state} capability=${snapshot.activation_coverage.capability_level}`,
|
|
999
|
+
`Passive coverage: ${snapshot.activation_coverage.capture_percent ?? 'insufficient data'}${snapshot.activation_coverage.capture_percent == null ? '' : '%'}; outcome closure=${snapshot.activation_coverage.closure_percent ?? 'insufficient data'}${snapshot.activation_coverage.closure_percent == null ? '' : '%'}; intervention follow-through=${snapshot.activation_coverage.effectiveness_percent ?? 'insufficient data'}${snapshot.activation_coverage.effectiveness_percent == null ? '' : '%'}`,
|
|
947
1000
|
`Agent disagreements: open=${snapshot.arbitrations.open_count} review_required=${snapshot.arbitrations.review_required_count}`,
|
|
948
1001
|
`Latest arbitration: ${arbitrationSummary}`,
|
|
949
1002
|
`Risky actions waiting for proof: ${snapshot.proof_waiting}`,
|
|
@@ -1361,6 +1414,10 @@ function renderFleetTui(state) {
|
|
|
1361
1414
|
const snapshot = state.snapshot;
|
|
1362
1415
|
const selectedAgent = snapshot.agents[state.agentIndex] || { id: 'no agent returned', status: 'unknown', role: '' };
|
|
1363
1416
|
const degraded = snapshot.degraded_hooks.length ? snapshot.degraded_hooks.join(', ') : 'none';
|
|
1417
|
+
const activation = snapshot.activation_coverage;
|
|
1418
|
+
const coverageValue = activation.available
|
|
1419
|
+
? `${activation.state}; capture=${activation.capture_percent ?? 'n/a'}%; closure=${activation.closure_percent ?? 'n/a'}%`
|
|
1420
|
+
: `${activation.state}; insufficient data`;
|
|
1364
1421
|
const gateSummary = `deploy=${snapshot.gates.deploy} publish=${snapshot.gates.publish} merge=${snapshot.gates.merge}`;
|
|
1365
1422
|
const fixCommand = snapshot.fix_commands[0] || 'npx @getmarrow/install doctor';
|
|
1366
1423
|
const recent = snapshot.recent_decisions[0] || 'none reported yet';
|
|
@@ -1404,9 +1461,13 @@ function renderFleetTui(state) {
|
|
|
1404
1461
|
hint: 'Latest fleet decision signal returned by Marrow.',
|
|
1405
1462
|
},
|
|
1406
1463
|
{
|
|
1407
|
-
label: '
|
|
1408
|
-
value:
|
|
1409
|
-
hint:
|
|
1464
|
+
label: 'Passive activation / coverage',
|
|
1465
|
+
value: coverageValue,
|
|
1466
|
+
hint: activation.drift
|
|
1467
|
+
? `Configuration drift detected. ${activation.exact_fix || fixCommand}`
|
|
1468
|
+
: degraded === 'none'
|
|
1469
|
+
? `Capability=${activation.capability_level}; no missing hooks reported.`
|
|
1470
|
+
: `Degraded hooks: ${degraded}. Repair before trusting passive coverage.`,
|
|
1410
1471
|
},
|
|
1411
1472
|
{
|
|
1412
1473
|
label: 'Deploy/publish/merge gates',
|
|
@@ -1725,9 +1786,8 @@ async function runFleetInteractive(options, input = process.stdin, output = proc
|
|
|
1725
1786
|
? state.snapshot.recent_decisions.map((decision, index) => `${index + 1}. ${decision}`).join(' ')
|
|
1726
1787
|
: 'No recent decisions returned yet.';
|
|
1727
1788
|
} else if (state.cursor === 7) {
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
: 'No degraded hooks reported.';
|
|
1789
|
+
const coverage = state.snapshot.activation_coverage;
|
|
1790
|
+
state.lastResult = `Passive activation=${coverage.state}; capability=${coverage.capability_level}; capture=${coverage.capture_percent ?? 'insufficient data'}; closure=${coverage.closure_percent ?? 'insufficient data'}; intervention follow-through=${coverage.effectiveness_percent ?? 'insufficient data'}${coverage.drift ? `. Drift detected. Fix: ${coverage.exact_fix || state.snapshot.fix_commands[0]}` : ''}`;
|
|
1731
1791
|
} else if (state.cursor === 8) {
|
|
1732
1792
|
state.lastResult = `Release gates: deploy=${state.snapshot.gates.deploy}, publish=${state.snapshot.gates.publish}, merge=${state.snapshot.gates.merge}.`;
|
|
1733
1793
|
} else {
|
package/src/installer.js
CHANGED
|
@@ -6,7 +6,38 @@ const crypto = require('node:crypto');
|
|
|
6
6
|
const DEFAULT_BASE_URL = 'https://api.getmarrow.ai';
|
|
7
7
|
const MARROW_BLOCK_START = '<!-- marrow:passive-start -->';
|
|
8
8
|
const MARROW_BLOCK_END = '<!-- marrow:passive-end -->';
|
|
9
|
-
const
|
|
9
|
+
const INSTALLER_ADAPTER_VERSION = '0.1.34';
|
|
10
|
+
const MCP_ADAPTER_VERSION = '3.9.50';
|
|
11
|
+
const SDK_ADAPTER_VERSION = '3.7.49';
|
|
12
|
+
const SDK_ADAPTER_INTEGRITY = 'sha512-9a6pWWACubWTmulG7TwwgMNP/iSzoEh4wCH2oWUyO8XHeD2Eokj1GXrkCyX86md3bV7vGX6kyllfswZDlGj6WA==';
|
|
13
|
+
const SDK_ADAPTER_TARBALL = `https://registry.npmjs.org/@getmarrow/sdk/-/sdk-${SDK_ADAPTER_VERSION}.tgz`;
|
|
14
|
+
const MCP_PACKAGE_SPEC = `@getmarrow/mcp@${MCP_ADAPTER_VERSION}`;
|
|
15
|
+
const MCP_CONTEXT_HOOK_COMMAND = `npx -y ${MCP_PACKAGE_SPEC} context-hook`;
|
|
16
|
+
const MCP_PRE_ACTION_HOOK_COMMAND = `npx -y ${MCP_PACKAGE_SPEC} pre-action-hook`;
|
|
17
|
+
const MCP_ACTION_RESULT_HOOK_COMMAND = `npx -y ${MCP_PACKAGE_SPEC} hook`;
|
|
18
|
+
const MCP_SESSION_END_HOOK_COMMAND = `npx -y ${MCP_PACKAGE_SPEC} session-hook`;
|
|
19
|
+
const NATIVE_HOOK_MATCHER = 'Bash|Edit|Write|MultiEdit|mcp__(?!marrow_).*';
|
|
20
|
+
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 HARNESS_CAPABILITY_REGISTRY = Object.freeze([
|
|
23
|
+
{ client: 'claude-code', capability_level: 'native_hooks', automatic: ['prompt', 'pre_action', 'action_result', 'session_end'], install_surface: 'mcp' },
|
|
24
|
+
{ client: 'cursor', capability_level: 'mcp', automatic: ['mcp_tool_calls'], install_surface: 'mcp' },
|
|
25
|
+
{ client: 'composer', capability_level: 'mcp', automatic: ['mcp_tool_calls'], install_surface: 'mcp' },
|
|
26
|
+
{ client: 'cline', capability_level: 'mcp', automatic: ['mcp_tool_calls'], install_surface: 'mcp' },
|
|
27
|
+
{ client: 'windsurf', capability_level: 'mcp', automatic: ['mcp_tool_calls'], install_surface: 'mcp' },
|
|
28
|
+
{ client: 'codex', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
|
|
29
|
+
{ client: 'opencode', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
|
|
30
|
+
{ client: 'hermes', capability_level: 'event_contract', automatic: [], install_surface: 'addon' },
|
|
31
|
+
{ client: 'openclaw', capability_level: 'event_contract', automatic: [], install_surface: 'addon' },
|
|
32
|
+
{ client: 'gemini', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
|
|
33
|
+
{ client: 'grok', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
|
|
34
|
+
{ client: 'deepseek', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
|
|
35
|
+
{ client: 'qwen', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
|
|
36
|
+
{ client: 'kimi', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
|
|
37
|
+
{ client: 'minimax', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
|
|
38
|
+
{ client: 'glm', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
|
|
39
|
+
{ client: 'custom', capability_level: 'event_contract', automatic: [], install_surface: 'event_contract' },
|
|
40
|
+
]);
|
|
10
41
|
|
|
11
42
|
function sourceClient() {
|
|
12
43
|
const raw = String(process.env.MARROW_CLIENT || process.env.MARROW_HARNESS || process.env.MARROW_AGENT_CLIENT || '').trim().toLowerCase().replace(/\s+/g, '-').replace(/^@/, '');
|
|
@@ -15,6 +46,7 @@ function sourceClient() {
|
|
|
15
46
|
claude_code: 'claude-code',
|
|
16
47
|
'claude-code': 'claude-code',
|
|
17
48
|
cursor: 'cursor',
|
|
49
|
+
composer: 'composer',
|
|
18
50
|
windsurf: 'windsurf',
|
|
19
51
|
openclaw: 'openclaw',
|
|
20
52
|
codex: 'codex',
|
|
@@ -422,38 +454,245 @@ function upsertBlock(content, block) {
|
|
|
422
454
|
return `${content}${separator}${block}\n`;
|
|
423
455
|
}
|
|
424
456
|
|
|
425
|
-
function
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
457
|
+
function exactHookConfigured(settings, eventName, command, matcher) {
|
|
458
|
+
const entries = settings?.hooks?.[eventName];
|
|
459
|
+
if (!Array.isArray(entries)) return false;
|
|
460
|
+
return entries.some((entry) => {
|
|
461
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return false;
|
|
462
|
+
if (matcher != null && entry.matcher !== matcher) return false;
|
|
463
|
+
if (!Array.isArray(entry.hooks)) return false;
|
|
464
|
+
return entry.hooks.some((hook) => (
|
|
465
|
+
hook
|
|
466
|
+
&& typeof hook === 'object'
|
|
467
|
+
&& !Array.isArray(hook)
|
|
468
|
+
&& hook.type === 'command'
|
|
469
|
+
&& typeof hook.command === 'string'
|
|
470
|
+
&& hook.command.trim() === command
|
|
471
|
+
));
|
|
472
|
+
});
|
|
473
|
+
}
|
|
435
474
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
475
|
+
function exactHookDescriptors(settings, eventName, command, matcher) {
|
|
476
|
+
const entries = settings?.hooks?.[eventName];
|
|
477
|
+
if (!Array.isArray(entries)) return [];
|
|
478
|
+
return entries.flatMap((entry) => {
|
|
479
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return [];
|
|
480
|
+
if (matcher != null && entry.matcher !== matcher) return [];
|
|
481
|
+
if (!Array.isArray(entry.hooks)) return [];
|
|
482
|
+
return entry.hooks.flatMap((hook) => {
|
|
483
|
+
if (!hook || typeof hook !== 'object' || Array.isArray(hook)
|
|
484
|
+
|| hook.type !== 'command' || typeof hook.command !== 'string'
|
|
485
|
+
|| hook.command.trim() !== command) return [];
|
|
486
|
+
return [{
|
|
487
|
+
matcher: typeof entry.matcher === 'string' ? entry.matcher : null,
|
|
488
|
+
command,
|
|
489
|
+
timeout: typeof hook.timeout === 'number' && Number.isFinite(hook.timeout)
|
|
490
|
+
? hook.timeout
|
|
491
|
+
: null,
|
|
492
|
+
}];
|
|
440
493
|
});
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function marrowHookSubcommand(command) {
|
|
498
|
+
if (typeof command !== 'string') return null;
|
|
499
|
+
const match = command.trim().match(
|
|
500
|
+
/^npx\s+(?:-y\s+)?@getmarrow\/mcp(?:@[^\s]+)?\s+(context-hook|pre-action-hook|hook|session-hook)$/,
|
|
501
|
+
);
|
|
502
|
+
return match?.[1] || null;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function reconcileMarrowCommandHook(settings, eventName, subcommand, command, matcher) {
|
|
506
|
+
const original = Array.isArray(settings?.hooks?.[eventName]) ? settings.hooks[eventName] : [];
|
|
507
|
+
let preferredHandler = null;
|
|
508
|
+
const retained = [];
|
|
509
|
+
for (const entry of original) {
|
|
510
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry) || !Array.isArray(entry.hooks)) {
|
|
511
|
+
retained.push(entry);
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
const remaining = [];
|
|
515
|
+
for (const hook of entry.hooks) {
|
|
516
|
+
const detected = hook && typeof hook === 'object' && !Array.isArray(hook)
|
|
517
|
+
&& hook.type === 'command' ? marrowHookSubcommand(hook.command) : null;
|
|
518
|
+
if (detected) {
|
|
519
|
+
const exactMatcher = matcher == null ? entry.matcher === undefined : entry.matcher === matcher;
|
|
520
|
+
if (detected === subcommand && (!preferredHandler || (hook.command === command && exactMatcher))) {
|
|
521
|
+
preferredHandler = hook;
|
|
522
|
+
}
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
remaining.push(hook);
|
|
526
|
+
}
|
|
527
|
+
if (remaining.length > 0) retained.push({ ...entry, hooks: remaining });
|
|
441
528
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
529
|
+
const canonical = { hooks: [{ ...(preferredHandler || {}), type: 'command', command }] };
|
|
530
|
+
if (matcher != null) canonical.matcher = matcher;
|
|
531
|
+
retained.push(canonical);
|
|
532
|
+
return retained;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function marrowHookDescriptors(settings, eventName, subcommand) {
|
|
536
|
+
const entries = settings?.hooks?.[eventName];
|
|
537
|
+
if (!Array.isArray(entries)) return [];
|
|
538
|
+
return entries.flatMap((entry) => {
|
|
539
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry) || !Array.isArray(entry.hooks)) return [];
|
|
540
|
+
return entry.hooks.flatMap((hook) => {
|
|
541
|
+
if (!hook || typeof hook !== 'object' || Array.isArray(hook)
|
|
542
|
+
|| hook.type !== 'command') return [];
|
|
543
|
+
const detected = marrowHookSubcommand(hook.command);
|
|
544
|
+
if (!detected || (subcommand && detected !== subcommand)) return [];
|
|
545
|
+
return [{
|
|
546
|
+
matcher: typeof entry.matcher === 'string' ? entry.matcher : null,
|
|
547
|
+
command: hook.command.trim(),
|
|
548
|
+
timeout: typeof hook.timeout === 'number' && Number.isFinite(hook.timeout) ? hook.timeout : null,
|
|
549
|
+
}];
|
|
445
550
|
});
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function safeJsonObject(filePath) {
|
|
555
|
+
try {
|
|
556
|
+
return parseJsonObject(filePath);
|
|
557
|
+
} catch {
|
|
558
|
+
return {};
|
|
446
559
|
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function claudeNativeHookFingerprint(settings) {
|
|
563
|
+
const contract = {
|
|
564
|
+
schema: 'marrow-claude-native-hooks.v3',
|
|
565
|
+
adapter_version: MCP_ADAPTER_VERSION,
|
|
566
|
+
expected_hooks: NATIVE_EXPECTED_HOOKS,
|
|
567
|
+
configured: {
|
|
568
|
+
prompt: exactHookConfigured(settings, 'UserPromptSubmit', MCP_CONTEXT_HOOK_COMMAND),
|
|
569
|
+
pre_action: exactHookConfigured(settings, 'PreToolUse', MCP_PRE_ACTION_HOOK_COMMAND, NATIVE_HOOK_MATCHER),
|
|
570
|
+
action_result_success: exactHookConfigured(settings, 'PostToolUse', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER),
|
|
571
|
+
action_result_failure: exactHookConfigured(settings, 'PostToolUseFailure', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER),
|
|
572
|
+
session_end: exactHookConfigured(settings, 'Stop', MCP_SESSION_END_HOOK_COMMAND),
|
|
573
|
+
},
|
|
574
|
+
descriptors: {
|
|
575
|
+
prompt: exactHookDescriptors(settings, 'UserPromptSubmit', MCP_CONTEXT_HOOK_COMMAND),
|
|
576
|
+
pre_action: exactHookDescriptors(settings, 'PreToolUse', MCP_PRE_ACTION_HOOK_COMMAND, NATIVE_HOOK_MATCHER),
|
|
577
|
+
action_result_success: exactHookDescriptors(settings, 'PostToolUse', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER),
|
|
578
|
+
action_result_failure: exactHookDescriptors(settings, 'PostToolUseFailure', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER),
|
|
579
|
+
session_end: exactHookDescriptors(settings, 'Stop', MCP_SESSION_END_HOOK_COMMAND),
|
|
580
|
+
},
|
|
581
|
+
active_marrow_handlers: {
|
|
582
|
+
prompt: marrowHookDescriptors(settings, 'UserPromptSubmit'),
|
|
583
|
+
pre_action: marrowHookDescriptors(settings, 'PreToolUse'),
|
|
584
|
+
action_result_success: marrowHookDescriptors(settings, 'PostToolUse'),
|
|
585
|
+
action_result_failure: marrowHookDescriptors(settings, 'PostToolUseFailure'),
|
|
586
|
+
session_end: marrowHookDescriptors(settings, 'Stop'),
|
|
587
|
+
},
|
|
588
|
+
};
|
|
589
|
+
return crypto.createHash('sha256').update(JSON.stringify(contract)).digest('hex');
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function upsertClaudeHooks(settingsPath) {
|
|
593
|
+
const settings = parseJsonObject(settingsPath);
|
|
594
|
+
const hooks = settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)
|
|
595
|
+
? settings.hooks
|
|
596
|
+
: {};
|
|
597
|
+
const postToolUse = reconcileMarrowCommandHook(
|
|
598
|
+
settings, 'PostToolUse', 'hook', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER,
|
|
599
|
+
);
|
|
600
|
+
const postToolUseFailure = reconcileMarrowCommandHook(
|
|
601
|
+
settings, 'PostToolUseFailure', 'hook', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER,
|
|
602
|
+
);
|
|
603
|
+
const preToolUse = reconcileMarrowCommandHook(
|
|
604
|
+
settings, 'PreToolUse', 'pre-action-hook', MCP_PRE_ACTION_HOOK_COMMAND, NATIVE_HOOK_MATCHER,
|
|
605
|
+
);
|
|
606
|
+
const userPromptSubmit = reconcileMarrowCommandHook(
|
|
607
|
+
settings, 'UserPromptSubmit', 'context-hook', MCP_CONTEXT_HOOK_COMMAND,
|
|
608
|
+
);
|
|
609
|
+
const stop = reconcileMarrowCommandHook(
|
|
610
|
+
settings, 'Stop', 'session-hook', MCP_SESSION_END_HOOK_COMMAND,
|
|
611
|
+
);
|
|
447
612
|
|
|
448
613
|
settings.hooks = {
|
|
449
614
|
...hooks,
|
|
615
|
+
PreToolUse: preToolUse,
|
|
450
616
|
PostToolUse: postToolUse,
|
|
617
|
+
PostToolUseFailure: postToolUseFailure,
|
|
451
618
|
UserPromptSubmit: userPromptSubmit,
|
|
619
|
+
Stop: stop,
|
|
452
620
|
};
|
|
453
621
|
|
|
454
622
|
return JSON.stringify(settings, null, 2) + '\n';
|
|
455
623
|
}
|
|
456
624
|
|
|
625
|
+
function activationProfile(detection, plan, changes, client) {
|
|
626
|
+
const registry = HARNESS_CAPABILITY_REGISTRY.find((entry) => entry.client === client)
|
|
627
|
+
|| HARNESS_CAPABILITY_REGISTRY.find((entry) => entry.client === 'custom');
|
|
628
|
+
const sdkDependency = inspectSdkDependency(detection);
|
|
629
|
+
const capabilityLevel = client === 'custom' && (plan.mode === 'sdk' || plan.mode === 'both')
|
|
630
|
+
? 'sdk_passive_runtime'
|
|
631
|
+
: registry.capability_level;
|
|
632
|
+
const expectedHooks = capabilityLevel === 'sdk_passive_runtime'
|
|
633
|
+
? ['pre_action', 'action_result', 'outcome_closure']
|
|
634
|
+
: [...registry.automatic];
|
|
635
|
+
const adapterVersion = capabilityLevel === 'native_hooks' || capabilityLevel === 'mcp'
|
|
636
|
+
? MCP_ADAPTER_VERSION
|
|
637
|
+
: capabilityLevel === 'sdk_passive_runtime'
|
|
638
|
+
? SDK_ADAPTER_VERSION
|
|
639
|
+
: INSTALLER_ADAPTER_VERSION;
|
|
640
|
+
const observedHooks = [];
|
|
641
|
+
const claudeSettings = safeJsonObject(detection.paths.claudeSettings);
|
|
642
|
+
if (capabilityLevel === 'native_hooks'
|
|
643
|
+
&& exactHookConfigured(claudeSettings, 'UserPromptSubmit', MCP_CONTEXT_HOOK_COMMAND)) observedHooks.push('prompt');
|
|
644
|
+
if (capabilityLevel === 'native_hooks'
|
|
645
|
+
&& exactHookConfigured(claudeSettings, 'PreToolUse', MCP_PRE_ACTION_HOOK_COMMAND, NATIVE_HOOK_MATCHER)) observedHooks.push('pre_action');
|
|
646
|
+
if (capabilityLevel === 'native_hooks'
|
|
647
|
+
&& exactHookConfigured(claudeSettings, 'PostToolUse', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER)
|
|
648
|
+
&& exactHookConfigured(claudeSettings, 'PostToolUseFailure', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER)) observedHooks.push('action_result');
|
|
649
|
+
if (capabilityLevel === 'native_hooks'
|
|
650
|
+
&& exactHookConfigured(claudeSettings, 'Stop', MCP_SESSION_END_HOOK_COMMAND)) observedHooks.push('session_end');
|
|
651
|
+
const passiveRuntime = safeRead(detection.paths.passiveRuntime);
|
|
652
|
+
if (capabilityLevel === 'sdk_passive_runtime'
|
|
653
|
+
&& sdkDependency.present
|
|
654
|
+
&& /await import\('@getmarrow\/sdk'\)/.test(passiveRuntime)
|
|
655
|
+
&& /runtime\.install\(\)/.test(passiveRuntime)) {
|
|
656
|
+
for (const hook of ['pre_action', 'action_result', 'outcome_closure']) {
|
|
657
|
+
if (!observedHooks.includes(hook)) observedHooks.push(hook);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
const mcpConfigs = [detection.paths.mcpJson, detection.paths.cursorMcp]
|
|
661
|
+
.map((filePath) => safeJsonObject(filePath));
|
|
662
|
+
if (capabilityLevel === 'mcp' && mcpConfigs.some((config) => (
|
|
663
|
+
config?.mcpServers?.marrow?.command === 'npx'
|
|
664
|
+
&& Array.isArray(config.mcpServers.marrow.args)
|
|
665
|
+
&& config.mcpServers.marrow.args.join(' ') === `-y ${MCP_PACKAGE_SPEC}`
|
|
666
|
+
))) observedHooks.push('mcp_tool_calls');
|
|
667
|
+
const fingerprintMaterial = changes
|
|
668
|
+
.filter((change) => change.applied || change.already_present)
|
|
669
|
+
.map((change) => `${change.label}:${crypto.createHash('sha256').update(safeRead(change.path)).digest('hex')}`)
|
|
670
|
+
.sort()
|
|
671
|
+
.join('|');
|
|
672
|
+
const configFingerprint = capabilityLevel === 'native_hooks'
|
|
673
|
+
? claudeNativeHookFingerprint(claudeSettings)
|
|
674
|
+
: crypto.createHash('sha256')
|
|
675
|
+
.update(`${client}:${capabilityLevel}:${expectedHooks.join(',')}:${fingerprintMaterial}`)
|
|
676
|
+
.digest('hex');
|
|
677
|
+
const complete = expectedHooks.length > 0 && expectedHooks.every((hook) => observedHooks.includes(hook));
|
|
678
|
+
const exactFix = complete
|
|
679
|
+
? null
|
|
680
|
+
: capabilityLevel === 'sdk_passive_runtime' && !sdkDependency.present
|
|
681
|
+
? `${sdkDependency.install_command} && npx @getmarrow/install --repair`
|
|
682
|
+
: capabilityLevel === 'governed_wrapper'
|
|
683
|
+
? `npx @getmarrow/install run --agent <agent-id> -- ${client}`
|
|
684
|
+
: 'npx @getmarrow/install --repair';
|
|
685
|
+
return {
|
|
686
|
+
adapter_version: adapterVersion,
|
|
687
|
+
capability_level: capabilityLevel,
|
|
688
|
+
config_fingerprint: configFingerprint,
|
|
689
|
+
expected_hooks: expectedHooks,
|
|
690
|
+
observed_hooks: observedHooks,
|
|
691
|
+
complete,
|
|
692
|
+
exact_fix: exactFix,
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
457
696
|
function upsertMcpServerConfig(filePath) {
|
|
458
697
|
const config = parseJsonObject(filePath);
|
|
459
698
|
const servers = config.mcpServers && typeof config.mcpServers === 'object' && !Array.isArray(config.mcpServers)
|
|
@@ -461,7 +700,7 @@ function upsertMcpServerConfig(filePath) {
|
|
|
461
700
|
: {};
|
|
462
701
|
servers.marrow = {
|
|
463
702
|
command: 'npx',
|
|
464
|
-
args: ['-y',
|
|
703
|
+
args: ['-y', MCP_PACKAGE_SPEC],
|
|
465
704
|
env: {
|
|
466
705
|
MARROW_API_KEY: '${MARROW_API_KEY}',
|
|
467
706
|
MARROW_BASE_URL: '${MARROW_BASE_URL}',
|
|
@@ -485,7 +724,7 @@ function inspectSdkDependency(detection) {
|
|
|
485
724
|
return {
|
|
486
725
|
required: true,
|
|
487
726
|
present: false,
|
|
488
|
-
install_command:
|
|
727
|
+
install_command: `npm install @getmarrow/sdk@${SDK_ADAPTER_VERSION}`,
|
|
489
728
|
warning: 'package.json could not be parsed; verify @getmarrow/sdk manually.',
|
|
490
729
|
};
|
|
491
730
|
}
|
|
@@ -496,11 +735,72 @@ function inspectSdkDependency(detection) {
|
|
|
496
735
|
packageJson.optionalDependencies,
|
|
497
736
|
packageJson.peerDependencies,
|
|
498
737
|
];
|
|
499
|
-
const
|
|
738
|
+
const declaredSpec = dependencyBlocks
|
|
739
|
+
.map((deps) => deps && typeof deps['@getmarrow/sdk'] === 'string' ? deps['@getmarrow/sdk'] : null)
|
|
740
|
+
.find(Boolean) || null;
|
|
741
|
+
const objectTargetsSdk = (value) => {
|
|
742
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
743
|
+
return Object.entries(value).some(([key, nested]) => (
|
|
744
|
+
key.includes('@getmarrow/sdk') || objectTargetsSdk(nested)
|
|
745
|
+
));
|
|
746
|
+
};
|
|
747
|
+
const overrideDetected = objectTargetsSdk(packageJson.overrides)
|
|
748
|
+
|| objectTargetsSdk(packageJson.resolutions)
|
|
749
|
+
|| objectTargetsSdk(packageJson.pnpm?.overrides);
|
|
750
|
+
let lockVerified = false;
|
|
751
|
+
try {
|
|
752
|
+
const lock = JSON.parse(safeRead(path.join(detection.root, 'package-lock.json')) || '{}');
|
|
753
|
+
const rootLock = lock?.packages?.[''];
|
|
754
|
+
const lockedSdk = lock?.packages?.['node_modules/@getmarrow/sdk'];
|
|
755
|
+
const lockedDeclaration = [
|
|
756
|
+
rootLock?.dependencies,
|
|
757
|
+
rootLock?.devDependencies,
|
|
758
|
+
rootLock?.optionalDependencies,
|
|
759
|
+
rootLock?.peerDependencies,
|
|
760
|
+
].map((deps) => deps && typeof deps['@getmarrow/sdk'] === 'string' ? deps['@getmarrow/sdk'] : null)
|
|
761
|
+
.find(Boolean) || null;
|
|
762
|
+
lockVerified = [2, 3].includes(lock?.lockfileVersion)
|
|
763
|
+
&& lockedDeclaration === declaredSpec
|
|
764
|
+
&& lockedSdk?.version === SDK_ADAPTER_VERSION
|
|
765
|
+
&& lockedSdk?.resolved === SDK_ADAPTER_TARBALL
|
|
766
|
+
&& lockedSdk?.integrity === SDK_ADAPTER_INTEGRITY;
|
|
767
|
+
} catch {
|
|
768
|
+
lockVerified = false;
|
|
769
|
+
}
|
|
770
|
+
const installedPackagePath = findUp(
|
|
771
|
+
detection.root,
|
|
772
|
+
[path.join('node_modules', '@getmarrow', 'sdk', 'package.json')],
|
|
773
|
+
);
|
|
774
|
+
let installedVersion = null;
|
|
775
|
+
let installedName = null;
|
|
776
|
+
try {
|
|
777
|
+
const installedPackage = installedPackagePath ? JSON.parse(safeRead(installedPackagePath)) : null;
|
|
778
|
+
installedVersion = typeof installedPackage?.version === 'string' ? installedPackage.version : null;
|
|
779
|
+
installedName = typeof installedPackage?.name === 'string' ? installedPackage.name : null;
|
|
780
|
+
} catch {
|
|
781
|
+
installedVersion = null;
|
|
782
|
+
installedName = null;
|
|
783
|
+
}
|
|
784
|
+
const declarationTrusted = typeof declaredSpec === 'string'
|
|
785
|
+
&& declaredSpec.trim().length > 0
|
|
786
|
+
&& /^[v0-9xX*<>=~^|.\s-]+$/.test(declaredSpec.trim());
|
|
787
|
+
const present = declarationTrusted
|
|
788
|
+
&& !overrideDetected
|
|
789
|
+
&& lockVerified
|
|
790
|
+
&& installedName === '@getmarrow/sdk'
|
|
791
|
+
&& installedVersion === SDK_ADAPTER_VERSION;
|
|
500
792
|
return {
|
|
501
793
|
required: true,
|
|
502
794
|
present,
|
|
503
|
-
|
|
795
|
+
declared: declaredSpec != null,
|
|
796
|
+
declared_spec: declaredSpec,
|
|
797
|
+
declaration_trusted: declarationTrusted,
|
|
798
|
+
override_detected: overrideDetected,
|
|
799
|
+
lock_verified: lockVerified,
|
|
800
|
+
installed_name: installedName,
|
|
801
|
+
installed_version: installedVersion,
|
|
802
|
+
expected_version: SDK_ADAPTER_VERSION,
|
|
803
|
+
install_command: present ? null : `npm install @getmarrow/sdk@${SDK_ADAPTER_VERSION}`,
|
|
504
804
|
};
|
|
505
805
|
}
|
|
506
806
|
|
|
@@ -569,8 +869,7 @@ function buildPlan(detection, options) {
|
|
|
569
869
|
}
|
|
570
870
|
|
|
571
871
|
function applyPlan(plan, options) {
|
|
572
|
-
const
|
|
573
|
-
for (const write of plan.writes) {
|
|
872
|
+
const prepared = plan.writes.map((write) => {
|
|
574
873
|
const before = safeRead(write.path);
|
|
575
874
|
let after;
|
|
576
875
|
if (write.type === 'file') {
|
|
@@ -587,6 +886,11 @@ function applyPlan(plan, options) {
|
|
|
587
886
|
throw new Error(`Unknown write type: ${write.type}`);
|
|
588
887
|
}
|
|
589
888
|
|
|
889
|
+
return { write, before, after };
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
const changes = [];
|
|
893
|
+
for (const { write, before, after } of prepared) {
|
|
590
894
|
const changed = before !== after;
|
|
591
895
|
const writeApplied = Boolean(options.yes && !options.dryRun && !options.doctor);
|
|
592
896
|
changes.push({
|
|
@@ -628,7 +932,6 @@ function isCanonicalTimestamp(value) {
|
|
|
628
932
|
|
|
629
933
|
function runtimeGateVerified(runtime) {
|
|
630
934
|
if (!runtime || typeof runtime !== 'object') return false;
|
|
631
|
-
if (runtime.ok === true) return true;
|
|
632
935
|
const gate = runtime.risk_gate;
|
|
633
936
|
if (!gate || typeof gate !== 'object') return false;
|
|
634
937
|
if (typeof gate.allow === 'boolean' || typeof gate.allowed === 'boolean') return true;
|
|
@@ -733,7 +1036,19 @@ async function runSelfTest(options) {
|
|
|
733
1036
|
const installValueMoment = buildInstallValueMoment(firstValueSignal, status, runtime, performance, firstValue, tokenValueProof);
|
|
734
1037
|
let activationReceipt = null;
|
|
735
1038
|
let activationVerified = false;
|
|
1039
|
+
let activationProfileReceipt = null;
|
|
736
1040
|
if (options.activation) {
|
|
1041
|
+
const activationAdapterVersion = options.activation.adapter_version || INSTALLER_ADAPTER_VERSION;
|
|
1042
|
+
const activationCapabilityLevel = options.activation.capability_level || 'event_contract';
|
|
1043
|
+
const activationExpectedHooks = Array.isArray(options.activation.expected_hooks) ? options.activation.expected_hooks : [];
|
|
1044
|
+
const activationConfigFingerprint = options.activation.config_fingerprint || crypto.createHash('sha256')
|
|
1045
|
+
.update(JSON.stringify({
|
|
1046
|
+
harness: options.activation.harness || options.client || 'custom',
|
|
1047
|
+
install_surface: options.activation.install_surface || 'unknown',
|
|
1048
|
+
adapter_version: activationAdapterVersion,
|
|
1049
|
+
expected_hooks: activationExpectedHooks,
|
|
1050
|
+
}))
|
|
1051
|
+
.digest('hex');
|
|
737
1052
|
activationReceipt = firstValue && firstValue.activation_receipt;
|
|
738
1053
|
const receiptValid = activationReceipt
|
|
739
1054
|
&& typeof activationReceipt === 'object'
|
|
@@ -750,7 +1065,58 @@ async function runSelfTest(options) {
|
|
|
750
1065
|
if (!receiptValid) {
|
|
751
1066
|
throw new Error('activation receipt did not verify the exact self-test decision, agent, runtime gate, and closed successful outcome');
|
|
752
1067
|
}
|
|
753
|
-
|
|
1068
|
+
activationProfileReceipt = await requestJson(`${baseUrl}/v1/agent/integrations/events`, {
|
|
1069
|
+
method: 'POST',
|
|
1070
|
+
headers,
|
|
1071
|
+
body: JSON.stringify({
|
|
1072
|
+
event_id: `activation-${activationConfigFingerprint.slice(0, 32)}`,
|
|
1073
|
+
event_type: 'activation_profile_registered',
|
|
1074
|
+
harness: options.activation.harness,
|
|
1075
|
+
agent_id: options.agentId,
|
|
1076
|
+
session_id: headers['x-marrow-session-id'],
|
|
1077
|
+
adapter_version: activationAdapterVersion,
|
|
1078
|
+
capability_level: activationCapabilityLevel,
|
|
1079
|
+
config_fingerprint: activationConfigFingerprint,
|
|
1080
|
+
expected_hooks: activationExpectedHooks,
|
|
1081
|
+
action: 'passive integration activation profile registered',
|
|
1082
|
+
occurred_at: new Date().toISOString(),
|
|
1083
|
+
}),
|
|
1084
|
+
});
|
|
1085
|
+
const profileCoverage = activationProfileReceipt?.activation_coverage;
|
|
1086
|
+
const profileReceipt = profileCoverage?.profile_receipt;
|
|
1087
|
+
const accountBindingId = profileCoverage?.account_binding_id;
|
|
1088
|
+
const expectedConfigurationBindingId = typeof accountBindingId === 'string'
|
|
1089
|
+
? crypto.createHash('sha256')
|
|
1090
|
+
.update(`agent-config-receipt:v2:${accountBindingId}:${activationConfigFingerprint}`)
|
|
1091
|
+
.digest('hex')
|
|
1092
|
+
: null;
|
|
1093
|
+
const expectedHooksMatch = Array.isArray(profileCoverage?.capture_coverage?.expected_hooks)
|
|
1094
|
+
&& [...profileCoverage.capture_coverage.expected_hooks].sort().join(',') === [...activationExpectedHooks].sort().join(',');
|
|
1095
|
+
const profileBound = Boolean(
|
|
1096
|
+
activationProfileReceipt?.accepted === true
|
|
1097
|
+
&& profileCoverage?.agent_id === options.agentId
|
|
1098
|
+
&& profileCoverage?.harness === options.activation.harness
|
|
1099
|
+
&& profileCoverage?.activation?.adapter_version === activationAdapterVersion
|
|
1100
|
+
&& profileCoverage?.activation?.capability_level === activationCapabilityLevel
|
|
1101
|
+
&& profileReceipt?.agent_id === options.agentId
|
|
1102
|
+
&& profileReceipt?.harness === options.activation.harness
|
|
1103
|
+
&& profileReceipt?.adapter_version === activationAdapterVersion
|
|
1104
|
+
&& profileReceipt?.capability_level === activationCapabilityLevel
|
|
1105
|
+
&& typeof accountBindingId === 'string'
|
|
1106
|
+
&& accountBindingId.length === 64
|
|
1107
|
+
&& profileReceipt?.account_binding_id === accountBindingId
|
|
1108
|
+
&& profileReceipt?.config_fingerprint_verified === true
|
|
1109
|
+
&& profileReceipt?.configuration_binding_id === expectedConfigurationBindingId
|
|
1110
|
+
&& profileReceipt?.expected_hooks_verified === true
|
|
1111
|
+
&& expectedHooksMatch
|
|
1112
|
+
);
|
|
1113
|
+
activationVerified = Boolean(
|
|
1114
|
+
firstValue.active
|
|
1115
|
+
&& runtimeGateVerified(runtime)
|
|
1116
|
+
&& (status.enabled ?? status.ok)
|
|
1117
|
+
&& options.activation.complete === true
|
|
1118
|
+
&& profileBound,
|
|
1119
|
+
);
|
|
754
1120
|
if (!activationVerified) {
|
|
755
1121
|
throw new Error('activation prerequisites were not all verified by the server');
|
|
756
1122
|
}
|
|
@@ -769,6 +1135,8 @@ async function runSelfTest(options) {
|
|
|
769
1135
|
runtime_before_you_act: runtime.before_you_act || null,
|
|
770
1136
|
activation_verified: activationVerified,
|
|
771
1137
|
activation_receipt: activationReceipt,
|
|
1138
|
+
activation_profile_receipt: activationProfileReceipt,
|
|
1139
|
+
activation_coverage: status.activation_coverage || firstValue.activation_coverage || null,
|
|
772
1140
|
first_value: firstValue && firstValue.ok !== false ? firstValue : null,
|
|
773
1141
|
first_value_signal: firstValueSignal,
|
|
774
1142
|
install_value_moment: installValueMoment,
|
|
@@ -916,7 +1284,7 @@ function printReport(report) {
|
|
|
916
1284
|
|
|
917
1285
|
process.stdout.write('\nPlanned changes:\n');
|
|
918
1286
|
for (const change of report.changes) {
|
|
919
|
-
const marker = change.
|
|
1287
|
+
const marker = change.applied ? 'wrote' : change.changed ? 'would write' : 'unchanged';
|
|
920
1288
|
process.stdout.write(`- ${marker}: ${change.label} (${change.path})\n`);
|
|
921
1289
|
}
|
|
922
1290
|
|
|
@@ -1025,6 +1393,9 @@ async function install(options) {
|
|
|
1025
1393
|
if (options.activate && options.selfTest === false) {
|
|
1026
1394
|
throw new Error('activate requires the server self-test');
|
|
1027
1395
|
}
|
|
1396
|
+
if (options.repair && options.yes !== true && !options.dryRun && !options.doctor) {
|
|
1397
|
+
throw new Error('repair requires explicit write authorization (--yes)');
|
|
1398
|
+
}
|
|
1028
1399
|
const detection = detectEnvironment(options.cwd);
|
|
1029
1400
|
const plan = buildPlan(detection, options);
|
|
1030
1401
|
const writeMode = options.doctor ? 'doctor' : options.dryRun ? 'dry-run' : options.repair ? 'repair' : options.yes ? 'write' : 'dry-run';
|
|
@@ -1032,6 +1403,7 @@ async function install(options) {
|
|
|
1032
1403
|
const client = detectedClient(detection);
|
|
1033
1404
|
options.client = client;
|
|
1034
1405
|
if (!options.agentId) options.agentId = stableAgentId(detection.root, client);
|
|
1406
|
+
const profile = activationProfile(detection, plan, changes, client);
|
|
1035
1407
|
options.activation = options.activate ? {
|
|
1036
1408
|
harness: client,
|
|
1037
1409
|
agent_id: options.agentId,
|
|
@@ -1041,14 +1413,20 @@ async function install(options) {
|
|
|
1041
1413
|
.filter((change) => change.changed && change.applied && /hook|runtime|rule|instruction|config/i.test(change.label))
|
|
1042
1414
|
.map((change) => change.label)
|
|
1043
1415
|
.slice(0, 20),
|
|
1044
|
-
capture_verified: changes.every((change) => change.applied),
|
|
1416
|
+
capture_verified: changes.every((change) => change.applied || change.already_present),
|
|
1417
|
+
adapter_version: profile.adapter_version,
|
|
1418
|
+
capability_level: profile.capability_level,
|
|
1419
|
+
config_fingerprint: profile.config_fingerprint,
|
|
1420
|
+
expected_hooks: profile.expected_hooks,
|
|
1421
|
+
observed_hooks: profile.observed_hooks,
|
|
1422
|
+
complete: profile.complete,
|
|
1045
1423
|
intervention_verified: false,
|
|
1046
1424
|
closure_verified: false,
|
|
1047
1425
|
} : null;
|
|
1048
1426
|
const configInspection = inspectNpmTokenConfig();
|
|
1049
1427
|
const sdkDependency = inspectSdkDependency(detection);
|
|
1050
1428
|
const configDiagnostics = configInspection.safe;
|
|
1051
|
-
const configRepairs = options.repair && !options.dryRun && !options.doctor
|
|
1429
|
+
const configRepairs = options.repair && options.yes && !options.dryRun && !options.doctor
|
|
1052
1430
|
? repairConfigDiagnostics(configDiagnostics)
|
|
1053
1431
|
: [];
|
|
1054
1432
|
const envHints = options.apiKey ? [] : findLikelyEnvFiles(detection);
|
|
@@ -1063,7 +1441,7 @@ async function install(options) {
|
|
|
1063
1441
|
if (options.activate && !selfTest.activation_verified) {
|
|
1064
1442
|
throw new Error('Marrow activation failed: server confirmation was not returned');
|
|
1065
1443
|
}
|
|
1066
|
-
const changedConfig = changes.some((change) => change.
|
|
1444
|
+
const changedConfig = changes.some((change) => change.applied) || configRepairs.some((repair) => repair.changed);
|
|
1067
1445
|
const selfTestPassed = Boolean(!selfTest.skipped && selfTest.active && !selfTest.error);
|
|
1068
1446
|
const remediation = options.repair
|
|
1069
1447
|
? {
|
|
@@ -1098,6 +1476,7 @@ async function install(options) {
|
|
|
1098
1476
|
agent_id: options.agentId,
|
|
1099
1477
|
server_confirmed: Boolean(selfTest.activation_verified),
|
|
1100
1478
|
receipt: selfTest.activation_receipt || null,
|
|
1479
|
+
profile,
|
|
1101
1480
|
},
|
|
1102
1481
|
changes,
|
|
1103
1482
|
doctor: {
|
|
@@ -1150,4 +1529,8 @@ module.exports = {
|
|
|
1150
1529
|
buildInstallValueMoment,
|
|
1151
1530
|
buildTokenValueProof,
|
|
1152
1531
|
stableAgentId,
|
|
1532
|
+
activationProfile,
|
|
1533
|
+
claudeNativeHookFingerprint,
|
|
1534
|
+
printReport,
|
|
1535
|
+
HARNESS_CAPABILITY_REGISTRY,
|
|
1153
1536
|
};
|