Add Bifrost intake Codex plugin

This commit is contained in:
Meta Vens 2026-05-19 17:37:18 +01:00
commit 9258a218ac
6 changed files with 473 additions and 1 deletions

View file

@ -0,0 +1,20 @@
{
"name": "gamecult-local",
"interface": {
"displayName": "GameCult Local"
},
"plugins": [
{
"name": "bifrost-intake",
"source": {
"source": "local",
"path": "./plugins/bifrost-intake"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "Coding"
}
]
}

View file

@ -58,9 +58,24 @@ node .\tools\agent-transport.mjs apply-snapshot --in E:\tmp\bifrost-agent-transp
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. 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 as an MCP plugin. It does not own a second queue. Its server wraps `tools/agent-transport.mjs`.
Tools:
- `enqueue_update_request`
- `list_update_requests`
- `claim_update_request`
- `close_update_request`
- `format_claimed_request`
- `create_transport_snapshot`
- `apply_transport_snapshot`
The plugin is listed in `.agents/plugins/marketplace.json` as `bifrost-intake`.
## Next Integration Points ## Next Integration Points
- Add an MCP wrapper that calls this CLI instead of storing transport state in VoidBot.
- Teach the VoidBot consensus feeder to enqueue Bifrost update requests after it writes a packet. - Teach the VoidBot consensus feeder to enqueue Bifrost update requests after it writes a packet.
- Let repo Faces claim by `targetRepoName`, then inject the claimed packet into Codex only when the repo matches their jurisdiction. - Let repo Faces claim by `targetRepoName`, then inject the claimed packet into Codex only when the repo matches their jurisdiction.
- Use CultNet direct pipes when a long-running agent runtime is available; keep raw snapshot files as the dead-simple bridge until then. - Use CultNet direct pipes when a long-running agent runtime is available; keep raw snapshot files as the dead-simple bridge until then.

View file

@ -0,0 +1,43 @@
{
"name": "bifrost-intake",
"version": "0.1.0",
"description": "Codex intake tools for Bifrost agent update requests stored in CultCache and synced through CultNet.",
"author": {
"name": "GameCult",
"email": "ops@gamecult.org",
"url": "https://github.com/GameCult"
},
"homepage": "https://github.com/GameCult/Bifrost",
"repository": "https://github.com/GameCult/Bifrost",
"license": "MIT",
"keywords": [
"bifrost",
"agent-transport",
"cultcache",
"cultnet",
"codex"
],
"skills": "./skills/",
"mcpServers": "./.mcp.json",
"interface": {
"displayName": "Bifrost Intake",
"shortDescription": "Claim and route Bifrost agent update requests",
"longDescription": "Expose Bifrost's CultCache-backed, CultNet-syncable agent transport lane to Codex so repo agents can enqueue, list, claim, close, and package update requests.",
"developerName": "GameCult",
"category": "Coding",
"capabilities": [
"Interactive",
"Write"
],
"websiteURL": "https://github.com/GameCult/Bifrost",
"privacyPolicyURL": "https://github.com/GameCult/Bifrost",
"termsOfServiceURL": "https://github.com/GameCult/Bifrost",
"defaultPrompt": [
"Claim the next Bifrost update request for this repo.",
"Enqueue this consensus packet for a repo Face.",
"List queued Bifrost agent requests."
],
"brandColor": "#4F46E5",
"screenshots": []
}
}

View file

@ -0,0 +1,9 @@
{
"mcpServers": {
"bifrost-intake": {
"command": "node",
"args": ["./scripts/mcp-server.mjs"],
"note": "Local Bifrost intake MCP server. Wraps Bifrost's CultCache/CultNet agent transport CLI."
}
}
}

View file

@ -0,0 +1,366 @@
#!/usr/bin/env node
import { spawn } from "node:child_process";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const pluginRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const bifrostRoot = resolve(pluginRoot, "..", "..");
const transportCli = resolve(bifrostRoot, "tools", "agent-transport.mjs");
const serverInfo = { name: "bifrost-intake", version: "0.1.0" };
const tools = [
{
name: "enqueue_update_request",
description: "Enqueue a Bifrost agent update request in the CultCache-backed intake store.",
inputSchema: {
type: "object",
additionalProperties: false,
required: ["repo", "title", "requestMarkdown"],
properties: {
repo: { type: "string", description: "Target repository short name, such as AetheriaLore." },
agent: { type: "string", description: "Optional target Face identity, such as nibu." },
title: { type: "string" },
requestMarkdown: { type: "string", description: "Consensus/task packet to inject when claimed." },
priority: { type: "integer", default: 50 },
repoFullName: { type: "string" },
sourceKind: { type: "string", default: "codex" },
sourceChannelId: { type: "string" },
sourceMessageIds: { type: "array", items: { type: "string" } },
sourcePacketPath: { type: "string" },
sourcePromptPath: { type: "string" },
createdBy: { type: "string" },
store: { type: "string", description: "Optional override path for the .cc store." }
}
}
},
{
name: "list_update_requests",
description: "List Bifrost agent update requests, optionally filtered by repo, Face, or status.",
inputSchema: {
type: "object",
additionalProperties: false,
properties: {
repo: { type: "string" },
agent: { type: "string" },
status: { type: "string", enum: ["queued", "claimed", "completed", "cancelled"] },
store: { type: "string" }
}
}
},
{
name: "claim_update_request",
description: "Claim the highest-priority queued request for a repository jurisdiction.",
inputSchema: {
type: "object",
additionalProperties: false,
required: ["repo"],
properties: {
repo: { type: "string" },
agent: { type: "string" },
claimedBy: { type: "string" },
store: { type: "string" }
}
}
},
{
name: "close_update_request",
description: "Mark a Bifrost update request completed or cancelled.",
inputSchema: {
type: "object",
additionalProperties: false,
required: ["id", "status"],
properties: {
id: { type: "string" },
status: { type: "string", enum: ["completed", "cancelled"] },
note: { type: "string" },
store: { type: "string" }
}
}
},
{
name: "format_claimed_request",
description: "Return a Codex-ready prompt packet for a claimed update request id.",
inputSchema: {
type: "object",
additionalProperties: false,
required: ["id"],
properties: {
id: { type: "string" },
store: { type: "string" }
}
}
},
{
name: "create_transport_snapshot",
description: "Write a CultNet raw snapshot of the Bifrost intake store.",
inputSchema: {
type: "object",
additionalProperties: false,
required: ["out"],
properties: {
out: { type: "string" },
messageId: { type: "string" },
store: { type: "string" }
}
}
},
{
name: "apply_transport_snapshot",
description: "Apply a CultNet raw snapshot into the Bifrost intake store.",
inputSchema: {
type: "object",
additionalProperties: false,
required: ["in"],
properties: {
in: { type: "string" },
store: { type: "string" }
}
}
}
];
let inputBuffer = Buffer.alloc(0);
process.stdin.on("data", (chunk) => {
inputBuffer = Buffer.concat([inputBuffer, chunk]);
drainMessages().catch((error) => {
sendError(null, -32603, error.message);
});
});
async function drainMessages() {
while (true) {
const headerEnd = inputBuffer.indexOf("\r\n\r\n");
if (headerEnd === -1) {
return;
}
const header = inputBuffer.subarray(0, headerEnd).toString("utf8");
const lengthMatch = /^Content-Length:\s*(\d+)/imu.exec(header);
if (!lengthMatch) {
throw new Error("MCP message is missing Content-Length header.");
}
const length = Number(lengthMatch[1]);
const bodyStart = headerEnd + 4;
const bodyEnd = bodyStart + length;
if (inputBuffer.length < bodyEnd) {
return;
}
const rawBody = inputBuffer.subarray(bodyStart, bodyEnd).toString("utf8");
inputBuffer = inputBuffer.subarray(bodyEnd);
await handleMessage(JSON.parse(rawBody));
}
}
async function handleMessage(message) {
if (!Object.prototype.hasOwnProperty.call(message, "id")) {
return;
}
try {
switch (message.method) {
case "initialize":
sendResult(message.id, {
protocolVersion: message.params?.protocolVersion ?? "2024-11-05",
capabilities: { tools: {} },
serverInfo,
});
return;
case "tools/list":
sendResult(message.id, { tools });
return;
case "tools/call":
sendResult(message.id, await callTool(message.params ?? {}));
return;
case "ping":
sendResult(message.id, {});
return;
default:
sendError(message.id, -32601, `Unknown method "${message.method}".`);
}
} catch (error) {
sendError(message.id, -32603, error instanceof Error ? error.message : String(error));
}
}
async function callTool(params) {
const name = params.name;
const args = params.arguments ?? {};
switch (name) {
case "enqueue_update_request":
return jsonToolResult(await runTransport([
"enqueue",
"--repo", requireString(args.repo, "repo"),
...optionalArg("--agent", args.agent),
"--title", requireString(args.title, "title"),
"--request", requireString(args.requestMarkdown, "requestMarkdown"),
"--priority", String(args.priority ?? 50),
...optionalArg("--repo-full-name", args.repoFullName),
...optionalArg("--source-kind", args.sourceKind ?? "codex"),
...optionalArg("--source-channel-id", args.sourceChannelId),
...optionalArg("--source-message-ids", Array.isArray(args.sourceMessageIds) ? args.sourceMessageIds.join(",") : undefined),
...optionalArg("--packet-path", args.sourcePacketPath),
...optionalArg("--prompt-path", args.sourcePromptPath),
...optionalArg("--created-by", args.createdBy),
...storeArg(args.store),
]));
case "list_update_requests":
return jsonToolResult(await runTransport([
"list",
...optionalArg("--repo", args.repo),
...optionalArg("--agent", args.agent),
...optionalArg("--status", args.status),
...storeArg(args.store),
]));
case "claim_update_request":
return jsonToolResult(await runTransport([
"claim",
"--repo", requireString(args.repo, "repo"),
...optionalArg("--agent", args.agent),
...optionalArg("--claimed-by", args.claimedBy),
...storeArg(args.store),
]));
case "close_update_request":
return jsonToolResult(await runTransport([
"close",
"--id", requireString(args.id, "id"),
"--status", requireString(args.status, "status"),
...optionalArg("--note", args.note),
...storeArg(args.store),
]));
case "format_claimed_request":
return textToolResult(formatClaimedRequest(await requireRequestById(args)));
case "create_transport_snapshot":
return jsonToolResult(await runTransport([
"snapshot",
"--out", requireString(args.out, "out"),
...optionalArg("--message-id", args.messageId),
...storeArg(args.store),
]));
case "apply_transport_snapshot":
return jsonToolResult(await runTransport([
"apply-snapshot",
"--in", requireString(args.in, "in"),
...storeArg(args.store),
]));
default:
throw new Error(`Unknown tool "${name}".`);
}
}
async function requireRequestById(args) {
const id = requireString(args.id, "id");
const requests = await runTransport(["list", ...storeArg(args.store)]);
const request = requests.find((candidate) => candidate.id === id);
if (!request) {
throw new Error(`No Bifrost update request found for id "${id}".`);
}
return request;
}
function formatClaimedRequest(request) {
return `# Bifrost Update Request
ID: ${request.id}
Status: ${request.status}
Target repo: ${request.targetRepoName}
Target Face: ${request.targetAgentIdentity ?? "any"}
Priority: ${request.priority}
Title: ${request.title}
## Request
${request.requestMarkdown}
## Handling
Work only if this request matches the current repository jurisdiction. When finished, close the request through Bifrost intake and include the PR, commit, or cancellation reason in the close note.`;
}
function runTransport(args) {
return new Promise((resolvePromise, rejectPromise) => {
const child = spawn(process.execPath, [transportCli, ...args], {
cwd: bifrostRoot,
windowsHide: true,
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => {
stdout += chunk.toString("utf8");
});
child.stderr.on("data", (chunk) => {
stderr += chunk.toString("utf8");
});
child.on("error", rejectPromise);
child.on("close", (code) => {
if (code !== 0) {
rejectPromise(new Error(stderr.trim() || `agent-transport exited with code ${code}`));
return;
}
try {
resolvePromise(JSON.parse(stdout));
} catch (error) {
rejectPromise(new Error(`agent-transport returned non-JSON output: ${stdout || stderr}`));
}
});
});
}
function jsonToolResult(value) {
return {
content: [
{
type: "text",
text: JSON.stringify(value, null, 2),
},
],
};
}
function textToolResult(text) {
return {
content: [
{
type: "text",
text,
},
],
};
}
function optionalArg(flag, value) {
if (typeof value !== "string" || value.trim().length === 0) {
return [];
}
return [flag, value.trim()];
}
function storeArg(value) {
return optionalArg("--store", value);
}
function requireString(value, name) {
if (typeof value !== "string" || value.trim().length === 0) {
throw new Error(`Missing required argument "${name}".`);
}
return value.trim();
}
function sendResult(id, result) {
sendMessage({ jsonrpc: "2.0", id, result });
}
function sendError(id, code, message) {
sendMessage({ jsonrpc: "2.0", id, error: { code, message } });
}
function sendMessage(message) {
const body = Buffer.from(JSON.stringify(message), "utf8");
process.stdout.write(`Content-Length: ${body.length}\r\n\r\n`);
process.stdout.write(body);
}

View file

@ -0,0 +1,19 @@
---
name: bifrost-intake
description: Use when Codex should enqueue, list, claim, close, or package Bifrost agent update requests for repo Faces through the Bifrost intake MCP tools.
---
# Bifrost Intake
Bifrost owns agent update request semantics. CultCache stores the request documents. CultNet moves raw snapshots and document updates. This plugin exposes that lane to Codex through MCP tools.
Use these tools when a user asks to feed a consensus packet to a repo Face, claim work for the current repo, inspect queued update requests, or close a request after work is handled.
Default flow:
1. Use `list_update_requests` to inspect queued work for a repo or Face.
2. Use `claim_update_request` with the repo name before injecting the request into Codex.
3. Use `format_claimed_request` when you need a compact prompt packet for a claimed request.
4. Use `close_update_request` when the work is completed or cancelled.
Do not store a parallel queue in VoidBot or Codex session memory. If the request is intended to persist, put it in Bifrost intake.