@bridge_gpt/mcp-server 0.2.60 → 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.
@@ -55,7 +55,9 @@
55
55
  */
56
56
  import { readFile } from "node:fs/promises";
57
57
  import { dirname, join } from "node:path";
58
- import { ConductorBridgeApiError, resolveConductorBridgeApiAccess, fetchConductorReadiness, fetchEpicRunState, safeDiagnosticMessage, } from "./conductor/bridge-api-client.js";
58
+ import { ConductorBridgeApiError, resolveEpicLaunchBridgeApiAccess, fetchConductorReadiness, fetchEpicRunState, safeDiagnosticMessage, } from "./conductor/bridge-api-client.js";
59
+ import { EPIC_LAUNCH_PROFILE_NAME_PATTERN, renderEpicLaunchCommand, shellQuote, } from "./epic-launch-profile.js";
60
+ import { validateHttpBaseUrl, describeBaseUrlRejection } from "./base-url.js";
59
61
  import { BRING_UP_REFUSAL_MESSAGES, BRING_UP_RUNBOOK, firstBringUpRefusal, } from "./conductor/bring-up-facts.js";
60
62
  import { runSetupEpicWorkflow, } from "./setup-epic.js";
61
63
  import { formatPlaneDiagnostic, launchPlaneControlPlane, requestPlaneScopedLanes, runDefaultPlanePreflight, runPlaneCli, } from "./plane/cli.js";
@@ -333,6 +335,18 @@ export function getDriveEpicUsage() {
333
335
  " --review-policy <src> PER-RUN review policy source.",
334
336
  " --replace-policy Authorize replacing a LIVE run's stored policy.",
335
337
  "",
338
+ "Launch target (BAPI-1260) — decided BEFORE any authenticated request:",
339
+ " --launch-profile <name> Select the host-local launch profile guided setup",
340
+ " approved (`install conductor --launch-profile <name>`):",
341
+ " target, repository, mode, runtime, and policy and",
342
+ " credential references. The command setup prints",
343
+ " already selects it and runs in a fresh shell.",
344
+ " --base-url <url> Explicit Bridge API target. With a profile it must",
345
+ " agree with it — an override never rebinds a profile.",
346
+ "Without either, BAPI_BASE_URL is used; with none of the three, drive-epic",
347
+ "refuses. Nothing defaults to production — select it deliberately. A stored",
348
+ "credential is sent only to the endpoint setup approved it for.",
349
+ "",
336
350
  " -h, --help Show this help",
337
351
  ].join("\n");
338
352
  }
@@ -359,6 +373,9 @@ export function parseDriveEpicArgs(argv) {
359
373
  let policyFile;
360
374
  let reviewPolicy;
361
375
  let replacePolicy = false;
376
+ // BAPI-1260 launch selection.
377
+ let launchProfile;
378
+ let baseUrl;
362
379
  // BAPI-1009: validated with the SHARED validator setup-epic uses, so a name
363
380
  // drive-epic accepts is exactly a name setup-epic accepts. A second rule here
364
381
  // would let a name pass the router and fail the delegate.
@@ -394,7 +411,9 @@ export function parseDriveEpicArgs(argv) {
394
411
  arg === "--repo" ||
395
412
  arg === "--feature-branch" ||
396
413
  arg === "--policy-file" ||
397
- arg === "--review-policy") {
414
+ arg === "--review-policy" ||
415
+ arg === "--launch-profile" ||
416
+ arg === "--base-url") {
398
417
  const value = argv[i + 1];
399
418
  if (value === undefined || value.startsWith("-")) {
400
419
  return { status: "error", message: `${arg} requires a value.` };
@@ -407,6 +426,10 @@ export function parseDriveEpicArgs(argv) {
407
426
  policyFile = value;
408
427
  else if (arg === "--review-policy")
409
428
  reviewPolicy = value;
429
+ else if (arg === "--launch-profile")
430
+ launchProfile = value;
431
+ else if (arg === "--base-url")
432
+ baseUrl = value;
410
433
  else if (takeFeatureBranch(value) === null) {
411
434
  return {
412
435
  status: "error",
@@ -416,7 +439,7 @@ export function parseDriveEpicArgs(argv) {
416
439
  i++;
417
440
  continue;
418
441
  }
419
- const eq = arg.match(/^(--plan-file|--repo|--feature-branch|--policy-file|--review-policy)=(.*)$/);
442
+ const eq = arg.match(/^(--plan-file|--repo|--feature-branch|--policy-file|--review-policy|--launch-profile|--base-url)=(.*)$/);
420
443
  if (eq) {
421
444
  const value = eq[2];
422
445
  if (eq[1] === "--feature-branch") {
@@ -437,6 +460,10 @@ export function parseDriveEpicArgs(argv) {
437
460
  policyFile = value;
438
461
  else if (eq[1] === "--review-policy")
439
462
  reviewPolicy = value;
463
+ else if (eq[1] === "--launch-profile")
464
+ launchProfile = value;
465
+ else if (eq[1] === "--base-url")
466
+ baseUrl = value;
440
467
  else
441
468
  repo = value;
442
469
  continue;
@@ -458,6 +485,22 @@ export function parseDriveEpicArgs(argv) {
458
485
  "may be selected — remove whichever one you did not mean.",
459
486
  };
460
487
  }
488
+ // BAPI-1260 — the launch selection is validated for SHAPE here, before any
489
+ // side effect. Whether it agrees with the profile and the environment is
490
+ // decided by the target resolver, still before any authenticated request.
491
+ if (launchProfile !== undefined && !EPIC_LAUNCH_PROFILE_NAME_PATTERN.test(launchProfile)) {
492
+ return {
493
+ status: "error",
494
+ message: "--launch-profile must match [a-z0-9][a-z0-9._-]{0,62}.",
495
+ };
496
+ }
497
+ if (baseUrl !== undefined) {
498
+ const validated = validateHttpBaseUrl(baseUrl);
499
+ if (!validated.ok) {
500
+ return { status: "error", message: describeBaseUrlRejection("--base-url", validated.reason) };
501
+ }
502
+ baseUrl = validated.baseUrl;
503
+ }
461
504
  if (positionals.length === 0) {
462
505
  return { status: "error", message: "Missing required epic key." };
463
506
  }
@@ -485,6 +528,8 @@ export function parseDriveEpicArgs(argv) {
485
528
  ...(policyFile ? { policyFile } : {}),
486
529
  ...(reviewPolicy ? { reviewPolicy } : {}),
487
530
  ...(replacePolicy ? { replacePolicy: true } : {}),
531
+ ...(launchProfile ? { launchProfile } : {}),
532
+ ...(baseUrl ? { baseUrl } : {}),
488
533
  };
489
534
  return { status: "ok", options };
490
535
  }
@@ -537,7 +582,7 @@ export function selectConductor(readiness) {
537
582
  * no quoting to get wrong.
538
583
  */
539
584
  export function buildSetupEpicArgv(options) {
540
- const { epicKey, planFile, repo, featureBranch, intoBase, attended, policyFile, reviewPolicy, replacePolicy, } = options;
585
+ const { epicKey, planFile, repo, featureBranch, intoBase, attended, policyFile, reviewPolicy, replacePolicy, launchProfile, baseUrl, } = options;
541
586
  return [
542
587
  "--epic-key",
543
588
  epicKey,
@@ -551,6 +596,11 @@ export function buildSetupEpicArgv(options) {
551
596
  ...(policyFile ? ["--policy-file", policyFile] : []),
552
597
  ...(reviewPolicy ? ["--review-policy", reviewPolicy] : []),
553
598
  ...(replacePolicy ? ["--replace-policy"] : []),
599
+ // BAPI-1260 — the SAME launch selection, so setup-epic resolves the same
600
+ // target and bound credential drive-epic just did. Appended only when
601
+ // supplied, so every pre-existing argv stays byte-identical.
602
+ ...(launchProfile ? ["--launch-profile", launchProfile] : []),
603
+ ...(baseUrl ? ["--base-url", baseUrl] : []),
554
604
  ];
555
605
  }
556
606
  /**
@@ -561,8 +611,25 @@ export function buildSetupEpicArgv(options) {
561
611
  * is only true once {@link runDriveEpicCli} has declined to delegate — so route
562
612
  * first, render second.
563
613
  */
564
- export function renderConductorHandoff(selection, epicKey) {
565
- const invocation = `npx -y ${MCP_PACKAGE_NAME} drive-epic ${epicKey} --plan-file <path>`;
614
+ export function renderConductorHandoff(selection, epicKey,
615
+ /**
616
+ * BAPI-1260 — the selected launch profile and the options this invocation
617
+ * carried. With a profile, the re-run command selects it and carries every
618
+ * non-secret flag it needs, shell-quoted, so it runs unchanged in a fresh
619
+ * shell. The plan file is the one thing that cannot be filled in yet, and it
620
+ * is the only placeholder left.
621
+ */
622
+ launch = { profile: null }) {
623
+ const invocation = launch.profile !== null
624
+ ? renderEpicLaunchCommand(launch.profile, {
625
+ epicKey,
626
+ planFile: "<path>",
627
+ ...(launch.options?.policyFile ? { policyFile: launch.options.policyFile } : {}),
628
+ ...(launch.options?.attended ? { attended: true } : {}),
629
+ ...(launch.options?.intoBase ? { intoBase: true } : {}),
630
+ ...(launch.options?.featureBranch ? { featureBranch: launch.options.featureBranch } : {}),
631
+ }).replace(`${shellQuote("<path>")}`, "<path>")
632
+ : `npx -y ${MCP_PACKAGE_NAME} drive-epic ${epicKey} --plan-file <path>`;
566
633
  return {
567
634
  conductor: "v2",
568
635
  invocation,
@@ -608,6 +675,16 @@ export function assertSingleConductorInvocation(text) {
608
675
  throw new Error(`drive-epic emitted more than one conductor invocation (${named.join(", ")}). ` +
609
676
  "Exactly one path may ever be presented.");
610
677
  }
678
+ // BAPI-1260 — the entry point itself, too. A rendered handoff now carries a
679
+ // complete, runnable `drive-epic <KEY> ...` command; two DIFFERENT runnable
680
+ // commands (say, one per profile or target) would hand the reader a choice of
681
+ // authority just as surely as naming two conductors. The same command
682
+ // repeated is still one invocation.
683
+ const runnable = new Set((text.match(/\bdrive-epic\s+[A-Z][A-Z0-9]*-[0-9]+[^\n]*/g) ?? []).map((line) => line.trim()));
684
+ if (runnable.size > 1) {
685
+ throw new Error(`drive-epic emitted ${runnable.size} different runnable launch commands. ` +
686
+ "Exactly one path may ever be presented.");
687
+ }
611
688
  }
612
689
  /**
613
690
  * Conservative bound on any single advisory field. Exists so malformed or
@@ -809,14 +886,31 @@ export function resolvePlaneReconcilerInstanceId(manifest, env, hostname) {
809
886
  }
810
887
  /** Terminal `epic_runs.status` values; every other status is a live run. */
811
888
  const TERMINAL_EPIC_RUN_STATUSES = ["abandoned", "done"];
889
+ /** The BAPI-1260 launch selection carried by parsed options. */
890
+ function launchSelectionOf(options) {
891
+ return {
892
+ ...(options.launchProfile ? { profileName: options.launchProfile } : {}),
893
+ ...(options.baseUrl ? { baseUrl: options.baseUrl } : {}),
894
+ };
895
+ }
812
896
  export function createDefaultDriveEpicDeps() {
813
897
  return {
814
- resolveAccess: async (repo, cwd) => {
815
- const result = await resolveConductorBridgeApiAccess({
898
+ resolveAccess: async (repo, cwd, launch) => {
899
+ // BAPI-1260: the target-first launch contract. The generic resolver's
900
+ // production default does not apply to an epic launch.
901
+ const result = await resolveEpicLaunchBridgeApiAccess(launch ?? {}, {
816
902
  ...(repo ? { repoName: repo } : {}),
817
903
  ...(cwd ? { cwd } : {}),
818
904
  });
819
- return result.ok ? { ok: true, access: result.access } : { ok: false, error: result.error };
905
+ return result.ok
906
+ ? { ok: true, access: result.access, profile: result.profile }
907
+ : { ok: false, error: result.error };
908
+ },
909
+ // Non-secret, and only ever a value the resolver just agreed with the
910
+ // environment on (a conflicting BAPI_BASE_URL was refused above), so the
911
+ // processes this command starts see the same target it resolved.
912
+ adoptLaunchTarget: (baseUrl) => {
913
+ process.env.BAPI_BASE_URL = baseUrl;
820
914
  },
821
915
  readReadiness: (access) => fetchConductorReadiness(access, globalThis.fetch),
822
916
  runSetupEpic: (argv) => runSetupEpicWorkflow(argv),
@@ -887,12 +981,19 @@ export async function runDriveEpicCli(argv, overrides = {}) {
887
981
  return 1;
888
982
  }
889
983
  const { epicKey, planFile, repo, featureBranch, intoBase } = parsed.options;
984
+ const launch = launchSelectionOf(parsed.options);
890
985
  try {
891
986
  // --- Resolve access. A missing credential is UNKNOWN, not not-ready. -----
892
- const accessResult = await deps.resolveAccess(repo);
987
+ // BAPI-1260: the TARGET is decided first — from --launch-profile,
988
+ // --base-url, and BAPI_BASE_URL — and only then is a credential bound to
989
+ // that target read. A missing or conflicting target refuses here, before
990
+ // any authenticated request; nothing defaults to production.
991
+ const accessResult = await deps.resolveAccess(repo, undefined, launch);
893
992
  if (!accessResult.ok) {
894
993
  return escalate(deps, epicKey, `conductor readiness could not be read: ${accessResult.error}`);
895
994
  }
995
+ const launchProfile = accessResult.profile ?? null;
996
+ deps.adoptLaunchTarget?.(accessResult.access.baseUrl);
896
997
  // --- Read readiness. Any failure here is UNKNOWN — with ONE exception. ---
897
998
  let readiness;
898
999
  try {
@@ -907,6 +1008,14 @@ export async function runDriveEpicCli(argv, overrides = {}) {
907
1008
  // because there unknown is still fatal (R14).
908
1009
  if (planFile !== undefined && err instanceof ConductorBridgeApiError && err.kind === "network") {
909
1010
  const endpoint = planeEndpointForBaseUrl(deps, accessResult.access.baseUrl);
1011
+ // BAPI-1260 — only a SELECTED LOCAL plane is ever started here. A
1012
+ // profile approved for durable execution names an authority this
1013
+ // machine does not own, so a refused port is that authority being
1014
+ // down — never permission to launch a local replacement.
1015
+ if (endpoint !== null && launchProfile !== null && launchProfile.executionMode !== "local") {
1016
+ return escalate(deps, epicKey, `launch profile '${launchProfile.name}' selects durable execution, and its control plane is ` +
1017
+ "not answering; no local replacement was started");
1018
+ }
910
1019
  if (endpoint !== null) {
911
1020
  const probe = await deps.probePlanePort(endpoint.host, endpoint.port);
912
1021
  if (probe.kind === "refused") {
@@ -1078,7 +1187,10 @@ export async function runDriveEpicCli(argv, overrides = {}) {
1078
1187
  deps.errorLog(refusal);
1079
1188
  return 1;
1080
1189
  }
1081
- const handoff = renderConductorHandoff(selection, epicKey);
1190
+ const handoff = renderConductorHandoff(selection, epicKey, {
1191
+ profile: launchProfile,
1192
+ options: parsed.options,
1193
+ });
1082
1194
  const text = handoff.lines.join("\n");
1083
1195
  assertSingleConductorInvocation(text);
1084
1196
  deps.log(text);
@@ -1287,7 +1399,7 @@ async function coldStartPlane(ctx) {
1287
1399
  */
1288
1400
  async function attachToPlane(ctx, manifest, readiness) {
1289
1401
  const { deps, epicKey } = ctx;
1290
- const rooted = await deps.resolveAccess(ctx.options.repo, manifest.repoRoot);
1402
+ const rooted = await deps.resolveAccess(ctx.options.repo, manifest.repoRoot, launchSelectionOf(ctx.options));
1291
1403
  if (!rooted.ok) {
1292
1404
  return refuse(deps, [
1293
1405
  `${epicKey}: cannot attach to the running plane — Bridge API credentials could not be ` +