Add generic outside-world bridge request
This commit is contained in:
parent
675c475752
commit
f1f91713ad
3 changed files with 147 additions and 0 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue