Fleet integrations
Five framework adapters and one shared client. Pick the one that matches your stack; install it; wire three lines of config; every LLM tool call becomes a BIP-322-signed action receipt that flows into your fleet project automatically.
The packages
| Package | Wraps | npm |
|---|---|---|
@orangecheck/agent-anthropic | Anthropic Messages API tool_use blocks | 0.1.0+ |
@orangecheck/agent-openai | OpenAI Chat Completions function-call / Responses tool_call | 0.1.0+ |
@orangecheck/agent-vercel | Vercel AI SDK tool() definitions | 0.1.0+ |
@orangecheck/agent-langgraph | LangGraph tool-node executes | 0.1.0+ |
@orangecheck/agent-mcp | Model Context Protocol invocations | 0.1.0+ |
@orangecheck/agent-fleet-client | Shared HTTP client (used by every adapter; install directly if you're rolling your own) | 0.1.0+ |
@orangecheck/webhook-verify | Drop-in HMAC verifier for receiving fleet webhooks | 0.1.0+ |
Every framework adapter exposes the same shape:
const { result, action, posted } = await invokeWithStampAndPost({
agent: agentSigner,
delegation: signedDelegation,
/* framework-specific tool-call payload */
call: (normalizedCall) => myExecute(normalizedCall),
fleet: {
apiToken: process.env.OC_TOKEN!,
projectId: process.env.OC_PROJECT_ID!,
},
});
If fleet is omitted the adapter still stamps + executes; posted just comes
back null. The post is fire-and-forget after the call, so a flaky fleet
never blocks a tool from running.
Anthropic (Claude tool_use)
import { invokeWithStampAndPost } from '@orangecheck/agent-anthropic';
const message = await anthropic.messages.create({
/* … */
});
for (const block of message.content) {
if (block.type !== 'tool_use') continue;
const { result, action, posted } = await invokeWithStampAndPost({
agent: agentSigner,
delegation: signedDelegation,
toolUse: block,
call: (toolUse) => myToolImpl(toolUse.input),
fleet: { apiToken: OC_TOKEN, projectId: OC_PROJECT_ID },
});
// feed `result` back to Claude as a tool_result content block.
// `action` + `posted` are your audit trail.
}
Scope check: scopeExercised defaults to anthropic:tool(name=<tool>) — the
tightest admissible sub-scope. Override via the scopeExercised field if your
delegation grants a broader verb.
OpenAI (function calling / Responses)
import { invokeWithStampAndPost } from '@orangecheck/agent-openai';
const completion = await openai.chat.completions.create({
/* … */
});
const fc = completion.choices[0].message.function_call;
if (fc) {
const { result, action } = await invokeWithStampAndPost({
agent: agentSigner,
delegation: signedDelegation,
call: fc,
execute: (parsed) => myFn(parsed.args),
fleet: { apiToken: OC_TOKEN, projectId: OC_PROJECT_ID },
});
}
Vercel AI SDK
import { ocTool } from '@orangecheck/agent-vercel';
import { tool } from 'ai';
const createInvoice = ocTool({
verb: 'invoice.create',
parameters: invoiceSchema,
execute: (args) => myInvoiceCreate(args),
});
const tools = {
'invoice.create': tool({
parameters: invoiceSchema,
execute: async (args, { toolCallId }) => {
const { result } = await createInvoice.execute(args, {
agent: agentSigner,
delegation: signedDelegation,
callId: toolCallId,
fleet: { apiToken: OC_TOKEN, projectId: OC_PROJECT_ID },
});
return result;
},
}),
};
LangGraph
import { ocToolNode } from '@orangecheck/agent-langgraph';
const createInvoice = ocToolNode({
verb: 'invoice.create',
execute: (args) => myInvoiceCreate(args),
});
const graph = new StateGraph(MyState).addNode(
'createInvoice',
async (state) => {
const { result } = await createInvoice.execute(pickArgs(state), {
agent: agentSigner,
delegation: signedDelegation,
callId: state.lastToolCallId,
graphState: state,
fleet: { apiToken: OC_TOKEN, projectId: OC_PROJECT_ID },
});
return mergeIntoState(state, result);
}
);
The graphStateHash (deterministic over the supplied state object) is embedded
in the action canonical message so a verifier replaying the graph against a
snapshot gets byte-identical receipts.
MCP (Model Context Protocol)
import { invokeWithStampAndPost } from '@orangecheck/agent-mcp';
const { result, action } = await invokeWithStampAndPost({
agent: agentSigner,
delegation: signedDelegation,
invocation: { server, tool, params },
call: (inv) => mcpClient.callTool(inv),
fleet: { apiToken: OC_TOKEN, projectId: OC_PROJECT_ID },
});
The stamp commits to (server, tool, params) before the call runs, so a hung
or failing MCP server still leaves an on-record commitment that the agent
attempted exactly this invocation.
Receiving the webhooks
When your fleet project has webhook endpoints subscribed, every accepted envelope fires a signed POST. Use the drop-in verifier:
import { verify } from '@orangecheck/webhook-verify';
import express from 'express';
const app = express();
app.use(express.raw({ type: 'application/json' }));
app.post('/webhooks/orangecheck', (req, res) => {
const ok = verify({
secret: process.env.OC_WEBHOOK_SECRET!, // shown ONCE at create
signature: req.header('X-OrangeCheck-Signature') ?? '',
rawBody: req.body,
});
if (!ok) return res.status(401).send('bad signature');
const event = JSON.parse(req.body.toString('utf8'));
// handle event.event_type, event.id, event.envelope
res.status(200).send('ok');
});
See Webhooks for the full event catalog and the delivery / retry semantics.
Direct API (no adapter)
If your stack isn't on the list above, install the shared client and post envelopes directly:
import { postActionToFleet } from '@orangecheck/agent-fleet-client';
await postActionToFleet(stampedAction, {
apiToken: process.env.OC_TOKEN!,
projectId: process.env.OC_PROJECT_ID!,
});
The client also exposes postDelegationToFleet, postRevocationToFleet, and
postSubdelegationToFleet if you prefer to drive the registry yourself instead
of going through the dashboard.
Authentication
All adapter posts authenticate via Bearer token:
Authorization: Bearer ock_<64-hex>
Tokens are minted at /settings § 03 · API tokens. The plaintext is returned
once at create — store it in your CI's secrets manager immediately. The
token authenticates as the project owner; revoke at any time from the same
surface.