@getmarrow/install 0.1.53 → 0.1.54

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.
Files changed (3) hide show
  1. package/README.md +11 -1
  2. package/package.json +1 -1
  3. package/src/installer.js +246 -17
package/README.md CHANGED
@@ -64,6 +64,12 @@ export MARROW_API_KEY=mrw_live_...
64
64
 
65
65
  The bare command is the default-on path: it detects and byte-idempotently writes managed configuration, runs the authenticated activation self-test, and starts the supported persistent controller. The key stays process-only and is never written to generated configuration or controller state. Use `--dry-run` for a non-writing preview, `doctor` for a read-only health check, or `--no-controller` to install and self-test without starting the controller. The explicit `activate` command remains equivalent and supported.
66
66
 
67
+ ### MCP tool profiles
68
+
69
+ Ordinary setup leaves `MARROW_TOOL_PROFILE` unset, which selects the documented 17-tool `primary` surface. Set `MARROW_TOOL_PROFILE=core` only for the legacy seven-tool minimal surface, or `MARROW_TOOL_PROFILE=full` for the complete advanced/legacy catalog. Explicit `primary`, `core`, and `full` values are accepted; any other value fails with an exact bounded repair and never falls back to a broader profile.
70
+
71
+ Tool visibility is not authorization. Every visible call still reaches Marrow's backend authentication, tenant, key-permission, plan, proof, and policy enforcement. `doctor --self-test` reports the configured and effective profile, the expected visible count, reloaded MCP visible names/count, and non-authorizing backend entitlement/upgrade projection. Until the owning harness restarts and matching MCP status is observed, actual visibility stays unavailable and the profile remains not-live. The self-test reads backend availability status; it does not invoke paid write tools to discover access.
72
+
67
73
  ## Keeping Marrow Current
68
74
 
69
75
  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.
@@ -93,7 +99,11 @@ npx @getmarrow/install controller stop
93
99
 
94
100
  Persistent controller lifecycle is currently Linux-only. On macOS or Windows, activation still writes supported configuration and verifies one server-side install self-test without certifying that hooks continuously ran; run `npx @getmarrow/install sidecar` under an owner-managed service and pass `--no-controller`. The controller does not silently upgrade packages, change governance policy, rotate credentials, or modify unrelated project configuration.
95
101
 
96
- ## What's New in v0.1.53
102
+ ## What's New in v0.1.54
103
+
104
+ v0.1.54 pins sealed MCP candidate `3.9.77` from source `1e782d8ba6bbb54bfaa322f0300565c7176f1969` and makes ordinary setup use the primary tool surface without writing a profile variable. Explicit `core` and `full` selections remain preserved, invalid values fail closed with a bounded repair, and human/JSON self-test output distinguishes configured/effective profile, expected visibility, actual post-reload visibility, and backend-projected entitlement state. Backend projections are status evidence only and always report `authorizes_calls: false`.
105
+
106
+ ## Previous: v0.1.53
97
107
 
98
108
  v0.1.53 pins the native-gate MCP candidate `3.9.75` and reconciles only Marrow-owned native hook surfaces. Codex uses `.codex/hooks.json`; Cursor and Composer use `.cursor/hooks.json`; Cline uses bounded executables under `.clinerules/hooks/`; Windsurf uses `.windsurf/hooks.json`; Gemini CLI receives named BeforeTool, AfterTool, and AfterAgent groups in `.gemini/settings.json`; Grok receives trusted global hooks in `~/.grok/hooks/marrow.json`. Grok PreToolUse validates strict private allow/deny JSON and fails closed with exit `2` when the child cannot provide an exact decision; PostToolUse/PostToolUseFailure emit compact results; one nonblocking Stop hook closes the turn with no duplicate SessionEnd hook. Grok hooks remain user-toggleable, so restart plus `/hooks` inspection is required and configuration never proves observed coverage.
99
109
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getmarrow/install",
3
- "version": "0.1.53",
3
+ "version": "0.1.54",
4
4
  "description": "Universal installer and governed runner for Marrow agent fleets.",
5
5
  "bin": {
6
6
  "marrow-install": "bin/marrow-install.js"
package/src/installer.js CHANGED
@@ -10,8 +10,9 @@ const { evidence: localControlEvidence } = require('./control-state');
10
10
  const DEFAULT_BASE_URL = 'https://api.getmarrow.ai';
11
11
  const MARROW_BLOCK_START = '<!-- marrow:passive-start -->';
12
12
  const MARROW_BLOCK_END = '<!-- marrow:passive-end -->';
13
- const MCP_ADAPTER_VERSION = '3.9.75';
14
- const MCP_ADAPTER_SOURCE_SHA = '7548a7ae2d7e95bb16c62470da41af06cb62c5c1';
13
+ const MCP_ADAPTER_VERSION = '3.9.77';
14
+ const MCP_ADAPTER_SOURCE_SHA = '1e782d8ba6bbb54bfaa322f0300565c7176f1969';
15
+ const MCP_ADAPTER_INTEGRITY = 'sha512-4SVvnyTaDh/mhVUry1LaGCYKXSB/mt5NCHijIOLP336jILSqe7ezjZ5/EwEOMTQ2JvLm6w0QE/jmGbcksqe86w==';
15
16
  const SDK_ADAPTER_VERSION = '3.7.62';
16
17
  const SDK_ADAPTER_INTEGRITY = 'sha512-n1i6Be09TpAQ9BPNRKY7aCvA2iSUPpJfw8djw2MELwpNbBCtKiZ29Jji77BK/6EFLUpSIcTW/Gmdf/ccf0JRYQ==';
17
18
  const SDK_ADAPTER_TARBALL = `https://registry.npmjs.org/@getmarrow/sdk/-/sdk-${SDK_ADAPTER_VERSION}.tgz`;
@@ -21,8 +22,8 @@ const ADAPTER_PROVENANCE = Object.freeze({
21
22
  package: '@getmarrow/mcp',
22
23
  version: MCP_ADAPTER_VERSION,
23
24
  source_sha: MCP_ADAPTER_SOURCE_SHA,
24
- integrity: null,
25
- integrity_state: 'registry_unavailable_until_publish',
25
+ integrity: MCP_ADAPTER_INTEGRITY,
26
+ integrity_state: 'sealed_local_candidate',
26
27
  }),
27
28
  sdk: Object.freeze({
28
29
  package: '@getmarrow/sdk',
@@ -91,6 +92,28 @@ const GEMINI_HOOK_TIMEOUT_MS = 5000;
91
92
  const GEMINI_CLOSEOUT_TIMEOUT_MS = 3000;
92
93
  const NATIVE_EXPECTED_HOOKS = ['prompt', 'pre_action', 'action_result', 'session_end'];
93
94
  const SOURCE_CLIENTS = new Set(['claude-code', 'cursor', 'composer', 'windsurf', 'openclaw', 'codex', 'gemini', 'grok', 'deepseek', 'qwen', 'kimi', 'minimax', 'cline', 'opencode', 'hermes', 'glm', 'mcp', 'ci', 'custom', 'unknown']);
95
+ const TOOL_PROFILES = new Set(['primary', 'core', 'full']);
96
+ const TOOL_PROFILE_EXPECTED_COUNTS = Object.freeze({ primary: 17, core: 7, full: null });
97
+ const TOOL_PROFILE_EXACT_FIX = 'Unset MARROW_TOOL_PROFILE to use primary, or set MARROW_TOOL_PROFILE=core or MARROW_TOOL_PROFILE=full, then restart the owning harness and run npx @getmarrow/install@latest doctor --self-test.';
98
+ const PRIMARY_TOOL_NAMES = Object.freeze([
99
+ 'marrow_agent_runtime',
100
+ 'marrow_arbitrate',
101
+ 'marrow_coordinate',
102
+ 'marrow_replay_compare',
103
+ 'marrow_decision_brief',
104
+ 'marrow_think',
105
+ 'marrow_commit',
106
+ 'marrow_workflow_gate',
107
+ 'marrow_completion_contracts',
108
+ 'marrow_evaluate_completion_contract',
109
+ 'marrow_agent_status',
110
+ 'marrow_value_report',
111
+ 'marrow_buyer_proof',
112
+ 'marrow_governance_timeline',
113
+ 'marrow_decision_trace',
114
+ 'marrow_fleet_lessons',
115
+ 'marrow_model_usage',
116
+ ]);
94
117
  const HARNESS_CAPABILITY_REGISTRY = Object.freeze([
95
118
  { client: 'claude-code', capability_level: 'native_hooks', automatic: ['prompt', 'pre_action', 'action_result', 'session_end'], install_surface: 'mcp' },
96
119
  { client: 'cursor', capability_level: 'native_hooks', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'mcp' },
@@ -118,6 +141,148 @@ function explicitMcpVersion(command) {
118
141
  return match ? match[1] : null;
119
142
  }
120
143
 
144
+ function resolveToolProfile(value) {
145
+ const structured = value !== null && typeof value === 'object' && !Array.isArray(value);
146
+ const candidate = structured
147
+ ? value.configured_profile
148
+ : value;
149
+ const absent = !structured && candidate === undefined;
150
+ const structuredUnset = structured && candidate === 'unset';
151
+ const configuredProfile = absent || structuredUnset
152
+ ? 'unset'
153
+ : candidate;
154
+ if (!absent && !structuredUnset && !TOOL_PROFILES.has(configuredProfile)) {
155
+ throw new Error(`Invalid MARROW_TOOL_PROFILE. ${TOOL_PROFILE_EXACT_FIX}`);
156
+ }
157
+ const effectiveProfile = configuredProfile === 'unset' ? 'primary' : configuredProfile;
158
+ return {
159
+ configured_profile: configuredProfile,
160
+ effective_profile: effectiveProfile,
161
+ expected_visible_count: TOOL_PROFILE_EXPECTED_COUNTS[effectiveProfile],
162
+ };
163
+ }
164
+
165
+ function normalizePrimaryToolAvailability(value) {
166
+ if (!value || typeof value !== 'object' || Array.isArray(value) || value.profile !== 'primary') return null;
167
+ const evidence = value.entitlement_evidence;
168
+ const counts = value.counts;
169
+ const tools = Array.isArray(value.tools) ? value.tools : [];
170
+ if (!evidence || typeof evidence !== 'object' || evidence.authorizing !== false) return null;
171
+ if (!['available', 'unavailable'].includes(evidence.state)) return null;
172
+ if (!counts || counts.total !== 17 || !Number.isInteger(counts.entitled) || !Number.isInteger(counts.upgrade_required)) return null;
173
+ if (counts.entitled + counts.upgrade_required !== counts.total || tools.length !== counts.total) return null;
174
+ const normalizedTools = [];
175
+ const seen = new Set();
176
+ for (const tool of tools) {
177
+ if (!tool || typeof tool !== 'object' || !PRIMARY_TOOL_NAMES.includes(tool.name) || seen.has(tool.name)) return null;
178
+ if (!['entitled', 'upgrade_required'].includes(tool.state) || typeof tool.always_available !== 'boolean') return null;
179
+ seen.add(tool.name);
180
+ normalizedTools.push({
181
+ name: tool.name,
182
+ state: tool.state,
183
+ always_available: tool.always_available,
184
+ plan_feature: typeof tool.plan_feature === 'string' ? tool.plan_feature : null,
185
+ minimum_plan: typeof tool.minimum_plan === 'string' ? tool.minimum_plan : null,
186
+ owner_management_url: typeof tool.owner_management_url === 'string' ? tool.owner_management_url : '',
187
+ });
188
+ }
189
+ if (PRIMARY_TOOL_NAMES.some((name) => !seen.has(name))) return null;
190
+ const entitled = normalizedTools.filter((tool) => tool.state === 'entitled').length;
191
+ const upgradeRequired = normalizedTools.filter((tool) => tool.state === 'upgrade_required').length;
192
+ if (entitled !== counts.entitled || upgradeRequired !== counts.upgrade_required) return null;
193
+ return {
194
+ profile: 'primary',
195
+ current_plan: typeof value.current_plan === 'string' ? value.current_plan : null,
196
+ owner_management_url: typeof value.owner_management_url === 'string' ? value.owner_management_url : '',
197
+ entitlement_evidence: {
198
+ state: evidence.state,
199
+ source: typeof evidence.source === 'string' ? evidence.source : 'entitlement_read_unavailable',
200
+ authoritative: evidence.authoritative === true,
201
+ authorizing: false,
202
+ },
203
+ counts: { total: 17, entitled, upgrade_required: upgradeRequired },
204
+ tools: normalizedTools,
205
+ };
206
+ }
207
+
208
+ function backendEntitlementProjection(statusProfile, contextProjection) {
209
+ const freshProjection = normalizePrimaryToolAvailability(contextProjection);
210
+ if (freshProjection) {
211
+ return {
212
+ evidence_state: freshProjection.entitlement_evidence.state,
213
+ source: 'authenticated_backend',
214
+ authorizes_calls: false,
215
+ primary_tool_availability: freshProjection,
216
+ };
217
+ }
218
+ const envelope = statusProfile?.backend_entitlement_projection;
219
+ const projected = normalizePrimaryToolAvailability(envelope?.primary_tool_availability);
220
+ const source = ['authenticated_backend', 'cached_or_stale_status', 'backend_projection_not_provided'].includes(envelope?.source)
221
+ ? envelope.source
222
+ : 'backend_projection_not_provided';
223
+ const available = envelope?.authorizes_calls === false
224
+ && envelope?.evidence_state === 'available'
225
+ && source === 'authenticated_backend'
226
+ && projected?.entitlement_evidence.state === 'available';
227
+ return {
228
+ evidence_state: available ? 'available' : 'unavailable',
229
+ source,
230
+ authorizes_calls: false,
231
+ primary_tool_availability: projected,
232
+ };
233
+ }
234
+
235
+ function buildMcpToolProfileReport(value, statusProfile = null, contextProjection = null, forceReload = false) {
236
+ const expected = resolveToolProfile(value);
237
+ const reportedNames = Array.isArray(statusProfile?.visible_tool_names)
238
+ ? statusProfile.visible_tool_names.filter((name) => typeof name === 'string')
239
+ : [];
240
+ const reportedCount = statusProfile?.visible_tool_count;
241
+ const reportedConfigured = statusProfile?.configured_profile;
242
+ const reportedEffective = statusProfile?.effective_profile;
243
+ const uniqueNames = new Set(reportedNames);
244
+ const profileIdentityMatches = reportedConfigured === expected.configured_profile
245
+ && reportedEffective === expected.effective_profile;
246
+ const reportedCatalogIsConsistent = Number.isInteger(reportedCount)
247
+ && reportedCount >= 0
248
+ && reportedNames.length === reportedCount
249
+ && uniqueNames.size === reportedCount;
250
+ const expectedCount = expected.effective_profile === 'full'
251
+ && profileIdentityMatches
252
+ && reportedCatalogIsConsistent
253
+ ? reportedCount
254
+ : expected.expected_visible_count;
255
+ const expectedPrimaryNames = expected.effective_profile !== 'primary'
256
+ || (reportedNames.length === PRIMARY_TOOL_NAMES.length
257
+ && PRIMARY_TOOL_NAMES.every((name) => uniqueNames.has(name)));
258
+ const visibilityLive = !forceReload
259
+ && profileIdentityMatches
260
+ && statusProfile?.local_visibility_grants_entitlement === false
261
+ && reportedCatalogIsConsistent
262
+ && reportedCount === expectedCount
263
+ && expectedPrimaryNames;
264
+ return {
265
+ configured_profile: expected.configured_profile,
266
+ effective_profile: expected.effective_profile,
267
+ expected_visible_count: expectedCount,
268
+ visible_tool_count: visibilityLive ? reportedCount : null,
269
+ actual_visible_count: visibilityLive ? reportedCount : null,
270
+ visible_tool_names: visibilityLive ? reportedNames : [],
271
+ local_visibility_grants_entitlement: false,
272
+ visibility_live: visibilityLive,
273
+ reload_required: !visibilityLive,
274
+ reported_configured_profile: typeof reportedConfigured === 'string' ? reportedConfigured : null,
275
+ reported_effective_profile: typeof reportedEffective === 'string' ? reportedEffective : null,
276
+ backend_entitlement_projection: backendEntitlementProjection(statusProfile, contextProjection),
277
+ };
278
+ }
279
+
280
+ function initialToolProfileReport(value) {
281
+ return {
282
+ ...buildMcpToolProfileReport(value),
283
+ };
284
+ }
285
+
121
286
  function readMcpPackageVersion(packageRoot) {
122
287
  try {
123
288
  const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
@@ -307,7 +472,7 @@ function sourceClient() {
307
472
  return aliases[raw] || (SOURCE_CLIENTS.has(raw) ? raw : 'custom');
308
473
  }
309
474
 
310
- function parseArgs(argv) {
475
+ function parseArgs(argv, env = process.env) {
311
476
  const options = {
312
477
  cwd: process.cwd(),
313
478
  yes: false,
@@ -315,9 +480,10 @@ function parseArgs(argv) {
315
480
  doctor: false,
316
481
  repair: false,
317
482
  mode: 'auto',
318
- apiKey: process.env.MARROW_API_KEY || '',
319
- baseUrl: process.env.MARROW_BASE_URL || DEFAULT_BASE_URL,
320
- agentId: process.env.MARROW_FLEET_AGENT_ID || process.env.MARROW_AGENT_ID || '',
483
+ apiKey: env.MARROW_API_KEY || '',
484
+ baseUrl: env.MARROW_BASE_URL || DEFAULT_BASE_URL,
485
+ agentId: env.MARROW_FLEET_AGENT_ID || env.MARROW_AGENT_ID || '',
486
+ toolProfile: resolveToolProfile(env.MARROW_TOOL_PROFILE),
321
487
  selfTest: true,
322
488
  selfTestExplicitlyDisabled: false,
323
489
  json: false,
@@ -439,6 +605,10 @@ Options:
439
605
  --agent-id <id> Agent/fleet id for self-test headers
440
606
  --no-controller Do not start the local background controller during install/repair
441
607
  --no-self-test Skip API smoke/self-test
608
+
609
+ Environment:
610
+ MARROW_TOOL_PROFILE Leave unset for primary (17 tools), or explicitly set primary, core, or full.
611
+ Visibility never grants entitlement; backend plans and permissions authorize calls.
442
612
  `;
443
613
  }
444
614
 
@@ -727,6 +897,7 @@ function passiveInstructions() {
727
897
  Marrow should run passively after install:
728
898
 
729
899
  - Use MCP plus these instructions in every workspace: \`npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp setup\`.
900
+ - Leave \`MARROW_TOOL_PROFILE\` unset for the 17-tool primary surface. Set \`MARROW_TOOL_PROFILE=core\` or \`MARROW_TOOL_PROFILE=full\` only as an explicit opt-in; backend plans and permissions still enforce access to every visible tool.
730
901
  - Use SDK passive runtime in owned Node processes: \`createPassiveRuntime().install()\`.
731
902
  - Native Claude hooks install only when \`.claude\` is present. Codex native hooks install into \`.codex/hooks.json\`; Cursor and Composer use \`.cursor/hooks.json\`; Cline uses non-overwriting executable files under \`.clinerules/hooks/\`; Windsurf uses \`.windsurf/hooks.json\`; Gemini CLI uses \`.gemini/settings.json\`; Grok uses trusted global \`~/.grok/hooks/marrow.json\`. Restart the host, enable/review hooks, disable Windsurf Restricted Mode where native hooks are required, and trust the workspace before claiming runtime coverage. The governed wrapper remains an explicit bounded fallback. Hermes, OpenClaw, and custom hosts need a bounded event adapter.
732
903
  - Keep passive token/model usage proof enabled. Empty savings stay zero until observed model usage lands. Do not invent token, cost, or time savings.
@@ -790,6 +961,7 @@ function envExample(options = {}) {
790
961
  MARROW_BASE_URL=${JSON.stringify(baseUrl)}
791
962
  MARROW_FLEET_AGENT_ID=${JSON.stringify(agentId)}
792
963
  MARROW_CLIENT=${JSON.stringify(client)}
964
+ # MARROW_TOOL_PROFILE is intentionally unset: ordinary setup uses primary. Set core or full only as an explicit opt-in.
793
965
  MARROW_ENFORCEMENT_MODE=auto
794
966
  MARROW_PASSIVE_BRIEF=auto
795
967
  MARROW_PASSIVE_VALUE_REPORT=true
@@ -1622,13 +1794,18 @@ function upsertMcpServerConfig(filePath, options = {}) {
1622
1794
  const servers = config.mcpServers && typeof config.mcpServers === 'object' && !Array.isArray(config.mcpServers)
1623
1795
  ? config.mcpServers
1624
1796
  : {};
1797
+ const existingProfile = resolveToolProfile(servers.marrow?.env?.MARROW_TOOL_PROFILE).configured_profile;
1798
+ const requestedProfile = resolveToolProfile(options.toolProfile).configured_profile;
1799
+ const configuredProfile = requestedProfile === 'unset' ? existingProfile : requestedProfile;
1800
+ const env = {
1801
+ MARROW_BASE_URL: baseUrl,
1802
+ MARROW_FLEET_AGENT_ID: agentId,
1803
+ };
1804
+ if (configuredProfile !== 'unset') env.MARROW_TOOL_PROFILE = configuredProfile;
1625
1805
  servers.marrow = {
1626
1806
  command: 'npx',
1627
1807
  args: ['-y', `--package=${MCP_PACKAGE_SPEC}`, 'marrow-mcp'],
1628
- env: {
1629
- MARROW_BASE_URL: baseUrl,
1630
- MARROW_FLEET_AGENT_ID: agentId,
1631
- },
1808
+ env,
1632
1809
  };
1633
1810
  config.mcpServers = servers;
1634
1811
  return JSON.stringify(config, null, 2) + '\n';
@@ -1919,7 +2096,7 @@ function buildPlan(detection, options) {
1919
2096
  type: 'json-transform',
1920
2097
  path: detection.paths.mcpJson,
1921
2098
  label: 'Project MCP server config',
1922
- transform: (filePath) => upsertMcpServerConfig(filePath, { agentId, baseUrl }),
2099
+ transform: (filePath) => upsertMcpServerConfig(filePath, { agentId, baseUrl, toolProfile: options.toolProfile }),
1923
2100
  });
1924
2101
  if (detection.cursor) {
1925
2102
  writes.push({
@@ -1932,7 +2109,7 @@ function buildPlan(detection, options) {
1932
2109
  type: 'json-transform',
1933
2110
  path: detection.paths.cursorMcp,
1934
2111
  label: 'Cursor MCP server config',
1935
- transform: (filePath) => upsertMcpServerConfig(filePath, { agentId, baseUrl }),
2112
+ transform: (filePath) => upsertMcpServerConfig(filePath, { agentId, baseUrl, toolProfile: options.toolProfile }),
1936
2113
  });
1937
2114
  }
1938
2115
  }
@@ -2106,12 +2283,14 @@ function runtimeGateVerified(runtime) {
2106
2283
  }
2107
2284
 
2108
2285
  async function runSelfTest(options) {
2109
- if (!options.selfTest) return { skipped: true, reason: 'disabled' };
2286
+ const initialProfile = initialToolProfileReport(options.toolProfile);
2287
+ if (!options.selfTest) return { skipped: true, reason: 'disabled', mcp_tool_profile: initialProfile };
2110
2288
  if (!options.apiKey) {
2111
2289
  return {
2112
2290
  skipped: true,
2113
2291
  reason: 'missing MARROW_API_KEY',
2114
2292
  exact_fix: 'export MARROW_API_KEY=mrw_live_... && npx @getmarrow/install --repair',
2293
+ mcp_tool_profile: initialProfile,
2115
2294
  };
2116
2295
  }
2117
2296
 
@@ -2157,6 +2336,14 @@ async function runSelfTest(options) {
2157
2336
  });
2158
2337
 
2159
2338
  const status = await requestJson(`${baseUrl}/v1/agent/status`, { headers });
2339
+ const context = await requestJson(`${baseUrl}/v1/agent/context`, { headers })
2340
+ .catch(() => null);
2341
+ const toolProfile = buildMcpToolProfileReport(
2342
+ options.toolProfile,
2343
+ status.mcp_tool_profile,
2344
+ context?.primary_tool_availability,
2345
+ Boolean(options.activation),
2346
+ );
2160
2347
  const runtime = await requestJson(`${baseUrl}/v1/agent/runtime`, {
2161
2348
  method: 'POST',
2162
2349
  headers,
@@ -2291,6 +2478,7 @@ async function runSelfTest(options) {
2291
2478
  }
2292
2479
  return {
2293
2480
  skipped: false,
2481
+ mcp_tool_profile: toolProfile,
2294
2482
  decision_id: decisionId,
2295
2483
  active: Boolean(status.enabled ?? status.ok),
2296
2484
  health: status.health || null,
@@ -2485,6 +2673,24 @@ function printReport(report) {
2485
2673
  }
2486
2674
 
2487
2675
  process.stdout.write('\nSelf-test:\n');
2676
+ const toolProfile = report.selfTest.mcp_tool_profile || report.toolProfile;
2677
+ if (toolProfile) {
2678
+ process.stdout.write(`- configured tool profile: ${toolProfile.configured_profile}\n`);
2679
+ process.stdout.write(`- effective tool profile: ${toolProfile.effective_profile}\n`);
2680
+ process.stdout.write(`- expected visible tools: ${toolProfile.expected_visible_count == null ? 'complete catalog (awaiting reloaded MCP count)' : toolProfile.expected_visible_count}\n`);
2681
+ process.stdout.write(`- actual visible tools: ${toolProfile.actual_visible_count == null ? 'unavailable until process reload' : toolProfile.actual_visible_count}\n`);
2682
+ process.stdout.write(`- visible tool names: ${toolProfile.visibility_live ? toolProfile.visible_tool_names.join(', ') : 'unavailable until process reload'}\n`);
2683
+ process.stdout.write(`- profile live: ${toolProfile.visibility_live ? 'yes' : 'no'}\n`);
2684
+ const projection = toolProfile.backend_entitlement_projection;
2685
+ const availability = projection?.primary_tool_availability;
2686
+ if (projection?.evidence_state === 'available' && availability?.entitlement_evidence?.state === 'available') {
2687
+ process.stdout.write(`- backend-projected entitled tools: ${availability.counts.entitled}\n`);
2688
+ process.stdout.write(`- backend-projected upgrade-required tools: ${availability.counts.upgrade_required}\n`);
2689
+ process.stdout.write(`- backend projection source: ${projection.source}; authorizes calls: no\n`);
2690
+ } else {
2691
+ process.stdout.write(`- backend-projected entitlements: unavailable (source: ${projection?.source || 'backend_projection_not_provided'}; non-authorizing)\n`);
2692
+ }
2693
+ }
2488
2694
  if (report.selfTest.skipped) {
2489
2695
  process.stdout.write(`- skipped: ${report.selfTest.reason}\n`);
2490
2696
  if (report.selfTest.exact_fix) process.stdout.write(`- exact fix: ${report.selfTest.exact_fix}\n`);
@@ -2650,6 +2856,9 @@ async function install(options) {
2650
2856
  if (options.repair && options.yes !== true && !options.dryRun && !options.doctor) {
2651
2857
  throw new Error('repair requires explicit write authorization (--yes)');
2652
2858
  }
2859
+ options.toolProfile = resolveToolProfile(options.toolProfile === undefined
2860
+ ? process.env.MARROW_TOOL_PROFILE
2861
+ : options.toolProfile);
2653
2862
  const detection = detectEnvironment(options.cwd);
2654
2863
  const client = detectedClient(detection);
2655
2864
  options.client = client;
@@ -2696,11 +2905,27 @@ async function install(options) {
2696
2905
  } catch (error) {
2697
2906
  const message = error instanceof Error ? error.message : String(error);
2698
2907
  if (options.activate) throw new Error(`Marrow activation failed: ${message}`);
2699
- selfTest = { skipped: false, active: false, error: message };
2908
+ selfTest = {
2909
+ skipped: false,
2910
+ active: false,
2911
+ error: message,
2912
+ mcp_tool_profile: initialToolProfileReport(options.toolProfile),
2913
+ };
2700
2914
  }
2701
2915
  if (options.activate && !selfTest.activation_verified) {
2702
2916
  throw new Error('Marrow activation failed: server confirmation was not returned');
2703
2917
  }
2918
+ const harnessReload = harnessReloadPlan(detection, changes);
2919
+ if (harnessReload.required && selfTest.mcp_tool_profile) {
2920
+ selfTest.mcp_tool_profile = {
2921
+ ...selfTest.mcp_tool_profile,
2922
+ visible_tool_count: null,
2923
+ actual_visible_count: null,
2924
+ visible_tool_names: [],
2925
+ visibility_live: false,
2926
+ reload_required: true,
2927
+ };
2928
+ }
2704
2929
  const changedConfig = changes.some((change) => change.applied) || configRepairs.some((repair) => repair.changed);
2705
2930
  const selfTestPassed = Boolean(!selfTest.skipped && selfTest.active && !selfTest.error);
2706
2931
  const controllerPlatform = options.controllerPlatform || process.platform;
@@ -2762,6 +2987,7 @@ async function install(options) {
2762
2987
  adapterProvenance: ADAPTER_PROVENANCE,
2763
2988
  mode: plan.mode,
2764
2989
  writeMode,
2990
+ toolProfile: selfTest.mcp_tool_profile || initialToolProfileReport(options.toolProfile),
2765
2991
  detected: {
2766
2992
  node: detection.node,
2767
2993
  python: detection.python,
@@ -2784,7 +3010,7 @@ async function install(options) {
2784
3010
  receipt: selfTest.activation_receipt || null,
2785
3011
  profile,
2786
3012
  },
2787
- harnessReload: harnessReloadPlan(detection, changes),
3013
+ harnessReload,
2788
3014
  firstCapture: firstCapturePath(detection, options.agentId),
2789
3015
  changes,
2790
3016
  doctor: {
@@ -2866,6 +3092,9 @@ module.exports = {
2866
3092
  GROK_SESSION_END_HOOK_COMMAND,
2867
3093
  GROK_NATIVE_HOOK_MATCHER,
2868
3094
  printReport,
3095
+ buildMcpToolProfileReport,
3096
+ resolveToolProfile,
3097
+ PRIMARY_TOOL_NAMES,
2869
3098
  ADAPTER_PROVENANCE,
2870
3099
  HARNESS_CAPABILITY_REGISTRY,
2871
3100
  defaultHarnessInstallMatrix,