@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
|
@@ -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) => {
|
|
@@ -2,11 +2,18 @@
|
|
|
2
2
|
* Shared OCAPI write-grant JSON helpers (BAPI-582 foundation).
|
|
3
3
|
*
|
|
4
4
|
* This module is the single source of truth for the OCAPI write-grant JSON that
|
|
5
|
-
* is both echoed by
|
|
5
|
+
* is both echoed by write handlers on a 403 and printed by the
|
|
6
6
|
* `check_permissions` diagnostic. Keeping one definition guarantees the
|
|
7
7
|
* remediation JSON an operator sees is identical no matter which surface it
|
|
8
8
|
* comes from.
|
|
9
|
+
*
|
|
10
|
+
* BAPI-766 converted the 403 handler response from prose to the universal SFCC
|
|
11
|
+
* failure envelope. The remediation is not lost — it moved from an English
|
|
12
|
+
* paragraph into structured, non-secret fields under `error.details.remediation`,
|
|
13
|
+
* where a caller can read it without parsing text and `result.isError === true`
|
|
14
|
+
* still detects the failure.
|
|
9
15
|
*/
|
|
16
|
+
import { formatSfccWriteFailure } from "./sfcc-result.js";
|
|
10
17
|
// ---------------------------------------------------------------------------
|
|
11
18
|
// Write resource families
|
|
12
19
|
// ---------------------------------------------------------------------------
|
|
@@ -52,29 +59,45 @@ export function formatOcapiWriteGrantJson(ocapiVersion, clientIdPlaceholder = "<
|
|
|
52
59
|
// ---------------------------------------------------------------------------
|
|
53
60
|
// 403 write-grant UX
|
|
54
61
|
// ---------------------------------------------------------------------------
|
|
55
|
-
/**
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
*/
|
|
60
|
-
export function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
: `\nResponse body:\n${JSON.stringify(body, null, 2)}\n`;
|
|
65
|
-
return (`HTTP 403: OCAPI write access denied for ${operation} ${path}.\n` +
|
|
66
|
-
bodyLine +
|
|
67
|
-
`\nTo grant write access, paste the JSON below in Business Manager:\n` +
|
|
68
|
-
` Administration > Site Development > Open Commerce API Settings → Data API tab\n\n` +
|
|
69
|
-
`${formatOcapiWriteGrantJson(ocapiVersion)}\n\n` +
|
|
70
|
-
`Replace <YOUR_CLIENT_ID> with the client_id from your dw.json.`);
|
|
62
|
+
/** Where an operator pastes the grant JSON in Business Manager. */
|
|
63
|
+
export const OCAPI_WRITE_GRANT_BUSINESS_MANAGER_PATH = "Administration > Site Development > Open Commerce API Settings → Data API tab";
|
|
64
|
+
/** The one instruction an operator must act on after pasting the grant JSON. */
|
|
65
|
+
export const OCAPI_WRITE_GRANT_CLIENT_ID_INSTRUCTION = "Replace <YOUR_CLIENT_ID> with the client_id from your dw.json.";
|
|
66
|
+
/** The fixed, Bridge-authored message for a 403 on an OCAPI write. */
|
|
67
|
+
export function ocapiWriteForbiddenMessage(operation, path) {
|
|
68
|
+
return (`OCAPI write access denied for ${operation} ${path}. The client is not granted ` +
|
|
69
|
+
`write access to this resource. See error.details.remediation for the exact ` +
|
|
70
|
+
`write-grant settings to paste in Business Manager.`);
|
|
71
71
|
}
|
|
72
72
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
73
|
+
* Build the unified 403 failure for a denied OCAPI write.
|
|
74
|
+
*
|
|
75
|
+
* The remediation an operator needs — where to paste, what to paste, and the one
|
|
76
|
+
* substitution to make — is carried as structured fields under
|
|
77
|
+
* `error.details.remediation` rather than as prose, so the result is parseable
|
|
78
|
+
* and `isError: true` detects it like every other SFCC failure. The grant JSON
|
|
79
|
+
* keeps the `<YOUR_CLIENT_ID>` placeholder: the real resolved client_id is never
|
|
80
|
+
* substituted, so nothing secret enters the response.
|
|
81
|
+
*
|
|
82
|
+
* The upstream 403 body is preserved verbatim under `error.details.body`, the
|
|
83
|
+
* same way an OCAPI read fault body is preserved.
|
|
75
84
|
*/
|
|
76
85
|
export function writeGrantForbiddenResult(params) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
86
|
+
const { operation, path, ocapiVersion, body } = params;
|
|
87
|
+
return formatSfccWriteFailure({
|
|
88
|
+
code: "OCAPI_WRITE_ERROR",
|
|
89
|
+
status: 403,
|
|
90
|
+
message: ocapiWriteForbiddenMessage(operation, path),
|
|
91
|
+
source: "ocapi",
|
|
92
|
+
details: {
|
|
93
|
+
operation,
|
|
94
|
+
path,
|
|
95
|
+
remediation: {
|
|
96
|
+
business_manager_path: OCAPI_WRITE_GRANT_BUSINESS_MANAGER_PATH,
|
|
97
|
+
instruction: OCAPI_WRITE_GRANT_CLIENT_ID_INSTRUCTION,
|
|
98
|
+
write_grant: buildOcapiWriteGrantSettings(ocapiVersion),
|
|
99
|
+
},
|
|
100
|
+
...(body === undefined ? {} : { body }),
|
|
101
|
+
},
|
|
102
|
+
});
|
|
80
103
|
}
|
|
@@ -6,14 +6,13 @@
|
|
|
6
6
|
* mutation. A non-null return value is
|
|
7
7
|
* the pre-formatted MCP rejection to return directly; a null return means the
|
|
8
8
|
* effective instance is a sandbox and the handler may proceed.
|
|
9
|
+
*
|
|
10
|
+
* BAPI-766 moved the rejection's *representation* to the shared SFCC formatter
|
|
11
|
+
* so the guardrail fails at the protocol level like every other SFCC failure.
|
|
12
|
+
* The guard's decision is untouched: `undefined` still defaults to `"sandbox"`,
|
|
13
|
+
* `"sandbox"` still proceeds, and every other value is still refused.
|
|
9
14
|
*/
|
|
10
|
-
|
|
11
|
-
// Helpers
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
/** Wrap text in the standard MCP text-result shape. */
|
|
14
|
-
function textResult(text) {
|
|
15
|
-
return { content: [{ type: "text", text }] };
|
|
16
|
-
}
|
|
15
|
+
import { formatSfccWriteFailure } from "./sfcc-result.js";
|
|
17
16
|
// ---------------------------------------------------------------------------
|
|
18
17
|
// Guard
|
|
19
18
|
// ---------------------------------------------------------------------------
|
|
@@ -22,18 +21,27 @@ function textResult(text) {
|
|
|
22
21
|
*
|
|
23
22
|
* SFCC write tools are sandbox-only in this foundation. The effective instance
|
|
24
23
|
* defaults to `"sandbox"` when `undefined` (some schemas default the value
|
|
25
|
-
* elsewhere), so an omitted instance is allowed. Any other value returns
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* elsewhere), so an omitted instance is allowed. Any other value returns the
|
|
25
|
+
* unified validation failure naming the rejected instance; a sandbox returns
|
|
26
|
+
* `null` so the caller proceeds.
|
|
27
|
+
*
|
|
28
|
+
* `details` carries non-sensitive guard metadata only — the rejected value is
|
|
29
|
+
* caller-supplied.
|
|
28
30
|
*/
|
|
29
31
|
export function rejectIfNotSandboxForWrite(instance) {
|
|
30
32
|
const effective = instance === undefined ? "sandbox" : instance;
|
|
31
33
|
if (effective === "sandbox")
|
|
32
34
|
return null;
|
|
33
|
-
return
|
|
34
|
-
|
|
35
|
+
return formatSfccWriteFailure({
|
|
36
|
+
code: "VALIDATION_ERROR",
|
|
35
37
|
status: 400,
|
|
36
38
|
message: `SFCC write tools are sandbox-only. Refusing to write against instance ` +
|
|
37
39
|
`'${effective}'. Re-run the write against a developer sandbox instance.`,
|
|
38
|
-
|
|
40
|
+
source: "tool",
|
|
41
|
+
details: {
|
|
42
|
+
guard: "sandbox_only",
|
|
43
|
+
received_instance: effective,
|
|
44
|
+
supported_instances: ["sandbox"],
|
|
45
|
+
},
|
|
46
|
+
});
|
|
39
47
|
}
|
|
@@ -1,26 +1,67 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared write-result formatter (BAPI-582 foundation).
|
|
2
|
+
* Shared write-result formatter (BAPI-582 foundation, unified by BAPI-766).
|
|
3
3
|
*
|
|
4
|
-
* This is the single formatting path for all
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* This is the single formatting path for all SFCC write tools. Handlers pass the
|
|
5
|
+
* raw `OcapiGetResult` from `ocapiPut`/`ocapiPatch` here rather than inventing
|
|
6
|
+
* their own 403/fault/success response logic, guaranteeing a consistent surface
|
|
7
|
+
* across every write tool.
|
|
8
|
+
*
|
|
9
|
+
* Successful results are unchanged and never gain `isError`. Every *failure*
|
|
10
|
+
* branch — including the 403 write-grant remediation, which used to be prose —
|
|
11
|
+
* now returns the universal nested envelope from `sfcc-result.ts`, so
|
|
12
|
+
* `result.isError === true` is total across a write tool's post-transport
|
|
13
|
+
* failures as well as its pre-transport ones.
|
|
8
14
|
*/
|
|
9
15
|
import { writeGrantForbiddenResult } from "./write-grants.js";
|
|
10
16
|
import { DEFAULT_OCAPI_VERSION } from "./config.js";
|
|
11
|
-
|
|
17
|
+
import { formatSfccWriteFailure } from "./sfcc-result.js";
|
|
18
|
+
/** Wrap text in the standard MCP text-result shape (success path only). */
|
|
12
19
|
function textResult(text) {
|
|
13
20
|
return { content: [{ type: "text", text }] };
|
|
14
21
|
}
|
|
15
22
|
// ---------------------------------------------------------------------------
|
|
23
|
+
// Messages
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
/** True for a plain (non-array, non-null) object. */
|
|
26
|
+
function isPlainObject(value) {
|
|
27
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
28
|
+
}
|
|
29
|
+
/** Bridge-authored fallback used when an OCAPI write body carries no usable `fault.message`. */
|
|
30
|
+
export function ocapiWriteFallbackMessage(status) {
|
|
31
|
+
return (`The SFCC OCAPI Data API returned HTTP ${status} for this write and supplied no ` +
|
|
32
|
+
`fault message. The upstream response body is preserved under error.details.`);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Pull a usable human-readable message out of an OCAPI write fault body.
|
|
36
|
+
*
|
|
37
|
+
* Raw response text is never promoted into `error.message` — an unparsed body
|
|
38
|
+
* falls through to the stable Bridge-authored fallback instead.
|
|
39
|
+
*/
|
|
40
|
+
function ocapiWriteFaultMessage(body, status) {
|
|
41
|
+
if (isPlainObject(body) && isPlainObject(body.fault)) {
|
|
42
|
+
const faultMessage = body.fault.message;
|
|
43
|
+
if (typeof faultMessage === "string" && faultMessage.trim() !== "") {
|
|
44
|
+
return faultMessage;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return ocapiWriteFallbackMessage(status);
|
|
48
|
+
}
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
16
50
|
// Formatter
|
|
17
51
|
// ---------------------------------------------------------------------------
|
|
18
52
|
/**
|
|
19
53
|
* Format an OCAPI write result into the standard MCP tool response.
|
|
20
54
|
*
|
|
21
|
-
* - `403` → the paste-ready write-grant
|
|
22
|
-
*
|
|
23
|
-
* -
|
|
55
|
+
* - `403` → the unified failure carrying the paste-ready write-grant
|
|
56
|
+
* remediation as structured data (via write-grants.ts).
|
|
57
|
+
* - success → JSON with `status`, `outcome`, and `body`; no `isError`.
|
|
58
|
+
* - other failure → the unified `OCAPI_WRITE_ERROR` failure, source `ocapi`,
|
|
59
|
+
* with the structured fault mapping and the upstream body under
|
|
60
|
+
* `error.details`.
|
|
61
|
+
*
|
|
62
|
+
* No `upstreamHttpStatus` is supplied: `ocapiPut`/`ocapiPatch` surface
|
|
63
|
+
* `Response.status` verbatim, so the reported status *is* the upstream status
|
|
64
|
+
* and the shared formatter would rightly omit a duplicate.
|
|
24
65
|
*/
|
|
25
66
|
export function formatOcapiWriteToolResult(result, operation, path, ocapiVersion = DEFAULT_OCAPI_VERSION) {
|
|
26
67
|
if (result.status === 403) {
|
|
@@ -38,10 +79,16 @@ export function formatOcapiWriteToolResult(result, operation, path, ocapiVersion
|
|
|
38
79
|
body: result.body,
|
|
39
80
|
}));
|
|
40
81
|
}
|
|
41
|
-
return
|
|
42
|
-
|
|
82
|
+
return formatSfccWriteFailure({
|
|
83
|
+
code: "OCAPI_WRITE_ERROR",
|
|
43
84
|
status: result.status,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
85
|
+
message: ocapiWriteFaultMessage(result.body, result.status),
|
|
86
|
+
source: "ocapi",
|
|
87
|
+
details: {
|
|
88
|
+
operation,
|
|
89
|
+
path,
|
|
90
|
+
fault: result.fault,
|
|
91
|
+
body: result.body,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
47
94
|
}
|
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared helpers for the SFCC write MCP tools (BAPI-584).
|
|
2
|
+
* Shared helpers for the SFCC write MCP tools (BAPI-584, unified by BAPI-766).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* the destructive tool annotations, the MCP text-result
|
|
7
|
-
* set of safe pre-transport
|
|
8
|
-
* single path-segment encoder.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* All nine write tools — the two custom-object-definition tools, the
|
|
5
|
+
* site-preference value tool, and the six system-object tools — need the same
|
|
6
|
+
* small set of primitives: the destructive tool annotations, the MCP text-result
|
|
7
|
+
* shape, a consistent set of safe pre-transport failure envelopes (validation /
|
|
8
|
+
* payload fault / unexpected), and a single path-segment encoder.
|
|
9
|
+
*
|
|
10
|
+
* Every failure helper here returns the *universal* nested failure envelope
|
|
11
|
+
* built by `sfcc-result.ts` — `{ error: { code, status, message, source,
|
|
12
|
+
* details } }` with `isError: true`. There is no write-local envelope
|
|
13
|
+
* construction and no second serialization path, so a write tool's own
|
|
14
|
+
* pre-transport failure is indistinguishable in shape from a gate rejection, an
|
|
15
|
+
* OCAPI read fault, or a WebDAV log failure. That is what makes
|
|
16
|
+
* `result.isError === true` a total predicate across the SFCC surface.
|
|
17
|
+
*
|
|
18
|
+
* Nothing here ever serializes a stack trace, a raw thrown value, or a complete
|
|
19
|
+
* Zod issue object: validation issues are projected down to `{ path, message }`.
|
|
11
20
|
*/
|
|
12
21
|
import { z } from "zod";
|
|
22
|
+
import { formatSfccWriteFailure } from "./sfcc-result.js";
|
|
23
|
+
import { SfccWritePayloadFault } from "./writes-system-object-payloads.js";
|
|
13
24
|
// ---------------------------------------------------------------------------
|
|
14
25
|
// Annotations
|
|
15
26
|
// ---------------------------------------------------------------------------
|
|
@@ -24,35 +35,86 @@ export const WRITE_ANNOTATIONS = {
|
|
|
24
35
|
idempotentHint: false,
|
|
25
36
|
openWorldHint: true,
|
|
26
37
|
};
|
|
27
|
-
/** Wrap text in the standard MCP text-result shape. */
|
|
38
|
+
/** Wrap text in the standard MCP text-result shape (success path only). */
|
|
28
39
|
export function textResult(text) {
|
|
29
40
|
return { content: [{ type: "text", text }] };
|
|
30
41
|
}
|
|
31
42
|
// ---------------------------------------------------------------------------
|
|
32
|
-
//
|
|
43
|
+
// Fixed messages
|
|
33
44
|
// ---------------------------------------------------------------------------
|
|
34
45
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
|
|
46
|
+
* The one message returned when write arguments fail schema validation. The
|
|
47
|
+
* offending fields are reported under `error.details.issues`, never inline.
|
|
48
|
+
*/
|
|
49
|
+
export const SFCC_WRITE_VALIDATION_ERROR_MESSAGE = "Input failed schema validation before any OCAPI call. " +
|
|
50
|
+
"See error.details.issues for the offending fields.";
|
|
51
|
+
/**
|
|
52
|
+
* The one message returned for an unexpected throw inside a write handler.
|
|
53
|
+
* Bridge-authored and fixed: the thrown value is never serialized, so a
|
|
54
|
+
* credential, token, request header, or stack frame carried by the exception
|
|
55
|
+
* cannot reach the caller.
|
|
38
56
|
*/
|
|
39
|
-
export
|
|
40
|
-
|
|
41
|
-
|
|
57
|
+
export const SFCC_WRITE_INTERNAL_ERROR_MESSAGE = "An unexpected internal error occurred while handling this SFCC write. " +
|
|
58
|
+
"The underlying failure detail is withheld from this response to avoid " +
|
|
59
|
+
"leaking credentials or internal state.";
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Pre-transport failure envelopes
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
/** Reduce Zod issues to field path + message; nothing else is disclosed. */
|
|
64
|
+
function safeZodIssues(err) {
|
|
65
|
+
const issues = Array.isArray(err.issues) ? err.issues : [];
|
|
66
|
+
return issues.map((issue) => ({
|
|
67
|
+
path: Array.isArray(issue.path) ? issue.path.map(String).join(".") : "",
|
|
68
|
+
message: typeof issue.message === "string" ? issue.message : "Invalid input.",
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Convert a Zod validation failure into the unified `VALIDATION_ERROR` envelope.
|
|
73
|
+
*
|
|
74
|
+
* Private: it has no caller outside this module — `preTransportErrorEnvelope`
|
|
75
|
+
* is the dispatcher every write handler actually uses. Issues are projected to
|
|
76
|
+
* the safe `{ path, message }` shape so no raw Zod issue object (with its
|
|
77
|
+
* `expected` / `received` / `unionErrors` internals) reaches a tool result.
|
|
78
|
+
*/
|
|
79
|
+
function zodValidationEnvelope(err) {
|
|
80
|
+
return formatSfccWriteFailure({
|
|
81
|
+
code: "VALIDATION_ERROR",
|
|
42
82
|
status: 400,
|
|
43
|
-
message:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
83
|
+
message: SFCC_WRITE_VALIDATION_ERROR_MESSAGE,
|
|
84
|
+
source: "tool",
|
|
85
|
+
details: { issues: safeZodIssues(err) },
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* A local, pre-transport payload-shape failure surfaced in OCAPI-fault
|
|
90
|
+
* vocabulary (e.g. `IdConflictException`), without a network round trip.
|
|
91
|
+
*
|
|
92
|
+
* Private for the same reason as the Zod variant: handlers reach it through the
|
|
93
|
+
* dispatcher. `source` is `"tool"` — the fault was decided locally, so labelling
|
|
94
|
+
* it `"ocapi"` would claim OCAPI said something it never did.
|
|
95
|
+
*/
|
|
96
|
+
function payloadFaultEnvelope(fault) {
|
|
97
|
+
return formatSfccWriteFailure({
|
|
98
|
+
code: "OCAPI_WRITE_ERROR",
|
|
99
|
+
status: fault.status,
|
|
100
|
+
message: fault.message,
|
|
101
|
+
source: "tool",
|
|
102
|
+
details: { fault: { type: fault.faultType, message: fault.message } },
|
|
103
|
+
});
|
|
49
104
|
}
|
|
50
105
|
/**
|
|
51
106
|
* A plain (non-Zod) validation rejection, e.g. a body/URL field mismatch that
|
|
52
|
-
* the schema alone cannot express.
|
|
107
|
+
* the schema alone cannot express. The caller's message is preserved verbatim;
|
|
108
|
+
* only the envelope around it changed.
|
|
53
109
|
*/
|
|
54
110
|
export function validationEnvelope(message) {
|
|
55
|
-
return
|
|
111
|
+
return formatSfccWriteFailure({
|
|
112
|
+
code: "VALIDATION_ERROR",
|
|
113
|
+
status: 400,
|
|
114
|
+
message,
|
|
115
|
+
source: "tool",
|
|
116
|
+
details: {},
|
|
117
|
+
});
|
|
56
118
|
}
|
|
57
119
|
/**
|
|
58
120
|
* Safe fallback envelope for an unexpected thrown error inside a write handler
|
|
@@ -60,20 +122,52 @@ export function validationEnvelope(message) {
|
|
|
60
122
|
* thrown value.
|
|
61
123
|
*/
|
|
62
124
|
export function unexpectedEnvelope() {
|
|
63
|
-
return
|
|
64
|
-
|
|
125
|
+
return formatSfccWriteFailure({
|
|
126
|
+
code: "INTERNAL_ERROR",
|
|
65
127
|
status: 500,
|
|
66
|
-
message:
|
|
67
|
-
|
|
128
|
+
message: SFCC_WRITE_INTERNAL_ERROR_MESSAGE,
|
|
129
|
+
source: "tool",
|
|
130
|
+
details: {},
|
|
131
|
+
});
|
|
68
132
|
}
|
|
69
133
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
134
|
+
* Recognize a Zod/schema failure without matching on the exception message.
|
|
135
|
+
*
|
|
136
|
+
* `instanceof` covers the normal case. The structural fallback covers a
|
|
137
|
+
* ZodError produced by a second copy of Zod (a duplicated dependency or a
|
|
138
|
+
* module-mocked import), where the prototype chain differs but the contract
|
|
139
|
+
* — a `ZodError` name plus an `issues` array — does not.
|
|
73
140
|
*/
|
|
74
|
-
|
|
141
|
+
function isSchemaFailure(err) {
|
|
75
142
|
if (err instanceof z.ZodError)
|
|
143
|
+
return true;
|
|
144
|
+
return (typeof err === "object" &&
|
|
145
|
+
err !== null &&
|
|
146
|
+
err.name === "ZodError" &&
|
|
147
|
+
Array.isArray(err.issues));
|
|
148
|
+
}
|
|
149
|
+
/** Recognize a local payload-shape fault, tolerating a duplicated module instance. */
|
|
150
|
+
function isPayloadFault(err) {
|
|
151
|
+
if (err instanceof SfccWritePayloadFault)
|
|
152
|
+
return true;
|
|
153
|
+
return (typeof err === "object" &&
|
|
154
|
+
err !== null &&
|
|
155
|
+
err.name === "SfccWritePayloadFault" &&
|
|
156
|
+
typeof err.faultType === "string" &&
|
|
157
|
+
typeof err.status === "number");
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Map a pre-transport error to its unified envelope: Zod validation failures
|
|
161
|
+
* become `VALIDATION_ERROR`, a recognized payload-shape fault becomes
|
|
162
|
+
* `OCAPI_WRITE_ERROR`, and everything else becomes the sanitized
|
|
163
|
+
* `INTERNAL_ERROR`. Used at the `.parse()` / payload-build boundary before any
|
|
164
|
+
* OCAPI mutation.
|
|
165
|
+
*/
|
|
166
|
+
export function preTransportErrorEnvelope(err) {
|
|
167
|
+
if (isSchemaFailure(err))
|
|
76
168
|
return zodValidationEnvelope(err);
|
|
169
|
+
if (isPayloadFault(err))
|
|
170
|
+
return payloadFaultEnvelope(err);
|
|
77
171
|
return unexpectedEnvelope();
|
|
78
172
|
}
|
|
79
173
|
// ---------------------------------------------------------------------------
|