From 01bc5558afb60b006af7b1e7b774066fba63dc7b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 17 Jun 2026 07:53:38 +0100 Subject: [PATCH] Gate dispatched GitHub CLI mutations --- README.md | 2 +- docs/agent-transport.md | 2 +- docs/bridge.md | 2 +- tests/Bifrost.Web.Tests/BridgeCliTests.cs | 126 +++++++++++++++++++--- tools/bifrost-bridge.mjs | 50 ++++++--- tools/dispatch-agent-requests.mjs | 51 +++++++++ tools/git-gate/gh-proxy.mjs | 118 ++++++++++++++++++++ tools/git-gate/gh.cmd | 18 ++++ tools/git-gate/git-proxy.mjs | 64 +++++++++++ tools/git-gate/git.cmd | 18 ++++ 10 files changed, 423 insertions(+), 28 deletions(-) create mode 100644 tools/git-gate/gh-proxy.mjs create mode 100644 tools/git-gate/gh.cmd create mode 100644 tools/git-gate/git-proxy.mjs create mode 100644 tools/git-gate/git.cmd diff --git a/README.md b/README.md index b36142b..ce073d4 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ For the full staged roadmap, including contribution points, revenue share, patro - set `Bridge:LocalBridgeToken` to allow the transitional local bridge actuator to register and receipt governed actions through `/bridge/actions/*` - set `BIFROST_BRIDGE_BASE_URL` and `BIFROST_BRIDGE_TOKEN` when `tools/bifrost-bridge.mjs` should round-trip every action through the app ledger before mutating GitHub, Discord, or Reddit - the same bridge URL/token pair lets `tools/dispatch-agent-requests.mjs` record `/dispatch/runs/*` receipts for launched Codex work, so Bifrost keeps a live run trail even when a turn fails -- Bifrost-dispatched Codex turns also inject a Git `pre-push` hook through environment config, so raw `git push` is blocked unless `tools/bifrost-bridge.mjs` explicitly authorizes the bridge-owned push +- Bifrost-dispatched Codex turns also inject Bifrost-owned GitHub gates through environment config: `git` and `gh` wrappers are prepended in `PATH`, a `pre-push` hook backs up raw Git pushes, raw `git push` and mutating `gh` commands stay blocked, and only `tools/bifrost-bridge.mjs` is allowed to authorize the bridge-owned GitHub mutation path - `tools/agent-transport.mjs` also uses that bridge configuration to record `/transport/receipts` entries for queue, claim, release, and close events on the request lane - `tools/governance-threads.mjs` uses that same bridge configuration to record `/governance/receipts` entries for topic opens, comments, approvals, and promotions - those agent activity scripts now fail closed without the bridge URL/token pair; `--allow-unreceipted-activity true` or `BIFROST_ALLOW_UNRECEIPTED_ACTIVITY=true` is the explicit operator-recovery hatch diff --git a/docs/agent-transport.md b/docs/agent-transport.md index d1f10c5..a7e35b3 100644 --- a/docs/agent-transport.md +++ b/docs/agent-transport.md @@ -126,7 +126,7 @@ node .\tools\agent-transport.mjs apply-snapshot --in E:\tmp\bifrost-agent-transp Dispatch receipts should use Bifrost's own public persona in the Bifrost governance channel. `tools/dispatch-agent-requests.mjs` reads `BIFROST_DISCORD_CHANNEL_ID` or `DISCORD_BIFROST_CHANNEL_ID` for the receipt target, uses persona name `Bifrost`, and defaults the persona avatar to the public `src/Bifrost.Web/wwwroot/img/bifrost-profile.png` raw GitHub URL unless `BIFROST_DISCORD_PERSONA_AVATAR_URL` or `DISCORD_PERSONA_AVATAR_URL_BIFROST` overrides it. Receipt text must lead with the concrete repo and work title, not a generic "recent consensus" summary, and must not expose request ids, workspace paths, log paths, or other debugging debris in Discord. The dispatcher now tags those Discord receipts with the Bifrost request id as bridge provenance, and dispatched Codex turns inherit default bridge provenance environment variables so any explicit `tools/bifrost-bridge.mjs` crossing inside the turn can land on the same request trail without hand-carried flags. When `BIFROST_BRIDGE_BASE_URL` and `BIFROST_BRIDGE_TOKEN` are configured, the dispatcher also records `/dispatch/runs/*` receipts in the hosted app so Bifrost keeps a first-class run ledger for launch, completion, cancellation, and failure. -Those dispatched turns also inherit a Bifrost-owned Git `pre-push` hook through environment config. That turns "use the bridge" from etiquette into substrate: raw `git push` is blocked inside dispatched work unless the bridge explicitly authorizes the push it owns. +Those dispatched turns also inherit Bifrost-owned GitHub gates through environment config. `git` and `gh` wrappers are prepended in `PATH`, a `pre-push` hook backs up raw Git pushes, raw `git push` stays blocked inside dispatched work even with `--no-verify`, and mutating `gh` commands are blocked too unless the bridge explicitly authorizes the GitHub mutation it owns. When that same bridge configuration is present, `tools/agent-transport.mjs` records `/transport/receipts` entries for queue, claim, release, and close operations on the request lane. The packet still lives in CultCache. The hosted receipt row is the Bifrost-side witness that an agent or dispatcher touched it. Those mutating scripts now fail closed without that bridge configuration. Use `--allow-unreceipted-activity true` or `BIFROST_ALLOW_UNRECEIPTED_ACTIVITY=true` only for explicit local recovery work where you are intentionally accepting a blind spot. diff --git a/docs/bridge.md b/docs/bridge.md index 1daf16e..3c4d19c 100644 --- a/docs/bridge.md +++ b/docs/bridge.md @@ -92,7 +92,7 @@ Current CLI wiring: - GitHub commands now require both values by default, even for dry-run, so the normal path cannot silently bypass the gate - pass `--source-kind`, `--source-id`, `--authority-ref`, `--work-item-id`, or `--motion-id` so agent actions satisfy policy - the dispatcher now preloads `BIFROST_BRIDGE_SOURCE_KIND`, `BIFROST_BRIDGE_SOURCE_ID`, and `BIFROST_BRIDGE_AUTHORITY_REF` into Codex turns launched from Bifrost update requests, so normal bridge use inside a dispatched turn carries request provenance by default -- those dispatched turns also inject a Bifrost-owned Git `pre-push` hook through environment config, so raw `git push` is blocked and `tools/bifrost-bridge.mjs` becomes the only normal publication path +- those dispatched turns also inject Bifrost-owned GitHub gates through environment config: `git` and `gh` wrappers are prepended in `PATH`, a `pre-push` hook backs up raw Git pushes, raw `git push` stays blocked even when `--no-verify` is used, and mutating `gh` commands are blocked unless the bridge explicitly authorizes them - the dispatcher also uses that same bridge configuration to post dispatch-run start/complete/fail receipts, including the worker pid, thread/turn ids when available, and result/log paths for the local operator trail - the agent transport CLI uses that same bridge configuration to post request-lane receipts for queue, claim, release, and close events, so Bifrost does not go blind between intake and dispatch - the governance threads CLI uses that same bridge configuration to post governance receipts for topic opens, comments, approvals, and promotions, so Bifrost does not lose the pre-dispatch reasoning trail diff --git a/tests/Bifrost.Web.Tests/BridgeCliTests.cs b/tests/Bifrost.Web.Tests/BridgeCliTests.cs index 63e8d0d..ee31fd8 100644 --- a/tests/Bifrost.Web.Tests/BridgeCliTests.cs +++ b/tests/Bifrost.Web.Tests/BridgeCliTests.cs @@ -225,13 +225,23 @@ console.log(JSON.stringify({ await RunGitAsync(["commit", "-m", "Initial commit"], workDir); await RunGitAsync(["remote", "add", "origin", remoteDir], workDir); - var blocked = await RunGitAsync( - ["push", "-u", "origin", "main"], + var blocked = await RunCommandAsync( + "powershell", + ["-NoProfile", "-Command", "git push -u origin main"], workDir, BuildDispatchedGitHookEnv()); Assert.NotEqual(0, blocked.ExitCode); Assert.Contains("Bifrost blocked git push", blocked.Stderr, StringComparison.OrdinalIgnoreCase); + + var blockedNoVerify = await RunCommandAsync( + "powershell", + ["-NoProfile", "-Command", "git push --no-verify -u origin main"], + workDir, + BuildDispatchedGitGateEnv()); + + Assert.NotEqual(0, blockedNoVerify.ExitCode); + Assert.Contains("Bifrost blocked git push", blockedNoVerify.Stderr, StringComparison.OrdinalIgnoreCase); } finally { @@ -263,17 +273,13 @@ console.log(JSON.stringify({ var fakeGhPath = Path.Combine(fakeToolsDir, "fake-gh.js"); await File.WriteAllTextAsync(fakeGhPath, """ -if (process.argv.includes("create")) { - console.log("https://github.com/GameCult/Bifrost/pull/999"); - process.exit(0); -} - -console.error(`Unexpected gh args: ${process.argv.slice(2).join(" ")}`); -process.exit(1); +console.log("https://github.com/GameCult/Bifrost/pull/999"); """, Encoding.UTF8); - var environment = BuildDispatchedGitHookEnv(); + var environment = BuildDispatchedGitGateEnv(); environment["BIFROST_ALLOW_UNGATED_GITHUB"] = "true"; + environment["BIFROST_REAL_GH"] = ResolveExecutable("node"); + environment["BIFROST_REAL_GH_ARGS"] = fakeGhPath; var result = await RunNodeAsync([ "tools/bifrost-bridge.mjs", @@ -285,8 +291,6 @@ process.exit(1); "--content", "Bridge-owned push.", "--body", "Bifrost bridge test PR.", "--base", "main", - "--gh-executable", "node", - "--gh-exec-args", fakeGhPath, ], environment); Assert.Equal(0, result.ExitCode); @@ -306,6 +310,40 @@ process.exit(1); } } + [Fact] + public async Task Dispatched_turn_github_cli_mutation_is_blocked_without_bridge_authorization() + { + var tempDir = Path.Combine(Path.GetTempPath(), $"bifrost-gh-gate-{Guid.NewGuid():N}"); + Directory.CreateDirectory(tempDir); + + try + { + var fakeGhPath = Path.Combine(tempDir, "fake-gh.cmd"); + await File.WriteAllTextAsync(fakeGhPath, """ +@echo off +echo should-not-run +exit /b 0 +""", Encoding.UTF8); + + var environment = BuildDispatchedGitGateEnv(); + environment["BIFROST_REAL_GH"] = fakeGhPath; + + var blocked = await RunCommandAsync( + "powershell", + ["-NoProfile", "-Command", "gh pr comment 42 --body blocked"], + RepoRoot, + environment); + + Assert.NotEqual(0, blocked.ExitCode); + Assert.Contains("Bifrost blocked GitHub CLI mutation", blocked.Stderr, StringComparison.OrdinalIgnoreCase); + Assert.DoesNotContain("should-not-run", blocked.Stdout, StringComparison.OrdinalIgnoreCase); + } + finally + { + DeleteDirectoryIfPresent(tempDir); + } + } + private static string RepoRoot => Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..")); @@ -349,12 +387,19 @@ process.exit(1); } private static async Task RunGitAsync(string[] args, string workingDirectory, IReadOnlyDictionary? environmentOverrides = null) + => await RunCommandAsync("git", args, workingDirectory, environmentOverrides); + + private static async Task RunCommandAsync( + string fileName, + string[] args, + string workingDirectory, + IReadOnlyDictionary? environmentOverrides = null) { using var process = new Process { StartInfo = new ProcessStartInfo { - FileName = "git", + FileName = fileName, WorkingDirectory = workingDirectory, RedirectStandardOutput = true, RedirectStandardError = true, @@ -386,15 +431,70 @@ process.exit(1); private static Dictionary BuildDispatchedGitHookEnv() { var hooksPath = Path.Combine(RepoRoot, "tools", "git-hooks"); + var gatePath = Path.Combine(RepoRoot, "tools", "git-gate"); + var path = Environment.GetEnvironmentVariable("Path") + ?? Environment.GetEnvironmentVariable("PATH") + ?? string.Empty; + var nextPath = string.IsNullOrWhiteSpace(path) + ? gatePath + : $"{gatePath};{path}"; return new Dictionary { ["BIFROST_ENFORCE_GITHUB_GATE"] = "true", + ["BIFROST_GIT_EXECUTABLE"] = Path.Combine(gatePath, "git.cmd"), + ["BIFROST_GH_EXECUTABLE"] = Path.Combine(gatePath, "gh.cmd"), + ["BIFROST_REAL_GIT"] = ResolveRealGitExecutable(), + ["BIFROST_NODE_EXECUTABLE"] = ResolveExecutable("node"), ["GIT_CONFIG_COUNT"] = "1", ["GIT_CONFIG_KEY_0"] = "core.hooksPath", ["GIT_CONFIG_VALUE_0"] = hooksPath, + ["PATH"] = nextPath, + ["Path"] = nextPath, }; } + private static Dictionary BuildDispatchedGitGateEnv() => BuildDispatchedGitHookEnv(); + + private static string ResolveRealGitExecutable() => ResolveExecutable("git"); + + private static string ResolveExecutable(string command) + { + using var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "where.exe", + WorkingDirectory = RepoRoot, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + } + }; + + process.StartInfo.ArgumentList.Add(command); + process.Start(); + var stdout = process.StandardOutput.ReadToEnd(); + var stderr = process.StandardError.ReadToEnd(); + process.WaitForExit(); + + if (process.ExitCode != 0) + { + throw new InvalidOperationException($"Could not resolve {command} for tests: {stderr}{stdout}"); + } + + var path = stdout + .Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) + .Select(line => line.Trim()) + .FirstOrDefault(line => !string.IsNullOrWhiteSpace(line)); + + if (string.IsNullOrWhiteSpace(path)) + { + throw new InvalidOperationException($"Could not resolve {command} for tests."); + } + + return path; + } + private static void DeleteDirectoryIfPresent(string path) { if (!Directory.Exists(path)) diff --git a/tools/bifrost-bridge.mjs b/tools/bifrost-bridge.mjs index 3b3cbc2..7efc254 100644 --- a/tools/bifrost-bridge.mjs +++ b/tools/bifrost-bridge.mjs @@ -129,7 +129,11 @@ async function createGitHubDraftPr(options) { base, "--head", branch, - ], repoRoot); + ], repoRoot, { + env: { + BIFROST_GITHUB_MUTATION_AUTHORIZED: "true", + }, + }); prUrl = pr.stdout.trim(); await bridgeAction?.complete({ receiptUrl: prUrl, @@ -218,6 +222,11 @@ async function commentGitHubPr(options) { "--input", payloadPath, ], repoRoot, + { + env: { + BIFROST_GITHUB_MUTATION_AUTHORIZED: "true", + }, + }, ).stdout, "GitHub PR comment response", ); @@ -867,7 +876,7 @@ async function readOptionText(options, inlineName, fileName, fallback) { } function git(args, cwd, options = {}) { - return run("git", args, cwd, options); + return run(resolveGitCommand(), args, cwd, options); } function runGitHubCli(options, args, cwd, runOptions = {}) { @@ -876,16 +885,25 @@ function runGitHubCli(options, args, cwd, runOptions = {}) { } function run(command, args, cwd, options = {}) { - const result = spawnSync(command, args, { - cwd, - encoding: "utf8", - stdio: ["ignore", "pipe", "pipe"], - env: { - ...process.env, - ...(options.env ?? {}), - }, - windowsHide: true, - }); + const env = { + ...process.env, + ...(options.env ?? {}), + }; + const result = isWindowsCommandScript(command) + ? spawnSync("cmd.exe", ["/d", "/s", "/c", command, ...args], { + cwd, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + env, + windowsHide: true, + }) + : spawnSync(command, args, { + cwd, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + env, + windowsHide: true, + }); if (result.status !== 0 && !options.allowFailure) { throw new Error(`${command} ${args.join(" ")} failed with ${result.status ?? "unknown"}:\n${result.stderr || result.stdout}`); @@ -936,6 +954,10 @@ function optionalString(value) { return trimmed.length > 0 ? trimmed : undefined; } +function resolveGitCommand() { + return optionalString(process.env.BIFROST_GIT_EXECUTABLE) ?? "git"; +} + async function beginBridgeAction(options, action) { const baseUrl = optionalString(options["bifrost-base-url"]) ?? optionalString(process.env.BIFROST_BRIDGE_BASE_URL); if (!baseUrl) { @@ -1085,6 +1107,10 @@ function resolveGitHubCommand(options) { }; } +function isWindowsCommandScript(command) { + return /\.cmd$/i.test(command) || /\.bat$/i.test(command); +} + function splitCommandArgs(value) { if (typeof value !== "string" || value.trim().length === 0) { return []; diff --git a/tools/dispatch-agent-requests.mjs b/tools/dispatch-agent-requests.mjs index 0c12859..1387b19 100644 --- a/tools/dispatch-agent-requests.mjs +++ b/tools/dispatch-agent-requests.mjs @@ -561,12 +561,19 @@ function renderDispatchReceiptContent(request) { function buildBridgeContextEnv(request) { const hooksPath = resolve(bifrostRoot, "tools", "git-hooks"); + const gitGatePath = resolve(bifrostRoot, "tools", "git-gate"); return { BIFROST_BRIDGE_SOURCE_KIND: "bifrost_agent_transport_request", BIFROST_BRIDGE_SOURCE_ID: request.id, BIFROST_BRIDGE_AUTHORITY_REF: "bifrost_dispatch_execution", BIFROST_ENFORCE_GITHUB_GATE: "true", + BIFROST_GIT_EXECUTABLE: resolve(gitGatePath, "git.cmd"), + BIFROST_GH_EXECUTABLE: resolve(gitGatePath, "gh.cmd"), + BIFROST_REAL_GIT: resolveRealGitExecutable(), + ...buildRealGitHubCliEnv(), + BIFROST_NODE_EXECUTABLE: process.execPath, ...buildGitHooksConfigEnv(hooksPath), + ...buildPathPrependEnv(gitGatePath), }; } @@ -587,6 +594,50 @@ function buildGitHooksConfigEnv(hooksPath) { }; } +function buildPathPrependEnv(directory) { + const currentPath = process.env.Path ?? process.env.PATH ?? ""; + const nextPath = currentPath ? `${directory};${currentPath}` : directory; + return { + PATH: nextPath, + Path: nextPath, + }; +} + +function buildRealGitHubCliEnv() { + const realGh = tryResolveExecutable("gh"); + return realGh ? { BIFROST_REAL_GH: realGh } : {}; +} + +function resolveRealGitExecutable() { + const resolved = tryResolveExecutable("git"); + if (!resolved) { + throw new Error("Could not resolve the real git executable for dispatched turn gating."); + } + + return resolved; +} + +function tryResolveExecutable(command) { + const locator = process.platform === "win32" ? "where.exe" : "which"; + const result = spawnSync(locator, [command], { + cwd: bifrostRoot, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + windowsHide: true, + }); + + if (result.status !== 0) { + return ""; + } + + const matches = `${result.stdout ?? ""}` + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean); + + return matches[0] ?? ""; +} + class CodexAppServerClient { constructor({ command, env, logPath }) { this.command = command; diff --git a/tools/git-gate/gh-proxy.mjs b/tools/git-gate/gh-proxy.mjs new file mode 100644 index 0000000..fb63cdd --- /dev/null +++ b/tools/git-gate/gh-proxy.mjs @@ -0,0 +1,118 @@ +#!/usr/bin/env node +import { spawnSync } from "node:child_process"; + +const args = process.argv.slice(2); +const realGh = process.env.BIFROST_REAL_GH; +const realGhArgs = splitCommandArgs(process.env.BIFROST_REAL_GH_ARGS ?? ""); +const mutatingCommands = new Set([ + "gist create", + "gist delete", + "gist edit", + "issue close", + "issue comment", + "issue create", + "issue delete", + "issue edit", + "issue lock", + "issue reopen", + "pr close", + "pr comment", + "pr create", + "pr edit", + "pr lock", + "pr merge", + "pr ready", + "pr reopen", + "pr review", + "release create", + "release delete", + "release edit", + "repo archive", + "repo create", + "repo delete", + "repo edit", + "repo fork", + "repo rename", + "secret delete", + "secret set", + "variable delete", + "variable set", +]); + +if (!realGh) { + process.stderr.write("Bifrost GitHub gate has no real GitHub CLI executable configured.\n"); + process.exit(1); +} + +if ( + process.env.BIFROST_ENFORCE_GITHUB_GATE === "true" && + isMutatingGitHubCliCommand(args) && + process.env.BIFROST_GITHUB_MUTATION_AUTHORIZED !== "true" +) { + process.stderr.write( + "Bifrost blocked GitHub CLI mutation in this dispatched turn. Use tools/bifrost-bridge.mjs so GitHub publication is gated and receipted.\n", + ); + process.exit(1); +} + +const result = isWindowsCommandScript(realGh) + ? spawnSync("cmd.exe", ["/d", "/s", "/c", realGh, ...realGhArgs, ...args], { + stdio: "inherit", + env: process.env, + windowsHide: true, + }) + : spawnSync(realGh, [...realGhArgs, ...args], { + stdio: "inherit", + env: process.env, + windowsHide: true, + }); + +if (result.error) { + throw result.error; +} + +process.exit(result.status ?? 1); + +function isWindowsCommandScript(command) { + return /\.cmd$/i.test(command) || /\.bat$/i.test(command); +} + +function isMutatingGitHubCliCommand(input) { + const [scope = "", action = ""] = input; + if (!scope) { + return false; + } + + if (scope === "api") { + return apiMethod(input) !== "GET"; + } + + return mutatingCommands.has(`${scope} ${action}`); +} + +function apiMethod(input) { + for (let index = 0; index < input.length; index += 1) { + const token = input[index]; + if (token === "--method") { + return `${input[index + 1] ?? "GET"}`.trim().toUpperCase(); + } + + if (token.startsWith("--method=")) { + return token.slice("--method=".length).trim().toUpperCase(); + } + } + + return input.includes("--input") ? "POST" : "GET"; +} + +function splitCommandArgs(value) { + if (!value) { + return []; + } + + const matches = value.match(/(?:[^\s"]+|"[^"]*")+/g) ?? []; + return matches + .map((part) => part.trim()) + .filter(Boolean) + .map((part) => (part.startsWith("\"") && part.endsWith("\"")) ? part.slice(1, -1) : part); +} diff --git a/tools/git-gate/gh.cmd b/tools/git-gate/gh.cmd new file mode 100644 index 0000000..9da53f9 --- /dev/null +++ b/tools/git-gate/gh.cmd @@ -0,0 +1,18 @@ +@echo off +setlocal + +set "REAL_GH=%BIFROST_REAL_GH%" +if not defined REAL_GH ( + echo Bifrost GitHub gate has no real GitHub CLI executable configured.>&2 + exit /b 1 +) + +set "NODE_EXE=%BIFROST_NODE_EXECUTABLE%" +if not defined NODE_EXE ( + echo Bifrost GitHub gate has no Node executable configured.>&2 + exit /b 1 +) + +set "NODE_SCRIPT=%~dp0gh-proxy.mjs" +"%NODE_EXE%" "%NODE_SCRIPT%" %* +exit /b %ERRORLEVEL% diff --git a/tools/git-gate/git-proxy.mjs b/tools/git-gate/git-proxy.mjs new file mode 100644 index 0000000..08e8b13 --- /dev/null +++ b/tools/git-gate/git-proxy.mjs @@ -0,0 +1,64 @@ +#!/usr/bin/env node +import { spawnSync } from "node:child_process"; + +const args = process.argv.slice(2); +const realGit = process.env.BIFROST_REAL_GIT; + +if (!realGit) { + process.stderr.write("Bifrost Git gate has no real git executable configured.\n"); + process.exit(1); +} + +const subcommand = findGitSubcommand(args); +if ( + process.env.BIFROST_ENFORCE_GITHUB_GATE === "true" && + subcommand === "push" && + process.env.BIFROST_GITHUB_PUSH_AUTHORIZED !== "true" +) { + process.stderr.write( + "Bifrost blocked git push in this dispatched turn. Use tools/bifrost-bridge.mjs so GitHub publication is gated and receipted.\n", + ); + process.exit(1); +} + +const result = spawnSync(realGit, args, { + stdio: "inherit", + env: process.env, + windowsHide: true, +}); + +if (result.error) { + throw result.error; +} + +process.exit(result.status ?? 1); + +function findGitSubcommand(input) { + const optionsWithSeparateValues = new Set([ + "-C", + "-c", + "--exec-path", + "--git-dir", + "--work-tree", + "--namespace", + "--super-prefix", + "--config-env", + ]); + + for (let index = 0; index < input.length; index += 1) { + const token = input[index]; + if (token === "--") { + return input[index + 1] ?? ""; + } + + if (!token.startsWith("-")) { + return token; + } + + if (optionsWithSeparateValues.has(token)) { + index += 1; + } + } + + return ""; +} diff --git a/tools/git-gate/git.cmd b/tools/git-gate/git.cmd new file mode 100644 index 0000000..c76c759 --- /dev/null +++ b/tools/git-gate/git.cmd @@ -0,0 +1,18 @@ +@echo off +setlocal + +set "REAL_GIT=%BIFROST_REAL_GIT%" +if not defined REAL_GIT ( + echo Bifrost Git gate has no real git executable configured.>&2 + exit /b 1 +) + +set "NODE_EXE=%BIFROST_NODE_EXECUTABLE%" +if not defined NODE_EXE ( + echo Bifrost Git gate has no Node executable configured.>&2 + exit /b 1 +) + +set "NODE_SCRIPT=%~dp0git-proxy.mjs" +"%NODE_EXE%" "%NODE_SCRIPT%" %* +exit /b %ERRORLEVEL%