@bridge_gpt/mcp-server 0.2.67 → 0.2.69
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 +272 -51
- 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 +1362 -0
- package/build/check-update-cli.js +74 -0
- package/build/claude-settings.js +102 -2
- package/build/cli-help.js +27 -0
- package/build/codex-home.js +9 -1
- package/build/codex-launcher-pin.js +598 -0
- package/build/command-assets-doctor.js +24 -70
- package/build/command-provisioning.js +29 -5
- package/build/commands.generated.js +9 -9
- 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/connect-github-api.js +33 -14
- package/build/credential-store.js +207 -47
- package/build/docs.generated.js +57 -3
- package/build/doctor.js +198 -4
- package/build/drive-epic.js +45 -8
- package/build/epic-coherence-input.test-fixtures.js +4 -3
- package/build/epic-implementer-bin.js +1369 -25
- package/build/epic-workarounds-path.js +39 -0
- package/build/executor/cli.js +30 -0
- package/build/executor/completion-contract.js +20 -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/http-client.js +43 -4
- 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 +1109 -695
- 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 +722 -252
- package/build/launcher-config-inspection.js +184 -19
- package/build/mcp-host-targets.js +4 -0
- package/build/mcp-identity.js +7 -0
- 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/session-upgrade-notice.js +86 -0
- package/build/setup-epic.js +108 -3
- package/build/start-tickets-prereqs.js +70 -16
- package/build/start-tickets.js +44 -5
- package/build/tool-call-advisory.js +162 -0
- package/build/tool-surface-gating.js +32 -57
- package/build/update-check.js +97 -16
- package/build/update-status.js +61 -71
- package/build/upgrade-advice.js +15 -11
- package/build/upgrade-cli.js +398 -17
- 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
package/build/upgrade-cli.js
CHANGED
|
@@ -30,9 +30,41 @@
|
|
|
30
30
|
*
|
|
31
31
|
* Every launcher claim is verified by RE-READING the configs after the write.
|
|
32
32
|
* The command never reports a transition it did not confirm on disk.
|
|
33
|
+
*
|
|
34
|
+
* ---------------------------------------------------------------------------
|
|
35
|
+
* BAPI-1445 — Codex's global config
|
|
36
|
+
* ---------------------------------------------------------------------------
|
|
37
|
+
* The three project JSON configs are not the only place a Bridge pin lives:
|
|
38
|
+
* Codex keeps its registration in one per-user TOML file
|
|
39
|
+
* (`~/.codex/config.toml`, or `$CODEX_HOME/config.toml` for an absolute
|
|
40
|
+
* override). A repin that skipped it left Codex on an old server for every
|
|
41
|
+
* project on the machine. Upgrade now inspects, repins, and verifies that file
|
|
42
|
+
* too, beside the JSON targets, with these boundaries:
|
|
43
|
+
*
|
|
44
|
+
* - Preflight refusal guarantees NO writes. An unsafe Codex file (a range, tag,
|
|
45
|
+
* or ahead pin, a duplicate or unlocatable registration, an unreadable file)
|
|
46
|
+
* stops the run in Phase 2, before local-install removal, `runInit`, any
|
|
47
|
+
* config write, or the permission write.
|
|
48
|
+
* - A failure AFTER `runInit` can leave the JSON configs upgraded. It exits
|
|
49
|
+
* through `did NOT converge` with exit code 1 — no rollback, no automatic
|
|
50
|
+
* retry, no completion line, and no reload guidance.
|
|
51
|
+
* - The Codex write is one surgical splice of the package specifier, written in
|
|
52
|
+
* place only if the file is byte-identical to what Phase 2 read. That compare
|
|
53
|
+
* is optimistic protection, not a lock.
|
|
54
|
+
* - Install is unchanged: it keeps its existing-table detection, its
|
|
55
|
+
* `manual-required` outcome, and its current guidance. Upgrade adds no install
|
|
56
|
+
* handoff and makes no repair calls. `LAUNCHER_CONFIG_TARGETS`, `runInit`,
|
|
57
|
+
* doctor, and Phases 1, 4, and 8 are untouched.
|
|
58
|
+
* - Windsurf is excluded by its `manual-instructions` write strategy: upgrade
|
|
59
|
+
* never inspects or writes its file, never prints its reload guidance, and
|
|
60
|
+
* leaves its manual setup with the operator.
|
|
61
|
+
* - Upgrade-notice delivery is a separate concern. The one thing this file does
|
|
62
|
+
* for it (BAPI-1446) is Phase 8's advisory SessionStart hook request, for a
|
|
63
|
+
* Claude Code installation only, pinned to the converged target version.
|
|
33
64
|
*/
|
|
34
65
|
import { spawn } from "child_process";
|
|
35
|
-
import { readFile, stat } from "fs/promises";
|
|
66
|
+
import { mkdir, readFile, stat, writeFile } from "fs/promises";
|
|
67
|
+
import os from "os";
|
|
36
68
|
import path from "path";
|
|
37
69
|
import { VERSION } from "./version.generated.js";
|
|
38
70
|
import { runInit } from "./init.js";
|
|
@@ -40,11 +72,48 @@ import { InitPreflightError } from "./init.js";
|
|
|
40
72
|
import { isNewerVersion } from "./update-check.js";
|
|
41
73
|
import { fetchLatestVersion } from "./cli-release.js";
|
|
42
74
|
import { CANONICAL_UPGRADE_COMMAND } from "./upgrade-advice.js";
|
|
43
|
-
import {
|
|
75
|
+
import { applyCodexRepin, inspectCodexLauncher } from "./codex-launcher-pin.js";
|
|
76
|
+
import { describeLauncherReason, inspectGlobalLauncherConfigs, inspectLauncherConfigs, isApplicableStandardLauncher, isPrerequisiteFailure, LAUNCHER_CONFIG_TARGETS, resolveCodexConfigLocation, } from "./launcher-config-inspection.js";
|
|
77
|
+
import { allHostTargets, HOST_PLATFORM_ORDER, MCP_HOST_TARGETS, } from "./mcp-host-targets.js";
|
|
44
78
|
import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
|
|
79
|
+
import { applyProjectPermissions, BRIDGE_PERMISSIONS_MANIFEST_RELPATH, BRIDGE_PROJECT_SETTINGS_RELPATH, describeSessionStartHookOutcome, } from "./bridge-permissions.js";
|
|
80
|
+
import { readMcpInstallState } from "./mcp-install-state.js";
|
|
81
|
+
import { ensureGitignored } from "./git-ignore-utils.js";
|
|
45
82
|
import { defaultNpmEntryFileExists, resolveNpmEntry } from "./windows-npm-entry.js";
|
|
46
83
|
/** The sentinel this CLI passes to the child it re-execs from `@latest`. */
|
|
47
84
|
const INTERNAL_REEXEC_FLAG = "--internal-reexec";
|
|
85
|
+
/** Production binding of {@link UpgradeCliDeps.applyBridgePermissions}. */
|
|
86
|
+
export async function defaultApplyBridgePermissions(cwd, env, sessionStartHook) {
|
|
87
|
+
const fsDeps = {
|
|
88
|
+
readFile: (filePath) => readFile(filePath, "utf-8"),
|
|
89
|
+
writeFile: (filePath, data) => writeFile(filePath, data, "utf-8"),
|
|
90
|
+
mkdir: (dirPath, options) => mkdir(dirPath, options),
|
|
91
|
+
};
|
|
92
|
+
for (const relPath of [BRIDGE_PERMISSIONS_MANIFEST_RELPATH, BRIDGE_PROJECT_SETTINGS_RELPATH]) {
|
|
93
|
+
try {
|
|
94
|
+
await ensureGitignored(cwd, relPath, fsDeps);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Advisory: the files are still written, and doctor reads them either way.
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return applyProjectPermissions({ cwd, env, sessionStartHook }, fsDeps);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Is this a Claude Code installation? Answered from the install state `install`
|
|
104
|
+
* recorded, not from a bare `.mcp.json`: that file is also written for other hosts
|
|
105
|
+
* and by hand, so its presence proves nothing about Claude Code. A missing,
|
|
106
|
+
* invalid, or unreadable state is "no", so the hook is never written on a guess.
|
|
107
|
+
*/
|
|
108
|
+
async function isClaudeCodeInstallation(deps) {
|
|
109
|
+
try {
|
|
110
|
+
const state = await readMcpInstallState(deps.cwd, { readFile: deps.readFile });
|
|
111
|
+
return state.status === "valid" && state.state.selectedPlatforms.includes("claude-code");
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
48
117
|
/**
|
|
49
118
|
* The fixed, secret-free message printed when, on Windows, the latest CLI could
|
|
50
119
|
* not be STARTED. Upgrade stops here rather than continuing with the local copy.
|
|
@@ -179,6 +248,10 @@ export function createDefaultUpgradeCliDeps(cwd) {
|
|
|
179
248
|
cwd,
|
|
180
249
|
fetchLatestVersion,
|
|
181
250
|
readFile: (filePath) => readFile(filePath, "utf-8"),
|
|
251
|
+
// In place on purpose: writing through the existing file keeps its mode, owner,
|
|
252
|
+
// and symlink behavior, none of which a rename-based write would preserve.
|
|
253
|
+
writeFile: (filePath, content) => writeFile(filePath, content, "utf-8"),
|
|
254
|
+
homedir: () => os.homedir(),
|
|
182
255
|
hasCompetingLocalInstall: defaultHasCompetingLocalInstall,
|
|
183
256
|
removeCompetingLocalInstall: (installCwd) => defaultRemoveCompetingLocalInstall(installCwd),
|
|
184
257
|
runInit,
|
|
@@ -186,6 +259,7 @@ export function createDefaultUpgradeCliDeps(cwd) {
|
|
|
186
259
|
stdout: (m) => console.log(m),
|
|
187
260
|
stderr: (m) => console.error(m),
|
|
188
261
|
env: process.env,
|
|
262
|
+
applyBridgePermissions: defaultApplyBridgePermissions,
|
|
189
263
|
};
|
|
190
264
|
}
|
|
191
265
|
// ---------------------------------------------------------------------------
|
|
@@ -296,6 +370,177 @@ export function isEveryLauncherAlreadyCurrent(actions) {
|
|
|
296
370
|
return applicable.every((a) => a.kind === "already-current");
|
|
297
371
|
}
|
|
298
372
|
// ---------------------------------------------------------------------------
|
|
373
|
+
// Global (Codex) config — BAPI-1445
|
|
374
|
+
// ---------------------------------------------------------------------------
|
|
375
|
+
/** The label that tells a reader this config is machine-wide, not per-project. */
|
|
376
|
+
const GLOBAL_SCOPE_LABEL = "global — applies to every project on this machine";
|
|
377
|
+
/** The path-plus-scope label a global config line carries before its colon. */
|
|
378
|
+
function globalConfigLabel(displayPath) {
|
|
379
|
+
return `${displayPath} (${GLOBAL_SCOPE_LABEL})`;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Project a global inspection onto the project-inspection shape so the report
|
|
383
|
+
* helpers reuse the existing transition, current, normalization, and shim wording
|
|
384
|
+
* rather than growing a second set that could drift.
|
|
385
|
+
*/
|
|
386
|
+
function asProjectInspection(found) {
|
|
387
|
+
return { ...found, relPath: globalConfigLabel(found.displayPath), topLevelKey: "mcp_servers" };
|
|
388
|
+
}
|
|
389
|
+
/** Render the dry-run preview line for the global Codex config, or null when inactive. */
|
|
390
|
+
export function formatGlobalDryRunLine(found) {
|
|
391
|
+
return formatDryRunLine(asProjectInspection(found));
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* The real-run action for the global Codex config, in the form
|
|
395
|
+
* {@link formatResultLine} already renders. Called only after verification
|
|
396
|
+
* passed: a failed write is never advertised as a completed repin.
|
|
397
|
+
*/
|
|
398
|
+
function globalResultAction(initial, targetVersion) {
|
|
399
|
+
const relPath = globalConfigLabel(initial.displayPath);
|
|
400
|
+
const registrationKey = initial.registrationKey ?? undefined;
|
|
401
|
+
switch (initial.action) {
|
|
402
|
+
case "repin":
|
|
403
|
+
return {
|
|
404
|
+
relPath,
|
|
405
|
+
kind: "repinned",
|
|
406
|
+
registrationKey,
|
|
407
|
+
fromVersion: initial.currentVersion ?? undefined,
|
|
408
|
+
toVersion: targetVersion,
|
|
409
|
+
};
|
|
410
|
+
case "normalize-unpinned":
|
|
411
|
+
return {
|
|
412
|
+
relPath,
|
|
413
|
+
kind: "normalized",
|
|
414
|
+
registrationKey,
|
|
415
|
+
fromSpec: initial.spec ?? undefined,
|
|
416
|
+
toVersion: targetVersion,
|
|
417
|
+
};
|
|
418
|
+
case "already-current":
|
|
419
|
+
return { relPath, kind: "already-current", registrationKey, toVersion: targetVersion };
|
|
420
|
+
case "skip-worktree-shim":
|
|
421
|
+
return { relPath, kind: "skipped-worktree-shim", registrationKey };
|
|
422
|
+
default:
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
async function snapshotCodexConfig(deps, env, targetVersion) {
|
|
427
|
+
const home = deps.homedir();
|
|
428
|
+
const location = resolveCodexConfigLocation(env, home);
|
|
429
|
+
const captured = { source: null };
|
|
430
|
+
const inspection = await inspectGlobalLauncherConfigs({
|
|
431
|
+
env,
|
|
432
|
+
homedir: home,
|
|
433
|
+
targetVersion,
|
|
434
|
+
readFile: async (filePath) => {
|
|
435
|
+
const text = await deps.readFile(filePath);
|
|
436
|
+
captured.source = text;
|
|
437
|
+
return text;
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
return { ...location, inspection, source: captured.source };
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* The guarded Phase 5 Codex write. Computes the one-token splice from the Phase 2
|
|
444
|
+
* snapshot, re-reads the file, and writes ONLY if it is still byte-identical to
|
|
445
|
+
* that snapshot. The comparison is optimistic protection against a concurrent
|
|
446
|
+
* edit, not a lock. Every failure is a fixed, path-specific sentence — never the
|
|
447
|
+
* TOML, an argument, an environment value, or exception text.
|
|
448
|
+
*/
|
|
449
|
+
async function applyGuardedCodexWrite(deps, snapshot, targetVersion) {
|
|
450
|
+
const where = snapshot.displayPath;
|
|
451
|
+
let expected;
|
|
452
|
+
try {
|
|
453
|
+
if (snapshot.source === null)
|
|
454
|
+
throw new Error("no snapshot");
|
|
455
|
+
expected = applyCodexRepin(snapshot.source, inspectCodexLauncher(snapshot.source), targetVersion);
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
return { failure: `${where}: the Codex configuration could not be updated safely; it was left unchanged` };
|
|
459
|
+
}
|
|
460
|
+
let current;
|
|
461
|
+
try {
|
|
462
|
+
current = await deps.readFile(snapshot.filePath);
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
return { failure: `${where}: the Codex configuration could not be re-read before writing; it was left unchanged` };
|
|
466
|
+
}
|
|
467
|
+
if (current !== snapshot.source) {
|
|
468
|
+
return { failure: `${where}: the Codex configuration changed after it was inspected; it was left unchanged` };
|
|
469
|
+
}
|
|
470
|
+
try {
|
|
471
|
+
await deps.writeFile(snapshot.filePath, expected);
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
return { failure: `${where}: the Codex configuration could not be written` };
|
|
475
|
+
}
|
|
476
|
+
return { expected };
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Phase 6 for Codex: re-inspect and prove the claim. A written config must be
|
|
480
|
+
* current at the exact target under its ORIGINAL registration name AND equal the
|
|
481
|
+
* expected spliced text exactly — a correct pin beside any other difference is
|
|
482
|
+
* still a failure. Initially inactive configs are nonparticipants and are never
|
|
483
|
+
* verified, created, or repaired.
|
|
484
|
+
*/
|
|
485
|
+
async function verifyCodexConfig(deps, env, snapshot, targetVersion, expected) {
|
|
486
|
+
const initial = snapshot.inspection;
|
|
487
|
+
if (initial.action === "skip-inactive")
|
|
488
|
+
return { inspection: null, failures: [] };
|
|
489
|
+
const where = snapshot.displayPath;
|
|
490
|
+
let after;
|
|
491
|
+
try {
|
|
492
|
+
after = await snapshotCodexConfig(deps, env, targetVersion);
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
return {
|
|
496
|
+
inspection: null,
|
|
497
|
+
failures: [`${where}: could not verify the Codex configuration after writing`],
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
const found = after.inspection;
|
|
501
|
+
const fail = (message) => ({ inspection: found, failures: [`${where}: ${message}`] });
|
|
502
|
+
if (!found.filePresent)
|
|
503
|
+
return fail("the Codex configuration disappeared after the write");
|
|
504
|
+
if (found.reason === "file-unreadable") {
|
|
505
|
+
return fail("the Codex configuration could not be read back after the write");
|
|
506
|
+
}
|
|
507
|
+
if (found.action === "duplicate-registration") {
|
|
508
|
+
return fail("the Codex configuration now carries two Bridge registrations");
|
|
509
|
+
}
|
|
510
|
+
if (found.reason === "unlocatable" || found.action === "skip-inactive") {
|
|
511
|
+
return fail("the Codex Bridge launcher can no longer be located after the write");
|
|
512
|
+
}
|
|
513
|
+
if (found.registrationKey !== initial.registrationKey) {
|
|
514
|
+
return fail("the Codex Bridge entry no longer lives under its original registration name");
|
|
515
|
+
}
|
|
516
|
+
if (initial.action === "skip-worktree-shim") {
|
|
517
|
+
return found.action === "skip-worktree-shim"
|
|
518
|
+
? { inspection: found, failures: [] }
|
|
519
|
+
: fail("a worktree shim was expected to be preserved but no longer looks like one");
|
|
520
|
+
}
|
|
521
|
+
if (found.action !== "already-current" || found.currentVersion !== targetVersion) {
|
|
522
|
+
return fail(`expected ${MCP_PACKAGE_NAME}@${targetVersion} after the write`);
|
|
523
|
+
}
|
|
524
|
+
if (expected !== null && after.source !== expected) {
|
|
525
|
+
return fail("the Codex configuration differs from the expected upgraded text");
|
|
526
|
+
}
|
|
527
|
+
return { inspection: found, failures: [] };
|
|
528
|
+
}
|
|
529
|
+
/** The host a project config path belongs to, derived from the registry. */
|
|
530
|
+
function hostIdForRelPath(relPath) {
|
|
531
|
+
return allHostTargets().find((t) => t.scope === "project" && t.relPath === relPath)?.id;
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* One `Reload <host>: <hint>.` line per changed, automatically managed host, in
|
|
535
|
+
* registry order. Hint text lives on the registry entry; nothing here names a
|
|
536
|
+
* host or a path.
|
|
537
|
+
*/
|
|
538
|
+
export function reloadGuidanceLines(changed) {
|
|
539
|
+
return HOST_PLATFORM_ORDER.map((id) => MCP_HOST_TARGETS[id])
|
|
540
|
+
.filter((t) => changed.has(t.id) && t.writeStrategy !== "manual-instructions" && t.reloadHint)
|
|
541
|
+
.map((t) => `Reload ${t.label}: ${t.reloadHint}.`);
|
|
542
|
+
}
|
|
543
|
+
// ---------------------------------------------------------------------------
|
|
299
544
|
// The command
|
|
300
545
|
// ---------------------------------------------------------------------------
|
|
301
546
|
export async function runUpgradeCli(argv, injected) {
|
|
@@ -341,13 +586,32 @@ export async function runUpgradeCli(argv, injected) {
|
|
|
341
586
|
stderr(`Bridge API upgrade failed: could not inspect launcher configs: ${msg}`);
|
|
342
587
|
return 1;
|
|
343
588
|
}
|
|
589
|
+
// BAPI-1445: Codex's global config, inspected beside the project targets in its
|
|
590
|
+
// OWN error boundary so an unexpected failure is reported with fixed text and
|
|
591
|
+
// the safe display path, never with exception text that could carry a path.
|
|
592
|
+
const env = deps.env ?? {};
|
|
593
|
+
let codexSnapshot;
|
|
594
|
+
try {
|
|
595
|
+
codexSnapshot = await snapshotCodexConfig(deps, env, target.version);
|
|
596
|
+
}
|
|
597
|
+
catch {
|
|
598
|
+
const where = resolveCodexConfigLocation(env, "").displayPath;
|
|
599
|
+
stderr(`Bridge API upgrade failed: could not inspect the Codex configuration at ${where}.`);
|
|
600
|
+
stderr(" Nothing was changed.");
|
|
601
|
+
return 1;
|
|
602
|
+
}
|
|
603
|
+
const codex = codexSnapshot.inspection;
|
|
344
604
|
const blocking = inspections.filter(isPrerequisiteFailure);
|
|
345
605
|
const ahead = inspections.filter((i) => i.action === "ahead");
|
|
346
|
-
|
|
606
|
+
const codexRefused = isPrerequisiteFailure(codex) || codex.action === "ahead";
|
|
607
|
+
if (blocking.length > 0 || ahead.length > 0 || codexRefused) {
|
|
347
608
|
stderr("Bridge API upgrade aborted — these configs cannot be safely reconciled:");
|
|
348
609
|
for (const found of [...blocking, ...ahead]) {
|
|
349
610
|
stderr(` ${found.relPath}: ${describeLauncherReason(found.reason)}`);
|
|
350
611
|
}
|
|
612
|
+
if (codexRefused) {
|
|
613
|
+
stderr(` ${codex.displayPath}: ${describeLauncherReason(codex.reason)}`);
|
|
614
|
+
}
|
|
351
615
|
stderr(" Nothing was changed. Resolve these by hand, then re-run the upgrade.");
|
|
352
616
|
return 1;
|
|
353
617
|
}
|
|
@@ -361,9 +625,7 @@ export async function runUpgradeCli(argv, injected) {
|
|
|
361
625
|
}
|
|
362
626
|
stdout(`- Would detect and remove competing local install in node_modules/${MCP_PACKAGE_NAME} if present.`);
|
|
363
627
|
stdout(`- Launcher pins (target ${MCP_PACKAGE_NAME}@${target.version}):`);
|
|
364
|
-
const lines = inspections
|
|
365
|
-
.map(formatDryRunLine)
|
|
366
|
-
.filter((l) => l !== null);
|
|
628
|
+
const lines = [...inspections.map(formatDryRunLine), formatGlobalDryRunLine(codex)].filter((l) => l !== null);
|
|
367
629
|
if (lines.length === 0) {
|
|
368
630
|
stdout(" (no applicable launcher configs found)");
|
|
369
631
|
}
|
|
@@ -371,11 +633,24 @@ export async function runUpgradeCli(argv, injected) {
|
|
|
371
633
|
for (const line of lines)
|
|
372
634
|
stdout(line);
|
|
373
635
|
}
|
|
374
|
-
|
|
636
|
+
const plannedInspections = [...inspections, asProjectInspection(codex)];
|
|
637
|
+
if (isEveryLauncherAlreadyCurrent(toPlannedActions(plannedInspections))) {
|
|
375
638
|
stdout(`- Would print: Already up-to-date.`);
|
|
376
639
|
}
|
|
377
640
|
stdout(`- Would refresh scaffolded artifacts (commands, agents, pipelines).`);
|
|
378
|
-
|
|
641
|
+
// Reload guidance only for hosts whose configuration this run would change.
|
|
642
|
+
const plannedHosts = new Set();
|
|
643
|
+
for (const found of inspections) {
|
|
644
|
+
if (PLANNED_CHANGE_ACTIONS.has(found.action)) {
|
|
645
|
+
const id = hostIdForRelPath(found.relPath);
|
|
646
|
+
if (id)
|
|
647
|
+
plannedHosts.add(id);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (PLANNED_CHANGE_ACTIONS.has(codex.action))
|
|
651
|
+
plannedHosts.add(codex.hostId);
|
|
652
|
+
for (const line of reloadGuidanceLines(plannedHosts))
|
|
653
|
+
stdout(`- Would print: ${line}`);
|
|
379
654
|
return 0;
|
|
380
655
|
}
|
|
381
656
|
// ---- Phase 4: competing local install — a reconciliation, not a transition ----
|
|
@@ -414,6 +689,21 @@ export async function runUpgradeCli(argv, injected) {
|
|
|
414
689
|
stderr(`Bridge API upgrade failed: ${msg}`);
|
|
415
690
|
return 1;
|
|
416
691
|
}
|
|
692
|
+
// ---- Phase 5b (BAPI-1445): the guarded Codex write ----
|
|
693
|
+
// Only after `runInit` succeeded, and only for a pin that has to move. A failure
|
|
694
|
+
// here is recorded as a convergence failure, never thrown: the JSON configs
|
|
695
|
+
// `runInit` already upgraded stay upgraded (no rollback) and nothing retries.
|
|
696
|
+
const codexFailures = [];
|
|
697
|
+
let codexExpected = null;
|
|
698
|
+
if (codex.action === "repin" || codex.action === "normalize-unpinned") {
|
|
699
|
+
const written = await applyGuardedCodexWrite(deps, codexSnapshot, target.version);
|
|
700
|
+
if ("failure" in written) {
|
|
701
|
+
codexFailures.push(written.failure);
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
codexExpected = written.expected;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
417
707
|
// ---- Phase 6: verify every claim by re-reading the configs ----
|
|
418
708
|
let verified;
|
|
419
709
|
try {
|
|
@@ -471,10 +761,22 @@ export async function runUpgradeCli(argv, injected) {
|
|
|
471
761
|
`after the write, found ${after.registrationKey ? `\`${after.registrationKey}\`` : "no recognized registration"}`);
|
|
472
762
|
}
|
|
473
763
|
}
|
|
764
|
+
// Codex is verified independently — and only when its write did not already fail.
|
|
765
|
+
let codexVerified = null;
|
|
766
|
+
if (codexFailures.length === 0) {
|
|
767
|
+
const codexCheck = await verifyCodexConfig(deps, env, codexSnapshot, target.version, codexExpected);
|
|
768
|
+
codexVerified = codexCheck.inspection;
|
|
769
|
+
codexFailures.push(...codexCheck.failures);
|
|
770
|
+
}
|
|
771
|
+
verificationFailures.push(...codexFailures);
|
|
772
|
+
// A Codex transition is reported only once it verified.
|
|
773
|
+
const codexAction = codexFailures.length === 0 ? globalResultAction(codex, target.version) : null;
|
|
474
774
|
// ---- Phase 7: report what actually happened ----
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
.
|
|
775
|
+
const codexLine = codexAction ? formatResultLine(codexAction) : null;
|
|
776
|
+
const resultLines = [
|
|
777
|
+
...initResult.configActions.map(formatResultLine),
|
|
778
|
+
codexLine,
|
|
779
|
+
].filter((l) => l !== null);
|
|
478
780
|
if (resultLines.length > 0) {
|
|
479
781
|
stdout("Launcher pins:");
|
|
480
782
|
for (const line of resultLines)
|
|
@@ -495,8 +797,29 @@ export async function runUpgradeCli(argv, injected) {
|
|
|
495
797
|
stderr("\nBridge API upgrade finished with an UNCONFIGURED MCP entry (see above).");
|
|
496
798
|
return 1;
|
|
497
799
|
}
|
|
498
|
-
|
|
499
|
-
|
|
800
|
+
// ---- Phase 8 (BAPI-1398): the project's /drive-epic-run permission rules ----
|
|
801
|
+
// Only after every gate above passed. Advisory: a malformed settings file or a
|
|
802
|
+
// refused launcher is reported and never changes the exit code.
|
|
803
|
+
// BAPI-1446: the same step carries the pinned SessionStart upgrade-notice hook,
|
|
804
|
+
// requested only for a Claude Code installation and pinned to the CONVERGED
|
|
805
|
+
// target version (never `VERSION`, `@latest`, or the fake-latest override).
|
|
806
|
+
if (deps.applyBridgePermissions) {
|
|
807
|
+
try {
|
|
808
|
+
const sessionStartHook = (await isClaudeCodeInstallation(deps))
|
|
809
|
+
? { pinnedVersion: target.version }
|
|
810
|
+
: undefined;
|
|
811
|
+
reportUpgradePermissions(await deps.applyBridgePermissions(deps.cwd, deps.env ?? {}, sessionStartHook), stdout, stderr);
|
|
812
|
+
}
|
|
813
|
+
catch {
|
|
814
|
+
stderr("Warning: could not write Bridge's permission rules (non-fatal).");
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
const applicableStandard = [
|
|
818
|
+
...verified.filter(isApplicableStandardLauncher),
|
|
819
|
+
...(codexVerified && isApplicableStandardLauncher(codexVerified) ? [codexVerified] : []),
|
|
820
|
+
];
|
|
821
|
+
const everyAction = [...initResult.configActions, ...(codexAction ? [codexAction] : [])];
|
|
822
|
+
if (isEveryLauncherAlreadyCurrent(everyAction)) {
|
|
500
823
|
// The exact string `/upgrade-bridge` is contracted to relay verbatim.
|
|
501
824
|
stdout("\nAlready up-to-date.");
|
|
502
825
|
}
|
|
@@ -507,13 +830,71 @@ export async function runUpgradeCli(argv, injected) {
|
|
|
507
830
|
}
|
|
508
831
|
else {
|
|
509
832
|
stdout(`\nUpgrade complete: launcher pins are at ${target.version}.`);
|
|
510
|
-
//
|
|
511
|
-
//
|
|
512
|
-
//
|
|
513
|
-
|
|
833
|
+
// Running sessions still hold the OLD MCP connection — MCP config is only read
|
|
834
|
+
// at session startup, so a launcher repin does not hot-reload it. Name the
|
|
835
|
+
// concrete action for each host whose configuration actually changed, from the
|
|
836
|
+
// registry's own hints and in its order.
|
|
837
|
+
const changedHosts = new Set();
|
|
838
|
+
for (const action of initResult.configActions) {
|
|
839
|
+
if (!CHANGED_ACTION_KINDS.has(action.kind))
|
|
840
|
+
continue;
|
|
841
|
+
const id = hostIdForRelPath(action.relPath);
|
|
842
|
+
if (id)
|
|
843
|
+
changedHosts.add(id);
|
|
844
|
+
}
|
|
845
|
+
if (codexAction && CHANGED_ACTION_KINDS.has(codexAction.kind))
|
|
846
|
+
changedHosts.add(codex.hostId);
|
|
847
|
+
for (const line of reloadGuidanceLines(changedHosts))
|
|
848
|
+
stdout(line);
|
|
514
849
|
}
|
|
515
850
|
return 0;
|
|
516
851
|
}
|
|
852
|
+
/** Planned actions that change a configuration and so warrant reload guidance. */
|
|
853
|
+
const PLANNED_CHANGE_ACTIONS = new Set([
|
|
854
|
+
"repin",
|
|
855
|
+
"normalize-unpinned",
|
|
856
|
+
"add",
|
|
857
|
+
"create",
|
|
858
|
+
]);
|
|
859
|
+
/** Real-run action kinds that changed a configuration. */
|
|
860
|
+
const CHANGED_ACTION_KINDS = new Set([
|
|
861
|
+
"repinned",
|
|
862
|
+
"normalized",
|
|
863
|
+
"added",
|
|
864
|
+
"created",
|
|
865
|
+
]);
|
|
866
|
+
/** Render the project permission result in upgrade's own report style. */
|
|
867
|
+
export function reportUpgradePermissions(result, stdout, stderr) {
|
|
868
|
+
if (result.hook) {
|
|
869
|
+
// Reported on its own, so a refused launcher or a malformed hooks structure
|
|
870
|
+
// never hides, or is hidden by, the permission-rule result.
|
|
871
|
+
const described = describeSessionStartHookOutcome(result.hook);
|
|
872
|
+
for (const line of described.lines)
|
|
873
|
+
stdout(line);
|
|
874
|
+
for (const warning of described.warnings)
|
|
875
|
+
stderr(`Warning: ${warning}.`);
|
|
876
|
+
}
|
|
877
|
+
if (!result.launcher.ok) {
|
|
878
|
+
stderr(`Warning: Bridge permission rules were not written: ${result.launcher.reason}.`);
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
const scope = result.project;
|
|
882
|
+
if (scope.status === "written" || scope.status === "unchanged") {
|
|
883
|
+
stdout(`Bridge permission rules (${scope.path}, launcher ${result.launcher.prefix}):`);
|
|
884
|
+
if (scope.inserted.length === 0 && scope.removedStale.length === 0) {
|
|
885
|
+
stdout(` all ${scope.alreadyPresent.length} rules already present.`);
|
|
886
|
+
}
|
|
887
|
+
for (const rule of scope.inserted)
|
|
888
|
+
stdout(` wrote ${rule}`);
|
|
889
|
+
for (const rule of scope.removedStale)
|
|
890
|
+
stdout(` removed stale ${rule}`);
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
if (scope.status === "malformed" || scope.status === "io-error") {
|
|
894
|
+
stderr(`Warning: ${scope.path} ${scope.status === "malformed" ? "is not valid JSON settings and was left unchanged" : "could not be written"}; ` +
|
|
895
|
+
`${scope.skipped.length} Bridge permission rule(s) were not written.`);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
517
898
|
/**
|
|
518
899
|
* Project pre-mutation inspections onto the action shape `isEveryLauncherAlreadyCurrent`
|
|
519
900
|
* consumes, so the dry run answers the currentness question exactly the way the
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// AUTO-GENERATED — do not edit manually. Regenerate with: npm run build
|
|
2
|
-
export const VERSION = "0.2.
|
|
3
|
-
export const BUILD_COMMIT = "
|
|
4
|
-
export const LAUNCHER_ARGS = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.
|
|
2
|
+
export const VERSION = "0.2.69";
|
|
3
|
+
export const BUILD_COMMIT = "5279e5545a81";
|
|
4
|
+
export const LAUNCHER_ARGS = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.69", "serve"];
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{readFileSync}from"node:fs";import path from"node:path";var WORKER_GUARD_REASON_PREFIX="bridge worker guard",WORKER_GUARD_MALFORMED_LINE=`${WORKER_GUARD_REASON_PREFIX}: payload could not be read; continuing fail-open`;function renderWorkerGuardReason(category,family){
|
|
3
|
-
`){endCommand();continue}if(ch===" "||ch===" "||ch==="\r"){endToken();continue}current+=ch,hasCurrent=!0}return endCommand(),commands}var WORKER_GUARD_MAX_PAYLOAD_BYTES=1e6;function asRecord(value){return value!==null&&typeof value=="object"&&!Array.isArray(value)?value:void 0}function isInside(root,candidate){let rel=path.relative(root,candidate);return rel===""?!0:rel.startsWith("..")?!1:!path.isAbsolute(rel)}function resolveEffectiveCwd(ctx,payloadCwd){let root=ctx.worktreePath;if(typeof root!="string"||!path.isAbsolute(root))return;let normalizedRoot=path.resolve(root);if(payloadCwd===void 0)return normalizedRoot;if(typeof payloadCwd!="string"||payloadCwd.length===0)return;let resolved=path.isAbsolute(payloadCwd)?path.resolve(payloadCwd):path.resolve(normalizedRoot,payloadCwd);return isInside(normalizedRoot,resolved)?resolved:void 0}function allTargetsContained(ctx,cwd,targets){let root=ctx.worktreePath;if(typeof root!="string"||!path.isAbsolute(root)||cwd===void 0)return!1;let normalizedRoot=path.resolve(root);return targets.every(target=>{if(target.length===0||target==="~"||target.startsWith("~/"))return!1;let resolved=path.isAbsolute(target)?path.resolve(target):path.resolve(cwd,target);return isInside(normalizedRoot,resolved)})}function parseGitInvocation(argv,baseCwd){let cwd=baseCwd,unsafe=!1,i=1;for(;i<argv.length;){let token=argv[i];if(token==="-C"){let dir=argv[i+1];dir===void 0||cwd===void 0?(unsafe=!0,cwd=void 0):cwd=path.isAbsolute(dir)?path.resolve(dir):path.resolve(cwd,dir),i+=2;continue}if(token==="-c"||token==="--namespace"||token==="--git-dir"||token==="--work-tree"){unsafe=!0,i+=2;continue}if(token.startsWith("-")){i+=1;continue}break}return{subcommand:argv[i]??"",args:argv.slice(i+1),cwd,unsafe}}function isOwnBranchDestination(destination,workerBranch){let colon=destination.indexOf(":"),dst=colon===-1?destination:destination.slice(colon+1);if(dst.length===0)return!1;let bare=dst.startsWith("+")?dst.slice(1):dst;return bare===workerBranch||bare===`refs/heads/${workerBranch}`}function evaluateGitPush(args,ctx){let deny={kind:"deny",category:"git-push",family:"git"},positional=[];for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="--"){positional.push(...args.slice(i+1));break}if(token.startsWith("-")){if(token==="--tags"||token==="--delete"||token==="-d"||token==="--mirror"||token==="--all"||token==="--follow-tags"||token.startsWith("--repo"))return deny;(token==="-o"||token==="--push-option"||token==="--receive-pack"||token==="--exec")&&(i+=1);continue}positional.push(token)}let workerBranch=ctx.workerBranch;return typeof workerBranch!="string"||workerBranch.length===0?deny:positional.length<=1?{kind:"allow"}:positional.slice(1).every(d=>isOwnBranchDestination(d,workerBranch))?{kind:"allow"}:deny}function evaluateGitReset(args,ctx){let deny={kind:"deny",category:"git-reset-hard",family:"git"};if(!args.includes("--hard"))return{kind:"allow"};let positional=[];for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="--"){positional.push(...args.slice(i+1));break}token.startsWith("-")||positional.push(token)}if(positional.length===0)return{kind:"allow"};let workerBranch=ctx.workerBranch;if(typeof workerBranch!="string"||workerBranch.length===0)return deny;let allowed=new Set([workerBranch,`origin/${workerBranch}`,`refs/heads/${workerBranch}`,`refs/remotes/origin/${workerBranch}`]);return positional.every(ref=>allowed.has(ref))?{kind:"allow"}:deny}function evaluateGitClean(args,ctx,cwd){let deny={kind:"deny",category:"filesystem-clean",family:"git"};if(cwd===void 0||!allTargetsContained(ctx,cwd,[cwd]))return deny;let pathspecs=[];for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="--"){pathspecs.push(...args.slice(i+1));break}if(token==="-e"||token==="--exclude"){i+=1;continue}token.startsWith("-")||pathspecs.push(token)}return pathspecs.length===0?{kind:"allow"}:allTargetsContained(ctx,cwd,pathspecs)?{kind:"allow"}:deny}function isRecursiveForcedRemove(args){let recursive=!1,force=!1;for(let token of args){if(token==="--")break;if(token==="--recursive")recursive=!0;else if(token==="--force")force=!0;else{if(token.startsWith("--"))continue;if(token.startsWith("-")&&token.length>1)for(let flag of token.slice(1))(flag==="r"||flag==="R")&&(recursive=!0),flag==="f"&&(force=!0)}}return recursive&&force}function evaluateRemove(argv,ctx,cwd){let deny={kind:"deny",category:"filesystem-remove",family:"rm"},args=argv.slice(1);if(!isRecursiveForcedRemove(args))return{kind:"allow"};let targets=[],sawDoubleDash=!1;for(let token of args){if(!sawDoubleDash&&token==="--"){sawDoubleDash=!0;continue}!sawDoubleDash&&token.startsWith("-")&&token.length>1||targets.push(token)}return targets.length===0?deny:allTargetsContained(ctx,cwd,targets)?{kind:"allow"}:deny}function isDatabaseCommand(executable){let base=path.basename(executable);return base==="psql"||base.startsWith("pg_")}var DESTRUCTIVE_SQL=/\b(?:DROP|TRUNCATE)\b/i;function evaluateDatabase(argv){let deny={kind:"deny",category:"database-destructive",family:"psql"};return argv.slice(1).some(arg=>DESTRUCTIVE_SQL.test(arg))?deny:{kind:"allow"}}var MUTATING_METHODS=new Set(["POST","PUT","PATCH","DELETE"]);function evaluateNetwork(argv){let deny={kind:"deny",category:"network-mutating",family:"curl"},base=path.basename(argv[0]??""),args=argv.slice(1);for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="-X"||token==="--request"||token==="--method"){let value=args[i+1];if(value===void 0||MUTATING_METHODS.has(value.toUpperCase()))return deny;i+=1;continue}if(token.startsWith("-X")&&token.length>2){if(MUTATING_METHODS.has(token.slice(2).toUpperCase()))return deny;continue}if(token.startsWith("--request=")||token.startsWith("--method=")){let value=token.slice(token.indexOf("=")+1);if(MUTATING_METHODS.has(value.toUpperCase()))return deny;continue}if(token==="--data"||token.startsWith("--data=")||token.startsWith("--data-")||token==="--form"||token==="-F"||token==="--upload-file"||token==="-T"||token==="--post-data"||token.startsWith("--post-data=")||token==="--post-file"||token.startsWith("--post-file=")||token==="--body-data"||token.startsWith("--body-data=")||token==="--body-file"||token.startsWith("--body-file=")||base==="curl"&&token==="-d")return deny}return{kind:"allow"}}function classify(executable){let base=path.basename(executable);return base==="git"?"git":base==="rm"?"rm":isDatabaseCommand(base)?"database":base==="curl"||base==="wget"?"network":null}function unsafeGitCategory(argv){let{subcommand}=parseGitInvocation(argv,void 0);return subcommand==="push"?"git-push":subcommand==="reset"?"git-reset-hard":subcommand==="clean"?"filesystem-clean":"git-ambiguous"}function unsafeSegmentDecision(family,argv){switch(family){case"git":return{kind:"deny",category:unsafeGitCategory(argv),family:"git"};case"rm":return{kind:"deny",category:"filesystem-remove",family:"rm"};case"database":return{kind:"deny",category:"database-destructive",family:"psql"};case"network":return{kind:"deny",category:"network-mutating",family:"curl"}}}var TRANSPARENT_PREFIXES=new Set(["sudo","env","command","nohup","time","nice","exec"]),STDIN_ARGUMENT_PREFIXES=new Set(["xargs"]),SHELL_EXECUTABLES=new Set(["sh","bash","zsh","dash","ksh"]),EVAL_BUILTINS=new Set(["eval"]);function unwrapTransparentPrefixes(argv){let rest=argv;for(let depth=0;depth<8;depth+=1){let head=rest[0];if(head===void 0||!TRANSPARENT_PREFIXES.has(path.basename(head)))return rest;let i=1;for(;i<rest.length;){let token=rest[i];if(path.basename(head)==="env"&&/^[A-Za-z_][A-Za-z0-9_]*=/.test(token)){i+=1;continue}if(token.startsWith("-")){i+=1;continue}break}rest=rest.slice(i)}return rest}function stripStdinPrefixOptions(argv){let VALUE_OPTS=new Set(["-I","-i","-L","-n","-P","-s","-d","-E","-e","-a","--replace","--max-lines","--max-args","--max-procs","--max-chars","--delimiter","--eof","--arg-file"]),i=1;for(;i<argv.length;){let token=argv[i];if(token==="--"){i+=1;break}if(!token.startsWith("-"))break;if(token.includes("=")){i+=1;continue}i+=VALUE_OPTS.has(token)?2:1}return argv.slice(i)}var WORKFLOW_NAMES="implement-ticket|plan-ticket|idea-to-pr|review-and-implement",WORKFLOW_SLASH_LAUNCH=new RegExp(`(?:^|\\s)/(?:bridge-)?(?:${WORKFLOW_NAMES})(?=\\s|$)`,"i"),WORKFLOW_NAME_EXACT=new RegExp(`^/?(?:[A-Za-z0-9_-]+:)?(?:bridge-)?(?:${WORKFLOW_NAMES})$`,"i"),CLAUDE_PACKAGE_TOKEN=/^(?:@anthropic-ai\/)?claude(?:-code)?(?:@\S+)?$/;function claudeLaunchArgs(argv){let base=path.basename(argv[0]??"");if(base==="claude")return argv.slice(1);if(base==="npx"){let index=argv.findIndex((token,i)=>i>0&&CLAUDE_PACKAGE_TOKEN.test(token));return index===-1?void 0:argv.slice(index+1)}}function isClaudeWorkflowPrintLaunch(segment){let args=claudeLaunchArgs(segment.argv);return args===void 0||!args.some(token=>token==="-p"||token==="--print"||token.startsWith("--print="))?!1:segment.unsafe?!0:args.some(token=>{let value=token.startsWith("--print=")?token.slice(8):token;return WORKFLOW_SLASH_LAUNCH.test(value)})}function evaluateSimpleCommand(segment,ctx,payloadCwd,depth=0,stdinFed=!1){let argv=unwrapTransparentPrefixes(segment.argv),executable=argv[0];if(executable===void 0)return{kind:"allow"};if(SHELL_EXECUTABLES.has(path.basename(executable))&&depth<4){let cIndex=argv.findIndex(t=>t==="-c"),inner=cIndex===-1?void 0:argv[cIndex+1];if(inner!==void 0){for(let nested of tokenizeShellCommand(inner)){let decision=evaluateSimpleCommand(nested,ctx,payloadCwd,depth+1,stdinFed);if(decision.kind==="deny")return decision}return{kind:"allow"}}}if(EVAL_BUILTINS.has(path.basename(executable))&&depth<4){for(let nested of tokenizeShellCommand(argv.slice(1).join(" "))){let decision=evaluateSimpleCommand(nested,ctx,payloadCwd,depth+1,stdinFed);if(decision.kind==="deny")return decision}return{kind:"allow"}}if(STDIN_ARGUMENT_PREFIXES.has(path.basename(executable))&&depth<4){let rest=stripStdinPrefixOptions(argv);return rest.length===0?{kind:"allow"}:evaluateSimpleCommand({argv:rest,unsafe:segment.unsafe},ctx,payloadCwd,depth+1,!0)}if(segment={argv,unsafe:segment.unsafe},ctx.correctionEnforced&&isClaudeWorkflowPrintLaunch(segment))return{kind:"deny",category:"workflow-launch-shell",family:"workflow"};let family=classify(executable);if(family===null)return{kind:"allow"};if(stdinFed||segment.unsafe)return unsafeSegmentDecision(family,segment.argv);let cwd=resolveEffectiveCwd(ctx,payloadCwd);if(family==="rm")return evaluateRemove(segment.argv,ctx,cwd);if(family==="database")return evaluateDatabase(segment.argv);if(family==="network")return evaluateNetwork(segment.argv);let git=parseGitInvocation(segment.argv,cwd);return git.subcommand==="push"?git.unsafe?{kind:"deny",category:"git-push",family:"git"}:evaluateGitPush(git.args,ctx):git.subcommand==="reset"?git.unsafe?{kind:"deny",category:"git-reset-hard",family:"git"}:evaluateGitReset(git.args,ctx):git.subcommand==="clean"?git.unsafe?{kind:"deny",category:"filesystem-clean",family:"git"}:evaluateGitClean(git.args,ctx,git.cwd):{kind:"allow"}}function workflowDeny(category){return{kind:"deny",category,family:"workflow"}}var FILE_TOOLS=new Set(["Read","Edit","Write","MultiEdit","NotebookEdit","Grep","Glob"]),INVOCATION_FIELDS={Skill:["skill","args"],Task:["prompt","description"],Agent:["prompt","description"],SlashCommand:["command"]},PLANNER_TOOL_NAME=/(?:request_plan_generation|get_pipeline_recipe|plan_ticket|implement_ticket)/i;function evaluateCorrectionToolRole(toolName,input){if(FILE_TOOLS.has(toolName))return{kind:"allow"};if(toolName==="Bash")return;let fields=INVOCATION_FIELDS[toolName];if(fields!==void 0){for(let field of fields){let value=input[field];if(value===void 0)continue;if(typeof value!="string")return workflowDeny("workflow-launch-skill");let nameField=toolName==="Skill"?field==="skill":toolName==="SlashCommand";if(WORKFLOW_SLASH_LAUNCH.test(value)||nameField&&WORKFLOW_NAME_EXACT.test(value.trim().split(/\s+/)[0]??""))return workflowDeny("workflow-launch-skill")}return{kind:"allow"}}if(/^mcp__/i.test(toolName)&&PLANNER_TOOL_NAME.test(toolName))return workflowDeny("workflow-planner-tool");let args=Object.values(input).filter(v=>typeof v=="string").join(" ");return WORKFLOW_SLASH_LAUNCH.test(args)?workflowDeny("workflow-entry"):void 0}function evaluateWorkerGuardPayload(payload,ctx){if(ctx.correctionContextInvalid)return workflowDeny("correction-context-invalid");let root=asRecord(payload);if(root===void 0)return ctx.correctionEnforced?workflowDeny("correction-payload-invalid"):{kind:"malformed"};let toolName=root.tool_name??root.toolName;if(typeof toolName!="string")return ctx.correctionEnforced?workflowDeny("correction-payload-invalid"):{kind:"malformed"};let input=asRecord(root.tool_input??root.toolInput);if(ctx.correctionEnforced){if(input===void 0)return workflowDeny("correction-payload-invalid");let decision=evaluateCorrectionToolRole(toolName,input);if(decision!==void 0)return decision}if(toolName!=="Bash")return{kind:"allow"};if(input===void 0)return{kind:"malformed"};let command=input.command;if(typeof command!="string")return ctx.correctionEnforced?workflowDeny("correction-payload-invalid"):{kind:"malformed"};let rawCwd=root.cwd??input.cwd,payloadCwd=typeof rawCwd=="string"?rawCwd:void 0;for(let segment of tokenizeShellCommand(command)){let decision=evaluateSimpleCommand(segment,ctx,payloadCwd);if(decision.kind==="deny")return decision}return{kind:"allow"}}function evaluateWorkerGuardInput(raw,ctx){let unreadable=()=>ctx.correctionContextInvalid?workflowDeny("correction-context-invalid"):ctx.correctionEnforced?workflowDeny("correction-payload-invalid"):{kind:"malformed"};if(typeof raw!="string"||raw.length>WORKER_GUARD_MAX_PAYLOAD_BYTES||raw.trim().length===0)return unreadable();let parsed;try{parsed=JSON.parse(raw)}catch{return unreadable()}try{return evaluateWorkerGuardPayload(parsed,ctx)}catch{return unreadable()}}var WORKER_GUARD_ROOT_FLAG="--worktree-root";function resolveWorkerGuardContext(env,cwd,argv=[]){let ctx={},root;for(let i=0;i<argv.length;i+=1){if(argv[i]===WORKER_GUARD_ROOT_FLAG){root=argv[i+1];break}if(argv[i].startsWith(`${WORKER_GUARD_ROOT_FLAG}=`)){root=argv[i].slice(WORKER_GUARD_ROOT_FLAG.length+1);break}}root===void 0&&typeof cwd=="string"&&(root=cwd),typeof root=="string"&&path.isAbsolute(root)&&(ctx.worktreePath=path.resolve(root));let branch=env.BAPI_WORKER_BRANCH;typeof branch=="string"&&branch.trim().length>0&&(ctx.workerBranch=branch.trim());let flagIndex=argv.indexOf("--correction-intent-version"),flagValue=flagIndex>=0?argv[flagIndex+1]:void 0,envValue=env.BAPI_CORRECTION_INTENT_VERSION;return ctx.correctionEnforced=flagValue==="1"||envValue==="1",ctx.correctionContextInvalid=flagIndex>=0&&flagValue!=="1"||envValue!==void 0&&envValue!=="1"||flagIndex>=0&&envValue!==void 0&&envValue!==flagValue,ctx}function runWorkerGuardHook(io){let exitCode=0,correctionRequested=io.argv.includes("--correction-intent-version")||io.env.BAPI_CORRECTION_INTENT_VERSION!==void 0;try{let raw=io.readStdin(),ctx=resolveWorkerGuardContext(io.env,io.cwd,io.argv),decision=evaluateWorkerGuardInput(raw,ctx);decision.kind==="deny"?(io.writeStderr(renderWorkerGuardReason(decision.category,decision.family)),exitCode=2):decision.kind==="malformed"&&(io.writeStderr(WORKER_GUARD_MALFORMED_LINE),exitCode=0)}catch{try{io.writeStderr(correctionRequested?renderWorkerGuardReason("correction-payload-invalid","workflow"):WORKER_GUARD_MALFORMED_LINE)}catch{}exitCode=correctionRequested?2:0}return exitCode}function readStdinSync(){return readFileSync(0,"utf-8")}process.exit(runWorkerGuardHook({readStdin:readStdinSync,env:process.env,cwd:process.cwd(),argv:process.argv.slice(2),writeStderr:text=>{process.stderr.write(`${text}
|
|
2
|
+
import{readFileSync}from"node:fs";import path from"node:path";var WORKER_GUARD_REASON_PREFIX="bridge worker guard",WORKER_GUARD_MALFORMED_LINE=`${WORKER_GUARD_REASON_PREFIX}: payload could not be read; continuing fail-open`;function renderWorkerGuardReason(category,family,form){if(category==="git-ambiguous")return`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). This shell form cannot be safely resolved: substitution, redirection, or stdin-fed arguments hide what git will run. Use literal arguments instead; for a commit message, write it to a file and run git commit -F <file>.`;if(form==="shell_form"){let pattern=category==="git-push"?"If the destination is your own branch and no force is used, run git push origin HEAD:<your work branch> alone.":category==="git-reset-hard"?"If resetting your own branch, run git reset --hard origin/<your work branch> alone.":category==="filesystem-clean"?"If all pathspecs are inside your worktree, run git clean with literal worktree-contained pathspecs alone.":category==="filesystem-remove"?"If all paths are inside your worktree, run rm with literal worktree-contained paths alone.":"If arguments are read-only, run literal read-only arguments alone, without redirects or $VAR.";return`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). Shell form cannot be checked: redirects, pipes, substitution, or stdin-fed arguments. `+pattern}let restriction={"git-push":"Push only to your own branch, without force. Use HEAD:<your work branch> as the destination.","git-reset-hard":"Hard reset only against your own branch ref.","filesystem-clean":"Clean only literal paths inside your worktree.","filesystem-remove":"Remove only literal paths inside your worktree.","database-destructive":"Destructive SQL is prohibited. Use read-only database arguments.","network-mutating":"Mutating requests are prohibited. Use read-only network arguments."};return restriction[category]?`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). ${restriction[category]}`:category==="workflow-entry"||category==="workflow-launch-skill"||category==="workflow-launch-shell"||category==="workflow-planner-tool"?`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). A correction worker must not start a full workflow. Reading or editing files that mention a workflow is allowed; make the focused correction directly.`:category==="correction-context-invalid"?`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). The correction context for this worker is not valid, so no tool call can be checked. Stop and report the launch problem.`:category==="correction-payload-invalid"?`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). The tool call could not be read as a tool name with a tool input, so it cannot be checked. Retry with a well-formed call.`:`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). This command family is restricted to the worker's own branch, worktree, and read-only network access. Use a different approach and continue.`}var UNSAFE_UNQUOTED=new Set(["$","`",">","<"]),SEPARATORS=new Set([";","|","&"]);function tokenizeShellCommand(command){let commands=[],argv=[],current="",hasCurrent=!1,unsafe=!1,endToken=()=>{hasCurrent&&(argv.push(current),current="",hasCurrent=!1)},endCommand=()=>{endToken(),(argv.length>0||unsafe)&&commands.push({argv,unsafe}),argv=[],unsafe=!1};for(let i=0;i<command.length;i+=1){let ch=command[i];if(ch==="\\"){let next=command[i+1];next!==void 0&&(current+=next,hasCurrent=!0,i+=1);continue}if(ch==="'"){let close=command.indexOf("'",i+1);if(close===-1){unsafe=!0,current+=command.slice(i+1),hasCurrent=!0;break}current+=command.slice(i+1,close),hasCurrent=!0,i=close;continue}if(ch==='"'){let j=i+1,body="",closed=!1;for(;j<command.length;){let c=command[j];if(c==="\\"&&j+1<command.length){body+=command[j+1],j+=2;continue}if(c==='"'){closed=!0;break}(c==="$"||c==="`")&&(unsafe=!0),body+=c,j+=1}closed||(unsafe=!0),current+=body,hasCurrent=!0,i=j;continue}if(UNSAFE_UNQUOTED.has(ch)){unsafe=!0,current+=ch,hasCurrent=!0;continue}if(SEPARATORS.has(ch)){endCommand();continue}if(ch===`
|
|
3
|
+
`){endCommand();continue}if(ch===" "||ch===" "||ch==="\r"){endToken();continue}current+=ch,hasCurrent=!0}return endCommand(),commands}function diagnosticProjections(command){if(command.length>WORKER_GUARD_MAX_PAYLOAD_BYTES)return[];let result=[],start=0,masked="",quote="",uncertain=!1,finish=end=>{let original=tokenizeShellCommand(command.slice(start,end)),projected=tokenizeShellCommand(masked),value=!uncertain&&projected.length===1?{argv:projected[0].argv,complete:!projected[0].unsafe}:void 0;result.push(...original.map((_,i)=>i===0?value:void 0)),masked="",uncertain=!1,start=end+1};for(let i=0;i<command.length;i+=1){let ch=command[i];if(ch==="\\"&"e!=="'"){masked+=command.slice(i,i+2),i+=1;continue}if(quote){masked+=ch,ch===quote&&(quote="");continue}if(ch==="'"||ch==='"'){quote=ch,masked+=ch;continue}if(ch===">"||ch==="<"){masked=masked.replace(/(^|\s)\d+$/,"$1");let j=i+1;for(command[j]===ch&&(j+=1),command[j]==="&"&&(j+=1);/[ \t]/.test(command[j]??"")&&j<command.length;)j+=1;let operandStart=j,operandQuote="";for(;j<command.length;){let c=command[j];if(c==="\\"&&operandQuote!=="'"){j+=2;continue}if(operandQuote){c===operandQuote&&(operandQuote=""),j+=1;continue}if(c==="'"||c==='"'){operandQuote=c,j+=1;continue}if(/[\s;|&<>]/.test(c))break;j+=1}(j===operandStart||operandQuote||command.slice(i,j).startsWith("<<"))&&(uncertain=!0),masked+=" ",i=j-1;continue}if(SEPARATORS.has(ch)||ch===`
|
|
4
|
+
`){finish(i);continue}masked+=ch}return quote&&(uncertain=!0),finish(command.length),result}var WORKER_GUARD_MAX_PAYLOAD_BYTES=1e6;function asRecord(value){return value!==null&&typeof value=="object"&&!Array.isArray(value)?value:void 0}function isInside(root,candidate){let rel=path.relative(root,candidate);return rel===""?!0:rel.startsWith("..")?!1:!path.isAbsolute(rel)}function resolveEffectiveCwd(ctx,payloadCwd){let root=ctx.worktreePath;if(typeof root!="string"||!path.isAbsolute(root))return;let normalizedRoot=path.resolve(root);if(payloadCwd===void 0)return normalizedRoot;if(typeof payloadCwd!="string"||payloadCwd.length===0)return;let resolved=path.isAbsolute(payloadCwd)?path.resolve(payloadCwd):path.resolve(normalizedRoot,payloadCwd);return isInside(normalizedRoot,resolved)?resolved:void 0}function allTargetsContained(ctx,cwd,targets){let root=ctx.worktreePath;if(typeof root!="string"||!path.isAbsolute(root)||cwd===void 0)return!1;let normalizedRoot=path.resolve(root);return targets.every(target=>{if(target.length===0||target==="~"||target.startsWith("~/"))return!1;let resolved=path.isAbsolute(target)?path.resolve(target):path.resolve(cwd,target);return isInside(normalizedRoot,resolved)})}function parseGitInvocation(argv,baseCwd){let cwd=baseCwd,unsafe=!1,i=1;for(;i<argv.length;){let token=argv[i];if(token==="-C"){let dir=argv[i+1];dir===void 0||cwd===void 0?(unsafe=!0,cwd=void 0):cwd=path.isAbsolute(dir)?path.resolve(dir):path.resolve(cwd,dir),i+=2;continue}if(token==="-c"||token==="--namespace"||token==="--git-dir"||token==="--work-tree"){unsafe=!0,i+=2;continue}if(token.startsWith("-")){i+=1;continue}break}return{subcommand:argv[i]??"",args:argv.slice(i+1),cwd,unsafe}}function isOwnBranchDestination(destination,workerBranch){let colon=destination.indexOf(":"),dst=colon===-1?destination:destination.slice(colon+1);if(dst.length===0)return!1;let bare=dst.startsWith("+")?dst.slice(1):dst;return bare===workerBranch||bare===`refs/heads/${workerBranch}`}function evaluateGitPush(args,ctx){let deny={kind:"deny",category:"git-push",family:"git"},positional=[];for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="--"){positional.push(...args.slice(i+1));break}if(token.startsWith("-")){if(token==="--tags"||token==="--delete"||token==="-d"||token==="--mirror"||token==="--all"||token==="--follow-tags"||token.startsWith("--repo"))return deny;(token==="-o"||token==="--push-option"||token==="--receive-pack"||token==="--exec")&&(i+=1);continue}positional.push(token)}let workerBranch=ctx.workerBranch;return typeof workerBranch!="string"||workerBranch.length===0?deny:positional.length<=1?{kind:"allow"}:positional.slice(1).every(d=>isOwnBranchDestination(d,workerBranch))?{kind:"allow"}:deny}function evaluateGitReset(args,ctx){let deny={kind:"deny",category:"git-reset-hard",family:"git"};if(!args.includes("--hard"))return{kind:"allow"};let positional=[];for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="--"){positional.push(...args.slice(i+1));break}token.startsWith("-")||positional.push(token)}if(positional.length===0)return{kind:"allow"};let workerBranch=ctx.workerBranch;if(typeof workerBranch!="string"||workerBranch.length===0)return deny;let allowed=new Set([workerBranch,`origin/${workerBranch}`,`refs/heads/${workerBranch}`,`refs/remotes/origin/${workerBranch}`]);return positional.every(ref=>allowed.has(ref))?{kind:"allow"}:deny}function evaluateGitClean(args,ctx,cwd){let deny={kind:"deny",category:"filesystem-clean",family:"git"};if(cwd===void 0||!allTargetsContained(ctx,cwd,[cwd]))return deny;let pathspecs=[];for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="--"){pathspecs.push(...args.slice(i+1));break}if(token==="-e"||token==="--exclude"){i+=1;continue}token.startsWith("-")||pathspecs.push(token)}return pathspecs.length===0?{kind:"allow"}:allTargetsContained(ctx,cwd,pathspecs)?{kind:"allow"}:deny}function isRecursiveForcedRemove(args){let recursive=!1,force=!1;for(let token of args){if(token==="--")break;if(token==="--recursive")recursive=!0;else if(token==="--force")force=!0;else{if(token.startsWith("--"))continue;if(token.startsWith("-")&&token.length>1)for(let flag of token.slice(1))(flag==="r"||flag==="R")&&(recursive=!0),flag==="f"&&(force=!0)}}return recursive&&force}function evaluateRemove(argv,ctx,cwd){let deny={kind:"deny",category:"filesystem-remove",family:"rm"},args=argv.slice(1);if(!isRecursiveForcedRemove(args))return{kind:"allow"};let targets=[],sawDoubleDash=!1;for(let token of args){if(!sawDoubleDash&&token==="--"){sawDoubleDash=!0;continue}!sawDoubleDash&&token.startsWith("-")&&token.length>1||targets.push(token)}return targets.length===0?deny:allTargetsContained(ctx,cwd,targets)?{kind:"allow"}:deny}function isDatabaseCommand(executable){let base=path.basename(executable);return base==="psql"||base.startsWith("pg_")}var DESTRUCTIVE_SQL=/\b(?:DROP|TRUNCATE)\b/i;function evaluateDatabase(argv){let deny={kind:"deny",category:"database-destructive",family:"psql"};return argv.slice(1).some(arg=>DESTRUCTIVE_SQL.test(arg))?deny:{kind:"allow"}}var MUTATING_METHODS=new Set(["POST","PUT","PATCH","DELETE"]);function evaluateNetwork(argv){let deny={kind:"deny",category:"network-mutating",family:"curl"},base=path.basename(argv[0]??""),args=argv.slice(1);for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="-X"||token==="--request"||token==="--method"){let value=args[i+1];if(value===void 0||MUTATING_METHODS.has(value.toUpperCase()))return deny;i+=1;continue}if(token.startsWith("-X")&&token.length>2){if(MUTATING_METHODS.has(token.slice(2).toUpperCase()))return deny;continue}if(token.startsWith("--request=")||token.startsWith("--method=")){let value=token.slice(token.indexOf("=")+1);if(MUTATING_METHODS.has(value.toUpperCase()))return deny;continue}if(token==="--data"||token.startsWith("--data=")||token.startsWith("--data-")||token==="--form"||token==="-F"||token==="--upload-file"||token==="-T"||token==="--post-data"||token.startsWith("--post-data=")||token==="--post-file"||token.startsWith("--post-file=")||token==="--body-data"||token.startsWith("--body-data=")||token==="--body-file"||token.startsWith("--body-file=")||base==="curl"&&token==="-d")return deny}return{kind:"allow"}}function classify(executable){let base=path.basename(executable);return base==="git"?"git":base==="rm"?"rm":isDatabaseCommand(base)?"database":base==="curl"||base==="wget"?"network":null}function unsafeGitCategory(argv){let{subcommand}=parseGitInvocation(argv,void 0);return subcommand==="push"?"git-push":subcommand==="reset"?"git-reset-hard":subcommand==="clean"?"filesystem-clean":"git-ambiguous"}function diagnosticPushForce(args){return args.some(t=>t==="--force"||t==="-f"||t==="--force-with-lease"||t.startsWith("--force-with-lease=")||t.startsWith("+"))}function diagnosticViolation(family,diagnostic,ctx,cwd){let full=unwrapTransparentPrefixes(diagnostic.argv),unknown=diagnostic.complete?-1:full.findIndex(t=>/[$`]/.test(t)),argv=unknown<0?full:full.slice(0,unknown);if(family==="database")return evaluateDatabase(argv).kind==="deny";if(family==="network")return evaluateNetwork(argv).kind==="deny";let git=parseGitInvocation(argv,cwd);return family==="git"&&git.subcommand==="push"&&diagnosticPushForce(git.args)?!0:diagnostic.complete?family==="rm"?evaluateRemove(argv,ctx,cwd).kind==="deny":git.unsafe?!0:git.subcommand==="push"?evaluateGitPush(git.args,ctx).kind==="deny":git.subcommand==="reset"?evaluateGitReset(git.args,ctx).kind==="deny":git.subcommand==="clean"?evaluateGitClean(git.args,ctx,git.cwd).kind==="deny":!1:family==="rm"?argv.slice(1).some(t=>!t.startsWith("-"))&&evaluateRemove(argv,ctx,cwd).kind==="deny":git.subcommand==="push"?git.args.filter(t=>!t.startsWith("-")).length>=2&&evaluateGitPush(git.args,ctx).kind==="deny":git.subcommand==="reset"?git.args.some(t=>!t.startsWith("-"))&&evaluateGitReset(git.args,ctx).kind==="deny":git.subcommand==="clean"?evaluateGitClean(git.args,ctx,git.cwd).kind==="deny":!1}function unsafeSegmentDecision(family,argv,ctx,cwd,diagnostic){let category=family==="git"?unsafeGitCategory(argv):family==="rm"?"filesystem-remove":family==="database"?"database-destructive":"network-mutating",decision={kind:"deny",category,family:family==="database"?"psql":family==="network"?"curl":family};return diagnostic&&category!=="git-ambiguous"&&!diagnosticViolation(family,diagnostic,ctx,cwd)?{...decision,form:"shell_form"}:decision}var TRANSPARENT_PREFIXES=new Set(["sudo","env","command","nohup","time","nice","exec"]),STDIN_ARGUMENT_PREFIXES=new Set(["xargs"]),SHELL_EXECUTABLES=new Set(["sh","bash","zsh","dash","ksh"]),EVAL_BUILTINS=new Set(["eval"]);function unwrapTransparentPrefixes(argv){let rest=argv;for(let depth=0;depth<8;depth+=1){let head=rest[0];if(head===void 0||!TRANSPARENT_PREFIXES.has(path.basename(head)))return rest;let i=1;for(;i<rest.length;){let token=rest[i];if(path.basename(head)==="env"&&/^[A-Za-z_][A-Za-z0-9_]*=/.test(token)){i+=1;continue}if(token.startsWith("-")){i+=1;continue}break}rest=rest.slice(i)}return rest}function stripStdinPrefixOptions(argv){let VALUE_OPTS=new Set(["-I","-i","-L","-n","-P","-s","-d","-E","-e","-a","--replace","--max-lines","--max-args","--max-procs","--max-chars","--delimiter","--eof","--arg-file"]),i=1;for(;i<argv.length;){let token=argv[i];if(token==="--"){i+=1;break}if(!token.startsWith("-"))break;if(token.includes("=")){i+=1;continue}i+=VALUE_OPTS.has(token)?2:1}return argv.slice(i)}var WORKFLOW_NAMES="implement-ticket|plan-ticket|idea-to-pr|review-and-implement",WORKFLOW_SLASH_LAUNCH=new RegExp(`(?:^|\\s)/(?:bridge-)?(?:${WORKFLOW_NAMES})(?=\\s|$)`,"i"),WORKFLOW_NAME_EXACT=new RegExp(`^/?(?:[A-Za-z0-9_-]+:)?(?:bridge-)?(?:${WORKFLOW_NAMES})$`,"i"),CLAUDE_PACKAGE_TOKEN=/^(?:@anthropic-ai\/)?claude(?:-code)?(?:@\S+)?$/;function claudeLaunchArgs(argv){let base=path.basename(argv[0]??"");if(base==="claude")return argv.slice(1);if(base==="npx"){let index=argv.findIndex((token,i)=>i>0&&CLAUDE_PACKAGE_TOKEN.test(token));return index===-1?void 0:argv.slice(index+1)}}function isClaudeWorkflowPrintLaunch(segment){let args=claudeLaunchArgs(segment.argv);return args===void 0||!args.some(token=>token==="-p"||token==="--print"||token.startsWith("--print="))?!1:segment.unsafe?!0:args.some(token=>{let value=token.startsWith("--print=")?token.slice(8):token;return WORKFLOW_SLASH_LAUNCH.test(value)})}function evaluateSimpleCommand(segment,ctx,payloadCwd,depth=0,stdinFed=!1,diagnostic){let argv=unwrapTransparentPrefixes(segment.argv),executable=argv[0];if(executable===void 0)return{kind:"allow"};if(SHELL_EXECUTABLES.has(path.basename(executable))&&depth<4){let cIndex=argv.findIndex(t=>t==="-c"),inner=cIndex===-1?void 0:argv[cIndex+1];if(inner!==void 0){let projections=diagnostic?.complete?diagnosticProjections(inner):[];for(let[i,nested]of tokenizeShellCommand(inner).entries()){let decision=evaluateSimpleCommand(nested,ctx,payloadCwd,depth+1,stdinFed,projections[i]);if(decision.kind==="deny")return decision}return{kind:"allow"}}}if(EVAL_BUILTINS.has(path.basename(executable))&&depth<4){let inner=argv.slice(1).join(" "),projections=diagnostic?.complete?diagnosticProjections(inner):[];for(let[i,nested]of tokenizeShellCommand(inner).entries()){let decision=evaluateSimpleCommand(nested,ctx,payloadCwd,depth+1,stdinFed,projections[i]);if(decision.kind==="deny")return decision}return{kind:"allow"}}if(STDIN_ARGUMENT_PREFIXES.has(path.basename(executable))&&depth<4){let rest=stripStdinPrefixOptions(argv);return rest.length===0?{kind:"allow"}:evaluateSimpleCommand({argv:rest,unsafe:segment.unsafe},ctx,payloadCwd,depth+1,!0,diagnostic?{argv:stripStdinPrefixOptions(unwrapTransparentPrefixes(diagnostic.argv)),complete:!1}:void 0)}if(segment={argv,unsafe:segment.unsafe},ctx.correctionEnforced&&isClaudeWorkflowPrintLaunch(segment))return{kind:"deny",category:"workflow-launch-shell",family:"workflow"};let family=classify(executable);if(family===null)return{kind:"allow"};if(stdinFed||segment.unsafe)return unsafeSegmentDecision(family,segment.argv,ctx,resolveEffectiveCwd(ctx,payloadCwd),diagnostic);let cwd=resolveEffectiveCwd(ctx,payloadCwd);if(family==="rm")return evaluateRemove(segment.argv,ctx,cwd);if(family==="database")return evaluateDatabase(segment.argv);if(family==="network")return evaluateNetwork(segment.argv);let git=parseGitInvocation(segment.argv,cwd);return git.subcommand==="push"?git.unsafe?unsafeSegmentDecision("git",segment.argv,ctx,cwd,diagnostic):evaluateGitPush(git.args,ctx):git.subcommand==="reset"?git.unsafe?unsafeSegmentDecision("git",segment.argv,ctx,cwd,diagnostic):evaluateGitReset(git.args,ctx):git.subcommand==="clean"?git.unsafe?unsafeSegmentDecision("git",segment.argv,ctx,cwd,diagnostic):evaluateGitClean(git.args,ctx,git.cwd):{kind:"allow"}}function workflowDeny(category){return{kind:"deny",category,family:"workflow"}}var FILE_TOOLS=new Set(["Read","Edit","Write","MultiEdit","NotebookEdit","Grep","Glob"]),INVOCATION_FIELDS={Skill:["skill","args"],Task:["prompt","description"],Agent:["prompt","description"],SlashCommand:["command"]},PLANNER_TOOL_NAME=/(?:request_plan_generation|get_pipeline_recipe|plan_ticket|implement_ticket)/i;function evaluateCorrectionToolRole(toolName,input){if(FILE_TOOLS.has(toolName))return{kind:"allow"};if(toolName==="Bash")return;let fields=INVOCATION_FIELDS[toolName];if(fields!==void 0){for(let field of fields){let value=input[field];if(value===void 0)continue;if(typeof value!="string")return workflowDeny("workflow-launch-skill");let nameField=toolName==="Skill"?field==="skill":toolName==="SlashCommand";if(WORKFLOW_SLASH_LAUNCH.test(value)||nameField&&WORKFLOW_NAME_EXACT.test(value.trim().split(/\s+/)[0]??""))return workflowDeny("workflow-launch-skill")}return{kind:"allow"}}if(/^mcp__/i.test(toolName)&&PLANNER_TOOL_NAME.test(toolName))return workflowDeny("workflow-planner-tool");let args=Object.values(input).filter(v=>typeof v=="string").join(" ");return WORKFLOW_SLASH_LAUNCH.test(args)?workflowDeny("workflow-entry"):void 0}function evaluateWorkerGuardPayload(payload,ctx){if(ctx.correctionContextInvalid)return workflowDeny("correction-context-invalid");let root=asRecord(payload);if(root===void 0)return ctx.correctionEnforced?workflowDeny("correction-payload-invalid"):{kind:"malformed"};let toolName=root.tool_name??root.toolName;if(typeof toolName!="string")return ctx.correctionEnforced?workflowDeny("correction-payload-invalid"):{kind:"malformed"};let input=asRecord(root.tool_input??root.toolInput);if(ctx.correctionEnforced){if(input===void 0)return workflowDeny("correction-payload-invalid");let decision=evaluateCorrectionToolRole(toolName,input);if(decision!==void 0)return decision}if(toolName!=="Bash")return{kind:"allow"};if(input===void 0)return{kind:"malformed"};let command=input.command;if(typeof command!="string")return ctx.correctionEnforced?workflowDeny("correction-payload-invalid"):{kind:"malformed"};let rawCwd=root.cwd??input.cwd,payloadCwd=typeof rawCwd=="string"?rawCwd:void 0,projections=diagnosticProjections(command);for(let[i,segment]of tokenizeShellCommand(command).entries()){let decision=evaluateSimpleCommand(segment,ctx,payloadCwd,0,!1,projections[i]);if(decision.kind==="deny")return decision}return{kind:"allow"}}function evaluateWorkerGuardInput(raw,ctx){let unreadable=()=>ctx.correctionContextInvalid?workflowDeny("correction-context-invalid"):ctx.correctionEnforced?workflowDeny("correction-payload-invalid"):{kind:"malformed"};if(typeof raw!="string"||raw.length>WORKER_GUARD_MAX_PAYLOAD_BYTES||raw.trim().length===0)return unreadable();let parsed;try{parsed=JSON.parse(raw)}catch{return unreadable()}try{return evaluateWorkerGuardPayload(parsed,ctx)}catch{return unreadable()}}var WORKER_GUARD_ROOT_FLAG="--worktree-root";function resolveWorkerGuardContext(env,cwd,argv=[]){let ctx={},root;for(let i=0;i<argv.length;i+=1){if(argv[i]===WORKER_GUARD_ROOT_FLAG){root=argv[i+1];break}if(argv[i].startsWith(`${WORKER_GUARD_ROOT_FLAG}=`)){root=argv[i].slice(WORKER_GUARD_ROOT_FLAG.length+1);break}}root===void 0&&typeof cwd=="string"&&(root=cwd),typeof root=="string"&&path.isAbsolute(root)&&(ctx.worktreePath=path.resolve(root));let branch=env.BAPI_WORKER_BRANCH;typeof branch=="string"&&branch.trim().length>0&&(ctx.workerBranch=branch.trim());let flagIndex=argv.indexOf("--correction-intent-version"),flagValue=flagIndex>=0?argv[flagIndex+1]:void 0,envValue=env.BAPI_CORRECTION_INTENT_VERSION;return ctx.correctionEnforced=flagValue==="1"||envValue==="1",ctx.correctionContextInvalid=flagIndex>=0&&flagValue!=="1"||envValue!==void 0&&envValue!=="1"||flagIndex>=0&&envValue!==void 0&&envValue!==flagValue,ctx}function runWorkerGuardHook(io){let exitCode=0,correctionRequested=io.argv.includes("--correction-intent-version")||io.env.BAPI_CORRECTION_INTENT_VERSION!==void 0;try{let raw=io.readStdin(),ctx=resolveWorkerGuardContext(io.env,io.cwd,io.argv),decision=evaluateWorkerGuardInput(raw,ctx);decision.kind==="deny"?(io.writeStderr(renderWorkerGuardReason(decision.category,decision.family,decision.form)),exitCode=2):decision.kind==="malformed"&&(io.writeStderr(WORKER_GUARD_MALFORMED_LINE),exitCode=0)}catch{try{io.writeStderr(correctionRequested?renderWorkerGuardReason("correction-payload-invalid","workflow"):WORKER_GUARD_MALFORMED_LINE)}catch{}exitCode=correctionRequested?2:0}return exitCode}function readStdinSync(){return readFileSync(0,"utf-8")}process.exit(runWorkerGuardHook({readStdin:readStdinSync,env:process.env,cwd:process.cwd(),argv:process.argv.slice(2),writeStderr:text=>{process.stderr.write(`${text}
|
|
4
5
|
`)}}));
|
|
@@ -20,7 +20,7 @@ export function runWorkerGuardHook(io) {
|
|
|
20
20
|
const ctx = resolveWorkerGuardContext(io.env, io.cwd, io.argv);
|
|
21
21
|
const decision = evaluateWorkerGuardInput(raw, ctx);
|
|
22
22
|
if (decision.kind === "deny") {
|
|
23
|
-
io.writeStderr(renderWorkerGuardReason(decision.category, decision.family));
|
|
23
|
+
io.writeStderr(renderWorkerGuardReason(decision.category, decision.family, decision.form));
|
|
24
24
|
exitCode = 2;
|
|
25
25
|
}
|
|
26
26
|
else if (decision.kind === "malformed") {
|