@bridge_gpt/mcp-server 0.2.37 → 0.2.39

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 (91) hide show
  1. package/README.md +193 -16
  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 +423 -0
  14. package/build/conduct-epic/cli.js +1732 -0
  15. package/build/conduct-epic/lock.js +302 -0
  16. package/build/conduct-epic/pr-state.js +197 -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/ocapi-shape.js +23 -4
  67. package/build/sfcc/permissions.js +25 -6
  68. package/build/sfcc/read-body.js +92 -0
  69. package/build/sfcc/read-projection.js +6 -2
  70. package/build/sfcc/reads-custom-object-def.js +33 -21
  71. package/build/sfcc/reads-site-preference.js +20 -7
  72. package/build/sfcc/reads-system-object.js +11 -5
  73. package/build/sfcc/register.js +61 -23
  74. package/build/sfcc/registration-inventory.js +89 -0
  75. package/build/sfcc/setup-status.js +18 -34
  76. package/build/sfcc/tool-wrapper.js +294 -17
  77. package/build/sfcc/write-grants.js +33 -1
  78. package/build/sfcc/write-guard.js +41 -12
  79. package/build/sfcc/write-result.js +16 -7
  80. package/build/sfcc/writes-custom-object-def.js +12 -4
  81. package/build/sfcc/writes-site-preference.js +6 -1
  82. package/build/sfcc/writes-system-object.js +11 -2
  83. package/build/sfcc/writes.js +13 -8
  84. package/build/start-tickets-prereqs.js +25 -15
  85. package/build/start-tickets.js +123 -21
  86. package/build/version.generated.js +1 -1
  87. package/build/worktree-core.js +9 -3
  88. package/docs/install/mcp-tool-integrations.md +54 -9
  89. package/docs/install/sfcc-integration.md +71 -24
  90. package/package.json +3 -3
  91. 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})`);
@@ -22,6 +22,13 @@
22
22
  * that legitimately live inside the returned resources: an OCAPI attribute
23
23
  * search document carries its own `count` and `hits`, and those stay untouched
24
24
  * inside `items`.
25
+ *
26
+ * `page` is built strictly from properties of the collection envelope itself,
27
+ * never from a field promoted out of an individual item (BAPI-769). A bare
28
+ * object response is the one shape where this matters concretely: `items` is
29
+ * always that single object, so its own internal `total`/`count` describes hits
30
+ * *inside* the document, not a count of `items`, and is never attached to
31
+ * `page.total` — see the bare-object branch of `normalizeOcapiBody` below.
25
32
  */
26
33
  /**
27
34
  * Single constructor for page metadata, shared by both normalizers.
@@ -79,6 +86,15 @@ export function normalizeOcapiPage(body) {
79
86
  * an attribute-search document with its own `count` and `hits` — becomes the
80
87
  * single item unchanged; its `count` is never promoted to collection metadata,
81
88
  * and a missing `hits` is never synthesized.
89
+ *
90
+ * `page` describes only the normalized `items` array; it is never built from a
91
+ * field promoted out of an item's own body (BAPI-769). The bare-object branch
92
+ * always produces exactly one item — the whole document — so a `total` count
93
+ * OCAPI reported about the document's own internal contents (e.g. a search
94
+ * document's zero-hit `total: 0`) is never attached to `page.total`, where it
95
+ * would misread as "0 of 1 returned item" for a wrapper whose `returned` is
96
+ * always 1. `page.total` is therefore absent on every bare-object response;
97
+ * `has_more` is unaffected — it still reflects the bare object's own `next`.
82
98
  */
83
99
  export function normalizeOcapiBody(body) {
84
100
  // Bare array
@@ -93,11 +109,14 @@ export function normalizeOcapiBody(body) {
93
109
  const items = obj.data;
94
110
  return { items, page: buildPageMeta(items, obj.total, obj.next) };
95
111
  }
96
- // Bare object (single resource, no wrapping envelope). A directly supplied
97
- // numeric `total` and paging signal are honored; the object's own `count`
98
- // is left inside the item and never reinterpreted as a total.
112
+ // Bare object (single resource, no wrapping envelope). `items` is always
113
+ // the single wrapper document, so `returned` is always 1 a `total` (and,
114
+ // for a search document, `count`/`hits`) reported INSIDE that document is
115
+ // never promoted to `page.total`: it describes hits within the document,
116
+ // not a count of `items`. `page.total` is therefore always absent here
117
+ // (BAPI-769); `next` is still honored for `has_more`, which is unaffected.
99
118
  const items = [body];
100
- return { items, page: buildPageMeta(items, obj.total, obj.next) };
119
+ return { items, page: buildPageMeta(items, undefined, obj.next) };
101
120
  }
102
121
  // Non-object (null, string, number, etc.)
103
122
  return { items: [], page: buildPageMeta([], undefined, null) };
@@ -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
  }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * SFCC read-body redaction seam (BAPI-769).
3
+ *
4
+ * `projection: "full"` (BAPI-767) makes several OCAPI Data API endpoints return
5
+ * complete attribute-definition documents — `system_object_attribute_search`,
6
+ * `custom_object_definition_attribute_*`, and `system_object_definitions/{type}/
7
+ * attribute_definitions/{id}`. Live sandbox verification (OCAPI Data API v25_6)
8
+ * showed one of those 27 keys is not schema metadata about the attribute: it is
9
+ * the attribute's own runtime VALUE. `default_value` can carry a real
10
+ * configuration value — an attribute named `llmAvailableModelsJson` can carry
11
+ * actual model-routing config as its default — and that value must never reach
12
+ * an MCP caller, in the inline response, the `docs/tmp/sfcc/` save, or a
13
+ * successful attribute-definition write echo.
14
+ *
15
+ * This module owns exactly that: withholding one named structured field. It
16
+ * deliberately does NOT port `src/python/sfcc_monitor/adapters/redaction.py`'s
17
+ * regex/entropy engine — there is nothing to detect heuristically, only one key
18
+ * to withhold — and it deliberately does NOT redact inside `ocapi-shape.ts`.
19
+ * `normalizeOcapiBody`'s documented invariant is verbatim passthrough; folding
20
+ * redaction into it would make that no longer true. `readSfccBody` composes the
21
+ * two steps explicitly instead: redact first, normalize second.
22
+ *
23
+ * Attribute default values are intentionally unavailable through this MCP
24
+ * surface. Business Manager (Administration → Site Development → an object
25
+ * type's attribute definition) is the supported path to read one.
26
+ */
27
+ import { normalizeOcapiBody } from "./ocapi-shape.js";
28
+ /**
29
+ * Marker substituted for a withheld `default_value` node.
30
+ *
31
+ * Deliberately distinct from the log-query system's own `[REDACTED]` marker
32
+ * (`sfcc/log-query.ts` and friends) — the two redaction seams are unrelated:
33
+ * this one withholds one named structured field before normalization, the log
34
+ * seam performs regex/entropy scrubbing of log text. Conflating the markers
35
+ * would make a caller unable to tell which seam produced a given redaction.
36
+ */
37
+ export const BRIDGE_REDACTED_MARKER = "[REDACTED_BY_BRIDGE]";
38
+ /** True for a plain (non-array, non-null) object. */
39
+ function isPlainObject(value) {
40
+ return typeof value === "object" && value !== null && !Array.isArray(value);
41
+ }
42
+ /**
43
+ * Recursively withhold every `default_value` key found anywhere in `body`.
44
+ *
45
+ * Whenever an object carries an own `default_value` key, that key's ENTIRE
46
+ * value — including the `object_attribute_value_definition` wrapper OCAPI
47
+ * shapes it as (`{ _type, id, position, value }`) — is replaced with
48
+ * `BRIDGE_REDACTED_MARKER`. The key itself is always kept; only its value
49
+ * changes. Recursion continues into every sibling and into the rest of the same
50
+ * object/array, so a document that embeds another attribute-definition-shaped
51
+ * object deeper in its tree is still covered — the live key survey found only
52
+ * one `default_value` per document, but the walker stays general rather than
53
+ * hard-coding that shape.
54
+ *
55
+ * Side-effect-free: `body` is never mutated. A fresh tree is built and
56
+ * returned, so the caller's original object is unchanged after the call. A
57
+ * `default_value` key that was never present on the source is never
58
+ * synthesized — absence stays absence.
59
+ */
60
+ export function redactSfccBody(body) {
61
+ if (Array.isArray(body)) {
62
+ return body.map((item) => redactSfccBody(item));
63
+ }
64
+ if (isPlainObject(body)) {
65
+ const result = {};
66
+ for (const [key, value] of Object.entries(body)) {
67
+ // Object.entries only yields own enumerable properties, so reaching this
68
+ // branch already means `body` owns `default_value` — no key is ever added
69
+ // where the source never had one.
70
+ if (key === "default_value") {
71
+ result[key] = BRIDGE_REDACTED_MARKER;
72
+ continue;
73
+ }
74
+ result[key] = redactSfccBody(value);
75
+ }
76
+ return result;
77
+ }
78
+ return body;
79
+ }
80
+ /**
81
+ * Redact, then normalize, a raw OCAPI response body.
82
+ *
83
+ * This is the read-path replacement for calling `normalizeOcapiBody` directly.
84
+ * Redaction happens BEFORE normalization, serialization, truncation, or save —
85
+ * one placement covers the inline MCP response and the `docs/tmp/sfcc/` save
86
+ * path identically, because both are derived from this same normalized result.
87
+ * `normalizeOcapiBody` itself remains a non-redacting verbatim shaper; it is
88
+ * `readSfccBody` that owns the redaction decision.
89
+ */
90
+ export function readSfccBody(body) {
91
+ return normalizeOcapiBody(redactSfccBody(body));
92
+ }
@@ -81,12 +81,16 @@ const PROJECTION_POLICY = {
81
81
  attribute_search: {
82
82
  defaultProjection: "full",
83
83
  placement: "body",
84
- inputDescription: "Detail level. Default: full (value_type, mandatory, localizable, searchable, site_specific); lean returns ids only.",
84
+ inputDescription: "Detail level. Default: full OCAPI's complete metadata projection " +
85
+ "(value_type, mandatory, localizable, searchable, site_specific); " +
86
+ "default_value is withheld. lean returns ids only.",
85
87
  },
86
88
  attribute_collection: {
87
89
  defaultProjection: "full",
88
90
  placement: "query",
89
- inputDescription: "Detail level. Default: full (value_type, mandatory, localizable, searchable, site_specific); lean returns ids only.",
91
+ inputDescription: "Detail level. Default: full OCAPI's complete metadata projection " +
92
+ "(value_type, mandatory, localizable, searchable, site_specific); " +
93
+ "default_value is withheld. lean returns ids only.",
90
94
  },
91
95
  broad_list: {
92
96
  defaultProjection: "lean",
@@ -11,17 +11,31 @@
11
11
  * OCAPI's CustomObjectDefinitions resource only exposes operations scoped to
12
12
  * a known object_type — there is no top-level list and no bare get-by-type
13
13
  * (unlike SystemObjectDefinitions). A caller-supplied object_type is always
14
- * required; OCAPI cannot enumerate custom object types.
14
+ * required; OCAPI cannot enumerate custom object type IDs directly.
15
+ *
16
+ * That is narrower than "wholly undiscoverable," and the tools below say so:
17
+ * `system_object_list` at `projection: "full"` returns a distinct
18
+ * `display_name` and `attribute_definition_count` per custom type, from which a
19
+ * candidate ID can be derived (e.g. strip spaces from "Product Quality Result"
20
+ * → "ProductQualityResult") and confirmed by checking that this module's own
21
+ * `custom_object_definition_attributes_get` returns an attribute count matching
22
+ * that row's `attribute_definition_count`. This adds no new tool and no new
23
+ * projection value — it is existing capability, stated accurately.
15
24
  *
16
25
  * Note: custom object TYPE creation/update is NOT available via OCAPI REST.
17
26
  * It is a v2 metadata-import capability (metadata XML → WebDAV → import job).
27
+ *
28
+ * Both tools can return complete attribute-definition documents at
29
+ * `projection: "full"` (BAPI-767). Bridge withholds each attribute's
30
+ * `default_value` from that response (BAPI-769, `read-body.ts`) — every other
31
+ * schema field (type, flags, labels) is unaffected.
18
32
  */
19
33
  import path from "path";
20
34
  import { z } from "zod";
21
35
  import { ocapiGet, ocapiPost } from "./client.js";
22
36
  import { withSfccGate } from "./tool-wrapper.js";
23
37
  import { formatOcapiReadFailure, withSfccReadErrorBoundary } from "./read-result.js";
24
- import { normalizeOcapiBody } from "./ocapi-shape.js";
38
+ import { readSfccBody } from "./read-body.js";
25
39
  import { applyProjectionToBody, projectionInputFor, projectionQueryEntries, } from "./read-projection.js";
26
40
  import { truncateAndSaveIfNeeded } from "./output.js";
27
41
  // ---------------------------------------------------------------------------
@@ -36,18 +50,17 @@ const READ_ANNOTATIONS = {
36
50
  // ---------------------------------------------------------------------------
37
51
  // Input schemas
38
52
  // ---------------------------------------------------------------------------
53
+ const OBJECT_TYPE_DISCOVERY_HINT = 'Known custom object type id (e.g. starting with "c_"). OCAPI cannot enumerate ' +
54
+ "type IDs directly, but you can discover a candidate via system_object_list at " +
55
+ "projection=\"full\" (distinct display_name + attribute_definition_count per " +
56
+ "custom type) and confirm it by checking that this tool's returned attribute " +
57
+ "count matches that row's attribute_definition_count.";
39
58
  const customObjectAttributesGetInput = z.object({
40
- object_type: z
41
- .string()
42
- .describe('Known custom object type identifier, e.g. a custom type id starting with "c_". ' +
43
- "OCAPI cannot enumerate custom object types — the type must already be known."),
59
+ object_type: z.string().describe(OBJECT_TYPE_DISCOVERY_HINT),
44
60
  projection: projectionInputFor("attribute_collection"),
45
61
  });
46
62
  const customObjectAttributeSearchInput = z.object({
47
- object_type: z
48
- .string()
49
- .describe('Known custom object type to search within, e.g. a custom type id starting with "c_". ' +
50
- "OCAPI cannot enumerate custom object types — the type must already be known."),
63
+ object_type: z.string().describe(OBJECT_TYPE_DISCOVERY_HINT),
51
64
  query: z.union([z.string(), z.record(z.string(), z.any())]).describe("Search query. A plain string is a case-insensitive substring match across " +
52
65
  "id and display_name; a structured OCAPI query object passes through unchanged."),
53
66
  start: z.number().optional().describe("Zero-based offset for paging."),
@@ -86,7 +99,7 @@ function buildCustomObjectAttributesGetHandler(gateDeps, getDocsDir) {
86
99
  if (!result.ok) {
87
100
  return formatOcapiReadFailure(result);
88
101
  }
89
- const normalized = normalizeOcapiBody(result.body);
102
+ const normalized = readSfccBody(result.body);
90
103
  const text = JSON.stringify(normalized, null, 2);
91
104
  const dir = path.join(await getDocsDir(), "sfcc");
92
105
  return saveAndReturn(text, dir, `custom-object-def-attributes-${safeType(object_type)}-${safeTimestamp()}.json`, normalized.page);
@@ -112,7 +125,7 @@ function buildCustomObjectAttributeSearchHandler(gateDeps, getDocsDir) {
112
125
  if (!result.ok) {
113
126
  return formatOcapiReadFailure(result);
114
127
  }
115
- const normalized = normalizeOcapiBody(result.body);
128
+ const normalized = readSfccBody(result.body);
116
129
  const text = JSON.stringify(normalized, null, 2);
117
130
  const dir = path.join(await getDocsDir(), "sfcc");
118
131
  return saveAndReturn(text, dir, `custom-object-def-search-${safeType(object_type)}-${safeTimestamp()}.json`, normalized.page);
@@ -127,20 +140,19 @@ export function registerSfccCustomObjectDefReadTools(registerTool, deps) {
127
140
  const { gateDeps, getDocsDir } = deps;
128
141
  registerTool("custom_object_definition_attributes_get", {
129
142
  description: "Retrieve attribute definitions for a KNOWN custom object type via " +
130
- "GET /custom_object_definitions/{type}/attribute_definitions. " +
131
- "Returns each attribute's full schema by default. " +
132
- "OCAPI cannot enumerate types object_type is required. " +
133
- "Type creation is v2 metadata-import only. Oversized payloads auto-saved locally.",
143
+ "GET /custom_object_definitions/{type}/attribute_definitions " +
144
+ "(default_value withheld). OCAPI cannot enumerate type IDs directly; " +
145
+ "derive one via system_object_list (projection=full, attribute_definition_count), " +
146
+ "confirm here. Type creation is v2 metadata-import only.",
134
147
  inputSchema: customObjectAttributesGetInput,
135
148
  annotations: READ_ANNOTATIONS,
136
149
  }, buildCustomObjectAttributesGetHandler(gateDeps, getDocsDir));
137
150
  registerTool("custom_object_definition_attribute_search", {
138
151
  description: "Search attribute definitions within a KNOWN custom object type via " +
139
- "POST /custom_object_definitions/{type}/attribute_definition_search. " +
140
- "Returns each attribute's full schema by default. " +
141
- "OCAPI cannot enumerate types; object_type is required. " +
142
- "Pass a string or OCAPI query. Type creation is v2 metadata-import only. " +
143
- "Oversized results auto-saved locally.",
152
+ "POST /custom_object_definitions/{type}/attribute_definition_search " +
153
+ "(default_value withheld). OCAPI cannot enumerate type IDs directly; " +
154
+ "derive one via system_object_list (projection=full, attribute_definition_count), " +
155
+ "confirm here. Type creation is v2 metadata-import only.",
144
156
  inputSchema: customObjectAttributeSearchInput,
145
157
  annotations: READ_ANNOTATIONS,
146
158
  }, buildCustomObjectAttributeSearchHandler(gateDeps, getDocsDir));
@@ -24,11 +24,18 @@
24
24
  * It is also, for now, accidentally protective. A name-heuristic scan of all 19
25
25
  * preference groups on the tested sandbox found secret-named preferences in six
26
26
  * of them — including one literally named `client_id` — and none of them expose
27
- * a value through this endpoint. No redaction seam is built here because there
28
- * is no value-bearing path to redact. If OCAPI ever starts returning values,
27
+ * a value through this endpoint. If OCAPI ever starts returning values here,
29
28
  * redaction must be added on the raw body BEFORE normalization, serialization,
30
29
  * truncation, or save, so one placement covers both the inline response and the
31
- * docs/tmp/sfcc/ save path.
30
+ * docs/tmp/sfcc/ save path — exactly the `readSfccBody` seam these three
31
+ * handlers already route through (BAPI-769).
32
+ *
33
+ * That value-bearing route already exists elsewhere: `SitePreferences`
34
+ * attribute-definition reads via `system_object_attribute_search` (with
35
+ * `object_type: "SitePreferences"`) can return each preference's
36
+ * `default_value`, and `readSfccBody` withholds it there today. This module's
37
+ * three preference-value tools remain ids-only for the reason stated above —
38
+ * OCAPI's `preference_search` itself never returns a value, `select` or not.
32
39
  *
33
40
  * NOTE: SCAPI Preferences API (sfcc.preferences) is intentionally NOT used.
34
41
  * This keeps the tool context on the OCAPI Data API, preserving the single
@@ -40,7 +47,7 @@ import { z } from "zod";
40
47
  import { ocapiGet, ocapiPost } from "./client.js";
41
48
  import { withSfccGate } from "./tool-wrapper.js";
42
49
  import { formatOcapiReadFailure, formatSfccReadFailure, withSfccReadErrorBoundary, } from "./read-result.js";
43
- import { normalizeOcapiBody } from "./ocapi-shape.js";
50
+ import { readSfccBody } from "./read-body.js";
44
51
  import { projectionInputFor, projectionQueryEntries } from "./read-projection.js";
45
52
  import { truncateAndSaveIfNeeded } from "./output.js";
46
53
  // ---------------------------------------------------------------------------
@@ -106,6 +113,12 @@ async function saveAndReturn(text, dir, filename, page) {
106
113
  * an API version while OCAPI simultaneously reports `_v: "25.6"` in the same
107
114
  * session; the constraint belongs to this Bridge tool, not to a version. The
108
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`.
109
122
  */
110
123
  function rejectIfNotSandbox(instance) {
111
124
  if (instance !== "sandbox") {
@@ -145,7 +158,7 @@ export function buildSitePreferenceGetHandler(gateDeps, getDocsDir) {
145
158
  if (!result.ok) {
146
159
  return formatOcapiReadFailure(result);
147
160
  }
148
- const normalized = normalizeOcapiBody(result.body);
161
+ const normalized = readSfccBody(result.body);
149
162
  const text = JSON.stringify(normalized, null, 2);
150
163
  const dir = path.join(await getDocsDir(), "sfcc");
151
164
  return saveAndReturn(text, dir, `site-preference-get-${safeGroup(group)}-${safeTimestamp()}.json`, normalized.page);
@@ -173,7 +186,7 @@ export function buildSitePreferenceSearchHandler(gateDeps, getDocsDir) {
173
186
  if (!result.ok) {
174
187
  return formatOcapiReadFailure(result);
175
188
  }
176
- const normalized = normalizeOcapiBody(result.body);
189
+ const normalized = readSfccBody(result.body);
177
190
  const text = JSON.stringify(normalized, null, 2);
178
191
  const dir = path.join(await getDocsDir(), "sfcc");
179
192
  return saveAndReturn(text, dir, `site-preference-search-${safeGroup(group)}-${safeTimestamp()}.json`, normalized.page);
@@ -196,7 +209,7 @@ export function buildSitePreferenceGroupListHandler(gateDeps, getDocsDir) {
196
209
  if (!result.ok) {
197
210
  return formatOcapiReadFailure(result);
198
211
  }
199
- const normalized = normalizeOcapiBody(result.body);
212
+ const normalized = readSfccBody(result.body);
200
213
  const text = JSON.stringify(normalized, null, 2);
201
214
  const dir = path.join(await getDocsDir(), "sfcc");
202
215
  return saveAndReturn(text, dir, `site-preference-group-list-${safeTimestamp()}.json`, normalized.page);
@@ -8,13 +8,18 @@
8
8
  *
9
9
  * All tools are read-only, run behind the T1 call-time gate, and route
10
10
  * oversized payloads through the sfcc/output.ts truncate-and-save seam.
11
+ *
12
+ * `system_object_attribute_search` can return complete attribute-definition
13
+ * documents at `projection: "full"` (BAPI-767). Bridge withholds each
14
+ * attribute's `default_value` from that response (BAPI-769, `read-body.ts`) —
15
+ * every other schema field (type, flags, labels) is unaffected.
11
16
  */
12
17
  import path from "path";
13
18
  import { z } from "zod";
14
19
  import { ocapiGet, ocapiPost } from "./client.js";
15
20
  import { withSfccGate } from "./tool-wrapper.js";
16
21
  import { formatOcapiReadFailure, withSfccReadErrorBoundary } from "./read-result.js";
17
- import { normalizeOcapiBody } from "./ocapi-shape.js";
22
+ import { readSfccBody } from "./read-body.js";
18
23
  import { applyProjectionToBody, projectionInputFor, projectionQueryEntries, } from "./read-projection.js";
19
24
  import { truncateAndSaveIfNeeded } from "./output.js";
20
25
  // ---------------------------------------------------------------------------
@@ -86,7 +91,7 @@ function buildSystemObjectListHandler(gateDeps, getDocsDir) {
86
91
  if (!result.ok) {
87
92
  return formatOcapiReadFailure(result);
88
93
  }
89
- const normalized = normalizeOcapiBody(result.body);
94
+ const normalized = readSfccBody(result.body);
90
95
  const text = JSON.stringify(normalized, null, 2);
91
96
  const dir = path.join(await getDocsDir(), "sfcc");
92
97
  return saveAndReturn(text, dir, `system-object-list-${safeTimestamp()}.json`, normalized.page);
@@ -100,7 +105,7 @@ function buildSystemObjectGetHandler(gateDeps, getDocsDir) {
100
105
  if (!result.ok) {
101
106
  return formatOcapiReadFailure(result);
102
107
  }
103
- const normalized = normalizeOcapiBody(result.body);
108
+ const normalized = readSfccBody(result.body);
104
109
  const text = JSON.stringify(normalized, null, 2);
105
110
  const dir = path.join(await getDocsDir(), "sfcc");
106
111
  return saveAndReturn(text, dir, `system-object-get-${safeType(object_type)}-${safeTimestamp()}.json`, normalized.page);
@@ -126,7 +131,7 @@ function buildSystemObjectAttributeSearchHandler(gateDeps, getDocsDir) {
126
131
  if (!result.ok) {
127
132
  return formatOcapiReadFailure(result);
128
133
  }
129
- const normalized = normalizeOcapiBody(result.body);
134
+ const normalized = readSfccBody(result.body);
130
135
  const text = JSON.stringify(normalized, null, 2);
131
136
  const dir = path.join(await getDocsDir(), "sfcc");
132
137
  return saveAndReturn(text, dir, `system-object-search-${safeType(object_type)}-${safeTimestamp()}.json`, normalized.page);
@@ -159,7 +164,8 @@ export function registerSystemObjectReadTools(registerTool, deps) {
159
164
  registerTool("system_object_attribute_search", {
160
165
  description: "Search attribute definitions for a system object type. Read-only. " +
161
166
  "POST /system_object_definitions/{type}/attribute_definition_search. " +
162
- "Use for targeted c_ attribute lookups; returns each attribute's full schema by default. " +
167
+ "Use for targeted c_ attribute lookups; returns each attribute's schema " +
168
+ "by default (default_value withheld). " +
163
169
  "Pass a plain string for text search or a structured OCAPI query. " +
164
170
  "Oversized results auto-saved locally.",
165
171
  inputSchema: systemObjectAttributeSearchInput,