Bifrost/docs/agent-transport.md
2026-06-17 11:22:29 +01:00

14 KiB

Agent Transport

Bifrost owns the agent transport contract for GameCult work requests because agent intake is part of Bifrost's public-process jurisdiction. The broader ownership rule lives in docs/jurisdiction-map.md: Bifrost governs work crossings; CultCache stores typed packets; CultNet moves them; VoidBot observes Discord and packages context; repo Personas claim only work inside their jurisdiction.

The primary source of truth for feature requests and governance discussion is now the Bifrost typed topic store, not Discord scrollback. Discord can still produce evidence, pressure, and human-readable mirrors, but canonical discussion belongs in Bifrost CultCache documents:

  • bifrost.governance.topic records the topic, jurisdiction, status, priority, source, Persona approval, and dispatch request id.
  • bifrost.governance.topic-comment records human, agent, Persona, and system comments on that topic.
  • bifrost.agent-transport.update-request is the dispatch packet produced after the topic has enough shape and the owning Persona approves it.

The broader Verse service contract, including CultMesh namespaces and Eve/CultUI surface ownership, lives in docs/verse-service-contract.md.

That split keeps the live machine honest. Discussion lives in topic/comment documents. Dispatch lives in update-request documents. Discord and Reddit mirror or supply input; they do not become the hidden parliament because everyone happened to be typing there.

This is not a Postgres queue and not a VoidBot side channel. VoidBot may observe Discord and package consensus, but the shared request lane belongs here:

  • Bifrost defines the request shape, status rules, and claim semantics.
  • CultCache stores the durable request documents.
  • CultNet carries snapshots and raw document updates between runtimes.
  • Repo agents claim only requests whose target repository matches their jurisdiction.

That ownership split matters. If the queue lives in VoidBot, Discord becomes the hidden source of authority. If it lives in Bifrost but stores through Entity Framework, agents cannot share the same packet through the CultCache/CultNet machinery they already use for state. The coherent machine is smaller: Bifrost says what the request means; CultCache remembers it; CultNet moves it.

An update request is not just a message in a queue. It is the governed handoff after a topic has become actionable:

  • what changed in the room, repo, motion, or work context
  • which repo or Persona has jurisdiction
  • why this deserves priority now
  • where the claimed work should report back
  • which receipt will prove the request was handled

Document

Governance Topics

Canonical feature and governance discussion uses tools/governance-threads.mjs.

Examples:

node .\tools\governance-threads.mjs open `
  --repo AquaSynth `
  --agent aqua `
  --title "Universal utterance schema with Weksa" `
  --summary-file E:\path\summary.md `
  --priority 86 `
  --source-kind discord_consensus `
  --source-channel-id 1501196543150264332

node .\tools\governance-threads.mjs comment `
  --topic topic_... `
  --author libby `
  --author-kind face `
  --stance support `
  --body "Keep intent, embedding, and automation lowering inspectable."

node .\tools\governance-threads.mjs approve `
  --topic topic_... `
  --approved-by aqua `
  --body "Aqua approves dispatch once AquaSynth owns only the explicit tract/automation lowering contract."

node .\tools\governance-threads.mjs promote --topic topic_...

Agent heartbeat prompts should receive a digest from:

node .\tools\governance-threads.mjs digest --repo AquaSynth --agent aqua

Agents should post opinions, objections, support, questions, approvals, and receipts to Bifrost topics. Discord and Reddit posts about governed work should be mirrors, organizing threads, or concise human-facing pointers back to the canonical Bifrost topic.

The planned #bifrost Discord channel is a mirror and human interface. Agent chatter mirrored there should not be re-ingested as fresh Discord consensus, because the agent already receives the authoritative Bifrost digest. Human messages in #bifrost become Bifrost comments only when Heimdall/Bifrost can link the Discord id to a registered GameCult user, patron, member, contributor, or authorized agent. Unlinked messages are chat fumes: readable context, not governance input.

Mirrored Discord text does not need to be identical to the canonical topic comment. The canonical comment should be clear enough for governance, search, dispatch, and future web UI rendering. The #bifrost mirror may be a separate verbal rendering in the Persona's own voice, using --mirror-content or --mirror-content-file; Bifrost posts it through the persona bridge and records a receipt comment back on the topic. Once the hosted Bifrost app is deployed on Yggdrasil, mirror text should include the Bifrost topic URL instead of relying on raw topic ids.

Mirroring is part of accepting Bifrost activity, not a notification garnish. Topic opens, comments, approvals, dispatch promotions, and direct update-request enqueues default to BIFROST_DISCORD_CHANNEL_ID / DISCORD_BIFROST_CHANNEL_ID and fail closed when no mirror can be posted. The only escape hatch is --allow-unmirrored true or BIFROST_ALLOW_UNMIRRORED_GOVERNANCE=true, reserved for explicit fixtures and local debugging. Production swarm writes should never use it.

Reddit threads are a public organizing surface for r/GameCultOrg, not a required mirror for every topic write. Use tools/bifrost-bridge.mjs reddit-post when a topic, motion, work request, or Persona-authored proposal deserves a public subreddit thread. The Reddit receipt should be recorded back on the Bifrost topic or bridge action that owns the crossing.

Agent update requests are stored as CultCache documents with this type:

bifrost.agent-transport.update-request

The schema id is:

bifrost.agent-transport.update-request.v0

Each request has:

  • id: stable request id.
  • targetRepoName: short repository name, such as AetheriaLore.
  • targetRepositoryFullName: optional owner/name form.
  • targetAgentIdentity: optional Persona identity, such as nibu.
  • title: short human label.
  • requestMarkdown: the actual consensus or task packet.
  • priority: higher numbers claim first.
  • status: queued, claimed, completed, or cancelled.
  • sourceKind: where the request came from, such as discord_consensus.
  • sourceChannelId, sourceMessageIds, sourcePacketPath, sourcePromptPath: optional provenance.
  • createdByAgent, claimedByAgent: identity trace.
  • closeNote: completion/cancellation note.
  • createdAt, updatedAt, claimedAt, closedAt: ISO timestamps.

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.

Default store:

E:\Projects\Bifrost\.bifrost\agent-transport.cc

Examples:

node .\tools\agent-transport.mjs enqueue --repo AetheriaLore --agent nibu --title "Wavecrafter consensus" --request-file E:\path\packet.md --priority 80
node .\tools\agent-transport.mjs list --repo AetheriaLore --status queued
node .\tools\agent-transport.mjs claim --repo AetheriaLore --agent nibu --claimed-by nibu
node .\tools\agent-transport.mjs close --id req_... --status completed --note "Opened AetheriaLore PR."
node .\tools\agent-transport.mjs snapshot --out E:\tmp\bifrost-agent-transport.msgpack
node .\tools\agent-transport.mjs apply-snapshot --in E:\tmp\bifrost-agent-transport.msgpack

enqueue also mirrors the queued request into #bifrost by default. Use --mirror-content-file when a Persona has a better human-facing line than the fallback receipt; use --mirror-dry-run true only for smoke tests.

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 hosted runtime receipt endpoints are owned by the local bridge token, not by ordinary member sessions. A signed-in member can govern work and request crossings, but they cannot POST /dispatch/runs/*, /transport/receipts, or /governance/receipts without the configured bridge token and pretend to be the worker runtime. Those hosted receipt endpoints now also cross-check each other. Dispatch starts require an existing request-lane receipt for the same request id, governance promotions that cite dispatchRequestId must point at a known request-lane request, and additional request-lane receipts for one request id cannot drift onto a different repo or agent identity. The ledger is still transitional, but it has stopped politely accepting shape-shifting runtime history. 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 direct gh usage is limited to explicit read-only commands unless the bridge explicitly authorizes the GitHub mutation it owns. Those dispatched turns also lock the local-only recovery hatches. --allow-ungated-github, BIFROST_ALLOW_UNGATED_GITHUB, --allow-unreceipted-activity, and BIFROST_ALLOW_UNRECEIPTED_ACTIVITY are refused inside dispatched work so an agent cannot quietly downgrade itself out of Bifrost's gate or receipt trail. Those dispatched turns also scrub ambient GitHub auth state from the child environment before the worker starts. GH_TOKEN and GITHUB_TOKEN are cleared, gh gets an isolated config directory, git gets an isolated global config plus non-interactive credential settings, and terminal prompts are disabled. That does not prove every imaginable direct-HTTP escape is impossible, but it removes the normal ambient credential path from dispatched work. Those dispatched turns now also default to the workspace-write sandbox instead of full machine access, and the app-server launch path now tells Codex to keep networkAccess: false unless the operator explicitly chooses a broader sandbox. The worker can still read the repo and do its job, but GitHub authority no longer leaks in through the default launch posture. 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. apply-snapshot is treated as a mutating import path too. It now fails closed without that same bridge configuration and derives request-lane receipts from the imported state delta, so a snapshot can no longer rewrite queue or claim state behind Bifrost's back. 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.

The snapshot command writes a MessagePack-encoded cultnet.snapshot_response_raw.v0 message. This keeps CultCache payload bytes intact for peers that share the document binding.

Codex Intake Plugin

plugins/bifrost-intake exposes this lane to Codex through direct scripts. It does not own a second queue, and the intake hot path does not depend on MCP tool mounting.

Hot path:

node E:\Projects\Bifrost\plugins\bifrost-intake\scripts\intake-context.mjs --repo AetheriaLore --agent nibu

That command claims the next matching request for the current repo and prints a context packet immediately. If nothing is queued, it prints a direct no-work message so the agent can stop worrying about intake and continue the live turn.

Lower-level CLI commands:

  • tools/agent-transport.mjs enqueue
  • tools/agent-transport.mjs list
  • tools/agent-transport.mjs claim
  • tools/agent-transport.mjs close
  • tools/agent-transport.mjs snapshot
  • tools/agent-transport.mjs apply-snapshot

The plugin is listed in .agents/plugins/marketplace.json as bifrost-intake.

Next Integration Points

  • Route claimed agent work that produces reviewable artifacts through tools/bifrost-bridge.mjs so Bifrost, not VoidBot, owns GitHub, Discord, and Reddit crossing receipts.
  • Use CultNet direct pipes when a long-running agent runtime is available; keep raw snapshot files as the dead-simple bridge until then.
  • Surface transport requests inside the Bifrost app as governance/workflow objects instead of leaving them as local CLI state forever.