@bridge_gpt/mcp-server 0.2.36 → 0.2.38

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 (92) hide show
  1. package/README.md +48 -8
  2. package/build/base-url.js +79 -0
  3. package/build/bridge-api-urls.js +9 -0
  4. package/build/chain-orchestrator.js +93 -15
  5. package/build/claude-user-config-doctor.js +317 -0
  6. package/build/commands.generated.js +2 -1
  7. package/build/conductor/bridge-api-client.js +178 -4
  8. package/build/conductor-bin.js +1 -1
  9. package/build/conductor-bundle-artifacts.js +7 -6
  10. package/build/credential-store.js +205 -4
  11. package/build/direct-ticket-tools.js +70 -0
  12. package/build/doctor.js +239 -80
  13. package/build/executor/cli.js +51 -1
  14. package/build/executor/credentials.js +1 -7
  15. package/build/executor/deps.js +18 -1
  16. package/build/executor/env.js +51 -25
  17. package/build/executor/heartbeat.js +138 -17
  18. package/build/executor/http-client.js +49 -8
  19. package/build/executor/job-errors.js +4 -0
  20. package/build/executor/job-runner.js +422 -22
  21. package/build/executor/observation.js +130 -0
  22. package/build/executor/permissions.js +104 -8
  23. package/build/executor/preflight.js +32 -0
  24. package/build/executor/runner.js +8 -0
  25. package/build/executor/test-clock.js +67 -3
  26. package/build/executor/types.js +4 -1
  27. package/build/executor/worker-command.js +11 -3
  28. package/build/executor/worker-config-isolation.js +287 -0
  29. package/build/executor/worker-finalization.js +68 -14
  30. package/build/executor/worktree.js +46 -4
  31. package/build/index.js +614 -244
  32. package/build/init.js +363 -73
  33. package/build/install-bridge.js +568 -80
  34. package/build/launcher-config-inspection.js +351 -0
  35. package/build/mcp-invoke.js +49 -6
  36. package/build/mcp-provisioning.js +30 -7
  37. package/build/mcp-registration-doctor.js +14 -5
  38. package/build/notifications.js +553 -0
  39. package/build/pipeline-orchestrator.js +146 -4
  40. package/build/pipeline-utils.js +3 -0
  41. package/build/pipelines.generated.js +22 -9
  42. package/build/plan-execution-ledger.js +550 -0
  43. package/build/plan-phase-routing.js +272 -0
  44. package/build/plane/alembic-head.js +110 -0
  45. package/build/plane/build-freshness.js +167 -0
  46. package/build/plane/cli.js +480 -0
  47. package/build/plane/defaults.js +266 -0
  48. package/build/plane/manifest.js +377 -0
  49. package/build/plane/member-logs.js +147 -0
  50. package/build/plane/member-roster.js +147 -0
  51. package/build/plane/preflight.js +289 -0
  52. package/build/plane/shutdown.js +195 -0
  53. package/build/plane/status.js +125 -0
  54. package/build/plane/supervisor.js +569 -0
  55. package/build/plane/test-fakes.js +156 -0
  56. package/build/plane/types.js +75 -0
  57. package/build/readme.generated.js +1 -1
  58. package/build/run-unit-tests-launcher.js +2 -0
  59. package/build/setup-epic.js +662 -27
  60. package/build/sfcc/log-gate.js +38 -11
  61. package/build/sfcc/log-query.js +55 -15
  62. package/build/sfcc/ocapi-shape.js +70 -14
  63. package/build/sfcc/output.js +41 -11
  64. package/build/sfcc/permissions.js +24 -2
  65. package/build/sfcc/read-body.js +92 -0
  66. package/build/sfcc/read-projection.js +185 -0
  67. package/build/sfcc/read-result.js +158 -0
  68. package/build/sfcc/reads-custom-object-def.js +57 -34
  69. package/build/sfcc/reads-site-preference.js +86 -33
  70. package/build/sfcc/reads-system-object.js +50 -38
  71. package/build/sfcc/sfcc-result.js +106 -0
  72. package/build/sfcc/tool-wrapper.js +56 -13
  73. package/build/sfcc/write-grants.js +45 -22
  74. package/build/sfcc/write-guard.js +21 -13
  75. package/build/sfcc/write-result.js +71 -15
  76. package/build/sfcc/write-tool-common.js +126 -32
  77. package/build/sfcc/writes-custom-object-def.js +6 -2
  78. package/build/sfcc/writes-system-object.js +11 -50
  79. package/build/start-tickets-prereqs.js +129 -0
  80. package/build/start-tickets.js +17 -13
  81. package/build/ticket-backend-metadata.js +59 -0
  82. package/build/ticket-key-utils.js +92 -0
  83. package/build/tool-error-envelope.js +71 -0
  84. package/build/tool-surface-gating.js +72 -0
  85. package/build/update-status.js +102 -0
  86. package/build/upgrade-advice.js +47 -0
  87. package/build/upgrade-cli.js +417 -101
  88. package/build/version.generated.js +1 -1
  89. package/build/worktree-core.js +73 -0
  90. package/docs/CONDUCTOR.md +23 -8
  91. package/package.json +3 -3
  92. package/pipelines/implement-ticket.json +15 -5
@@ -9,6 +9,34 @@
9
9
  * All three 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
11
  *
12
+ * PREFERENCE VALUES ARE NOT AVAILABLE HERE (BAPI-767).
13
+ * `preference_search` returns hits shaped `{_type, id}` and nothing more.
14
+ * Verified live against a developer sandbox (OCAPI v25_6): supplying
15
+ * `?select=(**)` changes the response not at all — zero hits gain a `value`
16
+ * key. That is why these two tools take no `projection` input while
17
+ * `site_preference_group_list` does: a projection that cannot alter the
18
+ * response is a flag with no observable effect.
19
+ *
20
+ * The consequence is a genuine caller-facing limitation, stated in both tool
21
+ * descriptions rather than left to be discovered: an ids-only read cannot
22
+ * distinguish "preference not set" from "preference set to the empty string".
23
+ *
24
+ * It is also, for now, accidentally protective. A name-heuristic scan of all 19
25
+ * preference groups on the tested sandbox found secret-named preferences in six
26
+ * of them — including one literally named `client_id` — and none of them expose
27
+ * a value through this endpoint. If OCAPI ever starts returning values here,
28
+ * redaction must be added on the raw body BEFORE normalization, serialization,
29
+ * truncation, or save, so one placement covers both the inline response and the
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.
39
+ *
12
40
  * NOTE: SCAPI Preferences API (sfcc.preferences) is intentionally NOT used.
13
41
  * This keeps the tool context on the OCAPI Data API, preserving the single
14
42
  * AM-OAuth boundary established in T1. Do not "modernize" these tools onto
@@ -18,7 +46,9 @@ import path from "path";
18
46
  import { z } from "zod";
19
47
  import { ocapiGet, ocapiPost } from "./client.js";
20
48
  import { withSfccGate } from "./tool-wrapper.js";
21
- import { normalizeOcapiBody } from "./ocapi-shape.js";
49
+ import { formatOcapiReadFailure, formatSfccReadFailure, withSfccReadErrorBoundary, } from "./read-result.js";
50
+ import { readSfccBody } from "./read-body.js";
51
+ import { projectionInputFor, projectionQueryEntries } from "./read-projection.js";
22
52
  import { truncateAndSaveIfNeeded } from "./output.js";
23
53
  // ---------------------------------------------------------------------------
24
54
  // Annotations (SPEC: read-only against developer sandboxes)
@@ -53,6 +83,7 @@ const sitePreferenceSearchInput = z.object({
53
83
  const sitePreferenceGroupListInput = z.object({
54
84
  count: z.number().optional().describe("Maximum number of site preference groups to return."),
55
85
  start: z.number().optional().describe("Zero-based offset for paging."),
86
+ projection: projectionInputFor("broad_list"),
56
87
  });
57
88
  // ---------------------------------------------------------------------------
58
89
  // Helpers
@@ -66,18 +97,37 @@ function safeGroup(group) {
66
97
  function textResult(text) {
67
98
  return { content: [{ type: "text", text }] };
68
99
  }
69
- async function saveAndReturn(text, dir, filename) {
70
- const output = await truncateAndSaveIfNeeded(text, dir, filename);
100
+ async function saveAndReturn(text, dir, filename, page) {
101
+ const output = await truncateAndSaveIfNeeded(text, dir, filename, page);
71
102
  return textResult(output);
72
103
  }
73
- // D-5: reject any non-sandbox instance context (safer than silently proceeding)
104
+ /**
105
+ * D-5: reject any non-sandbox instance context (safer than silently proceeding).
106
+ *
107
+ * The rejection decision is unchanged; BAPI-759 moved its representation to the
108
+ * shared formatter, so the guardrail fails at the protocol level like every
109
+ * other SFCC failure. Details carry non-sensitive guard metadata only — the
110
+ * rejected value is caller-supplied and enum-constrained.
111
+ *
112
+ * BAPI-766 reworded the message only. It used to open with "v1", which reads as
113
+ * an API version while OCAPI simultaneously reports `_v: "25.6"` in the same
114
+ * session; the constraint belongs to this Bridge tool, not to a version. The
115
+ * status, code, source, decision, and `details` payload are untouched.
116
+ */
74
117
  function rejectIfNotSandbox(instance) {
75
118
  if (instance !== "sandbox") {
76
- return textResult(JSON.stringify({
77
- error: "VALIDATION_ERROR",
119
+ return formatSfccReadFailure({
120
+ code: "VALIDATION_ERROR",
78
121
  status: 400,
79
- message: `v1 only supports the 'sandbox' instance context. Received: '${instance}'.`,
80
- }, null, 2));
122
+ message: `The Bridge site-preference tool only supports the 'sandbox' instance ` +
123
+ `context. Received: '${instance}'.`,
124
+ source: "tool",
125
+ details: {
126
+ guard: "sandbox_only",
127
+ received_instance: instance,
128
+ supported_instances: ["sandbox"],
129
+ },
130
+ });
81
131
  }
82
132
  return null;
83
133
  }
@@ -85,7 +135,7 @@ function rejectIfNotSandbox(instance) {
85
135
  // Handlers
86
136
  // ---------------------------------------------------------------------------
87
137
  export function buildSitePreferenceGetHandler(gateDeps, getDocsDir) {
88
- return withSfccGate(gateDeps, async (args, credentials) => {
138
+ return withSfccReadErrorBoundary(withSfccGate(gateDeps, async (args, credentials) => {
89
139
  const { group, instance, start, count } = sitePreferenceGetInput.parse(args);
90
140
  const guard = rejectIfNotSandbox(instance);
91
141
  if (guard)
@@ -100,16 +150,16 @@ export function buildSitePreferenceGetHandler(gateDeps, getDocsDir) {
100
150
  postBody.count = count;
101
151
  const result = await ocapiPost(`/site_preferences/preference_groups/${encodedGroup}/${instance}/preference_search`, postBody, credentials);
102
152
  if (!result.ok) {
103
- return textResult(JSON.stringify({ error: "OCAPI error", status: result.status, body: result.body }, null, 2));
153
+ return formatOcapiReadFailure(result);
104
154
  }
105
- const normalized = normalizeOcapiBody(result.body);
155
+ const normalized = readSfccBody(result.body);
106
156
  const text = JSON.stringify(normalized, null, 2);
107
157
  const dir = path.join(await getDocsDir(), "sfcc");
108
- return saveAndReturn(text, dir, `site-preference-get-${safeGroup(group)}-${safeTimestamp()}.json`);
109
- });
158
+ return saveAndReturn(text, dir, `site-preference-get-${safeGroup(group)}-${safeTimestamp()}.json`, normalized.page);
159
+ }));
110
160
  }
111
161
  export function buildSitePreferenceSearchHandler(gateDeps, getDocsDir) {
112
- return withSfccGate(gateDeps, async (args, credentials) => {
162
+ return withSfccReadErrorBoundary(withSfccGate(gateDeps, async (args, credentials) => {
113
163
  const { group, instance, query, start, count, sorts } = sitePreferenceSearchInput.parse(args);
114
164
  const guard = rejectIfNotSandbox(instance);
115
165
  if (guard)
@@ -128,18 +178,20 @@ export function buildSitePreferenceSearchHandler(gateDeps, getDocsDir) {
128
178
  postBody.sorts = sorts;
129
179
  const result = await ocapiPost(`/site_preferences/preference_groups/${encodedGroup}/${instance}/preference_search`, postBody, credentials);
130
180
  if (!result.ok) {
131
- return textResult(JSON.stringify({ error: "OCAPI error", status: result.status, body: result.body }, null, 2));
181
+ return formatOcapiReadFailure(result);
132
182
  }
133
- const normalized = normalizeOcapiBody(result.body);
183
+ const normalized = readSfccBody(result.body);
134
184
  const text = JSON.stringify(normalized, null, 2);
135
185
  const dir = path.join(await getDocsDir(), "sfcc");
136
- return saveAndReturn(text, dir, `site-preference-search-${safeGroup(group)}-${safeTimestamp()}.json`);
137
- });
186
+ return saveAndReturn(text, dir, `site-preference-search-${safeGroup(group)}-${safeTimestamp()}.json`, normalized.page);
187
+ }));
138
188
  }
139
189
  export function buildSitePreferenceGroupListHandler(gateDeps, getDocsDir) {
140
- return withSfccGate(gateDeps, async (args, credentials) => {
141
- const { count, start } = sitePreferenceGroupListInput.parse(args);
142
- const queryParams = {};
190
+ return withSfccReadErrorBoundary(withSfccGate(gateDeps, async (args, credentials) => {
191
+ const { count, start, projection } = sitePreferenceGroupListInput.parse(args);
192
+ const queryParams = {
193
+ ...projectionQueryEntries("broad_list", projection),
194
+ };
143
195
  if (count !== undefined)
144
196
  queryParams.count = String(count);
145
197
  if (start !== undefined)
@@ -149,13 +201,13 @@ export function buildSitePreferenceGroupListHandler(gateDeps, getDocsDir) {
149
201
  : "";
150
202
  const result = await ocapiGet(`/system_object_definitions/SitePreferences/attribute_groups${queryStr}`, credentials);
151
203
  if (!result.ok) {
152
- return textResult(JSON.stringify({ error: "OCAPI error", status: result.status, body: result.body }, null, 2));
204
+ return formatOcapiReadFailure(result);
153
205
  }
154
- const normalized = normalizeOcapiBody(result.body);
206
+ const normalized = readSfccBody(result.body);
155
207
  const text = JSON.stringify(normalized, null, 2);
156
208
  const dir = path.join(await getDocsDir(), "sfcc");
157
- return saveAndReturn(text, dir, `site-preference-group-list-${safeTimestamp()}.json`);
158
- });
209
+ return saveAndReturn(text, dir, `site-preference-group-list-${safeTimestamp()}.json`, normalized.page);
210
+ }));
159
211
  }
160
212
  /**
161
213
  * Register the three SFCC site-preference introspection read tools.
@@ -173,18 +225,19 @@ export function registerSitePreferenceTools(registerTool, deps) {
173
225
  annotations: READ_ANNOTATIONS,
174
226
  }, buildSitePreferenceGroupListHandler(gateDeps, getDocsDir));
175
227
  registerTool("site_preference_get", {
176
- description: "Read effective preferences for a site preference group from the developer sandbox " +
177
- "via a match-all POST /site_preferences/preference_groups/{group}/sandbox/preference_search. " +
178
- "Read-only, sandbox-only. Accepts optional `start`/`count` paging in the POST body. " +
179
- "Oversized payloads are auto-saved locally.",
228
+ description: "List preference IDENTIFIERS for a site preference group from the developer sandbox via a match-all " +
229
+ "POST /site_preferences/preference_groups/{group}/sandbox/preference_search. " +
230
+ "Returns ids only, NOT values 'unset' and 'empty string' are indistinguishable. " +
231
+ "Read-only, sandbox-only. Optional start/count paging. Oversized payloads auto-saved locally.",
180
232
  inputSchema: sitePreferenceGetInput,
181
233
  annotations: READ_ANNOTATIONS,
182
234
  }, buildSitePreferenceGetHandler(gateDeps, getDocsDir));
183
235
  registerTool("site_preference_search", {
184
- description: "Search/filter preferences within a site preference group from the developer sandbox " +
185
- "via POST /site_preferences/preference_groups/{group}/sandbox/preference_search. " +
186
- "Read-only; v1 sandbox only. Pass a plain string for id-only text search or a structured " +
187
- "OCAPI query object. Oversized results are auto-saved locally.",
236
+ description: "Search preference IDENTIFIERS within a site preference group via " +
237
+ "POST /site_preferences/preference_groups/{group}/sandbox/preference_search. " +
238
+ "Returns ids only this endpoint does not expose values. Read-only; v1 sandbox only. " +
239
+ "Pass a plain string for id-only text search or a structured OCAPI query. " +
240
+ "Oversized results auto-saved locally.",
188
241
  inputSchema: sitePreferenceSearchInput,
189
242
  annotations: READ_ANNOTATIONS,
190
243
  }, buildSitePreferenceSearchHandler(gateDeps, getDocsDir));
@@ -8,12 +8,19 @@
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
- import { normalizeOcapiBody } from "./ocapi-shape.js";
21
+ import { formatOcapiReadFailure, withSfccReadErrorBoundary } from "./read-result.js";
22
+ import { readSfccBody } from "./read-body.js";
23
+ import { applyProjectionToBody, projectionInputFor, projectionQueryEntries, } from "./read-projection.js";
17
24
  import { truncateAndSaveIfNeeded } from "./output.js";
18
25
  // ---------------------------------------------------------------------------
19
26
  // Annotations (SPEC: read-only against developer sandboxes)
@@ -30,19 +37,23 @@ const READ_ANNOTATIONS = {
30
37
  const systemObjectListInput = z.object({
31
38
  count: z.number().optional().describe("Maximum number of system object types to return."),
32
39
  start: z.number().optional().describe("Zero-based offset for paging."),
40
+ projection: projectionInputFor("broad_list"),
33
41
  });
42
+ // No `projection` field: GET /system_object_definitions/{type} already returns
43
+ // its complete document and ignores `select` (verified live — 13 keys with and
44
+ // without it). A flag here would have no observable effect, which is the defect
45
+ // BAPI-760 removed from this same tool. See read-projection.ts.
34
46
  const systemObjectGetInput = z.object({
35
47
  object_type: z.string().describe("System object type identifier, e.g. \"Product\" or \"Order\"."),
36
- expand_attribute_definitions: z.boolean().optional().describe("When true, include the full attribute definition list in the response. " +
37
- "May produce a large payload — auto-saved locally when oversized."),
38
48
  });
39
49
  const systemObjectAttributeSearchInput = z.object({
40
50
  object_type: z.string().describe("System object type to search within, e.g. \"Order\"."),
41
- query: z.union([z.string(), z.record(z.string(), z.any())]).describe("Search query. Pass a plain string for a text search across id and display_name, " +
42
- "or a structured OCAPI query object (term_query, filtered_query, etc.)."),
51
+ 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
+ "id and display_name; a structured OCAPI query object passes through unchanged."),
43
53
  start: z.number().optional().describe("Zero-based offset for paging."),
44
54
  count: z.number().optional().describe("Maximum number of results to return."),
45
55
  sorts: z.array(z.any()).optional().describe("Array of OCAPI sort descriptors."),
56
+ projection: projectionInputFor("attribute_search"),
46
57
  });
47
58
  // ---------------------------------------------------------------------------
48
59
  // Helpers
@@ -56,17 +67,19 @@ function safeType(objectType) {
56
67
  function textResult(text) {
57
68
  return { content: [{ type: "text", text }] };
58
69
  }
59
- async function saveAndReturn(text, dir, filename) {
60
- const output = await truncateAndSaveIfNeeded(text, dir, filename);
70
+ async function saveAndReturn(text, dir, filename, page) {
71
+ const output = await truncateAndSaveIfNeeded(text, dir, filename, page);
61
72
  return textResult(output);
62
73
  }
63
74
  // ---------------------------------------------------------------------------
64
75
  // Handlers
65
76
  // ---------------------------------------------------------------------------
66
77
  function buildSystemObjectListHandler(gateDeps, getDocsDir) {
67
- return withSfccGate(gateDeps, async (args, credentials) => {
68
- const { count, start } = systemObjectListInput.parse(args);
69
- const queryParams = {};
78
+ return withSfccReadErrorBoundary(withSfccGate(gateDeps, async (args, credentials) => {
79
+ const { count, start, projection } = systemObjectListInput.parse(args);
80
+ const queryParams = {
81
+ ...projectionQueryEntries("broad_list", projection),
82
+ };
70
83
  if (count !== undefined)
71
84
  queryParams.count = String(count);
72
85
  if (start !== undefined)
@@ -76,34 +89,31 @@ function buildSystemObjectListHandler(gateDeps, getDocsDir) {
76
89
  : "";
77
90
  const result = await ocapiGet(`/system_object_definitions${queryStr}`, credentials);
78
91
  if (!result.ok) {
79
- return textResult(JSON.stringify({ error: `OCAPI error`, status: result.status, body: result.body }, null, 2));
92
+ return formatOcapiReadFailure(result);
80
93
  }
81
- const normalized = normalizeOcapiBody(result.body);
94
+ const normalized = readSfccBody(result.body);
82
95
  const text = JSON.stringify(normalized, null, 2);
83
96
  const dir = path.join(await getDocsDir(), "sfcc");
84
- return saveAndReturn(text, dir, `system-object-list-${safeTimestamp()}.json`);
85
- });
97
+ return saveAndReturn(text, dir, `system-object-list-${safeTimestamp()}.json`, normalized.page);
98
+ }));
86
99
  }
87
100
  function buildSystemObjectGetHandler(gateDeps, getDocsDir) {
88
- return withSfccGate(gateDeps, async (args, credentials) => {
89
- const { object_type, expand_attribute_definitions } = systemObjectGetInput.parse(args);
101
+ return withSfccReadErrorBoundary(withSfccGate(gateDeps, async (args, credentials) => {
102
+ const { object_type } = systemObjectGetInput.parse(args);
90
103
  const encodedType = encodeURIComponent(object_type);
91
- const expandParam = expand_attribute_definitions === true
92
- ? "?expand=attribute_definitions"
93
- : "";
94
- const result = await ocapiGet(`/system_object_definitions/${encodedType}${expandParam}`, credentials);
104
+ const result = await ocapiGet(`/system_object_definitions/${encodedType}`, credentials);
95
105
  if (!result.ok) {
96
- return textResult(JSON.stringify({ error: `OCAPI error`, status: result.status, body: result.body }, null, 2));
106
+ return formatOcapiReadFailure(result);
97
107
  }
98
- const normalized = normalizeOcapiBody(result.body);
108
+ const normalized = readSfccBody(result.body);
99
109
  const text = JSON.stringify(normalized, null, 2);
100
110
  const dir = path.join(await getDocsDir(), "sfcc");
101
- return saveAndReturn(text, dir, `system-object-get-${safeType(object_type)}-${safeTimestamp()}.json`);
102
- });
111
+ return saveAndReturn(text, dir, `system-object-get-${safeType(object_type)}-${safeTimestamp()}.json`, normalized.page);
112
+ }));
103
113
  }
104
114
  function buildSystemObjectAttributeSearchHandler(gateDeps, getDocsDir) {
105
- return withSfccGate(gateDeps, async (args, credentials) => {
106
- const { object_type, query, start, count, sorts } = systemObjectAttributeSearchInput.parse(args);
115
+ return withSfccReadErrorBoundary(withSfccGate(gateDeps, async (args, credentials) => {
116
+ const { object_type, query, start, count, sorts, projection } = systemObjectAttributeSearchInput.parse(args);
107
117
  const encodedType = encodeURIComponent(object_type);
108
118
  // Coerce plain string queries into OCAPI text_query shape
109
119
  const resolvedQuery = typeof query === "string"
@@ -116,15 +126,16 @@ function buildSystemObjectAttributeSearchHandler(gateDeps, getDocsDir) {
116
126
  postBody.count = count;
117
127
  if (sorts !== undefined)
118
128
  postBody.sorts = sorts;
129
+ applyProjectionToBody(postBody, "attribute_search", projection);
119
130
  const result = await ocapiPost(`/system_object_definitions/${encodedType}/attribute_definition_search`, postBody, credentials);
120
131
  if (!result.ok) {
121
- return textResult(JSON.stringify({ error: `OCAPI error`, status: result.status, body: result.body }, null, 2));
132
+ return formatOcapiReadFailure(result);
122
133
  }
123
- const normalized = normalizeOcapiBody(result.body);
134
+ const normalized = readSfccBody(result.body);
124
135
  const text = JSON.stringify(normalized, null, 2);
125
136
  const dir = path.join(await getDocsDir(), "sfcc");
126
- return saveAndReturn(text, dir, `system-object-search-${safeType(object_type)}-${safeTimestamp()}.json`);
127
- });
137
+ return saveAndReturn(text, dir, `system-object-search-${safeType(object_type)}-${safeTimestamp()}.json`, normalized.page);
138
+ }));
128
139
  }
129
140
  /**
130
141
  * Register the three SFCC system-object introspection read tools.
@@ -137,25 +148,26 @@ export function registerSystemObjectReadTools(registerTool, deps) {
137
148
  description: "List all system object types from the developer sandbox via " +
138
149
  "GET /system_object_definitions. Read-only. " +
139
150
  "Accepts optional `count` and `start` for paging (OCAPI default pagination applies when omitted). " +
140
- "Oversized outputs are auto-saved locally and previewed inline.",
151
+ "Returns ids only by default; pass projection=full for all fields. " +
152
+ "Oversized outputs are auto-saved locally.",
141
153
  inputSchema: systemObjectListInput,
142
154
  annotations: READ_ANNOTATIONS,
143
155
  }, buildSystemObjectListHandler(gateDeps, getDocsDir));
144
156
  registerTool("system_object_get", {
145
- description: "Retrieve a system object type from the developer sandbox. Read-only. " +
157
+ description: "Retrieve a system object type definition from the developer sandbox. Read-only. " +
146
158
  "GET /system_object_definitions/{type}. " +
147
- "Prefer system_object_attribute_search for targeted attribute lookups " +
148
- "use this for a full type dump or expanded attribute list " +
149
- "(expand_attribute_definitions=true). Oversized payloads are auto-saved locally.",
159
+ "Prefer system_object_attribute_search for targeted attribute lookups. " +
160
+ "Oversized payloads are auto-saved locally.",
150
161
  inputSchema: systemObjectGetInput,
151
162
  annotations: READ_ANNOTATIONS,
152
163
  }, buildSystemObjectGetHandler(gateDeps, getDocsDir));
153
164
  registerTool("system_object_attribute_search", {
154
165
  description: "Search attribute definitions for a system object type. Read-only. " +
155
166
  "POST /system_object_definitions/{type}/attribute_definition_search. " +
156
- "Prefer over system_object_get+expand for targeted c_ attribute lookups. " +
157
- "Pass a plain string for text search or a structured OCAPI query object. " +
158
- "Oversized results are auto-saved locally.",
167
+ "Use for targeted c_ attribute lookups; returns each attribute's schema " +
168
+ "by default (default_value withheld). " +
169
+ "Pass a plain string for text search or a structured OCAPI query. " +
170
+ "Oversized results auto-saved locally.",
159
171
  inputSchema: systemObjectAttributeSearchInput,
160
172
  annotations: READ_ANNOTATIONS,
161
173
  }, buildSystemObjectAttributeSearchHandler(gateDeps, getDocsDir));
@@ -0,0 +1,106 @@
1
+ /**
2
+ * The universal SFCC failure contract (BAPI-766).
3
+ *
4
+ * BAPI-759 unified the SFCC *read* surface behind one formatter. This module
5
+ * generalizes that formatter so the same envelope covers every SFCC tool: reads,
6
+ * all nine writes, the OCAPI call-time gate, and the separate WebDAV log surface.
7
+ * There is exactly one normalization and one serialization implementation here —
8
+ * two parallel formatters would let the read and write sides drift, which is the
9
+ * failure this module exists to prevent.
10
+ *
11
+ * Two things are unified:
12
+ *
13
+ * 1. **The protocol predicate.** Every formatted failure sets `isError: true`,
14
+ * so a caller can detect "this call did not succeed" without parsing text.
15
+ * `result.isError === true` is therefore a *total* predicate for failure
16
+ * across the SFCC surface. `sfcc_setup_status` is the one deliberate
17
+ * exemption: reporting a not-configured repository is its *successful*
18
+ * output, and it sits outside every gate.
19
+ *
20
+ * 2. **The body shape.** Exactly one text content item, whose text parses as
21
+ * `{ error: { code, status, message, source, details } }`. Those four
22
+ * fields mean the same thing for an OCAPI fault, a tool-level guardrail, a
23
+ * gate rejection, a WebDAV failure, and an unexpected internal failure — one
24
+ * parse path reads all of them with no shape discrimination.
25
+ *
26
+ * Everything failure-specific lives under `error.details`, which is always
27
+ * present. An object-valued upstream body is preserved there verbatim, so
28
+ * upstream data is never discarded and never collides with the Bridge-owned
29
+ * envelope fields.
30
+ *
31
+ * Successful results are NOT passed through this formatter. `SfccToolResult`
32
+ * declares `isError` optional precisely so a content-only success (every
33
+ * successful read, every successful write) stays assignable without gaining the
34
+ * flag.
35
+ */
36
+ // ---------------------------------------------------------------------------
37
+ // Detail normalization
38
+ // ---------------------------------------------------------------------------
39
+ /** True for a plain (non-array, non-null) object whose fields can be preserved in place. */
40
+ function isPlainObject(value) {
41
+ return typeof value === "object" && value !== null && !Array.isArray(value);
42
+ }
43
+ /**
44
+ * Coerce arbitrary detail input into an object so `error.details` is always
45
+ * present and always the same kind of thing.
46
+ *
47
+ * A plain object is preserved verbatim (shallow copy) — this is what keeps an
48
+ * OCAPI fault body's `_v` / `fault.*` fields at their original nesting. Anything
49
+ * else (absent, null, a primitive, an array) is wrapped as `{ value }` rather
50
+ * than being spread, which would scatter an array across numeric keys or
51
+ * fabricate structure that upstream never sent.
52
+ */
53
+ function normalizeDetails(details) {
54
+ if (details === undefined)
55
+ return {};
56
+ if (isPlainObject(details))
57
+ return { ...details };
58
+ return { value: details };
59
+ }
60
+ // ---------------------------------------------------------------------------
61
+ // Formatter
62
+ // ---------------------------------------------------------------------------
63
+ /**
64
+ * Format any SFCC failure into the unified protocol result.
65
+ *
66
+ * Returns exactly one text content item and sets `isError: true`. The text
67
+ * serializes exactly one top-level `error` object. This is the single
68
+ * serialization point for the whole SFCC surface.
69
+ */
70
+ export function formatSfccFailure(input) {
71
+ const details = normalizeDetails(input.details);
72
+ // Only surface the upstream status when it actually differs from the status
73
+ // we are reporting. Equal statuses would be noise; a synthetic status has no
74
+ // upstream to report at all.
75
+ if (input.upstreamHttpStatus !== undefined &&
76
+ input.upstreamHttpStatus !== input.status) {
77
+ details.upstream_http_status = input.upstreamHttpStatus;
78
+ }
79
+ const envelope = {
80
+ error: {
81
+ code: input.code,
82
+ status: input.status,
83
+ message: input.message,
84
+ source: input.source,
85
+ details,
86
+ },
87
+ };
88
+ return {
89
+ isError: true,
90
+ content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }],
91
+ };
92
+ }
93
+ /**
94
+ * Read-side name for the shared formatter, kept so BAPI-759's read call sites
95
+ * read naturally. It is a thin delegate, not a second implementation.
96
+ */
97
+ export function formatSfccReadFailure(input) {
98
+ return formatSfccFailure(input);
99
+ }
100
+ /**
101
+ * Write-side name for the shared formatter. Same contract, same implementation —
102
+ * the discriminator between the two sides is `source`, not the function called.
103
+ */
104
+ export function formatSfccWriteFailure(input) {
105
+ return formatSfccFailure(input);
106
+ }
@@ -12,23 +12,55 @@
12
12
  */
13
13
  import { SFCC_VERSIONS, getSfccVersionConfig } from "./config.js";
14
14
  import { resolveSfccCredentials } from "./credentials.js";
15
+ import { formatSfccFailure } from "./sfcc-result.js";
15
16
  // ---------------------------------------------------------------------------
16
17
  // Not-configured envelope helpers
17
18
  // ---------------------------------------------------------------------------
19
+ /**
20
+ * Appended to every gate diagnostic (BAPI-759).
21
+ *
22
+ * A hard protocol failure in a repository that simply is not an SFCC project
23
+ * must not send a calling agent into a retry or self-repair loop, so the
24
+ * message says so outright. It is appended to — never a replacement for — each
25
+ * failure class's own distinct diagnostic.
26
+ */
27
+ const NO_RETRY_INSTRUCTION = "This repository is not configured for SFCC; do not retry SFCC tools.";
28
+ /**
29
+ * Build the unified gate failure result.
30
+ *
31
+ * The gate's 503 is synthetic — it is Bridge's own status, not something OCAPI
32
+ * returned — so no upstream HTTP status is supplied and `details` carries only
33
+ * the preserved `failure_class`.
34
+ *
35
+ * This runs for READ AND WRITE tools alike. `withSfccGate` is shared
36
+ * infrastructure — the nine write handlers in `writes-system-object.ts`,
37
+ * `writes-custom-object-def.ts`, and `writes-site-preference.ts` all wrap
38
+ * themselves in it — so a gate rejection on a write tool returns this envelope
39
+ * with `isError: true` too. That is intended, not incidental: a write tool that
40
+ * never reached OCAPI produced no write, so the protocol failure is accurate.
41
+ * The flag is set unconditionally, with no per-tool branch, tool-name check, or
42
+ * read/write mode parameter, precisely so no caller has to know which side of
43
+ * the surface it is on.
44
+ *
45
+ * BAPI-766 closed the split BAPI-759 left behind: a write tool's OWN
46
+ * pre-transport failures (Zod, sandbox guard, id mismatch, unexpected throw) and
47
+ * its post-transport OCAPI failures now use this same envelope, built through
48
+ * the shared formatter in `sfcc-result.ts`. `write-gate-rejection.test.ts`
49
+ * drives the real gate through the real write handlers and asserts that the two
50
+ * origins now produce the same shape, so neither can drift silently.
51
+ *
52
+ * `sfcc_log_query` keeps its own separate gate (`withSfccLogGate`) because it
53
+ * authenticates over WebDAV Basic auth rather than the OCAPI AM-OAuth boundary.
54
+ * Only the failure representation is shared — those two gates are not merged.
55
+ */
18
56
  function notConfigured(failureClass, message) {
19
- return {
20
- content: [
21
- {
22
- type: "text",
23
- text: JSON.stringify({
24
- error: "NOT_CONFIGURED",
25
- status: 503,
26
- failure_class: failureClass,
27
- message,
28
- }),
29
- },
30
- ],
31
- };
57
+ return formatSfccFailure({
58
+ code: "NOT_CONFIGURED",
59
+ status: 503,
60
+ message: `${message} ${NO_RETRY_INSTRUCTION}`,
61
+ source: "gate",
62
+ details: { failure_class: failureClass },
63
+ });
32
64
  }
33
65
  // ---------------------------------------------------------------------------
34
66
  // Gate
@@ -41,6 +73,17 @@ function notConfigured(failureClass, message) {
41
73
  * 2. Validates the value is an SFCC version.
42
74
  * 3. Resolves dw.json credentials.
43
75
  * 4. Calls the inner handler only if all checks pass.
76
+ *
77
+ * Every early rejection is the unified `isError: true` result — unconditionally,
78
+ * with no tool-name check, per-tool branch, exemption flag, or gate-mode
79
+ * parameter. `sfcc_setup_status`, the one SFCC tool whose successful output *is*
80
+ * a not-configured report, is exempt structurally: `register.ts` registers it
81
+ * outside this gate, so no code here needs to know about it.
82
+ *
83
+ * On the success path the inner handler's result is returned directly — not
84
+ * cloned, wrapped, or re-formatted — so write handlers keep ownership of their
85
+ * own result bodies. The generic `R` preserves each caller's concrete result
86
+ * type through the gate.
44
87
  */
45
88
  export function withSfccGate(deps, handler) {
46
89
  return async (args) => {