@bridge_gpt/mcp-server 0.2.38 → 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 +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 +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/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,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The executor's selected agent identity (BAPI-781).
|
|
3
|
+
*
|
|
4
|
+
* WHY A MODULE FOR ONE STRING. Before the adapter extraction the literal
|
|
5
|
+
* `"claude"` appeared throughout the spawn path — in the version probe, the
|
|
6
|
+
* model lookup, the deny probe, the executable name. Replacing those with a
|
|
7
|
+
* single shared constant is what lets `ExecutorOptions.agentId` default without
|
|
8
|
+
* every generic module re-hard-coding the same default, and it gives the
|
|
9
|
+
* eventual "make this configurable" change exactly one place to touch.
|
|
10
|
+
*
|
|
11
|
+
* Claude remains the default because it is the only implemented adapter. That is
|
|
12
|
+
* a statement about what exists, not a fallback: an agent explicitly selected
|
|
13
|
+
* but unimplemented resolves to an explicit unsupported result rather than
|
|
14
|
+
* quietly landing back here (see `executor-adapter-registry.ts`).
|
|
15
|
+
*/
|
|
16
|
+
import { DEFAULT_AGENT_NAME } from "../agent-registry.js";
|
|
17
|
+
/** The agent an executor spawns workers with when none is configured. */
|
|
18
|
+
export const DEFAULT_EXECUTOR_AGENT_ID = DEFAULT_AGENT_NAME;
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the effective agent id for a set of executor options.
|
|
21
|
+
*
|
|
22
|
+
* Normalizes blank/whitespace-only configuration to the default rather than
|
|
23
|
+
* passing it through: an empty string would resolve to `unknown-agent` and
|
|
24
|
+
* refuse every claim, which is a confusing way to report "you configured
|
|
25
|
+
* nothing".
|
|
26
|
+
*/
|
|
27
|
+
export function resolveExecutorAgentId(agentId) {
|
|
28
|
+
if (typeof agentId !== "string")
|
|
29
|
+
return DEFAULT_EXECUTOR_AGENT_ID;
|
|
30
|
+
const trimmed = agentId.trim();
|
|
31
|
+
return trimmed.length > 0 ? trimmed : DEFAULT_EXECUTOR_AGENT_ID;
|
|
32
|
+
}
|
package/build/executor/cli.js
CHANGED
|
@@ -10,13 +10,12 @@ import os from "node:os";
|
|
|
10
10
|
import { VERSION } from "../version.generated.js";
|
|
11
11
|
import { resolveWorktrunkBinary } from "../start-tickets-prereqs.js";
|
|
12
12
|
import { resolveStartTicketsRepoName } from "../start-tickets-repo.js";
|
|
13
|
+
import { DEFAULT_EXECUTOR_AGENT_ID } from "./agent-identity.js";
|
|
13
14
|
import { createDefaultExecutorDeps } from "./deps.js";
|
|
14
15
|
import { resolveBaseUrl, resolveExecutorApiAccess, EXECUTOR_BASE_URL_REQUIRED_MESSAGE, } from "./credentials.js";
|
|
15
16
|
import { createExecutorHttpClient } from "./http-client.js";
|
|
16
17
|
import { runExecutor } from "./runner.js";
|
|
17
18
|
import { runExecutorWatchCli } from "./watch-cli.js";
|
|
18
|
-
import { sweepOrphanedWorkerConfigDirectories } from "./worker-config-isolation.js";
|
|
19
|
-
import { pathApiForProvisioningPlatform } from "../mcp-provisioning.js";
|
|
20
19
|
/** Fixed executor timing/behavior defaults. */
|
|
21
20
|
const DEFAULT_POLL_INTERVAL_MS = 15_000;
|
|
22
21
|
const DEFAULT_HEARTBEAT_INTERVAL_MS = 60_000;
|
|
@@ -39,6 +38,8 @@ export function getExecutorUsage() {
|
|
|
39
38
|
"Options:",
|
|
40
39
|
" --repo <name> Repo to serve (repeatable).",
|
|
41
40
|
" --repos=<a,b> Comma-separated repos.",
|
|
41
|
+
" --epic-run-id <id> Dedicate this executor to an epic run (repeatable).",
|
|
42
|
+
" Omit for the default repository-wide behavior.",
|
|
42
43
|
" --base-url <url> Bridge API endpoint (overrides BAPI_BASE_URL).",
|
|
43
44
|
" --executor-id <id> Stable executor id (default: <hostname>-<pid>).",
|
|
44
45
|
" --max-concurrent <n> Max concurrent jobs (>= 1, default 1).",
|
|
@@ -66,9 +67,21 @@ function parseIntArg(value, flag) {
|
|
|
66
67
|
}
|
|
67
68
|
return { ok: true, value: n };
|
|
68
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* BAPI-794 — `epic_run_id` is always server-minted as `str(uuid.uuid4())`
|
|
72
|
+
* (`api/library/db/epic_runs.py`), so a standard UUID shape (any version) is a
|
|
73
|
+
* cheap, safe pre-validation: it rejects an obvious typo before any credential
|
|
74
|
+
* resolution or executor-loop startup, without over-constraining the CLI to a
|
|
75
|
+
* version the server does not itself enforce.
|
|
76
|
+
*/
|
|
77
|
+
const EPIC_RUN_ID_PATTERN = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
78
|
+
function isValidEpicRunId(value) {
|
|
79
|
+
return EPIC_RUN_ID_PATTERN.test(value);
|
|
80
|
+
}
|
|
69
81
|
/** Parse the executor CLI arguments into resolved options (pure, synchronous). */
|
|
70
82
|
export function parseExecutorArgs(argv, context) {
|
|
71
83
|
const repos = [];
|
|
84
|
+
const epicRunIds = [];
|
|
72
85
|
let executorId;
|
|
73
86
|
let maxConcurrent = DEFAULT_MAX_CONCURRENT;
|
|
74
87
|
let once = false;
|
|
@@ -97,6 +110,17 @@ export function parseExecutorArgs(argv, context) {
|
|
|
97
110
|
return { kind: "error", message: "--repos requires a value" };
|
|
98
111
|
repos.push(...v.split(",").map((s) => s.trim()).filter(Boolean));
|
|
99
112
|
}
|
|
113
|
+
else if (arg === "--epic-run-id") {
|
|
114
|
+
const v = argv[++i];
|
|
115
|
+
if (!v || !v.trim())
|
|
116
|
+
return { kind: "error", message: "--epic-run-id requires a value" };
|
|
117
|
+
const trimmed = v.trim();
|
|
118
|
+
if (!isValidEpicRunId(trimmed)) {
|
|
119
|
+
return { kind: "error", message: `--epic-run-id has an invalid value: ${trimmed}` };
|
|
120
|
+
}
|
|
121
|
+
if (!epicRunIds.includes(trimmed))
|
|
122
|
+
epicRunIds.push(trimmed);
|
|
123
|
+
}
|
|
100
124
|
else if (arg === "--executor-id") {
|
|
101
125
|
executorId = argv[++i];
|
|
102
126
|
if (!executorId)
|
|
@@ -172,28 +196,20 @@ export function parseExecutorArgs(argv, context) {
|
|
|
172
196
|
advisoryParserEnabled,
|
|
173
197
|
defaultJobTimeoutSeconds: DEFAULT_JOB_TIMEOUT_SECONDS,
|
|
174
198
|
baseUrl,
|
|
199
|
+
// BAPI-781 — the agent whose executor adapter owns argv, environment, MCP
|
|
200
|
+
// scoping, auth detection, deny enforcement, and lifecycle. Set explicitly
|
|
201
|
+
// at the bootstrap boundary so the literal `"claude"` does not reappear
|
|
202
|
+
// inside generic spawn code. There is no CLI flag for it yet: Claude is the
|
|
203
|
+
// only implemented adapter, and offering a flag that can only fail closed
|
|
204
|
+
// would advertise a choice that does not exist.
|
|
205
|
+
agentId: DEFAULT_EXECUTOR_AGENT_ID,
|
|
206
|
+
// BAPI-794 — `undefined` (never an empty array) when no --epic-run-id flag
|
|
207
|
+
// was supplied, so downstream `epicRunIds !== undefined` checks (the claim
|
|
208
|
+
// manifest builder, startup diagnostics) see an unambiguous "unscoped".
|
|
209
|
+
...(epicRunIds.length > 0 ? { epicRunIds } : {}),
|
|
175
210
|
};
|
|
176
211
|
return { kind: "ok", options };
|
|
177
212
|
}
|
|
178
|
-
/**
|
|
179
|
-
* Build the sweep boundary from executor deps, or `null` when this executor was
|
|
180
|
-
* constructed without the filesystem operations the sweep needs. `null` simply
|
|
181
|
-
* skips the sweep — unlike isolation itself, a missing sweep is not a safety
|
|
182
|
-
* problem, only uncollected residue.
|
|
183
|
-
*/
|
|
184
|
-
function buildWorkerConfigSweepDeps(deps) {
|
|
185
|
-
const { tmpdir, readdir, lstatPath, rmRecursive } = deps;
|
|
186
|
-
if (!tmpdir || !readdir || !lstatPath || !rmRecursive)
|
|
187
|
-
return null;
|
|
188
|
-
return {
|
|
189
|
-
tmpdir,
|
|
190
|
-
readdir,
|
|
191
|
-
lstatPath,
|
|
192
|
-
rmRecursive,
|
|
193
|
-
join: (...segments) => pathApiForProvisioningPlatform(deps.platform).join(...segments),
|
|
194
|
-
now: deps.now,
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
213
|
function hasRepoFlag(argv) {
|
|
198
214
|
return argv.some((a) => a === "--repo" || a === "--repos" || a.startsWith("--repos="));
|
|
199
215
|
}
|
|
@@ -235,6 +251,15 @@ export async function runExecutorCli(argv, overrides = {}) {
|
|
|
235
251
|
return 1;
|
|
236
252
|
}
|
|
237
253
|
const options = parsed.options;
|
|
254
|
+
// BAPI-794 — startup diagnostics: only non-secret run identifiers, never
|
|
255
|
+
// credentials, job payloads, or command arguments beyond the run IDs
|
|
256
|
+
// themselves (which are not secrets — see EPIC_RUN_ID_PATTERN's docstring).
|
|
257
|
+
if (options.epicRunIds !== undefined) {
|
|
258
|
+
errorLog(`executor scoped to epic run(s): ${options.epicRunIds.join(", ")}`);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
errorLog("executor running repository-wide (no --epic-run-id configured)");
|
|
262
|
+
}
|
|
238
263
|
// The mutating executor requires an EXPLICIT base URL (BAPI-676): `--base-url`
|
|
239
264
|
// then `BAPI_BASE_URL`, never an implicit production default. Fail here —
|
|
240
265
|
// before any credential resolution, HTTP client, or claim side effect.
|
|
@@ -273,24 +298,10 @@ export async function runExecutorCli(argv, overrides = {}) {
|
|
|
273
298
|
mcpVersion: VERSION,
|
|
274
299
|
fetch: deps.fetch,
|
|
275
300
|
});
|
|
276
|
-
// BAPI-
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
// failure is a bounded diagnostic, because refusing to start over leftover
|
|
281
|
-
// temp directories would be a worse failure than the leftovers.
|
|
282
|
-
const sweep = overrides.sweepOrphanedWorkerConfigDirectories ?? sweepOrphanedWorkerConfigDirectories;
|
|
283
|
-
try {
|
|
284
|
-
const isolationSweepDeps = buildWorkerConfigSweepDeps(deps);
|
|
285
|
-
if (isolationSweepDeps) {
|
|
286
|
-
const swept = await sweep(isolationSweepDeps);
|
|
287
|
-
for (const diagnostic of swept.diagnostics)
|
|
288
|
-
errorLog(diagnostic);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
catch {
|
|
292
|
-
errorLog("worker config isolation sweep did not complete; continuing startup");
|
|
293
|
-
}
|
|
301
|
+
// BAPI-790: the startup orphan sweep is gone with the thing it swept. The
|
|
302
|
+
// executor no longer creates a per-job Claude configuration directory, so an
|
|
303
|
+
// ungraceful exit (SIGKILL, OOM, host reboot) leaves no residue for a sweep to
|
|
304
|
+
// reclaim and startup has one less filesystem step to get wrong.
|
|
294
305
|
const run = overrides.runExecutor ?? runExecutor;
|
|
295
306
|
// Hand the loop the VALIDATED runtime configuration: the parsed options plus the
|
|
296
307
|
// normalized base URL resolution actually settled on.
|
package/build/executor/deps.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { execFile, spawn } from "node:child_process";
|
|
11
11
|
import { existsSync } from "node:fs";
|
|
12
|
-
import { readFile, writeFile, appendFile, mkdir, mkdtemp, chmod, rm, readdir, lstat, stat, statfs, } from "node:fs/promises";
|
|
12
|
+
import { open, readFile, writeFile, appendFile, mkdir, mkdtemp, chmod, rm, readdir, lstat, stat, statfs, } from "node:fs/promises";
|
|
13
13
|
import os from "node:os";
|
|
14
14
|
import { promisify } from "node:util";
|
|
15
15
|
import { resolveMcpShimInvocationForRuntime } from "../mcp-server-invocation.js";
|
|
@@ -77,6 +77,20 @@ export function createDefaultExecutorDeps() {
|
|
|
77
77
|
writeFile: (filePath, data) => writeFile(filePath, data, "utf-8"),
|
|
78
78
|
appendFile: (filePath, data) => appendFile(filePath, data, "utf-8"),
|
|
79
79
|
mkdir: (dirPath, opts) => mkdir(dirPath, opts),
|
|
80
|
+
// BAPI-793 worktree lock. `open(..., "wx")` is atomic at the OS level: it
|
|
81
|
+
// either creates the file or rejects with EEXIST, with no window between the
|
|
82
|
+
// two. `0o600` keeps the fencing claim token in the payload readable only by
|
|
83
|
+
// the operator who owns the executor.
|
|
84
|
+
writeFileExclusive: async (filePath, data) => {
|
|
85
|
+
const handle = await open(filePath, "wx", 0o600);
|
|
86
|
+
try {
|
|
87
|
+
await handle.writeFile(data, "utf-8");
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
await handle.close();
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
removeFile: (filePath) => rm(filePath, { force: true }),
|
|
80
94
|
stat: (filePath) => stat(filePath).then((s) => ({ mode: s.mode })),
|
|
81
95
|
statMtimeMs: (filePath) => stat(filePath)
|
|
82
96
|
.then((s) => s.mtimeMs)
|
package/build/executor/env.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Secret-free executor worker environment (BAPI-534, TDD §7
|
|
2
|
+
* Secret-free GENERIC executor worker environment (BAPI-534, TDD §7; BAPI-790,
|
|
3
|
+
* BAPI-791; generalized in BAPI-781).
|
|
3
4
|
*
|
|
4
5
|
* v2 executor workers get Bridge MCP access by default (BAPI-724), but NOT
|
|
5
6
|
* through this environment. Access is provisioned into the worktree's
|
|
@@ -12,11 +13,31 @@
|
|
|
12
13
|
* provisioning time instead), and all secret-bearing keys. It constructs a
|
|
13
14
|
* FRESH object from a strict allowlist of non-secret operational keys — it
|
|
14
15
|
* never copies arbitrary `process.env`, so credentials/tokens/headers cannot
|
|
15
|
-
* leak into
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* `
|
|
16
|
+
* leak into a spawned worker process.
|
|
17
|
+
*
|
|
18
|
+
* THIS BUILDER IS NOW AGENT-NEUTRAL AND CARRIES NO SECRET AT ALL (BAPI-781).
|
|
19
|
+
* The single `CLAUDE_CODE_OAUTH_TOKEN` passthrough that used to live here moved
|
|
20
|
+
* into the Claude adapter (`agent-launchers/claude-executor-adapter.ts`), which
|
|
21
|
+
* layers it on top of this base. That relocation is the point of the extraction:
|
|
22
|
+
* forwarding an operator's Claude credential is an AGENT-SPECIFIC decision, and
|
|
23
|
+
* a generic builder that knew about it would keep every future adapter
|
|
24
|
+
* Claude-shaped. A different adapter forwards a different name — or none — and
|
|
25
|
+
* must opt in explicitly rather than inherit Claude's exception.
|
|
26
|
+
*
|
|
27
|
+
* `ANTHROPIC_API_KEY` remains absolutely denied here, forwarded or not, and no
|
|
28
|
+
* adapter can re-admit it through this function — see `EXPLICIT_DENY_KEYS`.
|
|
29
|
+
*
|
|
30
|
+
* `CLAUDE_CONFIG_DIR` and `XDG_CONFIG_HOME` REMAIN on the deny list even though
|
|
31
|
+
* the executor no longer sets them. The deny list defends against the operator's
|
|
32
|
+
* own ambient values: a redirect exported in the shell that launched the
|
|
33
|
+
* executor must not reach a worker and silently change which configuration —
|
|
34
|
+
* and which MCP registrations — the CLI resolves.
|
|
35
|
+
*
|
|
36
|
+
* The `mcp-invoke` shim (a separate process) resolves `BAPI_API_KEY` itself at
|
|
37
|
+
* call time from the operator's real user-scoped credential store, exactly as it
|
|
38
|
+
* did before BAPI-731, so this worker environment never carries it. The model is
|
|
39
|
+
* still denied direct reads of that store by the deny layer
|
|
40
|
+
* (`Read(~/.config/bridge/**)` in `permissions.ts`).
|
|
20
41
|
*/
|
|
21
42
|
import { PR_BASE_BRANCH_ENV_VAR } from "../pr-base-contract.js";
|
|
22
43
|
/** Non-secret operational keys forwarded to the worker when present. */
|
|
@@ -39,17 +60,33 @@ const SECRET_NAME_FRAGMENTS = ["TOKEN", "SECRET", "PASSWORD", "API_KEY"];
|
|
|
39
60
|
/**
|
|
40
61
|
* Explicit deny-list of known secret / MCP / conductor keys.
|
|
41
62
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
63
|
+
* The two Claude configuration paths were added by BAPI-731, when the executor
|
|
64
|
+
* denied the inherited values and then set its own. BAPI-790 removed the setting
|
|
65
|
+
* half and KEPT the denying half: a worker must inherit the operator's real
|
|
66
|
+
* configuration (that is how it authenticates now), but it must inherit the
|
|
67
|
+
* DEFAULT one. An operator who exports `CLAUDE_CONFIG_DIR` or `XDG_CONFIG_HOME`
|
|
68
|
+
* in the shell that launches the executor would otherwise silently redirect every
|
|
69
|
+
* worker at a different configuration — and a different MCP registration set —
|
|
70
|
+
* without anything in the executor choosing that.
|
|
47
71
|
*/
|
|
48
|
-
const EXPLICIT_DENY_KEYS = [
|
|
72
|
+
export const EXPLICIT_DENY_KEYS = [
|
|
49
73
|
"BRIDGE_MCP_PROFILE",
|
|
50
74
|
"CONDUCTOR_NODE_PATH",
|
|
51
75
|
"BAPI_API_KEY",
|
|
76
|
+
// ANTHROPIC_API_KEY has NO passthrough exception (BAPI-791): it stays denied
|
|
77
|
+
// absolutely, unlike CLAUDE_CODE_OAUTH_TOKEN below, which is denied here too
|
|
78
|
+
// but is deliberately let back in through the single explicit branch in
|
|
79
|
+
// the Claude adapter's own env builder, after this allowlisted-copy loop runs.
|
|
52
80
|
"ANTHROPIC_API_KEY",
|
|
81
|
+
// CLAUDE_CODE_OAUTH_TOKEN is now denied here EXPLICITLY (BAPI-781). Under
|
|
82
|
+
// BAPI-791 it had to stay off this list, because the generic builder itself
|
|
83
|
+
// re-admitted it through a single unchanged-copy branch and an explicit deny
|
|
84
|
+
// would have cancelled that branch. The branch has moved into the Claude
|
|
85
|
+
// adapter, which layers the key on top of the object this function returns —
|
|
86
|
+
// so denying it here no longer breaks the passthrough, and it makes the
|
|
87
|
+
// generic base environment unambiguously credential-free rather than
|
|
88
|
+
// credential-free-by-omission.
|
|
89
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
53
90
|
"OPENAI_API_KEY",
|
|
54
91
|
"GITHUB_TOKEN",
|
|
55
92
|
"GH_TOKEN",
|
|
@@ -82,7 +119,7 @@ export function isExecutorEnvKeyAllowed(key) {
|
|
|
82
119
|
return false;
|
|
83
120
|
return true;
|
|
84
121
|
}
|
|
85
|
-
export function
|
|
122
|
+
export function buildExecutorBaseWorkerEnv(parentEnv, options = {}) {
|
|
86
123
|
const env = {};
|
|
87
124
|
for (const key of ALLOWED_ENV_KEYS) {
|
|
88
125
|
if (!isExecutorEnvKeyAllowed(key))
|
|
@@ -97,37 +134,11 @@ export function buildExecutorWorkerEnv(parentEnv, options = {}) {
|
|
|
97
134
|
options.effectiveBaseBranch.length > 0) {
|
|
98
135
|
env[PR_BASE_BRANCH_ENV_VAR] = options.effectiveBaseBranch;
|
|
99
136
|
}
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
if (typeof options.claudeConfigDir === "string" && options.claudeConfigDir.length > 0) {
|
|
107
|
-
env.CLAUDE_CONFIG_DIR = options.claudeConfigDir;
|
|
108
|
-
}
|
|
109
|
-
if (typeof options.xdgConfigHome === "string" && options.xdgConfigHome.length > 0) {
|
|
110
|
-
env.XDG_CONFIG_HOME = options.xdgConfigHome;
|
|
111
|
-
}
|
|
112
|
-
// The ONE deliberately-injected credential, and a genuine exception to this
|
|
113
|
-
// module's otherwise absolute secret-free rule. It is injected because
|
|
114
|
-
// isolation REMOVES the worker's previous authentication path: a worker used to
|
|
115
|
-
// authenticate as a side effect of inheriting the operator's `HOME` and
|
|
116
|
-
// `~/.claude.json`, and that inheritance is exactly the defect being closed.
|
|
117
|
-
// Per the measured inventory (`docs/claude/claude-cli-config-isolation-inventory.md`,
|
|
118
|
-
// finding 4) an operator OAuth/subscription login does NOT follow an isolated
|
|
119
|
-
// `CLAUDE_CONFIG_DIR`, while `ANTHROPIC_API_KEY` does — so this is the only
|
|
120
|
-
// supported way an isolated worker can authenticate at all.
|
|
121
|
-
//
|
|
122
|
-
// It is passed from an EXPLICIT executor-resolved value rather than copied
|
|
123
|
-
// (`ANTHROPIC_API_KEY` remains on `EXPLICIT_DENY_KEYS` and still matches the
|
|
124
|
-
// `API_KEY` secret fragment), so it travels only when the isolation strategy
|
|
125
|
-
// deliberately supplies it, never as ambient leakage. Nothing writes it to
|
|
126
|
-
// disk: the only file seeded into the isolated directory is the repo-scoped
|
|
127
|
-
// Bridge credential store (see `worker-config-isolation.ts`), which never
|
|
128
|
-
// carries the Anthropic key.
|
|
129
|
-
if (typeof options.anthropicApiKey === "string" && options.anthropicApiKey.length > 0) {
|
|
130
|
-
env.ANTHROPIC_API_KEY = options.anthropicApiKey;
|
|
131
|
-
}
|
|
137
|
+
// Nothing agent-specific is added here, and deliberately nothing secret. An
|
|
138
|
+
// adapter that needs to forward an operator-owned credential declares it as a
|
|
139
|
+
// passthrough and copies it onto the object this function returns; see
|
|
140
|
+
// `buildClaudeWorkerEnv`. Keeping the base free of every credential is what
|
|
141
|
+
// makes "which secrets can reach a worker?" answerable per adapter instead of
|
|
142
|
+
// per call site.
|
|
132
143
|
return env;
|
|
133
144
|
}
|
package/build/executor/index.js
CHANGED
|
@@ -10,5 +10,13 @@ export { runExecutor } from "./runner.js";
|
|
|
10
10
|
// Persistent-service packaging (BAPI-688). The CLI entry plus the PURE helpers
|
|
11
11
|
// doctor needs to enumerate and inspect generated units — doctor must share this
|
|
12
12
|
// module's naming/format contract instead of maintaining a second parser.
|
|
13
|
-
export { runExecutorInstallServiceCli, getExecutorInstallServiceUsage, buildExecutorServiceArtifact, writeExecutorServiceArtifact, inspectExecutorServiceArtifact, executorLaunchdDirForHome, executorSystemdDirForHome, executorIdFromLaunchdFilename, executorIdFromSystemdFilename,
|
|
13
|
+
export { runExecutorInstallServiceCli, getExecutorInstallServiceUsage, buildExecutorServiceArtifact, writeExecutorServiceArtifact, inspectExecutorServiceArtifact, executorLaunchdDirForHome, executorSystemdDirForHome, executorIdFromLaunchdFilename, executorIdFromSystemdFilename,
|
|
14
|
+
// Programmatic, lifecycle-free plan/write seam (BAPI-779) — how
|
|
15
|
+
// `install conductor` reaches the generator without shelling out to the CLI.
|
|
16
|
+
planExecutorServiceInstall, writeExecutorServicePlan, resolvePackagedExecutorInvocation, EXECUTOR_SERVICE_BASE_URL_ENV, EXECUTOR_SERVICE_PATH_ENV, } from "./service-unit.js";
|
|
17
|
+
// Consented lifecycle (BAPI-779, R-1). Deliberately a SEPARATE module from the
|
|
18
|
+
// generator above: importing the writer never grants the ability to start a
|
|
19
|
+
// persistent daemon.
|
|
20
|
+
export { buildExecutorServiceStartPlan, buildExecutorServiceStateProbe, formatExecutorServiceStartFailure, inspectExecutorServiceState, startExecutorService, } from "./service-lifecycle.js";
|
|
21
|
+
export { collectExecutorInstallPreflight, formatExecutorInstallPreflight, EXECUTOR_INSTALL_REQUIRED_TOOLS, EXECUTOR_INSTALL_CLAUDE_LOGIN_REMEDIATION, } from "./install-preflight.js";
|
|
14
22
|
export { runExecutorWatchCli, getExecutorWatchUsage, parseExecutorWatchArgs, } from "./watch-cli.js";
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Executor installation preflight for `install conductor` (BAPI-779).
|
|
3
|
+
*
|
|
4
|
+
* This is the gate that runs BEFORE the installer writes a service unit or
|
|
5
|
+
* starts anything: it answers "can this host actually run a conductor executor?"
|
|
6
|
+
* loudly, so an unsupported platform or a missing tool fails at the top of the
|
|
7
|
+
* phase instead of halfway through a persistent-daemon install.
|
|
8
|
+
*
|
|
9
|
+
* WHAT IS FATAL, AND WHAT IS NOT. Platform support, an explicit base URL, and
|
|
10
|
+
* the presence of `wt`, `git`, and `claude` are fatal — none of them can be
|
|
11
|
+
* worked around later, and provisioning a service that cannot run is worse than
|
|
12
|
+
* refusing. The Claude login marker is ADVISORY only, and that distinction is
|
|
13
|
+
* load-bearing: BAPI-790/791 retired every Bridge-managed Anthropic credential,
|
|
14
|
+
* so the definitive authentication check now happens at the first worker spawn,
|
|
15
|
+
* not here. Turning the advisory back into a gate would reintroduce exactly the
|
|
16
|
+
* onboarding barrier that epic removed.
|
|
17
|
+
*
|
|
18
|
+
* CREDENTIAL BOUNDARY. The only credential-adjacent read this module performs is
|
|
19
|
+
* the shared `detectClaudeLogin` marker probe of `<home>/.claude.json`, which
|
|
20
|
+
* reduces to a single boolean before returning. There is deliberately NO code
|
|
21
|
+
* path here that reads the OS keychain, a Claude credential file, `.mcp.json`,
|
|
22
|
+
* `CLAUDE_CODE_OAUTH_TOKEN`, `ANTHROPIC_API_KEY`, a setup token, or the Bridge
|
|
23
|
+
* credential store — the report type has no field capable of carrying one, and
|
|
24
|
+
* {@link ExecutorInstallPreflightDeps} exposes no seam that could reach one.
|
|
25
|
+
*/
|
|
26
|
+
import { detectClaudeLogin, formatClaudeLoginAdvisory } from "../claude-login.js";
|
|
27
|
+
import { evaluateExecutorPlatform, formatUnsupportedExecutorPlatform } from "./platform.js";
|
|
28
|
+
/** The executables a conductor executor cannot run without. */
|
|
29
|
+
export const EXECUTOR_INSTALL_REQUIRED_TOOLS = ["wt", "git", "claude"];
|
|
30
|
+
/** The exact remediation for a missing login marker — never a credential gate. */
|
|
31
|
+
export const EXECUTOR_INSTALL_CLAUDE_LOGIN_REMEDIATION = "claude login";
|
|
32
|
+
/** An absolute http(s) URL is the only shape a generated unit may bake. */
|
|
33
|
+
function isExplicitBaseUrl(value) {
|
|
34
|
+
if (typeof value !== "string" || value.trim().length === 0)
|
|
35
|
+
return false;
|
|
36
|
+
let parsed;
|
|
37
|
+
try {
|
|
38
|
+
parsed = new URL(value.trim());
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Collect the preflight report. Never throws: a failing probe becomes a
|
|
47
|
+
* finding, because a preflight that crashes tells the operator nothing.
|
|
48
|
+
*/
|
|
49
|
+
export async function collectExecutorInstallPreflight(input, deps) {
|
|
50
|
+
const fatalFindings = [];
|
|
51
|
+
const warnings = [];
|
|
52
|
+
const remediations = [];
|
|
53
|
+
// --- Platform (fatal) ---------------------------------------------------
|
|
54
|
+
// Reuses the single platform policy (`executor/platform.ts`) rather than
|
|
55
|
+
// re-listing darwin/linux, so the installer and the runner can never disagree
|
|
56
|
+
// about which hosts are supported.
|
|
57
|
+
const platformSupport = evaluateExecutorPlatform(input.platform);
|
|
58
|
+
if (!platformSupport.supported) {
|
|
59
|
+
fatalFindings.push(formatUnsupportedExecutorPlatform(String(input.platform)));
|
|
60
|
+
remediations.push("run the executor on a darwin or linux host; this phase writes and starts a " +
|
|
61
|
+
"user-domain service unit, which has no supported form here.");
|
|
62
|
+
}
|
|
63
|
+
// --- Explicit base URL (fatal) -----------------------------------------
|
|
64
|
+
const baseUrlExplicit = isExplicitBaseUrl(input.baseUrl);
|
|
65
|
+
if (!baseUrlExplicit) {
|
|
66
|
+
fatalFindings.push("no explicit Bridge API base URL was resolved for the service unit " +
|
|
67
|
+
"(an absolute http(s) URL is required).");
|
|
68
|
+
remediations.push("set BAPI_BASE_URL to an absolute http(s) endpoint and re-run.");
|
|
69
|
+
}
|
|
70
|
+
// --- Required tooling (fatal, one finding per tool) ---------------------
|
|
71
|
+
const tools = [];
|
|
72
|
+
for (const name of EXECUTOR_INSTALL_REQUIRED_TOOLS) {
|
|
73
|
+
let resolved = false;
|
|
74
|
+
try {
|
|
75
|
+
resolved = (await deps.lookupExecutable(name)) === true;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// A probe that throws is reported as "not resolved" rather than crashing
|
|
79
|
+
// the phase; the thrown value is discarded because it can echo a path.
|
|
80
|
+
resolved = false;
|
|
81
|
+
}
|
|
82
|
+
tools.push({ name, resolved });
|
|
83
|
+
if (!resolved) {
|
|
84
|
+
fatalFindings.push(`required executable '${name}' did not resolve on PATH.`);
|
|
85
|
+
remediations.push(`install '${name}' and make sure it resolves on this host's PATH.`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// --- Claude login (ADVISORY ONLY) --------------------------------------
|
|
89
|
+
// Reduced to a boolean by `detectClaudeLogin` itself, which never returns the
|
|
90
|
+
// parsed object. A missing marker is a warning, never a refusal: BAPI-791
|
|
91
|
+
// moved the definitive check to the first worker spawn.
|
|
92
|
+
const login = await detectClaudeLogin({
|
|
93
|
+
homedir: deps.homedir,
|
|
94
|
+
readFile: deps.readFile,
|
|
95
|
+
});
|
|
96
|
+
if (!login.detected) {
|
|
97
|
+
warnings.push(formatClaudeLoginAdvisory(login));
|
|
98
|
+
remediations.push(EXECUTOR_INSTALL_CLAUDE_LOGIN_REMEDIATION);
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
ok: fatalFindings.length === 0,
|
|
102
|
+
platform: String(input.platform),
|
|
103
|
+
platformSupported: platformSupport.supported,
|
|
104
|
+
baseUrlExplicit,
|
|
105
|
+
tools,
|
|
106
|
+
claudeLoginDetected: login.detected,
|
|
107
|
+
fatalFindings,
|
|
108
|
+
warnings,
|
|
109
|
+
remediations,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Render the report for an operator.
|
|
114
|
+
*
|
|
115
|
+
* Prints platform support, base-URL status, per-tool presence, and the boolean
|
|
116
|
+
* login advisory — and nothing else. There is no wording here about API keys,
|
|
117
|
+
* setup tokens, or credential capability, because this phase checks for none of
|
|
118
|
+
* those (see the module docstring's credential boundary).
|
|
119
|
+
*/
|
|
120
|
+
export function formatExecutorInstallPreflight(report) {
|
|
121
|
+
const lines = [
|
|
122
|
+
"Executor provisioning preflight",
|
|
123
|
+
"───────────────────────────────",
|
|
124
|
+
` platform: ${report.platform} (${report.platformSupported ? "supported" : "UNSUPPORTED"})`,
|
|
125
|
+
` base URL: ${report.baseUrlExplicit ? "explicit" : "MISSING"}`,
|
|
126
|
+
];
|
|
127
|
+
for (const tool of report.tools) {
|
|
128
|
+
lines.push(` ${tool.name.padEnd(15)}${tool.resolved ? "found on PATH" : "NOT FOUND on PATH"}`);
|
|
129
|
+
}
|
|
130
|
+
lines.push(` claude login: ${report.claudeLoginDetected ? "marker present (advisory)" : "no marker (advisory)"}`);
|
|
131
|
+
for (const finding of report.fatalFindings)
|
|
132
|
+
lines.push(` refused: ${finding}`);
|
|
133
|
+
for (const warning of report.warnings)
|
|
134
|
+
lines.push(` warn: ${warning}`);
|
|
135
|
+
for (const remediation of report.remediations)
|
|
136
|
+
lines.push(` → ${remediation}`);
|
|
137
|
+
return lines.join("\n");
|
|
138
|
+
}
|