@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.
Files changed (85) hide show
  1. package/README.md +189 -14
  2. package/build/agent-capabilities/probe-context.js +2 -1
  3. package/build/agent-launchers/claude-executor-adapter.js +392 -0
  4. package/build/agent-launchers/executor-adapter-inspection.js +163 -0
  5. package/build/agent-launchers/executor-adapter-registry.js +90 -0
  6. package/build/agent-launchers/executor-adapter.js +136 -0
  7. package/build/agent-registry.js +28 -0
  8. package/build/agents.generated.js +1 -1
  9. package/build/claude-login.js +85 -0
  10. package/build/claude-user-config-doctor.js +59 -33
  11. package/build/commands.generated.js +12 -11
  12. package/build/conduct-epic/bridge-client.js +345 -0
  13. package/build/conduct-epic/checkpoint-store.js +479 -0
  14. package/build/conduct-epic/cli.js +1765 -0
  15. package/build/conduct-epic/lock.js +302 -0
  16. package/build/conduct-epic/pr-state.js +286 -0
  17. package/build/conduct-epic/spawn.js +101 -0
  18. package/build/conductor/bridge-api-client.js +37 -2
  19. package/build/conductor/doctor.js +11 -1
  20. package/build/conductor/install-doctor.js +184 -10
  21. package/build/conductor-bin.js +7 -7
  22. package/build/credential-store.js +10 -4
  23. package/build/credentials-cli.js +34 -19
  24. package/build/docs.generated.js +1 -1
  25. package/build/doctor.js +579 -88
  26. package/build/executor/agent-identity.js +32 -0
  27. package/build/executor/cli.js +50 -39
  28. package/build/executor/deps.js +15 -1
  29. package/build/executor/env.js +56 -45
  30. package/build/executor/index.js +9 -1
  31. package/build/executor/install-preflight.js +138 -0
  32. package/build/executor/job-errors.js +200 -0
  33. package/build/executor/job-runner.js +619 -268
  34. package/build/executor/observation.js +165 -0
  35. package/build/executor/permissions.js +163 -36
  36. package/build/executor/platform.js +54 -0
  37. package/build/executor/preflight.js +175 -67
  38. package/build/executor/process.js +39 -7
  39. package/build/executor/runner.js +19 -0
  40. package/build/executor/service-lifecycle.js +269 -0
  41. package/build/executor/service-unit.js +121 -12
  42. package/build/executor/stale-artifacts.js +70 -0
  43. package/build/executor/test-clock.js +188 -24
  44. package/build/executor/worker-command.js +22 -58
  45. package/build/executor/worker-log.js +82 -0
  46. package/build/executor/worktree-lock.js +264 -0
  47. package/build/index.js +527 -357
  48. package/build/install-bridge-conductor.js +376 -38
  49. package/build/install-bridge.js +414 -114
  50. package/build/install-doctor.js +13 -0
  51. package/build/install-reexec.js +5 -3
  52. package/build/mcp-install-state.js +130 -0
  53. package/build/mcp-profile.js +11 -2
  54. package/build/mcp-provisioning.js +15 -0
  55. package/build/merge-pull-request.js +562 -0
  56. package/build/phase-result-artifacts.js +450 -0
  57. package/build/pipeline-orchestrator.js +4 -0
  58. package/build/pipeline-utils.js +16 -0
  59. package/build/pipelines.generated.js +7 -7
  60. package/build/plane/preflight.js +18 -14
  61. package/build/plane/supervisor.js +8 -1
  62. package/build/project-root.js +34 -0
  63. package/build/readme.generated.js +1 -1
  64. package/build/run-unit-tests-launcher.js +36 -9
  65. package/build/setup-epic.js +57 -4
  66. package/build/sfcc/permissions.js +25 -6
  67. package/build/sfcc/reads-site-preference.js +6 -0
  68. package/build/sfcc/register.js +61 -23
  69. package/build/sfcc/registration-inventory.js +89 -0
  70. package/build/sfcc/setup-status.js +18 -34
  71. package/build/sfcc/tool-wrapper.js +294 -17
  72. package/build/sfcc/write-grants.js +33 -1
  73. package/build/sfcc/write-guard.js +41 -12
  74. package/build/sfcc/writes-custom-object-def.js +6 -2
  75. package/build/sfcc/writes-site-preference.js +6 -1
  76. package/build/sfcc/writes-system-object.js +11 -2
  77. package/build/sfcc/writes.js +13 -8
  78. package/build/start-tickets-prereqs.js +25 -15
  79. package/build/start-tickets.js +123 -21
  80. package/build/version.generated.js +1 -1
  81. package/build/worktree-core.js +9 -3
  82. package/docs/install/mcp-tool-integrations.md +54 -9
  83. package/docs/install/sfcc-integration.md +71 -24
  84. package/package.json +3 -3
  85. 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
+ }