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


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:

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:

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:

  1. Truncated inputagledger verify requires the full export including the entries[] array. Some tooling strips long arrays; confirm the file size matches the API response size.
  2. Real tamperbrokenAt will pinpoint the first failed entry with one of payload_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'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.