@bridge_gpt/mcp-server 0.2.37 → 0.2.39
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 +193 -16
- package/build/agent-capabilities/probe-context.js +2 -1
- package/build/agent-launchers/claude-executor-adapter.js +392 -0
- package/build/agent-launchers/executor-adapter-inspection.js +163 -0
- package/build/agent-launchers/executor-adapter-registry.js +90 -0
- package/build/agent-launchers/executor-adapter.js +136 -0
- package/build/agent-registry.js +28 -0
- package/build/agents.generated.js +1 -1
- package/build/claude-login.js +85 -0
- package/build/claude-user-config-doctor.js +59 -33
- package/build/commands.generated.js +12 -11
- package/build/conduct-epic/bridge-client.js +345 -0
- package/build/conduct-epic/checkpoint-store.js +423 -0
- package/build/conduct-epic/cli.js +1732 -0
- package/build/conduct-epic/lock.js +302 -0
- package/build/conduct-epic/pr-state.js +197 -0
- package/build/conduct-epic/spawn.js +101 -0
- package/build/conductor/bridge-api-client.js +37 -2
- package/build/conductor/doctor.js +11 -1
- package/build/conductor/install-doctor.js +184 -10
- package/build/conductor-bin.js +7 -7
- package/build/credential-store.js +10 -4
- package/build/credentials-cli.js +34 -19
- package/build/docs.generated.js +1 -1
- package/build/doctor.js +579 -88
- package/build/executor/agent-identity.js +32 -0
- package/build/executor/cli.js +50 -39
- package/build/executor/deps.js +15 -1
- package/build/executor/env.js +56 -45
- package/build/executor/index.js +9 -1
- package/build/executor/install-preflight.js +138 -0
- package/build/executor/job-errors.js +200 -0
- package/build/executor/job-runner.js +619 -268
- package/build/executor/observation.js +165 -0
- package/build/executor/permissions.js +163 -36
- package/build/executor/platform.js +54 -0
- package/build/executor/preflight.js +175 -67
- package/build/executor/process.js +39 -7
- package/build/executor/runner.js +19 -0
- package/build/executor/service-lifecycle.js +269 -0
- package/build/executor/service-unit.js +121 -12
- package/build/executor/stale-artifacts.js +70 -0
- package/build/executor/test-clock.js +188 -24
- package/build/executor/worker-command.js +22 -58
- package/build/executor/worker-log.js +82 -0
- package/build/executor/worktree-lock.js +264 -0
- package/build/index.js +527 -357
- package/build/install-bridge-conductor.js +376 -38
- package/build/install-bridge.js +414 -114
- package/build/install-doctor.js +13 -0
- package/build/install-reexec.js +5 -3
- package/build/mcp-install-state.js +130 -0
- package/build/mcp-profile.js +11 -2
- package/build/mcp-provisioning.js +15 -0
- package/build/merge-pull-request.js +562 -0
- package/build/phase-result-artifacts.js +450 -0
- package/build/pipeline-orchestrator.js +4 -0
- package/build/pipeline-utils.js +16 -0
- package/build/pipelines.generated.js +7 -7
- package/build/plane/preflight.js +18 -14
- package/build/plane/supervisor.js +8 -1
- package/build/project-root.js +34 -0
- package/build/readme.generated.js +1 -1
- package/build/run-unit-tests-launcher.js +36 -9
- package/build/setup-epic.js +57 -4
- package/build/sfcc/ocapi-shape.js +23 -4
- package/build/sfcc/permissions.js +25 -6
- package/build/sfcc/read-body.js +92 -0
- package/build/sfcc/read-projection.js +6 -2
- package/build/sfcc/reads-custom-object-def.js +33 -21
- package/build/sfcc/reads-site-preference.js +20 -7
- package/build/sfcc/reads-system-object.js +11 -5
- package/build/sfcc/register.js +61 -23
- package/build/sfcc/registration-inventory.js +89 -0
- package/build/sfcc/setup-status.js +18 -34
- package/build/sfcc/tool-wrapper.js +294 -17
- package/build/sfcc/write-grants.js +33 -1
- package/build/sfcc/write-guard.js +41 -12
- package/build/sfcc/write-result.js +16 -7
- package/build/sfcc/writes-custom-object-def.js +12 -4
- package/build/sfcc/writes-site-preference.js +6 -1
- package/build/sfcc/writes-system-object.js +11 -2
- package/build/sfcc/writes.js +13 -8
- package/build/start-tickets-prereqs.js +25 -15
- package/build/start-tickets.js +123 -21
- package/build/version.generated.js +1 -1
- package/build/worktree-core.js +9 -3
- package/docs/install/mcp-tool-integrations.md +54 -9
- package/docs/install/sfcc-integration.md +71 -24
- package/package.json +3 -3
- package/build/executor/worker-config-isolation.js +0 -287
package/build/worktree-core.js
CHANGED
|
@@ -186,9 +186,15 @@ const CLEANUP_FAILURE_DIAGNOSTIC = "untracked-file cleanup did not complete; con
|
|
|
186
186
|
* settings, MCP registrations/trust, and worker logging are provisioned again — so
|
|
187
187
|
* anything cleanup removes is re-created by the provisioning that follows it.
|
|
188
188
|
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
189
|
+
* `.conductor/` content is NOT removed here, and must not be assumed to be
|
|
190
|
+
* (BAPI-814/D1). `.conductor/` is an ignored path in this repository, and `-fd`
|
|
191
|
+
* without `-x` leaves ignored paths alone — so a prior attempt's
|
|
192
|
+
* `.conductor/result.json` and `.conductor/critique.md` survive this cleanup
|
|
193
|
+
* entirely. Removing them is the ATTEMPT OWNER's job, performed explicitly in
|
|
194
|
+
* `executor/job-runner.ts` after the worktree lock is acquired and before the
|
|
195
|
+
* worker spawns (`removeStaleVerdictArtifacts`). That placement is deliberate:
|
|
196
|
+
* cleanup must be serialized against the lock, whereas this function runs during
|
|
197
|
+
* unlocked worktree preparation.
|
|
192
198
|
*
|
|
193
199
|
* FAIL-OPEN. Pre-change behavior performed no cleanup at all, so being unable to
|
|
194
200
|
* clean must never change the worktree row or the job outcome. Every failure path —
|
|
@@ -58,7 +58,8 @@ activated. `core` is always implicitly included.
|
|
|
58
58
|
| `core` | always |
|
|
59
59
|
| `pipeline-authoring` | `BRIDGE_MCP_PROFILE` includes `pipeline-authoring` or `full` |
|
|
60
60
|
| `conductor` | includes `conductor` or `full` |
|
|
61
|
-
| `sfcc` | includes `sfcc` or `full` (2 diagnostics ship in `core
|
|
61
|
+
| `sfcc` | includes `sfcc` or `full` — the OCAPI **read** tools and `sfcc_log_query` (2 diagnostics ship in `core`) |
|
|
62
|
+
| `sfcc-write` | includes `sfcc-write` or `full` — the destructive SFCC **write** tools. Independent of `sfcc`: neither group implies the other (BAPI-817) |
|
|
62
63
|
|
|
63
64
|
The `pipeline-authoring` tools live in `index.ts` alongside core, gated by
|
|
64
65
|
`ACTIVE_GROUPS.has("pipeline-authoring")`.
|
|
@@ -178,10 +179,43 @@ by tier.
|
|
|
178
179
|
| Tool | Dependencies (class) |
|
|
179
180
|
|---|---|
|
|
180
181
|
| `create_pull_request` | VCS **[BLOCK]** (Tier-1) |
|
|
182
|
+
| `merge_pull_request` | VCS **[BLOCK]** (Tier-1; protected server-decided merge — see below) |
|
|
181
183
|
| `resolve_ci_checks` | VCS **[BLOCK]** (Tier-1) |
|
|
182
184
|
| `poll_ci_checks` | VCS **[BLOCK]** (Tier-1); also **hidden until `ci_check_config` is resolved** (needs a prior `resolve_ci_checks`) |
|
|
183
185
|
| `materialize_fresh_base` | VCS / git base ref **[BLOCK]** (Tier-1; materializes a pinned base tree) |
|
|
184
186
|
|
|
187
|
+
#### `merge_pull_request` — what the caller can observe
|
|
188
|
+
|
|
189
|
+
The tool holds no VCS credential and performs no merge of its own; it asks the
|
|
190
|
+
server and reports the answer. Four constraints are worth knowing before calling it:
|
|
191
|
+
|
|
192
|
+
- **Only `merged: true` is success.** `dry_run`, `pending_approval`, and
|
|
193
|
+
`lease_held` are non-progress outcomes — nothing was merged, and repeating the
|
|
194
|
+
call unchanged will produce the same answer until the named condition moves.
|
|
195
|
+
- **Workflow files are refused.** The server reads the pull request's complete
|
|
196
|
+
changed-file list and declines any PR touching `.github/workflows/`, including a
|
|
197
|
+
file *renamed out of* that directory. A file list it cannot prove complete is
|
|
198
|
+
refused the same way, as `workflow_files_unreadable` — an unreadable diff is not
|
|
199
|
+
a clean one.
|
|
200
|
+
- **`auto_merge_enabled` must be on.** Merge configuration that is missing,
|
|
201
|
+
disabled, or malformed produces `dry_run` rather than a merge. Enable it on the
|
|
202
|
+
project default via `PUT /jira/epic-runs/supervisor-config/defaults/`; the
|
|
203
|
+
`dry_run` envelope carries that exact remediation in its `hint`.
|
|
204
|
+
- **No required checks, no merge.** When no authoritative required-check set can be
|
|
205
|
+
resolved, the tool returns `gate_unresolved` and never sends the merge request —
|
|
206
|
+
"zero required checks are all green" is not a CI pass.
|
|
207
|
+
- **Review is refused too, when the repository opts in.** If the done gate carries a
|
|
208
|
+
`review_state` condition, the tool also refuses when the verdict is not approving
|
|
209
|
+
for the exact `expected_head_sha`. `review_not_approved` is a wait — a verdict for
|
|
210
|
+
this head may still be posted, so it carries `retry_hint: "retry_later"`. The other
|
|
211
|
+
four need intervention (`needs_human`): `review_unavailable` (review state could not
|
|
212
|
+
be read), `review_source_unsupported` (`min_approvals` / `combination`, refused
|
|
213
|
+
rather than downgraded), `review_config_invalid` (review configuration exists but is
|
|
214
|
+
malformed), and `head_sha_drift` (the verdict belongs to a different commit). The
|
|
215
|
+
tool's own check is advisory; the server's head-bound review observation is
|
|
216
|
+
authoritative, so a tool-side pass followed by a server refusal is expected for a
|
|
217
|
+
legacy approved comment that carries no head proof.
|
|
218
|
+
|
|
185
219
|
### Full-automation (composite)
|
|
186
220
|
|
|
187
221
|
These orchestrate an idea → ticket → review → start chain, so they inherit the
|
|
@@ -239,11 +273,16 @@ integration dependency.
|
|
|
239
273
|
| `sfcc_setup_status` | — none (its purpose is to report which SFCC prerequisites are missing) |
|
|
240
274
|
| `check_permissions` | SFCC OCAPI **[BLOCK]** (probes OCAPI; reports not-OK without access / version config) |
|
|
241
275
|
|
|
242
|
-
### Gated behind the `sfcc` group
|
|
276
|
+
### Gated behind the `sfcc` group (reads + logs)
|
|
243
277
|
|
|
244
278
|
All are profile-gated **[BLOCK]** to register, and all require SFCC OCAPI creds +
|
|
245
|
-
`version` config + a reachable sandbox → SFCC OCAPI **[BLOCK]**.
|
|
246
|
-
|
|
279
|
+
`version` config + a reachable sandbox → SFCC OCAPI **[BLOCK]**.
|
|
280
|
+
|
|
281
|
+
Every SFCC tool — including the two always-on diagnostics above — additionally
|
|
282
|
+
crosses the resolved-hostname sandbox boundary at invocation time: if the host
|
|
283
|
+
the credentials resolve to is not a recognized developer sandbox, the call is
|
|
284
|
+
refused with `403 TARGET_NOT_SANDBOX` before any request leaves the machine
|
|
285
|
+
(BAPI-817).
|
|
247
286
|
|
|
248
287
|
**Reads:**
|
|
249
288
|
|
|
@@ -258,7 +297,13 @@ sandbox-only and destructive.
|
|
|
258
297
|
| `site_preference_get` | Profile-gated **[BLOCK]** · SFCC OCAPI **[BLOCK]** |
|
|
259
298
|
| `site_preference_search` | Profile-gated **[BLOCK]** · SFCC OCAPI **[BLOCK]** |
|
|
260
299
|
|
|
261
|
-
|
|
300
|
+
### Gated behind the `sfcc-write` group
|
|
301
|
+
|
|
302
|
+
Writes are destructive and sandbox-only. They are **not** registered by the
|
|
303
|
+
`sfcc` group — enabling introspection must never enable mutation — so a session
|
|
304
|
+
wanting both selects `sfcc,sfcc-write` (or `full`).
|
|
305
|
+
|
|
306
|
+
**Writes (sandbox-only):**
|
|
262
307
|
|
|
263
308
|
| Tool | Dependencies (class) |
|
|
264
309
|
|---|---|
|
|
@@ -272,7 +317,7 @@ sandbox-only and destructive.
|
|
|
272
317
|
| `custom_object_definition_attribute_update` | Profile-gated **[BLOCK]** · SFCC OCAPI write **[BLOCK]** |
|
|
273
318
|
| `site_preference_values_set` | Profile-gated **[BLOCK]** · SFCC OCAPI write **[BLOCK]** |
|
|
274
319
|
|
|
275
|
-
**Logs
|
|
320
|
+
**Logs** (gated by `sfcc`, not `sfcc-write`)**:**
|
|
276
321
|
|
|
277
322
|
| Tool | Dependencies (class) |
|
|
278
323
|
|---|---|
|
|
@@ -286,12 +331,12 @@ sandbox-only and destructive.
|
|
|
286
331
|
|---|---|---|
|
|
287
332
|
| **Ticket backend** (jira mode) | `create_ticket`, `get_ticket(s)`, `update_ticket_description`, `update_jira_status`, `get_jira_transitions`, and every AI generator (they read the ticket) | — |
|
|
288
333
|
| **Jira (only)** | `get_comments`, `add_comment`, `attachment`, `estimate_epic` | — |
|
|
289
|
-
| **Version control (VCS)** | `create_pull_request`, `resolve_ci_checks`, `poll_ci_checks`, `materialize_fresh_base`, `parse_repository`, `regenerate_directory_map`, `wait_for_done_gate`; **Tier-3** `request_plan_generation`/`get_plan`, `request_architecture`/`get_architecture`, `request_reimplement_context`/`get_reimplement_context`, `create_doc`(tdd/architecture) | **Tier-4** `request_clarifying_questions`, `request_ticket_critique`, `request_ticket_review`, `request_prd`, `create_doc`(prd/fsd) |
|
|
334
|
+
| **Version control (VCS)** | `create_pull_request`, `merge_pull_request`, `resolve_ci_checks`, `poll_ci_checks`, `materialize_fresh_base`, `parse_repository`, `regenerate_directory_map`, `wait_for_done_gate`; **Tier-3** `request_plan_generation`/`get_plan`, `request_architecture`/`get_architecture`, `request_reimplement_context`/`get_reimplement_context`, `create_doc`(tdd/architecture) | **Tier-4** `request_clarifying_questions`, `request_ticket_critique`, `request_ticket_review`, `request_prd`, `create_doc`(prd/fsd) |
|
|
290
335
|
| **Code index** (succeeded parse) | Tier-3 plan/architecture/reimplement/`create_doc`(tdd/architecture); `request_council` in `technical`/`discovery` modes | Tier-4 clarifying-questions/critique/review/prd/fsd |
|
|
291
|
-
| **SFCC OCAPI** | `check_permissions` +
|
|
336
|
+
| **SFCC OCAPI** | `check_permissions` + every gated SFCC read and write tool | — |
|
|
292
337
|
| **SFCC WebDAV logs** | `sfcc_log_query` | — |
|
|
293
338
|
| **Deep-research flag** | `request_deep_research`, `get_deep_research` | — |
|
|
294
|
-
| **Profile gating** | all `pipeline-authoring`, `conductor`, and
|
|
339
|
+
| **Profile gating** | all `pipeline-authoring`, `conductor`, `sfcc`, and `sfcc-write` tools | — |
|
|
295
340
|
|
|
296
341
|
## Notes & caveats
|
|
297
342
|
|
|
@@ -6,11 +6,25 @@ small set of sandbox-only writes — through the **OCAPI Data API**. This guide
|
|
|
6
6
|
setting up the OCAPI client that those tools authenticate against.
|
|
7
7
|
|
|
8
8
|
> **Sandbox / local development only.** This integration is intended for a **developer
|
|
9
|
-
> sandbox
|
|
10
|
-
>
|
|
11
|
-
>
|
|
12
|
-
>
|
|
13
|
-
>
|
|
9
|
+
> sandbox**, and that restriction is **enforced in code**: before any SFCC tool runs,
|
|
10
|
+
> Bridge validates the hostname your credentials actually resolve to — from `dw.json`
|
|
11
|
+
> or `SFCC_*` — against the sandbox forms listed below. An unrecognized host is refused
|
|
12
|
+
> with a `403` (`error.code: "TARGET_NOT_SANDBOX"`) before any request leaves your
|
|
13
|
+
> machine. The check reads the resolved hostname, never the `instance` tool argument,
|
|
14
|
+
> so omitting `instance` or passing `"sandbox"` cannot bypass it.
|
|
15
|
+
>
|
|
16
|
+
> Accepted sandbox hostname forms:
|
|
17
|
+
>
|
|
18
|
+
> - `<realm>-<nnn>.sandbox.<region>.dx.commercecloud.salesforce.com`
|
|
19
|
+
> - `<realm>-<nnn>.sandbox.dx.commercecloud.salesforce.com`
|
|
20
|
+
> - `<realm>-<nnn>.dx.commercecloud.salesforce.com`
|
|
21
|
+
>
|
|
22
|
+
> Anything else — a `production-`/`staging-`/`development-` prefixed host, or any
|
|
23
|
+
> `*.demandware.net` host — is rejected.
|
|
24
|
+
>
|
|
25
|
+
> Still do not configure the grants below on an instance that holds real data.
|
|
26
|
+
> Credentials stay local (in `dw.json` or `SFCC_*` env vars) and are never sent to
|
|
27
|
+
> Bridge.
|
|
14
28
|
|
|
15
29
|
For the full per-tool list and what each SFCC tool depends on, see
|
|
16
30
|
[MCP Tool Integration Dependencies](./mcp-tool-integrations.md). For the tool reference
|
|
@@ -34,20 +48,39 @@ In Business Manager for the sandbox:
|
|
|
34
48
|
|
|
35
49
|
**Administration → Site Development → Open Commerce API Settings → Data API** tab.
|
|
36
50
|
|
|
37
|
-
Add the
|
|
38
|
-
**Save**.
|
|
39
|
-
|
|
51
|
+
Add the client entry below to the `clients` array of the Data API settings, then
|
|
52
|
+
**Save**. It grants only the resource families and HTTP methods Bridge's SFCC tools
|
|
53
|
+
actually call — not a global `/**` grant. `check_permissions` prints the same JSON on
|
|
54
|
+
a 401/403, split into the two blocks.
|
|
55
|
+
|
|
56
|
+
**READ/SEARCH TOOL GRANTS** — required by the `sfcc` read tools. (`post` is OCAPI's
|
|
57
|
+
convention for its `*_search` endpoints, not a mutation.)
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"client_id": "<your-client-id-here>",
|
|
62
|
+
"resources": [
|
|
63
|
+
{ "resource_id": "/system_object_definitions", "methods": ["get"], "read_attributes": "(**)", "write_attributes": "(**)" },
|
|
64
|
+
{ "resource_id": "/system_object_definitions/**", "methods": ["get", "post"], "read_attributes": "(**)", "write_attributes": "(**)" },
|
|
65
|
+
{ "resource_id": "/site_preferences/**", "methods": ["get", "post"], "read_attributes": "(**)", "write_attributes": "(**)" },
|
|
66
|
+
{ "resource_id": "/custom_object_definitions/**", "methods": ["get", "post"], "read_attributes": "(**)", "write_attributes": "(**)" }
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**MUTATION GRANTS** — required **only if you enable `BRIDGE_MCP_PROFILE=sfcc-write`**,
|
|
72
|
+
which registers the nine destructive write tools. These are shipped capabilities, not
|
|
73
|
+
future work. No `delete` is granted, because no shipped write tool performs one; the
|
|
74
|
+
`get` entries are needed for the If-Match ETag round trip that precedes each `PATCH`.
|
|
40
75
|
|
|
41
76
|
```json
|
|
42
77
|
{
|
|
43
78
|
"client_id": "<your-client-id-here>",
|
|
44
79
|
"resources": [
|
|
45
|
-
{
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"resource_id": "/**"
|
|
50
|
-
}
|
|
80
|
+
{ "resource_id": "/system_object_definitions", "methods": ["get"], "read_attributes": "(**)", "write_attributes": "(**)" },
|
|
81
|
+
{ "resource_id": "/system_object_definitions/**", "methods": ["get", "put", "patch"], "read_attributes": "(**)", "write_attributes": "(**)" },
|
|
82
|
+
{ "resource_id": "/custom_object_definitions/**", "methods": ["get", "put", "patch"], "read_attributes": "(**)", "write_attributes": "(**)" },
|
|
83
|
+
{ "resource_id": "/site_preferences/**", "methods": ["get", "patch"], "read_attributes": "(**)", "write_attributes": "(**)" }
|
|
51
84
|
]
|
|
52
85
|
}
|
|
53
86
|
```
|
|
@@ -57,7 +90,9 @@ Notes:
|
|
|
57
90
|
- The `client_id` **must match** the Account Manager API client whose credentials you
|
|
58
91
|
put in `dw.json` / `SFCC_*` below. Replace the value above with your own client id if
|
|
59
92
|
it differs.
|
|
60
|
-
- If the Data API settings are empty, wrap the
|
|
93
|
+
- If the Data API settings are empty, wrap the entries in the standard settings
|
|
94
|
+
envelope. Merge the resource lists from the block(s) above into one `resources`
|
|
95
|
+
array — do not substitute a global `"resource_id": "/**"` grant:
|
|
61
96
|
|
|
62
97
|
```json
|
|
63
98
|
{
|
|
@@ -66,12 +101,10 @@ Notes:
|
|
|
66
101
|
{
|
|
67
102
|
"client_id": "<your-client-id-here>",
|
|
68
103
|
"resources": [
|
|
69
|
-
{
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
"resource_id": "/**"
|
|
74
|
-
}
|
|
104
|
+
{ "resource_id": "/system_object_definitions", "methods": ["get"], "read_attributes": "(**)", "write_attributes": "(**)" },
|
|
105
|
+
{ "resource_id": "/system_object_definitions/**", "methods": ["get", "post"], "read_attributes": "(**)", "write_attributes": "(**)" },
|
|
106
|
+
{ "resource_id": "/site_preferences/**", "methods": ["get", "post"], "read_attributes": "(**)", "write_attributes": "(**)" },
|
|
107
|
+
{ "resource_id": "/custom_object_definitions/**", "methods": ["get", "post"], "read_attributes": "(**)", "write_attributes": "(**)" }
|
|
75
108
|
]
|
|
76
109
|
}
|
|
77
110
|
]
|
|
@@ -109,14 +142,28 @@ or the `/teach-bridge` skill.
|
|
|
109
142
|
## 4. Enable the SFCC tools
|
|
110
143
|
|
|
111
144
|
The two diagnostic tools (`sfcc_setup_status`, `check_permissions`) are always
|
|
112
|
-
registered.
|
|
113
|
-
|
|
114
|
-
|
|
145
|
+
registered. Everything else is gated, behind **two independent profile groups**:
|
|
146
|
+
|
|
147
|
+
| Group | Registers |
|
|
148
|
+
|---|---|
|
|
149
|
+
| `sfcc` | the 8 OCAPI read tools + `sfcc_log_query` — read-only |
|
|
150
|
+
| `sfcc-write` | the 9 destructive write tools |
|
|
151
|
+
|
|
152
|
+
Neither implies the other. Add what you need to `BRIDGE_MCP_PROFILE` in the MCP server
|
|
153
|
+
`env` block (it is comma-separated), then **restart the MCP client**:
|
|
115
154
|
|
|
116
155
|
```json
|
|
117
156
|
"env": { "BRIDGE_MCP_PROFILE": "sfcc" }
|
|
118
157
|
```
|
|
119
158
|
|
|
159
|
+
For reads plus writes, use `"sfcc,sfcc-write"`. `full` expands to every group and is
|
|
160
|
+
therefore write-capable.
|
|
161
|
+
|
|
162
|
+
> **Migration.** `sfcc` used to register the nine write tools too. It no longer does.
|
|
163
|
+
> If you were relying on SFCC writes through `BRIDGE_MCP_PROFILE=sfcc`, change it to
|
|
164
|
+
> `BRIDGE_MCP_PROFILE=sfcc,sfcc-write`. `full` users keep write access and need no
|
|
165
|
+
> change.
|
|
166
|
+
|
|
120
167
|
## 5. Verify
|
|
121
168
|
|
|
122
169
|
Ask your agent to run:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bridge_gpt/mcp-server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.39",
|
|
4
4
|
"description": "Bridge API MCP server — exposes Jira endpoints as MCP tools for Claude Code agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"test": "npm run test:normal && npm run test:module-mocks",
|
|
30
30
|
"test:normal": "node scripts/run-unit-tests.js normal",
|
|
31
31
|
"test:module-mocks": "node scripts/run-unit-tests.js module-mocks",
|
|
32
|
-
"test:integration": "node --test build/integration/refresh-main.integration.test.js build/integration/command-provisioning.integration.test.js build/integration/start-tickets.integration.test.js build/integration/start-tickets-tier-handoff.integration.test.js build/integration/doctor.integration.test.js build/integration/agent-capabilities.integration.test.js build/integration/conductor-producer.integration.test.js build/integration/conductor-message-relay.integration.test.js build/integration/executor-http-runner.integration.test.js build/integration/executor-job-behaviors.integration.test.js build/integration/executor-recovery-jobs.integration.test.js build/integration/executor-spec-review-prompt.integration.test.js build/integration/resume-pre-spawn.git.integration.test.js build/integration/worker-finalization-origin.integration.test.js build/integration/post-remediation-merge-ci-wait.integration.test.js build/integration/executor-merge-supervision.integration.test.js build/integration/attachment-binary-roundtrip.integration.test.js build/integration/dependent-ticket-fresh-base.integration.test.js build/integration/execute-plan-instructions.integration.test.js build/integration/implement-ticket-finalization-order.integration.test.js build/integration/conductor-bundle-artifacts.integration.test.js build/integration/install-bridge-repo-resolution.integration.test.js build/integration/capability-report-contract.integration.test.js build/integration/request-brainstorm-general.integration.test.js build/integration/request-council-trigger-drop.integration.test.js build/integration/install-bridge-onboarding-launch.integration.test.js build/integration/install-bridge-conductor.integration.test.js build/integration/install-bridge-failure-guards.integration.test.js build/integration/learn-repository-pipeline.integration.test.js build/integration/visual-diff-mcp.integration.test.js build/integration/executor-mcp-provisioning.integration.test.js build/integration/executor-worktree-reentry.integration.test.js build/integration/sfcc-read-error-contract.integration.test.js build/integration/sfcc-response-envelope.integration.test.js build/integration/sfcc-total-error-contract.integration.test.js build/integration/serve-stdio.integration.test.js build/integration/install-alias.integration.test.js build/integration/setup-epic-validation.integration.test.js build/integration/plane-cli.integration.test.js",
|
|
32
|
+
"test:integration": "node --test build/conductor/conductor-runtime.integration.test.js build/integration/refresh-main.integration.test.js build/integration/command-provisioning.integration.test.js build/integration/start-tickets.integration.test.js build/integration/start-tickets-tier-handoff.integration.test.js build/integration/doctor.integration.test.js build/integration/agent-capabilities.integration.test.js build/integration/conductor-producer.integration.test.js build/integration/conductor-message-relay.integration.test.js build/integration/executor-http-runner.integration.test.js build/integration/executor-job-behaviors.integration.test.js build/integration/executor-recovery-jobs.integration.test.js build/integration/executor-spec-review-prompt.integration.test.js build/integration/resume-pre-spawn.git.integration.test.js build/integration/executor-containment.git.integration.test.js build/integration/conduct-epic-cli.integration.test.js build/integration/conduct-epic-corrections.integration.test.js build/integration/worker-finalization-origin.integration.test.js build/integration/post-remediation-merge-ci-wait.integration.test.js build/integration/executor-merge-supervision.integration.test.js build/integration/attachment-binary-roundtrip.integration.test.js build/integration/dependent-ticket-fresh-base.integration.test.js build/integration/execute-plan-instructions.integration.test.js build/integration/implement-ticket-finalization-order.integration.test.js build/integration/conductor-bundle-artifacts.integration.test.js build/integration/install-bridge-repo-resolution.integration.test.js build/integration/capability-report-contract.integration.test.js build/integration/request-brainstorm-general.integration.test.js build/integration/request-council-trigger-drop.integration.test.js build/integration/install-bridge-onboarding-launch.integration.test.js build/integration/install-bridge-conductor.integration.test.js build/integration/install-bridge-conductor-executor.integration.test.js build/integration/install-bridge-failure-guards.integration.test.js build/integration/learn-repository-pipeline.integration.test.js build/integration/visual-diff-mcp.integration.test.js build/integration/executor-mcp-provisioning.integration.test.js build/integration/executor-worktree-reentry.integration.test.js build/integration/sfcc-read-error-contract.integration.test.js build/integration/sfcc-read-surface.integration.test.js build/integration/sfcc-response-envelope.integration.test.js build/integration/sfcc-total-error-contract.integration.test.js build/integration/sfcc-target-boundary.integration.test.js build/integration/serve-stdio.integration.test.js build/integration/install-alias.integration.test.js build/integration/setup-epic-validation.integration.test.js build/integration/plane-cli.integration.test.js build/integration/executor-auth-retirement.integration.test.js build/integration/decision-implications-command-scaffold.test.js build/integration/strict-mcp-worker-contract.test.js build/integration/executor-adapter-flow.test.js build/integration/install-cli-contract.integration.test.js build/integration/inline-phase-state-tools.test.js",
|
|
33
33
|
"test:smoke": "node --test build/integration/packaged-cli-smoke.test.js",
|
|
34
34
|
"canary:agent-capabilities": "npm run build && node scripts/agent-capabilities-canary.mjs",
|
|
35
35
|
"prepublishOnly": "node scripts/bundle-assets.js && npm run build && node scripts/verify-shebang.cjs"
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"node": ">=18.0.0"
|
|
56
56
|
},
|
|
57
57
|
"overrides": {
|
|
58
|
-
"fast-uri": ">=4.1.
|
|
58
|
+
"fast-uri": ">=4.1.2",
|
|
59
59
|
"hono": ">=4.13.1",
|
|
60
60
|
"@hono/node-server": "^1.19.13",
|
|
61
61
|
"ip-address": ">=10.1.1"
|
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
/** Restrictive mode for the isolation directory itself (owner-only). */
|
|
2
|
-
export const ISOLATION_DIR_MODE = 0o700;
|
|
3
|
-
/** Owner-only mode for the seeded Bridge credential file. */
|
|
4
|
-
export const ISOLATION_FILE_MODE = 0o600;
|
|
5
|
-
/**
|
|
6
|
-
* Fixed name prefix for every directory this module creates. The startup sweep
|
|
7
|
-
* recognizes ONLY this prefix, which is what keeps it from deleting unrelated
|
|
8
|
-
* temporary directories that happen to share the parent.
|
|
9
|
-
*/
|
|
10
|
-
export const ISOLATION_DIR_PREFIX = "bapi-conductor-claude-cfg-";
|
|
11
|
-
/**
|
|
12
|
-
* Age past which the startup sweep may remove an orphan, deliberately far above
|
|
13
|
-
* any supported worker lifetime (the longest executor job timeout is measured in
|
|
14
|
-
* hours, not days). A directory younger than this may still belong to a LIVE
|
|
15
|
-
* worker owned by a concurrent executor process, so the threshold is the only
|
|
16
|
-
* thing preventing the sweep from deleting an in-use configuration.
|
|
17
|
-
*/
|
|
18
|
-
export const ISOLATION_ORPHAN_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
19
|
-
/** Platforms whose POSIX mode model supports the `0700` containment above. */
|
|
20
|
-
const SUPPORTED_PLATFORMS = ["darwin", "linux"];
|
|
21
|
-
/** Fixed remediation text per reason. Never includes a path or a value. */
|
|
22
|
-
const REASON_MESSAGES = {
|
|
23
|
-
"unsupported-platform": "worker config isolation is not supported on this platform (POSIX 0700/0600 containment is required)",
|
|
24
|
-
"unsupported-auth-layout": "no ANTHROPIC_API_KEY is available to the executor; an OAuth/subscription-only login does not " +
|
|
25
|
-
"follow an isolated CLAUDE_CONFIG_DIR, and copying operator credentials is not permitted",
|
|
26
|
-
"missing-filesystem-dependency": "the executor was constructed without the filesystem boundaries worker config isolation requires",
|
|
27
|
-
"unsafe-isolation-parent": "the isolation parent directory is a symbolic link or not a directory; refusing to create an " +
|
|
28
|
-
"isolated config directory beneath it",
|
|
29
|
-
"directory-creation-failed": "the isolated config directory could not be created or permissioned",
|
|
30
|
-
};
|
|
31
|
-
/**
|
|
32
|
-
* Render a capability failure as a bounded, secret-free sentence naming only the
|
|
33
|
-
* platform and the fixed reason — the exact text posted as an `error_message`.
|
|
34
|
-
*/
|
|
35
|
-
export function formatWorkerConfigIsolationReason(platform, reason) {
|
|
36
|
-
return `worker config isolation unavailable on ${platform}: ${REASON_MESSAGES[reason]}`;
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Decide whether isolation CAN be established here, without creating anything.
|
|
40
|
-
*
|
|
41
|
-
* Read-only by construction — this is what lets `doctor` reuse it (BAPI-731
|
|
42
|
-
* requirement 4) while keeping doctor's no-writes contract. It never reads the
|
|
43
|
-
* operator's `.claude.json`, never touches the credential store, and never
|
|
44
|
-
* returns a credential value: the auth check is presence-only.
|
|
45
|
-
*/
|
|
46
|
-
export function evaluateWorkerConfigIsolationCapability(deps) {
|
|
47
|
-
const platform = String(deps.platform);
|
|
48
|
-
if (!SUPPORTED_PLATFORMS.includes(deps.platform)) {
|
|
49
|
-
return { supported: false, platform, reason: "unsupported-platform" };
|
|
50
|
-
}
|
|
51
|
-
// Presence-only. The VALUE is never read into any result, log, or diagnostic.
|
|
52
|
-
const apiKey = deps.env?.ANTHROPIC_API_KEY;
|
|
53
|
-
if (typeof apiKey !== "string" || apiKey.trim().length === 0) {
|
|
54
|
-
return { supported: false, platform, reason: "unsupported-auth-layout" };
|
|
55
|
-
}
|
|
56
|
-
return { supported: true, platform, strategy: "api-key-env" };
|
|
57
|
-
}
|
|
58
|
-
function failure(platform, reason) {
|
|
59
|
-
return {
|
|
60
|
-
ok: false,
|
|
61
|
-
platform,
|
|
62
|
-
reason,
|
|
63
|
-
message: formatWorkerConfigIsolationReason(platform, reason),
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Create a private, owner-only configuration directory for ONE job attempt.
|
|
68
|
-
*
|
|
69
|
-
* The directory lives under the executor's temporary root — never inside the
|
|
70
|
-
* worktree, which would put executor-owned state into the diff the worker is
|
|
71
|
-
* about to commit and would be destroyed by the worktree's own reset/clean paths.
|
|
72
|
-
*
|
|
73
|
-
* Seeds NOTHING of the operator's Claude configuration. Inventory finding 3
|
|
74
|
-
* established that no trust entry is needed to keep a headless worker from
|
|
75
|
-
* prompting, and finding 4 established that the ANTHROPIC credential travels in
|
|
76
|
-
* the environment. No projected fragment of the operator's configuration is
|
|
77
|
-
* written, so none can get stale or over-grant.
|
|
78
|
-
*
|
|
79
|
-
* The single exception is {@link SeededBridgeCredential} — see that type for why
|
|
80
|
-
* an empty directory is NOT sufficient for the Bridge credential specifically:
|
|
81
|
-
* finding 4 does not extend to `BAPI_API_KEY`, which is denied from the worker
|
|
82
|
-
* environment on purpose, leaving the redirected store as its only route in.
|
|
83
|
-
*/
|
|
84
|
-
export async function establishWorkerConfigIsolation(deps, options = {}) {
|
|
85
|
-
const capability = evaluateWorkerConfigIsolationCapability(deps);
|
|
86
|
-
if (!capability.supported) {
|
|
87
|
-
return failure(capability.platform, capability.reason);
|
|
88
|
-
}
|
|
89
|
-
const platform = capability.platform;
|
|
90
|
-
let parent;
|
|
91
|
-
try {
|
|
92
|
-
parent = deps.tmpdir();
|
|
93
|
-
}
|
|
94
|
-
catch {
|
|
95
|
-
return failure(platform, "unsafe-isolation-parent");
|
|
96
|
-
}
|
|
97
|
-
if (typeof parent !== "string" || parent.length === 0) {
|
|
98
|
-
return failure(platform, "unsafe-isolation-parent");
|
|
99
|
-
}
|
|
100
|
-
// Reject a symlinked or non-directory parent BEFORE creating anything beneath
|
|
101
|
-
// it: `mkdtemp` under a symlink would silently place a credential-adjacent
|
|
102
|
-
// directory wherever the link points.
|
|
103
|
-
try {
|
|
104
|
-
const parentMeta = await deps.lstatPath(parent);
|
|
105
|
-
if (!parentMeta || parentMeta.isSymbolicLink || !parentMeta.isDirectory) {
|
|
106
|
-
return failure(platform, "unsafe-isolation-parent");
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
catch {
|
|
110
|
-
return failure(platform, "unsafe-isolation-parent");
|
|
111
|
-
}
|
|
112
|
-
// `mkdtemp` is atomic and collision-free, so two concurrent jobs on the same
|
|
113
|
-
// executor can never be handed the same directory.
|
|
114
|
-
let created;
|
|
115
|
-
try {
|
|
116
|
-
created = await deps.mkdtemp(deps.join(parent, ISOLATION_DIR_PREFIX));
|
|
117
|
-
if (typeof created !== "string" || created.length === 0) {
|
|
118
|
-
return failure(platform, "directory-creation-failed");
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
// The raw error may carry a host path; only the fixed reason escapes.
|
|
123
|
-
return failure(platform, "directory-creation-failed");
|
|
124
|
-
}
|
|
125
|
-
// Verify what we actually got is a real directory and not a link, then narrow
|
|
126
|
-
// it to owner-only. `mkdtemp` already creates at 0700 on POSIX; the explicit
|
|
127
|
-
// chmod makes the guarantee independent of the platform's umask behavior.
|
|
128
|
-
try {
|
|
129
|
-
const meta = await deps.lstatPath(created);
|
|
130
|
-
if (!meta || meta.isSymbolicLink || !meta.isDirectory) {
|
|
131
|
-
await safeDispose(deps, created);
|
|
132
|
-
return failure(platform, "unsafe-isolation-parent");
|
|
133
|
-
}
|
|
134
|
-
await deps.chmod(created, ISOLATION_DIR_MODE);
|
|
135
|
-
}
|
|
136
|
-
catch {
|
|
137
|
-
await safeDispose(deps, created);
|
|
138
|
-
return failure(platform, "directory-creation-failed");
|
|
139
|
-
}
|
|
140
|
-
// The isolated XDG root lives INSIDE the disposable directory, so a single
|
|
141
|
-
// recursive removal reclaims both and no second lifetime needs tracking.
|
|
142
|
-
const xdgConfigHome = deps.join(created, "xdg");
|
|
143
|
-
try {
|
|
144
|
-
await deps.mkdir(xdgConfigHome, { recursive: true });
|
|
145
|
-
await deps.chmod(xdgConfigHome, ISOLATION_DIR_MODE);
|
|
146
|
-
}
|
|
147
|
-
catch {
|
|
148
|
-
await safeDispose(deps, created);
|
|
149
|
-
return failure(platform, "directory-creation-failed");
|
|
150
|
-
}
|
|
151
|
-
// Seed the one credential the redirect would otherwise strip. Fail-CLOSED,
|
|
152
|
-
// matching this module's posture: a worker that cannot reach its Bridge MCP
|
|
153
|
-
// tools cannot do the job it was dispatched for, and the failure mode is a
|
|
154
|
-
// clean exit that looks like success. Refusing to spawn is strictly better
|
|
155
|
-
// than spawning a worker guaranteed to accomplish nothing.
|
|
156
|
-
const credential = options.bridgeCredential;
|
|
157
|
-
if (credential) {
|
|
158
|
-
if (!deps.writeFile) {
|
|
159
|
-
await safeDispose(deps, created);
|
|
160
|
-
return failure(platform, "missing-filesystem-dependency");
|
|
161
|
-
}
|
|
162
|
-
try {
|
|
163
|
-
const bridgeDir = deps.join(xdgConfigHome, "bridge");
|
|
164
|
-
await deps.mkdir(bridgeDir, { recursive: true });
|
|
165
|
-
await deps.chmod(bridgeDir, ISOLATION_DIR_MODE);
|
|
166
|
-
const storePath = deps.join(bridgeDir, "credentials.json");
|
|
167
|
-
await deps.writeFile(storePath,
|
|
168
|
-
// Exactly the shape resolveCredentialBundle reads: a logical target
|
|
169
|
-
// mapping to its named secret values.
|
|
170
|
-
`${JSON.stringify({ [`bapi:${credential.repoName}`]: { BAPI_API_KEY: credential.apiKey } }, null, 2)}\n`);
|
|
171
|
-
// Narrow AFTER writing: the content is secret, and a permissive window
|
|
172
|
-
// between create and chmod is the whole risk being closed here.
|
|
173
|
-
await deps.chmod(storePath, ISOLATION_FILE_MODE);
|
|
174
|
-
}
|
|
175
|
-
catch {
|
|
176
|
-
// The raw error may carry a host path or the secret; only the fixed
|
|
177
|
-
// reason escapes.
|
|
178
|
-
await safeDispose(deps, created);
|
|
179
|
-
return failure(platform, "directory-creation-failed");
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
let disposed = false;
|
|
183
|
-
return {
|
|
184
|
-
ok: true,
|
|
185
|
-
isolation: {
|
|
186
|
-
claudeConfigDir: created,
|
|
187
|
-
xdgConfigHome,
|
|
188
|
-
async dispose() {
|
|
189
|
-
if (disposed)
|
|
190
|
-
return null;
|
|
191
|
-
disposed = true;
|
|
192
|
-
try {
|
|
193
|
-
await deps.rmRecursive(created);
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
196
|
-
catch {
|
|
197
|
-
// Best-effort by contract: cleanup failure must never replace an
|
|
198
|
-
// already-determined job outcome, and the raw error never escapes.
|
|
199
|
-
return "worker config isolation directory could not be removed";
|
|
200
|
-
}
|
|
201
|
-
},
|
|
202
|
-
},
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
/** Remove a partially-created directory, swallowing every failure. */
|
|
206
|
-
async function safeDispose(deps, path) {
|
|
207
|
-
try {
|
|
208
|
-
await deps.rmRecursive(path);
|
|
209
|
-
}
|
|
210
|
-
catch {
|
|
211
|
-
/* best-effort */
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* Remove stale isolation directories left behind by an ungraceful executor exit
|
|
216
|
-
* (SIGKILL, OOM, host reboot) where per-job disposal never ran.
|
|
217
|
-
*
|
|
218
|
-
* Deliberately conservative, because this function deletes directories: it looks
|
|
219
|
-
* only inside the executor's own temporary root, recognizes only entries
|
|
220
|
-
* carrying {@link ISOLATION_DIR_PREFIX}, requires NON-following metadata to say
|
|
221
|
-
* the entry is a real directory (so a symlink is skipped rather than followed),
|
|
222
|
-
* and requires an age beyond {@link ISOLATION_ORPHAN_MAX_AGE_MS} so a live
|
|
223
|
-
* concurrent executor's active job directory is never removed.
|
|
224
|
-
*
|
|
225
|
-
* Never throws: startup must proceed even if the sweep cannot run at all.
|
|
226
|
-
*/
|
|
227
|
-
export async function sweepOrphanedWorkerConfigDirectories(deps) {
|
|
228
|
-
const result = { removed: 0, skipped: 0, diagnostics: [] };
|
|
229
|
-
const maxAgeMs = typeof deps.maxAgeMs === "number" ? deps.maxAgeMs : ISOLATION_ORPHAN_MAX_AGE_MS;
|
|
230
|
-
let parent;
|
|
231
|
-
try {
|
|
232
|
-
parent = deps.tmpdir();
|
|
233
|
-
if (typeof parent !== "string" || parent.length === 0) {
|
|
234
|
-
result.diagnostics.push("worker config isolation sweep skipped: no temporary root");
|
|
235
|
-
return result;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
catch {
|
|
239
|
-
result.diagnostics.push("worker config isolation sweep skipped: no temporary root");
|
|
240
|
-
return result;
|
|
241
|
-
}
|
|
242
|
-
let entries;
|
|
243
|
-
try {
|
|
244
|
-
entries = await deps.readdir(parent);
|
|
245
|
-
}
|
|
246
|
-
catch {
|
|
247
|
-
result.diagnostics.push("worker config isolation sweep skipped: temporary root unreadable");
|
|
248
|
-
return result;
|
|
249
|
-
}
|
|
250
|
-
const cutoff = deps.now() - maxAgeMs;
|
|
251
|
-
for (const entry of entries) {
|
|
252
|
-
// Prefix match FIRST: anything the module did not create is not ours to
|
|
253
|
-
// delete, whatever its age.
|
|
254
|
-
if (typeof entry !== "string" || !entry.startsWith(ISOLATION_DIR_PREFIX)) {
|
|
255
|
-
result.skipped += 1;
|
|
256
|
-
continue;
|
|
257
|
-
}
|
|
258
|
-
const candidate = deps.join(parent, entry);
|
|
259
|
-
let meta;
|
|
260
|
-
try {
|
|
261
|
-
meta = await deps.lstatPath(candidate);
|
|
262
|
-
}
|
|
263
|
-
catch {
|
|
264
|
-
result.skipped += 1;
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
// A symlink is skipped, never followed — removing it recursively could
|
|
268
|
-
// delete whatever it points at.
|
|
269
|
-
if (!meta || meta.isSymbolicLink || !meta.isDirectory) {
|
|
270
|
-
result.skipped += 1;
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
273
|
-
if (!(meta.mtimeMs < cutoff)) {
|
|
274
|
-
result.skipped += 1;
|
|
275
|
-
continue;
|
|
276
|
-
}
|
|
277
|
-
try {
|
|
278
|
-
await deps.rmRecursive(candidate);
|
|
279
|
-
result.removed += 1;
|
|
280
|
-
}
|
|
281
|
-
catch {
|
|
282
|
-
result.skipped += 1;
|
|
283
|
-
result.diagnostics.push("worker config isolation sweep could not remove a stale directory");
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
return result;
|
|
287
|
-
}
|