@getmarrow/install 0.1.33 → 0.1.35
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 +39 -4
- package/package.json +1 -1
- package/src/governed-runner.js +103 -6
- package/src/installer.js +429 -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,41 @@ Required secret:
|
|
|
62
62
|
export MARROW_API_KEY=mrw_live_...
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
##
|
|
65
|
+
## Keeping Marrow Current
|
|
66
|
+
|
|
67
|
+
Marrow's hosted API, website, and dashboard update automatically; local SDK dependencies, generated runtime files, MCP hooks/configuration, and pinned package versions do not silently rewrite themselves. Keeping them current delivers new client-side features, compatibility improvements, and any published security fixes. Supported clients report their package version during authenticated status/runtime activity, and Marrow returns a `client_update` notice with the exact action when the version is behind or unknown.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npx -y @getmarrow/install@latest activate
|
|
71
|
+
npx -y @getmarrow/install@latest doctor
|
|
72
|
+
|
|
73
|
+
# Use only when doctor reports drift
|
|
74
|
+
npx -y @getmarrow/install@latest --repair
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`activate` reconciles Marrow-managed entries while retaining unrelated user hooks and configuration. Detection and notification are automatic; local changes remain explicit and subject to the operator's normal change policy.
|
|
66
78
|
|
|
67
|
-
|
|
79
|
+
## What's New in v0.1.35
|
|
80
|
+
|
|
81
|
+
v0.1.35 turns server-side client update detection into a guided, low-friction operator workflow. Installer status, activation reports, and the Fleet Operator expose a request-specific advisory with the exact update and verification commands while keeping local mutation explicit:
|
|
82
|
+
|
|
83
|
+
- official installer requests identify the installed `@getmarrow/install` version;
|
|
84
|
+
- status, self-test, and Fleet Operator output show recommended, unrecognized, and security-required update states without conflating them;
|
|
85
|
+
- generated agent instructions tell the agent to notify the operator and obey local change policy;
|
|
86
|
+
- certified activation now pins MCP 3.9.51 and SDK 3.7.50, including the exact SDK registry integrity;
|
|
87
|
+
- `activate`, `doctor`, and `--repair` remain explicit commands and preserve unrelated hooks and configuration.
|
|
88
|
+
|
|
89
|
+
It preserves the passive-governance verification introduced in v0.1.34. Activation 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:
|
|
90
|
+
|
|
91
|
+
- Claude Code installation includes exact `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, and `Stop` hooks;
|
|
92
|
+
- matching pre-action/result receipts use one tool correlation, and activation fingerprints the exact hook contract without uploading configuration contents;
|
|
93
|
+
- the capability registry distinguishes native hooks, MCP, SDK passive runtime, governed wrappers, and custom event contracts;
|
|
94
|
+
- `activate` fails when the local integration is incomplete or the server does not accept the exact activation profile;
|
|
95
|
+
- `doctor` and `--repair` use the same configuration evidence without exposing configuration contents;
|
|
96
|
+
- the harness certification suite prevents support claims from overstating what is automatic;
|
|
97
|
+
- unavailable coverage remains “insufficient data” instead of a synthetic percentage.
|
|
98
|
+
|
|
99
|
+
It preserves agent-disagreement visibility from v0.1.33 and the server-verified first-run activation introduced in v0.1.29:
|
|
68
100
|
|
|
69
101
|
- GitHub and npm now advertise separate signed discovery placements;
|
|
70
102
|
- package metadata identifies the installer as agent governance rather than a general memory utility;
|
|
@@ -104,10 +136,13 @@ With a valid key, `activate`:
|
|
|
104
136
|
4. closes its outcome;
|
|
105
137
|
5. sends the exact self-test decision ID to Marrow for server-side verification;
|
|
106
138
|
6. reads agent status and the one-call runtime;
|
|
107
|
-
7.
|
|
139
|
+
7. registers the detected capability, expected hooks, and one-way configuration fingerprint;
|
|
140
|
+
8. returns an activation receipt with capture, intervention, closure, first-value state, and the exact next action.
|
|
108
141
|
|
|
109
142
|
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
143
|
|
|
144
|
+
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.
|
|
145
|
+
|
|
111
146
|
## Govern TUI
|
|
112
147
|
|
|
113
148
|
Open the interactive setup panel:
|
package/package.json
CHANGED
package/src/governed-runner.js
CHANGED
|
@@ -4,6 +4,7 @@ const fs = require('node:fs');
|
|
|
4
4
|
const os = require('node:os');
|
|
5
5
|
const path = require('node:path');
|
|
6
6
|
const readline = require('node:readline');
|
|
7
|
+
const { version: INSTALLER_PACKAGE_VERSION } = require('../package.json');
|
|
7
8
|
|
|
8
9
|
const DEFAULT_BASE_URL = 'https://api.getmarrow.ai';
|
|
9
10
|
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;
|
|
@@ -343,6 +344,8 @@ function headers(options) {
|
|
|
343
344
|
'X-Marrow-Agent-Id': options.agentId,
|
|
344
345
|
'X-Marrow-Session-Id': options.sessionId,
|
|
345
346
|
'X-Marrow-Client': sourceClient(options.client),
|
|
347
|
+
'X-Marrow-Package': '@getmarrow/install',
|
|
348
|
+
'X-Marrow-Package-Version': INSTALLER_PACKAGE_VERSION,
|
|
346
349
|
'User-Agent': '@getmarrow/install governed-runner',
|
|
347
350
|
};
|
|
348
351
|
return h;
|
|
@@ -657,6 +660,31 @@ async function statusOnly(parsed) {
|
|
|
657
660
|
return requestJson(parsed.options, 'GET', '/v1/agent/status');
|
|
658
661
|
}
|
|
659
662
|
|
|
663
|
+
function statusPanel(status = {}) {
|
|
664
|
+
const update = status.client_update && typeof status.client_update === 'object'
|
|
665
|
+
? status.client_update
|
|
666
|
+
: null;
|
|
667
|
+
const lines = [
|
|
668
|
+
'Marrow runtime status',
|
|
669
|
+
`Health: ${displayText(firstDefined(status.health, status.status, status.ok === false ? 'degraded' : 'unknown'), 40)}`,
|
|
670
|
+
];
|
|
671
|
+
const notification = update?.notification_state || update?.notification;
|
|
672
|
+
const priority = notification === 'security_required' || notification === 'recommended'
|
|
673
|
+
? notification
|
|
674
|
+
: update?.version_status === 'unknown' || notification === 'unknown' || notification === 'version_unknown'
|
|
675
|
+
? 'version_unknown'
|
|
676
|
+
: update?.priority || 'recommended';
|
|
677
|
+
if (update && (update.update_available === true || update.version_status === 'unknown' || notification === 'unknown' || notification === 'version_unknown' || priority === 'security_required')) {
|
|
678
|
+
lines.push(`Client update: ${displayText(priority, 32)}; installed=${displayText(update.installed_version || update.current_version || 'unknown', 32)}; latest=${displayText(update.latest_version || 'unknown', 32)}`);
|
|
679
|
+
lines.push('Automatic notification: yes; automatic local mutation: no; operator policy applies.');
|
|
680
|
+
if (update.update_command || update.exact_update_command) lines.push(`Update: ${displayText(update.update_command || update.exact_update_command, 240)}`);
|
|
681
|
+
if (update.verification_command || update.exact_verification_command) lines.push(`Verify: ${displayText(update.verification_command || update.exact_verification_command, 240)}`);
|
|
682
|
+
} else {
|
|
683
|
+
lines.push('Client update: current or unavailable.');
|
|
684
|
+
}
|
|
685
|
+
return lines.join('\n');
|
|
686
|
+
}
|
|
687
|
+
|
|
660
688
|
async function optionalRequestJson(options, method, route, body) {
|
|
661
689
|
try {
|
|
662
690
|
return { ok: true, data: await requestJson(options, method, route, body) };
|
|
@@ -806,6 +834,12 @@ function normalizeFixCommands(status, capacity) {
|
|
|
806
834
|
add(status.route_contract?.exact_fix);
|
|
807
835
|
add(status.auto_outcome_closure?.exact_fix);
|
|
808
836
|
add(status.capture_coverage?.exact_fix);
|
|
837
|
+
add(status.activation_coverage?.exact_fix);
|
|
838
|
+
add(status.activation_coverage?.drift?.repair_command, true);
|
|
839
|
+
add(status.passive_activation?.exact_fix);
|
|
840
|
+
if (status.client_update?.update_available === true || status.client_update?.version_status === 'unknown' || status.client_update?.notification_state === 'unknown' || status.client_update?.notification === 'version_unknown') {
|
|
841
|
+
add(status.client_update?.update_command || status.client_update?.exact_update_command, true);
|
|
842
|
+
}
|
|
809
843
|
add(capacity.exact_next_action, true);
|
|
810
844
|
add(capacity.next_action, true);
|
|
811
845
|
if (listValue(status.missed_hooks, status.degraded_hooks).length) add('npx @getmarrow/install --repair');
|
|
@@ -813,6 +847,52 @@ function normalizeFixCommands(status, capacity) {
|
|
|
813
847
|
return commands.slice(0, 6);
|
|
814
848
|
}
|
|
815
849
|
|
|
850
|
+
function normalizeActivationCoverage(status, report, fleet) {
|
|
851
|
+
const source = firstDefined(
|
|
852
|
+
status.activation_coverage,
|
|
853
|
+
status.passive_activation,
|
|
854
|
+
report.activation_coverage,
|
|
855
|
+
report.passive_activation,
|
|
856
|
+
fleet.activation_coverage,
|
|
857
|
+
{},
|
|
858
|
+
) || {};
|
|
859
|
+
const activation = source.activation || {};
|
|
860
|
+
const capture = source.capture_coverage || source.capture || source.coverage || {};
|
|
861
|
+
const closure = source.outcome_closure || source.closure || {};
|
|
862
|
+
const effectiveness = source.intervention_effectiveness || source.effectiveness || {};
|
|
863
|
+
const drift = source.drift && typeof source.drift === 'object' ? source.drift : {};
|
|
864
|
+
const available = source.available === true || capture.available === true;
|
|
865
|
+
const driftAvailable = available && drift.available === true && typeof drift.detected === 'boolean';
|
|
866
|
+
const percent = (value) => {
|
|
867
|
+
const number = Number(value);
|
|
868
|
+
if (!Number.isFinite(number)) return null;
|
|
869
|
+
return Math.max(0, Math.min(100, number));
|
|
870
|
+
};
|
|
871
|
+
const ratio = (value) => {
|
|
872
|
+
const number = Number(value);
|
|
873
|
+
if (!Number.isFinite(number)) return null;
|
|
874
|
+
return Math.max(0, Math.min(100, number * 100));
|
|
875
|
+
};
|
|
876
|
+
const metricPercent = (explicitPercent, rate, fallbackPercent) => {
|
|
877
|
+
const explicit = firstDefined(explicitPercent, fallbackPercent);
|
|
878
|
+
return explicit == null ? ratio(rate) : percent(explicit);
|
|
879
|
+
};
|
|
880
|
+
return {
|
|
881
|
+
available,
|
|
882
|
+
state: statusLabel(firstDefined(source.state, source.status), available ? 'active' : 'warming_up'),
|
|
883
|
+
capability_level: displayText(firstDefined(activation.capability_level, source.capability_level, source.capability, 'unknown'), 40),
|
|
884
|
+
capture_percent: metricPercent(capture.percent, capture.rate, source.capture_percent),
|
|
885
|
+
closure_percent: metricPercent(closure.percent, closure.rate, source.closure_percent),
|
|
886
|
+
effectiveness_percent: metricPercent(
|
|
887
|
+
effectiveness.followed_percent,
|
|
888
|
+
firstDefined(effectiveness.follow_through_rate, effectiveness.rate),
|
|
889
|
+
source.effectiveness_percent,
|
|
890
|
+
),
|
|
891
|
+
drift: driftAvailable ? drift.detected : null,
|
|
892
|
+
exact_fix: displayText(firstDefined(drift.repair_command, source.exact_fix, source.repair_command, ''), 180),
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
|
|
816
896
|
function normalizeFleetSnapshot(raw, options) {
|
|
817
897
|
const status = raw.status?.data || {};
|
|
818
898
|
const capacity = raw.capacity?.data || {};
|
|
@@ -873,6 +953,7 @@ function normalizeFleetSnapshot(raw, options) {
|
|
|
873
953
|
const recentDecisions = normalizeRecentDecisions(status, report, fleet);
|
|
874
954
|
const gates = normalizeGates(status, report);
|
|
875
955
|
const arbitrations = normalizeArbitrations(status, report, fleet);
|
|
956
|
+
const activationCoverage = normalizeActivationCoverage(status, report, fleet);
|
|
876
957
|
const fixCommands = normalizeFixCommands(status, capacity);
|
|
877
958
|
const errors = Object.entries(raw)
|
|
878
959
|
.filter(([, value]) => value && value.ok === false)
|
|
@@ -893,6 +974,8 @@ function normalizeFleetSnapshot(raw, options) {
|
|
|
893
974
|
degraded_hooks: missedHooks,
|
|
894
975
|
gates,
|
|
895
976
|
arbitrations,
|
|
977
|
+
activation_coverage: activationCoverage,
|
|
978
|
+
client_update: status.client_update || null,
|
|
896
979
|
agents,
|
|
897
980
|
fix_commands: fixCommands.length ? fixCommands : ['npx @getmarrow/install doctor'],
|
|
898
981
|
source_errors: errors,
|
|
@@ -944,12 +1027,17 @@ function fleetPanel(snapshot) {
|
|
|
944
1027
|
'',
|
|
945
1028
|
`Live agents: ${snapshot.live_agents ?? 'unknown'}`,
|
|
946
1029
|
`Active workflows: ${snapshot.active_workflows ?? 'unknown'}`,
|
|
1030
|
+
`Passive activation: ${snapshot.activation_coverage.state} capability=${snapshot.activation_coverage.capability_level}`,
|
|
1031
|
+
`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
1032
|
`Agent disagreements: open=${snapshot.arbitrations.open_count} review_required=${snapshot.arbitrations.review_required_count}`,
|
|
948
1033
|
`Latest arbitration: ${arbitrationSummary}`,
|
|
949
1034
|
`Risky actions waiting for proof: ${snapshot.proof_waiting}`,
|
|
950
1035
|
`Failed/stale outcomes: ${snapshot.failed_stale_outcomes}`,
|
|
951
1036
|
`Backpressure/capacity status: ${snapshot.backpressure_status}${snapshot.capacity_next_action ? ` - ${snapshot.capacity_next_action}` : ''}`,
|
|
952
1037
|
`Degraded hooks: ${degraded}`,
|
|
1038
|
+
snapshot.client_update && (snapshot.client_update.update_available === true || snapshot.client_update.version_status === 'unknown' || snapshot.client_update.notification_state === 'unknown' || snapshot.client_update.notification === 'version_unknown')
|
|
1039
|
+
? `Marrow client update: ${displayText(snapshot.client_update.notification_state === 'security_required' ? 'security_required' : snapshot.client_update.notification_state === 'recommended' ? 'recommended' : snapshot.client_update.version_status === 'unknown' || snapshot.client_update.notification_state === 'unknown' || snapshot.client_update.notification === 'version_unknown' ? 'version_unknown' : snapshot.client_update.priority || 'recommended', 32)}; installed=${displayText(snapshot.client_update.installed_version || snapshot.client_update.current_version || 'unknown', 32)}; latest=${displayText(snapshot.client_update.latest_version || 'unknown', 32)}; operator approval required`
|
|
1040
|
+
: 'Marrow client update: current or unavailable',
|
|
953
1041
|
`Deploy/publish/merge gates: deploy=${snapshot.gates.deploy} publish=${snapshot.gates.publish} merge=${snapshot.gates.merge}`,
|
|
954
1042
|
'',
|
|
955
1043
|
'Live agent roster:',
|
|
@@ -1361,6 +1449,10 @@ function renderFleetTui(state) {
|
|
|
1361
1449
|
const snapshot = state.snapshot;
|
|
1362
1450
|
const selectedAgent = snapshot.agents[state.agentIndex] || { id: 'no agent returned', status: 'unknown', role: '' };
|
|
1363
1451
|
const degraded = snapshot.degraded_hooks.length ? snapshot.degraded_hooks.join(', ') : 'none';
|
|
1452
|
+
const activation = snapshot.activation_coverage;
|
|
1453
|
+
const coverageValue = activation.available
|
|
1454
|
+
? `${activation.state}; capture=${activation.capture_percent ?? 'n/a'}%; closure=${activation.closure_percent ?? 'n/a'}%`
|
|
1455
|
+
: `${activation.state}; insufficient data`;
|
|
1364
1456
|
const gateSummary = `deploy=${snapshot.gates.deploy} publish=${snapshot.gates.publish} merge=${snapshot.gates.merge}`;
|
|
1365
1457
|
const fixCommand = snapshot.fix_commands[0] || 'npx @getmarrow/install doctor';
|
|
1366
1458
|
const recent = snapshot.recent_decisions[0] || 'none reported yet';
|
|
@@ -1404,9 +1496,13 @@ function renderFleetTui(state) {
|
|
|
1404
1496
|
hint: 'Latest fleet decision signal returned by Marrow.',
|
|
1405
1497
|
},
|
|
1406
1498
|
{
|
|
1407
|
-
label: '
|
|
1408
|
-
value:
|
|
1409
|
-
hint:
|
|
1499
|
+
label: 'Passive activation / coverage',
|
|
1500
|
+
value: coverageValue,
|
|
1501
|
+
hint: activation.drift
|
|
1502
|
+
? `Configuration drift detected. ${activation.exact_fix || fixCommand}`
|
|
1503
|
+
: degraded === 'none'
|
|
1504
|
+
? `Capability=${activation.capability_level}; no missing hooks reported.`
|
|
1505
|
+
: `Degraded hooks: ${degraded}. Repair before trusting passive coverage.`,
|
|
1410
1506
|
},
|
|
1411
1507
|
{
|
|
1412
1508
|
label: 'Deploy/publish/merge gates',
|
|
@@ -1725,9 +1821,8 @@ async function runFleetInteractive(options, input = process.stdin, output = proc
|
|
|
1725
1821
|
? state.snapshot.recent_decisions.map((decision, index) => `${index + 1}. ${decision}`).join(' ')
|
|
1726
1822
|
: 'No recent decisions returned yet.';
|
|
1727
1823
|
} else if (state.cursor === 7) {
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
: 'No degraded hooks reported.';
|
|
1824
|
+
const coverage = state.snapshot.activation_coverage;
|
|
1825
|
+
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
1826
|
} else if (state.cursor === 8) {
|
|
1732
1827
|
state.lastResult = `Release gates: deploy=${state.snapshot.gates.deploy}, publish=${state.snapshot.gates.publish}, merge=${state.snapshot.gates.merge}.`;
|
|
1733
1828
|
} else {
|
|
@@ -1779,6 +1874,7 @@ async function runCli(argv) {
|
|
|
1779
1874
|
|
|
1780
1875
|
if (parsed.options?.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
1781
1876
|
else if (result?.blocked) process.stderr.write(`BLOCKED: ${result.message || 'Marrow blocked this action.'}\n`);
|
|
1877
|
+
else if (parsed.command === 'status') process.stdout.write(`${statusPanel(result)}\n`);
|
|
1782
1878
|
else if (!['run', 'fleet', 'hermes', 'openclaw', 'integrations'].includes(parsed.command)) process.stdout.write('Marrow command completed.\n');
|
|
1783
1879
|
|
|
1784
1880
|
if (parsed.command === 'run' || result?.blocked) process.exitCode = result?.exitCode ?? (result?.ok === false ? 1 : 0);
|
|
@@ -1808,6 +1904,7 @@ module.exports = {
|
|
|
1808
1904
|
gateOnly,
|
|
1809
1905
|
proofOnly,
|
|
1810
1906
|
statusOnly,
|
|
1907
|
+
statusPanel,
|
|
1811
1908
|
runStatusCheck,
|
|
1812
1909
|
runGateCheck,
|
|
1813
1910
|
runGovernInteractive,
|
package/src/installer.js
CHANGED
|
@@ -2,11 +2,42 @@ const fs = require('node:fs');
|
|
|
2
2
|
const path = require('node:path');
|
|
3
3
|
const os = require('node:os');
|
|
4
4
|
const crypto = require('node:crypto');
|
|
5
|
+
const { version: INSTALLER_ADAPTER_VERSION } = require('../package.json');
|
|
5
6
|
|
|
6
7
|
const DEFAULT_BASE_URL = 'https://api.getmarrow.ai';
|
|
7
8
|
const MARROW_BLOCK_START = '<!-- marrow:passive-start -->';
|
|
8
9
|
const MARROW_BLOCK_END = '<!-- marrow:passive-end -->';
|
|
9
|
-
const
|
|
10
|
+
const MCP_ADAPTER_VERSION = '3.9.51';
|
|
11
|
+
const SDK_ADAPTER_VERSION = '3.7.50';
|
|
12
|
+
const SDK_ADAPTER_INTEGRITY = 'sha512-TyCXUKZAaKRPdfVqXEuIvWnOt4GLcIUbwhpX+EopzdALNzq+t5wUGHrqJlkNSvopppPTTdFVLJ7O1vtwofqFqw==';
|
|
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',
|
|
@@ -348,6 +380,7 @@ Marrow should run passively after install:
|
|
|
348
380
|
- Before risky work, use Marrow's decision brief or passive prompt hook.
|
|
349
381
|
- After meaningful work, record the outcome so future agents learn from it.
|
|
350
382
|
- Check health with \`marrow_agent_status\` or \`GET /v1/agent/status\`.
|
|
383
|
+
- When status/runtime returns a \`client_update\` notice, tell the operator and use its exact update and verification commands only when local change policy permits.
|
|
351
384
|
|
|
352
385
|
Required environment:
|
|
353
386
|
|
|
@@ -422,38 +455,245 @@ function upsertBlock(content, block) {
|
|
|
422
455
|
return `${content}${separator}${block}\n`;
|
|
423
456
|
}
|
|
424
457
|
|
|
425
|
-
function
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
458
|
+
function exactHookConfigured(settings, eventName, command, matcher) {
|
|
459
|
+
const entries = settings?.hooks?.[eventName];
|
|
460
|
+
if (!Array.isArray(entries)) return false;
|
|
461
|
+
return entries.some((entry) => {
|
|
462
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return false;
|
|
463
|
+
if (matcher != null && entry.matcher !== matcher) return false;
|
|
464
|
+
if (!Array.isArray(entry.hooks)) return false;
|
|
465
|
+
return entry.hooks.some((hook) => (
|
|
466
|
+
hook
|
|
467
|
+
&& typeof hook === 'object'
|
|
468
|
+
&& !Array.isArray(hook)
|
|
469
|
+
&& hook.type === 'command'
|
|
470
|
+
&& typeof hook.command === 'string'
|
|
471
|
+
&& hook.command.trim() === command
|
|
472
|
+
));
|
|
473
|
+
});
|
|
474
|
+
}
|
|
435
475
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
476
|
+
function exactHookDescriptors(settings, eventName, command, matcher) {
|
|
477
|
+
const entries = settings?.hooks?.[eventName];
|
|
478
|
+
if (!Array.isArray(entries)) return [];
|
|
479
|
+
return entries.flatMap((entry) => {
|
|
480
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return [];
|
|
481
|
+
if (matcher != null && entry.matcher !== matcher) return [];
|
|
482
|
+
if (!Array.isArray(entry.hooks)) return [];
|
|
483
|
+
return entry.hooks.flatMap((hook) => {
|
|
484
|
+
if (!hook || typeof hook !== 'object' || Array.isArray(hook)
|
|
485
|
+
|| hook.type !== 'command' || typeof hook.command !== 'string'
|
|
486
|
+
|| hook.command.trim() !== command) return [];
|
|
487
|
+
return [{
|
|
488
|
+
matcher: typeof entry.matcher === 'string' ? entry.matcher : null,
|
|
489
|
+
command,
|
|
490
|
+
timeout: typeof hook.timeout === 'number' && Number.isFinite(hook.timeout)
|
|
491
|
+
? hook.timeout
|
|
492
|
+
: null,
|
|
493
|
+
}];
|
|
440
494
|
});
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function marrowHookSubcommand(command) {
|
|
499
|
+
if (typeof command !== 'string') return null;
|
|
500
|
+
const match = command.trim().match(
|
|
501
|
+
/^npx\s+(?:-y\s+)?@getmarrow\/mcp(?:@[^\s]+)?\s+(context-hook|pre-action-hook|hook|session-hook)$/,
|
|
502
|
+
);
|
|
503
|
+
return match?.[1] || null;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function reconcileMarrowCommandHook(settings, eventName, subcommand, command, matcher) {
|
|
507
|
+
const original = Array.isArray(settings?.hooks?.[eventName]) ? settings.hooks[eventName] : [];
|
|
508
|
+
let preferredHandler = null;
|
|
509
|
+
const retained = [];
|
|
510
|
+
for (const entry of original) {
|
|
511
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry) || !Array.isArray(entry.hooks)) {
|
|
512
|
+
retained.push(entry);
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
const remaining = [];
|
|
516
|
+
for (const hook of entry.hooks) {
|
|
517
|
+
const detected = hook && typeof hook === 'object' && !Array.isArray(hook)
|
|
518
|
+
&& hook.type === 'command' ? marrowHookSubcommand(hook.command) : null;
|
|
519
|
+
if (detected) {
|
|
520
|
+
const exactMatcher = matcher == null ? entry.matcher === undefined : entry.matcher === matcher;
|
|
521
|
+
if (detected === subcommand && (!preferredHandler || (hook.command === command && exactMatcher))) {
|
|
522
|
+
preferredHandler = hook;
|
|
523
|
+
}
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
remaining.push(hook);
|
|
527
|
+
}
|
|
528
|
+
if (remaining.length > 0) retained.push({ ...entry, hooks: remaining });
|
|
441
529
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
530
|
+
const canonical = { hooks: [{ ...(preferredHandler || {}), type: 'command', command }] };
|
|
531
|
+
if (matcher != null) canonical.matcher = matcher;
|
|
532
|
+
retained.push(canonical);
|
|
533
|
+
return retained;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function marrowHookDescriptors(settings, eventName, subcommand) {
|
|
537
|
+
const entries = settings?.hooks?.[eventName];
|
|
538
|
+
if (!Array.isArray(entries)) return [];
|
|
539
|
+
return entries.flatMap((entry) => {
|
|
540
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry) || !Array.isArray(entry.hooks)) return [];
|
|
541
|
+
return entry.hooks.flatMap((hook) => {
|
|
542
|
+
if (!hook || typeof hook !== 'object' || Array.isArray(hook)
|
|
543
|
+
|| hook.type !== 'command') return [];
|
|
544
|
+
const detected = marrowHookSubcommand(hook.command);
|
|
545
|
+
if (!detected || (subcommand && detected !== subcommand)) return [];
|
|
546
|
+
return [{
|
|
547
|
+
matcher: typeof entry.matcher === 'string' ? entry.matcher : null,
|
|
548
|
+
command: hook.command.trim(),
|
|
549
|
+
timeout: typeof hook.timeout === 'number' && Number.isFinite(hook.timeout) ? hook.timeout : null,
|
|
550
|
+
}];
|
|
445
551
|
});
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function safeJsonObject(filePath) {
|
|
556
|
+
try {
|
|
557
|
+
return parseJsonObject(filePath);
|
|
558
|
+
} catch {
|
|
559
|
+
return {};
|
|
446
560
|
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function claudeNativeHookFingerprint(settings) {
|
|
564
|
+
const contract = {
|
|
565
|
+
schema: 'marrow-claude-native-hooks.v3',
|
|
566
|
+
adapter_version: MCP_ADAPTER_VERSION,
|
|
567
|
+
expected_hooks: NATIVE_EXPECTED_HOOKS,
|
|
568
|
+
configured: {
|
|
569
|
+
prompt: exactHookConfigured(settings, 'UserPromptSubmit', MCP_CONTEXT_HOOK_COMMAND),
|
|
570
|
+
pre_action: exactHookConfigured(settings, 'PreToolUse', MCP_PRE_ACTION_HOOK_COMMAND, NATIVE_HOOK_MATCHER),
|
|
571
|
+
action_result_success: exactHookConfigured(settings, 'PostToolUse', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER),
|
|
572
|
+
action_result_failure: exactHookConfigured(settings, 'PostToolUseFailure', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER),
|
|
573
|
+
session_end: exactHookConfigured(settings, 'Stop', MCP_SESSION_END_HOOK_COMMAND),
|
|
574
|
+
},
|
|
575
|
+
descriptors: {
|
|
576
|
+
prompt: exactHookDescriptors(settings, 'UserPromptSubmit', MCP_CONTEXT_HOOK_COMMAND),
|
|
577
|
+
pre_action: exactHookDescriptors(settings, 'PreToolUse', MCP_PRE_ACTION_HOOK_COMMAND, NATIVE_HOOK_MATCHER),
|
|
578
|
+
action_result_success: exactHookDescriptors(settings, 'PostToolUse', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER),
|
|
579
|
+
action_result_failure: exactHookDescriptors(settings, 'PostToolUseFailure', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER),
|
|
580
|
+
session_end: exactHookDescriptors(settings, 'Stop', MCP_SESSION_END_HOOK_COMMAND),
|
|
581
|
+
},
|
|
582
|
+
active_marrow_handlers: {
|
|
583
|
+
prompt: marrowHookDescriptors(settings, 'UserPromptSubmit'),
|
|
584
|
+
pre_action: marrowHookDescriptors(settings, 'PreToolUse'),
|
|
585
|
+
action_result_success: marrowHookDescriptors(settings, 'PostToolUse'),
|
|
586
|
+
action_result_failure: marrowHookDescriptors(settings, 'PostToolUseFailure'),
|
|
587
|
+
session_end: marrowHookDescriptors(settings, 'Stop'),
|
|
588
|
+
},
|
|
589
|
+
};
|
|
590
|
+
return crypto.createHash('sha256').update(JSON.stringify(contract)).digest('hex');
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function upsertClaudeHooks(settingsPath) {
|
|
594
|
+
const settings = parseJsonObject(settingsPath);
|
|
595
|
+
const hooks = settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)
|
|
596
|
+
? settings.hooks
|
|
597
|
+
: {};
|
|
598
|
+
const postToolUse = reconcileMarrowCommandHook(
|
|
599
|
+
settings, 'PostToolUse', 'hook', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER,
|
|
600
|
+
);
|
|
601
|
+
const postToolUseFailure = reconcileMarrowCommandHook(
|
|
602
|
+
settings, 'PostToolUseFailure', 'hook', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER,
|
|
603
|
+
);
|
|
604
|
+
const preToolUse = reconcileMarrowCommandHook(
|
|
605
|
+
settings, 'PreToolUse', 'pre-action-hook', MCP_PRE_ACTION_HOOK_COMMAND, NATIVE_HOOK_MATCHER,
|
|
606
|
+
);
|
|
607
|
+
const userPromptSubmit = reconcileMarrowCommandHook(
|
|
608
|
+
settings, 'UserPromptSubmit', 'context-hook', MCP_CONTEXT_HOOK_COMMAND,
|
|
609
|
+
);
|
|
610
|
+
const stop = reconcileMarrowCommandHook(
|
|
611
|
+
settings, 'Stop', 'session-hook', MCP_SESSION_END_HOOK_COMMAND,
|
|
612
|
+
);
|
|
447
613
|
|
|
448
614
|
settings.hooks = {
|
|
449
615
|
...hooks,
|
|
616
|
+
PreToolUse: preToolUse,
|
|
450
617
|
PostToolUse: postToolUse,
|
|
618
|
+
PostToolUseFailure: postToolUseFailure,
|
|
451
619
|
UserPromptSubmit: userPromptSubmit,
|
|
620
|
+
Stop: stop,
|
|
452
621
|
};
|
|
453
622
|
|
|
454
623
|
return JSON.stringify(settings, null, 2) + '\n';
|
|
455
624
|
}
|
|
456
625
|
|
|
626
|
+
function activationProfile(detection, plan, changes, client) {
|
|
627
|
+
const registry = HARNESS_CAPABILITY_REGISTRY.find((entry) => entry.client === client)
|
|
628
|
+
|| HARNESS_CAPABILITY_REGISTRY.find((entry) => entry.client === 'custom');
|
|
629
|
+
const sdkDependency = inspectSdkDependency(detection);
|
|
630
|
+
const capabilityLevel = client === 'custom' && (plan.mode === 'sdk' || plan.mode === 'both')
|
|
631
|
+
? 'sdk_passive_runtime'
|
|
632
|
+
: registry.capability_level;
|
|
633
|
+
const expectedHooks = capabilityLevel === 'sdk_passive_runtime'
|
|
634
|
+
? ['pre_action', 'action_result', 'outcome_closure']
|
|
635
|
+
: [...registry.automatic];
|
|
636
|
+
const adapterVersion = capabilityLevel === 'native_hooks' || capabilityLevel === 'mcp'
|
|
637
|
+
? MCP_ADAPTER_VERSION
|
|
638
|
+
: capabilityLevel === 'sdk_passive_runtime'
|
|
639
|
+
? SDK_ADAPTER_VERSION
|
|
640
|
+
: INSTALLER_ADAPTER_VERSION;
|
|
641
|
+
const observedHooks = [];
|
|
642
|
+
const claudeSettings = safeJsonObject(detection.paths.claudeSettings);
|
|
643
|
+
if (capabilityLevel === 'native_hooks'
|
|
644
|
+
&& exactHookConfigured(claudeSettings, 'UserPromptSubmit', MCP_CONTEXT_HOOK_COMMAND)) observedHooks.push('prompt');
|
|
645
|
+
if (capabilityLevel === 'native_hooks'
|
|
646
|
+
&& exactHookConfigured(claudeSettings, 'PreToolUse', MCP_PRE_ACTION_HOOK_COMMAND, NATIVE_HOOK_MATCHER)) observedHooks.push('pre_action');
|
|
647
|
+
if (capabilityLevel === 'native_hooks'
|
|
648
|
+
&& exactHookConfigured(claudeSettings, 'PostToolUse', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER)
|
|
649
|
+
&& exactHookConfigured(claudeSettings, 'PostToolUseFailure', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER)) observedHooks.push('action_result');
|
|
650
|
+
if (capabilityLevel === 'native_hooks'
|
|
651
|
+
&& exactHookConfigured(claudeSettings, 'Stop', MCP_SESSION_END_HOOK_COMMAND)) observedHooks.push('session_end');
|
|
652
|
+
const passiveRuntime = safeRead(detection.paths.passiveRuntime);
|
|
653
|
+
if (capabilityLevel === 'sdk_passive_runtime'
|
|
654
|
+
&& sdkDependency.present
|
|
655
|
+
&& /await import\('@getmarrow\/sdk'\)/.test(passiveRuntime)
|
|
656
|
+
&& /runtime\.install\(\)/.test(passiveRuntime)) {
|
|
657
|
+
for (const hook of ['pre_action', 'action_result', 'outcome_closure']) {
|
|
658
|
+
if (!observedHooks.includes(hook)) observedHooks.push(hook);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
const mcpConfigs = [detection.paths.mcpJson, detection.paths.cursorMcp]
|
|
662
|
+
.map((filePath) => safeJsonObject(filePath));
|
|
663
|
+
if (capabilityLevel === 'mcp' && mcpConfigs.some((config) => (
|
|
664
|
+
config?.mcpServers?.marrow?.command === 'npx'
|
|
665
|
+
&& Array.isArray(config.mcpServers.marrow.args)
|
|
666
|
+
&& config.mcpServers.marrow.args.join(' ') === `-y ${MCP_PACKAGE_SPEC}`
|
|
667
|
+
))) observedHooks.push('mcp_tool_calls');
|
|
668
|
+
const fingerprintMaterial = changes
|
|
669
|
+
.filter((change) => change.applied || change.already_present)
|
|
670
|
+
.map((change) => `${change.label}:${crypto.createHash('sha256').update(safeRead(change.path)).digest('hex')}`)
|
|
671
|
+
.sort()
|
|
672
|
+
.join('|');
|
|
673
|
+
const configFingerprint = capabilityLevel === 'native_hooks'
|
|
674
|
+
? claudeNativeHookFingerprint(claudeSettings)
|
|
675
|
+
: crypto.createHash('sha256')
|
|
676
|
+
.update(`${client}:${capabilityLevel}:${expectedHooks.join(',')}:${fingerprintMaterial}`)
|
|
677
|
+
.digest('hex');
|
|
678
|
+
const complete = expectedHooks.length > 0 && expectedHooks.every((hook) => observedHooks.includes(hook));
|
|
679
|
+
const exactFix = complete
|
|
680
|
+
? null
|
|
681
|
+
: capabilityLevel === 'sdk_passive_runtime' && !sdkDependency.present
|
|
682
|
+
? `${sdkDependency.install_command} && npx @getmarrow/install --repair`
|
|
683
|
+
: capabilityLevel === 'governed_wrapper'
|
|
684
|
+
? `npx @getmarrow/install run --agent <agent-id> -- ${client}`
|
|
685
|
+
: 'npx @getmarrow/install --repair';
|
|
686
|
+
return {
|
|
687
|
+
adapter_version: adapterVersion,
|
|
688
|
+
capability_level: capabilityLevel,
|
|
689
|
+
config_fingerprint: configFingerprint,
|
|
690
|
+
expected_hooks: expectedHooks,
|
|
691
|
+
observed_hooks: observedHooks,
|
|
692
|
+
complete,
|
|
693
|
+
exact_fix: exactFix,
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
|
|
457
697
|
function upsertMcpServerConfig(filePath) {
|
|
458
698
|
const config = parseJsonObject(filePath);
|
|
459
699
|
const servers = config.mcpServers && typeof config.mcpServers === 'object' && !Array.isArray(config.mcpServers)
|
|
@@ -461,7 +701,7 @@ function upsertMcpServerConfig(filePath) {
|
|
|
461
701
|
: {};
|
|
462
702
|
servers.marrow = {
|
|
463
703
|
command: 'npx',
|
|
464
|
-
args: ['-y',
|
|
704
|
+
args: ['-y', MCP_PACKAGE_SPEC],
|
|
465
705
|
env: {
|
|
466
706
|
MARROW_API_KEY: '${MARROW_API_KEY}',
|
|
467
707
|
MARROW_BASE_URL: '${MARROW_BASE_URL}',
|
|
@@ -485,7 +725,7 @@ function inspectSdkDependency(detection) {
|
|
|
485
725
|
return {
|
|
486
726
|
required: true,
|
|
487
727
|
present: false,
|
|
488
|
-
install_command:
|
|
728
|
+
install_command: `npm install @getmarrow/sdk@${SDK_ADAPTER_VERSION}`,
|
|
489
729
|
warning: 'package.json could not be parsed; verify @getmarrow/sdk manually.',
|
|
490
730
|
};
|
|
491
731
|
}
|
|
@@ -496,11 +736,72 @@ function inspectSdkDependency(detection) {
|
|
|
496
736
|
packageJson.optionalDependencies,
|
|
497
737
|
packageJson.peerDependencies,
|
|
498
738
|
];
|
|
499
|
-
const
|
|
739
|
+
const declaredSpec = dependencyBlocks
|
|
740
|
+
.map((deps) => deps && typeof deps['@getmarrow/sdk'] === 'string' ? deps['@getmarrow/sdk'] : null)
|
|
741
|
+
.find(Boolean) || null;
|
|
742
|
+
const objectTargetsSdk = (value) => {
|
|
743
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
744
|
+
return Object.entries(value).some(([key, nested]) => (
|
|
745
|
+
key.includes('@getmarrow/sdk') || objectTargetsSdk(nested)
|
|
746
|
+
));
|
|
747
|
+
};
|
|
748
|
+
const overrideDetected = objectTargetsSdk(packageJson.overrides)
|
|
749
|
+
|| objectTargetsSdk(packageJson.resolutions)
|
|
750
|
+
|| objectTargetsSdk(packageJson.pnpm?.overrides);
|
|
751
|
+
let lockVerified = false;
|
|
752
|
+
try {
|
|
753
|
+
const lock = JSON.parse(safeRead(path.join(detection.root, 'package-lock.json')) || '{}');
|
|
754
|
+
const rootLock = lock?.packages?.[''];
|
|
755
|
+
const lockedSdk = lock?.packages?.['node_modules/@getmarrow/sdk'];
|
|
756
|
+
const lockedDeclaration = [
|
|
757
|
+
rootLock?.dependencies,
|
|
758
|
+
rootLock?.devDependencies,
|
|
759
|
+
rootLock?.optionalDependencies,
|
|
760
|
+
rootLock?.peerDependencies,
|
|
761
|
+
].map((deps) => deps && typeof deps['@getmarrow/sdk'] === 'string' ? deps['@getmarrow/sdk'] : null)
|
|
762
|
+
.find(Boolean) || null;
|
|
763
|
+
lockVerified = [2, 3].includes(lock?.lockfileVersion)
|
|
764
|
+
&& lockedDeclaration === declaredSpec
|
|
765
|
+
&& lockedSdk?.version === SDK_ADAPTER_VERSION
|
|
766
|
+
&& lockedSdk?.resolved === SDK_ADAPTER_TARBALL
|
|
767
|
+
&& lockedSdk?.integrity === SDK_ADAPTER_INTEGRITY;
|
|
768
|
+
} catch {
|
|
769
|
+
lockVerified = false;
|
|
770
|
+
}
|
|
771
|
+
const installedPackagePath = findUp(
|
|
772
|
+
detection.root,
|
|
773
|
+
[path.join('node_modules', '@getmarrow', 'sdk', 'package.json')],
|
|
774
|
+
);
|
|
775
|
+
let installedVersion = null;
|
|
776
|
+
let installedName = null;
|
|
777
|
+
try {
|
|
778
|
+
const installedPackage = installedPackagePath ? JSON.parse(safeRead(installedPackagePath)) : null;
|
|
779
|
+
installedVersion = typeof installedPackage?.version === 'string' ? installedPackage.version : null;
|
|
780
|
+
installedName = typeof installedPackage?.name === 'string' ? installedPackage.name : null;
|
|
781
|
+
} catch {
|
|
782
|
+
installedVersion = null;
|
|
783
|
+
installedName = null;
|
|
784
|
+
}
|
|
785
|
+
const declarationTrusted = typeof declaredSpec === 'string'
|
|
786
|
+
&& declaredSpec.trim().length > 0
|
|
787
|
+
&& /^[v0-9xX*<>=~^|.\s-]+$/.test(declaredSpec.trim());
|
|
788
|
+
const present = declarationTrusted
|
|
789
|
+
&& !overrideDetected
|
|
790
|
+
&& lockVerified
|
|
791
|
+
&& installedName === '@getmarrow/sdk'
|
|
792
|
+
&& installedVersion === SDK_ADAPTER_VERSION;
|
|
500
793
|
return {
|
|
501
794
|
required: true,
|
|
502
795
|
present,
|
|
503
|
-
|
|
796
|
+
declared: declaredSpec != null,
|
|
797
|
+
declared_spec: declaredSpec,
|
|
798
|
+
declaration_trusted: declarationTrusted,
|
|
799
|
+
override_detected: overrideDetected,
|
|
800
|
+
lock_verified: lockVerified,
|
|
801
|
+
installed_name: installedName,
|
|
802
|
+
installed_version: installedVersion,
|
|
803
|
+
expected_version: SDK_ADAPTER_VERSION,
|
|
804
|
+
install_command: present ? null : `npm install @getmarrow/sdk@${SDK_ADAPTER_VERSION}`,
|
|
504
805
|
};
|
|
505
806
|
}
|
|
506
807
|
|
|
@@ -569,8 +870,7 @@ function buildPlan(detection, options) {
|
|
|
569
870
|
}
|
|
570
871
|
|
|
571
872
|
function applyPlan(plan, options) {
|
|
572
|
-
const
|
|
573
|
-
for (const write of plan.writes) {
|
|
873
|
+
const prepared = plan.writes.map((write) => {
|
|
574
874
|
const before = safeRead(write.path);
|
|
575
875
|
let after;
|
|
576
876
|
if (write.type === 'file') {
|
|
@@ -587,6 +887,11 @@ function applyPlan(plan, options) {
|
|
|
587
887
|
throw new Error(`Unknown write type: ${write.type}`);
|
|
588
888
|
}
|
|
589
889
|
|
|
890
|
+
return { write, before, after };
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
const changes = [];
|
|
894
|
+
for (const { write, before, after } of prepared) {
|
|
590
895
|
const changed = before !== after;
|
|
591
896
|
const writeApplied = Boolean(options.yes && !options.dryRun && !options.doctor);
|
|
592
897
|
changes.push({
|
|
@@ -628,7 +933,6 @@ function isCanonicalTimestamp(value) {
|
|
|
628
933
|
|
|
629
934
|
function runtimeGateVerified(runtime) {
|
|
630
935
|
if (!runtime || typeof runtime !== 'object') return false;
|
|
631
|
-
if (runtime.ok === true) return true;
|
|
632
936
|
const gate = runtime.risk_gate;
|
|
633
937
|
if (!gate || typeof gate !== 'object') return false;
|
|
634
938
|
if (typeof gate.allow === 'boolean' || typeof gate.allowed === 'boolean') return true;
|
|
@@ -651,6 +955,8 @@ async function runSelfTest(options) {
|
|
|
651
955
|
'content-type': 'application/json',
|
|
652
956
|
'x-marrow-session-id': `install-${Date.now()}`,
|
|
653
957
|
'x-marrow-client': options.client || sourceClient(),
|
|
958
|
+
'x-marrow-package': '@getmarrow/install',
|
|
959
|
+
'x-marrow-package-version': INSTALLER_ADAPTER_VERSION,
|
|
654
960
|
};
|
|
655
961
|
if (options.agentId) headers['x-marrow-agent-id'] = options.agentId;
|
|
656
962
|
|
|
@@ -733,7 +1039,19 @@ async function runSelfTest(options) {
|
|
|
733
1039
|
const installValueMoment = buildInstallValueMoment(firstValueSignal, status, runtime, performance, firstValue, tokenValueProof);
|
|
734
1040
|
let activationReceipt = null;
|
|
735
1041
|
let activationVerified = false;
|
|
1042
|
+
let activationProfileReceipt = null;
|
|
736
1043
|
if (options.activation) {
|
|
1044
|
+
const activationAdapterVersion = options.activation.adapter_version || INSTALLER_ADAPTER_VERSION;
|
|
1045
|
+
const activationCapabilityLevel = options.activation.capability_level || 'event_contract';
|
|
1046
|
+
const activationExpectedHooks = Array.isArray(options.activation.expected_hooks) ? options.activation.expected_hooks : [];
|
|
1047
|
+
const activationConfigFingerprint = options.activation.config_fingerprint || crypto.createHash('sha256')
|
|
1048
|
+
.update(JSON.stringify({
|
|
1049
|
+
harness: options.activation.harness || options.client || 'custom',
|
|
1050
|
+
install_surface: options.activation.install_surface || 'unknown',
|
|
1051
|
+
adapter_version: activationAdapterVersion,
|
|
1052
|
+
expected_hooks: activationExpectedHooks,
|
|
1053
|
+
}))
|
|
1054
|
+
.digest('hex');
|
|
737
1055
|
activationReceipt = firstValue && firstValue.activation_receipt;
|
|
738
1056
|
const receiptValid = activationReceipt
|
|
739
1057
|
&& typeof activationReceipt === 'object'
|
|
@@ -750,7 +1068,58 @@ async function runSelfTest(options) {
|
|
|
750
1068
|
if (!receiptValid) {
|
|
751
1069
|
throw new Error('activation receipt did not verify the exact self-test decision, agent, runtime gate, and closed successful outcome');
|
|
752
1070
|
}
|
|
753
|
-
|
|
1071
|
+
activationProfileReceipt = await requestJson(`${baseUrl}/v1/agent/integrations/events`, {
|
|
1072
|
+
method: 'POST',
|
|
1073
|
+
headers,
|
|
1074
|
+
body: JSON.stringify({
|
|
1075
|
+
event_id: `activation-${activationConfigFingerprint.slice(0, 32)}`,
|
|
1076
|
+
event_type: 'activation_profile_registered',
|
|
1077
|
+
harness: options.activation.harness,
|
|
1078
|
+
agent_id: options.agentId,
|
|
1079
|
+
session_id: headers['x-marrow-session-id'],
|
|
1080
|
+
adapter_version: activationAdapterVersion,
|
|
1081
|
+
capability_level: activationCapabilityLevel,
|
|
1082
|
+
config_fingerprint: activationConfigFingerprint,
|
|
1083
|
+
expected_hooks: activationExpectedHooks,
|
|
1084
|
+
action: 'passive integration activation profile registered',
|
|
1085
|
+
occurred_at: new Date().toISOString(),
|
|
1086
|
+
}),
|
|
1087
|
+
});
|
|
1088
|
+
const profileCoverage = activationProfileReceipt?.activation_coverage;
|
|
1089
|
+
const profileReceipt = profileCoverage?.profile_receipt;
|
|
1090
|
+
const accountBindingId = profileCoverage?.account_binding_id;
|
|
1091
|
+
const expectedConfigurationBindingId = typeof accountBindingId === 'string'
|
|
1092
|
+
? crypto.createHash('sha256')
|
|
1093
|
+
.update(`agent-config-receipt:v2:${accountBindingId}:${activationConfigFingerprint}`)
|
|
1094
|
+
.digest('hex')
|
|
1095
|
+
: null;
|
|
1096
|
+
const expectedHooksMatch = Array.isArray(profileCoverage?.capture_coverage?.expected_hooks)
|
|
1097
|
+
&& [...profileCoverage.capture_coverage.expected_hooks].sort().join(',') === [...activationExpectedHooks].sort().join(',');
|
|
1098
|
+
const profileBound = Boolean(
|
|
1099
|
+
activationProfileReceipt?.accepted === true
|
|
1100
|
+
&& profileCoverage?.agent_id === options.agentId
|
|
1101
|
+
&& profileCoverage?.harness === options.activation.harness
|
|
1102
|
+
&& profileCoverage?.activation?.adapter_version === activationAdapterVersion
|
|
1103
|
+
&& profileCoverage?.activation?.capability_level === activationCapabilityLevel
|
|
1104
|
+
&& profileReceipt?.agent_id === options.agentId
|
|
1105
|
+
&& profileReceipt?.harness === options.activation.harness
|
|
1106
|
+
&& profileReceipt?.adapter_version === activationAdapterVersion
|
|
1107
|
+
&& profileReceipt?.capability_level === activationCapabilityLevel
|
|
1108
|
+
&& typeof accountBindingId === 'string'
|
|
1109
|
+
&& accountBindingId.length === 64
|
|
1110
|
+
&& profileReceipt?.account_binding_id === accountBindingId
|
|
1111
|
+
&& profileReceipt?.config_fingerprint_verified === true
|
|
1112
|
+
&& profileReceipt?.configuration_binding_id === expectedConfigurationBindingId
|
|
1113
|
+
&& profileReceipt?.expected_hooks_verified === true
|
|
1114
|
+
&& expectedHooksMatch
|
|
1115
|
+
);
|
|
1116
|
+
activationVerified = Boolean(
|
|
1117
|
+
firstValue.active
|
|
1118
|
+
&& runtimeGateVerified(runtime)
|
|
1119
|
+
&& (status.enabled ?? status.ok)
|
|
1120
|
+
&& options.activation.complete === true
|
|
1121
|
+
&& profileBound,
|
|
1122
|
+
);
|
|
754
1123
|
if (!activationVerified) {
|
|
755
1124
|
throw new Error('activation prerequisites were not all verified by the server');
|
|
756
1125
|
}
|
|
@@ -769,10 +1138,13 @@ async function runSelfTest(options) {
|
|
|
769
1138
|
runtime_before_you_act: runtime.before_you_act || null,
|
|
770
1139
|
activation_verified: activationVerified,
|
|
771
1140
|
activation_receipt: activationReceipt,
|
|
1141
|
+
activation_profile_receipt: activationProfileReceipt,
|
|
1142
|
+
activation_coverage: status.activation_coverage || firstValue.activation_coverage || null,
|
|
772
1143
|
first_value: firstValue && firstValue.ok !== false ? firstValue : null,
|
|
773
1144
|
first_value_signal: firstValueSignal,
|
|
774
1145
|
install_value_moment: installValueMoment,
|
|
775
1146
|
token_value_proof: tokenValueProof,
|
|
1147
|
+
client_update: status.client_update || runtime.client_update || runtime.status?.client_update || null,
|
|
776
1148
|
performance_proof: performance && performance.ok !== false ? {
|
|
777
1149
|
avoided_mistakes: performance.avoided_mistakes ?? performance.avoided_repeated_mistakes ?? 0,
|
|
778
1150
|
reused_winning_decisions: performance.reused_winning_decisions ?? 0,
|
|
@@ -916,7 +1288,7 @@ function printReport(report) {
|
|
|
916
1288
|
|
|
917
1289
|
process.stdout.write('\nPlanned changes:\n');
|
|
918
1290
|
for (const change of report.changes) {
|
|
919
|
-
const marker = change.
|
|
1291
|
+
const marker = change.applied ? 'wrote' : change.changed ? 'would write' : 'unchanged';
|
|
920
1292
|
process.stdout.write(`- ${marker}: ${change.label} (${change.path})\n`);
|
|
921
1293
|
}
|
|
922
1294
|
|
|
@@ -931,6 +1303,18 @@ function printReport(report) {
|
|
|
931
1303
|
process.stdout.write(`- one-call runtime: ${report.selfTest.runtime_active ? 'active' : 'not verified'}\n`);
|
|
932
1304
|
if (report.selfTest.error) process.stdout.write(`- error: ${report.selfTest.error}\n`);
|
|
933
1305
|
if (report.selfTest.next_action) process.stdout.write(`- next action: ${report.selfTest.next_action}\n`);
|
|
1306
|
+
const update = report.selfTest.client_update;
|
|
1307
|
+
const notification = update?.notification_state || update?.notification;
|
|
1308
|
+
if (update && (update.update_available === true || update.version_status === 'unknown' || notification === 'unknown' || notification === 'version_unknown' || notification === 'security_required')) {
|
|
1309
|
+
process.stdout.write('\nMarrow client update:\n');
|
|
1310
|
+
process.stdout.write(`- priority: ${notification === 'security_required' ? 'security_required' : notification === 'recommended' ? 'recommended' : update.version_status === 'unknown' || notification === 'unknown' || notification === 'version_unknown' ? 'version_unknown' : update.priority || 'recommended'}\n`);
|
|
1311
|
+
process.stdout.write(`- installed: ${update.installed_version || update.current_version || 'unknown'}\n`);
|
|
1312
|
+
process.stdout.write(`- latest: ${update.latest_version || 'unknown'}\n`);
|
|
1313
|
+
process.stdout.write('- automatic notification: yes\n');
|
|
1314
|
+
process.stdout.write('- automatic local mutation: no; operator policy applies\n');
|
|
1315
|
+
if (update.update_command || update.exact_update_command) process.stdout.write(`- update: ${update.update_command || update.exact_update_command}\n`);
|
|
1316
|
+
if (update.verification_command || update.exact_verification_command) process.stdout.write(`- verify: ${update.verification_command || update.exact_verification_command}\n`);
|
|
1317
|
+
}
|
|
934
1318
|
if (report.selfTest.first_value_signal) {
|
|
935
1319
|
process.stdout.write('\nFirst value:\n');
|
|
936
1320
|
const valueMoment = report.selfTest.install_value_moment;
|
|
@@ -1025,6 +1409,9 @@ async function install(options) {
|
|
|
1025
1409
|
if (options.activate && options.selfTest === false) {
|
|
1026
1410
|
throw new Error('activate requires the server self-test');
|
|
1027
1411
|
}
|
|
1412
|
+
if (options.repair && options.yes !== true && !options.dryRun && !options.doctor) {
|
|
1413
|
+
throw new Error('repair requires explicit write authorization (--yes)');
|
|
1414
|
+
}
|
|
1028
1415
|
const detection = detectEnvironment(options.cwd);
|
|
1029
1416
|
const plan = buildPlan(detection, options);
|
|
1030
1417
|
const writeMode = options.doctor ? 'doctor' : options.dryRun ? 'dry-run' : options.repair ? 'repair' : options.yes ? 'write' : 'dry-run';
|
|
@@ -1032,6 +1419,7 @@ async function install(options) {
|
|
|
1032
1419
|
const client = detectedClient(detection);
|
|
1033
1420
|
options.client = client;
|
|
1034
1421
|
if (!options.agentId) options.agentId = stableAgentId(detection.root, client);
|
|
1422
|
+
const profile = activationProfile(detection, plan, changes, client);
|
|
1035
1423
|
options.activation = options.activate ? {
|
|
1036
1424
|
harness: client,
|
|
1037
1425
|
agent_id: options.agentId,
|
|
@@ -1041,14 +1429,20 @@ async function install(options) {
|
|
|
1041
1429
|
.filter((change) => change.changed && change.applied && /hook|runtime|rule|instruction|config/i.test(change.label))
|
|
1042
1430
|
.map((change) => change.label)
|
|
1043
1431
|
.slice(0, 20),
|
|
1044
|
-
capture_verified: changes.every((change) => change.applied),
|
|
1432
|
+
capture_verified: changes.every((change) => change.applied || change.already_present),
|
|
1433
|
+
adapter_version: profile.adapter_version,
|
|
1434
|
+
capability_level: profile.capability_level,
|
|
1435
|
+
config_fingerprint: profile.config_fingerprint,
|
|
1436
|
+
expected_hooks: profile.expected_hooks,
|
|
1437
|
+
observed_hooks: profile.observed_hooks,
|
|
1438
|
+
complete: profile.complete,
|
|
1045
1439
|
intervention_verified: false,
|
|
1046
1440
|
closure_verified: false,
|
|
1047
1441
|
} : null;
|
|
1048
1442
|
const configInspection = inspectNpmTokenConfig();
|
|
1049
1443
|
const sdkDependency = inspectSdkDependency(detection);
|
|
1050
1444
|
const configDiagnostics = configInspection.safe;
|
|
1051
|
-
const configRepairs = options.repair && !options.dryRun && !options.doctor
|
|
1445
|
+
const configRepairs = options.repair && options.yes && !options.dryRun && !options.doctor
|
|
1052
1446
|
? repairConfigDiagnostics(configDiagnostics)
|
|
1053
1447
|
: [];
|
|
1054
1448
|
const envHints = options.apiKey ? [] : findLikelyEnvFiles(detection);
|
|
@@ -1063,7 +1457,7 @@ async function install(options) {
|
|
|
1063
1457
|
if (options.activate && !selfTest.activation_verified) {
|
|
1064
1458
|
throw new Error('Marrow activation failed: server confirmation was not returned');
|
|
1065
1459
|
}
|
|
1066
|
-
const changedConfig = changes.some((change) => change.
|
|
1460
|
+
const changedConfig = changes.some((change) => change.applied) || configRepairs.some((repair) => repair.changed);
|
|
1067
1461
|
const selfTestPassed = Boolean(!selfTest.skipped && selfTest.active && !selfTest.error);
|
|
1068
1462
|
const remediation = options.repair
|
|
1069
1463
|
? {
|
|
@@ -1098,6 +1492,7 @@ async function install(options) {
|
|
|
1098
1492
|
agent_id: options.agentId,
|
|
1099
1493
|
server_confirmed: Boolean(selfTest.activation_verified),
|
|
1100
1494
|
receipt: selfTest.activation_receipt || null,
|
|
1495
|
+
profile,
|
|
1101
1496
|
},
|
|
1102
1497
|
changes,
|
|
1103
1498
|
doctor: {
|
|
@@ -1150,4 +1545,8 @@ module.exports = {
|
|
|
1150
1545
|
buildInstallValueMoment,
|
|
1151
1546
|
buildTokenValueProof,
|
|
1152
1547
|
stableAgentId,
|
|
1548
|
+
activationProfile,
|
|
1549
|
+
claudeNativeHookFingerprint,
|
|
1550
|
+
printReport,
|
|
1551
|
+
HARNESS_CAPABILITY_REGISTRY,
|
|
1153
1552
|
};
|