Add generic outside-world bridge request

This commit is contained in:
Meta Vens 2026-06-22 15:49:11 +01:00
commit f1f91713ad
3 changed files with 147 additions and 0 deletions

View file

@ -45,9 +45,11 @@ It intentionally uses boring local credentials while Heimdall's managed GitHub c
- GitHub actions use the local `gh` authenticated account.
- Discord posts use `BIFROST_DISCORD_BOT_TOKEN` or `DISCORD_BOT_TOKEN`.
- Reddit self-posts use `BIFROST_REDDIT_CLIENT_ID`, `BIFROST_REDDIT_REFRESH_TOKEN`, and optional `BIFROST_REDDIT_CLIENT_SECRET`.
- Future outside-world requests use `other-request` as a receipt-only bridge action until the target surface has a named Bifrost contract and actuator.
The CLI is not the final permission system. It is the working bridge actuator Bifrost owns now, so VoidBot and repo Personas can stop carrying this machinery themselves.
It now fails closed for all external write surfaces by default. GitHub mutations already required the hosted bridge gate; Discord posts, Discord DMs, and Reddit self-posts now require the same `BIFROST_BRIDGE_BASE_URL` and `BIFROST_BRIDGE_TOKEN` receipt path unless an operator explicitly uses `--allow-unreceipted-activity true` for local recovery. Dispatched work is not allowed to use that recovery hatch.
`other-request` also fails closed by default. It does not execute a transport; it asks Bifrost to authorize and receipt the request under `BridgeTargetSurface.Other` so new Persona mouths can start behind identity, Heimdall linkage, and provenance policy before anyone writes a surface-specific actuator.
## Bridge Action Ledger
@ -200,6 +202,19 @@ If the subreddit uses fixed custom flair templates, pass `--persona-flair-id`. I
Reddit is not the canonical vote ledger. Reddit comments, upvotes, and flair labels are evidence until a linked actor commits a Bifrost vote, priority signal, topic comment, or receipt.
### Future Surface Request
```powershell
node .\tools\bifrost-bridge.mjs other-request `
--identity epiphany.Persona `
--surface-name bluesky `
--target-locator at://did:example/app.bsky.feed.post/123 `
--heimdall-capability-ref heimdall:bluesky:capability:epiphany-persona `
--content "Persona asks Bifrost to authorize this public crossing."
```
The command records and completes a Bifrost bridge action using `targetSurface: Other` and `actionKind: Other`. It is the quarantine reliquary for public surfaces that are real enough to need identity and receipts, but not yet stable enough to deserve a first-class enum, actuator, or UI. Do not put provider bearer tokens in `--heimdall-capability-ref`; that value is the Heimdall-issued account/capability reference or a verifiable fingerprint-shaped reference.
## Discord Native Interface Target
Discord should become a native Bifrost interface for Bifrost-scoped work, not just an output pipe.

View file

@ -183,6 +183,70 @@ console.log(JSON.stringify({
Assert.Contains("BIFROST_BRIDGE_BASE_URL and BIFROST_BRIDGE_TOKEN", result.Stderr, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task Other_bridge_request_fails_closed_without_bifrost_receipt_gate()
{
var result = await RunNodeAsync([
"tools/bifrost-bridge.mjs",
"other-request",
"--identity", "epiphany.Persona",
"--surface-name", "future-surface",
"--target-locator", "future://surface/thread/1",
"--content", "test message",
"--dry-run", "true",
]);
Assert.NotEqual(0, result.ExitCode);
Assert.Contains("BIFROST_BRIDGE_BASE_URL and BIFROST_BRIDGE_TOKEN", result.Stderr, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task Other_bridge_request_allows_explicit_operator_recovery_dry_run()
{
var result = await RunNodeAsync([
"tools/bifrost-bridge.mjs",
"other-request",
"--identity", "epiphany.Persona",
"--surface-name", "future-surface",
"--target-locator", "future://surface/thread/1",
"--content", "test message",
"--heimdall-capability-ref", "heimdall:future-surface:capability:epiphany-persona",
"--allow-unreceipted-activity", "true",
"--dry-run", "true",
]);
Assert.True(result.ExitCode == 0, $"stdout:{Environment.NewLine}{result.Stdout}{Environment.NewLine}stderr:{Environment.NewLine}{result.Stderr}");
using var payload = JsonDocument.Parse(result.Stdout);
Assert.Equal("other-request", payload.RootElement.GetProperty("action").GetString());
Assert.True(payload.RootElement.GetProperty("dryRun").GetBoolean());
Assert.Equal("future-surface", payload.RootElement.GetProperty("surfaceName").GetString());
Assert.Equal("future://surface/thread/1", payload.RootElement.GetProperty("targetLocator").GetString());
var provenance = payload.RootElement.GetProperty("provenance");
Assert.Equal("epiphany.Persona", provenance.GetProperty("bifrostIdentity").GetString());
Assert.Equal("heimdall:future-surface:capability:epiphany-persona", provenance.GetProperty("heimdallCapabilityRef").GetString());
}
[Fact]
public async Task Other_bridge_request_recovery_hatch_is_rejected_inside_dispatched_turn()
{
var result = await RunNodeAsync([
"tools/bifrost-bridge.mjs",
"other-request",
"--identity", "epiphany.Persona",
"--surface-name", "future-surface",
"--target-locator", "future://surface/thread/1",
"--content", "test message",
"--allow-unreceipted-activity", "true",
"--dry-run", "true",
], new Dictionary<string, string?>
{
["BIFROST_LOCK_RECOVERY_HATCHES"] = "true",
});
Assert.NotEqual(0, result.ExitCode);
Assert.Contains("cannot use --allow-unreceipted-activity", result.Stderr, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task Discord_dm_bridge_command_fails_closed_without_bifrost_receipt_gate()
{

View file

@ -38,6 +38,9 @@ async function main() {
case "reddit-post":
await postRedditThread(options);
return;
case "other-request":
await requestOtherBridgeAction(options);
return;
default:
throw new Error(`Unknown command "${command}". Run "node tools/bifrost-bridge.mjs help".`);
}
@ -503,6 +506,69 @@ async function postRedditThread(options) {
});
}
async function requestOtherBridgeAction(options) {
ensureBridgeReceiptGate(options);
const identity = slugify(requireOption(options, "identity"));
const targetLocator = requireOption(options, "target-locator");
const surfaceName = optionalString(options["surface-name"]) ?? "future-surface";
const content = await readOptionText(options, "content", "content-file", "");
const title = optionalString(options.title) ?? `Outside-world bridge request for ${surfaceName}`;
const dryRun = options["dry-run"] === "true";
if (dryRun) {
printJson({
dryRun: true,
action: "other-request",
identity,
surfaceName,
targetLocator,
title,
content,
provenance: buildBridgeProvenance(options),
});
return;
}
const bridgeAction = await beginBridgeAction(options, {
actorKind: "Agent",
actorName: identity,
targetSurface: "Other",
actionKind: "Other",
targetRepositoryFullName: options["target-repository-full-name"] ?? "",
targetLocator,
title,
summary: content,
});
try {
await bridgeAction?.start();
await bridgeAction?.complete({
receiptUrl: `bifrost://bridge/actions/${bridgeAction.id}`,
externalReceiptId: bridgeAction.id,
receiptPayload: JSON.stringify(buildReceiptPayload(options, {
surfaceName,
targetLocator,
title,
})),
});
} catch (error) {
await bridgeAction?.fail(error);
throw error;
}
printJson({
action: "other-request",
ok: true,
bridgeActionId: bridgeAction.id,
receiptUrl: `bifrost://bridge/actions/${bridgeAction.id}`,
identity,
surfaceName,
targetLocator,
title,
provenance: buildBridgeProvenance(options),
});
}
async function getRedditAccessToken() {
const clientId = optionalString(process.env.BIFROST_REDDIT_CLIENT_ID ?? process.env.REDDIT_CLIENT_ID);
const clientSecret = process.env.BIFROST_REDDIT_CLIENT_SECRET ?? process.env.REDDIT_CLIENT_SECRET ?? "";
@ -1302,6 +1368,7 @@ Commands:
discord-post Post a message to Discord through the bot token or persona webhook pipe
discord-dm Send a Discord DM through Bifrost's bridge-owned bot token
reddit-post Create a self-post in r/GameCultOrg through the Bifrost Reddit app
other-request Record a Bifrost-gated request for a future outside-world surface
Examples:
node tools/bifrost-bridge.mjs github-draft-pr --repo-root E:/Projects/AetheriaLore --identity nibu --title "Nibu: Glitchcraft" --path Aetheria/Articles/Nibu/glitchcraft.md --content-file article.md
@ -1309,6 +1376,7 @@ Examples:
node tools/bifrost-bridge.mjs discord-post --channel-id 1501196543150264332 --persona-name Nibu --content "Draft PR opened: https://github.com/..."
node tools/bifrost-bridge.mjs discord-dm --recipient-id 123456789 --content "Moderation status update..."
node tools/bifrost-bridge.mjs reddit-post --title "Nibu: Reset-loop continuity" --persona-name Nibu --content-file thread.md
node tools/bifrost-bridge.mjs other-request --identity epiphany.Persona --surface-name bluesky --target-locator at://did:example/app.bsky.feed.post/123 --heimdall-capability-ref heimdall:bluesky:capability:epiphany-persona --content "Persona asks to speak."
GitHub note:
GitHub actions require BIFROST_BRIDGE_BASE_URL and BIFROST_BRIDGE_TOKEN so Bifrost can gate and receipt the crossing.