@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.
- package/README.md +48 -8
- package/build/base-url.js +79 -0
- package/build/bridge-api-urls.js +9 -0
- package/build/chain-orchestrator.js +93 -15
- package/build/claude-user-config-doctor.js +317 -0
- package/build/commands.generated.js +2 -1
- package/build/conductor/bridge-api-client.js +178 -4
- package/build/conductor-bin.js +1 -1
- package/build/conductor-bundle-artifacts.js +7 -6
- package/build/credential-store.js +205 -4
- package/build/direct-ticket-tools.js +70 -0
- package/build/doctor.js +239 -80
- package/build/executor/cli.js +51 -1
- package/build/executor/credentials.js +1 -7
- package/build/executor/deps.js +18 -1
- package/build/executor/env.js +51 -25
- package/build/executor/heartbeat.js +138 -17
- package/build/executor/http-client.js +49 -8
- package/build/executor/job-errors.js +4 -0
- package/build/executor/job-runner.js +422 -22
- package/build/executor/observation.js +130 -0
- package/build/executor/permissions.js +104 -8
- package/build/executor/preflight.js +32 -0
- package/build/executor/runner.js +8 -0
- package/build/executor/test-clock.js +67 -3
- package/build/executor/types.js +4 -1
- package/build/executor/worker-command.js +11 -3
- package/build/executor/worker-config-isolation.js +287 -0
- package/build/executor/worker-finalization.js +68 -14
- package/build/executor/worktree.js +46 -4
- package/build/index.js +614 -244
- package/build/init.js +363 -73
- package/build/install-bridge.js +568 -80
- package/build/launcher-config-inspection.js +351 -0
- package/build/mcp-invoke.js +49 -6
- package/build/mcp-provisioning.js +30 -7
- package/build/mcp-registration-doctor.js +14 -5
- package/build/notifications.js +553 -0
- package/build/pipeline-orchestrator.js +146 -4
- package/build/pipeline-utils.js +3 -0
- package/build/pipelines.generated.js +22 -9
- package/build/plan-execution-ledger.js +550 -0
- package/build/plan-phase-routing.js +272 -0
- package/build/plane/alembic-head.js +110 -0
- package/build/plane/build-freshness.js +167 -0
- package/build/plane/cli.js +480 -0
- package/build/plane/defaults.js +266 -0
- package/build/plane/manifest.js +377 -0
- package/build/plane/member-logs.js +147 -0
- package/build/plane/member-roster.js +147 -0
- package/build/plane/preflight.js +289 -0
- package/build/plane/shutdown.js +195 -0
- package/build/plane/status.js +125 -0
- package/build/plane/supervisor.js +569 -0
- package/build/plane/test-fakes.js +156 -0
- package/build/plane/types.js +75 -0
- package/build/readme.generated.js +1 -1
- package/build/run-unit-tests-launcher.js +2 -0
- package/build/setup-epic.js +662 -27
- package/build/sfcc/log-gate.js +38 -11
- package/build/sfcc/log-query.js +55 -15
- package/build/sfcc/ocapi-shape.js +70 -14
- package/build/sfcc/output.js +41 -11
- package/build/sfcc/permissions.js +24 -2
- package/build/sfcc/read-body.js +92 -0
- package/build/sfcc/read-projection.js +185 -0
- package/build/sfcc/read-result.js +158 -0
- package/build/sfcc/reads-custom-object-def.js +57 -34
- package/build/sfcc/reads-site-preference.js +86 -33
- package/build/sfcc/reads-system-object.js +50 -38
- package/build/sfcc/sfcc-result.js +106 -0
- package/build/sfcc/tool-wrapper.js +56 -13
- package/build/sfcc/write-grants.js +45 -22
- package/build/sfcc/write-guard.js +21 -13
- package/build/sfcc/write-result.js +71 -15
- package/build/sfcc/write-tool-common.js +126 -32
- package/build/sfcc/writes-custom-object-def.js +6 -2
- package/build/sfcc/writes-system-object.js +11 -50
- package/build/start-tickets-prereqs.js +129 -0
- package/build/start-tickets.js +17 -13
- package/build/ticket-backend-metadata.js +59 -0
- package/build/ticket-key-utils.js +92 -0
- package/build/tool-error-envelope.js +71 -0
- package/build/tool-surface-gating.js +72 -0
- package/build/update-status.js +102 -0
- package/build/upgrade-advice.js +47 -0
- package/build/upgrade-cli.js +417 -101
- package/build/version.generated.js +1 -1
- package/build/worktree-core.js +73 -0
- package/docs/CONDUCTOR.md +23 -8
- package/package.json +3 -3
- 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 {
|
|
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
|
-
|
|
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
|
|
77
|
-
|
|
119
|
+
return formatSfccReadFailure({
|
|
120
|
+
code: "VALIDATION_ERROR",
|
|
78
121
|
status: 400,
|
|
79
|
-
message: `
|
|
80
|
-
|
|
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
|
|
153
|
+
return formatOcapiReadFailure(result);
|
|
104
154
|
}
|
|
105
|
-
const normalized =
|
|
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
|
|
181
|
+
return formatOcapiReadFailure(result);
|
|
132
182
|
}
|
|
133
|
-
const normalized =
|
|
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
|
|
204
|
+
return formatOcapiReadFailure(result);
|
|
153
205
|
}
|
|
154
|
-
const normalized =
|
|
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: "
|
|
177
|
-
"
|
|
178
|
-
"
|
|
179
|
-
"Oversized payloads
|
|
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
|
|
185
|
-
"
|
|
186
|
-
"
|
|
187
|
-
"
|
|
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 {
|
|
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.
|
|
42
|
-
"
|
|
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
|
|
92
|
+
return formatOcapiReadFailure(result);
|
|
80
93
|
}
|
|
81
|
-
const normalized =
|
|
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
|
|
101
|
+
return withSfccReadErrorBoundary(withSfccGate(gateDeps, async (args, credentials) => {
|
|
102
|
+
const { object_type } = systemObjectGetInput.parse(args);
|
|
90
103
|
const encodedType = encodeURIComponent(object_type);
|
|
91
|
-
const
|
|
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
|
|
106
|
+
return formatOcapiReadFailure(result);
|
|
97
107
|
}
|
|
98
|
-
const normalized =
|
|
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
|
|
132
|
+
return formatOcapiReadFailure(result);
|
|
122
133
|
}
|
|
123
|
-
const normalized =
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
157
|
-
"
|
|
158
|
-
"
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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) => {
|