From ba01b78d688ccdda3fd0130dc23e8f0113b6d3b2 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 22 Jun 2026 14:03:07 +0100 Subject: [PATCH] Fix Bifrost bridge runtime and fixture env isolation --- docs/agent-transport.md | 2 +- tests/Bifrost.Web.Tests/BridgeCliTests.cs | 50 +++++++++++++++++++++++ tools/agent-transport.mjs | 37 ++++++++++++++--- tools/bifrost-bridge.mjs | 12 +++++- tools/dispatch-agent-requests.mjs | 12 +++++- tools/governance-threads.mjs | 29 +++++++++++-- tools/provider-advertisement.mjs | 20 ++++++++- 7 files changed, 147 insertions(+), 15 deletions(-) diff --git a/docs/agent-transport.md b/docs/agent-transport.md index 24f6c7c..683fedb 100644 --- a/docs/agent-transport.md +++ b/docs/agent-transport.md @@ -105,7 +105,7 @@ Each request has: ## CLI -`tools/agent-transport.mjs` is intentionally local and boring. It imports the sibling `CultCacheTS` and `CultNetTS` builds directly, writes a `.cc` store, and can emit/apply CultNet raw snapshots. +`tools/agent-transport.mjs` is intentionally local and boring. It imports the local CultCache/CultNet TypeScript runtimes, preferring the current `CultLib\packages\cultcache-ts` and `CultLib\packages\cultnet-ts` packages while retaining the older sibling `CultCacheTS` / `CultNetTS` checkout paths as compatibility reliquaries. It writes a `.cc` store and can emit/apply CultNet raw snapshots. Default store: diff --git a/tests/Bifrost.Web.Tests/BridgeCliTests.cs b/tests/Bifrost.Web.Tests/BridgeCliTests.cs index e7a50db..51656d9 100644 --- a/tests/Bifrost.Web.Tests/BridgeCliTests.cs +++ b/tests/Bifrost.Web.Tests/BridgeCliTests.cs @@ -201,6 +201,25 @@ console.log(JSON.stringify({ Assert.Contains("BIFROST_BRIDGE_BASE_URL and BIFROST_BRIDGE_TOKEN", result.Stderr, StringComparison.OrdinalIgnoreCase); } + [Fact] + public async Task Agent_transport_fixture_harness_does_not_inherit_live_discord_mirror_env() + { + var storePath = Path.Combine(Path.GetTempPath(), $"bifrost-agent-transport-{Guid.NewGuid():N}.cc"); + var result = await RunNodeAsync([ + "tools/agent-transport.mjs", + "enqueue", + "--repo", "Bifrost", + "--agent", "nibu", + "--title", "Discord inheritance guard", + "--request", "Do not post fixture noise.", + "--store", storePath, + "--allow-unreceipted-activity", "true", + ]); + + Assert.NotEqual(0, result.ExitCode); + Assert.Contains("require a Discord mirror", result.Stderr, StringComparison.OrdinalIgnoreCase); + } + [Fact] public async Task Agent_transport_mutation_allows_explicit_operator_recovery() { @@ -982,6 +1001,8 @@ exit /b 0 process.StartInfo.Environment["BIFROST_BRIDGE_TOKEN"] = string.Empty; process.StartInfo.Environment["BIFROST_ALLOW_UNGATED_GITHUB"] = string.Empty; process.StartInfo.Environment["BIFROST_ALLOW_UNRECEIPTED_ACTIVITY"] = string.Empty; + process.StartInfo.Environment["BIFROST_SKIP_LOCAL_ENV"] = "true"; + ClearLiveDiscordMirrorEnvironment(process.StartInfo.Environment); if (environmentOverrides is not null) { foreach (var pair in environmentOverrides) @@ -998,6 +1019,35 @@ exit /b 0 return new ProcessResult(process.ExitCode, stdout, stderr); } + private static void ClearLiveDiscordMirrorEnvironment(IDictionary environment) + { + string[] exactKeys = + [ + "BIFROST_ALLOW_UNMIRRORED_GOVERNANCE", + "BIFROST_DISCORD_BOT_TOKEN", + "BIFROST_DISCORD_CHANNEL_ID", + "BIFROST_DISCORD_PERSONA_AVATAR_URL", + "BIFROST_DISCORD_PERSONA_NAME", + "DISCORD_BIFROST_CHANNEL_ID", + "DISCORD_BOT_TOKEN", + "DISCORD_PERSONA_AVATAR_URL", + "DISCORD_PERSONA_AVATAR_URL_BIFROST", + "DISCORD_PERSONA_WEBHOOK_URL", + ]; + + foreach (var key in exactKeys) + { + environment[key] = string.Empty; + } + + foreach (var key in environment.Keys + .Where(key => key.StartsWith("BIFROST_DISCORD_PERSONA_WEBHOOK_URL_", StringComparison.OrdinalIgnoreCase)) + .ToArray()) + { + environment[key] = string.Empty; + } + } + private static async Task RunGitAsync(string[] args, string workingDirectory, IReadOnlyDictionary? environmentOverrides = null) => await RunCommandAsync("git", args, workingDirectory, environmentOverrides); diff --git a/tools/agent-transport.mjs b/tools/agent-transport.mjs index c79140e..7453c15 100644 --- a/tools/agent-transport.mjs +++ b/tools/agent-transport.mjs @@ -16,8 +16,17 @@ const defaultPersonaName = "Bifrost"; const defaultPersonaAvatarUrl = "https://raw.githubusercontent.com/GameCult/Bifrost/main/src/Bifrost.Web/wwwroot/img/bifrost-profile.png"; -const cultCacheRequire = createRequire(resolve(projectsRoot, "CultCacheTS", "node_modules", "cultcache-ts", "package.json")); -const cultNetRequire = createRequire(resolve(projectsRoot, "CultNetTS", "package.json")); +const cultCachePackagePath = resolveFirstExisting("CultCache TypeScript runtime", [ + resolve(projectsRoot, "CultLib", "packages", "cultcache-ts", "package.json"), + resolve(projectsRoot, "CultCacheTS", "package.json"), + resolve(projectsRoot, "CultCacheTS", "node_modules", "cultcache-ts", "package.json"), +]); +const cultNetPackagePath = resolveFirstExisting("CultNet TypeScript runtime", [ + resolve(projectsRoot, "CultLib", "packages", "cultnet-ts", "package.json"), + resolve(projectsRoot, "CultNetTS", "package.json"), +]); +const cultCacheRequire = createRequire(cultCachePackagePath); +const cultNetRequire = createRequire(cultNetPackagePath); const { CultCache, @@ -27,9 +36,19 @@ const { const { CultNetDocumentRegistry, defineCultNetDocumentBinding, -} = cultNetRequire(resolve(projectsRoot, "CultNetTS", "dist", "index.js")); +} = cultNetRequire("cultnet-ts"); const { encode, decode } = cultNetRequire("@msgpack/msgpack"); +function resolveFirstExisting(label, candidates) { + for (const candidate of candidates) { + if (existsSync(candidate)) { + return candidate; + } + } + + throw new Error(`${label} is unavailable. Tried: ${candidates.join(", ")}`); +} + const documentType = "bifrost.agent-transport.update-request"; const schemaId = "bifrost.agent-transport.update-request.v0"; const validStatuses = new Set(["queued", "claimed", "completed", "cancelled"]); @@ -81,8 +100,7 @@ const cultNetRegistry = new CultNetDocumentRegistry([ ]); async function main() { - loadLocalEnv(resolve(repoRoot, ".env")); - loadLocalEnv(resolve(projectsRoot, "VoidBot", ".env")); + loadBifrostLocalEnv(); const [command, ...rawArgs] = process.argv.slice(2); const options = parseArgs(rawArgs); @@ -128,6 +146,15 @@ async function main() { } } +function loadBifrostLocalEnv() { + if (process.env.BIFROST_SKIP_LOCAL_ENV === "true") { + return; + } + + loadLocalEnv(resolve(repoRoot, ".env")); + loadLocalEnv(resolve(projectsRoot, "VoidBot", ".env")); +} + function isMutatingCommand(command) { return command === "enqueue" || command === "claim" diff --git a/tools/bifrost-bridge.mjs b/tools/bifrost-bridge.mjs index 850fe4f..6806726 100644 --- a/tools/bifrost-bridge.mjs +++ b/tools/bifrost-bridge.mjs @@ -13,8 +13,7 @@ const repoRoot = resolve(scriptDir, ".."); const projectsRoot = resolve(repoRoot, ".."); async function main() { - loadLocalEnv(resolve(repoRoot, ".env")); - loadLocalEnv(resolve(projectsRoot, "VoidBot", ".env")); + loadBifrostLocalEnv(); const [command, ...rawArgs] = process.argv.slice(2); const options = parseArgs(rawArgs); @@ -44,6 +43,15 @@ async function main() { } } +function loadBifrostLocalEnv() { + if (process.env.BIFROST_SKIP_LOCAL_ENV === "true") { + return; + } + + loadLocalEnv(resolve(repoRoot, ".env")); + loadLocalEnv(resolve(projectsRoot, "VoidBot", ".env")); +} + async function createGitHubDraftPr(options) { ensureGitHubBridgeGate(options); const repoRoot = resolve(requireOption(options, "repo-root")); diff --git a/tools/dispatch-agent-requests.mjs b/tools/dispatch-agent-requests.mjs index b22f466..4fcddfb 100644 --- a/tools/dispatch-agent-requests.mjs +++ b/tools/dispatch-agent-requests.mjs @@ -17,8 +17,7 @@ const defaultPersonaAvatarUrl = "https://raw.githubusercontent.com/GameCult/Bifrost/main/src/Bifrost.Web/wwwroot/img/bifrost-profile.png"; async function main() { - loadLocalEnv(resolve(bifrostRoot, ".env")); - loadLocalEnv(resolve(defaultProjectsRoot, "VoidBot", ".env")); + loadBifrostLocalEnv(); const [command, ...rawArgs] = process.argv.slice(2); const options = parseArgs(rawArgs); @@ -43,6 +42,15 @@ async function main() { } } +function loadBifrostLocalEnv() { + if (process.env.BIFROST_SKIP_LOCAL_ENV === "true") { + return; + } + + loadLocalEnv(resolve(bifrostRoot, ".env")); + loadLocalEnv(resolve(defaultProjectsRoot, "VoidBot", ".env")); +} + async function dispatchQueuedRequests(options) { const max = parseInteger(options.max ?? "1", "max"); const statusDir = resolveOptionPath(options["status-dir"] ?? defaultStatusDir); diff --git a/tools/governance-threads.mjs b/tools/governance-threads.mjs index cebcc93..de1f272 100644 --- a/tools/governance-threads.mjs +++ b/tools/governance-threads.mjs @@ -18,13 +18,28 @@ const defaultPersonaName = "Bifrost"; const defaultPersonaAvatarUrl = "https://raw.githubusercontent.com/GameCult/Bifrost/main/src/Bifrost.Web/wwwroot/img/bifrost-profile.png"; -const cultCacheRequire = createRequire(resolve(projectsRoot, "CultCacheTS", "node_modules", "cultcache-ts", "package.json")); +const cultCachePackagePath = resolveFirstExisting("CultCache TypeScript runtime", [ + resolve(projectsRoot, "CultLib", "packages", "cultcache-ts", "package.json"), + resolve(projectsRoot, "CultCacheTS", "package.json"), + resolve(projectsRoot, "CultCacheTS", "node_modules", "cultcache-ts", "package.json"), +]); +const cultCacheRequire = createRequire(cultCachePackagePath); const { CultCache, SingleFileMessagePackBackingStore, defineDocumentType, } = cultCacheRequire("cultcache-ts"); +function resolveFirstExisting(label, candidates) { + for (const candidate of candidates) { + if (existsSync(candidate)) { + return candidate; + } + } + + throw new Error(`${label} is unavailable. Tried: ${candidates.join(", ")}`); +} + const topicType = "bifrost.governance.topic"; const topicSchemaId = "bifrost.governance.topic.v0"; const commentType = "bifrost.governance.topic-comment"; @@ -91,8 +106,7 @@ const commentDefinition = defineDocumentType({ }); async function main() { - loadLocalEnv(resolve(repoRoot, ".env")); - loadLocalEnv(resolve(projectsRoot, "VoidBot", ".env")); + loadBifrostLocalEnv(); const [command, ...rawArgs] = process.argv.slice(2); const options = parseArgs(rawArgs); @@ -138,6 +152,15 @@ async function main() { } } +function loadBifrostLocalEnv() { + if (process.env.BIFROST_SKIP_LOCAL_ENV === "true") { + return; + } + + loadLocalEnv(resolve(repoRoot, ".env")); + loadLocalEnv(resolve(projectsRoot, "VoidBot", ".env")); +} + async function openCache(storePath) { const cache = CultCache.builder() .withDocumentType(topicDefinition) diff --git a/tools/provider-advertisement.mjs b/tools/provider-advertisement.mjs index 4c70c76..1599ede 100644 --- a/tools/provider-advertisement.mjs +++ b/tools/provider-advertisement.mjs @@ -11,7 +11,7 @@ const repoRoot = resolve(scriptDir, ".."); const projectsRoot = resolve(repoRoot, ".."); const defaultStorePath = resolve(repoRoot, ".bifrost", "provider-advertisement.cc"); -const cultCacheRequire = createRequire(resolve(projectsRoot, "CultCacheTS", "package.json")); +const cultCacheRequire = createRequire(resolveCultCachePackagePath()); const cultCacheRuntime = loadCultCacheRuntime(); const { CultCache, @@ -21,8 +21,8 @@ const { function loadCultCacheRuntime() { const candidates = [ - resolve(projectsRoot, "CultCacheTS", "dist", "index.js"), resolve(projectsRoot, "CultLib", "packages", "cultcache-ts", "dist", "index.js"), + resolve(projectsRoot, "CultCacheTS", "dist", "index.js"), ]; for (const candidate of candidates) { @@ -39,6 +39,22 @@ function loadCultCacheRuntime() { throw new Error("CultCache TypeScript runtime with document APIs is unavailable."); } +function resolveCultCachePackagePath() { + const candidates = [ + resolve(projectsRoot, "CultLib", "packages", "cultcache-ts", "package.json"), + resolve(projectsRoot, "CultCacheTS", "package.json"), + resolve(projectsRoot, "CultCacheTS", "node_modules", "cultcache-ts", "package.json"), + ]; + + for (const candidate of candidates) { + if (existsSync(candidate)) { + return candidate; + } + } + + throw new Error(`CultCache TypeScript runtime is unavailable. Tried: ${candidates.join(", ")}`); +} + const documentType = "gamecult.eve.provider_advertisement"; const schemaId = "gamecult.eve.provider_advertisement.v1"; const documentId = "bifrost";