@bridge_gpt/mcp-server 0.2.36 → 0.2.37
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 +44 -6
- 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 +51 -14
- package/build/sfcc/output.js +41 -11
- package/build/sfcc/permissions.js +24 -2
- package/build/sfcc/read-projection.js +181 -0
- package/build/sfcc/read-result.js +158 -0
- package/build/sfcc/reads-custom-object-def.js +29 -18
- package/build/sfcc/reads-site-preference.js +75 -29
- package/build/sfcc/reads-system-object.js +40 -34
- 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 +61 -14
- package/build/sfcc/write-tool-common.js +126 -32
- 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,49 @@
|
|
|
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
|
+
/**
|
|
27
|
+
* Single constructor for page metadata, shared by both normalizers.
|
|
28
|
+
*
|
|
29
|
+
* Centralizing it means `returned`, `total`, and `has_more` cannot be derived
|
|
30
|
+
* two different ways in the strict and permissive paths. `total` is attached
|
|
31
|
+
* conditionally so an unsupplied total is an absent property rather than an
|
|
32
|
+
* explicit `undefined` that would serialize as a present-but-empty field.
|
|
8
33
|
*/
|
|
34
|
+
function buildPageMeta(items, rawTotal, rawNext) {
|
|
35
|
+
const page = {
|
|
36
|
+
returned: items.length,
|
|
37
|
+
has_more: rawNext != null,
|
|
38
|
+
};
|
|
39
|
+
if (typeof rawTotal === "number") {
|
|
40
|
+
page.total = rawTotal;
|
|
41
|
+
}
|
|
42
|
+
return page;
|
|
43
|
+
}
|
|
9
44
|
/**
|
|
10
45
|
* Parse a raw OCAPI response body into a normalized envelope result.
|
|
11
46
|
*
|
|
12
47
|
* Returns `null` when `body` is not a valid OCAPI paging envelope (missing
|
|
13
48
|
* `data` array or `count` field), allowing callers to surface a structured
|
|
14
|
-
* error instead of crashing.
|
|
49
|
+
* error instead of crashing. Strict validation is unchanged; only the returned
|
|
50
|
+
* metadata shape moved under `page`.
|
|
15
51
|
*/
|
|
16
52
|
export function normalizeOcapiPage(body) {
|
|
17
53
|
if (body === null ||
|
|
@@ -23,9 +59,7 @@ export function normalizeOcapiPage(body) {
|
|
|
23
59
|
const envelope = body;
|
|
24
60
|
return {
|
|
25
61
|
items: envelope.data,
|
|
26
|
-
|
|
27
|
-
total: envelope.total,
|
|
28
|
-
hasMore: envelope.next != null,
|
|
62
|
+
page: buildPageMeta(envelope.data, envelope.total, envelope.next),
|
|
29
63
|
};
|
|
30
64
|
}
|
|
31
65
|
/**
|
|
@@ -40,11 +74,16 @@ export function normalizeOcapiPage(body) {
|
|
|
40
74
|
*
|
|
41
75
|
* Unlike `normalizeOcapiPage`, this function never returns null and is
|
|
42
76
|
* forward-compatible with future OCAPI envelope shapes.
|
|
77
|
+
*
|
|
78
|
+
* Items are passed through verbatim in every branch. A bare object — including
|
|
79
|
+
* an attribute-search document with its own `count` and `hits` — becomes the
|
|
80
|
+
* single item unchanged; its `count` is never promoted to collection metadata,
|
|
81
|
+
* and a missing `hits` is never synthesized.
|
|
43
82
|
*/
|
|
44
83
|
export function normalizeOcapiBody(body) {
|
|
45
84
|
// Bare array
|
|
46
85
|
if (Array.isArray(body)) {
|
|
47
|
-
return { items: body,
|
|
86
|
+
return { items: body, page: buildPageMeta(body, undefined, null) };
|
|
48
87
|
}
|
|
49
88
|
// Object forms
|
|
50
89
|
if (body !== null && typeof body === "object") {
|
|
@@ -52,18 +91,16 @@ export function normalizeOcapiBody(body) {
|
|
|
52
91
|
// Wrapped envelope: has a data array (with or without explicit count)
|
|
53
92
|
if (Array.isArray(obj.data)) {
|
|
54
93
|
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
|
-
};
|
|
94
|
+
return { items, page: buildPageMeta(items, obj.total, obj.next) };
|
|
61
95
|
}
|
|
62
|
-
// Bare object (single resource, no wrapping envelope)
|
|
63
|
-
|
|
96
|
+
// Bare object (single resource, no wrapping envelope). A directly supplied
|
|
97
|
+
// numeric `total` and paging signal are honored; the object's own `count`
|
|
98
|
+
// is left inside the item and never reinterpreted as a total.
|
|
99
|
+
const items = [body];
|
|
100
|
+
return { items, page: buildPageMeta(items, obj.total, obj.next) };
|
|
64
101
|
}
|
|
65
102
|
// Non-object (null, string, number, etc.)
|
|
66
|
-
return { items: [],
|
|
103
|
+
return { items: [], page: buildPageMeta([], undefined, null) };
|
|
67
104
|
}
|
|
68
105
|
/**
|
|
69
106
|
* 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,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SFCC read projection policy (BAPI-767).
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS MODULE EXISTS
|
|
5
|
+
* ----------------------
|
|
6
|
+
* Before BAPI-767 no `select` was constructed anywhere in the SFCC read path.
|
|
7
|
+
* The only occurrence of the OCAPI full-projection expression in the tree was
|
|
8
|
+
* the *grant template* in `permissions.ts` (`read_attributes: "(**)"`) — a
|
|
9
|
+
* permission the tools were never actually exercising. The consequence was the
|
|
10
|
+
* headline finding of the 2026-08-11 introspection report: an agent could
|
|
11
|
+
* enumerate every attribute id on a type and learn nothing about any of them —
|
|
12
|
+
* not the `value_type`, not `mandatory`, `localizable`, `searchable`, or
|
|
13
|
+
* `site_specific`. The MCP could *write* a type it could not read back.
|
|
14
|
+
*
|
|
15
|
+
* The fix is a projection, and the reason it lives in one module rather than in
|
|
16
|
+
* each handler is that the *default* is the load-bearing part. "Complete by
|
|
17
|
+
* default on attribute reads, lean by default on broad lists" is a single
|
|
18
|
+
* ratified policy; six handlers each deciding their own default is how that
|
|
19
|
+
* policy silently drifts. Requirement 2 of the ticket asks for exactly one
|
|
20
|
+
* place with its own test, and this is it.
|
|
21
|
+
*
|
|
22
|
+
* THE `(**)` EXPRESSION IS PRIVATE
|
|
23
|
+
* --------------------------------
|
|
24
|
+
* Callers select a `projection` of `"full"` or `"lean"`. They never see or pass
|
|
25
|
+
* raw OCAPI select syntax. That keeps the tool surface stable if the expression
|
|
26
|
+
* ever changes, and — more importantly — stops an agent from constructing an
|
|
27
|
+
* arbitrary select expression against a live instance.
|
|
28
|
+
*
|
|
29
|
+
* EVERY CATEGORY HERE WAS VERIFIED LIVE
|
|
30
|
+
* -------------------------------------
|
|
31
|
+
* The category table is not documentation-sourced. Each entry was probed
|
|
32
|
+
* against a developer sandbox (OCAPI Data API v25_6) before it was written, and
|
|
33
|
+
* the observed key-count expansion is recorded next to it. This matters because
|
|
34
|
+
* an unverified projection flag is worse than no flag: it reads as a capability
|
|
35
|
+
* and delivers nothing.
|
|
36
|
+
*
|
|
37
|
+
* `system_object_get` is deliberately ABSENT from this policy. Its endpoint,
|
|
38
|
+
* GET /system_object_definitions/{type}, already returns its complete 13-field
|
|
39
|
+
* document and ignores `select` entirely (verified: 13 keys with and without).
|
|
40
|
+
* Giving it a `projection` flag would ship a parameter with no observable
|
|
41
|
+
* effect — the same defect BAPI-760 removed from that very tool when it deleted
|
|
42
|
+
* the no-op `expand_attribute_definitions` boolean. A tool that cannot honor a
|
|
43
|
+
* projection does not get a projection input.
|
|
44
|
+
*/
|
|
45
|
+
import { z } from "zod";
|
|
46
|
+
/** The two projection values, as a runtime-checkable list. */
|
|
47
|
+
export const SFCC_READ_PROJECTIONS = ["full", "lean"];
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// The policy
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
/**
|
|
52
|
+
* OCAPI's full-projection expression.
|
|
53
|
+
*
|
|
54
|
+
* Intentionally NOT exported. Only `selectExpressionFor` may map a projection
|
|
55
|
+
* onto it, so there is exactly one place where an outbound `select` value can
|
|
56
|
+
* originate.
|
|
57
|
+
*/
|
|
58
|
+
const FULL_SELECT_EXPRESSION = "(**)";
|
|
59
|
+
/**
|
|
60
|
+
* The single endpoint-category → projection policy table (Requirement 2).
|
|
61
|
+
*
|
|
62
|
+
* Verified live against a developer sandbox, 2026-08-13 (OCAPI v25_6). The
|
|
63
|
+
* key-count expansions below are observations, not estimates:
|
|
64
|
+
*
|
|
65
|
+
* attribute_search POST .../attribute_definition_search
|
|
66
|
+
* system_object: 4 → 25 keys
|
|
67
|
+
* custom_object_def: 4 → 26 keys
|
|
68
|
+
* attribute_collection GET .../attribute_definitions
|
|
69
|
+
* custom_object_def: 4 → 26 keys
|
|
70
|
+
* broad_list GET /system_object_definitions
|
|
71
|
+
* 4 → 13 keys
|
|
72
|
+
* GET .../SitePreferences/attribute_groups
|
|
73
|
+
* 4 → 10 keys
|
|
74
|
+
*
|
|
75
|
+
* The ratified requirement these defaults encode: complete by default on
|
|
76
|
+
* attribute reads, lean by default on broad lists, caller can override in
|
|
77
|
+
* either direction. An opt-in-only flag was explicitly rejected — the caller
|
|
78
|
+
* who does not know to ask is precisely the caller the report described.
|
|
79
|
+
*/
|
|
80
|
+
const PROJECTION_POLICY = {
|
|
81
|
+
attribute_search: {
|
|
82
|
+
defaultProjection: "full",
|
|
83
|
+
placement: "body",
|
|
84
|
+
inputDescription: "Detail level. Default: full (value_type, mandatory, localizable, searchable, site_specific); lean returns ids only.",
|
|
85
|
+
},
|
|
86
|
+
attribute_collection: {
|
|
87
|
+
defaultProjection: "full",
|
|
88
|
+
placement: "query",
|
|
89
|
+
inputDescription: "Detail level. Default: full (value_type, mandatory, localizable, searchable, site_specific); lean returns ids only.",
|
|
90
|
+
},
|
|
91
|
+
broad_list: {
|
|
92
|
+
defaultProjection: "lean",
|
|
93
|
+
placement: "query",
|
|
94
|
+
inputDescription: "Detail level. Default: lean (ids only); full returns all fields.",
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Resolution
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
/**
|
|
101
|
+
* Resolve the effective projection for a category.
|
|
102
|
+
*
|
|
103
|
+
* An omitted / undefined request resolves to the category default. An explicit
|
|
104
|
+
* caller value always wins — including an explicit `"lean"` on a full-default
|
|
105
|
+
* category, which is the "override in either direction" half of the
|
|
106
|
+
* requirement and is easy to lose to a truthiness check.
|
|
107
|
+
*/
|
|
108
|
+
export function resolveReadProjection(category, requested) {
|
|
109
|
+
return requested ?? PROJECTION_POLICY[category].defaultProjection;
|
|
110
|
+
}
|
|
111
|
+
/** The wire placement (`body` or `query`) for a category. */
|
|
112
|
+
export function projectionPlacementFor(category) {
|
|
113
|
+
return PROJECTION_POLICY[category].placement;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Map an effective projection onto an OCAPI `select` expression.
|
|
117
|
+
*
|
|
118
|
+
* Returns `undefined` for `lean` — deliberately, so `lean` means "send no
|
|
119
|
+
* `select` at all" rather than "send an empty one". OCAPI's unqualified
|
|
120
|
+
* response is the lean shape; there is no lean expression to construct.
|
|
121
|
+
*/
|
|
122
|
+
export function selectExpressionFor(projection) {
|
|
123
|
+
return projection === "full" ? FULL_SELECT_EXPRESSION : undefined;
|
|
124
|
+
}
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Application helpers
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
/**
|
|
129
|
+
* Apply the resolved projection to an OCAPI search POST body.
|
|
130
|
+
*
|
|
131
|
+
* Mutates and returns `body` so a handler can build its body normally and then
|
|
132
|
+
* hand it here as the last step. A `lean` resolution leaves the body untouched
|
|
133
|
+
* — no `select` key is added.
|
|
134
|
+
*
|
|
135
|
+
* Throws if the category is not body-placed, rather than silently writing a
|
|
136
|
+
* `select` into a body OCAPI would ignore for that endpoint.
|
|
137
|
+
*/
|
|
138
|
+
export function applyProjectionToBody(body, category, requested) {
|
|
139
|
+
if (projectionPlacementFor(category) !== "body") {
|
|
140
|
+
throw new Error(`SFCC read category "${category}" carries its projection in the query string, not the POST body.`);
|
|
141
|
+
}
|
|
142
|
+
const expression = selectExpressionFor(resolveReadProjection(category, requested));
|
|
143
|
+
if (expression !== undefined) {
|
|
144
|
+
body.select = expression;
|
|
145
|
+
}
|
|
146
|
+
return body;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Query-string entries carrying the resolved projection.
|
|
150
|
+
*
|
|
151
|
+
* Returns an empty object for `lean`, so a caller can spread it unconditionally
|
|
152
|
+
* into its query params without a conditional at each call site.
|
|
153
|
+
*
|
|
154
|
+
* Throws if the category is body-placed, for the same reason as above.
|
|
155
|
+
*/
|
|
156
|
+
export function projectionQueryEntries(category, requested) {
|
|
157
|
+
if (projectionPlacementFor(category) !== "query") {
|
|
158
|
+
throw new Error(`SFCC read category "${category}" carries its projection in the POST body, not the query string.`);
|
|
159
|
+
}
|
|
160
|
+
const expression = selectExpressionFor(resolveReadProjection(category, requested));
|
|
161
|
+
return expression === undefined ? {} : { select: expression };
|
|
162
|
+
}
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// Input schema
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
/**
|
|
167
|
+
* The optional `projection` Zod field for a category's input schema.
|
|
168
|
+
*
|
|
169
|
+
* The description is category-specific because the default differs by category,
|
|
170
|
+
* and the default is decision-critical *before* invocation — an agent choosing
|
|
171
|
+
* whether to pass `projection` cannot learn the answer from the response. It is
|
|
172
|
+
* kept to the fewest words that convey the default and the override, because
|
|
173
|
+
* every tool description and schema competes for the same tools/list token
|
|
174
|
+
* budget (see `integration/budget-config.ts`).
|
|
175
|
+
*/
|
|
176
|
+
export function projectionInputFor(category) {
|
|
177
|
+
return z
|
|
178
|
+
.enum(["full", "lean"])
|
|
179
|
+
.optional()
|
|
180
|
+
.describe(PROJECTION_POLICY[category].inputDescription);
|
|
181
|
+
}
|