@getmarrow/install 0.1.32 → 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 +18 -5
- package/package.json +1 -1
- package/src/governed-runner.js +125 -12
- 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:
|
|
@@ -160,7 +173,7 @@ npx @getmarrow/install --repair
|
|
|
160
173
|
npx @getmarrow/install fleet
|
|
161
174
|
```
|
|
162
175
|
|
|
163
|
-
The fleet view shows live agents, active workflows, risky actions waiting for proof, stale or failed outcomes, capture health, recent decisions, and exact repair commands. It is an operator surface for the authenticated account, not a public status dashboard.
|
|
176
|
+
The fleet view shows live agents, active workflows, agent disagreements and their latest arbitration receipt, risky actions waiting for proof, stale or failed outcomes, capture health, recent decisions, and exact repair commands. Press Enter on **Agent disagreements** to inspect the bound decision, selected proposal, whether Marrow selected a proposal, synthesized a safe sequence, held the action for owner review, or blocked the conflicting actions. Review-required work must be approved from an authenticated Marrow dashboard session; the TUI does not let an agent approve itself. It is an operator surface for the authenticated account, not a public status dashboard.
|
|
164
177
|
|
|
165
178
|
## Integration Paths
|
|
166
179
|
|
package/package.json
CHANGED
package/src/governed-runner.js
CHANGED
|
@@ -8,7 +8,7 @@ const readline = require('node:readline');
|
|
|
8
8
|
const DEFAULT_BASE_URL = 'https://api.getmarrow.ai';
|
|
9
9
|
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;
|
|
10
10
|
const GOVERN_TUI_ROW_COUNT = 7;
|
|
11
|
-
const FLEET_TUI_ROW_COUNT =
|
|
11
|
+
const FLEET_TUI_ROW_COUNT = 12;
|
|
12
12
|
function usage() {
|
|
13
13
|
return `Usage:
|
|
14
14
|
npx @getmarrow/install run --agent deploy-agent -- npm test
|
|
@@ -761,6 +761,38 @@ function normalizeGates(status, report) {
|
|
|
761
761
|
};
|
|
762
762
|
}
|
|
763
763
|
|
|
764
|
+
function normalizeArbitrations(status, report, fleet) {
|
|
765
|
+
const source = firstDefined(
|
|
766
|
+
report.arbitrations,
|
|
767
|
+
report.report?.arbitrations,
|
|
768
|
+
fleet.arbitrations,
|
|
769
|
+
status.arbitrations,
|
|
770
|
+
{},
|
|
771
|
+
) || {};
|
|
772
|
+
const receipts = listValue(
|
|
773
|
+
source.receipts,
|
|
774
|
+
source.items,
|
|
775
|
+
report.recent_arbitrations,
|
|
776
|
+
fleet.recent_arbitrations,
|
|
777
|
+
status.recent_arbitrations,
|
|
778
|
+
).slice(0, 8).map((receipt) => ({
|
|
779
|
+
id: displayText(firstDefined(receipt?.id, receipt?.receipt_id, 'arbitration'), 80),
|
|
780
|
+
decision_id: displayText(firstDefined(receipt?.decision_id, ''), 80),
|
|
781
|
+
resolution: statusLabel(receipt?.resolution, 'unknown'),
|
|
782
|
+
conflict_type: displayText(firstDefined(receipt?.conflict_type, 'action_conflict'), 60),
|
|
783
|
+
selected_proposal_id: displayText(firstDefined(receipt?.selected_proposal_id, ''), 80),
|
|
784
|
+
requesting_agent_id: displayText(firstDefined(receipt?.requesting_agent_id, ''), 80),
|
|
785
|
+
exact_next_action: displayText(firstDefined(receipt?.exact_next_action, ''), 180),
|
|
786
|
+
owner_approval_required: Boolean(receipt?.owner_approval_required),
|
|
787
|
+
created_at: displayText(firstDefined(receipt?.created_at, ''), 80),
|
|
788
|
+
}));
|
|
789
|
+
return {
|
|
790
|
+
open_count: numberValue(firstDefined(source.open_count, source.open, receipts.filter((item) => item.resolution !== 'selected' && item.resolution !== 'synthesized').length), 0),
|
|
791
|
+
review_required_count: numberValue(firstDefined(source.review_required_count, source.review_required, receipts.filter((item) => item.resolution === 'review_required').length), 0),
|
|
792
|
+
receipts,
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
764
796
|
function normalizeFixCommands(status, capacity) {
|
|
765
797
|
const commands = [];
|
|
766
798
|
const add = (value, commandOnly = false) => {
|
|
@@ -774,6 +806,9 @@ function normalizeFixCommands(status, capacity) {
|
|
|
774
806
|
add(status.route_contract?.exact_fix);
|
|
775
807
|
add(status.auto_outcome_closure?.exact_fix);
|
|
776
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);
|
|
777
812
|
add(capacity.exact_next_action, true);
|
|
778
813
|
add(capacity.next_action, true);
|
|
779
814
|
if (listValue(status.missed_hooks, status.degraded_hooks).length) add('npx @getmarrow/install --repair');
|
|
@@ -781,6 +816,52 @@ function normalizeFixCommands(status, capacity) {
|
|
|
781
816
|
return commands.slice(0, 6);
|
|
782
817
|
}
|
|
783
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
|
+
|
|
784
865
|
function normalizeFleetSnapshot(raw, options) {
|
|
785
866
|
const status = raw.status?.data || {};
|
|
786
867
|
const capacity = raw.capacity?.data || {};
|
|
@@ -840,6 +921,8 @@ function normalizeFleetSnapshot(raw, options) {
|
|
|
840
921
|
), raw.capacity?.ok ? 'ok' : 'unknown');
|
|
841
922
|
const recentDecisions = normalizeRecentDecisions(status, report, fleet);
|
|
842
923
|
const gates = normalizeGates(status, report);
|
|
924
|
+
const arbitrations = normalizeArbitrations(status, report, fleet);
|
|
925
|
+
const activationCoverage = normalizeActivationCoverage(status, report, fleet);
|
|
843
926
|
const fixCommands = normalizeFixCommands(status, capacity);
|
|
844
927
|
const errors = Object.entries(raw)
|
|
845
928
|
.filter(([, value]) => value && value.ok === false)
|
|
@@ -859,6 +942,8 @@ function normalizeFleetSnapshot(raw, options) {
|
|
|
859
942
|
recent_decisions: recentDecisions,
|
|
860
943
|
degraded_hooks: missedHooks,
|
|
861
944
|
gates,
|
|
945
|
+
arbitrations,
|
|
946
|
+
activation_coverage: activationCoverage,
|
|
862
947
|
agents,
|
|
863
948
|
fix_commands: fixCommands.length ? fixCommands : ['npx @getmarrow/install doctor'],
|
|
864
949
|
source_errors: errors,
|
|
@@ -898,6 +983,10 @@ function fleetPanel(snapshot) {
|
|
|
898
983
|
? snapshot.agents.slice(0, 5).map((agent) => ` - ${agent.id}${agent.role ? ` (${agent.role})` : ''}: ${agent.status}${agent.last_seen_at ? ` last_seen=${agent.last_seen_at}` : ''}`).join('\n')
|
|
899
984
|
: ' - no agent roster returned yet';
|
|
900
985
|
const fixes = snapshot.fix_commands.map((command) => ` - ${command}`).join('\n');
|
|
986
|
+
const arbitration = snapshot.arbitrations.receipts[0];
|
|
987
|
+
const arbitrationSummary = arbitration
|
|
988
|
+
? `${arbitration.resolution} (${arbitration.conflict_type})${arbitration.exact_next_action ? ` - ${arbitration.exact_next_action}` : ''}`
|
|
989
|
+
: 'none reported yet';
|
|
901
990
|
return [
|
|
902
991
|
'Marrow Fleet Operator',
|
|
903
992
|
'',
|
|
@@ -906,6 +995,10 @@ function fleetPanel(snapshot) {
|
|
|
906
995
|
'',
|
|
907
996
|
`Live agents: ${snapshot.live_agents ?? 'unknown'}`,
|
|
908
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 ? '' : '%'}`,
|
|
1000
|
+
`Agent disagreements: open=${snapshot.arbitrations.open_count} review_required=${snapshot.arbitrations.review_required_count}`,
|
|
1001
|
+
`Latest arbitration: ${arbitrationSummary}`,
|
|
909
1002
|
`Risky actions waiting for proof: ${snapshot.proof_waiting}`,
|
|
910
1003
|
`Failed/stale outcomes: ${snapshot.failed_stale_outcomes}`,
|
|
911
1004
|
`Backpressure/capacity status: ${snapshot.backpressure_status}${snapshot.capacity_next_action ? ` - ${snapshot.capacity_next_action}` : ''}`,
|
|
@@ -1321,9 +1414,14 @@ function renderFleetTui(state) {
|
|
|
1321
1414
|
const snapshot = state.snapshot;
|
|
1322
1415
|
const selectedAgent = snapshot.agents[state.agentIndex] || { id: 'no agent returned', status: 'unknown', role: '' };
|
|
1323
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`;
|
|
1324
1421
|
const gateSummary = `deploy=${snapshot.gates.deploy} publish=${snapshot.gates.publish} merge=${snapshot.gates.merge}`;
|
|
1325
1422
|
const fixCommand = snapshot.fix_commands[0] || 'npx @getmarrow/install doctor';
|
|
1326
1423
|
const recent = snapshot.recent_decisions[0] || 'none reported yet';
|
|
1424
|
+
const arbitration = snapshot.arbitrations.receipts[0];
|
|
1327
1425
|
const rows = [
|
|
1328
1426
|
{
|
|
1329
1427
|
label: 'Live agents',
|
|
@@ -1335,6 +1433,13 @@ function renderFleetTui(state) {
|
|
|
1335
1433
|
value: String(snapshot.active_workflows ?? 'unknown'),
|
|
1336
1434
|
hint: 'Current account-scoped workflow pressure from Marrow.',
|
|
1337
1435
|
},
|
|
1436
|
+
{
|
|
1437
|
+
label: 'Agent disagreements',
|
|
1438
|
+
value: `open=${snapshot.arbitrations.open_count} review_required=${snapshot.arbitrations.review_required_count}`,
|
|
1439
|
+
hint: arbitration
|
|
1440
|
+
? `${arbitration.resolution}: ${arbitration.exact_next_action || arbitration.conflict_type}`
|
|
1441
|
+
: 'No conflicting fleet proposals reported.',
|
|
1442
|
+
},
|
|
1338
1443
|
{
|
|
1339
1444
|
label: 'Risky actions waiting for proof',
|
|
1340
1445
|
value: String(snapshot.proof_waiting),
|
|
@@ -1356,9 +1461,13 @@ function renderFleetTui(state) {
|
|
|
1356
1461
|
hint: 'Latest fleet decision signal returned by Marrow.',
|
|
1357
1462
|
},
|
|
1358
1463
|
{
|
|
1359
|
-
label: '
|
|
1360
|
-
value:
|
|
1361
|
-
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.`,
|
|
1362
1471
|
},
|
|
1363
1472
|
{
|
|
1364
1473
|
label: 'Deploy/publish/merge gates',
|
|
@@ -1651,31 +1760,35 @@ async function runFleetInteractive(options, input = process.stdin, output = proc
|
|
|
1651
1760
|
state.status = 'Agent selection changed.';
|
|
1652
1761
|
render();
|
|
1653
1762
|
} else if (key.name === 'return') {
|
|
1654
|
-
if (state.cursor ===
|
|
1763
|
+
if (state.cursor === 10) {
|
|
1655
1764
|
cleanup();
|
|
1656
1765
|
output.write(`\n${state.snapshot.fix_commands[0] || 'npx @getmarrow/install doctor'}\n`);
|
|
1657
1766
|
resolve();
|
|
1658
1767
|
return;
|
|
1659
1768
|
}
|
|
1660
|
-
if (state.cursor ===
|
|
1769
|
+
if (state.cursor === 11) {
|
|
1661
1770
|
cleanup();
|
|
1662
1771
|
resolve();
|
|
1663
1772
|
return;
|
|
1664
1773
|
}
|
|
1665
1774
|
const selectedAgent = state.snapshot.agents[state.agentIndex];
|
|
1666
|
-
if (state.cursor === 0 || state.cursor ===
|
|
1775
|
+
if (state.cursor === 0 || state.cursor === 9) {
|
|
1667
1776
|
state.lastResult = selectedAgent
|
|
1668
1777
|
? `Agent ${selectedAgent.id}: status=${selectedAgent.status}${selectedAgent.role ? ` role=${selectedAgent.role}` : ''}${selectedAgent.last_seen_at ? ` last_seen=${selectedAgent.last_seen_at}` : ''}`
|
|
1669
1778
|
: 'No live agent roster returned yet. Check API key scope or wait for agents to log activity.';
|
|
1670
|
-
} else if (state.cursor ===
|
|
1779
|
+
} else if (state.cursor === 2) {
|
|
1780
|
+
const receipts = state.snapshot.arbitrations.receipts;
|
|
1781
|
+
state.lastResult = receipts.length
|
|
1782
|
+
? receipts.map((receipt, index) => `${index + 1}. ${receipt.id}: ${receipt.resolution}${receipt.decision_id ? `; decision=${receipt.decision_id}` : ''}${receipt.selected_proposal_id ? `; selected=${receipt.selected_proposal_id}` : ''}; ${receipt.exact_next_action || receipt.conflict_type}${receipt.owner_approval_required ? ' Owner approval must be issued from an authenticated Marrow dashboard session.' : ''}`).join(' ')
|
|
1783
|
+
: 'No agent disagreements or arbitration receipts returned yet.';
|
|
1784
|
+
} else if (state.cursor === 6) {
|
|
1671
1785
|
state.lastResult = state.snapshot.recent_decisions.length
|
|
1672
1786
|
? state.snapshot.recent_decisions.map((decision, index) => `${index + 1}. ${decision}`).join(' ')
|
|
1673
1787
|
: 'No recent decisions returned yet.';
|
|
1674
|
-
} else if (state.cursor === 6) {
|
|
1675
|
-
state.lastResult = state.snapshot.degraded_hooks.length
|
|
1676
|
-
? `Degraded hooks: ${state.snapshot.degraded_hooks.join(', ')}. Fix: ${state.snapshot.fix_commands[0]}`
|
|
1677
|
-
: 'No degraded hooks reported.';
|
|
1678
1788
|
} else if (state.cursor === 7) {
|
|
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]}` : ''}`;
|
|
1791
|
+
} else if (state.cursor === 8) {
|
|
1679
1792
|
state.lastResult = `Release gates: deploy=${state.snapshot.gates.deploy}, publish=${state.snapshot.gates.publish}, merge=${state.snapshot.gates.merge}.`;
|
|
1680
1793
|
} else {
|
|
1681
1794
|
state.lastResult = fleetPanel(state.snapshot).replace(/\n/g, ' ');
|
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
|
};
|