@bridge_gpt/mcp-server 0.2.66 → 0.2.68
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 +421 -83
- package/build/agent-capabilities/probes.js +15 -5
- package/build/agent-launchers/claude-executor-adapter.js +70 -16
- package/build/agent-launchers/executor-adapter.js +7 -4
- package/build/agents.generated.js +22 -2
- package/build/bitbucket-workspace-remote.js +136 -0
- package/build/bootstrap-project-identity.js +24 -0
- package/build/bounded-wait.js +67 -10
- package/build/bridge-permissions.js +829 -0
- package/build/claude-settings.js +18 -0
- package/build/cli-help.js +21 -0
- package/build/command-assets-doctor.js +24 -70
- package/build/command-provisioning.js +29 -5
- package/build/commands.generated.js +12 -12
- package/build/conductor/bridge-api-client.js +857 -1
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/command-drift-production.js +92 -0
- package/build/conductor/command-drift.js +89 -0
- package/build/conductor/doctor.js +72 -29
- package/build/conductor/epic-branch-ci-inspection.js +1375 -0
- package/build/conductor/epic-implementer-cli.js +98 -28
- package/build/conductor/errors.js +4 -3
- package/build/conductor/gate-discovery.js +890 -0
- package/build/conductor/guided-repair.js +6 -2
- package/build/conductor/hook-runtime-qualification.js +1 -1
- package/build/conductor/install-doctor.js +15 -3
- package/build/conductor/json-output.js +63 -0
- package/build/conductor/merge-execution-contract.js +27 -8
- package/build/conductor/merge-execution-eligibility.js +3 -2
- package/build/conductor/pr-ci-producer.js +1 -1
- package/build/conductor/readiness-cli.js +113 -12
- package/build/conductor/readiness-sections.js +410 -9
- package/build/conductor/readiness.js +388 -33
- package/build/conductor/recovery-cli.js +310 -12
- package/build/conductor/recovery-operations.js +167 -3
- package/build/conductor/repair-contract.js +54 -3
- package/build/conductor/repository-workflow-remedy.js +125 -0
- package/build/conductor/tools.js +2 -2
- package/build/conductor-bundle-artifacts.js +8 -0
- package/build/connect-bitbucket-api.js +41 -6
- package/build/connect-bitbucket.js +106 -33
- package/build/credential-store.js +207 -47
- package/build/docs.generated.js +62 -3
- package/build/doctor.js +92 -1
- package/build/drive-epic-key.js +6 -5
- package/build/drive-epic.js +129 -61
- package/build/epic-coherence-input.js +2 -1
- package/build/epic-coherence-input.test-fixtures.js +4 -3
- package/build/epic-implementer-bin.js +1379 -28
- package/build/epic-implementer-bundle-cli.js +2 -2
- package/build/epic-workarounds-path.js +39 -0
- package/build/executor/cli.js +33 -2
- package/build/executor/completion-contract.js +66 -10
- package/build/executor/completion-supervision.js +4 -0
- package/build/executor/deps.js +4 -1
- package/build/executor/env.js +19 -0
- package/build/executor/gate-qualification-job.js +499 -0
- package/build/executor/guarded-fast-forward-push.js +129 -0
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/http-client.js +20 -0
- package/build/executor/integration-job.js +122 -8
- package/build/executor/job-errors.js +33 -1
- package/build/executor/job-runner.js +813 -87
- package/build/executor/likely-tests.js +290 -0
- package/build/executor/live-worker-registry.js +1 -1
- package/build/executor/observation.js +37 -3
- package/build/executor/packaged-recipe.js +106 -0
- package/build/executor/permissions.js +115 -28
- package/build/executor/platform.js +1 -1
- package/build/executor/preflight.js +112 -26
- package/build/executor/process-containment.js +36 -12
- package/build/executor/process.js +15 -7
- package/build/executor/prompt-spec.js +33 -1
- package/build/executor/recently-failed-tests-file.js +71 -0
- package/build/executor/reconcile-overlap-job.js +41 -23
- package/build/executor/required-verification.js +1 -1
- package/build/executor/required-work-ledger.js +111 -18
- package/build/executor/resume-pre-spawn.js +2 -0
- package/build/executor/retirement.js +69 -0
- package/build/executor/runner.js +122 -13
- package/build/executor/service-unit.js +7 -1
- package/build/executor/test-clock.js +15 -0
- package/build/executor/types.js +32 -0
- package/build/executor/viewer-tabs.js +58 -8
- package/build/executor/worker-command.js +23 -7
- package/build/executor/worker-diagnostics.js +37 -0
- package/build/executor/worker-finalization.js +113 -34
- package/build/executor/worker-guard-hook.js +316 -58
- package/build/executor/worker-log.js +4 -3
- package/build/executor/worker-verification-guidance.js +37 -0
- package/build/executor/worktree.js +166 -8
- package/build/first-build-advisory.js +97 -0
- package/build/generated-assets-advisory.js +14 -0
- package/build/generated-assets-catalog.js +66 -0
- package/build/generated-assets-context.js +46 -0
- package/build/generated-assets-dependencies.js +26 -0
- package/build/generated-assets-doctor.js +50 -0
- package/build/generated-assets-health.js +65 -0
- package/build/generated-assets-host.js +17 -0
- package/build/generated-assets-inventory.js +17 -0
- package/build/generated-assets-paths.js +60 -0
- package/build/generated-assets-publisher.js +133 -0
- package/build/generated-assets-remedy.js +25 -0
- package/build/generated-assets-repair.js +72 -0
- package/build/generated-assets-session.js +71 -0
- package/build/generated-assets-test-support.js +35 -0
- package/build/index.js +1224 -1054
- package/build/init.js +54 -177
- package/build/install-bridge-conductor-gate.js +1372 -0
- package/build/install-bridge-conductor.js +296 -41
- package/build/install-bridge.js +547 -40
- package/build/mcp-install-state.js +50 -4
- package/build/merge-workflow-install.js +3 -4
- package/build/phase-context-git-probe.js +68 -0
- package/build/phase-result-artifacts.js +173 -5
- package/build/pipeline-utils.js +6 -2
- package/build/pipelines.generated.js +34 -160
- package/build/plan-execution-ledger.js +2 -32
- package/build/plane/alembic-head.js +12 -3
- package/build/plane/base-worktree-guard.js +147 -0
- package/build/plane/cli.js +217 -17
- package/build/plane/code-snapshot.js +468 -0
- package/build/plane/defaults.js +48 -3
- package/build/plane/endpoint.js +41 -0
- package/build/plane/manifest.js +136 -2
- package/build/plane/member-roster.js +94 -19
- package/build/plane/preflight.js +83 -2
- package/build/plane/process-cwd.js +67 -0
- package/build/plane/restart.js +542 -0
- package/build/plane/shutdown.js +31 -0
- package/build/plane/status.js +143 -8
- package/build/plane/supervisor.js +586 -179
- package/build/plane/types.js +54 -2
- package/build/printed-remedy-commands.js +52 -0
- package/build/readiness-check.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/repair-assets.js +93 -0
- package/build/review-generation.js +22 -0
- package/build/run-unit-tests-launcher.js +130 -49
- package/build/secret-redaction.js +33 -0
- package/build/setup-epic.js +152 -22
- package/build/sfcc/config.js +9 -0
- package/build/sfcc/credentials.js +100 -38
- package/build/sfcc/hostname.js +67 -0
- package/build/sfcc/permissions.js +17 -13
- package/build/sfcc/register.js +1 -1
- package/build/sfcc/tool-wrapper.js +39 -43
- package/build/sfcc/write-grants.js +17 -11
- package/build/sfcc/write-result.js +4 -3
- package/build/sfcc/writes-site-preference.js +20 -8
- package/build/start-tickets-prereqs.js +70 -16
- package/build/start-tickets.js +208 -36
- package/build/tool-error-envelope.js +61 -0
- package/build/tool-surface-gating.js +36 -0
- package/build/upgrade-cli.js +55 -1
- package/build/version.generated.js +3 -3
- package/build/visual-diff.js +22 -1
- package/build/worker-guard-hook-bin.js +4 -6
- package/build/worker-guard-hook-main.js +47 -0
- package/build/worktree-core.js +145 -6
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +867 -0
- package/docs/EPIC-IMPLEMENTER.md +728 -0
- package/docs/install/mcp-tool-integrations.md +24 -15
- package/docs/install/sfcc-integration.md +12 -4
- package/package.json +3 -3
- package/pipelines/greenfield-setup.json +4 -128
- package/pipelines/implement-ticket.json +3 -2
- package/pipelines/plan-epic.json +0 -5
- package/pipelines/review-ticket.json +1 -1
- package/docs/CONDUCTOR.md +0 -605
- package/pipelines/idea-to-pr.json +0 -49
|
@@ -8,14 +8,15 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { ocapiGet } from "./client.js";
|
|
10
10
|
import { normalizeOcapiPage } from "./ocapi-shape.js";
|
|
11
|
-
import { DEFAULT_OCAPI_VERSION } from "./config.js";
|
|
11
|
+
import { DEFAULT_OCAPI_VERSION, OCAPI_SETTINGS_STRUCTURE_VERSION } from "./config.js";
|
|
12
|
+
import { formatSafeHost } from "./hostname.js";
|
|
12
13
|
import { formatOcapiWriteGrantJson } from "./write-grants.js";
|
|
13
14
|
// ---------------------------------------------------------------------------
|
|
14
15
|
// OCAPI Settings JSON template
|
|
15
16
|
// Operators paste this in: Administration > Site Development > Open Commerce API Settings → Data API tab.
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
17
|
-
const OCAPI_SETTINGS_READ_SEARCH = (
|
|
18
|
-
_v:
|
|
18
|
+
const OCAPI_SETTINGS_READ_SEARCH = () => JSON.stringify({
|
|
19
|
+
_v: OCAPI_SETTINGS_STRUCTURE_VERSION,
|
|
19
20
|
clients: [
|
|
20
21
|
{
|
|
21
22
|
client_id: "<YOUR_CLIENT_ID>",
|
|
@@ -52,8 +53,9 @@ const OCAPI_SETTINGS_READ_SEARCH = (ocapiVersion) => JSON.stringify({
|
|
|
52
53
|
// JSON printed here is byte-identical to what future write handlers echo on a
|
|
53
54
|
// 403. This now covers all required write resource families:
|
|
54
55
|
// /system_object_definitions, /system_object_definitions/**,
|
|
55
|
-
// /custom_object_definitions/**, and /site_preferences
|
|
56
|
-
|
|
56
|
+
// /custom_object_definitions/**, and the site-scoped /sites/*/site_preferences/**
|
|
57
|
+
// (the org-level /site_preferences/** above stays read/search only).
|
|
58
|
+
const OCAPI_SETTINGS_WRITE_IMPORT = () => formatOcapiWriteGrantJson();
|
|
57
59
|
// ---------------------------------------------------------------------------
|
|
58
60
|
// Tool implementation
|
|
59
61
|
// ---------------------------------------------------------------------------
|
|
@@ -75,7 +77,7 @@ export async function checkPermissionsTool(credentials, ocapiVersion = DEFAULT_O
|
|
|
75
77
|
{
|
|
76
78
|
type: "text",
|
|
77
79
|
text: `OCAPI probe failed: ${msg}\n\n` +
|
|
78
|
-
ocapiSettingsInstructions(
|
|
80
|
+
ocapiSettingsInstructions(),
|
|
79
81
|
},
|
|
80
82
|
],
|
|
81
83
|
};
|
|
@@ -86,8 +88,10 @@ export async function checkPermissionsTool(credentials, ocapiVersion = DEFAULT_O
|
|
|
86
88
|
content: [
|
|
87
89
|
{
|
|
88
90
|
type: "text",
|
|
89
|
-
text: `✓ OCAPI access confirmed
|
|
90
|
-
`
|
|
91
|
+
text: `✓ OCAPI read access confirmed (GET /system_object_definitions). ` +
|
|
92
|
+
`Write access is not probed; the sfcc-write tools need the mutation grant — ` +
|
|
93
|
+
`run check_permissions after a 403 or see SFCC-SETUP-CHECKLIST.md.\n` +
|
|
94
|
+
` Instance: ${formatSafeHost(credentials.hostname)}\n` +
|
|
91
95
|
` OCAPI version: ${ocapiVersion}\n` +
|
|
92
96
|
` GET /system_object_definitions: ${formatProbeCount(normalized)}`,
|
|
93
97
|
},
|
|
@@ -99,8 +103,8 @@ export async function checkPermissionsTool(credentials, ocapiVersion = DEFAULT_O
|
|
|
99
103
|
content: [
|
|
100
104
|
{
|
|
101
105
|
type: "text",
|
|
102
|
-
text: `HTTP ${result.status}: OCAPI access denied for instance ${credentials.hostname
|
|
103
|
-
ocapiSettingsInstructions(
|
|
106
|
+
text: `HTTP ${result.status}: OCAPI access denied for instance ${formatSafeHost(credentials.hostname)}.\n\n` +
|
|
107
|
+
ocapiSettingsInstructions(),
|
|
104
108
|
},
|
|
105
109
|
],
|
|
106
110
|
};
|
|
@@ -157,13 +161,13 @@ function formatProbeCount(normalized) {
|
|
|
157
161
|
* A label that understates a grant is worse than no label, because the operator
|
|
158
162
|
* stops reading the JSON once the heading has told them it is safe.
|
|
159
163
|
*/
|
|
160
|
-
function ocapiSettingsInstructions(
|
|
164
|
+
function ocapiSettingsInstructions() {
|
|
161
165
|
return (`To grant OCAPI access, paste the JSON below in Business Manager:\n` +
|
|
162
166
|
` Administration > Site Development > Open Commerce API Settings → Data API tab\n\n` +
|
|
163
167
|
`--- READ/SEARCH TOOL GRANTS (required for the sfcc read tools) ---\n` +
|
|
164
|
-
`${OCAPI_SETTINGS_READ_SEARCH(
|
|
168
|
+
`${OCAPI_SETTINGS_READ_SEARCH()}\n\n` +
|
|
165
169
|
`--- MUTATION GRANTS (required for the nine sfcc-write tools) ---\n` +
|
|
166
|
-
`${OCAPI_SETTINGS_WRITE_IMPORT(
|
|
170
|
+
`${OCAPI_SETTINGS_WRITE_IMPORT()}\n\n` +
|
|
167
171
|
`Replace <YOUR_CLIENT_ID> with the client_id from your dw.json.`);
|
|
168
172
|
}
|
|
169
173
|
/**
|
package/build/sfcc/register.js
CHANGED
|
@@ -89,7 +89,7 @@ export function registerSfccTools(registerTool, deps) {
|
|
|
89
89
|
const gatedCheckPermissions = withSfccGate(gateDeps, async (_args, credentials) => checkPermissionsTool(credentials));
|
|
90
90
|
registerSfccTool("check_permissions", {
|
|
91
91
|
description: "Probe SFCC OCAPI access via GET /system_object_definitions. " +
|
|
92
|
-
"On 200: reports OK and the detected OCAPI version. " +
|
|
92
|
+
"On 200: reports read access OK and the detected OCAPI version. " +
|
|
93
93
|
"On 401/403: prints the exact OCAPI Settings JSON to paste in Business Manager " +
|
|
94
94
|
"(read/search and mutation grants).",
|
|
95
95
|
inputSchema: z.object({
|
|
@@ -65,6 +65,10 @@ import { SFCC_VERSIONS, getSfccVersionConfig } from "./config.js";
|
|
|
65
65
|
import { publishSfccVersion } from "./tools-configured.js";
|
|
66
66
|
import { resolveSfccCredentials } from "./credentials.js";
|
|
67
67
|
import { formatSfccFailure } from "./sfcc-result.js";
|
|
68
|
+
import { canonicalizeSfccHostname, formatSafeHost } from "./hostname.js";
|
|
69
|
+
// `canonicalizeSfccHostname` moved to hostname.ts; re-exported so existing
|
|
70
|
+
// importers of this module are unaffected.
|
|
71
|
+
export { canonicalizeSfccHostname };
|
|
68
72
|
// ---------------------------------------------------------------------------
|
|
69
73
|
// Hostname policy
|
|
70
74
|
// ---------------------------------------------------------------------------
|
|
@@ -108,47 +112,8 @@ export const APPROVED_SANDBOX_HOST_FORMS = [
|
|
|
108
112
|
"<realm>-<nnn>.sandbox.dx.commercecloud.salesforce.com",
|
|
109
113
|
"<realm>-<nnn>.dx.commercecloud.salesforce.com",
|
|
110
114
|
];
|
|
111
|
-
/** One DNS label: 1–63 chars, alphanumeric, internal hyphens only. */
|
|
112
|
-
const DNS_LABEL = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
113
115
|
/** Form B's leading label: an SFCC realm id followed by a numeric instance. */
|
|
114
116
|
const REALM_INSTANCE_LABEL = /^[a-z0-9]+-\d{3}$/;
|
|
115
|
-
/**
|
|
116
|
-
* Reduce a raw hostname to a canonical comparable form, or `null` if it is not
|
|
117
|
-
* a bare hostname at all.
|
|
118
|
-
*
|
|
119
|
-
* Only DNS-equivalent normalization is applied — lowercasing and stripping a
|
|
120
|
-
* single trailing root dot. Everything else is a rejection rather than a
|
|
121
|
-
* repair: a value carrying a scheme, port, path, query, credentials, or
|
|
122
|
-
* whitespace is not a hostname, and "cleaning it up" would mean guessing which
|
|
123
|
-
* part the operator meant. Guessing is how a boundary gets bypassed, so this
|
|
124
|
-
* returns `null` and the caller fails closed.
|
|
125
|
-
*/
|
|
126
|
-
export function canonicalizeSfccHostname(raw) {
|
|
127
|
-
if (typeof raw !== "string")
|
|
128
|
-
return null;
|
|
129
|
-
let value = raw.trim().toLowerCase();
|
|
130
|
-
if (value.endsWith("."))
|
|
131
|
-
value = value.slice(0, -1);
|
|
132
|
-
if (value === "" || value.length > 253)
|
|
133
|
-
return null;
|
|
134
|
-
// Anything that is not strictly `label(.label)*` — scheme, port, path, query,
|
|
135
|
-
// userinfo, whitespace, IPv6 brackets, wildcards.
|
|
136
|
-
if (!/^[a-z0-9.-]+$/.test(value))
|
|
137
|
-
return null;
|
|
138
|
-
if (value.includes(".."))
|
|
139
|
-
return null;
|
|
140
|
-
const labels = value.split(".");
|
|
141
|
-
if (labels.length < 2)
|
|
142
|
-
return null;
|
|
143
|
-
if (!labels.every((label) => DNS_LABEL.test(label)))
|
|
144
|
-
return null;
|
|
145
|
-
// A bare IPv4 address is not a sandbox hostname and must not be canonicalized
|
|
146
|
-
// into one; the suffix check below would reject it anyway, but rejecting here
|
|
147
|
-
// keeps "is this a hostname?" and "is this host approved?" separate concerns.
|
|
148
|
-
if (/^\d+(\.\d+)*$/.test(value))
|
|
149
|
-
return null;
|
|
150
|
-
return value;
|
|
151
|
-
}
|
|
152
117
|
/**
|
|
153
118
|
* Decide whether a canonical hostname is an approved developer sandbox.
|
|
154
119
|
*
|
|
@@ -216,9 +181,9 @@ export function isApprovedSfccSandboxHost(canonical) {
|
|
|
216
181
|
* raw `dw.json` contents never enter this envelope.
|
|
217
182
|
*/
|
|
218
183
|
function targetNotSandbox(rejectedHostname) {
|
|
219
|
-
// A
|
|
220
|
-
//
|
|
221
|
-
const safeRejected = rejectedHostname
|
|
184
|
+
// A valid host is echoed in full but bounded; anything that is not a bare
|
|
185
|
+
// hostname is replaced by a fixed label so arbitrary input is never echoed.
|
|
186
|
+
const safeRejected = formatSafeHost(rejectedHostname);
|
|
222
187
|
return formatSfccFailure({
|
|
223
188
|
code: "TARGET_NOT_SANDBOX",
|
|
224
189
|
status: 403,
|
|
@@ -267,9 +232,19 @@ export function withSfccTargetBoundary(handler, deps = {}) {
|
|
|
267
232
|
// actually returned.
|
|
268
233
|
const rawInstance = typeof safeArgs.instance === "string" ? safeArgs.instance : undefined;
|
|
269
234
|
const explicitHostname = rawInstance && rawInstance.includes(".") ? rawInstance : undefined;
|
|
235
|
+
// Refuse an unsafe explicit host BEFORE any credential lookup. The check
|
|
236
|
+
// below on the resolved host stays: this one only adds a refusal earlier, so
|
|
237
|
+
// an unknown host never reaches the resolver, and a host that resolution
|
|
238
|
+
// selects is still validated after it.
|
|
239
|
+
if (explicitHostname !== undefined) {
|
|
240
|
+
const explicitCanonical = canonicalizeSfccHostname(explicitHostname);
|
|
241
|
+
if (explicitCanonical === null || !isApprovedSfccSandboxHost(explicitCanonical)) {
|
|
242
|
+
return targetNotSandbox(explicitHostname);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
270
245
|
const credResult = await resolve(explicitHostname);
|
|
271
246
|
if (!credResult.ok) {
|
|
272
|
-
return handler(safeArgs, { approved: false, error: credResult.error });
|
|
247
|
+
return handler(safeArgs, { approved: false, error: credResult.error, kind: credResult.kind });
|
|
273
248
|
}
|
|
274
249
|
const rawHostname = credResult.credentials.hostname;
|
|
275
250
|
const canonical = canonicalizeSfccHostname(rawHostname);
|
|
@@ -334,6 +309,22 @@ function notConfigured(failureClass, message) {
|
|
|
334
309
|
details: { failure_class: failureClass },
|
|
335
310
|
});
|
|
336
311
|
}
|
|
312
|
+
/**
|
|
313
|
+
* Build the failure for a credential problem on a repository that IS set up.
|
|
314
|
+
*
|
|
315
|
+
* The repo has a `dw.json` (or the caller named an explicit instance), so the
|
|
316
|
+
* caller can fix the input. That is why this carries no `NO_RETRY_INSTRUCTION`
|
|
317
|
+
* and is a 400, not the 503 "not configured" envelope.
|
|
318
|
+
*/
|
|
319
|
+
function credentialResolutionFailed(message) {
|
|
320
|
+
return formatSfccFailure({
|
|
321
|
+
code: "CREDENTIAL_RESOLUTION_FAILED",
|
|
322
|
+
status: 400,
|
|
323
|
+
message,
|
|
324
|
+
source: "gate",
|
|
325
|
+
details: { failure_class: "credential-resolution" },
|
|
326
|
+
});
|
|
327
|
+
}
|
|
337
328
|
// ---------------------------------------------------------------------------
|
|
338
329
|
// Gate
|
|
339
330
|
// ---------------------------------------------------------------------------
|
|
@@ -402,6 +393,11 @@ export function withSfccGate(deps, handler) {
|
|
|
402
393
|
// reaches here — the boundary already refused it — so the only remaining
|
|
403
394
|
// case is credential resolution having failed outright.
|
|
404
395
|
if (!target.approved) {
|
|
396
|
+
// Only a genuinely absent dw.json means "this repo is not set up for
|
|
397
|
+
// SFCC". Every other resolver failure is fixable by the caller.
|
|
398
|
+
if (target.kind !== undefined && target.kind !== "no-dw-json") {
|
|
399
|
+
return credentialResolutionFailed(target.error);
|
|
400
|
+
}
|
|
405
401
|
return notConfigured("missing-dw-json", `SFCC credential resolution failed: ${target.error} ` +
|
|
406
402
|
"Ensure a dw.json file exists in your project root with " +
|
|
407
403
|
"hostname, client-id, and client-secret fields.");
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* where a caller can read it without parsing text and `result.isError === true`
|
|
14
14
|
* still detects the failure.
|
|
15
15
|
*/
|
|
16
|
+
import { OCAPI_SETTINGS_STRUCTURE_VERSION } from "./config.js";
|
|
16
17
|
import { formatSfccWriteFailure } from "./sfcc-result.js";
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
19
|
// Write resource families
|
|
@@ -25,7 +26,7 @@ export const OCAPI_WRITE_RESOURCE_IDS = [
|
|
|
25
26
|
"/system_object_definitions",
|
|
26
27
|
"/system_object_definitions/**",
|
|
27
28
|
"/custom_object_definitions/**",
|
|
28
|
-
"/site_preferences/**",
|
|
29
|
+
"/sites/*/site_preferences/**",
|
|
29
30
|
];
|
|
30
31
|
/**
|
|
31
32
|
* The exact HTTP methods each resource family needs, derived from the methods
|
|
@@ -50,27 +51,32 @@ export const OCAPI_WRITE_RESOURCE_IDS = [
|
|
|
50
51
|
* custom preference create.
|
|
51
52
|
* /custom_object_definitions/** put, patch (+get for ETag) — attribute
|
|
52
53
|
* definition create/update.
|
|
53
|
-
*
|
|
54
|
-
* a direct PATCH with no ETag round trip
|
|
54
|
+
* site-scoped site_preferences patch (+get) — site_preference_values_set,
|
|
55
|
+
* a direct PATCH with no ETag round trip on
|
|
56
|
+
* the site-scoped resource (the org-level
|
|
57
|
+
* /site_preferences/** is read/search only).
|
|
55
58
|
*/
|
|
56
59
|
export const OCAPI_WRITE_RESOURCE_METHODS = {
|
|
57
60
|
"/system_object_definitions": ["get"],
|
|
58
61
|
"/system_object_definitions/**": ["get", "put", "patch"],
|
|
59
62
|
"/custom_object_definitions/**": ["get", "put", "patch"],
|
|
60
|
-
"/site_preferences/**": ["get", "patch"],
|
|
63
|
+
"/sites/*/site_preferences/**": ["get", "patch"],
|
|
61
64
|
};
|
|
62
65
|
// ---------------------------------------------------------------------------
|
|
63
66
|
// Grant JSON builders
|
|
64
67
|
// ---------------------------------------------------------------------------
|
|
65
68
|
/**
|
|
66
|
-
* Build the OCAPI Settings write-grant object
|
|
69
|
+
* Build the OCAPI Settings write-grant object.
|
|
70
|
+
*
|
|
71
|
+
* `_v` is the settings-structure version, never an OCAPI API version, so no
|
|
72
|
+
* caller can override it.
|
|
67
73
|
*
|
|
68
74
|
* The `clientIdPlaceholder` stays as `<YOUR_CLIENT_ID>` by default so the output
|
|
69
75
|
* is safe to print and paste — the real resolved client_id is never substituted.
|
|
70
76
|
*/
|
|
71
|
-
export function buildOcapiWriteGrantSettings(
|
|
77
|
+
export function buildOcapiWriteGrantSettings(clientIdPlaceholder = "<YOUR_CLIENT_ID>") {
|
|
72
78
|
return {
|
|
73
|
-
_v:
|
|
79
|
+
_v: OCAPI_SETTINGS_STRUCTURE_VERSION,
|
|
74
80
|
clients: [
|
|
75
81
|
{
|
|
76
82
|
client_id: clientIdPlaceholder,
|
|
@@ -85,8 +91,8 @@ export function buildOcapiWriteGrantSettings(ocapiVersion, clientIdPlaceholder =
|
|
|
85
91
|
};
|
|
86
92
|
}
|
|
87
93
|
/** Pretty-printed JSON string of the write-grant settings. */
|
|
88
|
-
export function formatOcapiWriteGrantJson(
|
|
89
|
-
return JSON.stringify(buildOcapiWriteGrantSettings(
|
|
94
|
+
export function formatOcapiWriteGrantJson(clientIdPlaceholder = "<YOUR_CLIENT_ID>") {
|
|
95
|
+
return JSON.stringify(buildOcapiWriteGrantSettings(clientIdPlaceholder), null, 2);
|
|
90
96
|
}
|
|
91
97
|
// ---------------------------------------------------------------------------
|
|
92
98
|
// 403 write-grant UX
|
|
@@ -115,7 +121,7 @@ export function ocapiWriteForbiddenMessage(operation, path) {
|
|
|
115
121
|
* same way an OCAPI read fault body is preserved.
|
|
116
122
|
*/
|
|
117
123
|
export function writeGrantForbiddenResult(params) {
|
|
118
|
-
const { operation, path,
|
|
124
|
+
const { operation, path, body } = params;
|
|
119
125
|
return formatSfccWriteFailure({
|
|
120
126
|
code: "OCAPI_WRITE_ERROR",
|
|
121
127
|
status: 403,
|
|
@@ -127,7 +133,7 @@ export function writeGrantForbiddenResult(params) {
|
|
|
127
133
|
remediation: {
|
|
128
134
|
business_manager_path: OCAPI_WRITE_GRANT_BUSINESS_MANAGER_PATH,
|
|
129
135
|
instruction: OCAPI_WRITE_GRANT_CLIENT_ID_INSTRUCTION,
|
|
130
|
-
write_grant: buildOcapiWriteGrantSettings(
|
|
136
|
+
write_grant: buildOcapiWriteGrantSettings(),
|
|
131
137
|
},
|
|
132
138
|
...(body === undefined ? {} : { body }),
|
|
133
139
|
},
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
* pre-transport ones.
|
|
17
17
|
*/
|
|
18
18
|
import { writeGrantForbiddenResult } from "./write-grants.js";
|
|
19
|
-
import { DEFAULT_OCAPI_VERSION } from "./config.js";
|
|
20
19
|
import { formatSfccWriteFailure } from "./sfcc-result.js";
|
|
21
20
|
import { redactSfccBody } from "./read-body.js";
|
|
22
21
|
/** Wrap text in the standard MCP text-result shape (success path only). */
|
|
@@ -72,12 +71,14 @@ function ocapiWriteFaultMessage(body, status) {
|
|
|
72
71
|
* `Response.status` verbatim, so the reported status *is* the upstream status
|
|
73
72
|
* and the shared formatter would rightly omit a duplicate.
|
|
74
73
|
*/
|
|
75
|
-
export function formatOcapiWriteToolResult(result, operation, path,
|
|
74
|
+
export function formatOcapiWriteToolResult(result, operation, path,
|
|
75
|
+
// Kept for call compatibility. It no longer reaches the grant JSON: `_v` there
|
|
76
|
+
// is the settings-structure version, which an API version must not override.
|
|
77
|
+
_ocapiVersion) {
|
|
76
78
|
if (result.status === 403) {
|
|
77
79
|
return writeGrantForbiddenResult({
|
|
78
80
|
operation,
|
|
79
81
|
path,
|
|
80
|
-
ocapiVersion,
|
|
81
82
|
body: result.body,
|
|
82
83
|
});
|
|
83
84
|
}
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* SFCC site-preference value write tool (BAPI-584).
|
|
3
3
|
*
|
|
4
4
|
* Implements:
|
|
5
|
-
* site_preference_values_set — PATCH /site_preferences/preference_groups/{group}/sandbox
|
|
5
|
+
* site_preference_values_set — PATCH /sites/{site_id}/site_preferences/preference_groups/{group}/sandbox
|
|
6
6
|
*
|
|
7
|
-
* Sets custom site-preference VALUES through the OCAPI Data API
|
|
7
|
+
* Sets custom site-preference VALUES through the OCAPI Data API for ONE named
|
|
8
|
+
* site. `site_id` is required and is never derived from `dw.json`: a destructive
|
|
9
|
+
* write must not pick its target silently. Sandbox-only:
|
|
8
10
|
* the value endpoint would otherwise accept `production`, so the `{instance}`
|
|
9
11
|
* segment is forced to the `sandbox` literal AFTER the shared sandbox guard has
|
|
10
12
|
* validated the requested instance. The body is a FLAT map of `c_`-prefixed
|
|
@@ -35,7 +37,17 @@ const INSTANCE_ENUM = z.enum(["staging", "development", "sandbox", "production"]
|
|
|
35
37
|
// safeguard on the OCAPI route.
|
|
36
38
|
const INSTANCE_DESCRIBE = "OCAPI instance context. v1 supports the 'sandbox' context only; any other " +
|
|
37
39
|
"value is rejected before OCAPI is called.";
|
|
40
|
+
// A site id is one URL path segment. This is a conservative single-segment
|
|
41
|
+
// pattern (no separators, whitespace, or dots), not a claim about the full set
|
|
42
|
+
// OCAPI allows; the exact constraint is confirmed by owner check OR-1.
|
|
43
|
+
const SITE_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
44
|
+
const SITE_ID_DESCRIBE = "Site id to write to; required, never defaulted. Not 'instance'.";
|
|
38
45
|
const sitePreferenceValuesSetInput = z.object({
|
|
46
|
+
site_id: z
|
|
47
|
+
.string()
|
|
48
|
+
.min(1)
|
|
49
|
+
.regex(SITE_ID_PATTERN, "site_id must contain only letters, digits, '_' or '-'.")
|
|
50
|
+
.describe(SITE_ID_DESCRIBE),
|
|
39
51
|
group: z.string().describe("Custom site preference group id, e.g. 'LLMIntegration'."),
|
|
40
52
|
instance: INSTANCE_ENUM.optional().default("sandbox").describe(INSTANCE_DESCRIBE),
|
|
41
53
|
values: sitePreferenceValuesPatchBodySchema.describe("Flat map of c_-prefixed preference ids to values (string, number, boolean, or string[])."),
|
|
@@ -44,12 +56,12 @@ const sitePreferenceValuesSetInput = z.object({
|
|
|
44
56
|
// Path builder
|
|
45
57
|
// ---------------------------------------------------------------------------
|
|
46
58
|
/**
|
|
47
|
-
* Build the site-preference group PATCH path. The `{instance}` segment is the
|
|
59
|
+
* Build the site-scoped site-preference group PATCH path. The `{instance}` segment is the
|
|
48
60
|
* `sandbox` literal — a non-sandbox instance is rejected by the guard before
|
|
49
61
|
* this is called, so a non-sandbox value can never leak into transport.
|
|
50
62
|
*/
|
|
51
|
-
export function sitePreferenceGroupPath(group) {
|
|
52
|
-
return `/site_preferences/preference_groups/${encodedSegment(group)}/sandbox`;
|
|
63
|
+
export function sitePreferenceGroupPath(siteId, group) {
|
|
64
|
+
return `/sites/${encodedSegment(siteId)}/site_preferences/preference_groups/${encodedSegment(group)}/sandbox`;
|
|
53
65
|
}
|
|
54
66
|
// ---------------------------------------------------------------------------
|
|
55
67
|
// Handler
|
|
@@ -66,7 +78,7 @@ export function buildSitePreferenceValuesSetHandler(gateDeps) {
|
|
|
66
78
|
const guard = rejectIfNotSandboxForWrite(parsed.instance);
|
|
67
79
|
if (guard)
|
|
68
80
|
return guard;
|
|
69
|
-
const path = sitePreferenceGroupPath(parsed.group);
|
|
81
|
+
const path = sitePreferenceGroupPath(parsed.site_id, parsed.group);
|
|
70
82
|
const body = buildSitePreferenceValuesPatchPayload(parsed.values);
|
|
71
83
|
try {
|
|
72
84
|
// Direct PATCH (no ETag round trip): the value endpoint does not gate on
|
|
@@ -92,10 +104,10 @@ export const SITE_PREFERENCE_WRITE_TOOL_NAMES = ["site_preference_values_set"];
|
|
|
92
104
|
export function registerSitePreferenceWriteTools(registerTool, deps) {
|
|
93
105
|
const { gateDeps } = deps;
|
|
94
106
|
registerTool("site_preference_values_set", {
|
|
95
|
-
description: "Set site-preference VALUES via PATCH /site_preferences/preference_groups/{group}/sandbox. " +
|
|
107
|
+
description: "Set site-preference VALUES via PATCH /sites/{site_id}/site_preferences/preference_groups/{group}/sandbox. " +
|
|
96
108
|
"Preference must already be defined via custom_preference_definition_create. Sandbox-only, " +
|
|
97
109
|
"destructive; flat c_-prefixed id→value map (string/number/boolean/string[]). " +
|
|
98
|
-
"Bad group → 404
|
|
110
|
+
"Bad group → 404. 403 → paste-ready grant JSON.",
|
|
99
111
|
inputSchema: sitePreferenceValuesSetInput,
|
|
100
112
|
annotations: WRITE_ANNOTATIONS,
|
|
101
113
|
}, buildSitePreferenceValuesSetHandler(gateDeps));
|
|
@@ -17,6 +17,7 @@ import path from "path";
|
|
|
17
17
|
import { resolveBapiCredentials, getPrimaryCredentialStorePath, } from "./credential-store.js";
|
|
18
18
|
import { resolveStartTicketsRepoName } from "./start-tickets-repo.js";
|
|
19
19
|
import { probeWorktreeMcpRegistration } from "./mcp-registration-doctor.js";
|
|
20
|
+
import { VERSION } from "./version.generated.js";
|
|
20
21
|
import { probeWorktreeCommandAssets } from "./command-assets-doctor.js";
|
|
21
22
|
import { MCP_SERVER_NAME, MCP_PACKAGE_NAME } from "./mcp-identity.js";
|
|
22
23
|
// BAPI-941: both are LEAF modules (the pure classifier and its git-backed
|
|
@@ -159,7 +160,7 @@ const WORKTRUNK_INSTALL_HINTS = {
|
|
|
159
160
|
linux: "See the Worktrunk documentation for Linux install instructions: https://worktrunk.dev",
|
|
160
161
|
};
|
|
161
162
|
const GIT_INSTALL_HINTS = {
|
|
162
|
-
darwin: "xcode-select --install (or brew install git)",
|
|
163
|
+
darwin: "xcode-select --install (or brew install git). Installing another Git does not ensure PATH selects it; verify with which git and git --version.",
|
|
163
164
|
linux: "Install git with your distro package manager, e.g. apt install git",
|
|
164
165
|
win32: "Install Git for Windows: https://git-scm.com/download/win",
|
|
165
166
|
};
|
|
@@ -444,24 +445,19 @@ export function worktreeMcpReachabilityDescriptor() {
|
|
|
444
445
|
},
|
|
445
446
|
};
|
|
446
447
|
}
|
|
447
|
-
/**
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
* files, which are fill-only, never refreshed, and Git-excluded per worktree.
|
|
452
|
-
*/
|
|
453
|
-
const WORKTREE_COMMAND_ASSETS_HINT = "Re-run start-tickets to provision the packaged Claude command assets " +
|
|
454
|
-
"(.claude/commands/) into this worktree. Do not manually copy or commit the " +
|
|
455
|
-
"generated command files — they are bootstrapped per worktree and Git-excluded.";
|
|
448
|
+
/** Version-pinned fill-only repair; permissions and unsafe paths need their own fix. */
|
|
449
|
+
const WORKTREE_COMMAND_ASSETS_HINT = `Run npx -y ${MCP_PACKAGE_NAME}@${VERSION} repair-assets --project-root <established-root> --host claude ` +
|
|
450
|
+
"to fill missing packaged files. Resolve permission/path/platform problems separately; refresh/reconnect and run doctor.";
|
|
451
|
+
const WORKTREE_COMMAND_ASSETS_PROVISION_HINT = "In-place asset repair is not supported on this platform. Re-run start-tickets to provision the packaged Claude command assets (.claude/commands/) into this worktree. Existing files are preserved; refresh/reconnect and run doctor.";
|
|
456
452
|
const WORKTREE_COMMAND_ASSETS_INSTALL_HINTS = {
|
|
457
|
-
darwin:
|
|
453
|
+
darwin: WORKTREE_COMMAND_ASSETS_PROVISION_HINT,
|
|
458
454
|
linux: WORKTREE_COMMAND_ASSETS_HINT,
|
|
459
|
-
win32:
|
|
455
|
+
win32: WORKTREE_COMMAND_ASSETS_PROVISION_HINT,
|
|
460
456
|
};
|
|
461
457
|
/**
|
|
462
458
|
* Doctor-only, strictly read-only probe: does the current worktree contain every
|
|
463
459
|
* packaged Claude slash-command asset under `.claude/commands/`? Delegates to the
|
|
464
|
-
* read-only {@link probeWorktreeCommandAssets} using only `deps.
|
|
460
|
+
* read-only {@link probeWorktreeCommandAssets} using only `deps.commandAssetMetadata`,
|
|
465
461
|
* `deps.platform`, and `deps.cwd`. Reports an unavailable diagnostic (never a
|
|
466
462
|
* false "present") when the optional read-only filesystem boundary is absent. The
|
|
467
463
|
* probe performs NO writes, Git calls, or process spawns.
|
|
@@ -472,15 +468,14 @@ export function worktreeCommandAssetsDescriptor() {
|
|
|
472
468
|
label: "Worktree Claude command assets",
|
|
473
469
|
installHint: WORKTREE_COMMAND_ASSETS_INSTALL_HINTS,
|
|
474
470
|
probe: async (deps) => {
|
|
475
|
-
|
|
476
|
-
if (!readFile) {
|
|
471
|
+
if (!deps.commandAssetMetadata) {
|
|
477
472
|
return {
|
|
478
473
|
found: false,
|
|
479
474
|
detail: "command-asset probe unavailable (no read-only filesystem access)",
|
|
480
475
|
};
|
|
481
476
|
}
|
|
482
477
|
const result = await probeWorktreeCommandAssets(deps.cwd, {
|
|
483
|
-
|
|
478
|
+
metadata: deps.commandAssetMetadata,
|
|
484
479
|
platform: deps.platform,
|
|
485
480
|
});
|
|
486
481
|
return { found: result.found, detail: result.detail };
|
|
@@ -817,6 +812,7 @@ export function getDoctorOnlyPrereqDescriptors(_platform, _env, agent) {
|
|
|
817
812
|
return [
|
|
818
813
|
uvDescriptor(),
|
|
819
814
|
agentDescriptor(agent),
|
|
815
|
+
gitCompatibilityDescriptor(),
|
|
820
816
|
credentialResolutionDescriptor(),
|
|
821
817
|
worktreeMcpReachabilityDescriptor(),
|
|
822
818
|
worktreeCommandAssetsDescriptor(),
|
|
@@ -863,6 +859,7 @@ export async function probePrerequisite(deps, descriptor) {
|
|
|
863
859
|
label: descriptor.label,
|
|
864
860
|
found: outcome.found,
|
|
865
861
|
detail: outcome.detail,
|
|
862
|
+
gitCompatibility: outcome.gitCompatibility,
|
|
866
863
|
installHint: hintForPlatform(descriptor.installHint, deps.platform),
|
|
867
864
|
authNote: descriptor.authNote,
|
|
868
865
|
// Deliberately NOT carried through when the probe THREW: the catch above
|
|
@@ -872,6 +869,63 @@ export async function probePrerequisite(deps, descriptor) {
|
|
|
872
869
|
renderDetailWhenMissing: descriptor.renderDetailWhenMissing === true && !probeThrew ? true : undefined,
|
|
873
870
|
};
|
|
874
871
|
}
|
|
872
|
+
/** Release-specific advisory baseline; never a launcher admission requirement.
|
|
873
|
+
* Evidence: https://github.com/max-sixty/worktrunk/blob/v0.80.0/src/git/version.rs
|
|
874
|
+
*/
|
|
875
|
+
export const WORKTRUNK_GIT_BASELINE = "Worktrunk v0.80.0 (Git >= 2.43.0)";
|
|
876
|
+
/** Read-only, advisory compatibility with the documented release, not every release. */
|
|
877
|
+
export async function probeGitWorktrunkCompatibility(deps) {
|
|
878
|
+
const basis = WORKTRUNK_GIT_BASELINE;
|
|
879
|
+
let executable;
|
|
880
|
+
let version;
|
|
881
|
+
const unknown = (reason) => ({ status: "unknown", executable, version, basis, reason });
|
|
882
|
+
try {
|
|
883
|
+
const locator = getCommandProbe("git", deps.platform);
|
|
884
|
+
const located = await deps.runCommand(locator.file, locator.args, { timeoutMs: 3000 });
|
|
885
|
+
if (located.unavailable)
|
|
886
|
+
return unknown("locator-unavailable");
|
|
887
|
+
if (!commandSucceeded(located))
|
|
888
|
+
return unknown(located.exitCode === 1 ? "git-missing" : "locator-unavailable");
|
|
889
|
+
const candidate = located.stdout.trim().split(/\r?\n/)[0];
|
|
890
|
+
// Only absolute paths from a successful locator are diagnostic metadata.
|
|
891
|
+
if (!candidate || candidate.length > 512 || /[\x00-\x1f\x7f]/.test(candidate) ||
|
|
892
|
+
!(deps.platform === "win32" ? path.win32.isAbsolute(candidate) : path.posix.isAbsolute(candidate))) {
|
|
893
|
+
return unknown("locator-unavailable");
|
|
894
|
+
}
|
|
895
|
+
executable = candidate;
|
|
896
|
+
const result = await deps.runCommand(executable, ["--version"], { timeoutMs: 3000 });
|
|
897
|
+
if (!commandSucceeded(result))
|
|
898
|
+
return unknown("version-unavailable");
|
|
899
|
+
const match = /^git version (\d{1,5})\.(\d{1,5})(?:\.(\d{1,5}))?(?:\.windows\.\d+| \(Apple Git-[\w.-]+\))?$/.exec(result.stdout.trim());
|
|
900
|
+
if (!match)
|
|
901
|
+
return unknown("version-unparseable");
|
|
902
|
+
const parts = [Number(match[1]), Number(match[2]), Number(match[3] ?? 0)];
|
|
903
|
+
version = parts.join(".");
|
|
904
|
+
if (deps.env[WORKTRUNK_BINARY_OVERRIDE_ENV]?.trim())
|
|
905
|
+
return unknown("override-unconfirmed");
|
|
906
|
+
const minimum = [2, 43, 0];
|
|
907
|
+
const differing = parts.findIndex((n, i) => n !== minimum[i]);
|
|
908
|
+
const compatible = differing < 0 || parts[differing] > minimum[differing];
|
|
909
|
+
return { status: compatible ? "compatible" : "incompatible", executable, version, basis };
|
|
910
|
+
}
|
|
911
|
+
catch {
|
|
912
|
+
return unknown(executable ? "version-unavailable" : "locator-unavailable");
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
/** Pure formatter shared by doctor and first-build output. No raw probe output. */
|
|
916
|
+
export function formatGitCompatibilityDiagnostic(result) {
|
|
917
|
+
const metadata = [result.executable, result.version && `Git ${result.version}`].filter(Boolean).join("; ");
|
|
918
|
+
return `Git compatibility advisory: ${result.status}${metadata ? ` — ${metadata}` : ""}; comparison: ${result.basis}` +
|
|
919
|
+
(result.reason ? `; ${result.reason}` : "") +
|
|
920
|
+
(result.status === "incompatible" ? ". Verify the Git executable/version selected on PATH and install a supported Git manually." : "") +
|
|
921
|
+
(result.reason === "override-unconfirmed" ? ". Custom Worktrunk release requirement is unconfirmed." : "");
|
|
922
|
+
}
|
|
923
|
+
export function gitCompatibilityDescriptor() {
|
|
924
|
+
return {
|
|
925
|
+
id: "git-compatibility", label: "Git compatibility advisory", installHint: GIT_INSTALL_HINTS,
|
|
926
|
+
probe: async (deps) => ({ found: true, gitCompatibility: await probeGitWorktrunkCompatibility(deps) }),
|
|
927
|
+
};
|
|
928
|
+
}
|
|
875
929
|
/**
|
|
876
930
|
* Enforce the preflight descriptors fail-fast (matching the existing
|
|
877
931
|
* `runPreflight` behaviour). Iterates the shared preflight descriptors in order
|