@bridge_gpt/mcp-server 0.2.59 → 0.2.62
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 +181 -51
- package/build/agent-guidance.generated.js +1 -1
- package/build/agent-notes.js +7 -1
- package/build/commands.generated.js +6 -6
- package/build/conductor/bridge-api-client.js +406 -4
- package/build/conductor/doctor.js +255 -53
- package/build/conductor/epic-runtime.js +11 -2
- package/build/conductor/install-doctor.js +24 -2
- package/build/conductor/local-merge.js +319 -26
- package/build/conductor/merge-execution-contract.js +86 -0
- package/build/conductor/merge-execution-eligibility.js +130 -0
- package/build/conductor/readiness-sections.js +256 -16
- package/build/conductor/readiness.js +203 -29
- package/build/conductor/recovery-operations.js +8 -2
- package/build/connect-bitbucket-api.js +6 -1
- package/build/connect-bitbucket.js +398 -88
- package/build/credential-store.js +172 -0
- package/build/docs.generated.js +2 -2
- package/build/doctor.js +277 -15
- package/build/drive-epic.js +147 -12
- package/build/epic-implementer-bin.js +15 -15
- package/build/epic-implementer-claude-hook-bin.js +1 -1
- package/build/epic-implementer-env.js +43 -14
- package/build/epic-integration-pr.js +61 -15
- package/build/epic-launch-profile.js +455 -0
- package/build/executor/ci-rerun-job.js +269 -0
- package/build/executor/cli.js +40 -0
- package/build/executor/credentials.js +28 -1
- package/build/executor/env.js +60 -0
- package/build/executor/http-client.js +45 -1
- package/build/executor/index.js +21 -4
- package/build/executor/integration-job.js +759 -0
- package/build/executor/job-runner.js +218 -9
- package/build/executor/job-types.js +12 -2
- package/build/executor/merge-job.js +11 -2
- package/build/executor/observation.js +114 -7
- package/build/executor/runner.js +101 -1
- package/build/executor/service-lifecycle.js +136 -0
- package/build/executor/service-unit.js +427 -9
- package/build/executor/types.js +48 -0
- package/build/index.js +479 -240
- package/build/install-bridge-conductor.js +599 -14
- package/build/install-bridge.js +448 -46
- package/build/mcp-install-state.js +42 -4
- package/build/merge-pull-request.js +103 -10
- package/build/merge-workflow-codeowners.js +224 -0
- package/build/merge-workflow-config.js +187 -0
- package/build/merge-workflow-drift.js +97 -0
- package/build/merge-workflow-install.js +638 -0
- package/build/merge-workflow.js +956 -0
- package/build/pipelines.generated.js +3 -3
- package/build/plane/cli.js +24 -2
- package/build/plane/preflight.js +68 -0
- package/build/plane/supervisor.js +6 -0
- package/build/readme.generated.js +1 -1
- package/build/run-unit-tests-launcher.js +5 -0
- package/build/setup-epic.js +263 -38
- package/build/version.generated.js +3 -3
- package/package.json +3 -3
- package/build/conduct-epic/bridge-client.js +0 -773
- package/build/conduct-epic/checkpoint-store.js +0 -542
- package/build/conduct-epic/cli.js +0 -3145
- package/build/conduct-epic/cut-protocol.js +0 -392
- package/build/conduct-epic/lock.js +0 -302
- package/build/conduct-epic/pr-state.js +0 -286
- package/build/conduct-epic/spawn.js +0 -113
- package/build/conductor/cli.js +0 -1278
- package/build/conductor-bundle-cli.js +0 -256
- package/build/direct-ticket-tools.js +0 -70
- package/build/executor/reconcile-overlap-governance.js +0 -129
- package/build/implement-epic/bridge-client.js +0 -773
- package/build/implement-epic/checkpoint-store.js +0 -542
- package/build/implement-epic/cli.js +0 -3158
- package/build/implement-epic/cut-protocol.js +0 -392
- package/build/implement-epic/lock.js +0 -302
- package/build/implement-epic/pr-state.js +0 -286
- package/build/implement-epic/spawn.js +0 -113
- package/build/plan-epic-conductor-eligibility.js +0 -213
package/build/doctor.js
CHANGED
|
@@ -10,9 +10,14 @@
|
|
|
10
10
|
* found/missing plus the exact per-OS manual install command for each missing one.
|
|
11
11
|
*
|
|
12
12
|
* The audience is security-conscious/corporate, so the doctor is STRICTLY
|
|
13
|
-
* read-only: no `--fix`, no auto-install, no npm `postinstall`, no
|
|
14
|
-
* spawning, no MCP server startup.
|
|
15
|
-
* (`which`/`where`, `bash --version`,
|
|
13
|
+
* read-only: no `--fix`, no auto-install, no npm `postinstall`, no interactive
|
|
14
|
+
* terminal spawning, no MCP server startup. Every child process it starts is a
|
|
15
|
+
* QUERY: read-only PATH probes (`which`/`where`, `bash --version`,
|
|
16
|
+
* `git rev-parse`) and, on a host that has generated executor service units, the
|
|
17
|
+
* one read-only session probe that section issues (`loginctl show-user
|
|
18
|
+
* --property=Linger` / `launchctl print gui/<uid>`, BAPI-1137). Nothing here can
|
|
19
|
+
* reach a mutating lifecycle verb — `enable-linger`, `bootstrap`, `start` and
|
|
20
|
+
* their kin exist in this file only as remediation TEXT the operator runs.
|
|
16
21
|
*/
|
|
17
22
|
import { readdir, readFile, stat } from "fs/promises";
|
|
18
23
|
import { spawn } from "child_process";
|
|
@@ -29,7 +34,8 @@ import { resolveExecutorAgentAdapter } from "./agent-launchers/executor-adapter-
|
|
|
29
34
|
import { parseDefaultOnEnvFlag } from "./env-flags.js";
|
|
30
35
|
import { createBridgeApiUrls } from "./bridge-api-urls.js";
|
|
31
36
|
import { probeToolSurface } from "./tool-surface-gating.js";
|
|
32
|
-
import { resolveBapiCredentials } from "./credential-store.js";
|
|
37
|
+
import { readStoredBinding, resolveBapiCredentials } from "./credential-store.js";
|
|
38
|
+
import { listEpicLaunchProfiles } from "./epic-launch-profile.js";
|
|
33
39
|
import { resolveConductorBridgeApiAccess } from "./conductor/bridge-api-client.js";
|
|
34
40
|
import { getLiveRepositoryConductors, getParseDispatcherHealth, } from "./epic-shared/bridge-client.js";
|
|
35
41
|
import { resolveRequiredStartTicketsRepoName } from "./start-tickets-repo.js";
|
|
@@ -39,7 +45,7 @@ import { MCP_SERVER_NAME, MCP_PACKAGE_NAME, } from "./mcp-identity.js";
|
|
|
39
45
|
import { DEFAULT_AGENT_NAME, resolveAgentSpec, isAgentName, formatValidAgentNames, } from "./agent-registry.js";
|
|
40
46
|
import { getDoctorPrereqDescriptors, probePrerequisite, } from "./start-tickets-prereqs.js";
|
|
41
47
|
import { resolveProfiles } from "./mcp-profile.js";
|
|
42
|
-
import { executorLaunchdDirForHome, executorSystemdDirForHome, executorIdFromLaunchdFilename, executorIdFromSystemdFilename, inspectExecutorServiceArtifact, EXECUTOR_SERVICE_BASE_URL_ENV, } from "./executor/index.js";
|
|
48
|
+
import { executorLaunchdDirForHome, executorSystemdDirForHome, executorIdFromLaunchdFilename, executorIdFromSystemdFilename, inspectExecutorServiceArtifact, inspectExecutorServiceDurability, EXECUTOR_SERVICE_BASE_URL_ENV, } from "./executor/index.js";
|
|
43
49
|
import { DEFAULT_BAPI_BASE_URL } from "./executor/credentials.js";
|
|
44
50
|
import { detectClaudeLogin, formatClaudeLoginAdvisory } from "./claude-login.js";
|
|
45
51
|
import { validateBranchName } from "./base-ref.js";
|
|
@@ -1005,6 +1011,53 @@ export function formatToolSurfaceDiagnosticReport(diag) {
|
|
|
1005
1011
|
lines.push("to observe surface changes; no project MCP configuration change is required.");
|
|
1006
1012
|
return lines.join("\n");
|
|
1007
1013
|
}
|
|
1014
|
+
// ---------------------------------------------------------------------------
|
|
1015
|
+
// Executor provisioning diagnostics (BAPI-688) — STRICTLY read-only.
|
|
1016
|
+
//
|
|
1017
|
+
// Enumerates the deterministic, convention-named executor service units this
|
|
1018
|
+
// package generates (`executor install-service`) and reports their non-secret
|
|
1019
|
+
// configuration plus credential readiness. It never writes, never runs a
|
|
1020
|
+
// MUTATING `launchctl`/`systemctl`/`loginctl` verb, and — like every advisory
|
|
1021
|
+
// section — never influences the doctor exit code.
|
|
1022
|
+
//
|
|
1023
|
+
// It does spawn ONE child process, and only on a host that already has such a
|
|
1024
|
+
// unit: the read-only session-durability query added by BAPI-1137, through
|
|
1025
|
+
// `defaultReadOnlyLifecycleRunner` below. That is a real change to this
|
|
1026
|
+
// section's behavior and is stated here rather than left to be discovered —
|
|
1027
|
+
// with no injected runner it issues none, and the fact degrades to
|
|
1028
|
+
// `unverifiable` rather than being guessed.
|
|
1029
|
+
//
|
|
1030
|
+
// Discovery is derived SOLELY from the fixed OS directory + filename prefix, not
|
|
1031
|
+
// from `.bridge/install-state.json`: install state is conductor install history,
|
|
1032
|
+
// not an artifact registry, so a unit written by hand or by another machine's
|
|
1033
|
+
// operator is still found, and a stale state entry never invents one.
|
|
1034
|
+
// ---------------------------------------------------------------------------
|
|
1035
|
+
/**
|
|
1036
|
+
* Shell-free runner for the ONE read-only durability query this section issues
|
|
1037
|
+
* (BAPI-1137).
|
|
1038
|
+
*
|
|
1039
|
+
* `shell: false` plus an argument ARRAY, matching every other service-manager
|
|
1040
|
+
* invocation in this package: a uid or identifier is data here, never syntax.
|
|
1041
|
+
* The probes it runs — `loginctl show-user --property=Linger`,
|
|
1042
|
+
* `launchctl print gui/<uid>` — are queries; nothing in this file can reach a
|
|
1043
|
+
* mutating lifecycle command. Output is captured only so the lifecycle module
|
|
1044
|
+
* can map a recognized state token, and is never surfaced.
|
|
1045
|
+
*/
|
|
1046
|
+
function defaultReadOnlyLifecycleRunner(executable, args) {
|
|
1047
|
+
return new Promise((resolve, reject) => {
|
|
1048
|
+
const child = spawn(executable, args, { shell: false, stdio: ["ignore", "pipe", "pipe"] });
|
|
1049
|
+
let stdout = "";
|
|
1050
|
+
let stderr = "";
|
|
1051
|
+
child.stdout?.on("data", (chunk) => {
|
|
1052
|
+
stdout += chunk.toString("utf-8");
|
|
1053
|
+
});
|
|
1054
|
+
child.stderr?.on("data", (chunk) => {
|
|
1055
|
+
stderr += chunk.toString("utf-8");
|
|
1056
|
+
});
|
|
1057
|
+
child.on("error", reject);
|
|
1058
|
+
child.on("close", (code) => resolve({ exitCode: code ?? 1, stdout, stderr }));
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1008
1061
|
/** Where generated units live, and how their filenames encode the executor id. */
|
|
1009
1062
|
function resolveExecutorServiceConvention(platform, homeDir) {
|
|
1010
1063
|
if (platform === "darwin") {
|
|
@@ -1097,6 +1150,36 @@ export async function collectExecutorServiceDiagnostics(deps) {
|
|
|
1097
1150
|
const pathApi = path.posix;
|
|
1098
1151
|
const units = [];
|
|
1099
1152
|
const warnings = [];
|
|
1153
|
+
// BAPI-1137 — the host-level durability prerequisite, probed ONCE per run
|
|
1154
|
+
// rather than once per unit: lingering and the GUI login domain are properties
|
|
1155
|
+
// of the host and the user, not of any particular unit. Without an injected
|
|
1156
|
+
// read-only runner no command is issued at all and the fact stays
|
|
1157
|
+
// `unverifiable`, so a caller that supplies no runner keeps a zero-probe run.
|
|
1158
|
+
let durability = {
|
|
1159
|
+
prerequisite: "unverifiable",
|
|
1160
|
+
remediation: null,
|
|
1161
|
+
};
|
|
1162
|
+
if (deps.runLifecycleCommand) {
|
|
1163
|
+
try {
|
|
1164
|
+
const inspection = await inspectExecutorServiceDurability({
|
|
1165
|
+
platform: deps.platform === "darwin" ? "darwin" : "linux",
|
|
1166
|
+
// The durability probe addresses the USER's session/linger state, not
|
|
1167
|
+
// a unit, so it needs neither a unit path nor a service identifier.
|
|
1168
|
+
unitPath: "",
|
|
1169
|
+
serviceIdentifier: "durability-probe",
|
|
1170
|
+
uid: deps.uid ?? null,
|
|
1171
|
+
}, deps.runLifecycleCommand);
|
|
1172
|
+
durability = {
|
|
1173
|
+
prerequisite: inspection.prerequisite,
|
|
1174
|
+
remediation: inspection.remediation,
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
catch {
|
|
1178
|
+
// Sanitized: a probe that throws leaves the fact unverifiable rather than
|
|
1179
|
+
// surfacing exception text or inventing a state.
|
|
1180
|
+
durability = { prerequisite: "unverifiable", remediation: null };
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1100
1183
|
for (const name of matching) {
|
|
1101
1184
|
const unitPath = pathApi.join(convention.dir, name);
|
|
1102
1185
|
const executorId = convention.idFromFilename(name) ?? name;
|
|
@@ -1120,12 +1203,33 @@ export async function collectExecutorServiceDiagnostics(deps) {
|
|
|
1120
1203
|
baseUrlClass: "missing",
|
|
1121
1204
|
repos: [],
|
|
1122
1205
|
credentials: [],
|
|
1123
|
-
|
|
1206
|
+
// A unit whose bytes could not be read establishes NOTHING about what it
|
|
1207
|
+
// declares. `unverifiable`, never `confirmed-absent`.
|
|
1208
|
+
noStandingGithubWriteCredential: "unverifiable",
|
|
1209
|
+
warnings: [
|
|
1210
|
+
"unit could not be read; its configuration cannot be verified.",
|
|
1211
|
+
"regenerate it through durable-host mode (`install conductor`) if this persists.",
|
|
1212
|
+
],
|
|
1124
1213
|
});
|
|
1125
1214
|
continue;
|
|
1126
1215
|
}
|
|
1127
1216
|
const inspection = inspectExecutorServiceArtifact(convention.kind, content);
|
|
1128
1217
|
const unitWarnings = [...inspection.problems];
|
|
1218
|
+
// BAPI-1137 — the classification comes from the GENERATOR's shared helper,
|
|
1219
|
+
// applied to the unit read off disk. doctor maintains no second notion of
|
|
1220
|
+
// what "declares no GitHub write credential" means, so a unit this section
|
|
1221
|
+
// calls compliant is exactly a unit the generator would agree to write.
|
|
1222
|
+
const credentialDeclaration = inspection.noStandingGithubWriteCredential;
|
|
1223
|
+
unitWarnings.push(...inspection.credentialDeclarationProblems);
|
|
1224
|
+
if (credentialDeclaration === "present") {
|
|
1225
|
+
unitWarnings.push("this unit declares something outside the closed durable-host contract — regenerate it " +
|
|
1226
|
+
"through durable-host mode (`install conductor`) and remove the hand edit.");
|
|
1227
|
+
}
|
|
1228
|
+
else if (credentialDeclaration === "unverifiable") {
|
|
1229
|
+
unitWarnings.push("this unit could only be parsed partially, so the no-standing-GitHub-write-credential " +
|
|
1230
|
+
"property could not be established — regenerate it through durable-host mode " +
|
|
1231
|
+
"(`install conductor`).");
|
|
1232
|
+
}
|
|
1129
1233
|
let baseUrlClass;
|
|
1130
1234
|
if (!inspection.baseUrl) {
|
|
1131
1235
|
baseUrlClass = "missing";
|
|
@@ -1153,10 +1257,24 @@ export async function collectExecutorServiceDiagnostics(deps) {
|
|
|
1153
1257
|
baseUrlClass,
|
|
1154
1258
|
repos: inspection.repos,
|
|
1155
1259
|
credentials,
|
|
1260
|
+
noStandingGithubWriteCredential: credentialDeclaration,
|
|
1156
1261
|
warnings: unitWarnings,
|
|
1157
1262
|
});
|
|
1158
1263
|
}
|
|
1159
|
-
return { status: "collected", units, warnings };
|
|
1264
|
+
return { status: "collected", units, warnings, durability };
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* The rendered name of the BAPI-1137 credential diagnostic.
|
|
1268
|
+
*
|
|
1269
|
+
* Named for the NARROW claim it actually makes — the unit's own declarations —
|
|
1270
|
+
* rather than something broader like "host holds no GitHub credential", which
|
|
1271
|
+
* the check cannot support and an operator would reasonably read as covering
|
|
1272
|
+
* their SSH keys, keychain, and `gh` login.
|
|
1273
|
+
*/
|
|
1274
|
+
export const EXECUTOR_UNIT_CREDENTIAL_DIAGNOSTIC_NAME = "unit-declares-no-github-write-credential";
|
|
1275
|
+
/** How each classification renders, once, so the three spellings cannot drift. */
|
|
1276
|
+
function formatCredentialDeclarationLine(classification) {
|
|
1277
|
+
return ` ${EXECUTOR_UNIT_CREDENTIAL_DIAGNOSTIC_NAME}: ${classification}`;
|
|
1160
1278
|
}
|
|
1161
1279
|
/**
|
|
1162
1280
|
* Render the advisory "Executor provisioning" section (pure formatting — no I/O).
|
|
@@ -1179,6 +1297,7 @@ export function formatExecutorServiceDiagnosticsReport(diagnostics) {
|
|
|
1179
1297
|
lines.push(` executor id: ${unit.executorId}`);
|
|
1180
1298
|
lines.push(` ${EXECUTOR_SERVICE_BASE_URL_ENV}: ${unit.baseUrl ?? "(absent)"} [${unit.baseUrlClass}]`);
|
|
1181
1299
|
lines.push(` repos: ${unit.repos.length > 0 ? unit.repos.join(", ") : "(none parsed)"}`);
|
|
1300
|
+
lines.push(formatCredentialDeclarationLine(unit.noStandingGithubWriteCredential));
|
|
1182
1301
|
for (const cred of unit.credentials) {
|
|
1183
1302
|
lines.push(cred.resolved
|
|
1184
1303
|
? ` credential ${cred.target}: resolved (source: ${cred.source})`
|
|
@@ -1188,11 +1307,52 @@ export function formatExecutorServiceDiagnosticsReport(diagnostics) {
|
|
|
1188
1307
|
lines.push(`WARN ${unit.path}: ${warning}`);
|
|
1189
1308
|
}
|
|
1190
1309
|
}
|
|
1310
|
+
// The host-level durability prerequisite, rendered SEPARATELY from every unit
|
|
1311
|
+
// above: it is a property of this host and user, and attaching it to a unit
|
|
1312
|
+
// would imply the two were one fact.
|
|
1313
|
+
lines.push("");
|
|
1314
|
+
if (diagnostics.durability.prerequisite === "configured") {
|
|
1315
|
+
lines.push("found durable session: configured — services keep running without an active login.");
|
|
1316
|
+
}
|
|
1317
|
+
else if (diagnostics.durability.prerequisite === "session-bound") {
|
|
1318
|
+
// Deliberately NOT a "found" line. The prerequisite this platform can check
|
|
1319
|
+
// is satisfied, but what it establishes is a login session, not logout
|
|
1320
|
+
// durability, and saying otherwise would contradict launchd.
|
|
1321
|
+
lines.push("WARN durable session: session-bound — a login session exists, but it is NOT logout-durable.");
|
|
1322
|
+
if (diagnostics.durability.remediation) {
|
|
1323
|
+
lines.push(` operator action: ${diagnostics.durability.remediation}`);
|
|
1324
|
+
}
|
|
1325
|
+
lines.push(" A LaunchAgent stops with the login it runs under; closing the lid or logging out", " stops the executor. doctor performed a READ-ONLY query and changed nothing.");
|
|
1326
|
+
}
|
|
1327
|
+
else if (diagnostics.durability.prerequisite === "not-configured") {
|
|
1328
|
+
lines.push("WARN durable session: NOT durably configured for this host.");
|
|
1329
|
+
if (diagnostics.durability.remediation) {
|
|
1330
|
+
lines.push(` remediation (operator-run): ${diagnostics.durability.remediation}`);
|
|
1331
|
+
}
|
|
1332
|
+
lines.push(" doctor performed a READ-ONLY query only: it did not run `loginctl enable-linger`,", " change any session setting, or alter the exit code.");
|
|
1333
|
+
}
|
|
1334
|
+
else if (diagnostics.durability.prerequisite === "not-applicable") {
|
|
1335
|
+
lines.push("SKIP durable session: this platform has no session/linger prerequisite.");
|
|
1336
|
+
}
|
|
1337
|
+
else {
|
|
1338
|
+
lines.push("SKIP durable session: unverifiable — the read-only probe produced no recognized answer.", " That is not evidence that this host is, or is not, durably configured.");
|
|
1339
|
+
}
|
|
1340
|
+
lines.push("");
|
|
1341
|
+
lines.push("Unit-content compliance and service-manager state are INDEPENDENT facts: the manager may");
|
|
1342
|
+
lines.push("still be running a previously loaded definition rather than the bytes currently on disk, so a");
|
|
1343
|
+
lines.push("compliant unit file is not evidence about the running process, and neither is evidence that");
|
|
1344
|
+
lines.push("Bridge has observed an executor claim.");
|
|
1345
|
+
lines.push("");
|
|
1346
|
+
lines.push(`The ${EXECUTOR_UNIT_CREDENTIAL_DIAGNOSTIC_NAME} fact covers the unit's OWN declarations —`);
|
|
1347
|
+
lines.push("its environment keys and its arguments. Host-wide SSH keys and agents, login keychains, `gh`");
|
|
1348
|
+
lines.push("login state, git credential helpers, and independently managed Claude worker authentication");
|
|
1349
|
+
lines.push("are outside it and are never inspected or asserted here.");
|
|
1191
1350
|
lines.push("");
|
|
1192
1351
|
lines.push("Generated units carry only the base URL, the generating shell's PATH, repositories, and", "executor id — credentials resolve at");
|
|
1193
1352
|
lines.push("launch time from BAPI_API_KEY or the user-scoped bapi:<repo> store, so no key value is stored on");
|
|
1194
1353
|
lines.push("disk or printed here. Starting, stopping, and enabling a unit stay operator-managed. This section");
|
|
1195
|
-
lines.push("is advisory and
|
|
1354
|
+
lines.push("is advisory — including the credential-declaration and durable-session facts, which are excluded");
|
|
1355
|
+
lines.push("from required-prerequisite computation — and never changes the doctor exit code.");
|
|
1196
1356
|
return lines.join("\n");
|
|
1197
1357
|
}
|
|
1198
1358
|
export async function collectPlaneCredentialDiagnostic(deps) {
|
|
@@ -1370,6 +1530,12 @@ export async function collectOperationalAdvisories(deps) {
|
|
|
1370
1530
|
}
|
|
1371
1531
|
}
|
|
1372
1532
|
// BAPI-1102 — a pure path-presence check, using the SAME injected `stat` and
|
|
1533
|
+
// (BAPI-1137) still ONLY a path-presence check: the helpers' CONTENTS are
|
|
1534
|
+
// never read, they are never executed, and neither file is ever deleted. The
|
|
1535
|
+
// guidance below names durable-host mode as their replacement; removing them
|
|
1536
|
+
// stays an operator step, because they live outside this repository and
|
|
1537
|
+
// product code that deleted files in `~/.local/bin` would be a far larger
|
|
1538
|
+
// authority than a read-only diagnostic should hold.
|
|
1373
1539
|
// `homedir` seams every other advisory here uses, so it stays deterministic
|
|
1374
1540
|
// under test and reaches no real filesystem in one. A `stat` that throws is
|
|
1375
1541
|
// "absent"; it is never retried, and a permission error is not reported as a
|
|
@@ -1437,8 +1603,10 @@ export function formatOperationalAdvisoryReport(diagnostic) {
|
|
|
1437
1603
|
for (const helper of diagnostic.legacyDriveHelpers) {
|
|
1438
1604
|
lines.push(` ${helper}`);
|
|
1439
1605
|
}
|
|
1440
|
-
lines.push(" These predate the scoped two-phase bring-up and are retired.
|
|
1441
|
-
"
|
|
1606
|
+
lines.push(" These predate the scoped two-phase bring-up and are retired. The supported\n" +
|
|
1607
|
+
" replacement is DURABLE-HOST MODE: run `install conductor` to plan, write, and\n" +
|
|
1608
|
+
" start the executor as a persistent service, then confirm\n" +
|
|
1609
|
+
" `drive-epic <EPIC> --plan-file <dag>` works for you, and remove them.\n" +
|
|
1442
1610
|
" doctor performed a PATH-PRESENCE check only: it did not run, read, or\n" +
|
|
1443
1611
|
" modify either file, and it cannot delete them for you.");
|
|
1444
1612
|
}
|
|
@@ -1447,12 +1615,78 @@ export function formatOperationalAdvisoryReport(diagnostic) {
|
|
|
1447
1615
|
return lines.join("\n");
|
|
1448
1616
|
}
|
|
1449
1617
|
/**
|
|
1450
|
-
*
|
|
1451
|
-
*
|
|
1452
|
-
*
|
|
1453
|
-
* missing or the platform is unsupported. Never installs, mutates, spawns tabs,
|
|
1454
|
-
* or starts the MCP server.
|
|
1618
|
+
* Read-only inventory of this host's epic-launch profiles and whether each
|
|
1619
|
+
* one's credential binding matches its target. It reports; it never binds or
|
|
1620
|
+
* migrates anything.
|
|
1455
1621
|
*/
|
|
1622
|
+
export async function collectEpicLaunchProfileDiagnostic(deps) {
|
|
1623
|
+
const listing = await listEpicLaunchProfiles(deps);
|
|
1624
|
+
if (!listing.ok)
|
|
1625
|
+
return { store: "unreadable", message: listing.message };
|
|
1626
|
+
if (!listing.present)
|
|
1627
|
+
return { store: "absent" };
|
|
1628
|
+
const host = deps.hostname();
|
|
1629
|
+
const profiles = [];
|
|
1630
|
+
for (const entry of listing.entries) {
|
|
1631
|
+
if (!entry.validation.ok) {
|
|
1632
|
+
profiles.push({ name: entry.name, usable: false, problem: entry.validation.kind });
|
|
1633
|
+
continue;
|
|
1634
|
+
}
|
|
1635
|
+
const p = entry.validation.profile;
|
|
1636
|
+
const bound = await readStoredBinding(p.credentialRef.target, {
|
|
1637
|
+
env: deps.env,
|
|
1638
|
+
homedir: deps.homedir,
|
|
1639
|
+
platform: process.platform,
|
|
1640
|
+
readFile: deps.readFile,
|
|
1641
|
+
// Presence probe via read: the binding reader treats any failure as unbound.
|
|
1642
|
+
stat: async (filePath) => {
|
|
1643
|
+
await deps.readFile(filePath);
|
|
1644
|
+
return { mode: 0o600 };
|
|
1645
|
+
},
|
|
1646
|
+
});
|
|
1647
|
+
const hostMatches = p.host === host;
|
|
1648
|
+
const credentialBinding = bound === null ? "unbound" : bound === p.baseUrl ? "matches" : "different";
|
|
1649
|
+
profiles.push({
|
|
1650
|
+
name: p.name,
|
|
1651
|
+
usable: hostMatches && credentialBinding === "matches",
|
|
1652
|
+
target: p.baseUrl,
|
|
1653
|
+
repo: p.repoName,
|
|
1654
|
+
mode: p.executionMode,
|
|
1655
|
+
hostMatches,
|
|
1656
|
+
credentialBinding,
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
return { store: "present", profiles };
|
|
1660
|
+
}
|
|
1661
|
+
/** Render the section, or `null` when this host has no profile store. */
|
|
1662
|
+
export function formatEpicLaunchProfileDiagnosticReport(diagnostic) {
|
|
1663
|
+
if (diagnostic.store === "absent")
|
|
1664
|
+
return null;
|
|
1665
|
+
const lines = ["", "Epic launch profiles (read-only)", ""];
|
|
1666
|
+
if (diagnostic.store === "unreadable") {
|
|
1667
|
+
lines.push(` [warn] ${diagnostic.message}`);
|
|
1668
|
+
return lines.join("\n");
|
|
1669
|
+
}
|
|
1670
|
+
if (diagnostic.profiles.length === 0)
|
|
1671
|
+
lines.push(" (none)");
|
|
1672
|
+
for (const p of diagnostic.profiles) {
|
|
1673
|
+
if (p.problem !== undefined) {
|
|
1674
|
+
lines.push(` [fail] ${p.name}: ${p.problem} — re-run \`install conductor --launch-profile ${p.name}\`.`);
|
|
1675
|
+
continue;
|
|
1676
|
+
}
|
|
1677
|
+
const tag = p.usable ? "[ok] " : "[fail]";
|
|
1678
|
+
lines.push(` ${tag} ${p.name}: ${p.target} repo=${p.repo} mode=${p.mode}`);
|
|
1679
|
+
if (p.hostMatches === false)
|
|
1680
|
+
lines.push(" approved on a different host — profiles are host-local.");
|
|
1681
|
+
if (p.credentialBinding === "unbound") {
|
|
1682
|
+
lines.push(" stored credential not yet approved for this target — re-run guided setup to bind it.");
|
|
1683
|
+
}
|
|
1684
|
+
else if (p.credentialBinding === "different") {
|
|
1685
|
+
lines.push(" stored credential is approved for a DIFFERENT target — a launch will refuse to send it.");
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
return lines.join("\n");
|
|
1689
|
+
}
|
|
1456
1690
|
export async function runDoctorCli(argv, overrides = {}) {
|
|
1457
1691
|
const log = overrides.log ?? ((m) => console.log(m));
|
|
1458
1692
|
const errorLog = overrides.errorLog ?? ((m) => console.error(m));
|
|
@@ -1688,6 +1922,13 @@ export async function runDoctorCli(argv, overrides = {}) {
|
|
|
1688
1922
|
((p) => readFile(p, "utf-8")),
|
|
1689
1923
|
stat: overrides.executorService?.stat ?? injectedFs.stat ?? ((p) => stat(p)),
|
|
1690
1924
|
resolveCredentials: overrides.executorService?.resolveCredentials,
|
|
1925
|
+
// BAPI-1137 — the durability prerequisite needs a uid and a read-only
|
|
1926
|
+
// runner. `process.getuid` is absent on win32, where a null uid simply
|
|
1927
|
+
// leaves the fact unverifiable rather than interpolating `undefined`
|
|
1928
|
+
// into a domain target.
|
|
1929
|
+
uid: overrides.executorService?.uid ??
|
|
1930
|
+
(typeof process.getuid === "function" ? process.getuid() : null),
|
|
1931
|
+
runLifecycleCommand: overrides.executorService?.runLifecycleCommand ?? defaultReadOnlyLifecycleRunner,
|
|
1691
1932
|
};
|
|
1692
1933
|
const executorDiagnostics = await collectExecutorServiceDiagnostics(executorDeps);
|
|
1693
1934
|
log(formatExecutorServiceDiagnosticsReport(executorDiagnostics));
|
|
@@ -1745,6 +1986,27 @@ export async function runDoctorCli(argv, overrides = {}) {
|
|
|
1745
1986
|
}));
|
|
1746
1987
|
}
|
|
1747
1988
|
}
|
|
1989
|
+
// Advisory epic-launch profile section (BAPI-1260). Read-only: it never binds,
|
|
1990
|
+
// migrates, or rewrites a profile or a credential — setup does that, with
|
|
1991
|
+
// consent. Never affects the exit code.
|
|
1992
|
+
if (overrides.launchProfiles !== false) {
|
|
1993
|
+
try {
|
|
1994
|
+
const injectedFs = deps;
|
|
1995
|
+
const report = formatEpicLaunchProfileDiagnosticReport(await collectEpicLaunchProfileDiagnostic({
|
|
1996
|
+
env: overrides.launchProfiles?.env ?? deps.env,
|
|
1997
|
+
homedir: overrides.launchProfiles?.homedir ?? injectedFs.homedir ?? os.homedir,
|
|
1998
|
+
readFile: overrides.launchProfiles?.readFile ??
|
|
1999
|
+
injectedFs.readFile ??
|
|
2000
|
+
((p) => readFile(p, "utf-8")),
|
|
2001
|
+
hostname: overrides.launchProfiles?.hostname ?? os.hostname,
|
|
2002
|
+
}));
|
|
2003
|
+
if (report !== null)
|
|
2004
|
+
log(report);
|
|
2005
|
+
}
|
|
2006
|
+
catch {
|
|
2007
|
+
/* advisory only: an unexpected failure drops the section, never the doctor */
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
1748
2010
|
// Advisory operational-advisories section (BAPI-963). Read by the operator
|
|
1749
2011
|
// asking "what is already running?" right after the readiness sections above.
|
|
1750
2012
|
//
|