diff --git a/README.md b/README.md index 8aa37f8..4f895fa 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,8 @@ Add to your Claude Code config (`~/.claude.json` or project `.mcp.json`): "args": ["/absolute/path/to/agent-bridge/dist/index.js"], "env": { "AGENT_BRIDGE_URL": "https://your-project.supabase.co", - "AGENT_BRIDGE_KEY": "your-anon-key-here" + "AGENT_BRIDGE_KEY": "your-anon-key-here", + "AGENT_BRIDGE_AGENT": "codex" } } } @@ -98,7 +99,7 @@ Restart your MCP client. Three tools become available: `post_context`, `get_cont | Parameter | Type | Required | Description | |-----------|------|----------|-------------| -| `source` | string | yes | Agent name (e.g. `"claude-code"`) | +| `source` | string | yes | Agent name. When `AGENT_BRIDGE_AGENT` is set, this must match it. | | `category` | string | yes | Entry type (see [Categories](#categories)) | | `content` | string | yes | The context message | | `priority` | string | no | `info` (default), `high`, `urgent` | @@ -155,6 +156,8 @@ cp bin/agent-bridge ~/.openclaw/scripts/agent-bridge The CLI reads credentials from `~/.agent-bridge/config` (created in step 3). +When `AGENT_BRIDGE_AGENT` is set for the MCP server or CLI, posts with a different `source` are rejected. This prevents a wrapped runtime from writing rows labelled as another agent after atrib has already signed the original tool arguments. + ### CLI Reference ```bash diff --git a/bin/agent-bridge b/bin/agent-bridge index 5467401..1f96c4d 100755 --- a/bin/agent-bridge +++ b/bin/agent-bridge @@ -95,6 +95,15 @@ cmd_post() { esac done + if [[ -n "${AGENT_BRIDGE_AGENT:-}" ]]; then + if [[ -z "$source" ]]; then + source="$AGENT_BRIDGE_AGENT" + elif [[ "$source" != "$AGENT_BRIDGE_AGENT" ]]; then + echo "Error: --source must match AGENT_BRIDGE_AGENT ($AGENT_BRIDGE_AGENT); got $source" >&2 + exit 1 + fi + fi + if [[ -z "$source" || -z "$category" || -z "$content" ]]; then echo "Error: --source, --category, and content are required" >&2 exit 1 diff --git a/src/server.ts b/src/server.ts index 071fa0f..6e322c4 100644 --- a/src/server.ts +++ b/src/server.ts @@ -25,6 +25,7 @@ import { filterContextRows, mergeEnvelopeMetadata, } from "./message-envelope.js"; +import { resolvePostSource } from "./source-identity.js"; const headers = { apikey: SUPABASE_KEY, @@ -67,7 +68,8 @@ export async function startServer() { properties: { source: { type: "string", - description: 'Agent posting this entry (e.g. "claude-code", "sido")', + description: + "Agent posting this entry. Must match AGENT_BRIDGE_AGENT when configured.", }, category: { type: "string", @@ -236,10 +238,20 @@ export async function startServer() { switch (name) { case "post_context": { + let source: string | undefined; + try { + source = resolvePostSource(args?.source, process.env.AGENT_BRIDGE_AGENT); + } catch (e) { + throw new McpError( + ErrorCode.InvalidParams, + e instanceof Error ? e.message : "source identity mismatch" + ); + } + const postArgs = { ...(args ?? {}), source }; const receiptId = normalizeReceiptId(args?.atrib_receipt_id); - const envelope = buildMessageEnvelope(args, receiptId); + const envelope = buildMessageEnvelope(postArgs, receiptId); const body: Record = { - source: args?.source, + source, category: args?.category, content: args?.content, priority: args?.priority || "info", diff --git a/src/source-identity.ts b/src/source-identity.ts new file mode 100644 index 0000000..fa6b797 --- /dev/null +++ b/src/source-identity.ts @@ -0,0 +1,21 @@ +function normalizeSource(value: unknown): string | undefined { + if (typeof value !== "string") return undefined; + const trimmed = value.trim(); + return trimmed ? trimmed : undefined; +} + +export function resolvePostSource( + requestedSource: unknown, + configuredAgent: unknown +): string | undefined { + const source = normalizeSource(requestedSource); + const configured = normalizeSource(configuredAgent); + + if (source && configured && source !== configured) { + throw new Error( + `source must match AGENT_BRIDGE_AGENT (${configured}); got ${source}` + ); + } + + return source ?? configured; +} diff --git a/test/source_identity.test.ts b/test/source_identity.test.ts new file mode 100644 index 0000000..a671403 --- /dev/null +++ b/test/source_identity.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest"; +import { resolvePostSource } from "../src/source-identity.js"; + +describe("resolvePostSource", () => { + it("accepts a source that matches the configured agent", () => { + expect(resolvePostSource("codex", "codex")).toBe("codex"); + }); + + it("falls back to the configured agent when no source is passed", () => { + expect(resolvePostSource(undefined, "claude-code")).toBe("claude-code"); + }); + + it("keeps legacy source arguments when no configured agent exists", () => { + expect(resolvePostSource("sido", undefined)).toBe("sido"); + }); + + it("rejects source labels that do not match the configured agent", () => { + expect(() => resolvePostSource("claude-code", "codex")).toThrow( + "source must match AGENT_BRIDGE_AGENT (codex); got claude-code" + ); + }); +});