@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
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { isModelTier, isValidModelAlias, resolveModelAlias } from "../agent-registry.js";
|
|
2
|
+
import { evaluateClaudeMcpShadowingPolicy, inspectClaudeUserConfigForMcpShadowing, resolveClaudeUserConfigPath, } from "../claude-user-config-doctor.js";
|
|
3
|
+
import { runDenyEnforcementPreflight } from "../conductor/deny-enforcement-preflight.js";
|
|
4
|
+
import { buildExecutorBaseWorkerEnv } from "../executor/env.js";
|
|
5
|
+
import { provisionExecutorDenyLayer } from "../executor/permissions.js";
|
|
6
|
+
import { SUPPORTED_EXECUTOR_PLATFORMS, evaluateExecutorPlatform } from "../executor/platform.js";
|
|
7
|
+
import { normalizeMcpServerNames } from "../executor/observation.js";
|
|
8
|
+
import { supported } from "./executor-adapter.js";
|
|
9
|
+
/** Stable identity of this reference implementation. */
|
|
10
|
+
export const CLAUDE_ADAPTER_ID = "claude-reference";
|
|
11
|
+
/** Registry strategy identifier that selects this adapter's factory. */
|
|
12
|
+
export const CLAUDE_ADAPTER_STRATEGY_ID = "claude-strict-mcp-v1";
|
|
13
|
+
/** Contract revision this implementation targets. */
|
|
14
|
+
export const CLAUDE_ADAPTER_VERSION = "1.0.0";
|
|
15
|
+
/** The one operator-owned credential name this adapter forwards. */
|
|
16
|
+
export const CLAUDE_OAUTH_TOKEN_ENV = "CLAUDE_CODE_OAUTH_TOKEN";
|
|
17
|
+
/** The provisioned MCP server a Claude worker requires. */
|
|
18
|
+
export const CLAUDE_REQUIRED_MCP_SERVER_NAME = "bridge-api";
|
|
19
|
+
/** Fixed replacement written in place of a redacted secret value. */
|
|
20
|
+
export const CLAUDE_REDACTION_REPLACEMENT = "[redacted]";
|
|
21
|
+
/** Upper bound on retained version text, matching preflight's existing cap. */
|
|
22
|
+
const VERSION_DETAIL_MAX = 200;
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Executable discovery
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
/**
|
|
27
|
+
* Normalize a version probe result: stdout first, stderr second, bounded.
|
|
28
|
+
*
|
|
29
|
+
* Preserved verbatim from `preflight.ts`'s `normalizeVersion` — the ordering is
|
|
30
|
+
* not arbitrary, and re-deriving it would silently change the `agent_version`
|
|
31
|
+
* value that goes onto the wire in the claim manifest.
|
|
32
|
+
*/
|
|
33
|
+
function normalizeClaudeVersion(output) {
|
|
34
|
+
return (output.stdout || output.stderr || "").trim().slice(0, VERSION_DETAIL_MAX);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Parse the probe into bounded identification.
|
|
38
|
+
*
|
|
39
|
+
* NO SEMANTIC-VERSION PARSING, deliberately. The Claude CLI does not guarantee a
|
|
40
|
+
* parseable version format, so the fingerprint is the normalized version text
|
|
41
|
+
* itself (collapsed whitespace) rather than an invented `major.minor.patch`
|
|
42
|
+
* extraction that would degrade to nonsense the first time the output format
|
|
43
|
+
* changes. A failed probe yields a null fingerprint rather than a fabricated one.
|
|
44
|
+
*/
|
|
45
|
+
export function parseClaudeVersion(output) {
|
|
46
|
+
const version = normalizeClaudeVersion(output);
|
|
47
|
+
if (output.exitCode !== 0 || version.length === 0) {
|
|
48
|
+
return { version, fingerprint: null };
|
|
49
|
+
}
|
|
50
|
+
return { version, fingerprint: version.replace(/\s+/g, " ").trim() };
|
|
51
|
+
}
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// MCP init-event parsing
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
/** True only for an exact `{"type":"system","subtype":"init"}` object. */
|
|
56
|
+
function isSystemInitEvent(value) {
|
|
57
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
58
|
+
return false;
|
|
59
|
+
const obj = value;
|
|
60
|
+
return obj.type === "system" && obj.subtype === "init";
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Parse one stream line for Claude's `system`/`init` event.
|
|
64
|
+
*
|
|
65
|
+
* Moved verbatim from `observation.ts`. The malformed arm is the load-bearing
|
|
66
|
+
* one: a valid init event whose `mcp_servers` list cannot be read returns
|
|
67
|
+
* `malformed-init` rather than an empty name set, because an unreadable shape is
|
|
68
|
+
* not evidence of an empty surface. Dropping a single unreadable entry would let
|
|
69
|
+
* a mismatched surface pass as a match.
|
|
70
|
+
*/
|
|
71
|
+
export function parseClaudeInitEvent(line) {
|
|
72
|
+
const trimmed = typeof line === "string" ? line.trim() : "";
|
|
73
|
+
if (trimmed.length === 0)
|
|
74
|
+
return { kind: "not-init" };
|
|
75
|
+
let parsed;
|
|
76
|
+
try {
|
|
77
|
+
parsed = JSON.parse(trimmed);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// A line we cannot parse tells us nothing at all — advisory, not malformed.
|
|
81
|
+
return { kind: "not-init" };
|
|
82
|
+
}
|
|
83
|
+
if (!isSystemInitEvent(parsed))
|
|
84
|
+
return { kind: "not-init" };
|
|
85
|
+
const servers = parsed.mcp_servers;
|
|
86
|
+
if (!Array.isArray(servers))
|
|
87
|
+
return { kind: "malformed-init" };
|
|
88
|
+
const names = [];
|
|
89
|
+
for (const entry of servers) {
|
|
90
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
91
|
+
return { kind: "malformed-init" };
|
|
92
|
+
}
|
|
93
|
+
const name = entry.name;
|
|
94
|
+
if (typeof name !== "string" || name.trim().length === 0) {
|
|
95
|
+
return { kind: "malformed-init" };
|
|
96
|
+
}
|
|
97
|
+
names.push(name);
|
|
98
|
+
}
|
|
99
|
+
return { kind: "init", serverNames: normalizeMcpServerNames(names) };
|
|
100
|
+
}
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// Not-authenticated detection
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
/**
|
|
105
|
+
* Recognize Claude's not-logged-in / invalid-token failure in its stream-json
|
|
106
|
+
* output.
|
|
107
|
+
*
|
|
108
|
+
* Moved verbatim from `job-errors.ts`'s `classifyClaudeAuthenticationFailure`,
|
|
109
|
+
* with one addition required by the contract: the result carries a stable reason
|
|
110
|
+
* CODE and never the matched line. The excerpt this reads can contain arbitrary
|
|
111
|
+
* agent output and the classification travels into a `/fail` report, so
|
|
112
|
+
* returning the match would turn a diagnostic into a disclosure channel.
|
|
113
|
+
*/
|
|
114
|
+
export function classifyClaudeAuthFailure(stdoutExcerpt) {
|
|
115
|
+
if (typeof stdoutExcerpt !== "string" || stdoutExcerpt.length === 0) {
|
|
116
|
+
return { notAuthenticated: false, reasonCode: null };
|
|
117
|
+
}
|
|
118
|
+
for (const rawLine of stdoutExcerpt.split("\n")) {
|
|
119
|
+
const trimmed = rawLine.trim();
|
|
120
|
+
if (trimmed.length === 0)
|
|
121
|
+
continue;
|
|
122
|
+
let parsed;
|
|
123
|
+
try {
|
|
124
|
+
parsed = JSON.parse(trimmed);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
130
|
+
continue;
|
|
131
|
+
const obj = parsed;
|
|
132
|
+
if (obj.type !== "result" || obj.is_error !== true)
|
|
133
|
+
continue;
|
|
134
|
+
const resultText = typeof obj.result === "string" ? obj.result : "";
|
|
135
|
+
const notLoggedIn = obj.error === "authentication_failed" ||
|
|
136
|
+
resultText.includes("Not logged in") ||
|
|
137
|
+
resultText.includes("Please run /login");
|
|
138
|
+
const invalidToken = obj.api_error_status === 401 || resultText.includes("OAuth access token is invalid");
|
|
139
|
+
if (notLoggedIn)
|
|
140
|
+
return { notAuthenticated: true, reasonCode: "not-logged-in" };
|
|
141
|
+
if (invalidToken)
|
|
142
|
+
return { notAuthenticated: true, reasonCode: "invalid-token" };
|
|
143
|
+
}
|
|
144
|
+
return { notAuthenticated: false, reasonCode: null };
|
|
145
|
+
}
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Model alias resolution
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
/**
|
|
150
|
+
* Resolve the Claude model alias from a job payload against the SUPPLIED spec.
|
|
151
|
+
*
|
|
152
|
+
* Moved from `worker-command.ts`, with the hard-coded `resolveAgentSpec("claude")`
|
|
153
|
+
* lookup removed: the adapter is constructed from a spec, so the alias policy
|
|
154
|
+
* reads that spec instead of re-resolving one. A direct alias is accepted only
|
|
155
|
+
* when it passes both registry alias validation AND the spec's static allowlist;
|
|
156
|
+
* anything else falls through to tier resolution, and an unusable result is null
|
|
157
|
+
* (meaning "omit `--model`") rather than an unvalidated passthrough.
|
|
158
|
+
*/
|
|
159
|
+
export function resolveClaudeModelAlias(spec, payload) {
|
|
160
|
+
const p = payload && typeof payload === "object" ? payload : {};
|
|
161
|
+
const directRaw = p.model_alias;
|
|
162
|
+
if (typeof directRaw === "string" && directRaw.trim().length > 0) {
|
|
163
|
+
const direct = directRaw.trim();
|
|
164
|
+
const allowed = !spec.staticModelAliasAllowlist || spec.staticModelAliasAllowlist.includes(direct);
|
|
165
|
+
if (isValidModelAlias(direct) && allowed) {
|
|
166
|
+
return direct;
|
|
167
|
+
}
|
|
168
|
+
// An invalid/disallowed direct alias falls through to the tier path — never
|
|
169
|
+
// returned as-is.
|
|
170
|
+
}
|
|
171
|
+
const tier = isModelTier(p.model_tier) ? p.model_tier : null;
|
|
172
|
+
return resolveModelAlias(spec, tier, null);
|
|
173
|
+
}
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// Spawn shape
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
/**
|
|
178
|
+
* Build the exact headless Claude argv.
|
|
179
|
+
*
|
|
180
|
+
* Moved verbatim from `worker-command.ts`'s `buildClaudeExecutorArgv`. The
|
|
181
|
+
* ordering is the golden shape the tests pin:
|
|
182
|
+
*
|
|
183
|
+
* -p <prompt> --output-format stream-json --verbose
|
|
184
|
+
* [--model <alias>]
|
|
185
|
+
* (--dangerously-skip-permissions | --permission-mode acceptEdits)
|
|
186
|
+
* --strict-mcp-config --mcp-config <verified path>
|
|
187
|
+
*
|
|
188
|
+
* Exactly ONE permission form is emitted. Emitting both would let the skip flag
|
|
189
|
+
* win silently and make the `accept_edits` policy look effective while doing
|
|
190
|
+
* nothing. The posture changes how Claude PROMPTS, not what it may do — the deny
|
|
191
|
+
* layer is provisioned in both postures.
|
|
192
|
+
*
|
|
193
|
+
* Every entry is pushed SEPARATELY and never joined into a command string, so a
|
|
194
|
+
* prompt or path containing a space or a shell metacharacter is just a value.
|
|
195
|
+
*/
|
|
196
|
+
export function buildClaudeArgv(prompt, alias, mcpConfigPath, posture) {
|
|
197
|
+
const argv = ["-p", prompt, "--output-format", "stream-json", "--verbose"];
|
|
198
|
+
if (alias) {
|
|
199
|
+
argv.push("--model", alias);
|
|
200
|
+
}
|
|
201
|
+
if (posture === "accept_edits") {
|
|
202
|
+
argv.push("--permission-mode", "acceptEdits");
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
argv.push("--dangerously-skip-permissions");
|
|
206
|
+
}
|
|
207
|
+
argv.push("--strict-mcp-config", "--mcp-config", mcpConfigPath);
|
|
208
|
+
return argv;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Build the Claude worker environment: the generic executor base, plus the one
|
|
212
|
+
* conditional OAuth passthrough.
|
|
213
|
+
*
|
|
214
|
+
* Moved from `env.ts`, where the OAuth branch used to sit inside the generic
|
|
215
|
+
* builder. It belongs HERE because it is an agent-specific credential decision:
|
|
216
|
+
* a Codex adapter would forward something else entirely, and a generic builder
|
|
217
|
+
* that knew about Claude's token would be exactly the Claude-shaped coupling
|
|
218
|
+
* this ticket removes.
|
|
219
|
+
*
|
|
220
|
+
* The copy is byte-for-byte and unconditional on content: no trimming, parsing,
|
|
221
|
+
* validation, logging, or fallback. A non-string or absent value is silently
|
|
222
|
+
* skipped rather than synthesized. `ANTHROPIC_API_KEY` has no equivalent branch
|
|
223
|
+
* and cannot reach a worker environment through any path in this function.
|
|
224
|
+
*/
|
|
225
|
+
export function buildClaudeWorkerEnv(parentEnv, options = {}) {
|
|
226
|
+
const env = buildExecutorBaseWorkerEnv(parentEnv, options);
|
|
227
|
+
const token = parentEnv[CLAUDE_OAUTH_TOKEN_ENV];
|
|
228
|
+
if (typeof token === "string") {
|
|
229
|
+
env[CLAUDE_OAUTH_TOKEN_ENV] = token;
|
|
230
|
+
}
|
|
231
|
+
return env;
|
|
232
|
+
}
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// The adapter factory
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
/**
|
|
237
|
+
* Construct the Claude reference adapter from a registry `AgentSpec`.
|
|
238
|
+
*
|
|
239
|
+
* The spec is supplied rather than resolved internally so the executable and
|
|
240
|
+
* model metadata stay owned by `agent-registry.ts` — this adapter adds a second
|
|
241
|
+
* command map to the codebase nowhere.
|
|
242
|
+
*/
|
|
243
|
+
export function createClaudeExecutorAdapter(spec, deps = {}) {
|
|
244
|
+
const executable = {
|
|
245
|
+
executable: spec.command,
|
|
246
|
+
versionArgv: ["--version"],
|
|
247
|
+
parseVersion: parseClaudeVersion,
|
|
248
|
+
};
|
|
249
|
+
const platform = {
|
|
250
|
+
supportedPlatforms: SUPPORTED_EXECUTOR_PLATFORMS,
|
|
251
|
+
evaluate: evaluateExecutorPlatform,
|
|
252
|
+
};
|
|
253
|
+
const headlessInvocation = {
|
|
254
|
+
buildSpawnShape(input) {
|
|
255
|
+
const envOptions = input.effectiveBaseBranch === undefined
|
|
256
|
+
? {}
|
|
257
|
+
: { effectiveBaseBranch: input.effectiveBaseBranch };
|
|
258
|
+
return {
|
|
259
|
+
executable: spec.command,
|
|
260
|
+
argv: buildClaudeArgv(input.prompt, input.modelAlias, input.mcpConfigPath, input.posture),
|
|
261
|
+
env: buildClaudeWorkerEnv(input.parentEnv, envOptions),
|
|
262
|
+
};
|
|
263
|
+
},
|
|
264
|
+
resolveModelAlias: (payload) => resolveClaudeModelAlias(spec, payload),
|
|
265
|
+
};
|
|
266
|
+
const mcpScoping = {
|
|
267
|
+
strategyId: "strict-mcp-config",
|
|
268
|
+
requiredServerName: CLAUDE_REQUIRED_MCP_SERVER_NAME,
|
|
269
|
+
supportsStrictConfig: true,
|
|
270
|
+
};
|
|
271
|
+
const mcpInitParsing = {
|
|
272
|
+
strategyId: "claude-system-init",
|
|
273
|
+
parseInitEvent: parseClaudeInitEvent,
|
|
274
|
+
};
|
|
275
|
+
/**
|
|
276
|
+
* The subscription-billed, operator-owned passthrough — the ONLY credential
|
|
277
|
+
* this adapter names. It is billed against the operator's own Claude seat, not
|
|
278
|
+
* against a Bridge-managed account, which is exactly why it is a passthrough
|
|
279
|
+
* and not a carrier.
|
|
280
|
+
*/
|
|
281
|
+
const oauthPassthrough = {
|
|
282
|
+
passthroughId: "claude-code-oauth-token",
|
|
283
|
+
envName: CLAUDE_OAUTH_TOKEN_ENV,
|
|
284
|
+
billingClass: "subscription",
|
|
285
|
+
ownership: "operator-owned",
|
|
286
|
+
rule: "forward-when-present",
|
|
287
|
+
};
|
|
288
|
+
const auth = {
|
|
289
|
+
// EXACTLY ZERO Bridge-managed carriers (BAPI-791). Not "none configured" —
|
|
290
|
+
// none exist. A worker authenticates through the operator's interactive
|
|
291
|
+
// login, and no Bridge-stored token may appear here.
|
|
292
|
+
managedAuthCarriers: [],
|
|
293
|
+
operatorOwnedPassthroughs: [oauthPassthrough],
|
|
294
|
+
};
|
|
295
|
+
const authFailureDetection = {
|
|
296
|
+
strategyId: "claude-stream-json-result",
|
|
297
|
+
classify: classifyClaudeAuthFailure,
|
|
298
|
+
};
|
|
299
|
+
const denyEnforcement = {
|
|
300
|
+
strategyId: "claude-settings-deny",
|
|
301
|
+
async provisionWorktreeDenyLayer(input) {
|
|
302
|
+
const provision = deps.provisionDenyLayer ?? provisionExecutorDenyLayer;
|
|
303
|
+
const provisioningDeps = deps.denyProvisioningDeps;
|
|
304
|
+
if (!provisioningDeps) {
|
|
305
|
+
// Fail-OPEN, matching the deny layer's established posture: a missing
|
|
306
|
+
// provisioning boundary shrinks safety margin but must not kill a job.
|
|
307
|
+
return {
|
|
308
|
+
ok: false,
|
|
309
|
+
warning: "deny-layer provisioning deps were not supplied to the Claude adapter; " +
|
|
310
|
+
"continuing fail-open",
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
return provision(input.worktreePath, { baseBranch: input.baseBranch }, provisioningDeps);
|
|
314
|
+
},
|
|
315
|
+
async probeEnforcement(options = {}) {
|
|
316
|
+
const probe = deps.runDenyPreflight ?? runDenyEnforcementPreflight;
|
|
317
|
+
const result = await probe(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs });
|
|
318
|
+
return {
|
|
319
|
+
enforced: result.enforced === true,
|
|
320
|
+
layer: String(result.layer),
|
|
321
|
+
degraded: result.degraded === true,
|
|
322
|
+
detail: typeof result.detail === "string" ? result.detail : "",
|
|
323
|
+
warnings: Array.isArray(result.warnings) ? [...result.warnings] : [],
|
|
324
|
+
};
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
const redaction = {
|
|
328
|
+
strategyId: "claude-env-name-redaction",
|
|
329
|
+
// The OAuth passthrough is the only secret this adapter's spawn can carry.
|
|
330
|
+
// Names only — this list can never hold a value.
|
|
331
|
+
secretEnvNames: [CLAUDE_OAUTH_TOKEN_ENV],
|
|
332
|
+
replacement: CLAUDE_REDACTION_REPLACEMENT,
|
|
333
|
+
};
|
|
334
|
+
const lifecycle = {
|
|
335
|
+
strategyId: "claude-no-lifecycle",
|
|
336
|
+
// EXPLICITLY none, not omitted. BAPI-790 deleted the per-job configuration
|
|
337
|
+
// directory that used to need cleanup and orphan sweeping, so there is
|
|
338
|
+
// genuinely nothing to do — and saying so is different from forgetting to.
|
|
339
|
+
declaration: { kind: "none" },
|
|
340
|
+
};
|
|
341
|
+
const advisoryMcpInspection = {
|
|
342
|
+
strategyId: "claude-user-config-shadowing",
|
|
343
|
+
async inspect(input) {
|
|
344
|
+
const inspect = deps.inspectClaudeUserConfig ?? inspectClaudeUserConfigForMcpShadowing;
|
|
345
|
+
const readFile = deps.readFile;
|
|
346
|
+
if (!readFile) {
|
|
347
|
+
return {
|
|
348
|
+
warnings: [
|
|
349
|
+
"Claude user-config MCP shadowing check did not run; no read boundary was supplied",
|
|
350
|
+
],
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
try {
|
|
354
|
+
const inspection = await inspect({
|
|
355
|
+
claudeConfigPath: resolveClaudeUserConfigPath(input.homedir, input.platform),
|
|
356
|
+
platform: input.platform,
|
|
357
|
+
cwd: input.cwd,
|
|
358
|
+
mainRepositoryPath: input.cwd,
|
|
359
|
+
...(input.worktreePath === undefined ? {} : { worktreePath: input.worktreePath }),
|
|
360
|
+
}, { readFile });
|
|
361
|
+
return { warnings: evaluateClaudeMcpShadowingPolicy(inspection).warnings };
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
// "Could not look" must never become "found a collision".
|
|
365
|
+
return {
|
|
366
|
+
warnings: [
|
|
367
|
+
"Claude user-config MCP shadowing check did not complete; could not verify worker MCP integrity",
|
|
368
|
+
],
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
return {
|
|
374
|
+
identity: {
|
|
375
|
+
agentId: spec.name,
|
|
376
|
+
adapterId: CLAUDE_ADAPTER_ID,
|
|
377
|
+
adapterVersion: CLAUDE_ADAPTER_VERSION,
|
|
378
|
+
strategyId: CLAUDE_ADAPTER_STRATEGY_ID,
|
|
379
|
+
},
|
|
380
|
+
executable: supported(executable),
|
|
381
|
+
platform: supported(platform),
|
|
382
|
+
headlessInvocation: supported(headlessInvocation),
|
|
383
|
+
mcpScoping: supported(mcpScoping),
|
|
384
|
+
mcpInitParsing: supported(mcpInitParsing),
|
|
385
|
+
auth: supported(auth),
|
|
386
|
+
authFailureDetection: supported(authFailureDetection),
|
|
387
|
+
denyEnforcement: supported(denyEnforcement),
|
|
388
|
+
redaction: supported(redaction),
|
|
389
|
+
lifecycle: supported(lifecycle),
|
|
390
|
+
advisoryMcpInspection: supported(advisoryMcpInspection),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { redactAdapterDiagnostic, validateExecutorAdapterCapabilities } from "./executor-adapter.js";
|
|
2
|
+
/** Bound on any probe failure text retained in the result. */
|
|
3
|
+
const FAILURE_DETAIL_MAX = 300;
|
|
4
|
+
/** Read a capability's value, or null when unsupported/absent. */
|
|
5
|
+
function capabilityValue(declared) {
|
|
6
|
+
if (!declared || declared.supported !== true)
|
|
7
|
+
return null;
|
|
8
|
+
return declared.value;
|
|
9
|
+
}
|
|
10
|
+
/** Bound and redact a caught error into safe detail text. */
|
|
11
|
+
function safeFailure(error, env, redaction) {
|
|
12
|
+
const raw = error instanceof Error ? error.message : String(error ?? "unknown error");
|
|
13
|
+
const redacted = redaction ? redactAdapterDiagnostic(raw, env, redaction) : raw;
|
|
14
|
+
return redacted.slice(0, FAILURE_DETAIL_MAX);
|
|
15
|
+
}
|
|
16
|
+
/** Project the auth declaration into metadata-only report shape. */
|
|
17
|
+
function summarizeAuth(auth) {
|
|
18
|
+
return {
|
|
19
|
+
managedCarriers: auth.managedAuthCarriers.map((carrier) => ({
|
|
20
|
+
carrierId: carrier.carrierId,
|
|
21
|
+
billingClass: carrier.billingClass,
|
|
22
|
+
ownership: carrier.ownership,
|
|
23
|
+
// NAMES only. The values these name are never read here.
|
|
24
|
+
secretEnvNames: carrier.secretEnvNames,
|
|
25
|
+
validationStrategyId: carrier.validationStrategyId,
|
|
26
|
+
})),
|
|
27
|
+
passthroughs: auth.operatorOwnedPassthroughs.map((passthrough) => ({
|
|
28
|
+
passthroughId: passthrough.passthroughId,
|
|
29
|
+
envName: passthrough.envName,
|
|
30
|
+
billingClass: passthrough.billingClass,
|
|
31
|
+
ownership: passthrough.ownership,
|
|
32
|
+
rule: passthrough.rule,
|
|
33
|
+
})),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Build the inspection for an adapter whose resolution already FAILED.
|
|
38
|
+
*
|
|
39
|
+
* A refused adapter still gets a report rather than an empty one: an operator
|
|
40
|
+
* looking at doctor needs to see which agent was selected and which capability
|
|
41
|
+
* was missing. Nothing is probed, because there is no validated adapter to probe
|
|
42
|
+
* with.
|
|
43
|
+
*/
|
|
44
|
+
export function inspectionForFailedResolution(agentName, resolution) {
|
|
45
|
+
return {
|
|
46
|
+
identity: {
|
|
47
|
+
agentId: agentName,
|
|
48
|
+
adapterId: "(unresolved)",
|
|
49
|
+
adapterVersion: "(unresolved)",
|
|
50
|
+
strategyId: "(unresolved)",
|
|
51
|
+
},
|
|
52
|
+
ok: false,
|
|
53
|
+
capabilities: [],
|
|
54
|
+
refusals: resolution.refusals ?? [],
|
|
55
|
+
executable: null,
|
|
56
|
+
platform: null,
|
|
57
|
+
auth: null,
|
|
58
|
+
lifecycleKind: null,
|
|
59
|
+
warnings: [resolution.message],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Inspect a RESOLVED adapter.
|
|
64
|
+
*
|
|
65
|
+
* Ordering is deliberately fail-fast and cheapest-first: capability validation
|
|
66
|
+
* (pure), then the platform gate (pure), and only then the probes that cost a
|
|
67
|
+
* subprocess. An adapter that is incomplete, or a platform that is unsupported,
|
|
68
|
+
* is settled before anything external runs — which is what lets preflight
|
|
69
|
+
* promise that a refused adapter triggers no probes at all.
|
|
70
|
+
*/
|
|
71
|
+
export async function inspectExecutorAgentAdapter(adapter, deps, options = {}) {
|
|
72
|
+
const validation = validateExecutorAdapterCapabilities(adapter);
|
|
73
|
+
const redaction = capabilityValue(adapter.redaction);
|
|
74
|
+
const warnings = [];
|
|
75
|
+
const base = {
|
|
76
|
+
identity: adapter.identity,
|
|
77
|
+
ok: validation.ok,
|
|
78
|
+
capabilities: validation.capabilities,
|
|
79
|
+
refusals: validation.ok ? [] : validation.refusals,
|
|
80
|
+
executable: null,
|
|
81
|
+
platform: null,
|
|
82
|
+
auth: null,
|
|
83
|
+
lifecycleKind: null,
|
|
84
|
+
warnings,
|
|
85
|
+
};
|
|
86
|
+
if (!validation.ok) {
|
|
87
|
+
// Incomplete adapter: report the refusals and probe NOTHING. Probing an
|
|
88
|
+
// adapter we have already refused would spend a subprocess to learn about a
|
|
89
|
+
// configuration that cannot be used.
|
|
90
|
+
return { inspection: base, denyEnforcement: null };
|
|
91
|
+
}
|
|
92
|
+
const auth = capabilityValue(adapter.auth);
|
|
93
|
+
if (auth)
|
|
94
|
+
base.auth = summarizeAuth(auth);
|
|
95
|
+
const lifecycle = capabilityValue(adapter.lifecycle);
|
|
96
|
+
if (lifecycle)
|
|
97
|
+
base.lifecycleKind = lifecycle.declaration.kind;
|
|
98
|
+
const platformCapability = capabilityValue(adapter.platform);
|
|
99
|
+
if (platformCapability) {
|
|
100
|
+
base.platform = platformCapability.evaluate(deps.platform);
|
|
101
|
+
}
|
|
102
|
+
const executableCapability = capabilityValue(adapter.executable);
|
|
103
|
+
if (executableCapability) {
|
|
104
|
+
base.executable = {
|
|
105
|
+
command: executableCapability.executable,
|
|
106
|
+
version: "",
|
|
107
|
+
fingerprint: null,
|
|
108
|
+
probed: false,
|
|
109
|
+
failure: null,
|
|
110
|
+
};
|
|
111
|
+
// An unsupported platform short-circuits the probe. Running the agent's
|
|
112
|
+
// binary on a host the adapter just declared unsupported tells us nothing
|
|
113
|
+
// actionable, and the platform verdict is already the answer.
|
|
114
|
+
const platformOk = base.platform === null || base.platform.supported;
|
|
115
|
+
if (options.probeExecutable === true && platformOk) {
|
|
116
|
+
try {
|
|
117
|
+
const output = await deps.runCommand(executableCapability.executable, [
|
|
118
|
+
...executableCapability.versionArgv,
|
|
119
|
+
]);
|
|
120
|
+
const identification = executableCapability.parseVersion(output);
|
|
121
|
+
base.executable = {
|
|
122
|
+
command: executableCapability.executable,
|
|
123
|
+
version: identification.version,
|
|
124
|
+
fingerprint: identification.fingerprint,
|
|
125
|
+
probed: true,
|
|
126
|
+
failure: output.exitCode === 0
|
|
127
|
+
? null
|
|
128
|
+
: `version probe exited ${output.exitCode}`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
base.executable = {
|
|
133
|
+
command: executableCapability.executable,
|
|
134
|
+
version: "",
|
|
135
|
+
fingerprint: null,
|
|
136
|
+
probed: true,
|
|
137
|
+
failure: safeFailure(error, deps.env, redaction),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
let denyEnforcement = null;
|
|
143
|
+
const denyCapability = capabilityValue(adapter.denyEnforcement);
|
|
144
|
+
if (options.probeDenyEnforcement === true && denyCapability) {
|
|
145
|
+
try {
|
|
146
|
+
denyEnforcement = await denyCapability.probeEnforcement(options.denyProbeTimeoutMs === undefined ? {} : { timeoutMs: options.denyProbeTimeoutMs });
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
// A THROWN probe is not "unknown", it is UNVERIFIED — and unverified deny
|
|
150
|
+
// enforcement is fail-closed everywhere else, so it is represented here as
|
|
151
|
+
// an explicit not-enforced verdict rather than a null the caller might
|
|
152
|
+
// read as "skipped".
|
|
153
|
+
denyEnforcement = {
|
|
154
|
+
enforced: false,
|
|
155
|
+
layer: "none",
|
|
156
|
+
degraded: true,
|
|
157
|
+
detail: safeFailure(error, deps.env, redaction),
|
|
158
|
+
warnings: ["deny-enforcement probe threw; enforcement is UNVERIFIED on this host"],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return { inspection: base, denyEnforcement };
|
|
163
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { resolveAgentSpec } from "../agent-registry.js";
|
|
2
|
+
import { createClaudeExecutorAdapter } from "./claude-executor-adapter.js";
|
|
3
|
+
import { formatAdapterRefusal, validateExecutorAdapterCapabilities } from "./executor-adapter.js";
|
|
4
|
+
/**
|
|
5
|
+
* The strategy → factory table. The ONLY place an adapter implementation is
|
|
6
|
+
* bound to a strategy identifier.
|
|
7
|
+
*
|
|
8
|
+
* Exactly one entry today. Codex and cursor-agent are deliberately absent rather
|
|
9
|
+
* than present-and-stubbed: an absent strategy produces an explicit unsupported
|
|
10
|
+
* result, whereas a stub would produce an adapter that passes resolution and
|
|
11
|
+
* fails later, further from the cause.
|
|
12
|
+
*/
|
|
13
|
+
const ADAPTER_FACTORIES = {
|
|
14
|
+
"claude-strict-mcp-v1": (spec, deps) => createClaudeExecutorAdapter(spec, deps.claude ?? {}),
|
|
15
|
+
};
|
|
16
|
+
/** List the strategy identifiers that have an implementation. */
|
|
17
|
+
export function listImplementedAdapterStrategies() {
|
|
18
|
+
return Object.keys(ADAPTER_FACTORIES).sort();
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolve `agentName` to a fully validated executor adapter.
|
|
22
|
+
*
|
|
23
|
+
* Order matters and is fail-closed at every step: the agent must exist, it must
|
|
24
|
+
* carry adapter metadata, that metadata's strategy must have an implementation,
|
|
25
|
+
* the built adapter's identity must match the metadata, and every required
|
|
26
|
+
* capability must be supported. Only then is an adapter returned.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveExecutorAgentAdapter(agentName, deps = {}) {
|
|
29
|
+
const spec = resolveAgentSpec(agentName);
|
|
30
|
+
if (!spec) {
|
|
31
|
+
return {
|
|
32
|
+
supported: false,
|
|
33
|
+
kind: "unknown-agent",
|
|
34
|
+
message: `no agent named '${agentName}' is registered; cannot resolve an executor adapter`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const registration = spec.executorAdapter;
|
|
38
|
+
if (!registration) {
|
|
39
|
+
return {
|
|
40
|
+
supported: false,
|
|
41
|
+
kind: "no-adapter-registration",
|
|
42
|
+
message: `agent '${spec.name}' declares no executor adapter registration, so it cannot spawn ` +
|
|
43
|
+
"conductor workers. This is not a fallback condition: no other agent's adapter is substituted.",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const factory = ADAPTER_FACTORIES[registration.strategyId];
|
|
47
|
+
if (!factory) {
|
|
48
|
+
return {
|
|
49
|
+
supported: false,
|
|
50
|
+
kind: "unimplemented-strategy",
|
|
51
|
+
message: `executor adapter strategy '${registration.strategyId}' (agent '${spec.name}') has no ` +
|
|
52
|
+
`implementation. Implemented strategies: ${listImplementedAdapterStrategies().join(", ")}.`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const adapter = factory(spec, deps);
|
|
56
|
+
// Cross-check the built adapter against the metadata that selected it. A
|
|
57
|
+
// factory returning an implementation for a different agent/strategy/version
|
|
58
|
+
// would otherwise be reported under the wrong identity everywhere downstream.
|
|
59
|
+
const identity = adapter.identity;
|
|
60
|
+
const mismatches = [];
|
|
61
|
+
if (identity.agentId !== spec.name) {
|
|
62
|
+
mismatches.push(`agentId '${identity.agentId}' != registry agent '${spec.name}'`);
|
|
63
|
+
}
|
|
64
|
+
if (identity.adapterId !== registration.adapterId) {
|
|
65
|
+
mismatches.push(`adapterId '${identity.adapterId}' != registered '${registration.adapterId}'`);
|
|
66
|
+
}
|
|
67
|
+
if (identity.strategyId !== registration.strategyId) {
|
|
68
|
+
mismatches.push(`strategyId '${identity.strategyId}' != registered '${registration.strategyId}'`);
|
|
69
|
+
}
|
|
70
|
+
if (identity.adapterVersion !== registration.adapterVersion) {
|
|
71
|
+
mismatches.push(`adapterVersion '${identity.adapterVersion}' != registered '${registration.adapterVersion}'`);
|
|
72
|
+
}
|
|
73
|
+
if (mismatches.length > 0) {
|
|
74
|
+
return {
|
|
75
|
+
supported: false,
|
|
76
|
+
kind: "identity-mismatch",
|
|
77
|
+
message: `executor adapter identity does not match its registry registration: ${mismatches.join("; ")}`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const validation = validateExecutorAdapterCapabilities(adapter);
|
|
81
|
+
if (!validation.ok) {
|
|
82
|
+
return {
|
|
83
|
+
supported: false,
|
|
84
|
+
kind: "incomplete-capabilities",
|
|
85
|
+
message: formatAdapterRefusal(identity, validation.refusals),
|
|
86
|
+
refusals: validation.refusals,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return { supported: true, adapter, spec, validation };
|
|
90
|
+
}
|