From 9258a218ac13da5a24a4ca1c532f3655bcea9093 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 19 May 2026 17:37:18 +0100 Subject: [PATCH] Add Bifrost intake Codex plugin --- .agents/plugins/marketplace.json | 20 + docs/agent-transport.md | 17 +- .../bifrost-intake/.codex-plugin/plugin.json | 43 ++ plugins/bifrost-intake/.mcp.json | 9 + plugins/bifrost-intake/scripts/mcp-server.mjs | 366 ++++++++++++++++++ .../skills/bifrost-intake/SKILL.md | 19 + 6 files changed, 473 insertions(+), 1 deletion(-) create mode 100644 .agents/plugins/marketplace.json create mode 100644 plugins/bifrost-intake/.codex-plugin/plugin.json create mode 100644 plugins/bifrost-intake/.mcp.json create mode 100644 plugins/bifrost-intake/scripts/mcp-server.mjs create mode 100644 plugins/bifrost-intake/skills/bifrost-intake/SKILL.md diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json new file mode 100644 index 0000000..cfe249c --- /dev/null +++ b/.agents/plugins/marketplace.json @@ -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" + } + ] +} diff --git a/docs/agent-transport.md b/docs/agent-transport.md index df70822..565822c 100644 --- a/docs/agent-transport.md +++ b/docs/agent-transport.md @@ -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. +## 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 -- 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. - 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. diff --git a/plugins/bifrost-intake/.codex-plugin/plugin.json b/plugins/bifrost-intake/.codex-plugin/plugin.json new file mode 100644 index 0000000..4c5c52e --- /dev/null +++ b/plugins/bifrost-intake/.codex-plugin/plugin.json @@ -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": [] + } +} diff --git a/plugins/bifrost-intake/.mcp.json b/plugins/bifrost-intake/.mcp.json new file mode 100644 index 0000000..53669b4 --- /dev/null +++ b/plugins/bifrost-intake/.mcp.json @@ -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." + } + } +} diff --git a/plugins/bifrost-intake/scripts/mcp-server.mjs b/plugins/bifrost-intake/scripts/mcp-server.mjs new file mode 100644 index 0000000..473db7c --- /dev/null +++ b/plugins/bifrost-intake/scripts/mcp-server.mjs @@ -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); +} diff --git a/plugins/bifrost-intake/skills/bifrost-intake/SKILL.md b/plugins/bifrost-intake/skills/bifrost-intake/SKILL.md new file mode 100644 index 0000000..55724c8 --- /dev/null +++ b/plugins/bifrost-intake/skills/bifrost-intake/SKILL.md @@ -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.