Advertise current bridge capabilities
This commit is contained in:
parent
72ffa0d9f7
commit
718ae94caa
3 changed files with 218 additions and 11 deletions
|
|
@ -20,7 +20,7 @@ This repository now includes a viable alpha foundation slice of the ASP.NET Core
|
|||
- GitHub App webhook ingestion for issue, pull request, and review sync
|
||||
- Heimdall-signed external patron support intake for Patreon and PayPal support facts
|
||||
- Eve Motion Verse surface and command endpoint for governance participation, with Razor motion forms demoted to a transitional browser lowering
|
||||
- local bridge tooling for agent-owned GitHub draft PRs and Discord posts
|
||||
- local bridge tooling for agent-owned GitHub draft PRs and PR comments, Discord posts and DMs, Reddit self-posts, and receipt-only future-surface requests
|
||||
- local bridge tooling for Reddit self-posts in `r/GameCultOrg`, including Persona flair labels through the Bifrost Reddit app
|
||||
- CultCache-backed governance topic threads for feature requests, discussion comments, Persona approvals, and dispatch promotion
|
||||
- CultCache/CultNet-backed agent intake tooling for repo Persona update requests
|
||||
|
|
@ -75,14 +75,14 @@ For the full staged roadmap, including contribution points, revenue share, patro
|
|||
- POST `/auth/bifrost/register` with `identity`, optional `displayName`, and optional `returnUrl` to create a Bifrost-native account without OAuth; this signs in as `Authenticated` but does not grant active member access
|
||||
- set `Bootstrap:AdminGitHubLogins` with at least one GitHub login for the first active admin path
|
||||
- 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
|
||||
- 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, or before recording a receipt-only future-surface request
|
||||
- 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
|
||||
- those hosted runtime receipt lanes are local-bridge-owned writes, not ordinary member-session writes: `/dispatch/runs/*`, `/transport/receipts`, and `/governance/receipts` now require the configured bridge token and do not inherit browser-user identity into agent activity rows
|
||||
- those hosted runtime receipts also validate linkage instead of accepting free-floating text: dispatch runs require an existing request-lane receipt for the same request id, governance promotion receipts require a known promoted dispatch request, and request-lane rows cannot silently drift to a different repo or agent identity midstream
|
||||
- GitHub bridge actions that claim Bifrost-owned provenance now validate that provenance too: a `bifrost_agent_transport_request` source must point at a known request-lane request for the same repo, and governance-backed bridge actions must point at a known Bifrost topic instead of a decorative source string
|
||||
- governance-backed GitHub publication is also no longer allowed to jump the queue: a Bifrost governance topic must have been approved or promoted before it can justify a GitHub bridge action
|
||||
- non-GitHub bridge activity is no longer allowed to drift off-book either: `discord-post`, `discord-dm`, and `reddit-post` now require `BIFROST_BRIDGE_BASE_URL` and `BIFROST_BRIDGE_TOKEN` by default, with `--allow-unreceipted-activity true` reserved for explicit local recovery and refused inside dispatched work
|
||||
- Persona and agent `discord-post` / `reddit-post` bridge actions also require a Bifrost identity plus Heimdall-backed capability/account reference in the hosted ledger; pass `--identity`, `--heimdall-capability-ref`, and the Epiphany provenance flags or set the matching environment variables. The bridge token is an actuator credential, not identity or provider authority.
|
||||
- non-GitHub bridge activity is no longer allowed to drift off-book either: `discord-post`, `discord-dm`, `reddit-post`, and `other-request` now require `BIFROST_BRIDGE_BASE_URL` and `BIFROST_BRIDGE_TOKEN` by default, with `--allow-unreceipted-activity true` reserved for explicit local recovery and refused inside dispatched work
|
||||
- Persona and agent `discord-post`, `reddit-post`, and `other-request` bridge actions also require a Bifrost identity plus Heimdall-backed capability/account reference in the hosted ledger; pass `--identity`, `--heimdall-capability-ref`, and the Epiphany provenance flags or set the matching environment variables. The bridge token is an actuator credential, not identity or provider authority.
|
||||
- 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` stays blocked, and direct `gh` usage is limited to explicit read-only commands unless `tools/bifrost-bridge.mjs` authorizes the bridge-owned GitHub write path
|
||||
- dispatched Codex turns now default to `workspace-write` instead of full machine access, and the app-server sandbox policy disables network access unless the operator explicitly chooses a broader sandbox
|
||||
- `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
|
||||
|
|
|
|||
147
tests/Bifrost.Web.Tests/ProviderAdvertisementTests.cs
Normal file
147
tests/Bifrost.Web.Tests/ProviderAdvertisementTests.cs
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Bifrost.Web.Tests;
|
||||
|
||||
public sealed class ProviderAdvertisementTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Provider_advertisement_names_current_bridge_and_patron_contracts()
|
||||
{
|
||||
var result = await RunNodeAsync([
|
||||
"tools/provider-advertisement.mjs",
|
||||
"print",
|
||||
"--generated-at",
|
||||
"2026-06-22T00:00:00.000Z",
|
||||
]);
|
||||
|
||||
Assert.True(result.ExitCode == 0, $"stdout:{Environment.NewLine}{result.Stdout}{Environment.NewLine}stderr:{Environment.NewLine}{result.Stderr}");
|
||||
using var payload = JsonDocument.Parse(result.Stdout);
|
||||
var root = payload.RootElement;
|
||||
|
||||
AssertEndpoint(root, "bridge-action-ledger", "bifrost.bridge_action.v0", "bridge");
|
||||
AssertEndpoint(root, "patron-support-intake", "bifrost.patron_support_event.v0", "patronage");
|
||||
AssertEndpoint(root, "github-webhooks", "bifrost.work_item.v0", "github");
|
||||
AssertSchemaPurpose(root, "bifrost.bridge_action.v0", "current hosted governed crossing command witness");
|
||||
AssertSchemaPurpose(root, "bifrost.bridge_receipt.v0", "current hosted governed crossing result witness");
|
||||
AssertSchemaPurpose(root, "bifrost.patron_support_event.v0", "current hosted Heimdall-signed patron support fact consumed by Bifrost");
|
||||
AssertBoundaryCommand(root, "bridge", "open GitHub draft PRs and PR comments through Bifrost gate");
|
||||
AssertBoundaryCommand(root, "bridge", "record receipt-only future-surface requests before named actuators exist");
|
||||
AssertBoundaryCommand(root, "patron", "consume Heimdall-signed Patreon and PayPal support facts");
|
||||
AssertBoundaryForbiddenAuthority(root, "patron", "does not store Patreon or PayPal provider tokens");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Interface_binding_exposes_current_agent_bridge_capabilities()
|
||||
{
|
||||
var result = await RunNodeAsync([
|
||||
"tools/provider-advertisement.mjs",
|
||||
"print-binding",
|
||||
"--generated-at",
|
||||
"2026-06-22T00:00:00.000Z",
|
||||
]);
|
||||
|
||||
Assert.True(result.ExitCode == 0, $"stdout:{Environment.NewLine}{result.Stdout}{Environment.NewLine}stderr:{Environment.NewLine}{result.Stderr}");
|
||||
using var payload = JsonDocument.Parse(result.Stdout);
|
||||
var root = payload.RootElement;
|
||||
var provider = root.GetProperty("provider");
|
||||
|
||||
AssertArrayContains(provider.GetProperty("capabilities"), "github-bridge");
|
||||
AssertArrayContains(provider.GetProperty("capabilities"), "github-work-sync");
|
||||
AssertArrayContains(provider.GetProperty("capabilities"), "future-surface-bridge");
|
||||
AssertArrayContains(provider.GetProperty("capabilities"), "heimdall-patron-support-intake");
|
||||
|
||||
var stats = root.GetProperty("surface").GetProperty("root").GetProperty("props");
|
||||
Assert.Equal("2026-06-22T00:00:00.000Z", stats.GetProperty("generatedAt").GetString());
|
||||
|
||||
var bridgeMetric = root
|
||||
.GetProperty("surface")
|
||||
.GetProperty("root")
|
||||
.GetProperty("children")
|
||||
.EnumerateArray()
|
||||
.SelectMany(panel => panel.GetProperty("children").EnumerateArray())
|
||||
.Single(node => node.GetProperty("id").GetString() == "metric-bridge");
|
||||
|
||||
var bridgeLine = bridgeMetric.GetProperty("props").GetProperty("value").GetString();
|
||||
Assert.Contains("GitHub yes", bridgeLine, StringComparison.Ordinal);
|
||||
Assert.Contains("Other yes", bridgeLine, StringComparison.Ordinal);
|
||||
Assert.Contains("Patron yes", bridgeLine, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static void AssertEndpoint(JsonElement root, string id, string schemaId, string lowering)
|
||||
{
|
||||
var endpoint = root.GetProperty("endpoints")
|
||||
.EnumerateArray()
|
||||
.Single(item => item.GetProperty("id").GetString() == id);
|
||||
Assert.Equal(schemaId, endpoint.GetProperty("schemaId").GetString());
|
||||
AssertArrayContains(endpoint.GetProperty("lowerings"), lowering);
|
||||
}
|
||||
|
||||
private static void AssertSchemaPurpose(JsonElement root, string id, string purpose)
|
||||
{
|
||||
var schema = root.GetProperty("schemas")
|
||||
.EnumerateArray()
|
||||
.Single(item => item.GetProperty("id").GetString() == id);
|
||||
Assert.Equal(purpose, schema.GetProperty("purpose").GetString());
|
||||
}
|
||||
|
||||
private static void AssertBoundaryCommand(JsonElement root, string area, string command)
|
||||
{
|
||||
var boundary = Boundary(root, area);
|
||||
AssertArrayContains(boundary.GetProperty("commands"), command);
|
||||
}
|
||||
|
||||
private static void AssertBoundaryForbiddenAuthority(JsonElement root, string area, string forbiddenAuthority)
|
||||
{
|
||||
var boundary = Boundary(root, area);
|
||||
AssertArrayContains(boundary.GetProperty("forbiddenAuthority"), forbiddenAuthority);
|
||||
}
|
||||
|
||||
private static JsonElement Boundary(JsonElement root, string area)
|
||||
=> root.GetProperty("commandBoundaries")
|
||||
.EnumerateArray()
|
||||
.Single(item => item.GetProperty("area").GetString() == area);
|
||||
|
||||
private static void AssertArrayContains(JsonElement array, string expected)
|
||||
{
|
||||
Assert.Contains(array.EnumerateArray(), item => item.GetString() == expected);
|
||||
}
|
||||
|
||||
private static string RepoRoot =>
|
||||
Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", ".."));
|
||||
|
||||
private static async Task<ProcessResult> RunNodeAsync(string[] args)
|
||||
{
|
||||
using var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "node",
|
||||
WorkingDirectory = RepoRoot,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var arg in args)
|
||||
{
|
||||
process.StartInfo.ArgumentList.Add(arg);
|
||||
}
|
||||
|
||||
process.StartInfo.Environment["BIFROST_BRIDGE_BASE_URL"] = string.Empty;
|
||||
process.StartInfo.Environment["BIFROST_BRIDGE_TOKEN"] = string.Empty;
|
||||
process.StartInfo.Environment["BIFROST_SKIP_LOCAL_ENV"] = "true";
|
||||
process.StartInfo.Environment["BIFROST_REDDIT_CLIENT_ID"] = string.Empty;
|
||||
process.StartInfo.Environment["BIFROST_REDDIT_REFRESH_TOKEN"] = string.Empty;
|
||||
|
||||
process.Start();
|
||||
var stdout = await process.StandardOutput.ReadToEndAsync();
|
||||
var stderr = await process.StandardError.ReadToEndAsync();
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
return new ProcessResult(process.ExitCode, stdout, stderr);
|
||||
}
|
||||
|
||||
private sealed record ProcessResult(int ExitCode, string Stdout, string Stderr);
|
||||
}
|
||||
|
|
@ -144,6 +144,15 @@ async function main() {
|
|||
case "print":
|
||||
printJson(buildAdvertisement(options));
|
||||
return;
|
||||
case "print-surface":
|
||||
printJson(buildOperatorSurface(await collectStats(options)));
|
||||
return;
|
||||
case "print-binding":
|
||||
{
|
||||
const stats = await collectStats(options);
|
||||
printJson(buildInterfaceBinding(buildOperatorSurface(stats), stats));
|
||||
}
|
||||
return;
|
||||
case "schema":
|
||||
printJson({
|
||||
documentType,
|
||||
|
|
@ -204,6 +213,9 @@ function buildAdvertisement(options) {
|
|||
endpoint("operator-tui", `${locatedService}/eve/tui`, "gamecult.eve.surface.v1", ["tui", "nightwing-tui"]),
|
||||
endpoint("operator-gui", `${locatedService}/eve/gui`, "gamecult.eve.surface.v1", ["gui", "browser", "eve-native"]),
|
||||
endpoint("operator-commands", `${locatedService}/commands`, "bifrost.bridge_action.v0", ["command"]),
|
||||
endpoint("bridge-action-ledger", `${locatedService}/bridge/actions`, "bifrost.bridge_action.v0", ["command", "bridge", "receipt"]),
|
||||
endpoint("patron-support-intake", `${locatedService}/heimdall/patron-support/events`, "bifrost.patron_support_event.v0", ["heimdall", "patronage", "intake"]),
|
||||
endpoint("github-webhooks", `${locatedService}/github/webhooks`, "bifrost.work_item.v0", ["github", "work-sync", "review-sync"]),
|
||||
endpoint("motion-surface", "https://bifrost.gamecult.org/eve/governance/surface", "gamecult.eve.surface.v1", ["product", "governance", "motion"]),
|
||||
endpoint("motion-commands", "https://bifrost.gamecult.org/eve/governance/commands", "bifrost.motion_command.v0", ["command", "governance", "motion"]),
|
||||
],
|
||||
|
|
@ -239,8 +251,9 @@ function buildAdvertisement(options) {
|
|||
schema("bifrost.vote.v0", "bifrost.vote", "motion vote witness"),
|
||||
schema("bifrost.motion_command.v0", "bifrost.motion-command", "Eve Motion Verse command envelope for create, vote, and close"),
|
||||
schema("bifrost.ledger_entry.v0", "bifrost.ledger-entry", "planned contributor/patron ledger witness"),
|
||||
schema("bifrost.bridge_action.v0", "bifrost.bridge-action", "planned governed crossing command witness"),
|
||||
schema("bifrost.bridge_receipt.v0", "bifrost.bridge-receipt", "planned governed crossing result witness"),
|
||||
schema("bifrost.bridge_action.v0", "bifrost.bridge-action", "current hosted governed crossing command witness"),
|
||||
schema("bifrost.bridge_receipt.v0", "bifrost.bridge-receipt", "current hosted governed crossing result witness"),
|
||||
schema("bifrost.patron_support_event.v0", "bifrost.patron-support-event", "current hosted Heimdall-signed patron support fact consumed by Bifrost"),
|
||||
schema("bifrost.member_capability_snapshot.v0", "bifrost.member-capability-snapshot", "planned Heimdall-consumed membership capability witness"),
|
||||
],
|
||||
witnesses: [
|
||||
|
|
@ -324,10 +337,12 @@ function buildAdvertisement(options) {
|
|||
]),
|
||||
boundary("patron", "Bifrost", [
|
||||
"record patron pressure",
|
||||
"consume Heimdall-signed Patreon and PayPal support facts",
|
||||
"surface reward influence",
|
||||
"emit standing receipts",
|
||||
], [
|
||||
"does not charge cards",
|
||||
"does not store Patreon or PayPal provider tokens",
|
||||
"does not execute external payout rails",
|
||||
]),
|
||||
boundary("project", "Bifrost plus project maintainers", [
|
||||
|
|
@ -349,6 +364,8 @@ function buildAdvertisement(options) {
|
|||
"prepare governed public crossings",
|
||||
"execute approved handoffs",
|
||||
"record receipts",
|
||||
"open GitHub draft PRs and PR comments through Bifrost gate",
|
||||
"record receipt-only future-surface requests before named actuators exist",
|
||||
"post Persona-flaired Reddit organizing threads",
|
||||
], [
|
||||
"does not treat local protocol JSON as work authority",
|
||||
|
|
@ -426,8 +443,14 @@ async function collectStats(options) {
|
|||
redditPost: Boolean(process.env.BIFROST_REDDIT_CLIENT_ID && process.env.BIFROST_REDDIT_REFRESH_TOKEN),
|
||||
githubDraftPr: true,
|
||||
githubPrComment: true,
|
||||
githubWebhookSync: true,
|
||||
otherRequest: true,
|
||||
patronSupportIntake: true,
|
||||
bridgeLedgerConfigured: Boolean(process.env.BIFROST_BRIDGE_BASE_URL && process.env.BIFROST_BRIDGE_TOKEN),
|
||||
credentialSource: process.env.BIFROST_DISCORD_BOT_TOKEN ? "BIFROST_DISCORD_BOT_TOKEN" : process.env.DISCORD_BOT_TOKEN ? "DISCORD_BOT_TOKEN-fallback" : "missing",
|
||||
redditCredentialSource: process.env.BIFROST_REDDIT_CLIENT_ID && process.env.BIFROST_REDDIT_REFRESH_TOKEN ? "BIFROST_REDDIT_CLIENT_ID+BIFROST_REDDIT_REFRESH_TOKEN" : "missing",
|
||||
bridgeLedgerCredentialSource: process.env.BIFROST_BRIDGE_BASE_URL && process.env.BIFROST_BRIDGE_TOKEN ? "BIFROST_BRIDGE_BASE_URL+BIFROST_BRIDGE_TOKEN" : "missing",
|
||||
patronSupportAuthority: "Heimdall HMAC via /heimdall/patron-support/events",
|
||||
},
|
||||
summary: {
|
||||
status: health.ok && ready.ok ? "ready" : "degraded",
|
||||
|
|
@ -553,7 +576,12 @@ function buildOperatorSurface(stats) {
|
|||
metricNode("daemon", "Daemon", `health ${stats.summary.health} / ready ${stats.summary.ready}`, stats.health.ok && stats.ready.ok ? "ok" : "warn"),
|
||||
metricNode("containers", "Containers", `${stats.summary.dockerRunning} up / ${stats.summary.dockerHealthy} healthy`, stats.summary.dockerHealthy > 0 ? "ok" : "warn"),
|
||||
metricNode("stores", "Stores", witnessHealthLine(stats.witnesses), witnessHealthTone(stats.witnesses)),
|
||||
metricNode("bridge", "Bridge", `Discord post ${yesNo(stats.bridge.discordPost)} / DM ${yesNo(stats.bridge.discordDm)} / Reddit ${yesNo(stats.bridge.redditPost)}`, stats.bridge.redditPost ? "ok" : "warn"),
|
||||
metricNode(
|
||||
"bridge",
|
||||
"Bridge",
|
||||
`GitHub ${yesNo(stats.bridge.githubDraftPr && stats.bridge.githubPrComment)} / Discord ${yesNo(stats.bridge.discordPost)} / Reddit ${yesNo(stats.bridge.redditPost)} / Other ${yesNo(stats.bridge.otherRequest)} / Patron ${yesNo(stats.bridge.patronSupportIntake)}`,
|
||||
stats.bridge.githubDraftPr && stats.bridge.githubPrComment && stats.bridge.otherRequest && stats.bridge.patronSupportIntake ? "ok" : "warn",
|
||||
),
|
||||
]),
|
||||
panelNode("activity", "Activity", [
|
||||
metricNode("topics", "Topics", `${stats.summary.governanceTopics} total / ${stats.summary.openTopics} open / ${stats.summary.recentGovernanceTopics} in ${stats.summary.recentWindowHours}h`, stats.governance.ok ? "ok" : "warn"),
|
||||
|
|
@ -616,7 +644,20 @@ function buildInterfaceBinding(surface, stats) {
|
|||
route("cultcache-witness", ".bifrost/provider-advertisement.cc", "local-cultcache", true),
|
||||
route("websocket-bridge", "ws://192.168.1.66:8797/eve/deck", "compatibility-eve-deck", true),
|
||||
],
|
||||
capabilities: ["operator-stats", "bridge-health", "governance-counts", "agent-transport-counts", "motion-surface", "motion-commands"],
|
||||
capabilities: [
|
||||
"operator-stats",
|
||||
"bridge-health",
|
||||
"governance-counts",
|
||||
"agent-transport-counts",
|
||||
"motion-surface",
|
||||
"motion-commands",
|
||||
"github-bridge",
|
||||
"github-work-sync",
|
||||
"discord-bridge",
|
||||
"reddit-bridge",
|
||||
"future-surface-bridge",
|
||||
"heimdall-patron-support-intake",
|
||||
],
|
||||
usesCultMesh: true,
|
||||
status: stats.summary.status,
|
||||
transport: "CultMesh Eve interface binding.",
|
||||
|
|
@ -822,6 +863,22 @@ function parseAdvertisement(input) {
|
|||
}
|
||||
}
|
||||
|
||||
const requiredEndpoints = new Set(["bridge-action-ledger", "patron-support-intake", "github-webhooks"]);
|
||||
const endpointIds = new Set(advertisement.endpoints.map((endpoint) => endpoint.id));
|
||||
for (const id of requiredEndpoints) {
|
||||
if (!endpointIds.has(id)) {
|
||||
throw new Error(`Provider advertisement must name endpoint ${id}.`);
|
||||
}
|
||||
}
|
||||
|
||||
const requiredSchemas = new Set(["bifrost.bridge_action.v0", "bifrost.bridge_receipt.v0", "bifrost.patron_support_event.v0"]);
|
||||
const schemaIds = new Set(advertisement.schemas.map((schema) => schema.id));
|
||||
for (const id of requiredSchemas) {
|
||||
if (!schemaIds.has(id)) {
|
||||
throw new Error(`Provider advertisement must name schema ${id}.`);
|
||||
}
|
||||
}
|
||||
|
||||
return advertisement;
|
||||
}
|
||||
|
||||
|
|
@ -907,9 +964,11 @@ function printHelp() {
|
|||
process.stdout.write(`Bifrost Eve provider advertisement
|
||||
|
||||
Commands:
|
||||
export Write the Bifrost gamecult.eve.provider_advertisement.v1 document to a CultCache .cc witness
|
||||
print Print the advertisement as protocol-debug JSON without writing state
|
||||
schema Print document type metadata
|
||||
export Write advertisement, operator surface, and interface binding to a CultCache .cc witness
|
||||
print Print the advertisement as protocol-debug JSON without writing state
|
||||
print-surface Print the current operator Eve surface JSON without writing state
|
||||
print-binding Print the current Eve interface binding JSON without writing state
|
||||
schema Print document type metadata
|
||||
|
||||
Options:
|
||||
--out <path> Override export path; defaults to .bifrost/provider-advertisement.cc
|
||||
|
|
@ -917,6 +976,7 @@ Options:
|
|||
|
||||
Examples:
|
||||
node tools/provider-advertisement.mjs print
|
||||
node tools/provider-advertisement.mjs print-binding
|
||||
node tools/provider-advertisement.mjs export --out .bifrost/provider-advertisement.cc
|
||||
`);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue