@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
package/build/sfcc/log-gate.js
CHANGED
|
@@ -11,20 +11,36 @@
|
|
|
11
11
|
* (BM username + 40-char WebDAV access key) that lives entirely server-side; the
|
|
12
12
|
* spike proved an AM bearer token 401s on `/Logs`. Readiness is resolved by the
|
|
13
13
|
* backend `GET /jira/sfcc/logs/capability` probe, which never returns a secret.
|
|
14
|
+
*
|
|
15
|
+
* BAPI-766 changed only the *representation* of this gate's failures: they now
|
|
16
|
+
* use the universal SFCC envelope from `sfcc-result.ts`, so one detection rule
|
|
17
|
+
* (`result.isError === true`) and one parse path cover the log surface too. The
|
|
18
|
+
* gate itself remains operationally separate — the WebDAV Basic-auth boundary is
|
|
19
|
+
* still distinct from the OCAPI AM-OAuth boundary, and the two gates are not
|
|
20
|
+
* merged. A capability-probe failure is reported with source `"webdav"`, never
|
|
21
|
+
* `"ocapi"`, because that is where it actually originates.
|
|
14
22
|
*/
|
|
23
|
+
import { formatSfccFailure } from "./sfcc-result.js";
|
|
15
24
|
// ---------------------------------------------------------------------------
|
|
16
25
|
// Envelope helpers
|
|
17
26
|
// ---------------------------------------------------------------------------
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Build the unified log-gate not-configured failure.
|
|
29
|
+
*
|
|
30
|
+
* The 503 is synthetic — Bridge's own status, not something WebDAV returned — so
|
|
31
|
+
* no upstream HTTP status is supplied. `source` is `"gate"`, matching the OCAPI
|
|
32
|
+
* gate: the rejection is the gate's decision, not an upstream response.
|
|
33
|
+
*/
|
|
21
34
|
function notConfigured(failureClass, message, limits) {
|
|
22
|
-
return
|
|
23
|
-
|
|
35
|
+
return formatSfccFailure({
|
|
36
|
+
code: "NOT_CONFIGURED",
|
|
24
37
|
status: 503,
|
|
25
|
-
failure_class: failureClass,
|
|
26
38
|
message,
|
|
27
|
-
|
|
39
|
+
source: "gate",
|
|
40
|
+
details: {
|
|
41
|
+
failure_class: failureClass,
|
|
42
|
+
...(limits !== undefined ? { limits } : {}),
|
|
43
|
+
},
|
|
28
44
|
});
|
|
29
45
|
}
|
|
30
46
|
// ---------------------------------------------------------------------------
|
|
@@ -54,22 +70,33 @@ export function withSfccLogGate(deps, handler) {
|
|
|
54
70
|
const resp = await fetch(url, { headers: await deps.getGetHeaders() });
|
|
55
71
|
if (!resp.ok) {
|
|
56
72
|
// Distinguish a Bridge auth/connectivity failure from "not configured".
|
|
57
|
-
|
|
58
|
-
|
|
73
|
+
// The reported status IS the upstream status, so the shared formatter
|
|
74
|
+
// rightly omits a duplicate `upstream_http_status`.
|
|
75
|
+
return formatSfccFailure({
|
|
76
|
+
code: resp.status === 401 || resp.status === 403
|
|
77
|
+
? "UNAUTHORIZED"
|
|
78
|
+
: "SERVICE_UNAVAILABLE",
|
|
59
79
|
status: resp.status,
|
|
60
80
|
message: "Could not read the SFCC log capability from Bridge API " +
|
|
61
81
|
"(/jira/sfcc/logs/capability). Ensure your Bridge API key is set and the " +
|
|
62
82
|
"repo is authorized. Run sfcc_setup_status for a full diagnostic.",
|
|
83
|
+
source: "webdav",
|
|
84
|
+
details: {},
|
|
85
|
+
upstreamHttpStatus: resp.status,
|
|
63
86
|
});
|
|
64
87
|
}
|
|
65
88
|
body = (await resp.json());
|
|
66
89
|
}
|
|
67
90
|
catch {
|
|
68
|
-
|
|
69
|
-
|
|
91
|
+
// A synthetic 502: no upstream responded at all, so no upstream status
|
|
92
|
+
// may be reported.
|
|
93
|
+
return formatSfccFailure({
|
|
94
|
+
code: "BAD_GATEWAY",
|
|
70
95
|
status: 502,
|
|
71
96
|
message: "Could not reach Bridge API to resolve the SFCC log capability. " +
|
|
72
97
|
"Check that BAPI_BASE_URL points to a running Bridge API instance.",
|
|
98
|
+
source: "webdav",
|
|
99
|
+
details: {},
|
|
73
100
|
});
|
|
74
101
|
}
|
|
75
102
|
if (body?.configured !== true) {
|
package/build/sfcc/log-query.js
CHANGED
|
@@ -11,9 +11,17 @@
|
|
|
11
11
|
* The backend response has already passed through the `RedactionPort`, so its JSON
|
|
12
12
|
* is returned to the LLM unchanged; there is deliberately NO local retrieval
|
|
13
13
|
* bypass in TypeScript.
|
|
14
|
+
*
|
|
15
|
+
* BAPI-766 moved this tool's failure representation onto the universal SFCC
|
|
16
|
+
* envelope (`sfcc-result.ts`), so `result.isError === true` and one parse path
|
|
17
|
+
* cover the log surface exactly as they cover reads and writes. Redaction is
|
|
18
|
+
* untouched: a non-OK backend response contributes only its Bridge-authored
|
|
19
|
+
* string `detail` to `error.details`, and never raw text or an unknown payload
|
|
20
|
+
* whose redaction cannot be established (see `safeBackendDetail`).
|
|
14
21
|
*/
|
|
15
22
|
import { z } from "zod";
|
|
16
23
|
import { withSfccLogGate } from "./log-gate.js";
|
|
24
|
+
import { formatSfccFailure } from "./sfcc-result.js";
|
|
17
25
|
// ---------------------------------------------------------------------------
|
|
18
26
|
// Bounds — mirror src/python/sfcc_monitor/log_query.py (kept in lock-step).
|
|
19
27
|
// ---------------------------------------------------------------------------
|
|
@@ -24,11 +32,42 @@ const MAX_SELECTED_PREFIXES = 5;
|
|
|
24
32
|
const MAX_MAX_ENTRIES = 2000;
|
|
25
33
|
const SUPPORTED_ENVIRONMENTS = ["production", "staging", "development"];
|
|
26
34
|
const PREFIX_RE = /^[a-z][a-z0-9]*$/;
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
/** Bridge-authored message for a non-OK backend query response. */
|
|
36
|
+
function backendFailureMessage(status) {
|
|
37
|
+
return (`The Bridge API SFCC log-query endpoint returned HTTP ${status}. ` +
|
|
38
|
+
`No log content is included in this response.`);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Project a non-OK backend response body down to what is provably safe.
|
|
42
|
+
*
|
|
43
|
+
* Every failure the backend raises (`api/routes/sfcc_log_query.py`) is an
|
|
44
|
+
* `HTTPException` whose `detail` is a fixed Bridge-authored string — never log
|
|
45
|
+
* content. Anything else (a request-validation 422's structured list, an
|
|
46
|
+
* unparsed body, an unexpected payload) has no redaction guarantee, so it is
|
|
47
|
+
* dropped rather than forwarded. Redaction is a shipped safety property
|
|
48
|
+
* (BAPI-554); reformatting must not become a way around it.
|
|
49
|
+
*/
|
|
50
|
+
function safeBackendDetail(text) {
|
|
51
|
+
let parsed;
|
|
52
|
+
try {
|
|
53
|
+
parsed = JSON.parse(text);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
59
|
+
return {};
|
|
60
|
+
const detail = parsed.detail;
|
|
61
|
+
return typeof detail === "string" ? { detail } : {};
|
|
29
62
|
}
|
|
30
63
|
function validationError(message) {
|
|
31
|
-
return
|
|
64
|
+
return formatSfccFailure({
|
|
65
|
+
code: "VALIDATION_ERROR",
|
|
66
|
+
status: 400,
|
|
67
|
+
message,
|
|
68
|
+
source: "tool",
|
|
69
|
+
details: {},
|
|
70
|
+
});
|
|
32
71
|
}
|
|
33
72
|
// ---------------------------------------------------------------------------
|
|
34
73
|
// Zod input schema
|
|
@@ -118,26 +157,27 @@ function buildHandler(deps) {
|
|
|
118
157
|
});
|
|
119
158
|
}
|
|
120
159
|
catch {
|
|
121
|
-
|
|
122
|
-
|
|
160
|
+
// Synthetic 502: nothing responded, so no upstream status may be reported.
|
|
161
|
+
return formatSfccFailure({
|
|
162
|
+
code: "BAD_GATEWAY",
|
|
123
163
|
status: 502,
|
|
124
164
|
message: "Could not reach Bridge API to run the SFCC log query. Check that " +
|
|
125
165
|
"BAPI_BASE_URL points to a running Bridge API instance.",
|
|
166
|
+
source: "webdav",
|
|
167
|
+
details: {},
|
|
126
168
|
});
|
|
127
169
|
}
|
|
128
170
|
const text = await resp.text();
|
|
129
171
|
if (!resp.ok) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
catch {
|
|
135
|
-
/* keep raw text detail */
|
|
136
|
-
}
|
|
137
|
-
return jsonResult({
|
|
138
|
-
error: resp.status >= 500 ? "SERVICE_UNAVAILABLE" : "REQUEST_FAILED",
|
|
172
|
+
// The reported status IS the upstream status, so the shared formatter
|
|
173
|
+
// rightly omits a duplicate `upstream_http_status`.
|
|
174
|
+
return formatSfccFailure({
|
|
175
|
+
code: resp.status >= 500 ? "SERVICE_UNAVAILABLE" : "REQUEST_FAILED",
|
|
139
176
|
status: resp.status,
|
|
140
|
-
|
|
177
|
+
message: backendFailureMessage(resp.status),
|
|
178
|
+
source: "webdav",
|
|
179
|
+
details: safeBackendDetail(text),
|
|
180
|
+
upstreamHttpStatus: resp.status,
|
|
141
181
|
});
|
|
142
182
|
}
|
|
143
183
|
// The backend response has already passed through RedactionPort — return it
|
|
@@ -5,13 +5,56 @@
|
|
|
5
5
|
* envelope. This module provides a typed extractor for the `data` array so
|
|
6
6
|
* sub-task read tools share one normalizer rather than each re-deriving the
|
|
7
7
|
* shape.
|
|
8
|
+
*
|
|
9
|
+
* Collection metadata lives under a single `page` object rather than at the top
|
|
10
|
+
* level (BAPI-760). Two properties of that metadata are load-bearing:
|
|
11
|
+
*
|
|
12
|
+
* - `returned` is the actual length of the `items` array this call produced.
|
|
13
|
+
* It is never read from the upstream `count` field, because OCAPI's `count`
|
|
14
|
+
* is a request-echo (the page size asked for) and not a guarantee about how
|
|
15
|
+
* many records came back.
|
|
16
|
+
* - `total` exists ONLY when OCAPI itself supplied a numeric `total`. It is
|
|
17
|
+
* never inferred from `items.length`, and it is absent — not `null`, not
|
|
18
|
+
* `0` — when OCAPI omitted it. A consumer that sees no `total` knows the
|
|
19
|
+
* instance-wide count is genuinely unavailable rather than zero.
|
|
20
|
+
*
|
|
21
|
+
* Nesting the metadata under `page` also keeps it from colliding with fields
|
|
22
|
+
* that legitimately live inside the returned resources: an OCAPI attribute
|
|
23
|
+
* search document carries its own `count` and `hits`, and those stay untouched
|
|
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.
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* Single constructor for page metadata, shared by both normalizers.
|
|
35
|
+
*
|
|
36
|
+
* Centralizing it means `returned`, `total`, and `has_more` cannot be derived
|
|
37
|
+
* two different ways in the strict and permissive paths. `total` is attached
|
|
38
|
+
* conditionally so an unsupplied total is an absent property rather than an
|
|
39
|
+
* explicit `undefined` that would serialize as a present-but-empty field.
|
|
8
40
|
*/
|
|
41
|
+
function buildPageMeta(items, rawTotal, rawNext) {
|
|
42
|
+
const page = {
|
|
43
|
+
returned: items.length,
|
|
44
|
+
has_more: rawNext != null,
|
|
45
|
+
};
|
|
46
|
+
if (typeof rawTotal === "number") {
|
|
47
|
+
page.total = rawTotal;
|
|
48
|
+
}
|
|
49
|
+
return page;
|
|
50
|
+
}
|
|
9
51
|
/**
|
|
10
52
|
* Parse a raw OCAPI response body into a normalized envelope result.
|
|
11
53
|
*
|
|
12
54
|
* Returns `null` when `body` is not a valid OCAPI paging envelope (missing
|
|
13
55
|
* `data` array or `count` field), allowing callers to surface a structured
|
|
14
|
-
* error instead of crashing.
|
|
56
|
+
* error instead of crashing. Strict validation is unchanged; only the returned
|
|
57
|
+
* metadata shape moved under `page`.
|
|
15
58
|
*/
|
|
16
59
|
export function normalizeOcapiPage(body) {
|
|
17
60
|
if (body === null ||
|
|
@@ -23,9 +66,7 @@ export function normalizeOcapiPage(body) {
|
|
|
23
66
|
const envelope = body;
|
|
24
67
|
return {
|
|
25
68
|
items: envelope.data,
|
|
26
|
-
|
|
27
|
-
total: envelope.total,
|
|
28
|
-
hasMore: envelope.next != null,
|
|
69
|
+
page: buildPageMeta(envelope.data, envelope.total, envelope.next),
|
|
29
70
|
};
|
|
30
71
|
}
|
|
31
72
|
/**
|
|
@@ -40,11 +81,25 @@ export function normalizeOcapiPage(body) {
|
|
|
40
81
|
*
|
|
41
82
|
* Unlike `normalizeOcapiPage`, this function never returns null and is
|
|
42
83
|
* forward-compatible with future OCAPI envelope shapes.
|
|
84
|
+
*
|
|
85
|
+
* Items are passed through verbatim in every branch. A bare object — including
|
|
86
|
+
* an attribute-search document with its own `count` and `hits` — becomes the
|
|
87
|
+
* single item unchanged; its `count` is never promoted to collection metadata,
|
|
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`.
|
|
43
98
|
*/
|
|
44
99
|
export function normalizeOcapiBody(body) {
|
|
45
100
|
// Bare array
|
|
46
101
|
if (Array.isArray(body)) {
|
|
47
|
-
return { items: body,
|
|
102
|
+
return { items: body, page: buildPageMeta(body, undefined, null) };
|
|
48
103
|
}
|
|
49
104
|
// Object forms
|
|
50
105
|
if (body !== null && typeof body === "object") {
|
|
@@ -52,18 +107,19 @@ export function normalizeOcapiBody(body) {
|
|
|
52
107
|
// Wrapped envelope: has a data array (with or without explicit count)
|
|
53
108
|
if (Array.isArray(obj.data)) {
|
|
54
109
|
const items = obj.data;
|
|
55
|
-
return {
|
|
56
|
-
items,
|
|
57
|
-
count: typeof obj.count === "number" ? obj.count : items.length,
|
|
58
|
-
total: typeof obj.total === "number" ? obj.total : undefined,
|
|
59
|
-
hasMore: obj.next != null,
|
|
60
|
-
};
|
|
110
|
+
return { items, page: buildPageMeta(items, obj.total, obj.next) };
|
|
61
111
|
}
|
|
62
|
-
// Bare object (single resource, no wrapping envelope)
|
|
63
|
-
|
|
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.
|
|
118
|
+
const items = [body];
|
|
119
|
+
return { items, page: buildPageMeta(items, undefined, obj.next) };
|
|
64
120
|
}
|
|
65
121
|
// Non-object (null, string, number, etc.)
|
|
66
|
-
return { items: [],
|
|
122
|
+
return { items: [], page: buildPageMeta([], undefined, null) };
|
|
67
123
|
}
|
|
68
124
|
/**
|
|
69
125
|
* SPEC §4.5 `value_type` mapper.
|
package/build/sfcc/output.js
CHANGED
|
@@ -4,23 +4,30 @@
|
|
|
4
4
|
* Re-exports the truncation + docs-path utilities through a stable sfcc/
|
|
5
5
|
* interface so sub-task read tools import from here instead of reaching into
|
|
6
6
|
* the host module directly. This boundary makes a future extraction cheap.
|
|
7
|
+
*
|
|
8
|
+
* Oversized responses return a JSON descriptor rather than a sliced prefix
|
|
9
|
+
* (BAPI-760). Slicing a serialized JSON payload at a fixed character offset
|
|
10
|
+
* almost always cuts mid-token, so what reached the caller was neither the data
|
|
11
|
+
* nor valid JSON — and appending an English note made it unparseable by
|
|
12
|
+
* construction. Every branch here now emits JSON the caller can parse, and the
|
|
13
|
+
* collection metadata travels outside the omitted data so `returned`, `total`,
|
|
14
|
+
* and `has_more` survive truncation.
|
|
7
15
|
*/
|
|
8
16
|
import path from "path";
|
|
9
17
|
import { mkdir, writeFile } from "fs/promises";
|
|
10
18
|
/** Maximum characters returned inline; content beyond this is saved locally. */
|
|
11
19
|
export const SFCC_MAX_INLINE = 50_000;
|
|
12
|
-
/** Truncation note appended to truncated payloads. */
|
|
13
|
-
function truncationNote(savedPath) {
|
|
14
|
-
return `\n\n[Response truncated — full payload saved to ${savedPath}]`;
|
|
15
|
-
}
|
|
16
20
|
/**
|
|
17
21
|
* If `text` exceeds `SFCC_MAX_INLINE`, save it to `dir/filename` and return a
|
|
18
|
-
*
|
|
22
|
+
* JSON truncation descriptor naming the saved file and carrying `page`.
|
|
23
|
+
* Otherwise returns `text` unchanged.
|
|
19
24
|
*
|
|
20
|
-
* Write failures are non-fatal:
|
|
21
|
-
*
|
|
25
|
+
* Write failures are non-fatal: a JSON save-failure descriptor carrying the
|
|
26
|
+
* complete payload is returned rather than throwing. This is deliberately kept
|
|
27
|
+
* on the successful-read path — the OCAPI read itself succeeded, so it does not
|
|
28
|
+
* route through `formatSfccReadFailure` and does not set `isError`.
|
|
22
29
|
*/
|
|
23
|
-
export async function truncateAndSaveIfNeeded(text, dir, filename, deps = {}) {
|
|
30
|
+
export async function truncateAndSaveIfNeeded(text, dir, filename, page, deps = {}) {
|
|
24
31
|
if (text.length <= SFCC_MAX_INLINE) {
|
|
25
32
|
return text;
|
|
26
33
|
}
|
|
@@ -32,8 +39,31 @@ export async function truncateAndSaveIfNeeded(text, dir, filename, deps = {}) {
|
|
|
32
39
|
await wf(filePath, text, "utf-8");
|
|
33
40
|
}
|
|
34
41
|
catch (err) {
|
|
35
|
-
|
|
36
|
-
|
|
42
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
43
|
+
const descriptor = {
|
|
44
|
+
truncated: false,
|
|
45
|
+
save_failed: true,
|
|
46
|
+
oversized: true,
|
|
47
|
+
warning: `Response was NOT truncated because the local save failed: ${message}. ` +
|
|
48
|
+
`The complete payload is returned inline.`,
|
|
49
|
+
page,
|
|
50
|
+
};
|
|
51
|
+
// Parse before embedding so callers receive JSON data rather than a
|
|
52
|
+
// JSON string containing escaped JSON. If the caller's text was never
|
|
53
|
+
// valid JSON, fall back to carrying it verbatim as a string — the helper
|
|
54
|
+
// itself must never emit invalid JSON.
|
|
55
|
+
try {
|
|
56
|
+
descriptor.data = JSON.parse(text);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
descriptor.data_text = text;
|
|
60
|
+
}
|
|
61
|
+
return JSON.stringify(descriptor);
|
|
37
62
|
}
|
|
38
|
-
|
|
63
|
+
const descriptor = {
|
|
64
|
+
truncated: true,
|
|
65
|
+
saved_path: filePath,
|
|
66
|
+
page,
|
|
67
|
+
};
|
|
68
|
+
return JSON.stringify(descriptor);
|
|
39
69
|
}
|
|
@@ -82,7 +82,6 @@ export async function checkPermissionsTool(credentials, ocapiVersion = DEFAULT_O
|
|
|
82
82
|
}
|
|
83
83
|
if (result.ok) {
|
|
84
84
|
const normalized = normalizeOcapiPage(result.body);
|
|
85
|
-
const itemCount = normalized ? normalized.count : "unknown";
|
|
86
85
|
return {
|
|
87
86
|
content: [
|
|
88
87
|
{
|
|
@@ -90,7 +89,7 @@ export async function checkPermissionsTool(credentials, ocapiVersion = DEFAULT_O
|
|
|
90
89
|
text: `✓ OCAPI access confirmed.\n` +
|
|
91
90
|
` Instance: ${credentials.hostname.split(".")[0]}\n` +
|
|
92
91
|
` OCAPI version: ${ocapiVersion}\n` +
|
|
93
|
-
` GET /system_object_definitions: ${
|
|
92
|
+
` GET /system_object_definitions: ${formatProbeCount(normalized)}`,
|
|
94
93
|
},
|
|
95
94
|
],
|
|
96
95
|
};
|
|
@@ -116,6 +115,29 @@ export async function checkPermissionsTool(credentials, ocapiVersion = DEFAULT_O
|
|
|
116
115
|
],
|
|
117
116
|
};
|
|
118
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Render the probe's count line without overstating what OCAPI reported.
|
|
120
|
+
*
|
|
121
|
+
* The probe reads one page, so the number of records it got back is a page
|
|
122
|
+
* count, not the instance-wide total. Saying "N items returned" made a page of
|
|
123
|
+
* 25 look like an instance that owns exactly 25 system objects. Three distinct
|
|
124
|
+
* cases are now distinguishable:
|
|
125
|
+
*
|
|
126
|
+
* - OCAPI supplied a total → "25 of 73 items returned"
|
|
127
|
+
* - OCAPI omitted the total → the number is labelled as the current page and
|
|
128
|
+
* the missing instance total is stated outright
|
|
129
|
+
* - strict normalization failed → neither number is claimed
|
|
130
|
+
*/
|
|
131
|
+
function formatProbeCount(normalized) {
|
|
132
|
+
if (!normalized) {
|
|
133
|
+
return "page count and instance total unavailable (unrecognized response envelope)";
|
|
134
|
+
}
|
|
135
|
+
const { returned, total } = normalized.page;
|
|
136
|
+
if (total !== undefined) {
|
|
137
|
+
return `${returned} of ${total} items returned`;
|
|
138
|
+
}
|
|
139
|
+
return `${returned} items returned on this page (OCAPI did not provide the instance total)`;
|
|
140
|
+
}
|
|
119
141
|
function ocapiSettingsInstructions(ocapiVersion) {
|
|
120
142
|
return (`To grant OCAPI access, paste the JSON below in Business Manager:\n` +
|
|
121
143
|
` Administration > Site Development > Open Commerce API Settings → Data API tab\n\n` +
|
|
@@ -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
|
+
}
|