@bridge_gpt/mcp-server 0.2.38 → 0.2.41
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 +189 -14
- 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 +479 -0
- package/build/conduct-epic/cli.js +1765 -0
- package/build/conduct-epic/lock.js +302 -0
- package/build/conduct-epic/pr-state.js +286 -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/permissions.js +25 -6
- package/build/sfcc/reads-site-preference.js +6 -0
- 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/writes-custom-object-def.js +6 -2
- 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
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/** Construct a supported capability. */
|
|
2
|
+
export function supported(value) {
|
|
3
|
+
return { supported: true, value };
|
|
4
|
+
}
|
|
5
|
+
/** Construct an explicitly unsupported capability. */
|
|
6
|
+
export function unsupported(reason, strategyId) {
|
|
7
|
+
return strategyId === undefined
|
|
8
|
+
? { supported: false, reason }
|
|
9
|
+
: { supported: false, reason, strategyId };
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The capability keys an adapter MUST supply as supported.
|
|
13
|
+
*
|
|
14
|
+
* Exported as a frozen tuple so the validator, the doctor renderer, and the
|
|
15
|
+
* tests all enumerate the same list. A capability added here is automatically
|
|
16
|
+
* enforced everywhere rather than in the one place someone remembered.
|
|
17
|
+
*/
|
|
18
|
+
export const REQUIRED_ADAPTER_CAPABILITIES = Object.freeze([
|
|
19
|
+
"executable",
|
|
20
|
+
"platform",
|
|
21
|
+
"headlessInvocation",
|
|
22
|
+
"mcpScoping",
|
|
23
|
+
"mcpInitParsing",
|
|
24
|
+
"auth",
|
|
25
|
+
"authFailureDetection",
|
|
26
|
+
"denyEnforcement",
|
|
27
|
+
"redaction",
|
|
28
|
+
"lifecycle",
|
|
29
|
+
]);
|
|
30
|
+
/**
|
|
31
|
+
* Read a capability's declared strategy ID, when it has one.
|
|
32
|
+
*
|
|
33
|
+
* Capability values are heterogeneous and only some carry `strategyId`, so this
|
|
34
|
+
* reads structurally rather than by capability key — a new capability with a
|
|
35
|
+
* strategy ID is picked up without editing this function.
|
|
36
|
+
*/
|
|
37
|
+
function readStrategyId(value) {
|
|
38
|
+
if (!value || typeof value !== "object")
|
|
39
|
+
return null;
|
|
40
|
+
const candidate = value.strategyId;
|
|
41
|
+
return typeof candidate === "string" && candidate.length > 0 ? candidate : null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Validate that every required capability is present AND supported.
|
|
45
|
+
*
|
|
46
|
+
* Both failure modes produce a refusal, and the refusal names which one it was.
|
|
47
|
+
* Nothing here substitutes a default implementation, consults the host, or falls
|
|
48
|
+
* back to Claude behavior — the entire value of this function is that it has no
|
|
49
|
+
* escape hatch. It also never throws: a malformed adapter object degrades into
|
|
50
|
+
* refusals, because throwing would tempt a caller into a catch-and-continue that
|
|
51
|
+
* reintroduces exactly the defaulting this prevents.
|
|
52
|
+
*/
|
|
53
|
+
export function validateExecutorAdapterCapabilities(adapter) {
|
|
54
|
+
const identity = adapter.identity;
|
|
55
|
+
const refusals = [];
|
|
56
|
+
const capabilities = [];
|
|
57
|
+
for (const capability of REQUIRED_ADAPTER_CAPABILITIES) {
|
|
58
|
+
const declared = adapter[capability];
|
|
59
|
+
if (declared === undefined || declared === null) {
|
|
60
|
+
refusals.push({
|
|
61
|
+
capability,
|
|
62
|
+
kind: "absent",
|
|
63
|
+
reason: `required capability '${capability}' was not declared by adapter '${identity.adapterId}'`,
|
|
64
|
+
});
|
|
65
|
+
capabilities.push({
|
|
66
|
+
capability,
|
|
67
|
+
supported: false,
|
|
68
|
+
strategyId: null,
|
|
69
|
+
reason: "not declared",
|
|
70
|
+
});
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (declared.supported !== true) {
|
|
74
|
+
const reason = typeof declared.reason === "string" && declared.reason.length > 0
|
|
75
|
+
? declared.reason
|
|
76
|
+
: `capability '${capability}' is declared unsupported`;
|
|
77
|
+
const refusal = { capability, kind: "unsupported", reason };
|
|
78
|
+
if (typeof declared.strategyId === "string")
|
|
79
|
+
refusal.strategyId = declared.strategyId;
|
|
80
|
+
refusals.push(refusal);
|
|
81
|
+
capabilities.push({
|
|
82
|
+
capability,
|
|
83
|
+
supported: false,
|
|
84
|
+
strategyId: typeof declared.strategyId === "string" ? declared.strategyId : null,
|
|
85
|
+
reason,
|
|
86
|
+
});
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
capabilities.push({
|
|
90
|
+
capability,
|
|
91
|
+
supported: true,
|
|
92
|
+
strategyId: readStrategyId(declared.value),
|
|
93
|
+
reason: null,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (refusals.length > 0) {
|
|
97
|
+
return { ok: false, identity, refusals, capabilities };
|
|
98
|
+
}
|
|
99
|
+
return { ok: true, identity, capabilities };
|
|
100
|
+
}
|
|
101
|
+
/** Render a bounded, secret-free summary of why an adapter was refused. */
|
|
102
|
+
export function formatAdapterRefusal(identity, refusals) {
|
|
103
|
+
const names = refusals.map((r) => `${r.capability} (${r.kind})`).join(", ");
|
|
104
|
+
return (`executor agent adapter '${identity.adapterId}' for agent '${identity.agentId}' is incomplete: ` +
|
|
105
|
+
`${names}. A missing or unsupported capability is never defaulted to host or Claude behavior.`);
|
|
106
|
+
}
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Secret-safe diagnostics
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
/**
|
|
111
|
+
* Replace every occurrence of each declared secret's VALUE with a fixed marker.
|
|
112
|
+
*
|
|
113
|
+
* Implemented with `split`/`join` rather than a `RegExp`, and that choice is
|
|
114
|
+
* load-bearing twice over. A secret value is arbitrary text: built into a
|
|
115
|
+
* pattern it could throw on an unbalanced bracket (turning redaction into a
|
|
116
|
+
* crash), and used as a replacement string a `$&` or `$1` inside it would be
|
|
117
|
+
* re-expanded by the replacer (turning redaction into corruption). Literal
|
|
118
|
+
* splitting has neither failure mode and needs no escaping.
|
|
119
|
+
*
|
|
120
|
+
* Empty and non-string values are skipped: replacing every empty string would
|
|
121
|
+
* shred the diagnostic into markers while hiding nothing.
|
|
122
|
+
*/
|
|
123
|
+
export function redactAdapterDiagnostic(text, parentEnv, redaction) {
|
|
124
|
+
if (typeof text !== "string" || text.length === 0)
|
|
125
|
+
return "";
|
|
126
|
+
let output = text;
|
|
127
|
+
for (const name of redaction.secretEnvNames) {
|
|
128
|
+
const value = parentEnv[name];
|
|
129
|
+
if (typeof value !== "string" || value.length === 0)
|
|
130
|
+
continue;
|
|
131
|
+
if (!output.includes(value))
|
|
132
|
+
continue;
|
|
133
|
+
output = output.split(value).join(redaction.replacement);
|
|
134
|
+
}
|
|
135
|
+
return output;
|
|
136
|
+
}
|
package/build/agent-registry.js
CHANGED
|
@@ -47,6 +47,34 @@ export const AGENT_REGISTRY = {
|
|
|
47
47
|
// can be validated against a static allowlist.
|
|
48
48
|
tierModels: { cheap: "haiku", basic: "sonnet", premium: "opus" },
|
|
49
49
|
staticModelAliasAllowlist: ["haiku", "sonnet", "opus"],
|
|
50
|
+
// BAPI-781: the reference executor adapter. `managedCarriers` is EMPTY and
|
|
51
|
+
// `passthroughs` names only the operator's own OAuth variable — Bridge
|
|
52
|
+
// manages no Anthropic credential (BAPI-791), and `ANTHROPIC_API_KEY`
|
|
53
|
+
// appears in neither list.
|
|
54
|
+
executorAdapter: {
|
|
55
|
+
adapterId: "claude-reference",
|
|
56
|
+
strategyId: "claude-strict-mcp-v1",
|
|
57
|
+
adapterVersion: "1.0.0",
|
|
58
|
+
supportedPlatforms: ["darwin", "linux"],
|
|
59
|
+
capabilityStrategyIds: {
|
|
60
|
+
mcpScoping: "strict-mcp-config",
|
|
61
|
+
mcpInitParsing: "claude-system-init",
|
|
62
|
+
authFailureDetection: "claude-stream-json-result",
|
|
63
|
+
denyEnforcement: "claude-settings-deny",
|
|
64
|
+
redaction: "claude-env-name-redaction",
|
|
65
|
+
lifecycle: "claude-no-lifecycle",
|
|
66
|
+
},
|
|
67
|
+
managedCarriers: [],
|
|
68
|
+
passthroughs: [
|
|
69
|
+
{
|
|
70
|
+
passthroughId: "claude-code-oauth-token",
|
|
71
|
+
envName: "CLAUDE_CODE_OAUTH_TOKEN",
|
|
72
|
+
billingClass: "subscription",
|
|
73
|
+
ownership: "operator-owned",
|
|
74
|
+
rule: "forward-when-present",
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
},
|
|
50
78
|
},
|
|
51
79
|
"cursor-agent": {
|
|
52
80
|
name: "cursor-agent",
|
|
@@ -26,6 +26,6 @@ export const AGENTS = {
|
|
|
26
26
|
"model": "opus",
|
|
27
27
|
"color": "yellow"
|
|
28
28
|
},
|
|
29
|
-
"body": "\n<!-- Platform coverage: this agent reaches Claude Code + GitHub Copilot. The\n companion `regression-check` slash command (commands/src/regression-check.md)\n reaches Cursor + Claude Code. Union: Cursor, Copilot, and Claude Code all\n get this review, either via the agent or the command. -->\n\n## ROLE ##\n\nYou are a precise, evidence-driven regression analyst. Your job is to make the blast radius of a proposed code change explicit — which code genuinely depends on the symbols it touches, and what could regress if those dependents aren't accounted for. You do NOT discover call-sites by hand: a deterministic subcommand (`regression-check`) does that structurally via `ast-grep` and `ripgrep`. Your skill is synthesizing those structural findings into a clear, ranked risk report and naming concrete de-risking moves.\n\n## CONTEXT ##\n\nThis is **lightweight mode** — the default. It produces a *structural* report: real call-sites (ast-grep) versus the wider textual mention set (ripgrep, covering tests/mocks/strings/config). The gap between those two sets is the primary signal: a symbol with many text mentions but few real call-sites likely has callers, mocks, or config the change hasn't accounted for.\n\nYou do NOT discover or propose general refactor candidates (code-quality issues unrelated to a specific proposed change) — that is the `refactor-reviewer` agent's job. If the user is asking for a general quality scan rather than the impact of a specific change, halt and point them at `refactor-reviewer` instead.\n\nPinecone semantic code search is NOT available to you — `ast-grep` + `ripgrep` is the working layer, and this produces a full structural-only report on any repository, indexed or not.\n\n## OBJECTIVE ##\n\nGiven a proposed change (a git diff/PR range, or a ticket description naming the touched symbols), run `regression-check` and turn its structured findings into:\n1. A \"systems accounted for / not accounted for\" report.\n2. Concrete de-risking guidance: which affected caller to update, or where a compatibility/guard seam is needed.\n\n---\n\n## METHODOLOGY ##\n\n### Step 1: Determine the Input Shape\n\n- **Diff/PR invocation**: the user supplies (or you can resolve) a git diff range, a PR number, or \"my staged changes\" / \"my current branch\". Resolve this to a `--diff <range>` value when you can (e.g. `main...HEAD`, a commit SHA range, or omit `--diff` entirely to use the default working-tree-vs-HEAD diff).\n- **Ticket-description invocation**: no diff exists yet (the change is still planned). Read the ticket/requirement text and extract the specific function/class/symbol names it names as the target of the change. Pass them via `--symbols a,b,c`.\n\nIf neither a diff nor any extractable symbol names are available, halt and ask the user to provide one.\n\n### Step 2: Run the Deterministic Core\n\nExecute exactly:\n\n```bash\
|
|
29
|
+
"body": "\n<!-- Platform coverage: this agent reaches Claude Code + GitHub Copilot. The\n companion `regression-check` slash command (commands/src/regression-check.md)\n reaches Cursor + Claude Code. Union: Cursor, Copilot, and Claude Code all\n get this review, either via the agent or the command. -->\n\n## ROLE ##\n\nYou are a precise, evidence-driven regression analyst. Your job is to make the blast radius of a proposed code change explicit — which code genuinely depends on the symbols it touches, and what could regress if those dependents aren't accounted for. You do NOT discover call-sites by hand: a deterministic subcommand (`regression-check`) does that structurally via `ast-grep` and `ripgrep`. Your skill is synthesizing those structural findings into a clear, ranked risk report and naming concrete de-risking moves.\n\n## CONTEXT ##\n\nThis is **lightweight mode** — the default. It produces a *structural* report: real call-sites (ast-grep) versus the wider textual mention set (ripgrep, covering tests/mocks/strings/config). The gap between those two sets is the primary signal: a symbol with many text mentions but few real call-sites likely has callers, mocks, or config the change hasn't accounted for.\n\nYou do NOT discover or propose general refactor candidates (code-quality issues unrelated to a specific proposed change) — that is the `refactor-reviewer` agent's job. If the user is asking for a general quality scan rather than the impact of a specific change, halt and point them at `refactor-reviewer` instead.\n\nPinecone semantic code search is NOT available to you — `ast-grep` + `ripgrep` is the working layer, and this produces a full structural-only report on any repository, indexed or not.\n\n## OBJECTIVE ##\n\nGiven a proposed change (a git diff/PR range, or a ticket description naming the touched symbols), run `regression-check` and turn its structured findings into:\n1. A \"systems accounted for / not accounted for\" report.\n2. Concrete de-risking guidance: which affected caller to update, or where a compatibility/guard seam is needed.\n\n---\n\n## METHODOLOGY ##\n\n### Step 1: Determine the Input Shape\n\n- **Diff/PR invocation**: the user supplies (or you can resolve) a git diff range, a PR number, or \"my staged changes\" / \"my current branch\". Resolve this to a `--diff <range>` value when you can (e.g. `main...HEAD`, a commit SHA range, or omit `--diff` entirely to use the default working-tree-vs-HEAD diff).\n- **Ticket-description invocation**: no diff exists yet (the change is still planned). Read the ticket/requirement text and extract the specific function/class/symbol names it names as the target of the change. Pass them via `--symbols a,b,c`.\n\nIf neither a diff nor any extractable symbol names are available, halt and ask the user to provide one.\n\n### Packaged CLI launcher (`BAPI_MCP_CLI`)\n\nResolve the packaged-CLI launcher **once**, before the first shell-out below, and reuse that one resolved value for every packaged-CLI invocation. Call it `<launcher>`.\n\n- Read the `BAPI_MCP_CLI` environment variable.\n- **Unset, empty, or whitespace-only** — `<launcher>` is exactly `npx -y @bridge_gpt/mcp-server`. This is the default, and the resulting shell command is byte-identical to what it was before this override existed.\n- **Otherwise** — `<launcher>` is that value, used verbatim as the command prefix. It names a local launcher, such as `node /absolute/path/to/mcp_server/build/index.js`. Use it for local pilots and pre-publish verification.\n\nWhen the override is set, apply the established single-quote escaping rule (`'` → `'\\''`, then wrap the whole value in single quotes) before interpolating `<launcher>` into a Bash command string, keep every dynamic argument independently quoted rather than concatenated into the launcher value, and never put a credential or a credential-bearing environment assignment into it.\n\nThis must stay identical to the `/regression-check` command's contract: the command and this agent are two delivery surfaces of ONE review, and a launcher that resolved differently between them would silently run the published package on one surface and a local build on the other. A stale local build is exactly as misleading as a stale npm publish.\n\n### Step 2: Run the Deterministic Core\n\nExecute exactly:\n\n```bash\n<launcher> regression-check --mode lightweight --json [--diff <range> | --symbols a,b,c]\n```\n\nDo NOT hand-roll your own `ast-grep`/`ripgrep` invocations or re-discover call-sites yourself — the subcommand owns that structural analysis. Your job starts with its JSON output.\n\n### Step 3: Parse the Findings\n\nParse the JSON: `summary.symbols_analyzed`, `summary.truncated`, `summary.tools_used`, `summary.degraded_flags`, and the `findings` array (`symbol`, `file`, `definition_location`, `call_sites` (`count`, `by_file`), `broad_mentions`).\n\n**Fail-open**: if `summary.degraded_flags` is non-empty (e.g. `ast-grep` or `ripgrep` was unavailable), do NOT treat the run as a failure. Proceed with whatever data IS present, and call out each degraded section explicitly in the report's Summary — never silently omit the gap. A `[DEGRADED]` finding still tells you something (e.g. broad mentions are known but real call-sites are unknown); report it as such rather than discarding it. If `summary.truncated` is `true`, state that the symbol set was capped and name which symbols were analyzed.\n\n**Ripgrep degradation formatting**: if any entry in `summary.degraded_flags` mentions ripgrep, render it with an action-first visual hierarchy of three scannable segments rather than as a plain sentence:\n1. **Status/Problem** — a warning header with a semantic warning indicator (e.g. ⚠️).\n2. **Root Cause** — a brief note that `rg` must be a real binary on `PATH`; a shell function/alias is invisible to the spawned subcommand.\n3. **Remediation** — a standalone, copy-pasteable install command (e.g. `brew install ripgrep`) on its own line.\n\nApply monospace typography (backticks) to every reference to a system command, CLI tool (`rg`), environment term (`PATH`), or installation package, in this warning and throughout the report.\n\n### Step 4: Synthesize Risk\n\nFor each symbol, compare `call_sites.count` against `broad_mentions.length`:\n- **Accounted for**: every real call-site and every broad mention is either already touched by the change or clearly unaffected by it (e.g. a doc/comment mention).\n- **Not accounted for**: a real call-site, or a broad mention not yet inspected, sits in a file the proposed change does not touch. Name the specific file.\n\nRank \"not accounted for\" items by how directly they call the changed symbol (a real call-site outranks a textual mention).\n\n### Step 5: Propose De-Risking — Diagnostic Synthesis, Not a Patch\n\nFor each \"not accounted for\" item, name ONE of:\n- **Update the affected caller**: the caller's usage will break or behave differently; point to the exact `file:line` (from `call_sites.by_file` / `broad_mentions`) and describe what needs to change there.\n- **Add a compatibility/guard seam**: when updating every caller isn't the right call (e.g. a public API, a config key still read elsewhere), describe the seam needed (a deprecation shim, a fallback default, a feature flag) — not the seam's full implementation.\n\nYou are NOT implementing the fix. State what needs to happen and where; leave the actual edit to the developer or a follow-up task.\n\n---\n\n## RESPONSE FORMAT ##\n\nProduce a chat report (no file write required) with these sections:\n\n```markdown\n# Regression Review: [Symbol(s) / Change Description]\n\n**Mode**: lightweight\n**Input**: [--diff <range> | --symbols a,b,c]\n**Tools used**: [summary.tools_used, joined]\n**Degraded**: [list summary.degraded_flags, or \"none\"]\n**Symbols analyzed**: [summary.symbols_analyzed.length][ — TRUNCATED, capped at N if summary.truncated]\n\n## Summary\n\n[2-3 sentences: overall risk level, how many symbols are fully accounted for vs. not, and any degraded-tool caveats from Step 3.]\n\n## Systems Accounted For / Not Accounted For\n\nRender as a `.data-table`-style markdown table (bold header row; left-aligned `Symbol` / `File` columns; tight ✅/⚠️ status indicators) with one row per changed symbol:\n\n| Symbol | File | Real Call-Sites | Broad Mentions | Status |\n|---|---|---|---|---|\n| `helper` | `src/foo.py` | 3 | 4 | ⚠️ Not accounted for |\n| `caller` | `src/foo.py` | 1 | 1 | ✅ Accounted for |\n\nIf `definition_location` is `null` for a symbol, degrade gracefully — do not leave the `File` column blank. Render a muted `—` placeholder there instead.\n\n## De-Risking Guidance\n\n### 1. [Symbol / File]\n- **Issue**: [what's not accounted for, with file:line]\n- **Recommendation**: Update the affected caller at `file:line` | Add a compatibility/guard seam — [describe]\n\n[Continue for each not-accounted-for item]\n\n---\n\n*Generated by regression-reviewer (lightweight mode). Structural findings via ast-grep + ripgrep; Pinecone semantic search not available.*\n```\n\n### Important Reminders\n\n- Never re-discover call-sites by hand — always run `regression-check` first.\n- Never silently drop a degraded section — name it.\n- Never propose a full implementation — name the needed change and where it goes, not the code itself.\n"
|
|
30
30
|
}
|
|
31
31
|
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only Claude login marker advisory (BAPI-791).
|
|
3
|
+
*
|
|
4
|
+
* BAPI-790 and BAPI-791 together retired every Bridge-managed Anthropic
|
|
5
|
+
* credential: there is no stored OAuth token, no API-key readiness check, and
|
|
6
|
+
* no worker-auth strategy left to diagnose. A worker now authenticates through
|
|
7
|
+
* the executor host's own interactive `claude login` (inherited via `HOME`), or,
|
|
8
|
+
* headlessly, through an operator-exported `CLAUDE_CODE_OAUTH_TOKEN` that Bridge
|
|
9
|
+
* only forwards and never stores or inspects (see `executor/env.ts`).
|
|
10
|
+
*
|
|
11
|
+
* `readFile()` is passed a path this module builds with `path.join`, using the
|
|
12
|
+
* runtime's own path module (this helper reads the LOCAL host's home directory,
|
|
13
|
+
* never a cross-platform provisioning target), so it works the same on `darwin`,
|
|
14
|
+
* `linux`, and `win32`.
|
|
15
|
+
*
|
|
16
|
+
* This module is the entire replacement for what doctor/preflight used to
|
|
17
|
+
* report about Anthropic credential readiness: one low-information marker,
|
|
18
|
+
* derived solely from the presence of a top-level `oauthAccount` object in the
|
|
19
|
+
* host's own `~/.claude.json`. It is deliberately NOT a readiness check —
|
|
20
|
+
* `oauthAccount` records that an interactive login was completed at some point,
|
|
21
|
+
* not that the credential it left behind is still valid, unexpired, or that the
|
|
22
|
+
* first real Claude spawn will actually authenticate. It exists only so a
|
|
23
|
+
* doctor/preflight report has something to say about the host's login state
|
|
24
|
+
* without resolving, storing, or validating a credential itself.
|
|
25
|
+
*
|
|
26
|
+
* SECRET SAFETY. `detectClaudeLogin` reads only whether `oauthAccount` is a
|
|
27
|
+
* non-null object — it never returns the parsed object, an account identifier,
|
|
28
|
+
* or any other `.claude.json` content. `formatClaudeLoginAdvisory` renders a
|
|
29
|
+
* single fixed line per state and never serializes anything read from the file.
|
|
30
|
+
* Neither function touches `CLAUDE_CODE_OAUTH_TOKEN`, `ANTHROPIC_API_KEY`, the
|
|
31
|
+
* Bridge credential store, or the OS keychain — see
|
|
32
|
+
* {@link ClaudeLoginDetectionDeps}, whose only two dependencies are `homedir()`
|
|
33
|
+
* and `readFile()`.
|
|
34
|
+
*/
|
|
35
|
+
import path from "path";
|
|
36
|
+
/**
|
|
37
|
+
* Detect whether the host's `~/.claude.json` carries a top-level `oauthAccount`
|
|
38
|
+
* object, the marker an interactive `claude login` leaves behind.
|
|
39
|
+
*
|
|
40
|
+
* Fails CLOSED to "not detected" for every I/O or shape problem — a missing
|
|
41
|
+
* file, an unreadable file, malformed JSON, a non-object top level, or an
|
|
42
|
+
* `oauthAccount` that is absent, `null`, a string, an array, or any other
|
|
43
|
+
* non-object shape. None of those differences are exposed to the caller: this
|
|
44
|
+
* function never throws and never attaches a path or a raw error to its
|
|
45
|
+
* result, because the result is a report about a login marker, not a
|
|
46
|
+
* filesystem diagnostic.
|
|
47
|
+
*/
|
|
48
|
+
export async function detectClaudeLogin(deps) {
|
|
49
|
+
let raw;
|
|
50
|
+
try {
|
|
51
|
+
raw = await deps.readFile(path.join(deps.homedir(), ".claude.json"));
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return { detected: false };
|
|
55
|
+
}
|
|
56
|
+
let parsed;
|
|
57
|
+
try {
|
|
58
|
+
parsed = JSON.parse(raw);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return { detected: false };
|
|
62
|
+
}
|
|
63
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
64
|
+
return { detected: false };
|
|
65
|
+
}
|
|
66
|
+
const oauthAccount = parsed.oauthAccount;
|
|
67
|
+
const isAccountObject = oauthAccount !== null && typeof oauthAccount === "object" && !Array.isArray(oauthAccount);
|
|
68
|
+
return isAccountObject ? { detected: true } : { detected: false };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Render the fixed, one-line advisory for a login-detection result.
|
|
72
|
+
*
|
|
73
|
+
* Exactly one line per state, and neither line carries any content read from
|
|
74
|
+
* `.claude.json` — no account identifier, no token, no expiry. The negative
|
|
75
|
+
* line does not claim a worker will fail; the positive line does not claim a
|
|
76
|
+
* worker will succeed. Both are advisory only: this marker cannot guarantee
|
|
77
|
+
* that the first real Claude spawn will authenticate (see the module doc).
|
|
78
|
+
*/
|
|
79
|
+
export function formatClaudeLoginAdvisory(result) {
|
|
80
|
+
return result.detected
|
|
81
|
+
? "Claude login: detected (~/.claude.json has an OAuth account). This is advisory only — it " +
|
|
82
|
+
"does not guarantee the next worker spawn will authenticate."
|
|
83
|
+
: "Claude login: not detected. Run `claude login` on this host, or, on a headless host, " +
|
|
84
|
+
"export CLAUDE_CODE_OAUTH_TOKEN before starting the executor.";
|
|
85
|
+
}
|
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Read-only inspection of the machine's Claude user configuration
|
|
3
|
-
* (`~/.claude.json`) for `bridge-api` MCP registrations that
|
|
3
|
+
* (`~/.claude.json`) for `bridge-api` MCP registrations that could SHADOW the
|
|
4
4
|
* secret-free registration the conductor provisions into a worker worktree
|
|
5
|
-
* (BAPI-727).
|
|
5
|
+
* (BAPI-727), reported as an ADVISORY since BAPI-790.
|
|
6
6
|
*
|
|
7
7
|
* EMPIRICALLY ESTABLISHED BEHAVIOR — the only precedence claim this module makes:
|
|
8
8
|
* a `projects["<main repository path>"].mcpServers["bridge-api"]` entry was
|
|
9
|
-
* observed to win over a linked worktree's own `.mcp.json`. That
|
|
10
|
-
* this inspector
|
|
11
|
-
* the user-level entry
|
|
9
|
+
* observed to win over a linked worktree's own `.mcp.json`. That WAS the failure
|
|
10
|
+
* this inspector existed to block: the worker silently talked to whatever server
|
|
11
|
+
* the user-level entry named (in practice, production) instead of the endpoint the
|
|
12
12
|
* executor provisioned.
|
|
13
13
|
*
|
|
14
|
+
* WHY IT IS NOW ADVISORY (BAPI-790). Workers are spawned with
|
|
15
|
+
* `--strict-mcp-config --mcp-config <worktree>/.mcp.json`, so no entry in
|
|
16
|
+
* `~/.claude.json` is loaded by a worker at any scope — measured as inventory
|
|
17
|
+
* Finding 6 — and the executor asserts the worker's actual loaded surface from
|
|
18
|
+
* its own `system`/`init` event on every spawn rather than trusting that flag.
|
|
19
|
+
* The collision can no longer produce wrong work, so it is reported with the
|
|
20
|
+
* exact command that clears it and blocks nothing. The inspection itself is
|
|
21
|
+
* unchanged: same scopes, same fixed labels, same secret safety.
|
|
22
|
+
*
|
|
14
23
|
* NOT ESTABLISHED, AND MUST NOT BE INFERRED FROM THIS MODULE:
|
|
15
24
|
* - user-scope (`mcpServers`) versus project-scope precedence,
|
|
16
25
|
* - precedence of a `projects["<worktree path>"]` entry against anything else,
|
|
@@ -44,9 +53,18 @@ export function resolveClaudeUserConfigPath(homeDirectory, platform) {
|
|
|
44
53
|
return pathApiForProvisioningPlatform(platform).join(homeDirectory, CLAUDE_USER_CONFIG_FILENAME);
|
|
45
54
|
}
|
|
46
55
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
56
|
+
* RETIRED (BAPI-790) — this override is now behaviorally inert.
|
|
57
|
+
*
|
|
58
|
+
* It used to downgrade a confirmed shadowing collision from a refusal to a loud
|
|
59
|
+
* warning. There is no refusal left to downgrade: strict flag-scoped MCP loading
|
|
60
|
+
* means an operator registration cannot shadow a worker, so the finding is always
|
|
61
|
+
* advisory and setting this variable changes nothing.
|
|
62
|
+
*
|
|
63
|
+
* The constant is kept exported, and {@link isClaudeMcpShadowingOverrideEnabled}
|
|
64
|
+
* with it, so an operator who still has it set in a service unit or shell profile
|
|
65
|
+
* gets no error — and so the name stays greppable to whoever wonders why it stopped
|
|
66
|
+
* mattering. Nothing in the policy consults it. Do not reintroduce a branch on it
|
|
67
|
+
* without re-establishing that shadowing can affect a worker again.
|
|
50
68
|
*/
|
|
51
69
|
export const CONDUCTOR_ALLOW_CLAUDE_MCP_SHADOWING_ENV = "BAPI_CONDUCTOR_ALLOW_CLAUDE_MCP_SHADOWING";
|
|
52
70
|
/** A plain, non-array object. */
|
|
@@ -236,8 +254,12 @@ export async function inspectClaudeUserConfigForMcpShadowing(input, deps) {
|
|
|
236
254
|
const OVERRIDE_TRUTHY = new Set(["1", "true", "yes", "on"]);
|
|
237
255
|
/**
|
|
238
256
|
* True only for a trimmed, case-insensitive `1`, `true`, `yes`, or `on`. Absent,
|
|
239
|
-
* blank, `0`, `false`, `no`, `off`, and anything else are disabled
|
|
240
|
-
*
|
|
257
|
+
* blank, `0`, `false`, `no`, `off`, and anything else are disabled.
|
|
258
|
+
*
|
|
259
|
+
* RETIRED (BAPI-790): no policy branches on this any more — see
|
|
260
|
+
* {@link CONDUCTOR_ALLOW_CLAUDE_MCP_SHADOWING_ENV}. Retained as a pure predicate
|
|
261
|
+
* so the parsing rule stays documented and testable rather than being deleted and
|
|
262
|
+
* re-guessed if the override is ever revived.
|
|
241
263
|
*/
|
|
242
264
|
export function isClaudeMcpShadowingOverrideEnabled(env) {
|
|
243
265
|
const raw = env?.[CONDUCTOR_ALLOW_CLAUDE_MCP_SHADOWING_ENV];
|
|
@@ -283,35 +305,39 @@ export function formatClaudeUserConfigDiagnostic(diagnostic) {
|
|
|
283
305
|
"unverifiable project root (the main repository path could not be resolved)");
|
|
284
306
|
}
|
|
285
307
|
}
|
|
308
|
+
/**
|
|
309
|
+
* The exact remediation command every shadowing advisory carries (BAPI-790).
|
|
310
|
+
*
|
|
311
|
+
* A same-named registration is almost always a local-scope leftover, and this is
|
|
312
|
+
* the one command that removes it. Centralized so the executor advisory, the
|
|
313
|
+
* doctor prerequisite, and the operator runbook cannot drift apart.
|
|
314
|
+
*/
|
|
315
|
+
export const CLAUDE_MCP_SHADOWING_REMEDIATION_COMMAND = "claude mcp remove bridge-api -s local";
|
|
286
316
|
/**
|
|
287
317
|
* The SINGLE policy every consumer (executor preflight, per-worktree enforcement,
|
|
288
318
|
* doctor) applies to an inspection result, so none of them re-derives
|
|
289
|
-
* classification
|
|
319
|
+
* classification.
|
|
320
|
+
*
|
|
321
|
+
* ADVISORY ONLY (BAPI-790). This refused by default under BAPI-727, when an
|
|
322
|
+
* operator's same-named `bridge-api` registration could genuinely win over the
|
|
323
|
+
* worktree's provisioned `.mcp.json` and send a worker at the wrong endpoint.
|
|
324
|
+
* Strict flag-scoped loading removes that possibility at the mechanism level —
|
|
325
|
+
* the operator's registration is not loaded at all (inventory Finding 6) — and
|
|
326
|
+
* the per-spawn init-event assertion verifies the loaded surface on every spawn
|
|
327
|
+
* instead of trusting the flag. So the collision is reported, with the exact
|
|
328
|
+
* command that clears it, and nothing is blocked.
|
|
290
329
|
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
* who deliberately opted in still sees exactly what was detected. Degraded
|
|
294
|
-
* diagnostics are always warnings — they are inconclusive, never a collision.
|
|
330
|
+
* Degraded diagnostics remain warnings for the reason they always were: they are
|
|
331
|
+
* inconclusive, never evidence of a collision.
|
|
295
332
|
*/
|
|
296
|
-
export function evaluateClaudeMcpShadowingPolicy(inspection
|
|
333
|
+
export function evaluateClaudeMcpShadowingPolicy(inspection) {
|
|
297
334
|
const warnings = inspection.diagnostics.map(formatClaudeUserConfigDiagnostic);
|
|
298
335
|
if (inspection.findings.length === 0)
|
|
299
336
|
return { ok: true, warnings };
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
...warnings,
|
|
307
|
-
],
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
return {
|
|
311
|
-
ok: false,
|
|
312
|
-
refusals: findingMessages.map((m) => `${m}. Refusing to proceed: worktree MCP provisioning may be shadowed. Delete, or ` +
|
|
313
|
-
`move aside, that registration, or set ` +
|
|
314
|
-
`${CONDUCTOR_ALLOW_CLAUDE_MCP_SHADOWING_ENV}=1 to override.`),
|
|
315
|
-
warnings,
|
|
316
|
-
};
|
|
337
|
+
// The finding's own fixed scope/shape labels are preserved verbatim; only the
|
|
338
|
+
// trailing sentence changes from a refusal to an advisory.
|
|
339
|
+
const findingMessages = inspection.findings.map((finding) => `${formatClaudeMcpShadowFinding(finding)}. Advisory only: strict worker MCP loading ` +
|
|
340
|
+
"means this registration is not loaded by a worker, and the per-spawn MCP surface " +
|
|
341
|
+
`assertion verifies that. To clear it: ${CLAUDE_MCP_SHADOWING_REMEDIATION_COMMAND}`);
|
|
342
|
+
return { ok: true, warnings: [...findingMessages, ...warnings] };
|
|
317
343
|
}
|