@bridge_gpt/mcp-server 0.2.34 → 0.2.36
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 +456 -370
- package/build/agent-capabilities/probe-context.js +8 -1
- package/build/agent-capabilities/probes.js +7 -1
- package/build/agents.generated.js +1 -1
- package/build/claude-review-workflow.js +264 -0
- package/build/cli-release.js +53 -0
- package/build/commands.generated.js +4 -4
- package/build/conductor/bridge-api-client.js +215 -0
- package/build/conductor/deny-enforcement-preflight.js +1 -0
- package/build/conductor/done-gate.js +44 -5
- package/build/conductor/epic-reconcile.js +6 -0
- package/build/conductor/install-doctor.js +462 -0
- package/build/conductor-bin.js +3 -3
- package/build/conductor-bundle-artifacts.js +30 -9
- package/build/doctor.js +234 -1
- package/build/executor/cli.js +32 -5
- package/build/executor/credentials.js +45 -11
- package/build/executor/deps.js +14 -0
- package/build/executor/env.js +23 -6
- package/build/executor/index.js +4 -0
- package/build/executor/job-runner.js +119 -9
- package/build/executor/permissions.js +12 -2
- package/build/executor/preflight.js +95 -8
- package/build/executor/prompt-spec.js +51 -0
- package/build/executor/runner.js +15 -2
- package/build/executor/service-unit.js +876 -0
- package/build/executor/test-clock.js +8 -0
- package/build/executor/types.js +0 -17
- package/build/executor/worker-command.js +62 -9
- package/build/index.js +575 -143
- package/build/init.js +153 -51
- package/build/install-bridge-conductor.js +491 -0
- package/build/install-bridge.js +628 -175
- package/build/install-reexec.js +233 -0
- package/build/mcp-host-config.js +11 -1
- package/build/mcp-install-state.js +32 -0
- package/build/mcp-provisioning.js +22 -6
- package/build/pipelines.generated.js +14 -8
- package/build/readme.generated.js +1 -1
- package/build/run-unit-tests-launcher.js +257 -0
- package/build/setup-epic.js +117 -8
- package/build/upgrade-cli.js +1 -15
- package/build/version.generated.js +1 -1
- package/docs/CONDUCTOR.md +115 -4
- package/docs/install/mcp-tool-integrations.md +29 -21
- package/package.json +8 -5
- package/pipelines/implement-ticket.json +6 -1
- package/build/conductor/supervisor-judgment-python.js +0 -141
- package/build/conductor/supervisor-judgment.js +0 -215
package/build/doctor.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* spawning, no MCP server startup. It only ever runs read-only PATH probes
|
|
15
15
|
* (`which`/`where`, `bash --version`, `git rev-parse`) through the injected deps.
|
|
16
16
|
*/
|
|
17
|
-
import { readFile, stat } from "fs/promises";
|
|
17
|
+
import { readdir, readFile, stat } from "fs/promises";
|
|
18
18
|
import { spawn } from "child_process";
|
|
19
19
|
import os from "os";
|
|
20
20
|
import path from "path";
|
|
@@ -28,6 +28,8 @@ import { resolveBapiCredentials } from "./credential-store.js";
|
|
|
28
28
|
import { DEFAULT_AGENT_NAME, resolveAgentSpec, isAgentName, formatValidAgentNames, } from "./agent-registry.js";
|
|
29
29
|
import { getDoctorPrereqDescriptors, probePrerequisite, } from "./start-tickets-prereqs.js";
|
|
30
30
|
import { resolveProfiles } from "./mcp-profile.js";
|
|
31
|
+
import { executorLaunchdDirForHome, executorSystemdDirForHome, executorIdFromLaunchdFilename, executorIdFromSystemdFilename, inspectExecutorServiceArtifact, EXECUTOR_SERVICE_BASE_URL_ENV, } from "./executor/index.js";
|
|
32
|
+
import { DEFAULT_BAPI_BASE_URL } from "./executor/credentials.js";
|
|
31
33
|
/**
|
|
32
34
|
* The report/usage title (BAPI-669, U9b). `doctor` diagnoses the whole Bridge
|
|
33
35
|
* install — install status, prerequisites, launcher cache, and tool surface — so it
|
|
@@ -74,6 +76,16 @@ export function getDoctorUsage() {
|
|
|
74
76
|
"ignore notifications/tools/list_changed must reconnect or start a new MCP",
|
|
75
77
|
"session to observe surface changes; no project MCP config change is required.",
|
|
76
78
|
"",
|
|
79
|
+
"It also includes an advisory 'Executor provisioning' section (BAPI-688): the",
|
|
80
|
+
"generated executor service units on this host. It enumerates them read-only by",
|
|
81
|
+
"fixed convention — ~/Library/LaunchAgents/com.bridge-gpt.executor.*.plist on",
|
|
82
|
+
"macOS, ~/.config/systemd/user/bridge-gpt-executor-*.service on Linux (Windows",
|
|
83
|
+
"Task Scheduler setup is manual, so it reports SKIP) — and reports each unit's",
|
|
84
|
+
"embedded BAPI_BASE_URL and repositories plus whether a Bridge API credential",
|
|
85
|
+
"resolves for each repo (target bapi:<repo>, source only, never the key value).",
|
|
86
|
+
"It reads no install-state file, writes nothing, runs no launchctl/systemctl, and",
|
|
87
|
+
"is advisory: missing, malformed, or credential-less units never change the exit code.",
|
|
88
|
+
"",
|
|
77
89
|
"Conductor ledger / native-module diagnostics (the SQLite ledger's native",
|
|
78
90
|
"binding load status and Node-version skew) live under a separate command:",
|
|
79
91
|
" conductor doctor",
|
|
@@ -513,6 +525,196 @@ export function formatToolSurfaceDiagnosticReport(diag) {
|
|
|
513
525
|
lines.push("to observe surface changes; no project MCP configuration change is required.");
|
|
514
526
|
return lines.join("\n");
|
|
515
527
|
}
|
|
528
|
+
/** Where generated units live, and how their filenames encode the executor id. */
|
|
529
|
+
function resolveExecutorServiceConvention(platform, homeDir) {
|
|
530
|
+
if (platform === "darwin") {
|
|
531
|
+
return {
|
|
532
|
+
dir: executorLaunchdDirForHome(homeDir),
|
|
533
|
+
kind: "launchd-plist",
|
|
534
|
+
idFromFilename: executorIdFromLaunchdFilename,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
if (platform === "linux") {
|
|
538
|
+
return {
|
|
539
|
+
dir: executorSystemdDirForHome(homeDir),
|
|
540
|
+
kind: "systemd-service",
|
|
541
|
+
idFromFilename: executorIdFromSystemdFilename,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Resolve one repository's credential and IMMEDIATELY discard the key, keeping
|
|
548
|
+
* only a source/status label. The resolved value never enters a diagnostic
|
|
549
|
+
* object, a warning, or the rendered report.
|
|
550
|
+
*/
|
|
551
|
+
async function probeRepoCredential(repo, deps, credDeps) {
|
|
552
|
+
const target = `bapi:${repo}`;
|
|
553
|
+
const resolve = deps.resolveCredentials ?? resolveBapiCredentials;
|
|
554
|
+
try {
|
|
555
|
+
const result = await resolve(repo, credDeps);
|
|
556
|
+
if (result.ok) {
|
|
557
|
+
return { repo, target, resolved: true, source: String(result.credentials.source) };
|
|
558
|
+
}
|
|
559
|
+
return { repo, target, resolved: false, kind: String(result.kind) };
|
|
560
|
+
}
|
|
561
|
+
catch {
|
|
562
|
+
// Sanitized: a resolver throw is reported as a kind, never as exception text.
|
|
563
|
+
return { repo, target, resolved: false, kind: "unavailable" };
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Collect read-only diagnostics for every convention-named executor service unit
|
|
568
|
+
* on this host. Never throws: a vanished directory entry, an unreadable unit, or
|
|
569
|
+
* a malformed body becomes a sanitized advisory warning and collection continues.
|
|
570
|
+
*/
|
|
571
|
+
export async function collectExecutorServiceDiagnostics(deps) {
|
|
572
|
+
if (deps.platform === "win32") {
|
|
573
|
+
return {
|
|
574
|
+
status: "skipped",
|
|
575
|
+
reason: "Windows executor services are set up manually through Task Scheduler, so there is no " +
|
|
576
|
+
"generated unit to enumerate (`executor install-service` prints the command line instead).",
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
const convention = resolveExecutorServiceConvention(deps.platform, deps.homedir());
|
|
580
|
+
if (!convention) {
|
|
581
|
+
return {
|
|
582
|
+
status: "skipped",
|
|
583
|
+
reason: `platform '${deps.platform}' has no generated executor service format.`,
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
let entries;
|
|
587
|
+
try {
|
|
588
|
+
entries = await deps.readdir(convention.dir);
|
|
589
|
+
}
|
|
590
|
+
catch {
|
|
591
|
+
return {
|
|
592
|
+
status: "skipped",
|
|
593
|
+
reason: `no executor service units found (${convention.dir} is absent or unreadable).`,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
// Sort BEFORE inspection so report ordering is deterministic regardless of the
|
|
597
|
+
// order the filesystem happened to return entries in.
|
|
598
|
+
const matching = entries
|
|
599
|
+
.filter((name) => convention.idFromFilename(name) !== null)
|
|
600
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
601
|
+
if (matching.length === 0) {
|
|
602
|
+
return {
|
|
603
|
+
status: "skipped",
|
|
604
|
+
reason: `no executor service units found in ${convention.dir}.`,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
const credDeps = {
|
|
608
|
+
env: deps.env,
|
|
609
|
+
homedir: deps.homedir,
|
|
610
|
+
platform: deps.platform,
|
|
611
|
+
readFile: deps.readFile,
|
|
612
|
+
stat: deps.stat,
|
|
613
|
+
stderr: () => { },
|
|
614
|
+
};
|
|
615
|
+
// Only darwin/linux reach here (win32 and unsupported platforms returned
|
|
616
|
+
// above), so unit paths always use POSIX semantics — matching the generators.
|
|
617
|
+
const pathApi = path.posix;
|
|
618
|
+
const units = [];
|
|
619
|
+
const warnings = [];
|
|
620
|
+
for (const name of matching) {
|
|
621
|
+
const unitPath = pathApi.join(convention.dir, name);
|
|
622
|
+
const executorId = convention.idFromFilename(name) ?? name;
|
|
623
|
+
try {
|
|
624
|
+
await deps.stat(unitPath);
|
|
625
|
+
}
|
|
626
|
+
catch {
|
|
627
|
+
// The entry vanished or became unreadable between enumeration and stat.
|
|
628
|
+
warnings.push(`${unitPath}: disappeared or became unreadable during collection.`);
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
let content;
|
|
632
|
+
try {
|
|
633
|
+
content = await deps.readFile(unitPath);
|
|
634
|
+
}
|
|
635
|
+
catch {
|
|
636
|
+
units.push({
|
|
637
|
+
path: unitPath,
|
|
638
|
+
executorId,
|
|
639
|
+
baseUrl: null,
|
|
640
|
+
baseUrlClass: "missing",
|
|
641
|
+
repos: [],
|
|
642
|
+
credentials: [],
|
|
643
|
+
warnings: ["unit could not be read; its configuration cannot be verified."],
|
|
644
|
+
});
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
const inspection = inspectExecutorServiceArtifact(convention.kind, content);
|
|
648
|
+
const unitWarnings = [...inspection.problems];
|
|
649
|
+
let baseUrlClass;
|
|
650
|
+
if (!inspection.baseUrl) {
|
|
651
|
+
baseUrlClass = "missing";
|
|
652
|
+
unitWarnings.push(`no ${EXECUTOR_SERVICE_BASE_URL_ENV} in the unit — the executor requires an explicit base ` +
|
|
653
|
+
"URL and will fail to start.");
|
|
654
|
+
}
|
|
655
|
+
else if (inspection.baseUrl.replace(/\/+$/, "") === DEFAULT_BAPI_BASE_URL) {
|
|
656
|
+
baseUrlClass = "production-default";
|
|
657
|
+
unitWarnings.push(`${EXECUTOR_SERVICE_BASE_URL_ENV} points at production (${DEFAULT_BAPI_BASE_URL}) — legal, ` +
|
|
658
|
+
"but worth confirming this host is meant to serve production jobs.");
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
baseUrlClass = "configured";
|
|
662
|
+
}
|
|
663
|
+
// Probe each DISTINCT repository once per unit, even when the unit repeats it.
|
|
664
|
+
const distinctRepos = Array.from(new Set(inspection.repos));
|
|
665
|
+
const credentials = [];
|
|
666
|
+
for (const repo of distinctRepos) {
|
|
667
|
+
credentials.push(await probeRepoCredential(repo, deps, credDeps));
|
|
668
|
+
}
|
|
669
|
+
units.push({
|
|
670
|
+
path: unitPath,
|
|
671
|
+
executorId,
|
|
672
|
+
baseUrl: inspection.baseUrl,
|
|
673
|
+
baseUrlClass,
|
|
674
|
+
repos: inspection.repos,
|
|
675
|
+
credentials,
|
|
676
|
+
warnings: unitWarnings,
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
return { status: "collected", units, warnings };
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Render the advisory "Executor provisioning" section (pure formatting — no I/O).
|
|
683
|
+
* Every line is found / WARN / SKIP; nothing here changes the exit code.
|
|
684
|
+
*/
|
|
685
|
+
export function formatExecutorServiceDiagnosticsReport(diagnostics) {
|
|
686
|
+
const lines = ["", "Executor provisioning (generated service units — advisory)", ""];
|
|
687
|
+
if (diagnostics.status === "skipped") {
|
|
688
|
+
lines.push(`SKIP ${diagnostics.reason}`);
|
|
689
|
+
lines.push("");
|
|
690
|
+
lines.push("This section is read-only and advisory: it enumerates units by fixed directory and filename");
|
|
691
|
+
lines.push("prefix, and never changes the doctor exit code.");
|
|
692
|
+
return lines.join("\n");
|
|
693
|
+
}
|
|
694
|
+
for (const warning of diagnostics.warnings) {
|
|
695
|
+
lines.push(`WARN ${warning}`);
|
|
696
|
+
}
|
|
697
|
+
for (const unit of diagnostics.units) {
|
|
698
|
+
lines.push(`found ${unit.path}`);
|
|
699
|
+
lines.push(` executor id: ${unit.executorId}`);
|
|
700
|
+
lines.push(` ${EXECUTOR_SERVICE_BASE_URL_ENV}: ${unit.baseUrl ?? "(absent)"} [${unit.baseUrlClass}]`);
|
|
701
|
+
lines.push(` repos: ${unit.repos.length > 0 ? unit.repos.join(", ") : "(none parsed)"}`);
|
|
702
|
+
for (const cred of unit.credentials) {
|
|
703
|
+
lines.push(cred.resolved
|
|
704
|
+
? ` credential ${cred.target}: resolved (source: ${cred.source})`
|
|
705
|
+
: ` credential ${cred.target}: NOT resolved (${cred.kind})`);
|
|
706
|
+
}
|
|
707
|
+
for (const warning of unit.warnings) {
|
|
708
|
+
lines.push(`WARN ${unit.path}: ${warning}`);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
lines.push("");
|
|
712
|
+
lines.push("Generated units carry only the base URL, the generating shell's PATH, repositories, and", "executor id — credentials resolve at");
|
|
713
|
+
lines.push("launch time from BAPI_API_KEY or the user-scoped bapi:<repo> store, so no key value is stored on");
|
|
714
|
+
lines.push("disk or printed here. Starting, stopping, and enabling a unit stay operator-managed. This section");
|
|
715
|
+
lines.push("is advisory and never changes the doctor exit code.");
|
|
716
|
+
return lines.join("\n");
|
|
717
|
+
}
|
|
516
718
|
/**
|
|
517
719
|
* CLI entry for the read-only `doctor` subcommand. Returns a process exit code.
|
|
518
720
|
* Help returns 0; parser errors return 1; otherwise it prints the report and
|
|
@@ -621,6 +823,37 @@ export async function runDoctorCli(argv, overrides = {}) {
|
|
|
621
823
|
}));
|
|
622
824
|
}
|
|
623
825
|
}
|
|
826
|
+
// Advisory executor-provisioning section (BAPI-688). Strictly read-only: it
|
|
827
|
+
// enumerates convention-named units under a fixed OS directory, parses their
|
|
828
|
+
// non-secret configuration, and probes credential resolution WITHOUT retaining
|
|
829
|
+
// any key. It is collected independently of `DoctorCollectionResult` and, like
|
|
830
|
+
// the sections above, never affects the exit code — missing units, malformed
|
|
831
|
+
// units, and unresolved credentials are all advisory.
|
|
832
|
+
if (overrides.executorService !== false) {
|
|
833
|
+
try {
|
|
834
|
+
const injectedFs = deps;
|
|
835
|
+
const executorDeps = {
|
|
836
|
+
platform: overrides.executorService?.platform ?? deps.platform,
|
|
837
|
+
env: overrides.executorService?.env ?? deps.env,
|
|
838
|
+
homedir: overrides.executorService?.homedir ?? injectedFs.homedir ?? os.homedir,
|
|
839
|
+
readdir: overrides.executorService?.readdir ?? ((p) => readdir(p)),
|
|
840
|
+
readFile: overrides.executorService?.readFile ??
|
|
841
|
+
injectedFs.readFile ??
|
|
842
|
+
((p) => readFile(p, "utf-8")),
|
|
843
|
+
stat: overrides.executorService?.stat ?? injectedFs.stat ?? ((p) => stat(p)),
|
|
844
|
+
resolveCredentials: overrides.executorService?.resolveCredentials,
|
|
845
|
+
};
|
|
846
|
+
const executorDiagnostics = await collectExecutorServiceDiagnostics(executorDeps);
|
|
847
|
+
log(formatExecutorServiceDiagnosticsReport(executorDiagnostics));
|
|
848
|
+
}
|
|
849
|
+
catch {
|
|
850
|
+
// Any unexpected failure still renders a sanitized advisory SKIP line.
|
|
851
|
+
log(formatExecutorServiceDiagnosticsReport({
|
|
852
|
+
status: "skipped",
|
|
853
|
+
reason: "executor service units could not be enumerated on this host.",
|
|
854
|
+
}));
|
|
855
|
+
}
|
|
856
|
+
}
|
|
624
857
|
if (!collection.ok)
|
|
625
858
|
return 1;
|
|
626
859
|
return collection.results.some((r) => !r.found) ? 1 : 0;
|
package/build/executor/cli.js
CHANGED
|
@@ -11,7 +11,7 @@ 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
13
|
import { createDefaultExecutorDeps } from "./deps.js";
|
|
14
|
-
import { resolveExecutorApiAccess, } from "./credentials.js";
|
|
14
|
+
import { resolveBaseUrl, resolveExecutorApiAccess, EXECUTOR_BASE_URL_REQUIRED_MESSAGE, } from "./credentials.js";
|
|
15
15
|
import { createExecutorHttpClient } from "./http-client.js";
|
|
16
16
|
import { runExecutor } from "./runner.js";
|
|
17
17
|
import { runExecutorWatchCli } from "./watch-cli.js";
|
|
@@ -22,7 +22,12 @@ const DEFAULT_DEADMAN_MS = 120_000;
|
|
|
22
22
|
const DEFAULT_TERM_GRACE_MS = 10_000;
|
|
23
23
|
const DEFAULT_BASE_BRANCH = "main";
|
|
24
24
|
const DEFAULT_JOB_TIMEOUT_SECONDS = 20 * 60;
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* ONE job per executor process. Exported so the invariant can be pinned against
|
|
27
|
+
* the real constant rather than a copy: BAPI-688 adds more executor *processes*
|
|
28
|
+
* (persistent service units), and must never turn one process into multi-slot.
|
|
29
|
+
*/
|
|
30
|
+
export const DEFAULT_MAX_CONCURRENT = 1;
|
|
26
31
|
export function getExecutorUsage() {
|
|
27
32
|
return [
|
|
28
33
|
"Usage: mcp-server executor --repo <name> [--repo <name> ...] [options]",
|
|
@@ -32,6 +37,7 @@ export function getExecutorUsage() {
|
|
|
32
37
|
"Options:",
|
|
33
38
|
" --repo <name> Repo to serve (repeatable).",
|
|
34
39
|
" --repos=<a,b> Comma-separated repos.",
|
|
40
|
+
" --base-url <url> Bridge API endpoint (overrides BAPI_BASE_URL).",
|
|
35
41
|
" --executor-id <id> Stable executor id (default: <hostname>-<pid>).",
|
|
36
42
|
" --max-concurrent <n> Max concurrent jobs (>= 1, default 1).",
|
|
37
43
|
" --once Run a single preflight/claim cycle and exit.",
|
|
@@ -41,6 +47,12 @@ export function getExecutorUsage() {
|
|
|
41
47
|
" --base-branch <name> Base branch (default main).",
|
|
42
48
|
" --no-advisory-parser Disable the advisory stream-json parser.",
|
|
43
49
|
" -h, --help Show this help.",
|
|
50
|
+
"",
|
|
51
|
+
"Subcommands:",
|
|
52
|
+
" executor watch <job> Attach (read-only) to a running job's worker log.",
|
|
53
|
+
" executor install-service Generate a launchd/systemd user service unit for a",
|
|
54
|
+
" stable --executor-id so the executor survives reboot.",
|
|
55
|
+
" See `executor install-service --help`.",
|
|
44
56
|
].join("\n");
|
|
45
57
|
}
|
|
46
58
|
function parseIntArg(value, flag) {
|
|
@@ -63,6 +75,7 @@ export function parseExecutorArgs(argv, context) {
|
|
|
63
75
|
let deadmanMs = DEFAULT_DEADMAN_MS;
|
|
64
76
|
let baseBranch = DEFAULT_BASE_BRANCH;
|
|
65
77
|
let advisoryParserEnabled = true;
|
|
78
|
+
let baseUrl;
|
|
66
79
|
for (let i = 0; i < argv.length; i++) {
|
|
67
80
|
const arg = argv[i];
|
|
68
81
|
if (arg === "--help" || arg === "-h")
|
|
@@ -120,6 +133,12 @@ export function parseExecutorArgs(argv, context) {
|
|
|
120
133
|
return { kind: "error", message: "--base-branch requires a value" };
|
|
121
134
|
baseBranch = v;
|
|
122
135
|
}
|
|
136
|
+
else if (arg === "--base-url") {
|
|
137
|
+
const v = argv[++i];
|
|
138
|
+
if (!v)
|
|
139
|
+
return { kind: "error", message: "--base-url requires a value" };
|
|
140
|
+
baseUrl = v;
|
|
141
|
+
}
|
|
123
142
|
else if (arg === "--no-advisory-parser") {
|
|
124
143
|
advisoryParserEnabled = false;
|
|
125
144
|
}
|
|
@@ -150,6 +169,7 @@ export function parseExecutorArgs(argv, context) {
|
|
|
150
169
|
baseBranch,
|
|
151
170
|
advisoryParserEnabled,
|
|
152
171
|
defaultJobTimeoutSeconds: DEFAULT_JOB_TIMEOUT_SECONDS,
|
|
172
|
+
baseUrl,
|
|
153
173
|
};
|
|
154
174
|
return { kind: "ok", options };
|
|
155
175
|
}
|
|
@@ -194,20 +214,27 @@ export async function runExecutorCli(argv, overrides = {}) {
|
|
|
194
214
|
return 1;
|
|
195
215
|
}
|
|
196
216
|
const options = parsed.options;
|
|
217
|
+
// The mutating executor requires an EXPLICIT base URL (BAPI-676): `--base-url`
|
|
218
|
+
// then `BAPI_BASE_URL`, never an implicit production default. Fail here —
|
|
219
|
+
// before any credential resolution, HTTP client, or claim side effect.
|
|
220
|
+
const baseUrlResult = resolveBaseUrl(deps.env, options.baseUrl);
|
|
221
|
+
if (!baseUrlResult.ok) {
|
|
222
|
+
errorLog(`Error: ${EXECUTOR_BASE_URL_REQUIRED_MESSAGE}`);
|
|
223
|
+
return 1;
|
|
224
|
+
}
|
|
225
|
+
const baseUrl = baseUrlResult.baseUrl;
|
|
197
226
|
// Resolve credentials for EVERY configured repo so multi-repo mode sends the
|
|
198
227
|
// correct repo-bound `X-API-Key` per claim/heartbeat/complete/fail — not just
|
|
199
228
|
// the first repo's key. Uses the (injectable) single-repo resolver per repo.
|
|
200
229
|
const resolveApi = overrides.resolveApiAccess ?? resolveExecutorApiAccess;
|
|
201
230
|
const apiKeyByRepo = {};
|
|
202
|
-
let baseUrl = "";
|
|
203
231
|
for (const repo of options.repos) {
|
|
204
|
-
const access = await resolveApi(repo, deps);
|
|
232
|
+
const access = await resolveApi(repo, deps, baseUrl);
|
|
205
233
|
if (!access.ok) {
|
|
206
234
|
errorLog(`Error: ${access.error}`);
|
|
207
235
|
return 1;
|
|
208
236
|
}
|
|
209
237
|
apiKeyByRepo[repo] = access.apiKey;
|
|
210
|
-
baseUrl = access.baseUrl;
|
|
211
238
|
}
|
|
212
239
|
const createHttpClient = overrides.createHttpClient ?? createExecutorHttpClient;
|
|
213
240
|
const httpClient = createHttpClient({
|
|
@@ -7,25 +7,59 @@
|
|
|
7
7
|
* diagnostics/results.
|
|
8
8
|
*/
|
|
9
9
|
import { resolveBapiCredentials } from "../credential-store.js";
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* The documented production Bridge API base URL — the value an operator pastes
|
|
12
|
+
* into `BAPI_BASE_URL` or `--base-url` when they *do* mean production.
|
|
13
|
+
*
|
|
14
|
+
* It is NOT an implicit executor fallback (BAPI-676): the executor requires an
|
|
15
|
+
* explicit base URL and fails fast when none is supplied, so pointing at
|
|
16
|
+
* production is always a deliberate act. Other flows (MCP server,
|
|
17
|
+
* install-bridge) legitimately keep their own production defaults.
|
|
18
|
+
*/
|
|
11
19
|
export const DEFAULT_BAPI_BASE_URL = "https://bridgegpt-api.com";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Shared, secret-free prerequisite message for a missing executor base URL.
|
|
22
|
+
* Names both supported sources and states the no-production-default contract so
|
|
23
|
+
* the CLI and preflight diagnostics stay identically worded. It interpolates no
|
|
24
|
+
* configuration or credential value.
|
|
25
|
+
*/
|
|
26
|
+
export const EXECUTOR_BASE_URL_REQUIRED_MESSAGE = "no Bridge API base URL configured for the executor: pass --base-url <url> or set BAPI_BASE_URL " +
|
|
27
|
+
`(production is ${DEFAULT_BAPI_BASE_URL}). The executor never defaults to production.`;
|
|
28
|
+
/** Trim, strip every trailing slash, and treat an empty result as absent. */
|
|
29
|
+
function normalizeBaseUrlCandidate(raw) {
|
|
30
|
+
if (typeof raw !== "string")
|
|
31
|
+
return null;
|
|
32
|
+
const normalized = raw.trim().replace(/\/+$/, "");
|
|
33
|
+
return normalized.length > 0 ? normalized : null;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the executor's base URL: explicit override (`--base-url`) first, then
|
|
37
|
+
* `BAPI_BASE_URL`, then failure. A blank override is treated as absent so a
|
|
38
|
+
* nonblank environment value can still win; there is NO production fallback.
|
|
39
|
+
*/
|
|
40
|
+
export function resolveBaseUrl(env, explicitBaseUrl) {
|
|
41
|
+
const fromFlag = normalizeBaseUrlCandidate(explicitBaseUrl);
|
|
42
|
+
if (fromFlag !== null)
|
|
43
|
+
return { ok: true, baseUrl: fromFlag };
|
|
44
|
+
const fromEnv = normalizeBaseUrlCandidate(env?.BAPI_BASE_URL);
|
|
45
|
+
if (fromEnv !== null)
|
|
46
|
+
return { ok: true, baseUrl: fromEnv };
|
|
47
|
+
return { ok: false };
|
|
18
48
|
}
|
|
19
49
|
/**
|
|
20
50
|
* Resolve API access for a single repo. Returns a structured, secret-free
|
|
21
51
|
* failure for credential-not-found, read/parse failure, missing key, or an
|
|
22
52
|
* invalid repo name — never throwing, never echoing credential-file contents.
|
|
23
53
|
*/
|
|
24
|
-
export async function resolveExecutorApiAccess(repoName, deps) {
|
|
54
|
+
export async function resolveExecutorApiAccess(repoName, deps, explicitBaseUrl) {
|
|
25
55
|
const trimmed = typeof repoName === "string" ? repoName.trim() : "";
|
|
26
56
|
if (trimmed.length === 0) {
|
|
27
57
|
return { ok: false, error: "invalid repo name: repo name is required" };
|
|
28
58
|
}
|
|
59
|
+
const url = resolveBaseUrl(deps.env, explicitBaseUrl);
|
|
60
|
+
if (!url.ok) {
|
|
61
|
+
return { ok: false, error: EXECUTOR_BASE_URL_REQUIRED_MESSAGE };
|
|
62
|
+
}
|
|
29
63
|
const storeDeps = {
|
|
30
64
|
env: deps.env,
|
|
31
65
|
homedir: deps.homedir,
|
|
@@ -43,17 +77,17 @@ export async function resolveExecutorApiAccess(repoName, deps) {
|
|
|
43
77
|
return {
|
|
44
78
|
ok: true,
|
|
45
79
|
apiKey: result.credentials.apiKey,
|
|
46
|
-
baseUrl:
|
|
80
|
+
baseUrl: url.baseUrl,
|
|
47
81
|
};
|
|
48
82
|
}
|
|
49
83
|
/**
|
|
50
84
|
* Resolve credentials for every configured repo before the executor starts
|
|
51
85
|
* claiming. Each repo resolves independently; failures are reported per-repo.
|
|
52
86
|
*/
|
|
53
|
-
export async function resolveAllExecutorApiAccess(repos, deps) {
|
|
87
|
+
export async function resolveAllExecutorApiAccess(repos, deps, explicitBaseUrl) {
|
|
54
88
|
const out = [];
|
|
55
89
|
for (const repo of repos) {
|
|
56
|
-
const access = await resolveExecutorApiAccess(repo, deps);
|
|
90
|
+
const access = await resolveExecutorApiAccess(repo, deps, explicitBaseUrl);
|
|
57
91
|
if (access.ok) {
|
|
58
92
|
out.push({ ok: true, repoName: repo, apiKey: access.apiKey, baseUrl: access.baseUrl });
|
|
59
93
|
}
|
package/build/executor/deps.js
CHANGED
|
@@ -8,9 +8,11 @@
|
|
|
8
8
|
* server's JSON-RPC channel in adjacent tooling).
|
|
9
9
|
*/
|
|
10
10
|
import { execFile, spawn } from "node:child_process";
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
11
12
|
import { readFile, writeFile, appendFile, mkdir, stat, statfs } from "node:fs/promises";
|
|
12
13
|
import os from "node:os";
|
|
13
14
|
import { promisify } from "node:util";
|
|
15
|
+
import { resolveMcpShimInvocationForRuntime } from "../mcp-server-invocation.js";
|
|
14
16
|
const execFileAsync = promisify(execFile);
|
|
15
17
|
/** Bounded subprocess output buffer (bytes). */
|
|
16
18
|
const MAX_COMMAND_BUFFER = 10 * 1024 * 1024;
|
|
@@ -113,5 +115,17 @@ export function createDefaultExecutorDeps() {
|
|
|
113
115
|
},
|
|
114
116
|
log: (message) => console.error(message),
|
|
115
117
|
errorLog: (message) => console.error(message),
|
|
118
|
+
// Resolve how the worker MCP shim launches at dispatch time from THIS
|
|
119
|
+
// running executor's own on-disk build (absolute-build-path primary,
|
|
120
|
+
// npm-channel fallback) — never from process.cwd(). Mirrors
|
|
121
|
+
// `buildMcpProvisioningDeps` in `start-tickets.ts`. `nodeExecutable` stays
|
|
122
|
+
// "node" (not `process.execPath`) to preserve today's worker runtime
|
|
123
|
+
// selection; see `mcp-server-invocation.ts`'s scope-boundary note.
|
|
124
|
+
mcpServerInvocation: resolveMcpShimInvocationForRuntime({
|
|
125
|
+
moduleUrl: import.meta.url,
|
|
126
|
+
nodeExecutable: "node",
|
|
127
|
+
argv1: process.argv[1],
|
|
128
|
+
fileExists: existsSync,
|
|
129
|
+
}),
|
|
116
130
|
};
|
|
117
131
|
}
|
package/build/executor/env.js
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Secret-free executor worker environment (BAPI-534, TDD §7).
|
|
3
3
|
*
|
|
4
|
-
* v2 executor workers get
|
|
5
|
-
*
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* v2 executor workers get Bridge MCP access by default (BAPI-724), but NOT
|
|
5
|
+
* through this environment. Access is provisioned into the worktree's
|
|
6
|
+
* `.mcp.json`/`.cursor/mcp.json` (secret-free shim registration, resolved and
|
|
7
|
+
* written BEFORE spawn — see `../mcp-provisioning.js` wiring in
|
|
8
|
+
* `job-runner.ts`), never via env vars. This builder still omits
|
|
9
|
+
* `BRIDGE_MCP_PROFILE` (an absent profile selects the intended `core` tool
|
|
10
|
+
* group default), every `BAPI_CONDUCTOR_*` key, `CONDUCTOR_NODE_PATH` (the
|
|
11
|
+
* shim's node executable is resolved into the registration's command/args at
|
|
12
|
+
* provisioning time instead), and all secret-bearing keys. It constructs a
|
|
13
|
+
* FRESH object from a strict allowlist of non-secret operational keys — it
|
|
14
|
+
* never copies arbitrary `process.env`, so credentials/tokens/headers cannot
|
|
15
|
+
* leak into the spawned `claude` process. The `mcp-invoke` shim (a separate
|
|
16
|
+
* process) resolves `BAPI_API_KEY` itself at call time from
|
|
17
|
+
* `~/.config/bridge/credentials.json` — this worker environment never carries
|
|
18
|
+
* it.
|
|
10
19
|
*/
|
|
11
20
|
import { PR_BASE_BRANCH_ENV_VAR } from "../pr-base-contract.js";
|
|
12
21
|
/** Non-secret operational keys forwarded to the worker when present. */
|
|
@@ -42,6 +51,14 @@ const EXPLICIT_DENY_KEYS = [
|
|
|
42
51
|
* allowlist is locally auditable/testable. A key is allowed iff it is in the
|
|
43
52
|
* allowlist AND is not explicitly denied, does not begin with `BAPI_CONDUCTOR_`,
|
|
44
53
|
* and does not contain a secret-name fragment.
|
|
54
|
+
*
|
|
55
|
+
* The `BAPI_CONDUCTOR_` rejection is a WHOLE-PREFIX rule, not an enumerated list,
|
|
56
|
+
* which is what makes it hold for keys added later. BAPI-722's
|
|
57
|
+
* `BAPI_CONDUCTOR_DENY_PROBE_TIMEOUT_MS` is rejected twice over — it is absent from
|
|
58
|
+
* `ALLOWED_ENV_KEYS` and it carries the prefix — so it stays executor-process
|
|
59
|
+
* configuration and can never reach a spawned worker. That is deliberate: a worker
|
|
60
|
+
* runs no deny probe, and letting job-side state influence the executor's own
|
|
61
|
+
* safety-probe budget would invert the trust boundary.
|
|
45
62
|
*/
|
|
46
63
|
export function isExecutorEnvKeyAllowed(key) {
|
|
47
64
|
if (!ALLOWED_ENV_KEYS.includes(key))
|
package/build/executor/index.js
CHANGED
|
@@ -7,4 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export { runExecutorCli, getExecutorUsage, parseExecutorArgs } from "./cli.js";
|
|
9
9
|
export { runExecutor } from "./runner.js";
|
|
10
|
+
// Persistent-service packaging (BAPI-688). The CLI entry plus the PURE helpers
|
|
11
|
+
// doctor needs to enumerate and inspect generated units — doctor must share this
|
|
12
|
+
// module's naming/format contract instead of maintaining a second parser.
|
|
13
|
+
export { runExecutorInstallServiceCli, getExecutorInstallServiceUsage, buildExecutorServiceArtifact, writeExecutorServiceArtifact, inspectExecutorServiceArtifact, executorLaunchdDirForHome, executorSystemdDirForHome, executorIdFromLaunchdFilename, executorIdFromSystemdFilename, EXECUTOR_SERVICE_BASE_URL_ENV, EXECUTOR_SERVICE_PATH_ENV, } from "./service-unit.js";
|
|
10
14
|
export { runExecutorWatchCli, getExecutorWatchUsage, parseExecutorWatchArgs, } from "./watch-cli.js";
|