@bridge_gpt/mcp-server 0.2.38 → 0.2.41

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 (85) hide show
  1. package/README.md +189 -14
  2. package/build/agent-capabilities/probe-context.js +2 -1
  3. package/build/agent-launchers/claude-executor-adapter.js +392 -0
  4. package/build/agent-launchers/executor-adapter-inspection.js +163 -0
  5. package/build/agent-launchers/executor-adapter-registry.js +90 -0
  6. package/build/agent-launchers/executor-adapter.js +136 -0
  7. package/build/agent-registry.js +28 -0
  8. package/build/agents.generated.js +1 -1
  9. package/build/claude-login.js +85 -0
  10. package/build/claude-user-config-doctor.js +59 -33
  11. package/build/commands.generated.js +12 -11
  12. package/build/conduct-epic/bridge-client.js +345 -0
  13. package/build/conduct-epic/checkpoint-store.js +479 -0
  14. package/build/conduct-epic/cli.js +1765 -0
  15. package/build/conduct-epic/lock.js +302 -0
  16. package/build/conduct-epic/pr-state.js +286 -0
  17. package/build/conduct-epic/spawn.js +101 -0
  18. package/build/conductor/bridge-api-client.js +37 -2
  19. package/build/conductor/doctor.js +11 -1
  20. package/build/conductor/install-doctor.js +184 -10
  21. package/build/conductor-bin.js +7 -7
  22. package/build/credential-store.js +10 -4
  23. package/build/credentials-cli.js +34 -19
  24. package/build/docs.generated.js +1 -1
  25. package/build/doctor.js +579 -88
  26. package/build/executor/agent-identity.js +32 -0
  27. package/build/executor/cli.js +50 -39
  28. package/build/executor/deps.js +15 -1
  29. package/build/executor/env.js +56 -45
  30. package/build/executor/index.js +9 -1
  31. package/build/executor/install-preflight.js +138 -0
  32. package/build/executor/job-errors.js +200 -0
  33. package/build/executor/job-runner.js +619 -268
  34. package/build/executor/observation.js +165 -0
  35. package/build/executor/permissions.js +163 -36
  36. package/build/executor/platform.js +54 -0
  37. package/build/executor/preflight.js +175 -67
  38. package/build/executor/process.js +39 -7
  39. package/build/executor/runner.js +19 -0
  40. package/build/executor/service-lifecycle.js +269 -0
  41. package/build/executor/service-unit.js +121 -12
  42. package/build/executor/stale-artifacts.js +70 -0
  43. package/build/executor/test-clock.js +188 -24
  44. package/build/executor/worker-command.js +22 -58
  45. package/build/executor/worker-log.js +82 -0
  46. package/build/executor/worktree-lock.js +264 -0
  47. package/build/index.js +527 -357
  48. package/build/install-bridge-conductor.js +376 -38
  49. package/build/install-bridge.js +414 -114
  50. package/build/install-doctor.js +13 -0
  51. package/build/install-reexec.js +5 -3
  52. package/build/mcp-install-state.js +130 -0
  53. package/build/mcp-profile.js +11 -2
  54. package/build/mcp-provisioning.js +15 -0
  55. package/build/merge-pull-request.js +562 -0
  56. package/build/phase-result-artifacts.js +450 -0
  57. package/build/pipeline-orchestrator.js +4 -0
  58. package/build/pipeline-utils.js +16 -0
  59. package/build/pipelines.generated.js +7 -7
  60. package/build/plane/preflight.js +18 -14
  61. package/build/plane/supervisor.js +8 -1
  62. package/build/project-root.js +34 -0
  63. package/build/readme.generated.js +1 -1
  64. package/build/run-unit-tests-launcher.js +36 -9
  65. package/build/setup-epic.js +57 -4
  66. package/build/sfcc/permissions.js +25 -6
  67. package/build/sfcc/reads-site-preference.js +6 -0
  68. package/build/sfcc/register.js +61 -23
  69. package/build/sfcc/registration-inventory.js +89 -0
  70. package/build/sfcc/setup-status.js +18 -34
  71. package/build/sfcc/tool-wrapper.js +294 -17
  72. package/build/sfcc/write-grants.js +33 -1
  73. package/build/sfcc/write-guard.js +41 -12
  74. package/build/sfcc/writes-custom-object-def.js +6 -2
  75. package/build/sfcc/writes-site-preference.js +6 -1
  76. package/build/sfcc/writes-system-object.js +11 -2
  77. package/build/sfcc/writes.js +13 -8
  78. package/build/start-tickets-prereqs.js +25 -15
  79. package/build/start-tickets.js +123 -21
  80. package/build/version.generated.js +1 -1
  81. package/build/worktree-core.js +9 -3
  82. package/docs/install/mcp-tool-integrations.md +54 -9
  83. package/docs/install/sfcc-integration.md +71 -24
  84. package/package.json +3 -3
  85. package/build/executor/worker-config-isolation.js +0 -287
@@ -65,6 +65,7 @@ export const MODULE_MOCK_MANIFEST = [
65
65
  "build/sfcc/reads-site-preference.test.js",
66
66
  "build/sfcc/reads-system-object.test.js",
67
67
  "build/sfcc/register.test.js",
68
+ "build/sfcc/registration-boundary.test.js",
68
69
  "build/sfcc/setup-status.test.js",
69
70
  "build/sfcc/tool-wrapper.test.js",
70
71
  "build/sfcc/write-gate-rejection.test.js",
@@ -96,14 +97,24 @@ export const FORCE_EXIT_QUARANTINE = ["build/secret-safety.test.js"];
96
97
  // normal unit suite.
97
98
  const INTEGRATION_DIR_PREFIX = "build/integration/";
98
99
  const INTEGRATION_DIR_ALLOWLIST = new Set(["build/integration/measure-tools.test.js"]);
99
- // Pre-existing compiled test files that were not wired into ANY script before
100
- // this launcher existed (discovered while building it). BAPI-683 preserves
101
- // the exact current running test set rather than silently picking these up —
102
- // see docs/BAPI-683-mcp-ci-windows-npm-test-launcher.md for the follow-up.
103
- const KNOWN_UNREGISTERED_ORPHANS = new Set([
100
+ // Compiled test files that live OUTSIDE build/integration/ but still belong to
101
+ // the separately-invoked `npm run test:integration` lane, so the unit lane must
102
+ // skip them. BAPI-683 originally introduced this set as
103
+ // KNOWN_UNREGISTERED_ORPHANS: two pre-existing files that were wired into no
104
+ // script at all, preserved as-is rather than silently picked up. BAPI-822
105
+ // retired that state — every entry here is now named explicitly by
106
+ // scripts.test:integration, and validateManifest below fails if an entry
107
+ // disappears from the build output.
108
+ //
109
+ // Only add a file here when it genuinely needs the integration lane (an
110
+ // MCP_INTEGRATION=1 gate, real subprocesses, or an optional native dependency
111
+ // like better-sqlite3). A plain compiled test outside build/integration/ is
112
+ // discovered into the unit lane automatically and needs no registration —
113
+ // tests/pytest/mcp/test_mcp_server_test_registration_multicomponent.py fails if
114
+ // a *.integration.test.ts ends up in neither lane.
115
+ export const INTEGRATION_LANE_ONLY = [
104
116
  "build/conductor/conductor-runtime.integration.test.js",
105
- "build/sfcc/register-site-preference.integration.test.js",
106
- ]);
117
+ ];
107
118
  /** Recursively lists compiled `*.test.js` files under `<root>/<buildDir>`, sorted and package-relative. */
108
119
  export function discoverTestFiles(options) {
109
120
  const { root, buildDir = DEFAULT_BUILD_DIR, fsImpl = { readdirSync } } = options;
@@ -133,7 +144,7 @@ export function discoverTestFiles(options) {
133
144
  results.sort();
134
145
  return results;
135
146
  }
136
- /** Throws if the module-mock or force-exit-quarantine manifest has drifted from the discovered build output. */
147
+ /** Throws if the module-mock, force-exit-quarantine, or integration-lane manifest has drifted from the discovered build output. */
137
148
  export function validateManifest(discovered) {
138
149
  const discoveredSet = new Set(discovered);
139
150
  const seen = new Set();
@@ -157,6 +168,21 @@ export function validateManifest(discovered) {
157
168
  `(rebuild with 'npm run build', or remove it from FORCE_EXIT_QUARANTINE if the file was deleted/renamed)`);
158
169
  }
159
170
  }
171
+ // The integration-lane set is the one exclusion that removes a discovered
172
+ // file from every unit batch, so a stale entry is invisible by construction:
173
+ // the excluded file simply stops running and nothing reports it. Validating
174
+ // it against the build output turns that into a loud failure (BAPI-822).
175
+ for (const entry of INTEGRATION_LANE_ONLY) {
176
+ if (seen.has(entry)) {
177
+ throw new Error(`run-unit-tests: integration-lane entry duplicated or also present in another manifest: ${entry}`);
178
+ }
179
+ seen.add(entry);
180
+ if (!discoveredSet.has(entry)) {
181
+ throw new Error(`run-unit-tests: integration-lane entry not found in build output: ${entry} ` +
182
+ `(rebuild with 'npm run build', or remove it from INTEGRATION_LANE_ONLY — and from ` +
183
+ `scripts.test:integration — if the file was deleted/renamed)`);
184
+ }
185
+ }
160
186
  }
161
187
  /** Selects and validates the requested group's file list from the discovered inventory. */
162
188
  export function selectTestGroup(mode, discovered) {
@@ -165,12 +191,13 @@ export function selectTestGroup(mode, discovered) {
165
191
  }
166
192
  validateManifest(discovered);
167
193
  const moduleMockSet = new Set(MODULE_MOCK_MANIFEST);
194
+ const integrationLaneSet = new Set(INTEGRATION_LANE_ONLY);
168
195
  const normal = discovered.filter((file) => {
169
196
  if (moduleMockSet.has(file))
170
197
  return false;
171
198
  if (file.startsWith(INTEGRATION_DIR_PREFIX) && !INTEGRATION_DIR_ALLOWLIST.has(file))
172
199
  return false;
173
- if (KNOWN_UNREGISTERED_ORPHANS.has(file))
200
+ if (integrationLaneSet.has(file))
174
201
  return false;
175
202
  return true;
176
203
  });
@@ -38,6 +38,10 @@ import os from "node:os";
38
38
  import readline from "node:readline";
39
39
  import { approveEpicPlan, createEpicRunWithDisposition, fetchEpicRunState, replaceEpicRunPolicy, resolveConductorBridgeApiAccess, storeEpicPlan, validateEpicPlan, ConductorBridgeApiError, } from "./conductor/bridge-api-client.js";
40
40
  import { validateBranchName } from "./base-ref.js";
41
+ // BAPI-814/E1a: reused from the conduct-epic client on purpose. The override is
42
+ // ONE repository-wide row with ONE reader; adding a second wrapper under
43
+ // `conductor/` would give the v2 path its own copy to drift from.
44
+ import { getIndexBranch } from "./conduct-epic/bridge-client.js";
41
45
  import { hashPlan } from "./conductor/plan.js";
42
46
  /** Accepted `policy_json.review_policy.source` values (the `ReviewPolicy` surface). */
43
47
  export const SETUP_EPIC_REVIEW_POLICY_SOURCES = [
@@ -45,6 +49,36 @@ export const SETUP_EPIC_REVIEW_POLICY_SOURCES = [
45
49
  "native_review_decision",
46
50
  "none",
47
51
  ];
52
+ /**
53
+ * The warning lines for an active repository-wide index-branch override, or an
54
+ * empty array when there is nothing to say (BAPI-814/E1a).
55
+ *
56
+ * Fail-open by construction. This is optional enrichment layered onto a command
57
+ * whose real job is creating an epic run, so an unreadable override must not
58
+ * become a preflight failure: an unavailable lookup produces one bounded,
59
+ * sanitized diagnostic and `setup-epic` continues. The client already returns a
60
+ * sanitized `error` string and never the raw service body, so nothing further
61
+ * needs stripping here.
62
+ */
63
+ async function describeActiveIndexOverride(access, deps) {
64
+ const result = await getIndexBranch(access, deps.fetch);
65
+ if (!result.ok) {
66
+ return [
67
+ ` [warn] The repository's indexed-branch override could not be read: ${result.error}. ` +
68
+ `Continuing — this is optional context, not a prerequisite.`,
69
+ ];
70
+ }
71
+ const override = result.value.override;
72
+ if (override === null)
73
+ return [];
74
+ return [
75
+ ` [warn] ${access.repoName} has an ACTIVE index-branch override: ` +
76
+ `'${override.override_branch}'. Repository parses currently index that branch, ` +
77
+ `and the base-branch config field this run may fall back on resolves through the ` +
78
+ `same override.`,
79
+ ` Setup is CONTINUING — this is a warning, not a refusal.`,
80
+ ];
81
+ }
48
82
  /** Echoed single-line prompt on stderr (mirrors connect-github's helper). */
49
83
  function defaultPromptLine(promptText) {
50
84
  return new Promise((resolve) => {
@@ -102,9 +136,13 @@ export function getSetupEpicUsage() {
102
136
  " --review-policy <src> PER-RUN review policy source, one of:",
103
137
  ` ${SETUP_EPIC_REVIEW_POLICY_SOURCES.join(", ")}.`,
104
138
  " Composed into policy_json.review_policy on create.",
105
- " This setting is per-run: repository-level review-policy",
106
- " defaults are NOT persisted in supervisor project",
107
- " defaults yet that is BAPI-694.",
139
+ " This flag sets the policy for THIS run only and never",
140
+ " changes a repository default. To persist one, pass",
141
+ " --review-policy to `install-bridge conductor`, which",
142
+ " stores it as the supervisor project default; that",
143
+ " default is stamped into a new run only when the run",
144
+ " sets no review policy of its own, so a value supplied",
145
+ " here always wins.",
108
146
  " --json Emit a single JSON result object on stdout",
109
147
  " -h, --help Show this help",
110
148
  "",
@@ -981,6 +1019,21 @@ export async function runSetupEpicCli(argv, overrides = {}) {
981
1019
  say(`Local hash: ${localHash}`);
982
1020
  for (const w of warnings)
983
1021
  say(` [warn] ${w}`);
1022
+ // --- Active index-branch override (BAPI-814/E1a) -------------------------
1023
+ // Non-mutating enrichment, placed once credentials and repository identity are
1024
+ // both known and before any run-state read or mutating request.
1025
+ //
1026
+ // WARNING ONLY, never a refusal. The override is repository-wide: while it is
1027
+ // active every parse indexes that branch, and the create-PR config-field
1028
+ // fallback and feature-branch provisioning read the same overridden
1029
+ // `config_code_repositories.base_branch`. A v2 operator setting up an epic in
1030
+ // that repository needs to know, but the condition is legitimate — it is
1031
+ // usually another epic mid-flight — so setup continues.
1032
+ //
1033
+ // Emitted in `--dry-run` too: the condition already exists in the repository
1034
+ // and is independent of whether this invocation writes anything.
1035
+ for (const line of await describeActiveIndexOverride(access, deps))
1036
+ say(line);
984
1037
  // --- Feature branch selection (BAPI-655) --------------------------------
985
1038
  // Resolved AFTER local/access context is known but BEFORE any run-state read
986
1039
  // or mutating request, so malformed interactive input fails before network
@@ -1171,7 +1224,7 @@ export async function runSetupEpicCli(argv, overrides = {}) {
1171
1224
  say(` - feature branch: ${featureBranch} (create from repository base branch; no request made in dry-run)`);
1172
1225
  }
1173
1226
  if (opts.reviewPolicy !== undefined) {
1174
- say(` - review policy: ${opts.reviewPolicy} (per-run policy_json.review_policy; not persisted as a repository default until BAPI-694)`);
1227
+ say(` - review policy: ${opts.reviewPolicy} (per-run policy_json.review_policy; sets no repository default use \`install-bridge conductor --review-policy\` for that)`);
1175
1228
  }
1176
1229
  say(` - POST /jira/epic-runs/runs/${opts.epicKey}/plan (v${plan.plan_version})`);
1177
1230
  say(` - POST /jira/epic-runs/runs/${opts.epicKey}/approve-plan (v${plan.plan_version})`);
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Issues a harmless GET /system_object_definitions probe. On 200: reports the
5
5
  * detected OCAPI version. On 401/403: prints the exact OCAPI Settings JSON to
6
- * paste in Business Manager, split into read-only (v1) vs. write/import (v2)
7
- * grant blocks.
6
+ * paste in Business Manager, split into read/search grants (for the `sfcc` read
7
+ * tools) and mutation grants (for the nine shipped `sfcc-write` tools).
8
8
  */
9
9
  import { ocapiGet } from "./client.js";
10
10
  import { normalizeOcapiPage } from "./ocapi-shape.js";
@@ -14,7 +14,7 @@ import { formatOcapiWriteGrantJson } from "./write-grants.js";
14
14
  // OCAPI Settings JSON template
15
15
  // Operators paste this in: Administration > Site Development > Open Commerce API Settings → Data API tab.
16
16
  // ---------------------------------------------------------------------------
17
- const OCAPI_SETTINGS_READ_ONLY = (ocapiVersion) => JSON.stringify({
17
+ const OCAPI_SETTINGS_READ_SEARCH = (ocapiVersion) => JSON.stringify({
18
18
  _v: ocapiVersion,
19
19
  clients: [
20
20
  {
@@ -138,12 +138,31 @@ function formatProbeCount(normalized) {
138
138
  }
139
139
  return `${returned} items returned on this page (OCAPI did not provide the instance total)`;
140
140
  }
141
+ /**
142
+ * Render the two grant blocks an operator pastes in Business Manager.
143
+ *
144
+ * Both labels were wrong before BAPI-817, in opposite directions:
145
+ *
146
+ * - The first block was labelled **"READ-ONLY GRANTS"** while actually
147
+ * granting `methods: ["get","post"]` with `write_attributes: "(**)"` across
148
+ * three resource families. An operator pasted a grant they had been told was
149
+ * read-only. It is renamed to read/search, which is what it is — the `post`
150
+ * is OCAPI's search convention (`*_search` endpoints are POSTs), not a
151
+ * mutation, but calling that "read-only" is still a claim about a grant the
152
+ * operator cannot verify without reading OCAPI's own semantics.
153
+ * - The second was labelled **"v2 — forward-looking"** for capabilities that
154
+ * had already shipped. All nine write tools exist; the grant is required
155
+ * today by anyone enabling `sfcc-write`, not speculative future work.
156
+ *
157
+ * A label that understates a grant is worse than no label, because the operator
158
+ * stops reading the JSON once the heading has told them it is safe.
159
+ */
141
160
  function ocapiSettingsInstructions(ocapiVersion) {
142
161
  return (`To grant OCAPI access, paste the JSON below in Business Manager:\n` +
143
162
  ` Administration > Site Development > Open Commerce API Settings → Data API tab\n\n` +
144
- `--- READ-ONLY GRANTS (v1 required now) ---\n` +
145
- `${OCAPI_SETTINGS_READ_ONLY(ocapiVersion)}\n\n` +
146
- `--- WRITE/IMPORT GRANTS (v2 forward-looking, paste once) ---\n` +
163
+ `--- READ/SEARCH TOOL GRANTS (required for the sfcc read tools) ---\n` +
164
+ `${OCAPI_SETTINGS_READ_SEARCH(ocapiVersion)}\n\n` +
165
+ `--- MUTATION GRANTS (required for the nine sfcc-write tools) ---\n` +
147
166
  `${OCAPI_SETTINGS_WRITE_IMPORT(ocapiVersion)}\n\n` +
148
167
  `Replace <YOUR_CLIENT_ID> with the client_id from your dw.json.`);
149
168
  }
@@ -113,6 +113,12 @@ async function saveAndReturn(text, dir, filename, page) {
113
113
  * an API version while OCAPI simultaneously reports `_v: "25.6"` in the same
114
114
  * session; the constraint belongs to this Bridge tool, not to a version. The
115
115
  * status, code, source, decision, and `details` payload are untouched.
116
+ *
117
+ * ⚠️ BAPI-817: like `write-guard.ts#rejectIfNotSandboxForWrite`, this checks a
118
+ * caller-supplied OCAPI instance-CONTEXT argument, NOT the host that will be
119
+ * contacted. It is secondary. The instance actually reached is decided by the
120
+ * resolved-hostname boundary in `tool-wrapper.ts`, which runs before this and
121
+ * does not consult `instance`.
116
122
  */
117
123
  function rejectIfNotSandbox(instance) {
118
124
  if (instance !== "sandbox") {
@@ -5,10 +5,32 @@
5
5
  * SFCC tools with a single call adjacent to `registerConductorTools`. All
6
6
  * SFCC business logic lives inside the sfcc/ module; index.ts stays a
7
7
  * thin registration/composition layer.
8
+ *
9
+ * ---------------------------------------------------------------------------
10
+ * Two structural properties this module owns (BAPI-817)
11
+ * ---------------------------------------------------------------------------
12
+ *
13
+ * **1. The target boundary is unavoidable.** The raw host registrar is used in
14
+ * exactly one place — `registerSfccTool`, the adapter below — and every other
15
+ * registration in this module and in every child registrar goes through that
16
+ * adapter. So the resolved-hostname sandbox check runs for all twenty tools:
17
+ * the two always-on diagnostics, the eight reads (including the five with no
18
+ * `instance` parameter at all), the nine writes, and `sfcc_log_query`. Asking
19
+ * each handler to remember a check is what produced the unguarded reads in the
20
+ * first place; wrapping the registrar removes the opportunity to forget.
21
+ *
22
+ * **2. Reads and writes are separately gated.** Before BAPI-817 one boolean
23
+ * (`includeReadTools`, from `ACTIVE_GROUPS.has("sfcc")`) registered eighteen
24
+ * tools, nine of them destructive — while the README described the group as
25
+ * "the 7 heavy SFCC read tools" and stated "All SFCC tools are read-only".
26
+ * Enabling introspection silently enabled mutation. This module now takes two
27
+ * independent flags so the `sfcc` profile is genuinely read-oriented and the
28
+ * nine writes require the deliberate `sfcc-write` opt-in (the ticket's
29
+ * Decision 2(c)).
8
30
  */
9
31
  import { z } from "zod";
10
32
  import { buildSfccSetupStatusHandler } from "./setup-status.js";
11
- import { withSfccGate } from "./tool-wrapper.js";
33
+ import { withSfccGate, withSfccTargetBoundary } from "./tool-wrapper.js";
12
34
  import { checkPermissionsTool } from "./permissions.js";
13
35
  import { registerSystemObjectReadTools } from "./reads-system-object.js";
14
36
  import { registerSfccCustomObjectDefReadTools } from "./reads-custom-object-def.js";
@@ -22,7 +44,9 @@ import { registerSfccLogQueryTool } from "./log-query.js";
22
44
  * Register all SFCC tools through the host's registerTool wrapper.
23
45
  *
24
46
  * Call this once in index.ts, immediately after `registerConductorTools(registerTool)`.
25
- * Sub-task tickets (2–4) add their read tools here; they never touch index.ts.
47
+ * Read and write surfaces are selected independently by `deps.includeReadTools`
48
+ * and `deps.includeWriteTools`; the two always-on diagnostics register
49
+ * regardless. Every path goes through `registerSfccTool`.
26
50
  */
27
51
  export function registerSfccTools(registerTool, deps) {
28
52
  const gateDeps = {
@@ -30,8 +54,17 @@ export function registerSfccTools(registerTool, deps) {
30
54
  getGetHeaders: deps.getGetHeaders,
31
55
  repoName: deps.repoName,
32
56
  };
57
+ /**
58
+ * The one and only bridge between the host registrar and the SFCC surface.
59
+ *
60
+ * Every SFCC tool is registered through this, so every SFCC tool crosses the
61
+ * resolved-hostname boundary before its own handler runs. `registerTool` (the
62
+ * raw host registrar) must not be referenced anywhere else in this module or
63
+ * passed to a child registrar — `registration-boundary.test.ts` pins that.
64
+ */
65
+ const registerSfccTool = (name, config, handler) => registerTool(name, config, withSfccTargetBoundary(handler));
33
66
  // sfcc_setup_status — aggregate prerequisite reporter.
34
- registerTool("sfcc_setup_status", {
67
+ registerSfccTool("sfcc_setup_status", {
35
68
  description: "Report on every SFCC prerequisite: Bridge API key, repo name, version config, " +
36
69
  "dw.json presence/uniqueness, and AM token acquisition. Always-registered; " +
37
70
  "returns status without requiring full SFCC configuration to be complete.",
@@ -40,53 +73,58 @@ export function registerSfccTools(registerTool, deps) {
40
73
  }, buildSfccSetupStatusHandler(deps.buildGetUrl, deps.getGetHeaders, deps.repoName, deps.getResolvedApiKey));
41
74
  // check_permissions — OCAPI access probe + settings JSON printer.
42
75
  const gatedCheckPermissions = withSfccGate(gateDeps, async (_args, credentials) => checkPermissionsTool(credentials));
43
- registerTool("check_permissions", {
76
+ registerSfccTool("check_permissions", {
44
77
  description: "Probe SFCC OCAPI access via GET /system_object_definitions. " +
45
78
  "On 200: reports OK and the detected OCAPI version. " +
46
79
  "On 401/403: prints the exact OCAPI Settings JSON to paste in Business Manager " +
47
- "(split read-only vs. write/import grants).",
80
+ "(read/search and mutation grants).",
48
81
  inputSchema: z.object({
49
82
  instance: z
50
83
  .string()
84
+ .min(1)
51
85
  .optional()
52
- .describe("Explicit sandbox hostname to use instead of dw.json auto-detection."),
86
+ .describe("Sandbox hostname to use instead of dw.json. Still sandbox-validated."),
53
87
  }),
54
88
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
55
89
  }, gatedCheckPermissions);
56
- // Heavy read tools are gated behind ACTIVE_GROUPS.has("sfcc") (BAPI-453).
57
- // sfcc_setup_status and check_permissions above remain always-on (D-4 decision).
90
+ // The read profile eight OCAPI reads plus sfcc_log_query — is gated behind
91
+ // ACTIVE_GROUPS.has("sfcc") (BAPI-453). sfcc_setup_status and
92
+ // check_permissions above remain always-on (D-4 decision).
58
93
  if (deps.includeReadTools) {
59
94
  // system_object_list, system_object_get, system_object_attribute_search (BAPI-401 T1–T3)
60
- registerSystemObjectReadTools(registerTool, {
95
+ registerSystemObjectReadTools(registerSfccTool, {
61
96
  gateDeps,
62
97
  getDocsDir: deps.getDocsDir,
63
98
  });
64
99
  // custom_object_definition_attributes_get, custom_object_definition_attribute_search (BAPI-402 T4–T5)
65
- registerSfccCustomObjectDefReadTools(registerTool, {
66
- gateDeps,
67
- getDocsDir: deps.getDocsDir,
68
- });
69
- // site_preference_get, site_preference_search (BAPI-403 T6 — optional)
70
- registerSitePreferenceTools(registerTool, {
100
+ registerSfccCustomObjectDefReadTools(registerSfccTool, {
71
101
  gateDeps,
72
102
  getDocsDir: deps.getDocsDir,
73
103
  });
74
- // SFCC write registration seam (BAPI-582). No write tools ship yet — future
75
- // write-surface tickets register here. Gated behind the same includeReadTools
76
- // branch so writes become default-on with reads whenever the sfcc profile
77
- // group is active.
78
- registerSfccWriteTools(registerTool, {
104
+ // site_preference_group_list, site_preference_get, site_preference_search (BAPI-403 T6)
105
+ registerSitePreferenceTools(registerSfccTool, {
79
106
  gateDeps,
80
107
  getDocsDir: deps.getDocsDir,
81
108
  });
82
109
  // sfcc_log_query (BAPI-556 T7) — gated, on-demand redacted/filtered log query.
83
- // Uses its OWN call-time gate (log capability = WebDAV Basic auth), NOT the
84
- // OCAPI version/dw.json/AM-token gate above.
85
- registerSfccLogQueryTool(registerTool, {
110
+ // Uses its OWN call-time capability gate (log capability = WebDAV Basic
111
+ // auth), NOT the OCAPI version/dw.json/AM-token gate above. It is read-only,
112
+ // so it belongs to the read profile.
113
+ registerSfccLogQueryTool(registerSfccTool, {
86
114
  buildGetUrl: deps.buildGetUrl,
87
115
  getGetHeaders: deps.getGetHeaders,
88
116
  getPostHeaders: deps.getPostHeaders,
89
117
  repoName: deps.repoName,
90
118
  });
91
119
  }
120
+ // The nine destructive write tools are gated INDEPENDENTLY, behind
121
+ // ACTIVE_GROUPS.has("sfcc-write") (BAPI-817). This branch is deliberately not
122
+ // nested inside the read branch: `sfcc-write` alone is a valid selection, and
123
+ // `sfcc` alone must never register a tool carrying WRITE_ANNOTATIONS.
124
+ if (deps.includeWriteTools) {
125
+ registerSfccWriteTools(registerSfccTool, {
126
+ gateDeps,
127
+ getDocsDir: deps.getDocsDir,
128
+ });
129
+ }
92
130
  }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Canonical inventory of every SFCC MCP tool name, by registration mode
3
+ * (BAPI-816).
4
+ *
5
+ * `register.ts` (`registerSfccTools`) decides WHICH tools register; this
6
+ * module is the single readable description of what that decision produces.
7
+ * Registrar tests compare captured registrations against these composed
8
+ * arrays instead of independently maintained numeric pins, which is exactly
9
+ * what let `register-site-preference.integration.test.ts` pin a stale `10`
10
+ * while the real registrar had already grown to 20 — a full write surface's
11
+ * worth of drift that no test caught (BAPI-816).
12
+ *
13
+ * `includeReadTools` is the legacy `SfccHostDeps` flag name (`register.ts`).
14
+ * Since BAPI-817 it gates the READ profile only — the eight OCAPI reads plus
15
+ * the log-query tool — while `includeWriteTools` independently gates the nine
16
+ * writes. The name predates both surfaces and was kept for call-site stability.
17
+ */
18
+ import { SFCC_WRITE_TOOL_NAMES } from "./writes.js";
19
+ /**
20
+ * Registered unconditionally, regardless of `includeReadTools` (D-4 decision
21
+ * in register.ts): the aggregate prerequisite reporter and the OCAPI access
22
+ * probe.
23
+ */
24
+ export const SFCC_ALWAYS_REGISTERED_TOOL_NAMES = [
25
+ "sfcc_setup_status",
26
+ "check_permissions",
27
+ ];
28
+ /**
29
+ * The eight read tools registered by the three read-module registrars
30
+ * (`registerSystemObjectReadTools`, `registerSfccCustomObjectDefReadTools`,
31
+ * `registerSitePreferenceTools`) when `includeReadTools` is true.
32
+ */
33
+ export const SFCC_READ_TOOL_NAMES = [
34
+ "system_object_list",
35
+ "system_object_get",
36
+ "system_object_attribute_search",
37
+ "custom_object_definition_attributes_get",
38
+ "custom_object_definition_attribute_search",
39
+ "site_preference_group_list",
40
+ "site_preference_get",
41
+ "site_preference_search",
42
+ ];
43
+ /** The on-demand redacted/filtered log-query tool (BAPI-556 T7). */
44
+ export const SFCC_LOG_QUERY_TOOL_NAME = "sfcc_log_query";
45
+ /**
46
+ * The `sfcc` READ profile: the eight OCAPI reads plus the log-query tool
47
+ * (BAPI-817).
48
+ *
49
+ * This is the exact set `includeReadTools` registers. Every member is read-only
50
+ * and carries read-only annotations; by construction no tool annotated
51
+ * `WRITE_ANNOTATIONS` can appear here, which is what makes "enabling `sfcc`
52
+ * cannot enable a mutation" checkable rather than merely intended.
53
+ */
54
+ export const SFCC_READ_PROFILE_TOOL_NAMES = [
55
+ ...SFCC_READ_TOOL_NAMES,
56
+ SFCC_LOG_QUERY_TOOL_NAME,
57
+ ];
58
+ /**
59
+ * The `sfcc-write` WRITE profile: the nine destructive tools (BAPI-817).
60
+ *
61
+ * Reused from `writes.ts#SFCC_WRITE_TOOL_NAMES` rather than duplicated, so the
62
+ * profile inventory and the registrar's own list cannot drift apart.
63
+ */
64
+ export const SFCC_WRITE_PROFILE_TOOL_NAMES = [
65
+ ...SFCC_WRITE_TOOL_NAMES,
66
+ ];
67
+ /**
68
+ * Every profile-gated tool, across both groups.
69
+ *
70
+ * Retained as the aggregate for consumers that care about "gated vs always-on"
71
+ * rather than which group gates it — but composed from the two profile
72
+ * inventories above, never hand-maintained. Order matches registration order in
73
+ * `register.ts`: the read branch runs first, then the write branch.
74
+ */
75
+ export const SFCC_PROFILE_GATED_TOOL_NAMES = [
76
+ ...SFCC_READ_PROFILE_TOOL_NAMES,
77
+ ...SFCC_WRITE_PROFILE_TOOL_NAMES,
78
+ ];
79
+ /**
80
+ * The complete SFCC registration surface: always-on plus both gated profiles.
81
+ *
82
+ * This is the twenty-tool surface a `BRIDGE_MCP_PROFILE=sfcc,sfcc-write` (or
83
+ * `full`) session exposes, and the iteration source for the boundary-coverage
84
+ * test — so a newly added tool cannot silently evade that coverage.
85
+ */
86
+ export const SFCC_ALL_REGISTERED_TOOL_NAMES = [
87
+ ...SFCC_ALWAYS_REGISTERED_TOOL_NAMES,
88
+ ...SFCC_PROFILE_GATED_TOOL_NAMES,
89
+ ];
@@ -6,7 +6,6 @@
6
6
  * prevent the others from running.
7
7
  */
8
8
  import { SFCC_VERSIONS, getSfccVersionConfig } from "./config.js";
9
- import { resolveSfccCredentials } from "./credentials.js";
10
9
  import { getAmToken } from "./client.js";
11
10
  // ---------------------------------------------------------------------------
12
11
  // Implementation
@@ -57,39 +56,20 @@ export async function sfccSetupStatusTool(deps) {
57
56
  versionStatus = "— Skipped (Bridge API not configured)";
58
57
  }
59
58
  lines.push(`3. SFCC Version: ${versionStatus}`);
60
- // 4. dw.json / credential resolution
61
- let credStatus = "✗ Missing";
62
- let resolvedCredentials = null;
63
- try {
64
- const result = await resolveSfccCredentials();
65
- if (result.ok) {
66
- // Report source and instance ID only — never the secret values.
67
- resolvedCredentials = {
68
- hostname: result.credentials.hostname.split(".")[0] ?? result.credentials.hostname,
69
- source: result.credentials.source,
70
- };
71
- credStatus = `✓ Found (${resolvedCredentials.source})`;
72
- }
73
- else {
74
- credStatus = `✗ ${result.error}`;
75
- }
76
- }
77
- catch (err) {
78
- credStatus = `✗ Resolution error: ${err instanceof Error ? err.message : String(err)}`;
79
- }
80
- lines.push(`4. dw.json / Credentials: ${credStatus}`);
81
- // 5. AM token acquisition (only if credentials were resolved)
59
+ // 4. dw.json / credential resolution — read from the boundary-approved target
60
+ // rather than resolved again here (BAPI-817).
61
+ const target = deps.target;
62
+ // Report source and instance ID only — never the secret values.
63
+ const instanceLabel = target.approved
64
+ ? target.hostname.split(".")[0] ?? target.hostname
65
+ : null;
66
+ lines.push(`4. dw.json / Credentials: ${target.approved ? `✓ Found (${target.credentials.source})` : `✗ ${target.error}`}`);
67
+ // 5. AM token acquisition (only when the boundary approved a target).
82
68
  let tokenStatus = "— Skipped (credentials not available)";
83
- if (resolvedCredentials) {
69
+ if (target.approved) {
84
70
  try {
85
- const credResult = await resolveSfccCredentials();
86
- if (credResult.ok) {
87
- await getAmToken(credResult.credentials);
88
- tokenStatus = `✓ Token acquired for instance ${resolvedCredentials.hostname}`;
89
- }
90
- else {
91
- tokenStatus = "✗ Credentials not resolved";
92
- }
71
+ await getAmToken(target.credentials);
72
+ tokenStatus = `✓ Token acquired for instance ${instanceLabel}`;
93
73
  }
94
74
  catch (err) {
95
75
  // Sanitize: strip any token/secret-looking content from error message.
@@ -135,10 +115,14 @@ export async function sfccSetupStatusTool(deps) {
135
115
  }
136
116
  /**
137
117
  * Build the sfcc_setup_status handler bound to the host's runtime deps.
118
+ *
119
+ * The returned handler takes the `SfccTargetContext` the registration-level
120
+ * sandbox boundary supplies (BAPI-817) and forwards it, so this tool reports on
121
+ * the same target the boundary approved and never resolves one of its own.
138
122
  */
139
123
  export function buildSfccSetupStatusHandler(buildGetUrl, getGetHeaders, repoName, getApiKey) {
140
- return async (_args) => {
124
+ return async (_args, target) => {
141
125
  const apiKey = await getApiKey();
142
- return sfccSetupStatusTool({ buildGetUrl, getGetHeaders, repoName, apiKey });
126
+ return sfccSetupStatusTool({ buildGetUrl, getGetHeaders, repoName, apiKey, target });
143
127
  };
144
128
  }