OpenClaw Integration
OpenClaw is an open-source personal AI assistant — a single-agent runtime that runs on your machine, with channel plugins (Discord, Slack, Telegram, Matrix), provider plugins (Anthropic, OpenAI, Ollama), and a typed plugin SDK. AGLedger is a cryptographic notary for automated work. Wired together, every action your OpenClaw agent takes becomes a signed, hash-chained, tamper-evident record.
This guide covers three integration shapes, each validated end-to-end against AGLedger v0.22.18 and live gpt-oss:120b agent runs:
| Path | What it gives you | Best when |
|------|-------------------|-----------|
| Plugin hooks | Every tool the agent runs is notarized — once before the call, once after. Runs in the plugin layer; the agent does not see it. | You want every action notarized regardless of which tool the agent picks. |
| MCP server | The agent calls AGLedger as a discrete tool when accountability matters for the step. | The agent decides per-step whether to notarize. |
| CLI shell tool | An OpenClaw skill calls agledger api … as a subprocess. | You don't want to build a TypeScript plugin; you have a SKILL.md and a shell. |
The three are not mutually exclusive. Plugin hooks for blanket coverage plus MCP for per-step explicit notarization is a sensible combination.
Prerequisites
- AGLedger instance — Docker Compose or Helm (
agledger/agledger:0.22.18). See Installation. - An admin or platform API key. From the install bootstrap or your operator. Treat it like a database password.
- At least one agent provisioned in your tenant. Records require a
principalAgentId. Follow thenextStepschain onPOST /v1/admin/enterprises→POST /v1/admin/api-keys→POST /v1/admin/agents. Fresh installs ship with zero agents on purpose; the API surfaces this. - Node 22 or newer, pnpm or npm, TypeScript familiarity for the plugin or MCP paths. The CLI path needs only a shell.
Path 1 — Plugin hooks
The OpenClaw plugin SDK exposes before_tool_call (rewrite, block, or pre-notarize) and after_tool_call (observe results, errors, duration). The mapping to AGLedger is direct:
before_tool_call— write a record stating what the agent is about to do.after_tool_call— submit a receipt against that record carrying the result evidence.
Both entries are signed, hash-chained, and verifiable offline. If the agent context resets between the two, the chain still holds them together byte-for-byte.
Step 1 — Create the plugin package
// package.json
{
"name": "@your-org/openclaw-agledger-plugin",
"version": "0.1.0",
"type": "module",
"openclaw": {
"extensions": ["./index.ts"],
"compat": { "pluginApi": ">=2026.3.24-beta.2", "minGatewayVersion": "2026.3.24-beta.2" },
"build": { "openclawVersion": "2026.3.24-beta.2", "pluginSdkVersion": "2026.3.24-beta.2" }
},
"dependencies": {
"@agledger/sdk": "^0.7.2"
}
}
// openclaw.plugin.json
{
"id": "agledger",
"name": "AGLedger",
"description": "Notarizes every agent tool call to an AGLedger instance.",
"activation": { "onStartup": true },
"configSchema": {
"type": "object",
"additionalProperties": false,
"required": ["apiUrl", "apiKey"],
"properties": {
"apiUrl": { "type": "string", "title": "AGLedger gateway URL" },
"apiKey": { "type": "string", "title": "AGLedger admin or platform API key", "uiHints": { "sensitive": true } },
"principalAgentId": { "type": "string", "title": "Principal agent UUID for this OpenClaw instance" },
"type": { "type": "string", "default": "OPENCLAW-TOOL-CALL-v1", "title": "Record type" },
"failMode": { "type": "string", "enum": ["fail-open", "fail-closed"], "default": "fail-open" }
}
}
}
Step 2 — Write the plugin entry
// index.ts
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { AgledgerClient } from "@agledger/sdk";
export default definePluginEntry({
id: "agledger",
name: "AGLedger",
description: "Notarizes every agent tool call to an AGLedger instance.",
register(api) {
// Map of toolCallId → AGLedger recordId so after_tool_call can submit
// a receipt against the matching record.
const inflight = new Map<string, string>();
api.on(
"before_tool_call",
async (event) => {
const cfg = event.context.pluginConfig as {
apiUrl: string;
apiKey: string;
principalAgentId?: string;
type: string;
failMode: "fail-open" | "fail-closed";
};
const c = new AgledgerClient({ baseUrl: cfg.apiUrl, apiKey: cfg.apiKey });
try {
const record = await c.records.create({
type: cfg.type,
contractVersion: "1",
platform: "openclaw",
principalAgentId: cfg.principalAgentId,
criteria: {
tool: event.toolName,
params: event.params,
sessionId: event.sessionId,
agentRunId: event.agentRunId,
},
autoActivate: true,
});
inflight.set(event.toolCallId, record.id);
return; // observe-only: don't block the tool
} catch (err) {
if (cfg.failMode === "fail-closed") {
return {
cancel: { reason: `AGLedger record failed: ${(err as Error).message}` },
};
}
// fail-open: log and let the tool run. The chain will be incomplete
// for this call; that's the customer-chosen tradeoff.
api.logger?.warn("agledger record failed", err);
return;
}
},
{ priority: 80, timeoutMs: 5_000 },
);
api.on(
"after_tool_call",
async (event) => {
const cfg = event.context.pluginConfig as {
apiUrl: string;
apiKey: string;
failMode: "fail-open" | "fail-closed";
};
const recordId = inflight.get(event.toolCallId);
if (!recordId) return;
inflight.delete(event.toolCallId);
const c = new AgledgerClient({ baseUrl: cfg.apiUrl, apiKey: cfg.apiKey });
try {
await c.receipts.submit(recordId, {
evidence: {
ok: !event.error,
durationMs: event.durationMs,
result: event.result,
error: event.error ? String(event.error) : null,
},
});
} catch (err) {
if (cfg.failMode === "fail-closed") {
// The tool already ran. Failing closed here doesn't undo the work,
// but it surfaces the chain inconsistency to the agent.
return {
cancel: { reason: `AGLedger receipt failed: ${(err as Error).message}` },
};
}
api.logger?.warn("agledger receipt failed", err);
}
},
{ priority: 80, timeoutMs: 5_000 },
);
},
});
Step 3 — Configure and install
# Build and publish to ClawHub (or your private registry):
pnpm build
openclaw plugins publish
# On each OpenClaw machine:
openclaw plugins install clawhub:@your-org/openclaw-agledger-plugin
// ~/.openclaw/openclaw.json (excerpt)
{
"plugins": {
"entries": {
"agledger": {
"apiUrl": "https://agledger.your-org.example",
"apiKey": "agl_adm_…",
"principalAgentId": "019dfabd-72e2-74bb-9001-d70d29f72c80",
"type": "OPENCLAW-TOOL-CALL-v1",
"failMode": "fail-open"
}
}
}
}
Step 4 — Verify it's working
After the agent runs a task, fetch the chain for any of the records the plugin created and verify it offline:
agledger api GET /v1/records?platform=openclaw --query '{"limit":1}'
# pick an id from the response, then:
agledger api GET /v1/records/<id>/audit-export > audit.json
agledger verify audit.json
# → { "valid": true, "totalEntries": N, "verifiedEntries": N, ... }
A successful verify (valid: true and verifiedEntries === totalEntries) proves the chain has not been tampered with, every entry was signed by a key the verifier recognizes, and the signatures match. No live API call is required for verify — the cryptographic root of trust is the export's embedded signingPublicKeys.
Path 2 — MCP server
OpenClaw is an MCP client out of the box. Register AGLedger's MCP server as an outbound integration; the agent gets three tools (agledger_discover, agledger_api, agledger_verify) and calls them when accountability matters for the step.
openclaw mcp set agledger '{
"command": "npx",
"args": ["-y", "@agledger/mcp-server"],
"env": {
"AGLEDGER_API_URL": "https://agledger.your-org.example",
"AGLEDGER_API_KEY": "agl_adm_…"
}
}'
The agent will have agledger_discover (called first to learn identity and scopes), agledger_api (generic pass-through), and agledger_verify (offline verification of an audit export). Each AGLedger response carries a nextSteps array that the agent follows to walk the lifecycle.
Pin to @agledger/[email protected] or later. Earlier versions returned empty content[] to LLM-driven MCP runtimes (tracked as F-531, fixed in 2.3.2).
OpenClaw rejects NODE_OPTIONS and PYTHONPATH env keys to prevent injection — irrelevant for @agledger/mcp-server, but worth knowing if you customize the env block.
Path 3 — CLI shell tool
For a simple OpenClaw skill that calls AGLedger from a shell, install the CLI globally and invoke it as a subprocess:
npm install -g @agledger/cli
agledger login --api-url https://agledger.your-org.example --api-key agl_adm_…
# stores in ~/.agledger/config.json (mode 0600)
Then in your skill or shell command:
# Notarize the action
agledger api POST /v1/records --json --data '{
"type":"OPENCLAW-ACTION-v1",
"contractVersion":"1",
"platform":"openclaw",
"principalAgentId":"<your agent uuid>",
"criteria":{"action":"send_message","channel":"slack:#general"},
"autoActivate":true
}'
# After the action completes, submit a receipt
agledger api POST /v1/records/<id>/receipts --json --data '{
"evidence":{"ok":true,"messageId":"…"}
}'
# Verify the chain
agledger api GET /v1/records/<id>/audit-export --json > audit.json
agledger verify audit.json --json
This path was validated end-to-end against v0.22.18 in our integration test (gpt-oss:120b on a remote Ollama box → @agledger/[email protected] → AGLedger). The record reached FULFILLED, the chain export returned chainIntegrity: true, and 7 of 7 entries were signed.
Choosing between the three
A typical production OpenClaw deployment combines them:
- Plugin hooks for blanket coverage — every tool call leaves a signed trail.
- MCP server for explicit per-step notarization — when the agent reasons "this specific decision needs accountability," it has a discrete tool to invoke. Pair with
before_agent_replyplugin hooks for a gated flow where critical replies require a verdict before the model finalizes. - CLI for skills, debug runs, and operator-side chain verification.
If you are integrating for the first time, start with Path 3 (CLI) for a quick proof-of-concept, then move to Path 1 (plugin hooks) for production coverage.
Troubleshooting
"Records require a principal agent — POST /v1/records will reject with VALIDATION_ERROR"
Fresh AGLedger installs ship with zero agents on purpose. Provision one before pointing OpenClaw at it: POST /v1/admin/agents { "name": "openclaw-host-1", "enterpriseId": "<your tenant id>" }. The API's recoveryHint walks you through this; the agent UUID it returns is your plugin's principalAgentId config value.
"Agents can only set their own capabilities"
Capabilities are agent-scoped permissions; they are optional for record creation. Your plugin does not need to set them. If you do want capabilities for typed access control, set them from inside the agent's own key, not the platform key.
MCP tool calls return empty results
If you are pinned to @agledger/mcp-server ≤ 2.3.1, every tool response carries data in structuredContent with content[] empty. LLM-driven MCP runtimes only forward content[] to the model. Upgrade to @agledger/[email protected] or later (F-531 closed 2026-05-06).
Audit-export verify returns valid: false
Two common causes:
- Truncated input —
agledger verifyrequires the full export including theentries[]array. Some tooling strips long arrays; confirm the file size matches the API response size. - Real tamper —
brokenAtwill pinpoint the first failed entry with one ofpayload_hash_mismatch,chain_break,signature_invalid,unknown_key,position_gap,malformed_entry,unsupported_algorithm. If the export came directly from AGLedger and the entry is malformed, file an issue.
Version compatibility
This guide was tested against:
- OpenClaw: plugin SDK
2026.3.24-beta.2 - AGLedger API: v0.22.18
- AGLedger TS SDK:
@agledger/sdkv0.7.2 - AGLedger CLI:
@agledger/cliv0.7.2 - AGLedger MCP Server:
@agledger/mcp-serverv2.3.3 (pin v2.3.2 or later for working LLM-MCP runtimes)
OpenClaw's plugin SDK is in public preview. Pin your version and re-test after upgrades. Plugin event field names (event.toolCallId, event.params, event.result, event.durationMs, event.error, event.context.pluginConfig) match the OpenClaw plugins/hooks reference at the time of writing — verify against openclaw/plugin-sdk/plugin-entry source if you see field-name drift.
Validated against AGLedger v0.22.18 on 2026-05-06. Three integration shapes — raw HTTP (proxy for plugin hooks), MCP server, and CLI pass-through — each reached FULFILLED with chainIntegrity: true and 7/7 signed entries.