@bridge_gpt/mcp-server 0.2.67 → 0.2.68
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 +178 -15
- package/build/agent-launchers/claude-executor-adapter.js +27 -0
- package/build/agents.generated.js +22 -2
- package/build/bitbucket-workspace-remote.js +136 -0
- package/build/bootstrap-project-identity.js +24 -0
- package/build/bridge-permissions.js +829 -0
- package/build/claude-settings.js +18 -0
- package/build/cli-help.js +21 -0
- package/build/command-assets-doctor.js +24 -70
- package/build/command-provisioning.js +29 -5
- package/build/commands.generated.js +8 -8
- package/build/conductor/bridge-api-client.js +714 -0
- package/build/conductor/command-drift-production.js +92 -0
- package/build/conductor/command-drift.js +89 -0
- package/build/conductor/doctor.js +69 -26
- package/build/conductor/epic-branch-ci-inspection.js +1375 -0
- package/build/conductor/epic-implementer-cli.js +76 -22
- package/build/conductor/gate-discovery.js +890 -0
- package/build/conductor/install-doctor.js +15 -3
- package/build/conductor/json-output.js +63 -0
- package/build/conductor/readiness-cli.js +95 -10
- package/build/conductor/readiness-sections.js +369 -3
- package/build/conductor/readiness.js +211 -8
- package/build/conductor/recovery-cli.js +110 -4
- package/build/conductor/recovery-operations.js +99 -1
- package/build/connect-bitbucket-api.js +41 -6
- package/build/connect-bitbucket.js +106 -33
- package/build/credential-store.js +207 -47
- package/build/docs.generated.js +57 -3
- package/build/doctor.js +92 -1
- package/build/drive-epic.js +45 -8
- package/build/epic-coherence-input.test-fixtures.js +4 -3
- package/build/epic-implementer-bin.js +1374 -25
- package/build/epic-workarounds-path.js +39 -0
- package/build/executor/cli.js +30 -0
- package/build/executor/completion-contract.js +18 -5
- package/build/executor/deps.js +4 -1
- package/build/executor/gate-qualification-job.js +499 -0
- package/build/executor/guarded-fast-forward-push.js +129 -0
- package/build/executor/integration-job.js +122 -8
- package/build/executor/job-errors.js +8 -1
- package/build/executor/job-runner.js +589 -47
- package/build/executor/likely-tests.js +290 -0
- package/build/executor/live-worker-registry.js +1 -1
- package/build/executor/observation.js +31 -2
- package/build/executor/packaged-recipe.js +106 -0
- package/build/executor/permissions.js +115 -28
- package/build/executor/platform.js +1 -1
- package/build/executor/preflight.js +61 -12
- package/build/executor/process-containment.js +36 -12
- package/build/executor/process.js +15 -7
- package/build/executor/prompt-spec.js +33 -1
- package/build/executor/recently-failed-tests-file.js +71 -0
- package/build/executor/reconcile-overlap-job.js +41 -23
- package/build/executor/required-verification.js +1 -1
- package/build/executor/required-work-ledger.js +105 -17
- package/build/executor/retirement.js +69 -0
- package/build/executor/runner.js +114 -11
- package/build/executor/test-clock.js +15 -0
- package/build/executor/types.js +32 -0
- package/build/executor/viewer-tabs.js +58 -8
- package/build/executor/worker-command.js +23 -7
- package/build/executor/worker-diagnostics.js +37 -0
- package/build/executor/worker-finalization.js +31 -5
- package/build/executor/worker-guard-hook.js +179 -25
- package/build/executor/worker-log.js +4 -3
- package/build/executor/worker-verification-guidance.js +37 -0
- package/build/executor/worktree.js +88 -11
- package/build/first-build-advisory.js +97 -0
- package/build/generated-assets-advisory.js +14 -0
- package/build/generated-assets-catalog.js +66 -0
- package/build/generated-assets-context.js +46 -0
- package/build/generated-assets-dependencies.js +26 -0
- package/build/generated-assets-doctor.js +50 -0
- package/build/generated-assets-health.js +65 -0
- package/build/generated-assets-host.js +17 -0
- package/build/generated-assets-inventory.js +17 -0
- package/build/generated-assets-paths.js +60 -0
- package/build/generated-assets-publisher.js +133 -0
- package/build/generated-assets-remedy.js +25 -0
- package/build/generated-assets-repair.js +72 -0
- package/build/generated-assets-session.js +71 -0
- package/build/generated-assets-test-support.js +35 -0
- package/build/index.js +964 -768
- package/build/init.js +54 -177
- package/build/install-bridge-conductor-gate.js +1372 -0
- package/build/install-bridge-conductor.js +289 -36
- package/build/install-bridge.js +544 -37
- package/build/mcp-install-state.js +50 -4
- package/build/merge-workflow-install.js +3 -4
- package/build/pipeline-utils.js +6 -2
- package/build/pipelines.generated.js +22 -142
- package/build/plan-execution-ledger.js +2 -32
- package/build/plane/manifest.js +16 -1
- package/build/plane/preflight.js +54 -1
- package/build/printed-remedy-commands.js +16 -1
- package/build/readme.generated.js +1 -1
- package/build/repair-assets.js +93 -0
- package/build/run-unit-tests-launcher.js +130 -49
- package/build/secret-redaction.js +33 -0
- package/build/setup-epic.js +108 -3
- package/build/start-tickets-prereqs.js +70 -16
- package/build/start-tickets.js +29 -5
- package/build/tool-surface-gating.js +36 -0
- package/build/upgrade-cli.js +55 -1
- package/build/version.generated.js +3 -3
- package/build/worker-guard-hook-bin.js +3 -2
- package/build/worker-guard-hook-main.js +1 -1
- package/build/worktree-core.js +15 -2
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +30 -12
- package/docs/EPIC-IMPLEMENTER.md +68 -2
- package/package.json +3 -3
- package/pipelines/greenfield-setup.json +4 -128
- package/pipelines/implement-ticket.json +3 -2
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { access, lstat, realpath, readFile } from "node:fs/promises";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { WORKFLOW_HOSTS, buildPackagedAssetCatalog } from "./generated-assets-catalog.js";
|
|
5
|
+
import { authorizeAssetRepair, assetRootBinding } from "./generated-assets-context.js";
|
|
6
|
+
import { validateAssetRoot } from "./generated-assets-paths.js";
|
|
7
|
+
import { readMcpInstallState } from "./mcp-install-state.js";
|
|
8
|
+
import { repairPackagedAssets } from "./generated-assets-repair.js";
|
|
9
|
+
import { createConfinedAssetPublisher } from "./generated-assets-publisher.js";
|
|
10
|
+
import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
|
|
11
|
+
import { VERSION } from "./version.generated.js";
|
|
12
|
+
export const REPAIR_ASSETS_HELP = `Usage: repair-assets [--project-root <directory>] [--host <claude|cursor|codex|copilot>]
|
|
13
|
+
Fill missing packaged workflow files only; preserve existing customized files.
|
|
14
|
+
Explicit --project-root plus --host authorizes provisioning without inventory.
|
|
15
|
+
Implicit selection requires root-bound inventory at exact cwd; subdirectories must
|
|
16
|
+
select --project-root. Never infer installed hosts from folders or selectedPlatforms.
|
|
17
|
+
Reports restored/preserved/unreadable/failed files; incomplete repair exits 1.
|
|
18
|
+
Linux procfs supports confined exclusive publication. Other platforms refuse writes
|
|
19
|
+
when a confined adapter is unavailable. Existing readable files remain preserved.
|
|
20
|
+
Excludes custom pipelines/instructions, registrations, credentials, generated outputs,
|
|
21
|
+
onboarding, API setup, and Windsurf. Refresh/reconnect after repair, then run doctor.
|
|
22
|
+
Unpinned npx -y ${MCP_PACKAGE_NAME} repair-assets intentionally selects the latest
|
|
23
|
+
package; use @${VERSION} to repair from this running version.\n`;
|
|
24
|
+
export async function runRepairAssetsCli(argv, options = {}) {
|
|
25
|
+
const log = options.log ?? (text => process.stdout.write(`${text}\n`));
|
|
26
|
+
const error = options.error ?? (text => process.stderr.write(`${text}\n`));
|
|
27
|
+
if (argv.length === 1 && (argv[0] === "--help" || argv[0] === "-h")) {
|
|
28
|
+
log(REPAIR_ASSETS_HELP);
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
let requestedRoot;
|
|
32
|
+
const hosts = [];
|
|
33
|
+
for (let i = 0; i < argv.length; i++) {
|
|
34
|
+
const flag = argv[i];
|
|
35
|
+
const value = argv[++i];
|
|
36
|
+
if (!value || value.startsWith("--")) {
|
|
37
|
+
error("repair-assets: missing option value; use --help.");
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
if (flag === "--project-root" && !requestedRoot)
|
|
41
|
+
requestedRoot = value;
|
|
42
|
+
else if (flag === "--host" && WORKFLOW_HOSTS.includes(value))
|
|
43
|
+
hosts.push(value);
|
|
44
|
+
else {
|
|
45
|
+
error("repair-assets: unknown, duplicate, or invalid option; use --help.");
|
|
46
|
+
return 1;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const fs = options.fs ?? { lstat, realpath, readFile: (p) => readFile(p, "utf-8"), access: (p) => access(p, constants.R_OK) };
|
|
50
|
+
const root = await validateAssetRoot(requestedRoot ?? options.cwd ?? process.cwd(), options.home ?? os.homedir(), fs);
|
|
51
|
+
if (!root) {
|
|
52
|
+
error("repair-assets: select an existing canonical project directory, excluding home and filesystem roots.");
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
const state = await readMcpInstallState(root, fs);
|
|
56
|
+
const inventory = state.status === "valid" ? state.state.packagedAssets : undefined;
|
|
57
|
+
const explicitPair = requestedRoot !== undefined && hosts.length > 0;
|
|
58
|
+
const selected = [...new Set(hosts.length ? hosts : inventory?.assets.map(a => a.host) ?? [])];
|
|
59
|
+
if (!authorizeAssetRepair(root, selected, explicitPair, inventory)) {
|
|
60
|
+
error("repair-assets: installation context is uncertain. Supply both --project-root and --host for explicit provisioning.");
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
|
63
|
+
log(`Selected project root: ${root}`);
|
|
64
|
+
log(`Invoking package: ${VERSION}; ${explicitPair ? "explicit fill-only provisioning" : "inventory-authorized repair"}.`);
|
|
65
|
+
const boundInventory = inventory?.rootBinding === assetRootBinding(root) ? inventory : undefined;
|
|
66
|
+
if (boundInventory)
|
|
67
|
+
log(`Recorded package: ${boundInventory.packageVersion}; invoking bundle may differ.`);
|
|
68
|
+
const catalog = buildPackagedAssetCatalog();
|
|
69
|
+
if (boundInventory) {
|
|
70
|
+
const historical = new Set(boundInventory.assets.map(a => a.id));
|
|
71
|
+
const introduced = catalog.assets.filter(a => a.hosts.some(h => selected.includes(h)) && !historical.has(a.id));
|
|
72
|
+
log(`Newly introduced catalog assets: ${introduced.length} (provisioning, not historical loss).`);
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const result = await (options.repair ?? repairPackagedAssets)({ root, hosts: selected, explicitPair, inventory: boundInventory, fs,
|
|
76
|
+
catalog, publisher: options.publisher ?? createConfinedAssetPublisher(options.platform) });
|
|
77
|
+
for (const status of ["restored", "preserved", "unreadable", "failed"]) {
|
|
78
|
+
const items = result.findings.filter(f => f.status === status);
|
|
79
|
+
log(`${status}: ${items.length}${items.length ? ` (${items.slice(0, 5).map(f => `${f.destination}: ${f.reason}`).join(", ")}${items.length > 5 ? ", …" : ""})` : ""}`);
|
|
80
|
+
}
|
|
81
|
+
if (result.packageProblems.length)
|
|
82
|
+
error(`Package problems: ${result.packageProblems.join(", ")}`);
|
|
83
|
+
if (result.cleanupFailures.length)
|
|
84
|
+
error("Some owned temporary files could not be removed. Inspect .bridge-repair-*.tmp in the reported asset parents; do not remove other files.");
|
|
85
|
+
if (!result.complete)
|
|
86
|
+
error("repair-assets: requested availability restoration is incomplete. Resolve reported permission/path/platform problems before retrying.");
|
|
87
|
+
return result.complete ? 0 : 1;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
error("repair-assets: repair failed; no unsafe fallback was attempted.");
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
* compiled module; the logic lives here (under src/, built by tsc) so it can
|
|
17
17
|
* be unit-tested with node:test like every other module in this package.
|
|
18
18
|
*/
|
|
19
|
-
import { readdirSync } from "fs";
|
|
19
|
+
import { readdirSync, mkdtempSync, mkdirSync, readFileSync, writeFileSync, renameSync, rmSync, existsSync } from "fs";
|
|
20
|
+
import { tmpdir } from "os";
|
|
20
21
|
import { spawnSync } from "child_process";
|
|
21
22
|
import path from "path";
|
|
22
23
|
const DEFAULT_BUILD_DIR = "build";
|
|
@@ -76,6 +77,7 @@ export const MODULE_MOCK_MANIFEST = [
|
|
|
76
77
|
"build/connect-github.test.js",
|
|
77
78
|
"build/council-submission.test.js",
|
|
78
79
|
"build/council-wait-recovery.test.js",
|
|
80
|
+
"build/generated-assets-publisher.test.js",
|
|
79
81
|
"build/index-artifacts.test.js",
|
|
80
82
|
"build/index-council-filenames.test.js",
|
|
81
83
|
"build/index-generate-decision-page.integration.test.js",
|
|
@@ -314,27 +316,31 @@ export function selectTestGroup(mode, discovered) {
|
|
|
314
316
|
*/
|
|
315
317
|
export function partitionTestBatches(files, options = {}) {
|
|
316
318
|
const { execPath = process.execPath, nodeOptions = [], maxLength = MAX_COMMAND_LENGTH } = options;
|
|
317
|
-
const
|
|
319
|
+
const size = (batch, index) => {
|
|
320
|
+
const args = options.argumentBuilder?.(batch, index) ?? [...nodeOptions, ...batch];
|
|
321
|
+
// Quoted serialization conservatively includes spaces/backslashes/quotes on
|
|
322
|
+
// Windows; legacy callers without an argument builder keep their old budget.
|
|
323
|
+
return options.argumentBuilder
|
|
324
|
+
? [execPath, ...args].map((arg) => JSON.stringify(arg)).join(" ").length
|
|
325
|
+
: [execPath, ...args].join(" ").length;
|
|
326
|
+
};
|
|
318
327
|
const batches = [];
|
|
319
328
|
let current = [];
|
|
320
|
-
let currentLength = prefixLength;
|
|
321
329
|
for (const file of files) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
throw new Error(`run-unit-tests: "${file}" alone exceeds the command-length budget ` +
|
|
325
|
-
`(${prefixLength + addition} > ${maxLength} chars) — cannot batch it safely`);
|
|
330
|
+
if (size([file], batches.length) > maxLength) {
|
|
331
|
+
throw new Error(`run-unit-tests: single file alone exceeds the command-length budget`);
|
|
326
332
|
}
|
|
327
|
-
if (current.length
|
|
333
|
+
if (current.length && size([...current, file], batches.length) > maxLength) {
|
|
328
334
|
batches.push(current);
|
|
329
335
|
current = [];
|
|
330
|
-
|
|
336
|
+
if (size([file], batches.length) > maxLength) {
|
|
337
|
+
throw new Error(`run-unit-tests: single file alone exceeds the command-length budget`);
|
|
338
|
+
}
|
|
331
339
|
}
|
|
332
340
|
current.push(file);
|
|
333
|
-
currentLength += addition;
|
|
334
341
|
}
|
|
335
|
-
if (current.length
|
|
342
|
+
if (current.length)
|
|
336
343
|
batches.push(current);
|
|
337
|
-
}
|
|
338
344
|
return batches;
|
|
339
345
|
}
|
|
340
346
|
/** Discovers, selects, batches, and spawns the requested test group; returns an aggregate exit code. */
|
|
@@ -355,30 +361,7 @@ export function runSelectedGroup(mode, options) {
|
|
|
355
361
|
const batchable = selected.filter((file) => !quarantineSet.has(file));
|
|
356
362
|
const quarantined = selected.filter((file) => quarantineSet.has(file));
|
|
357
363
|
const extraFlags = mode === "module-mocks" ? ["--experimental-test-module-mocks"] : [];
|
|
358
|
-
|
|
359
|
-
const batches = partitionTestBatches(batchable, { execPath, nodeOptions });
|
|
360
|
-
log(`run-unit-tests: group="${mode}" files=${selected.length} batches=${batches.length}` +
|
|
361
|
-
(quarantined.length > 0 ? ` quarantined=${quarantined.length}` : ""));
|
|
362
|
-
let aggregateExit = 0;
|
|
363
|
-
const spawn = (args) => {
|
|
364
|
-
const result = spawnFn(execPath, args, {
|
|
365
|
-
cwd: root,
|
|
366
|
-
stdio: "inherit",
|
|
367
|
-
shell: false,
|
|
368
|
-
});
|
|
369
|
-
if ((result.status ?? 1) !== 0) {
|
|
370
|
-
aggregateExit = 1;
|
|
371
|
-
}
|
|
372
|
-
};
|
|
373
|
-
batches.forEach((batch, index) => {
|
|
374
|
-
log(`run-unit-tests: batch ${index + 1}/${batches.length} (${batch.length} file(s))`);
|
|
375
|
-
spawn([...nodeOptions, ...batch]);
|
|
376
|
-
});
|
|
377
|
-
quarantined.forEach((file, index) => {
|
|
378
|
-
log(`run-unit-tests: quarantined ${index + 1}/${quarantined.length} ${file} (single-file, --test-force-exit)`);
|
|
379
|
-
spawn([...nodeOptions, "--test-force-exit", file]);
|
|
380
|
-
});
|
|
381
|
-
return aggregateExit;
|
|
364
|
+
return runWithReports(mode, options, batchable, quarantined, [...extraFlags, "--test"], log);
|
|
382
365
|
}
|
|
383
366
|
/**
|
|
384
367
|
* Returns the integration lane's file list: every compiled `*.test.js` under
|
|
@@ -420,20 +403,118 @@ export function runIntegrationLane(options) {
|
|
|
420
403
|
throw new Error("run-integration-tests: discovered zero integration tests — refusing to run " +
|
|
421
404
|
"(did you forget 'npm run build'?)");
|
|
422
405
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
406
|
+
return runWithReports("integration", options, selected, [], ["--test"], log);
|
|
407
|
+
}
|
|
408
|
+
/** Shared reporter argument builder: sizing and execution cannot diverge. */
|
|
409
|
+
export function reporterArguments(nodeOptions, files, destination, forceExit = false) {
|
|
410
|
+
return [...nodeOptions, ...(forceExit ? ["--test-force-exit"] : []),
|
|
411
|
+
"--test-reporter=spec", "--test-reporter-destination=stdout",
|
|
412
|
+
"--test-reporter=junit", `--test-reporter-destination=${destination}`, ...files];
|
|
413
|
+
}
|
|
414
|
+
function runWithReports(group, options, ordinary, quarantined, nodeOptions, log) {
|
|
415
|
+
const { root, execPath = process.execPath, spawnFn = spawnSync } = options;
|
|
416
|
+
const retained = options.reportOutputDirectory !== undefined;
|
|
417
|
+
let directory;
|
|
426
418
|
let aggregateExit = 0;
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
if ((result.status ?? 1) !== 0) {
|
|
435
|
-
aggregateExit = 1;
|
|
419
|
+
try {
|
|
420
|
+
if (retained) {
|
|
421
|
+
if (!options.reportOutputDirectory?.trim())
|
|
422
|
+
throw new Error("empty report directory");
|
|
423
|
+
directory = path.resolve(options.reportOutputDirectory);
|
|
424
|
+
// Exclusive allocation refuses stale reports and concurrent invocations.
|
|
425
|
+
mkdirSync(directory, { recursive: false });
|
|
436
426
|
}
|
|
437
|
-
|
|
427
|
+
else {
|
|
428
|
+
directory = mkdtempSync(path.join(tmpdir(), "bridge-junit-"));
|
|
429
|
+
}
|
|
430
|
+
const outputDirectory = directory;
|
|
431
|
+
const reportName = (index) => `report-${index}.xml`;
|
|
432
|
+
const args = (files, index, forceExit = false) => reporterArguments(nodeOptions, files, path.join(outputDirectory, reportName(index)), forceExit);
|
|
433
|
+
// Source mapping comes from the build's actual rootDir/outDir, including
|
|
434
|
+
// authored scripts (which do not pass through tsc).
|
|
435
|
+
const config = JSON.parse(readFileSync(new URL("../tsconfig.json", import.meta.url), "utf8"));
|
|
436
|
+
const { rootDir, outDir } = config.compilerOptions;
|
|
437
|
+
if (rootDir !== "src" || outDir !== "build")
|
|
438
|
+
throw new Error("unsupported build source mapping");
|
|
439
|
+
const sourceFor = (file) => {
|
|
440
|
+
const posix = file.replaceAll("\\", "/");
|
|
441
|
+
if (posix.startsWith(`${outDir}/`) && posix.endsWith(".js")) {
|
|
442
|
+
return `mcp_server/${rootDir}/${posix.slice(outDir.length + 1, -3)}.ts`;
|
|
443
|
+
}
|
|
444
|
+
if (posix.startsWith("scripts/") && posix.endsWith(".js"))
|
|
445
|
+
return `mcp_server/${posix}`;
|
|
446
|
+
throw new Error("unmapped test source");
|
|
447
|
+
};
|
|
448
|
+
// Node 24.16 discovery confirmed explicit testcase file attribution.
|
|
449
|
+
// Older local/scheduled runtimes use one source per invocation, so the
|
|
450
|
+
// inventory provides an unambiguous fallback without adding concurrency.
|
|
451
|
+
const [major, minor] = process.versions.node.split(".").map(Number);
|
|
452
|
+
const explicitAttribution = major > 24 || (major === 24 && minor >= 16);
|
|
453
|
+
const batches = explicitAttribution
|
|
454
|
+
? partitionTestBatches(ordinary, { execPath, nodeOptions, argumentBuilder: args })
|
|
455
|
+
: ordinary.map((file, index) => {
|
|
456
|
+
partitionTestBatches([file], { execPath, argumentBuilder: (files) => args(files, index) });
|
|
457
|
+
return [file];
|
|
458
|
+
});
|
|
459
|
+
const plans = [];
|
|
460
|
+
const addPlan = (files, forceExit) => {
|
|
461
|
+
const index = plans.length;
|
|
462
|
+
partitionTestBatches(files, { execPath, argumentBuilder: (values) => args(values, index, forceExit) });
|
|
463
|
+
const sources = Object.fromEntries(files.map((file) => [`mcp_server/${file}`, sourceFor(file)]));
|
|
464
|
+
// integrationSuite declares skipped suites inside the shared harness;
|
|
465
|
+
// Node attributes those cases to the wrapper, rather than its caller.
|
|
466
|
+
// Map that real declaration source using the same verified build config.
|
|
467
|
+
// Older runtimes still need exactly one entry for invocation attribution.
|
|
468
|
+
const harness = `${outDir}/integration/harness.js`;
|
|
469
|
+
if (group === "integration" && explicitAttribution &&
|
|
470
|
+
existsSync(path.join(root, harness)) &&
|
|
471
|
+
existsSync(path.join(root, rootDir, "integration/harness.ts"))) {
|
|
472
|
+
sources[`mcp_server/${harness}`] = sourceFor(harness);
|
|
473
|
+
}
|
|
474
|
+
plans.push({ report: reportName(index), files, forceExit, state: "planned", status: null,
|
|
475
|
+
sources });
|
|
476
|
+
};
|
|
477
|
+
batches.forEach((batch) => addPlan(batch, false));
|
|
478
|
+
quarantined.forEach((file) => addPlan([file], true));
|
|
479
|
+
const inventory = { version: 1, lane: group === "integration" ? "integration" : "unit", group,
|
|
480
|
+
nodeVersion: process.version, attribution: explicitAttribution ? "reporter-file" : "single-source", spawns: plans };
|
|
481
|
+
const publish = () => {
|
|
482
|
+
const temporary = path.join(outputDirectory, "inventory.json.tmp");
|
|
483
|
+
writeFileSync(temporary, JSON.stringify(inventory, null, 2));
|
|
484
|
+
renameSync(temporary, path.join(outputDirectory, "inventory.json"));
|
|
485
|
+
};
|
|
486
|
+
publish(); // Complete expected inventory is durable before the first spawn.
|
|
487
|
+
log(`run-unit-tests: group="${group}" files=${ordinary.length + quarantined.length} batches=${plans.length}`);
|
|
488
|
+
for (const [index, plan] of plans.entries()) {
|
|
489
|
+
plan.state = "started";
|
|
490
|
+
publish();
|
|
491
|
+
try {
|
|
492
|
+
const result = spawnFn(execPath, args(plan.files, index, plan.forceExit), { cwd: root, stdio: "inherit", shell: false });
|
|
493
|
+
plan.status = result.status;
|
|
494
|
+
plan.state = result.status === null || !existsSync(path.join(outputDirectory, plan.report)) ? "failed" : "completed";
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
plan.state = "failed";
|
|
498
|
+
}
|
|
499
|
+
if (plan.state !== "completed" || plan.status !== 0)
|
|
500
|
+
aggregateExit = 1;
|
|
501
|
+
publish();
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
// No exception/environment dump reaches CI logs or exported evidence.
|
|
506
|
+
log("run-unit-tests: report allocation, inventory publication, or execution failed");
|
|
507
|
+
aggregateExit = 1;
|
|
508
|
+
}
|
|
509
|
+
finally {
|
|
510
|
+
if (!retained && directory) {
|
|
511
|
+
try {
|
|
512
|
+
rmSync(directory, { recursive: true, force: true });
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
aggregateExit = 1;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
438
519
|
return aggregateExit;
|
|
439
520
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Shared leaf scrubber for durable untrusted text. */
|
|
2
|
+
const REDACTED = "[REDACTED]";
|
|
3
|
+
/**
|
|
4
|
+
* Patterns for values that must never reach durable state or a log line.
|
|
5
|
+
*
|
|
6
|
+
* Each rewrites only the secret itself and keeps the surrounding key or scheme, so a
|
|
7
|
+
* redacted record still shows *that* a credential was involved — useful for audit,
|
|
8
|
+
* and materially different from dropping the field.
|
|
9
|
+
*/
|
|
10
|
+
const SECRET_PATTERNS = [
|
|
11
|
+
// Bearer / token schemes in an Authorization header or bare.
|
|
12
|
+
[/\b(Bearer|Basic|Token)\s+[A-Za-z0-9._~+/=-]{8,}/gi, `$1 ${REDACTED}`],
|
|
13
|
+
// authorization: <anything>
|
|
14
|
+
[/\b(authorization)\s*[:=]\s*\S+/gi, `$1: ${REDACTED}`],
|
|
15
|
+
// Well-known provider key shapes.
|
|
16
|
+
[/\b(sk|pk|rk)-[A-Za-z0-9_-]{16,}/g, REDACTED],
|
|
17
|
+
[/\bgh[pousr]_[A-Za-z0-9]{16,}/g, REDACTED],
|
|
18
|
+
// key/secret/password/token assignments in prose, env, JSON, or CLI form.
|
|
19
|
+
[
|
|
20
|
+
/\b([A-Za-z0-9_-]*(?:api[_-]?key|secret|password|passwd|token|credential)[A-Za-z0-9_-]*)\s*[:=]\s*("[^"]*"|'[^']*'|\S+)/gi,
|
|
21
|
+
`$1=${REDACTED}`,
|
|
22
|
+
],
|
|
23
|
+
// Credentials embedded in a URL.
|
|
24
|
+
[/\b([a-z][a-z0-9+.-]*:\/\/)[^/\s:@]+:[^/\s@]+@/gi, `$1${REDACTED}@`],
|
|
25
|
+
];
|
|
26
|
+
/** Redact secret-bearing substrings while leaving safe technical text intact. */
|
|
27
|
+
export function redactSecrets(value) {
|
|
28
|
+
let output = value;
|
|
29
|
+
for (const [pattern, replacement] of SECRET_PATTERNS) {
|
|
30
|
+
output = output.replace(pattern, replacement);
|
|
31
|
+
}
|
|
32
|
+
return output;
|
|
33
|
+
}
|
package/build/setup-epic.js
CHANGED
|
@@ -170,6 +170,7 @@ export const SETUP_EPIC_REVIEW_POLICY_SOURCES = [
|
|
|
170
170
|
"verdict_protocol",
|
|
171
171
|
"native_review_decision",
|
|
172
172
|
"none",
|
|
173
|
+
"repository_policy",
|
|
173
174
|
];
|
|
174
175
|
export function createDefaultSetupEpicDeps() {
|
|
175
176
|
return {
|
|
@@ -260,6 +261,10 @@ export function getSetupEpicUsage() {
|
|
|
260
261
|
" --review-policy <src> PER-RUN review policy source, one of:",
|
|
261
262
|
` ${SETUP_EPIC_REVIEW_POLICY_SOURCES.join(", ")}.`,
|
|
262
263
|
" Composed into policy_json.review_policy on create.",
|
|
264
|
+
" Omitted, setup-epic sends no review policy: the server",
|
|
265
|
+
" stamps repository_policy at first approval for a new",
|
|
266
|
+
" feature-branch run with no repository default. AI review",
|
|
267
|
+
" is an explicit opt-in (--review-policy verdict_protocol).",
|
|
263
268
|
" This flag sets the policy for THIS run only and never",
|
|
264
269
|
" changes a repository default. To persist one, pass",
|
|
265
270
|
" --review-policy to `install-bridge conductor`, which",
|
|
@@ -1263,6 +1268,72 @@ export function policyDeclaresNotifyWebhook(policy) {
|
|
|
1263
1268
|
const url = notify.webhook_url;
|
|
1264
1269
|
return typeof url === "string" && url.trim() !== "";
|
|
1265
1270
|
}
|
|
1271
|
+
/**
|
|
1272
|
+
* BAPI-1397 (F1) — does this policy set `notify.local_sink` together with a
|
|
1273
|
+
* `notify.webhook_url`?
|
|
1274
|
+
*
|
|
1275
|
+
* Mirrors the server's `NotifyPolicy._reject_local_sink_and_webhook_together`
|
|
1276
|
+
* exactly, and nothing more: `local_sink: true` with a PRESENT, non-null
|
|
1277
|
+
* `webhook_url` conflicts; `local_sink: true` with the URL absent or `null` is
|
|
1278
|
+
* fine; `local_sink: false` (or absent) never conflicts. Every other policy rule
|
|
1279
|
+
* stays the server's to judge. Checked locally so a policy the server would
|
|
1280
|
+
* reject is refused before any plane starts or any request is sent.
|
|
1281
|
+
*/
|
|
1282
|
+
export function policyNotifyDestinationsConflict(policy) {
|
|
1283
|
+
if (policy === undefined || policy === null)
|
|
1284
|
+
return false;
|
|
1285
|
+
const notify = policy.notify;
|
|
1286
|
+
if (typeof notify !== "object" || notify === null || Array.isArray(notify))
|
|
1287
|
+
return false;
|
|
1288
|
+
const block = notify;
|
|
1289
|
+
return block.local_sink === true && block.webhook_url !== undefined && block.webhook_url !== null;
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Name the selected policy for a launch line or a refusal: its resolved path and
|
|
1293
|
+
* source, or — for a stored or default policy that has no file — a deliberate
|
|
1294
|
+
* no-file label. Only the path is named; the file's content never is.
|
|
1295
|
+
*/
|
|
1296
|
+
export function describeSelectedPolicy(policyPath, source) {
|
|
1297
|
+
return policyPath !== undefined
|
|
1298
|
+
? `'${policyPath}' (${source} policy file)`
|
|
1299
|
+
: `the ${source} run policy (no policy file)`;
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* The refusal for a policy whose notify destinations conflict. Names the file and
|
|
1303
|
+
* the two keys only — never the webhook URL or any other policy content.
|
|
1304
|
+
*/
|
|
1305
|
+
export function notifyDestinationsConflictMessage(policyPath, source) {
|
|
1306
|
+
return (`The run policy ${describeSelectedPolicy(policyPath, source)} sets notify.local_sink ` +
|
|
1307
|
+
"together with notify.webhook_url. The server permits only one delivery destination, so " +
|
|
1308
|
+
"it would reject this policy. Remove notify.local_sink or notify.webhook_url and re-run. " +
|
|
1309
|
+
"No plane was started and no request was sent.");
|
|
1310
|
+
}
|
|
1311
|
+
/** Group-read and other-read permission bits. */
|
|
1312
|
+
const GROUP_OR_OTHER_READ_BITS = 0o044;
|
|
1313
|
+
/**
|
|
1314
|
+
* BAPI-1397 (F1) — warn when a policy file that carries a `notify.webhook_url` is
|
|
1315
|
+
* readable by other users. A warning only, never a refusal: the launch continues.
|
|
1316
|
+
*
|
|
1317
|
+
* Returns the warning text, or `null` when there is nothing to say — including
|
|
1318
|
+
* when the file's mode cannot be read (a failed `stat` is silently skipped). The
|
|
1319
|
+
* text names the path only; it never carries the URL, the policy body, or any
|
|
1320
|
+
* filesystem error text.
|
|
1321
|
+
*/
|
|
1322
|
+
export async function worldReadableWebhookPolicyWarning(policyPath, policy, stat) {
|
|
1323
|
+
if (!policyDeclaresNotifyWebhook(policy))
|
|
1324
|
+
return null;
|
|
1325
|
+
let mode;
|
|
1326
|
+
try {
|
|
1327
|
+
mode = (await stat(policyPath)).mode;
|
|
1328
|
+
}
|
|
1329
|
+
catch {
|
|
1330
|
+
return null;
|
|
1331
|
+
}
|
|
1332
|
+
if (typeof mode !== "number" || (mode & GROUP_OR_OTHER_READ_BITS) === 0)
|
|
1333
|
+
return null;
|
|
1334
|
+
return (`[warn] Policy file '${policyPath}' holds a notify webhook URL and other users on this ` +
|
|
1335
|
+
`machine can read it. Restrict it with: chmod 600 '${policyPath}'`);
|
|
1336
|
+
}
|
|
1266
1337
|
export function evaluateSetupEpicUnattendedPrerequisites(readiness, requestedPolicy) {
|
|
1267
1338
|
const unattended = readiness?.unattended ?? null;
|
|
1268
1339
|
if (unattended === null) {
|
|
@@ -1974,6 +2045,21 @@ async function executeSetupEpic(argv, overrides, runIdSink, handoff) {
|
|
|
1974
2045
|
return 1;
|
|
1975
2046
|
}
|
|
1976
2047
|
const localHash = hashPlan(plan);
|
|
2048
|
+
// --- Notify destination conflict, explicit file (BAPI-1397, F1) ----------
|
|
2049
|
+
// An explicit `--policy-file` is always the selected policy, so a conflict the
|
|
2050
|
+
// server would reject is refused HERE — before credentials are resolved and
|
|
2051
|
+
// before any request. A file that cannot be read or parsed is left to the
|
|
2052
|
+
// policy read below, which reports it in its own words, unchanged.
|
|
2053
|
+
const pinnedLaunchPolicy = handoff ? getPinnedEpicLaunchPolicy(handoff.selection) : undefined;
|
|
2054
|
+
if (opts.policyFile !== undefined) {
|
|
2055
|
+
const earlyPolicy = pinnedLaunchPolicy !== undefined
|
|
2056
|
+
? { ok: true, policy: pinnedLaunchPolicy }
|
|
2057
|
+
: await readSetupEpicPolicyFile(opts.policyFile, deps.readFile);
|
|
2058
|
+
if (earlyPolicy.ok && policyNotifyDestinationsConflict(earlyPolicy.policy)) {
|
|
2059
|
+
deps.errorLog(notifyDestinationsConflictMessage(opts.policyFile, "explicit"));
|
|
2060
|
+
return 1;
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
1977
2063
|
// Resolve the selected target and live-run identity before choosing a policy.
|
|
1978
2064
|
// The offline local-only path keeps its no-network contract.
|
|
1979
2065
|
let access;
|
|
@@ -2092,8 +2178,12 @@ async function executeSetupEpic(argv, overrides, runIdSink, handoff) {
|
|
|
2092
2178
|
const selectedPolicyPath = opts.policyFile ?? (existingRunId === null ? selectedProfilePolicyPath : undefined);
|
|
2093
2179
|
const selectedPolicySource = existingRunId !== null
|
|
2094
2180
|
? "stored" : opts.policyFile !== undefined ? "explicit" : selectedProfilePolicyPath !== undefined ? "profile" : "default";
|
|
2181
|
+
// BAPI-1397 (F1): the source NAMED next to a policy path in operator output. An
|
|
2182
|
+
// explicit file is always "explicit" here, even when the effective-policy facts
|
|
2183
|
+
// above report a reused run's policy as "stored".
|
|
2184
|
+
const policyLabelSource = opts.policyFile !== undefined ? "explicit" : selectedPolicySource;
|
|
2095
2185
|
if (selectedPolicyPath !== undefined) {
|
|
2096
|
-
const pinned =
|
|
2186
|
+
const pinned = pinnedLaunchPolicy;
|
|
2097
2187
|
if (pinned !== undefined) {
|
|
2098
2188
|
filePolicy = pinned;
|
|
2099
2189
|
}
|
|
@@ -2105,6 +2195,17 @@ async function executeSetupEpic(argv, overrides, runIdSink, handoff) {
|
|
|
2105
2195
|
}
|
|
2106
2196
|
filePolicy = policyRead.policy;
|
|
2107
2197
|
}
|
|
2198
|
+
// BAPI-1397 (F1): the same server rule, for a profile-selected file. Still
|
|
2199
|
+
// before any write or plane bring-up; the refusal names the resolved file.
|
|
2200
|
+
if (policyNotifyDestinationsConflict(filePolicy)) {
|
|
2201
|
+
deps.errorLog(notifyDestinationsConflictMessage(selectedPolicyPath, policyLabelSource));
|
|
2202
|
+
return 1;
|
|
2203
|
+
}
|
|
2204
|
+
// BAPI-1397 (F1): a webhook URL in a file other users can read is a warning,
|
|
2205
|
+
// never a refusal. Only the path is named.
|
|
2206
|
+
const readableWarning = await worldReadableWebhookPolicyWarning(selectedPolicyPath, filePolicy, deps.stat);
|
|
2207
|
+
if (readableWarning !== null)
|
|
2208
|
+
deps.errorLog(readableWarning);
|
|
2108
2209
|
}
|
|
2109
2210
|
const policyDeclaresBranch = filePolicy !== undefined &&
|
|
2110
2211
|
["base_branch", "baseBranch"].some((key) => key in filePolicy && filePolicy[key] !== undefined && filePolicy[key] !== null);
|
|
@@ -2270,7 +2371,10 @@ async function executeSetupEpic(argv, overrides, runIdSink, handoff) {
|
|
|
2270
2371
|
"human-gated) — pass --attended to opt out"
|
|
2271
2372
|
: "";
|
|
2272
2373
|
const shape = inherited === "" ? "complete run policy" : "run policy";
|
|
2273
|
-
|
|
2374
|
+
// BAPI-1397 (F1): the RESOLVED path and source — `opts.policyFile` is
|
|
2375
|
+
// undefined for a profile-selected or composed policy.
|
|
2376
|
+
say(`Policy: ${describeSelectedPolicy(selectedPolicyPath, policyLabelSource)} ` +
|
|
2377
|
+
`(${shape}, applied at creation${inherited})`);
|
|
2274
2378
|
}
|
|
2275
2379
|
if (effectiveFeatureBranch !== undefined && featureBranch === undefined) {
|
|
2276
2380
|
say(`Feature: ${effectiveFeatureBranch} (from the policy file; cut on origin with local ` +
|
|
@@ -2858,7 +2962,8 @@ async function executeSetupEpic(argv, overrides, runIdSink, handoff) {
|
|
|
2858
2962
|
// Re-sanitizing here would only destroy the field name that makes the
|
|
2859
2963
|
// server's field-named 422 actionable. The property is pinned by the
|
|
2860
2964
|
// regression test below rather than left to this comment.
|
|
2861
|
-
deps.errorLog(`The run policy
|
|
2965
|
+
deps.errorLog(`The run policy ${describeSelectedPolicy(selectedPolicyPath, policyLabelSource)} ` +
|
|
2966
|
+
`was rejected by the server.\n` +
|
|
2862
2967
|
`${errorDetail(err)}\n` +
|
|
2863
2968
|
`No run was created and no automation-start charge occurred. Fix the ` +
|
|
2864
2969
|
`named field in the policy file and re-run.`);
|