@bridge_gpt/mcp-server 0.2.51 → 0.2.53
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 +59 -13
- package/build/agent-capabilities/probe-context.js +15 -7
- package/build/agent-capabilities/probes.js +42 -6
- package/build/agent-launchers/claude-executor-adapter.js +98 -14
- package/build/commands.generated.js +7 -5
- package/build/conduct-epic/cut-protocol.js +17 -3
- package/build/conductor/bridge-api-client.js +232 -5
- package/build/conductor/cli.js +23 -0
- package/build/conductor/deny-enforcement-preflight.js +107 -10
- package/build/conductor/doctor.js +428 -5
- package/build/conductor/install-doctor.js +65 -656
- package/build/conductor/local-merge.js +170 -11
- package/build/conductor/readiness-cli.js +152 -0
- package/build/conductor/readiness-sections.js +666 -0
- package/build/conductor/readiness.js +710 -0
- package/build/conductor/tools.js +56 -3
- package/build/conductor-bin.js +21 -17
- package/build/connect-bitbucket-api.js +370 -0
- package/build/connect-bitbucket.js +437 -0
- package/build/docs.generated.js +1 -1
- package/build/doctor.js +40 -1
- package/build/drive-epic.js +423 -11
- package/build/env-file-link.js +164 -0
- package/build/epic-integration-pr.js +10 -0
- package/build/executor/cli.js +41 -6
- package/build/executor/deps.js +5 -1
- package/build/executor/env-file-guard.js +113 -0
- package/build/executor/env.js +78 -1
- package/build/executor/heartbeat.js +9 -0
- package/build/executor/http-client.js +90 -22
- package/build/executor/job-errors.js +43 -2
- package/build/executor/job-runner.js +130 -28
- package/build/executor/merge-job.js +67 -16
- package/build/executor/permissions.js +106 -0
- package/build/executor/preflight.js +38 -13
- package/build/executor/resume-pre-spawn.js +2 -1
- package/build/executor/runner.js +175 -4
- package/build/executor/service-unit.js +15 -0
- package/build/executor/terminal-mutation.js +22 -1
- package/build/executor/types.js +86 -0
- package/build/executor/worker-command.js +21 -5
- package/build/executor/worker-guard-hook.js +939 -0
- package/build/executor/worker-log.js +56 -0
- package/build/executor/worktree.js +11 -0
- package/build/git-reachability.js +147 -0
- package/build/index.js +4734 -4270
- package/build/install-bridge.js +95 -0
- package/build/install-doctor.js +154 -2
- package/build/pipelines.generated.js +6 -4
- package/build/plan-epic-conductor-eligibility.js +37 -7
- package/build/plane/alembic-head.js +40 -11
- package/build/plane/build-freshness.js +22 -11
- package/build/plane/cli.js +78 -15
- package/build/plane/defaults.js +165 -0
- package/build/plane/manifest.js +63 -8
- package/build/plane/member-logs.js +6 -0
- package/build/plane/member-roster.js +195 -11
- package/build/plane/preflight.js +402 -44
- package/build/plane/shutdown.js +25 -3
- package/build/plane/status.js +11 -0
- package/build/plane/supervisor.js +343 -14
- package/build/plane/test-fakes.js +43 -0
- package/build/plane/types.js +118 -11
- package/build/pr-base-contract.js +20 -0
- package/build/readiness-check.js +412 -0
- package/build/readme.generated.js +1 -1
- package/build/review-synthesis-config.js +60 -0
- package/build/scripts/executor-protocol-contract-driver.js +311 -0
- package/build/setup-epic.js +560 -139
- package/build/sfcc/log-query.js +2 -1
- package/build/start-tickets-conductor.js +11 -2
- package/build/start-tickets.js +69 -2
- package/build/version.generated.js +3 -3
- package/build/worker-containment-diagnostic.js +97 -0
- package/build/worker-guard-hook-bin.js +6 -0
- package/docs/CONDUCTOR.md +27 -0
- package/docs/install/mcp-tool-integrations.md +3 -2
- package/package.json +4 -3
- package/pipelines/{full-automation.json → idea-to-pr.json} +1 -1
package/README.md
CHANGED
|
@@ -352,7 +352,7 @@ later boot.
|
|
|
352
352
|
"mcpServers": {
|
|
353
353
|
"bridge": {
|
|
354
354
|
"command": "npx",
|
|
355
|
-
"args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.
|
|
355
|
+
"args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.53", "serve"],
|
|
356
356
|
"env": {
|
|
357
357
|
"BAPI_BASE_URL": "https://bridgegpt-api.com",
|
|
358
358
|
"BAPI_REPO_NAME": "your-repo",
|
|
@@ -374,7 +374,7 @@ later boot.
|
|
|
374
374
|
"bridge": {
|
|
375
375
|
"type": "stdio",
|
|
376
376
|
"command": "npx",
|
|
377
|
-
"args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.
|
|
377
|
+
"args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.53", "serve"],
|
|
378
378
|
"env": {
|
|
379
379
|
"BAPI_BASE_URL": "https://bridgegpt-api.com",
|
|
380
380
|
"BAPI_REPO_NAME": "your-repo",
|
|
@@ -396,7 +396,7 @@ later boot.
|
|
|
396
396
|
"bridge": {
|
|
397
397
|
"type": "stdio",
|
|
398
398
|
"command": "npx",
|
|
399
|
-
"args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.
|
|
399
|
+
"args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.53", "serve"],
|
|
400
400
|
"env": {
|
|
401
401
|
"BAPI_BASE_URL": "https://bridgegpt-api.com",
|
|
402
402
|
"BAPI_REPO_NAME": "your-repo",
|
|
@@ -421,7 +421,7 @@ you select `copilot-cli`; the shape below is what it produces.
|
|
|
421
421
|
"bridge": {
|
|
422
422
|
"type": "local",
|
|
423
423
|
"command": "npx",
|
|
424
|
-
"args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.
|
|
424
|
+
"args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.53", "serve"],
|
|
425
425
|
"tools": ["*"],
|
|
426
426
|
"env": {
|
|
427
427
|
"BAPI_BASE_URL": "https://bridgegpt-api.com",
|
|
@@ -445,7 +445,7 @@ Windsurf only supports global MCP configuration.
|
|
|
445
445
|
"mcpServers": {
|
|
446
446
|
"bridge": {
|
|
447
447
|
"command": "npx",
|
|
448
|
-
"args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.
|
|
448
|
+
"args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.53", "serve"],
|
|
449
449
|
"env": {
|
|
450
450
|
"BAPI_BASE_URL": "https://bridgegpt-api.com",
|
|
451
451
|
"BAPI_REPO_NAME": "your-repo",
|
|
@@ -464,7 +464,7 @@ Windsurf only supports global MCP configuration.
|
|
|
464
464
|
```toml
|
|
465
465
|
[mcp_servers.bridge]
|
|
466
466
|
command = "npx"
|
|
467
|
-
args = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.
|
|
467
|
+
args = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.53", "serve"]
|
|
468
468
|
|
|
469
469
|
[mcp_servers.bridge.env]
|
|
470
470
|
BAPI_BASE_URL = "https://bridgegpt-api.com"
|
|
@@ -570,7 +570,7 @@ The tools worth knowing for most tickets.
|
|
|
570
570
|
**1. Review Ticket**
|
|
571
571
|
- **What it does:** Runs a full quality review of a ticket: clarifying questions and a critique plus an alternate-model second opinion, then evaluates the findings and produces a decision page for accepting or rejecting them.
|
|
572
572
|
- **When it's useful:** (Refinement) Right after a ticket is drafted, before anyone starts building — to surface gaps and tighten it.
|
|
573
|
-
- **How to use it:** `/review-
|
|
573
|
+
- **How to use it:** `/review-tickets BAPI-123` (command only — "review" as free text is easily mistaken for a freehand agent review). A single key reviews inline in the current session — no terminal tab, no worktree. For several tickets at once, `/review-tickets BAPI-123 BAPI-456` opens one terminal tab per ticket and reviews them in parallel with no worktrees; every review flag applies, and `--review KEY=auto,rounds=N` sets per-ticket overrides.
|
|
574
574
|
- **Flags:** `--auto` auto-accept findings and skip the approval gates · `--rounds=1` a cheaper single-pass review that still evaluates findings and captures decisions · `--rounds=2` force the full second-opinion review · omit `--rounds` to let the difficulty-adaptive review policy decide.
|
|
575
575
|
|
|
576
576
|
**2. Start Tickets**
|
|
@@ -581,7 +581,7 @@ The tools worth knowing for most tickets.
|
|
|
581
581
|
|
|
582
582
|
**3. Review and Start**
|
|
583
583
|
- **What it does:** Spawns one worktree per ticket; each session reviews the ticket inline and, after a per-ticket proceed/halt gate, hands off to a **fresh implementation session** that reuses the same worktree — review and implementation run in two separate agent contexts, not one shared session.
|
|
584
|
-
- **When it's useful:** (Refinement | Implementation | Automation) The **recommended front door** for "review these tickets, then implement the ones that pass," starting from existing ticket keys (unlike `/
|
|
584
|
+
- **When it's useful:** (Refinement | Implementation | Automation) The **recommended front door** for "review these tickets, then implement the ones that pass," starting from existing ticket keys (unlike `/idea-to-pr`, which only accepts an idea).
|
|
585
585
|
- **How to use it:** `/review-and-start BAPI-248 BAPI-250` (single or multiple keys flow through the identical code path).
|
|
586
586
|
- **Flags:** `--auto` auto-approves both the review and the implementation phase of every spawned session · `--rounds=1|2` forwarded to the review phase · `--agent`, `--base-branch`, `--max-parallel`, `--dry-run` mirror `/start-tickets`.
|
|
587
587
|
|
|
@@ -637,6 +637,12 @@ Good to know, but not needed every day.
|
|
|
637
637
|
- **How to use it:** Ask your agent — "Generate an image of a dashboard showing SOC2 evidence freshness as a traffic-light grid."
|
|
638
638
|
- **Flags:** `provider` openai (`gpt-image-2`) / gemini (Imagen, which adds an invisible SynthID watermark) · `quality` low (default, cheapest) / medium / high · `size` 1024x1024 / 1024x1536 / 1536x1024. The image is saved to `BAPI_DOCS_DIR/images/` and also returned inline.
|
|
639
639
|
|
|
640
|
+
**6. Create Decision Page**
|
|
641
|
+
- **What it does:** Turns open decisions from the current conversation into an interactive HTML decision page, then folds the submitted answers back into the conversation.
|
|
642
|
+
- **When it's useful:** (Refinement) Use it whenever a conversation has accumulated more open choices than are comfortable to settle in prose — not for a single simple question, which is faster to just ask directly.
|
|
643
|
+
- **How to use it:** `/create-decision-page <topic or open questions>` — e.g. *"how we handle rate limiting"* — or leave it empty to frame the open choices already raised earlier in the conversation.
|
|
644
|
+
- **Flags:** None.
|
|
645
|
+
|
|
640
646
|
### Now and then
|
|
641
647
|
|
|
642
648
|
Useful once in a while.
|
|
@@ -688,11 +694,35 @@ Useful once in a while.
|
|
|
688
694
|
- **When it's useful:** (Architecture | Refinement) When you need a sizing pass across an epic, or across a set of tickets you name explicitly.
|
|
689
695
|
- **How to use it:** `/estimate-epic BAPI-123`
|
|
690
696
|
- **Flags:** Pass an epic key, or an explicit list of ticket keys to estimate as one group.
|
|
697
|
+
|
|
698
|
+
**9. Regression Check**
|
|
699
|
+
- **What it does:** Runs the deterministic regression-reviewer (lightweight mode) against a proposed code change and reports its blast radius — which real call-sites, tests, mocks, or config the change does and doesn't account for.
|
|
700
|
+
- **When it's useful:** (Correctness) Before merging a change, to see which real callers, tests, or config it leaves unaccounted for.
|
|
701
|
+
- **How to use it:** `/regression-check` reviews the working tree against `HEAD` by default; `/regression-check --diff main...HEAD` reviews an explicit range.
|
|
702
|
+
- **Flags:** `--diff <range>` review a specific diff/PR range instead of the working tree vs. `HEAD` · `--symbols a,b,c` review a not-yet-diffed planned change by naming its symbols directly.
|
|
703
|
+
|
|
704
|
+
**10. Scan Test Coverage**
|
|
705
|
+
- **What it does:** Scans recently shipped tickets from git history and reports which features have or could gain integration tests, and which can only be smoke tested.
|
|
706
|
+
- **When it's useful:** (Testing) Periodically, to find recently shipped features still missing integration-test coverage and how each could be smoke tested instead.
|
|
707
|
+
- **How to use it:** `/scan-test-coverage` runs incrementally from the last recorded marker in `docs/test-coverage/STATE.md`.
|
|
708
|
+
- **Flags:** `--since=YYYY-MM-DD` override the window start date · `--full` ignore the marker and use a 6-month lookback · `--limit=N` cap the number of features investigated.
|
|
709
|
+
|
|
710
|
+
**11. Estimate Ticket**
|
|
711
|
+
- **What it does:** One-ticket shortcut over the existing epic estimation capability: estimates a single Jira ticket via the same `estimate_epic` tool.
|
|
712
|
+
- **When it's useful:** (Estimation) When you want one ticket's estimate without typing out an epic key or an explicit ticket list.
|
|
713
|
+
- **How to use it:** `/estimate-ticket BAPI-123`
|
|
714
|
+
- **Flags:** `--allow-partial` the sole optional flag.
|
|
715
|
+
|
|
716
|
+
**12. Draft Ticket**
|
|
717
|
+
- **What it does:** Delegates to the Jira Ticket Writer to create only a local Markdown draft — it does not create or update a Jira issue.
|
|
718
|
+
- **When it's useful:** (Refinement) When you want a reviewable local ticket draft before committing to an upload workflow.
|
|
719
|
+
- **How to use it:** `/draft-ticket <description>`
|
|
720
|
+
- **Flags:** None.
|
|
691
721
|
<!-- END GENERATED: mcp-tool-documentation -->
|
|
692
722
|
|
|
693
723
|
### Workflow commands
|
|
694
724
|
|
|
695
|
-
Slash commands that drive several tools at once. Start Tickets, Review and Start, and Explore Ticket are documented above under [Regularly useful](#regularly-useful) — the rest live here.
|
|
725
|
+
Slash commands that drive several tools at once. Start Tickets, Review and Start, and Explore Ticket are documented above under [Regularly useful](#regularly-useful); Create Decision Page is documented above under [Occasionally useful](#occasionally-useful); Regression Check and Scan Test Coverage are documented above under [Now and then](#now-and-then) — the rest live here.
|
|
696
726
|
|
|
697
727
|
**1. Implement Ticket**
|
|
698
728
|
- **What it does:** Full build for one ticket: generate a plan, write the code, commit, open a PR, and monitor CI.
|
|
@@ -991,7 +1021,7 @@ Each `KEY` must match `[A-Z]+-[0-9]+` (e.g., `BAPI-248`). The CLI creates/switch
|
|
|
991
1021
|
npx -y @bridge_gpt/mcp-server start-tickets --agent cursor-agent BAPI-248
|
|
992
1022
|
```
|
|
993
1023
|
|
|
994
|
-
**Difficulty-based model routing.** Before launching each agent, the CLI selects an implementation **model tier** from the ticket's `difficulty` (1-2 → cheap, 3-
|
|
1024
|
+
**Difficulty-based model routing.** Before launching each agent, the CLI selects an implementation **model tier** from the ticket's `difficulty` (1-2 → cheap, 3-6 → basic, 7-10 → premium) and injects it as a `--model` flag at the spawn boundary. The Python backend returns only the coarse tier (`GET /jira/tickets/{KEY}/model-tier`, computing + caching difficulty on demand); this CLI alone maps a tier to the agent-specific alias (`claude`: `haiku`/`sonnet`/`opus`; `cursor-agent`: version-suffixed strings validated against `cursor-agent --list-models`). It is gated per repo by `difficulty_model_routing_enabled` (default **ON**) with an optional `difficulty_model_tier_overrides` JSON map (tier → alias). Routing is **fail-open**: missing credentials, an evaluation failure/timeout, a backend `fallback`, an invalid/unavailable alias, an unadvertised Cursor model, or an agent without `--model` support all default to the **premium** (Opus) model — and when even the premium alias cannot be resolved/validated for the agent, `--model` is omitted (the agent uses its default) — each surfaced as a per-ticket warning rather than failing the spawn. `--dry-run` does **not** create worktrees or open tabs, but it **does** resolve routing read-only to preview the `--model` each tab would use.
|
|
995
1025
|
|
|
996
1026
|
**Coarse `--tier` override.** Passing `--tier cheap|basic|premium` bypasses **only** the per-ticket difficulty lookup above and applies that one tier to every ticket; the tier is still resolved to an alias through the same agent registry + `difficulty_model_tier_overrides` and validated the same way (including the live `cursor-agent --list-models` check). It is never treated as a raw `--model` alias, and no API key or credential belongs in the spawned command (the CLI resolves credentials itself via `resolveBapiCredentials`). A malformed/unrecognized `--tier` value is **fail-open**: the CLI logs one concise warning and routes every ticket on the premium (Opus) fallback rather than aborting. `/review-and-implement` uses this flag to hand its review-time tier snapshot to the fresh implementation session.
|
|
997
1027
|
|
|
@@ -1059,6 +1089,10 @@ unauthorized, or malformed — it escalates and prints no conductor invocation a
|
|
|
1059
1089
|
all, because an unknown owner is not the same as a not-ready one. No branch,
|
|
1060
1090
|
including every error path, ever offers you two paths.
|
|
1061
1091
|
|
|
1092
|
+
`drive-epic` owns no branch behavior of its own — it forwards `--feature-branch
|
|
1093
|
+
<name>` and `--into-base` verbatim to `setup-epic`, which decides the strategy (see
|
|
1094
|
+
below). With neither flag, a multi-ticket epic runs on `epic/<KEY>`.
|
|
1095
|
+
|
|
1062
1096
|
Two conductors is a transitional state. When one is eliminated, `drive-epic` is
|
|
1063
1097
|
the only thing that changes.
|
|
1064
1098
|
|
|
@@ -1072,6 +1106,18 @@ npx -y @bridge_gpt/mcp-server setup-epic --epic-key <KEY> --plan-file <path-to-e
|
|
|
1072
1106
|
|
|
1073
1107
|
The plan sidecar is produced by the `decompose-epic` pipeline step. `setup-epic` validates it locally (unique ticket keys, resolvable dependency references, acyclicity) before sending anything, so a malformed plan fails legibly instead of as a bare HTTP 400. It is **idempotent**: re-running it on an epic that already has a live run reuses that run rather than minting a second one. `--dry-run` validates and previews the calls without mutating anything; `--json` emits a single machine-readable result object.
|
|
1074
1108
|
|
|
1109
|
+
**The command above uses an epic branch.** For a plan with **two or more** nodes and
|
|
1110
|
+
no branch flag, `setup-epic` derives `epic/<KEY>`: every child-ticket PR targets that
|
|
1111
|
+
branch, and one draft integration PR targets the repository base branch. This holds on
|
|
1112
|
+
every path, CI and `--json` included. A single-node plan keeps the repository base
|
|
1113
|
+
branch, matching plain `start-tickets`.
|
|
1114
|
+
|
|
1115
|
+
> **Behavior change (BAPI-1009).** The same no-flag command previously ran children
|
|
1116
|
+
> directly into the repository base branch. **Pass `--into-base` to restore that**, if
|
|
1117
|
+
> your automation depends on it. Pass `--feature-branch <name>` to override the derived
|
|
1118
|
+
> name, or to force a branch for a single-node plan. Passing both is a parse error,
|
|
1119
|
+
> refused before any file read, credential resolution, or network call.
|
|
1120
|
+
|
|
1075
1121
|
Once the plan is approved, the **server-side reconciler** picks the run up within ~30s. To execute claimed jobs on your machine, run `executor`:
|
|
1076
1122
|
|
|
1077
1123
|
```
|
|
@@ -1441,7 +1487,7 @@ The full surface, for when you need the complete enumeration. Day-to-day, use [U
|
|
|
1441
1487
|
|
|
1442
1488
|
### MCP tools
|
|
1443
1489
|
|
|
1444
|
-
The authoritative tool catalog covers **
|
|
1490
|
+
The authoritative tool catalog covers **73 tools** (enumerated below). What's actually registered in a session depends on `BRIDGE_MCP_PROFILE`: the default `core` profile loads a trimmed subset, and the conductor/pipeline-authoring/SFCC tools are added only under their respective profiles (see [Environment Variables](#environment-variables)). Async AI tools follow a request/get pattern: call the `request_*` tool to kick off generation, then the matching `get_*` tool to retrieve the result (or pass `wait_for_result: true` to poll automatically).
|
|
1445
1491
|
|
|
1446
1492
|
- **Connectivity & identity** — `ping` (its JSON also carries `docs_dir`, `role`, and `customer_type`)
|
|
1447
1493
|
- **Team & access** — `invite_member` (admin-only; mints a scoped access key for a teammate on an already-configured project — the plaintext key is shown exactly once)
|
|
@@ -1462,10 +1508,10 @@ Pipelines are declarative, multi-step workflows your AI agent executes step-by-s
|
|
|
1462
1508
|
| Pipeline | Description | Invoke with |
|
|
1463
1509
|
|---|---|---|
|
|
1464
1510
|
| `implement-ticket` | Generate a plan, execute the implementation, commit, open a PR, and monitor CI | `/implement-ticket PROJ-123` |
|
|
1465
|
-
| `review-ticket` | Full ticket quality review: clarifying questions + critique plus an automatic second-opinion pass, then evaluation and decision capture. The backend owns round orchestration; pass `--rounds=1` for a single-pass review or `--rounds=2` to force the full second-opinion review, or omit `--rounds` to let the backend decide adaptively. | `/review-
|
|
1511
|
+
| `review-ticket` | Full ticket quality review: clarifying questions + critique plus an automatic second-opinion pass, then evaluation and decision capture. The backend owns round orchestration; pass `--rounds=1` for a single-pass review or `--rounds=2` to force the full second-opinion review, or omit `--rounds` to let the backend decide adaptively. Internal atomic pipeline invoked by `/review-tickets` (inline for one key, spawned per tab for several) and by `/review-and-implement`, `/review-and-start`, `/start-tickets`, and `/conduct-epic`. | `/review-tickets PROJ-123` |
|
|
1466
1512
|
| `idea-to-ticket` | Turn an idea into a Jira Task/Spike (or Epic + children) with research, dedup, and critique | `/idea-to-ticket "<idea>"` |
|
|
1467
1513
|
| `plan-epic` | Decompose an epic into sub-tasks with a structured exploration doc for each | `/plan-epic "<epic>"` |
|
|
1468
|
-
| `full-automation` | Client-side wrapper: runs the `idea-to-ticket` recipe, then hands the created keys to `/review-and-start --auto` | `/
|
|
1514
|
+
| `full-automation` | Client-side wrapper: runs the `idea-to-ticket` recipe, then hands the created keys to `/review-and-start --auto` | `/idea-to-pr "<idea>"` |
|
|
1469
1515
|
| `learn-repository` | Analyze codebase architecture, testing, review, and documentation standards, then upload to Bridge | `/learn-repository` |
|
|
1470
1516
|
|
|
1471
1517
|
### Pipeline response envelope
|
|
@@ -20,8 +20,10 @@ import { DEFAULT_PROBE_TIMEOUT_MS, } from "./types.js";
|
|
|
20
20
|
* `--trust` is MANDATORY headless or the workspace-trust prompt hangs; cursor has
|
|
21
21
|
* its own `--workspace` cwd flag. All `OutputFormat` values (including `stream-json`)
|
|
22
22
|
* pass through the generic `--output-format <fmt>` slot.
|
|
23
|
-
* - claude: `-p [--dangerously-skip-permissions]
|
|
24
|
-
* stream-json --verbose] <prompt>`.
|
|
23
|
+
* - claude: `-p [--permission-mode auto|acceptEdits | --dangerously-skip-permissions]
|
|
24
|
+
* [--model <m>] [--output-format json|stream-json --verbose] <prompt>`. The
|
|
25
|
+
* worker default is `--permission-mode auto` (BAPI-1020); bypass is emitted only
|
|
26
|
+
* for the explicit revert posture and for the probe that must still measure it. Claude has NO working-dir flag — cwd is set via
|
|
25
27
|
* the spawn options, never an argument. `stream-json` REQUIRES `--verbose`. The
|
|
26
28
|
* prompt is ALWAYS the final argv element so mocked prompt extraction stays valid.
|
|
27
29
|
*/
|
|
@@ -32,11 +34,17 @@ export function buildHeadlessArgs(agentName, opts) {
|
|
|
32
34
|
}
|
|
33
35
|
// claude (and any positional-prompt agent without a cwd flag)
|
|
34
36
|
const args = ["-p"];
|
|
35
|
-
// BAPI-725: exactly ONE permission form, matching the
|
|
36
|
-
// adapter's argv builder
|
|
37
|
-
// `permissionMode` wins when
|
|
38
|
-
//
|
|
39
|
-
|
|
37
|
+
// BAPI-725, widened by BAPI-1020: exactly ONE permission form, matching the
|
|
38
|
+
// Claude executor adapter's argv builder
|
|
39
|
+
// (`agent-launchers/claude-executor-adapter.ts`). `permissionMode` wins when
|
|
40
|
+
// both are supplied — emitting both would let the skip flag take effect and the
|
|
41
|
+
// probe would report on a posture it never ran. That precedence is UNCHANGED;
|
|
42
|
+
// only the set of accepted modes grew, so a probe can now measure the `auto`
|
|
43
|
+
// posture every worker actually runs under.
|
|
44
|
+
if (opts.permissionMode === "auto") {
|
|
45
|
+
args.push("--permission-mode", "auto");
|
|
46
|
+
}
|
|
47
|
+
else if (opts.permissionMode === "acceptEdits") {
|
|
40
48
|
args.push("--permission-mode", "acceptEdits");
|
|
41
49
|
}
|
|
42
50
|
else if (opts.skipPermissions === true) {
|
|
@@ -419,10 +419,15 @@ function evaluateDenyRun(run, allowedMarker) {
|
|
|
419
419
|
export async function runDenyEnforcementCheck(ctx, opts) {
|
|
420
420
|
const marker = ctx.marker("DENY_ALLOWED");
|
|
421
421
|
const prompt = denyProbePrompt();
|
|
422
|
-
|
|
422
|
+
// Exactly ONE permission form per posture, mirroring the adapter's argv map.
|
|
423
|
+
const permissionMode = opts?.permissionPosture === "auto"
|
|
424
|
+
? "auto"
|
|
425
|
+
: opts?.permissionPosture === "accept_edits"
|
|
426
|
+
? "acceptEdits"
|
|
427
|
+
: undefined;
|
|
423
428
|
const runOpts = {
|
|
424
|
-
skipPermissions:
|
|
425
|
-
permissionMode
|
|
429
|
+
skipPermissions: permissionMode === undefined,
|
|
430
|
+
permissionMode,
|
|
426
431
|
model: opts?.model,
|
|
427
432
|
timeoutMs: opts?.timeoutMs,
|
|
428
433
|
};
|
|
@@ -493,7 +498,8 @@ export async function runDenyEnforcementCheck(ctx, opts) {
|
|
|
493
498
|
return {
|
|
494
499
|
result: withDenyLayer({
|
|
495
500
|
status: "fail",
|
|
496
|
-
detail: "deny is NOT enforced for this installed Claude version under
|
|
501
|
+
detail: "deny is NOT enforced for this installed Claude version under the probed " +
|
|
502
|
+
`permission posture (${opts?.permissionPosture ?? "skip_permissions"}) ` +
|
|
497
503
|
"(neither settings permissions.deny nor a PreToolUse hook rejected the denied call)",
|
|
498
504
|
elapsedMs: hookRun.elapsedMs,
|
|
499
505
|
evidence: h.evidence,
|
|
@@ -507,8 +513,10 @@ const denyEnforcement = {
|
|
|
507
513
|
description: "Runs a no-deny control run first (proving the agent actually attempts the canary call), then spawns " +
|
|
508
514
|
"claude headless with --dangerously-skip-permissions in a disposable temp project carrying a " +
|
|
509
515
|
"permissions.deny rule, attempts the denied call, and asserts it is rejected — reporting which layer " +
|
|
510
|
-
"enforced (settings-deny vs a PreToolUse hook fallback).
|
|
511
|
-
"
|
|
516
|
+
"enforced (settings-deny vs a PreToolUse hook fallback). Since BAPI-1020 this is the EXPLICIT REVERT " +
|
|
517
|
+
"posture rather than the worker default, and it is still probed for exactly that reason: " +
|
|
518
|
+
"`skip_permissions` remains selectable and must stay guarded. A fail means deny is NOT enforced on " +
|
|
519
|
+
"the installed CLI (or cannot be verified) and is a load-bearing executor preflight signal.",
|
|
512
520
|
tier: "heavy",
|
|
513
521
|
appliesTo: ["claude"],
|
|
514
522
|
spawnsAgent: true,
|
|
@@ -517,6 +525,33 @@ const denyEnforcement = {
|
|
|
517
525
|
return result;
|
|
518
526
|
},
|
|
519
527
|
};
|
|
528
|
+
/**
|
|
529
|
+
* The same check under the posture every worker actually runs (BAPI-1020).
|
|
530
|
+
*
|
|
531
|
+
* Registered as a SEPARATE probe rather than replacing the bypass probe, because
|
|
532
|
+
* the two answer different questions and one cannot stand in for the other. Under
|
|
533
|
+
* bypass the CLI's own classifier is out of the picture entirely, so the probe
|
|
534
|
+
* measures the deny layer alone; under `auto` the classifier is in the path and a
|
|
535
|
+
* denied call could in principle wait for an approval that headless `-p` mode can
|
|
536
|
+
* never supply. The control-run discipline is identical and applies unchanged: a
|
|
537
|
+
* marker-free run is a REFUSAL, never enforcement.
|
|
538
|
+
*/
|
|
539
|
+
const denyEnforcementAuto = {
|
|
540
|
+
id: "deny-enforcement-auto",
|
|
541
|
+
title: "Enforces a permissions.deny rule under --permission-mode auto",
|
|
542
|
+
description: "The deny-enforcement check under the DEFAULT worker posture (BAPI-1020): a no-deny control run " +
|
|
543
|
+
"first, then claude headless with --permission-mode auto in a disposable temp project carrying a " +
|
|
544
|
+
"permissions.deny rule, asserting the denied call is rejected and reporting which layer enforced. " +
|
|
545
|
+
"This is the posture every conductor worker spawns under, so a fail here means the executor would " +
|
|
546
|
+
"be claiming jobs whose deny layer is unverified for the posture they actually run in.",
|
|
547
|
+
tier: "heavy",
|
|
548
|
+
appliesTo: ["claude"],
|
|
549
|
+
spawnsAgent: true,
|
|
550
|
+
async run(ctx) {
|
|
551
|
+
const { result } = await runDenyEnforcementCheck(ctx, { permissionPosture: "auto" });
|
|
552
|
+
return result;
|
|
553
|
+
},
|
|
554
|
+
};
|
|
520
555
|
/** All probes in deterministic report order. */
|
|
521
556
|
export const ALL_PROBES = [
|
|
522
557
|
binaryResolves,
|
|
@@ -526,6 +561,7 @@ export const ALL_PROBES = [
|
|
|
526
561
|
preambleMidPrompt,
|
|
527
562
|
outputFormat,
|
|
528
563
|
denyEnforcement,
|
|
564
|
+
denyEnforcementAuto,
|
|
529
565
|
workspaceFlag,
|
|
530
566
|
noCwdFlag,
|
|
531
567
|
];
|
|
@@ -2,7 +2,9 @@ import { isModelTier, isValidModelAlias, resolveModelAlias } from "../agent-regi
|
|
|
2
2
|
import { evaluateClaudeMcpShadowingPolicy, inspectClaudeUserConfigForMcpShadowing, resolveClaudeUserConfigPath, } from "../claude-user-config-doctor.js";
|
|
3
3
|
import { runDenyEnforcementPreflight } from "../conductor/deny-enforcement-preflight.js";
|
|
4
4
|
import { buildExecutorBaseWorkerEnv } from "../executor/env.js";
|
|
5
|
+
import { ExecutorNamedError } from "../executor/job-errors.js";
|
|
5
6
|
import { provisionExecutorDenyLayer } from "../executor/permissions.js";
|
|
7
|
+
import { WORKER_GUARD_ROOT_FLAG } from "../executor/worker-guard-hook.js";
|
|
6
8
|
import { SUPPORTED_EXECUTOR_PLATFORMS, evaluateExecutorPlatform } from "../executor/platform.js";
|
|
7
9
|
import { normalizeMcpServerNames } from "../executor/observation.js";
|
|
8
10
|
import { supported } from "./executor-adapter.js";
|
|
@@ -179,17 +181,31 @@ export function resolveClaudeModelAlias(spec, payload) {
|
|
|
179
181
|
* Build the exact headless Claude argv.
|
|
180
182
|
*
|
|
181
183
|
* Moved verbatim from `worker-command.ts`'s `buildClaudeExecutorArgv`. The
|
|
182
|
-
* ordering is the golden shape the tests pin:
|
|
184
|
+
* ordering is the golden shape the tests pin, unchanged by BAPI-1020:
|
|
183
185
|
*
|
|
184
186
|
* -p <prompt> --output-format stream-json --verbose
|
|
185
187
|
* [--model <alias>]
|
|
186
|
-
* (--
|
|
188
|
+
* (--permission-mode auto | --permission-mode acceptEdits | --dangerously-skip-permissions)
|
|
187
189
|
* --strict-mcp-config --mcp-config <verified path>
|
|
188
190
|
*
|
|
191
|
+
* The DEFAULT form is now `--permission-mode auto` (BAPI-1020): Claude's own
|
|
192
|
+
* classifier runs, a flagged call is denied with a reason, and the worker
|
|
193
|
+
* continues. Claude CLI 2.1.258 headless behavior was confirmed during design —
|
|
194
|
+
* under `auto` a denied call does not wait for an approval that can never arrive
|
|
195
|
+
* in `-p` mode. `--dangerously-skip-permissions` is now emitted ONLY for the
|
|
196
|
+
* explicit `skip_permissions` revert.
|
|
197
|
+
*
|
|
189
198
|
* Exactly ONE permission form is emitted. Emitting both would let the skip flag
|
|
190
199
|
* win silently and make the `accept_edits` policy look effective while doing
|
|
191
|
-
* nothing. The
|
|
192
|
-
*
|
|
200
|
+
* nothing. The branch is an EXHAUSTIVE three-way map rather than the two-way
|
|
201
|
+
* `if/else` it replaced, whose `else` was the skip flag: under the old shape a
|
|
202
|
+
* posture the union grew but this function did not would have fallen through into
|
|
203
|
+
* bypass, silently. It now throws the same fixed, secret-free contract error the
|
|
204
|
+
* payload resolver uses, so an unmapped posture fails loudly instead of buying
|
|
205
|
+
* full bypass.
|
|
206
|
+
*
|
|
207
|
+
* The posture changes how Claude PROMPTS, not what it may do — the deny layer and
|
|
208
|
+
* the deterministic worker guard are provisioned in EVERY posture.
|
|
193
209
|
*
|
|
194
210
|
* Every entry is pushed SEPARATELY and never joined into a command string, so a
|
|
195
211
|
* prompt or path containing a space or a shell metacharacter is just a value.
|
|
@@ -199,15 +215,34 @@ export function buildClaudeArgv(prompt, alias, mcpConfigPath, posture) {
|
|
|
199
215
|
if (alias) {
|
|
200
216
|
argv.push("--model", alias);
|
|
201
217
|
}
|
|
202
|
-
|
|
203
|
-
argv.push("--permission-mode", "acceptEdits");
|
|
204
|
-
}
|
|
205
|
-
else {
|
|
206
|
-
argv.push("--dangerously-skip-permissions");
|
|
207
|
-
}
|
|
218
|
+
argv.push(...claudePermissionArgs(posture));
|
|
208
219
|
argv.push("--strict-mcp-config", "--mcp-config", mcpConfigPath);
|
|
209
220
|
return argv;
|
|
210
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* The single permission form for one posture — the whole policy→argv mapping,
|
|
224
|
+
* in one exhaustive place.
|
|
225
|
+
*
|
|
226
|
+
* Returns an ARRAY so every caller appends exactly what this function decided;
|
|
227
|
+
* there is no path on which a caller can add a second form. The thrown message
|
|
228
|
+
* names the FIELD and the accepted spellings and never the supplied value,
|
|
229
|
+
* matching `resolveWorkerPermissionPosture` — this text can reach the server
|
|
230
|
+
* through `/fail`, so echoing an operator's mistyped policy value would turn a
|
|
231
|
+
* typo into an exfiltration path.
|
|
232
|
+
*/
|
|
233
|
+
function claudePermissionArgs(posture) {
|
|
234
|
+
switch (posture) {
|
|
235
|
+
case "auto":
|
|
236
|
+
return ["--permission-mode", "auto"];
|
|
237
|
+
case "accept_edits":
|
|
238
|
+
return ["--permission-mode", "acceptEdits"];
|
|
239
|
+
case "skip_permissions":
|
|
240
|
+
return ["--dangerously-skip-permissions"];
|
|
241
|
+
default:
|
|
242
|
+
throw new ExecutorNamedError("ContractError.PermissionPosture", "worker_permission_posture is not a supported posture " +
|
|
243
|
+
"(expected 'auto', 'skip_permissions', or 'accept_edits').");
|
|
244
|
+
}
|
|
245
|
+
}
|
|
211
246
|
/**
|
|
212
247
|
* Build the Claude worker environment: the generic executor base, plus the one
|
|
213
248
|
* conditional OAuth passthrough.
|
|
@@ -224,9 +259,11 @@ export function buildClaudeArgv(prompt, alias, mcpConfigPath, posture) {
|
|
|
224
259
|
* and cannot reach a worker environment through any path in this function.
|
|
225
260
|
*
|
|
226
261
|
* `options` is forwarded to the generic builder unchanged, including BAPI-844's
|
|
227
|
-
* `indexScope`. This adapter neither reads nor
|
|
228
|
-
* agent-specific credential decision, so
|
|
229
|
-
* where the deny-list and the shared shape
|
|
262
|
+
* `indexScope` and BAPI-1020's `workerBranch`. This adapter neither reads nor
|
|
263
|
+
* validates either one: neither is an agent-specific credential decision, so both
|
|
264
|
+
* stay owned by the generic builder, where the deny-list and the shared shape
|
|
265
|
+
* check both live. In particular `workerBranch` is never read from `parentEnv`
|
|
266
|
+
* here — the generic builder denies that key outright.
|
|
230
267
|
*/
|
|
231
268
|
export function buildClaudeWorkerEnv(parentEnv, options = {}) {
|
|
232
269
|
const env = buildExecutorBaseWorkerEnv(parentEnv, options);
|
|
@@ -236,6 +273,43 @@ export function buildClaudeWorkerEnv(parentEnv, options = {}) {
|
|
|
236
273
|
}
|
|
237
274
|
return env;
|
|
238
275
|
}
|
|
276
|
+
/**
|
|
277
|
+
* Shell-quote a path for embedding in a hook command string.
|
|
278
|
+
*
|
|
279
|
+
* Same convention as `start-tickets-conductor.ts`'s conductor-hook command:
|
|
280
|
+
* single-quote for POSIX safety, escaping any embedded single quote. Claude Code
|
|
281
|
+
* interprets a hook `command` through a shell, so a worktree path containing a
|
|
282
|
+
* space — or an apostrophe — must survive that interpretation intact.
|
|
283
|
+
*/
|
|
284
|
+
function shellQuoteHookPath(value) {
|
|
285
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* The PreToolUse command that runs the packaged deterministic worker guard.
|
|
289
|
+
*
|
|
290
|
+
* Invoked through the EXECUTOR's own Node executable rather than a bare shebang,
|
|
291
|
+
* for the same reason `conductorNodePath` exists (BAPI-527): the worker's `PATH`
|
|
292
|
+
* may resolve a different Node — or none — and a guard that fails to launch is a
|
|
293
|
+
* guard that is not there. A launch failure still fails open, but it should not be
|
|
294
|
+
* something the environment can cause casually.
|
|
295
|
+
*
|
|
296
|
+
* The command carries exactly ONE argument: the trusted worktree root. Templating
|
|
297
|
+
* it here is what makes every path-containment answer trustworthy — provisioning
|
|
298
|
+
* already HOLDS the authoritative worktree path, whereas the hook process's own
|
|
299
|
+
* `cwd` is not guaranteed by the PreToolUse contract to be the project root, and a
|
|
300
|
+
* containment check measured from the wrong root reports success rather than
|
|
301
|
+
* failing safe. The path is not a secret (it is already visible in `ps` for the
|
|
302
|
+
* worker itself) and it is shell-quoted, so a directory containing a space or an
|
|
303
|
+
* apostrophe survives intact.
|
|
304
|
+
*
|
|
305
|
+
* The BRANCH is deliberately NOT an argument: it travels in `BAPI_WORKER_BRANCH`,
|
|
306
|
+
* where the executor's env deny-list can guarantee an ambient value never
|
|
307
|
+
* substitutes for the job's own.
|
|
308
|
+
*/
|
|
309
|
+
export function buildWorkerGuardHookCommand(binPath, execPath, worktreePath) {
|
|
310
|
+
return (`${shellQuoteHookPath(execPath)} ${shellQuoteHookPath(binPath)} ` +
|
|
311
|
+
`${WORKER_GUARD_ROOT_FLAG} ${shellQuoteHookPath(worktreePath)}`);
|
|
312
|
+
}
|
|
239
313
|
// ---------------------------------------------------------------------------
|
|
240
314
|
// The adapter factory
|
|
241
315
|
// ---------------------------------------------------------------------------
|
|
@@ -268,6 +342,9 @@ export function createClaudeExecutorAdapter(spec, deps = {}) {
|
|
|
268
342
|
if (input.indexScope !== undefined) {
|
|
269
343
|
envOptions.indexScope = input.indexScope;
|
|
270
344
|
}
|
|
345
|
+
if (input.workerBranch !== undefined) {
|
|
346
|
+
envOptions.workerBranch = input.workerBranch;
|
|
347
|
+
}
|
|
271
348
|
return {
|
|
272
349
|
executable: spec.command,
|
|
273
350
|
argv: buildClaudeArgv(input.prompt, input.modelAlias, input.mcpConfigPath, input.posture),
|
|
@@ -323,7 +400,14 @@ export function createClaudeExecutorAdapter(spec, deps = {}) {
|
|
|
323
400
|
"continuing fail-open",
|
|
324
401
|
};
|
|
325
402
|
}
|
|
326
|
-
|
|
403
|
+
const options = {
|
|
404
|
+
baseBranch: input.baseBranch,
|
|
405
|
+
workerGuardHookCommand: buildWorkerGuardHookCommand(input.workerGuardHookBinPath, deps.execPath ?? process.execPath, input.worktreePath),
|
|
406
|
+
};
|
|
407
|
+
if (input.workerBranch !== undefined) {
|
|
408
|
+
options.workerBranch = input.workerBranch;
|
|
409
|
+
}
|
|
410
|
+
return provision(input.worktreePath, options, provisioningDeps);
|
|
327
411
|
},
|
|
328
412
|
async probeEnforcement(options = {}) {
|
|
329
413
|
const probe = deps.runDenyPreflight ?? runDenyEnforcementPreflight;
|