@bridge_gpt/mcp-server 0.2.34 → 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.
Files changed (109) hide show
  1. package/README.md +490 -366
  2. package/build/agent-capabilities/probe-context.js +8 -1
  3. package/build/agent-capabilities/probes.js +7 -1
  4. package/build/agents.generated.js +1 -1
  5. package/build/base-url.js +79 -0
  6. package/build/bridge-api-urls.js +9 -0
  7. package/build/chain-orchestrator.js +93 -15
  8. package/build/claude-review-workflow.js +264 -0
  9. package/build/claude-user-config-doctor.js +317 -0
  10. package/build/cli-release.js +53 -0
  11. package/build/commands.generated.js +6 -5
  12. package/build/conductor/bridge-api-client.js +393 -4
  13. package/build/conductor/deny-enforcement-preflight.js +1 -0
  14. package/build/conductor/done-gate.js +44 -5
  15. package/build/conductor/epic-reconcile.js +6 -0
  16. package/build/conductor/install-doctor.js +462 -0
  17. package/build/conductor-bin.js +3 -3
  18. package/build/conductor-bundle-artifacts.js +37 -15
  19. package/build/credential-store.js +205 -4
  20. package/build/direct-ticket-tools.js +70 -0
  21. package/build/doctor.js +473 -81
  22. package/build/executor/cli.js +83 -6
  23. package/build/executor/credentials.js +39 -11
  24. package/build/executor/deps.js +32 -1
  25. package/build/executor/env.js +71 -28
  26. package/build/executor/heartbeat.js +138 -17
  27. package/build/executor/http-client.js +49 -8
  28. package/build/executor/index.js +4 -0
  29. package/build/executor/job-errors.js +4 -0
  30. package/build/executor/job-runner.js +530 -20
  31. package/build/executor/observation.js +130 -0
  32. package/build/executor/permissions.js +114 -8
  33. package/build/executor/preflight.js +127 -8
  34. package/build/executor/prompt-spec.js +51 -0
  35. package/build/executor/runner.js +23 -2
  36. package/build/executor/service-unit.js +876 -0
  37. package/build/executor/test-clock.js +75 -3
  38. package/build/executor/types.js +4 -18
  39. package/build/executor/worker-command.js +73 -12
  40. package/build/executor/worker-config-isolation.js +287 -0
  41. package/build/executor/worker-finalization.js +68 -14
  42. package/build/executor/worktree.js +46 -4
  43. package/build/index.js +1039 -237
  44. package/build/init.js +503 -111
  45. package/build/install-bridge-conductor.js +491 -0
  46. package/build/install-bridge.js +1188 -247
  47. package/build/install-reexec.js +233 -0
  48. package/build/launcher-config-inspection.js +351 -0
  49. package/build/mcp-host-config.js +11 -1
  50. package/build/mcp-install-state.js +32 -0
  51. package/build/mcp-invoke.js +49 -6
  52. package/build/mcp-provisioning.js +52 -13
  53. package/build/mcp-registration-doctor.js +14 -5
  54. package/build/notifications.js +553 -0
  55. package/build/pipeline-orchestrator.js +146 -4
  56. package/build/pipeline-utils.js +3 -0
  57. package/build/pipelines.generated.js +32 -13
  58. package/build/plan-execution-ledger.js +550 -0
  59. package/build/plan-phase-routing.js +272 -0
  60. package/build/plane/alembic-head.js +110 -0
  61. package/build/plane/build-freshness.js +167 -0
  62. package/build/plane/cli.js +480 -0
  63. package/build/plane/defaults.js +266 -0
  64. package/build/plane/manifest.js +377 -0
  65. package/build/plane/member-logs.js +147 -0
  66. package/build/plane/member-roster.js +147 -0
  67. package/build/plane/preflight.js +289 -0
  68. package/build/plane/shutdown.js +195 -0
  69. package/build/plane/status.js +125 -0
  70. package/build/plane/supervisor.js +569 -0
  71. package/build/plane/test-fakes.js +156 -0
  72. package/build/plane/types.js +75 -0
  73. package/build/readme.generated.js +1 -1
  74. package/build/run-unit-tests-launcher.js +259 -0
  75. package/build/setup-epic.js +772 -28
  76. package/build/sfcc/log-gate.js +38 -11
  77. package/build/sfcc/log-query.js +55 -15
  78. package/build/sfcc/ocapi-shape.js +51 -14
  79. package/build/sfcc/output.js +41 -11
  80. package/build/sfcc/permissions.js +24 -2
  81. package/build/sfcc/read-projection.js +181 -0
  82. package/build/sfcc/read-result.js +158 -0
  83. package/build/sfcc/reads-custom-object-def.js +29 -18
  84. package/build/sfcc/reads-site-preference.js +75 -29
  85. package/build/sfcc/reads-system-object.js +40 -34
  86. package/build/sfcc/sfcc-result.js +106 -0
  87. package/build/sfcc/tool-wrapper.js +56 -13
  88. package/build/sfcc/write-grants.js +45 -22
  89. package/build/sfcc/write-guard.js +21 -13
  90. package/build/sfcc/write-result.js +61 -14
  91. package/build/sfcc/write-tool-common.js +126 -32
  92. package/build/sfcc/writes-system-object.js +11 -50
  93. package/build/start-tickets-prereqs.js +129 -0
  94. package/build/start-tickets.js +17 -13
  95. package/build/ticket-backend-metadata.js +59 -0
  96. package/build/ticket-key-utils.js +92 -0
  97. package/build/tool-error-envelope.js +71 -0
  98. package/build/tool-surface-gating.js +72 -0
  99. package/build/update-status.js +102 -0
  100. package/build/upgrade-advice.js +47 -0
  101. package/build/upgrade-cli.js +412 -110
  102. package/build/version.generated.js +1 -1
  103. package/build/worktree-core.js +73 -0
  104. package/docs/CONDUCTOR.md +132 -6
  105. package/docs/install/mcp-tool-integrations.md +29 -21
  106. package/package.json +9 -6
  107. package/pipelines/implement-ticket.json +19 -4
  108. package/build/conductor/supervisor-judgment-python.js +0 -141
  109. package/build/conductor/supervisor-judgment.js +0 -215
@@ -0,0 +1,106 @@
1
+ /**
2
+ * The universal SFCC failure contract (BAPI-766).
3
+ *
4
+ * BAPI-759 unified the SFCC *read* surface behind one formatter. This module
5
+ * generalizes that formatter so the same envelope covers every SFCC tool: reads,
6
+ * all nine writes, the OCAPI call-time gate, and the separate WebDAV log surface.
7
+ * There is exactly one normalization and one serialization implementation here —
8
+ * two parallel formatters would let the read and write sides drift, which is the
9
+ * failure this module exists to prevent.
10
+ *
11
+ * Two things are unified:
12
+ *
13
+ * 1. **The protocol predicate.** Every formatted failure sets `isError: true`,
14
+ * so a caller can detect "this call did not succeed" without parsing text.
15
+ * `result.isError === true` is therefore a *total* predicate for failure
16
+ * across the SFCC surface. `sfcc_setup_status` is the one deliberate
17
+ * exemption: reporting a not-configured repository is its *successful*
18
+ * output, and it sits outside every gate.
19
+ *
20
+ * 2. **The body shape.** Exactly one text content item, whose text parses as
21
+ * `{ error: { code, status, message, source, details } }`. Those four
22
+ * fields mean the same thing for an OCAPI fault, a tool-level guardrail, a
23
+ * gate rejection, a WebDAV failure, and an unexpected internal failure — one
24
+ * parse path reads all of them with no shape discrimination.
25
+ *
26
+ * Everything failure-specific lives under `error.details`, which is always
27
+ * present. An object-valued upstream body is preserved there verbatim, so
28
+ * upstream data is never discarded and never collides with the Bridge-owned
29
+ * envelope fields.
30
+ *
31
+ * Successful results are NOT passed through this formatter. `SfccToolResult`
32
+ * declares `isError` optional precisely so a content-only success (every
33
+ * successful read, every successful write) stays assignable without gaining the
34
+ * flag.
35
+ */
36
+ // ---------------------------------------------------------------------------
37
+ // Detail normalization
38
+ // ---------------------------------------------------------------------------
39
+ /** True for a plain (non-array, non-null) object whose fields can be preserved in place. */
40
+ function isPlainObject(value) {
41
+ return typeof value === "object" && value !== null && !Array.isArray(value);
42
+ }
43
+ /**
44
+ * Coerce arbitrary detail input into an object so `error.details` is always
45
+ * present and always the same kind of thing.
46
+ *
47
+ * A plain object is preserved verbatim (shallow copy) — this is what keeps an
48
+ * OCAPI fault body's `_v` / `fault.*` fields at their original nesting. Anything
49
+ * else (absent, null, a primitive, an array) is wrapped as `{ value }` rather
50
+ * than being spread, which would scatter an array across numeric keys or
51
+ * fabricate structure that upstream never sent.
52
+ */
53
+ function normalizeDetails(details) {
54
+ if (details === undefined)
55
+ return {};
56
+ if (isPlainObject(details))
57
+ return { ...details };
58
+ return { value: details };
59
+ }
60
+ // ---------------------------------------------------------------------------
61
+ // Formatter
62
+ // ---------------------------------------------------------------------------
63
+ /**
64
+ * Format any SFCC failure into the unified protocol result.
65
+ *
66
+ * Returns exactly one text content item and sets `isError: true`. The text
67
+ * serializes exactly one top-level `error` object. This is the single
68
+ * serialization point for the whole SFCC surface.
69
+ */
70
+ export function formatSfccFailure(input) {
71
+ const details = normalizeDetails(input.details);
72
+ // Only surface the upstream status when it actually differs from the status
73
+ // we are reporting. Equal statuses would be noise; a synthetic status has no
74
+ // upstream to report at all.
75
+ if (input.upstreamHttpStatus !== undefined &&
76
+ input.upstreamHttpStatus !== input.status) {
77
+ details.upstream_http_status = input.upstreamHttpStatus;
78
+ }
79
+ const envelope = {
80
+ error: {
81
+ code: input.code,
82
+ status: input.status,
83
+ message: input.message,
84
+ source: input.source,
85
+ details,
86
+ },
87
+ };
88
+ return {
89
+ isError: true,
90
+ content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }],
91
+ };
92
+ }
93
+ /**
94
+ * Read-side name for the shared formatter, kept so BAPI-759's read call sites
95
+ * read naturally. It is a thin delegate, not a second implementation.
96
+ */
97
+ export function formatSfccReadFailure(input) {
98
+ return formatSfccFailure(input);
99
+ }
100
+ /**
101
+ * Write-side name for the shared formatter. Same contract, same implementation —
102
+ * the discriminator between the two sides is `source`, not the function called.
103
+ */
104
+ export function formatSfccWriteFailure(input) {
105
+ return formatSfccFailure(input);
106
+ }
@@ -12,23 +12,55 @@
12
12
  */
13
13
  import { SFCC_VERSIONS, getSfccVersionConfig } from "./config.js";
14
14
  import { resolveSfccCredentials } from "./credentials.js";
15
+ import { formatSfccFailure } from "./sfcc-result.js";
15
16
  // ---------------------------------------------------------------------------
16
17
  // Not-configured envelope helpers
17
18
  // ---------------------------------------------------------------------------
19
+ /**
20
+ * Appended to every gate diagnostic (BAPI-759).
21
+ *
22
+ * A hard protocol failure in a repository that simply is not an SFCC project
23
+ * must not send a calling agent into a retry or self-repair loop, so the
24
+ * message says so outright. It is appended to — never a replacement for — each
25
+ * failure class's own distinct diagnostic.
26
+ */
27
+ const NO_RETRY_INSTRUCTION = "This repository is not configured for SFCC; do not retry SFCC tools.";
28
+ /**
29
+ * Build the unified gate failure result.
30
+ *
31
+ * The gate's 503 is synthetic — it is Bridge's own status, not something OCAPI
32
+ * returned — so no upstream HTTP status is supplied and `details` carries only
33
+ * the preserved `failure_class`.
34
+ *
35
+ * This runs for READ AND WRITE tools alike. `withSfccGate` is shared
36
+ * infrastructure — the nine write handlers in `writes-system-object.ts`,
37
+ * `writes-custom-object-def.ts`, and `writes-site-preference.ts` all wrap
38
+ * themselves in it — so a gate rejection on a write tool returns this envelope
39
+ * with `isError: true` too. That is intended, not incidental: a write tool that
40
+ * never reached OCAPI produced no write, so the protocol failure is accurate.
41
+ * The flag is set unconditionally, with no per-tool branch, tool-name check, or
42
+ * read/write mode parameter, precisely so no caller has to know which side of
43
+ * the surface it is on.
44
+ *
45
+ * BAPI-766 closed the split BAPI-759 left behind: a write tool's OWN
46
+ * pre-transport failures (Zod, sandbox guard, id mismatch, unexpected throw) and
47
+ * its post-transport OCAPI failures now use this same envelope, built through
48
+ * the shared formatter in `sfcc-result.ts`. `write-gate-rejection.test.ts`
49
+ * drives the real gate through the real write handlers and asserts that the two
50
+ * origins now produce the same shape, so neither can drift silently.
51
+ *
52
+ * `sfcc_log_query` keeps its own separate gate (`withSfccLogGate`) because it
53
+ * authenticates over WebDAV Basic auth rather than the OCAPI AM-OAuth boundary.
54
+ * Only the failure representation is shared — those two gates are not merged.
55
+ */
18
56
  function notConfigured(failureClass, message) {
19
- return {
20
- content: [
21
- {
22
- type: "text",
23
- text: JSON.stringify({
24
- error: "NOT_CONFIGURED",
25
- status: 503,
26
- failure_class: failureClass,
27
- message,
28
- }),
29
- },
30
- ],
31
- };
57
+ return formatSfccFailure({
58
+ code: "NOT_CONFIGURED",
59
+ status: 503,
60
+ message: `${message} ${NO_RETRY_INSTRUCTION}`,
61
+ source: "gate",
62
+ details: { failure_class: failureClass },
63
+ });
32
64
  }
33
65
  // ---------------------------------------------------------------------------
34
66
  // Gate
@@ -41,6 +73,17 @@ function notConfigured(failureClass, message) {
41
73
  * 2. Validates the value is an SFCC version.
42
74
  * 3. Resolves dw.json credentials.
43
75
  * 4. Calls the inner handler only if all checks pass.
76
+ *
77
+ * Every early rejection is the unified `isError: true` result — unconditionally,
78
+ * with no tool-name check, per-tool branch, exemption flag, or gate-mode
79
+ * parameter. `sfcc_setup_status`, the one SFCC tool whose successful output *is*
80
+ * a not-configured report, is exempt structurally: `register.ts` registers it
81
+ * outside this gate, so no code here needs to know about it.
82
+ *
83
+ * On the success path the inner handler's result is returned directly — not
84
+ * cloned, wrapped, or re-formatted — so write handlers keep ownership of their
85
+ * own result bodies. The generic `R` preserves each caller's concrete result
86
+ * type through the gate.
44
87
  */
45
88
  export function withSfccGate(deps, handler) {
46
89
  return async (args) => {
@@ -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 future write handlers on a 403 and printed by the
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
- * Build the human-facing 403 remediation text for a failed write operation.
57
- * Includes the 403 status, the failed operation/path, and the exact write-grant
58
- * JSON operators must paste in Business Manager.
59
- */
60
- export function buildOcapiWriteGrant403Text(params) {
61
- const { operation, path, ocapiVersion, body } = params;
62
- const bodyLine = body === undefined
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
- * MCP-shaped wrapper around `buildOcapiWriteGrant403Text` so future write
74
- * handlers can return the grant UX without re-implementing content wrapping.
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
- return {
78
- content: [{ type: "text", text: buildOcapiWriteGrant403Text(params) }],
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 a
26
- * structured JSON validation error naming the rejected instance; a sandbox
27
- * returns `null` so the caller proceeds.
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 textResult(JSON.stringify({
34
- error: "VALIDATION_ERROR",
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 future SFCC write tools. Handlers
5
- * pass the raw `OcapiGetResult` from `ocapiPut`/`ocapiPatch` here rather than
6
- * inventing their own 403/fault/success response logic, guaranteeing a
7
- * consistent surface across every write tool.
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
- /** Wrap text in the standard MCP text-result shape. */
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 remediation (via write-grants.ts).
22
- * - success JSON with `status`, `outcome`, and `body`.
23
- * - other failure → JSON with `error: "OCAPI_WRITE_ERROR"`, `status`, `fault`, `body`.
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 textResult(JSON.stringify({
42
- error: "OCAPI_WRITE_ERROR",
82
+ return formatSfccWriteFailure({
83
+ code: "OCAPI_WRITE_ERROR",
43
84
  status: result.status,
44
- fault: result.fault,
45
- body: result.body,
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
- * The two write-surface modules (`writes-custom-object-def.ts` and
5
- * `writes-site-preference.ts`) both need the same small set of primitives:
6
- * the destructive tool annotations, the MCP text-result shape, a consistent
7
- * set of safe pre-transport error envelopes (validation / unexpected), and a
8
- * single path-segment encoder. Centralizing them here guarantees every write
9
- * tool returns an identical envelope shape and never leaks a stack trace or a
10
- * raw thrown value to the caller.
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
- // Pre-transport error envelopes
43
+ // Fixed messages
33
44
  // ---------------------------------------------------------------------------
34
45
  /**
35
- * Convert a Zod validation failure into a safe `VALIDATION_ERROR` envelope.
36
- * The flattened issues are surfaced so callers can see which field failed,
37
- * without exposing any thrown-error internals.
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 function zodValidationEnvelope(err) {
40
- return textResult(JSON.stringify({
41
- error: "VALIDATION_ERROR",
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: "Input failed schema validation before any OCAPI call.",
44
- issues: err.issues.map((issue) => ({
45
- path: issue.path.join("."),
46
- message: issue.message,
47
- })),
48
- }, null, 2));
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. Same envelope shape as the Zod variant.
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 textResult(JSON.stringify({ error: "VALIDATION_ERROR", status: 400, message }, null, 2));
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 textResult(JSON.stringify({
64
- error: "INTERNAL_ERROR",
125
+ return formatSfccWriteFailure({
126
+ code: "INTERNAL_ERROR",
65
127
  status: 500,
66
- message: "Unexpected SFCC write tool failure.",
67
- }, null, 2));
128
+ message: SFCC_WRITE_INTERNAL_ERROR_MESSAGE,
129
+ source: "tool",
130
+ details: {},
131
+ });
68
132
  }
69
133
  /**
70
- * Map a pre-transport error to its envelope: Zod validation errors become a
71
- * `VALIDATION_ERROR`, everything else becomes the generic `INTERNAL_ERROR`.
72
- * Used for the `.parse()` boundary before any OCAPI mutation.
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
- export function preTransportErrorEnvelope(err) {
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
  // ---------------------------------------------------------------------------