Add Bifrost intake Codex plugin
This commit is contained in:
parent
f5c9b1ef59
commit
9258a218ac
6 changed files with 473 additions and 1 deletions
20
.agents/plugins/marketplace.json
Normal file
20
.agents/plugins/marketplace.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
43
plugins/bifrost-intake/.codex-plugin/plugin.json
Normal file
43
plugins/bifrost-intake/.codex-plugin/plugin.json
Normal 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": []
|
||||
}
|
||||
}
|
||||
9
plugins/bifrost-intake/.mcp.json
Normal file
9
plugins/bifrost-intake/.mcp.json
Normal 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."
|
||||
}
|
||||
}
|
||||
}
|
||||
366
plugins/bifrost-intake/scripts/mcp-server.mjs
Normal file
366
plugins/bifrost-intake/scripts/mcp-server.mjs
Normal 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);
|
||||
}
|
||||
19
plugins/bifrost-intake/skills/bifrost-intake/SKILL.md
Normal file
19
plugins/bifrost-intake/skills/bifrost-intake/SKILL.md
Normal 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.
|
||||
Loading…
Reference in a new issue