@sanna-ai

Sanna Governance

OpenClaw plugin that enforces Sanna governance constitutions on AI agent tool execution

当前版本
v1.0.2
code-plugin社区source-linked

@sanna-ai/openclaw

CI

sanna.dev

The Problem

OpenClaw gives your agent powerful tools — exec, bash, browser, web_fetch, write. Nothing in the default setup stops the agent from sending emails through exec, hitting external APIs, reading credentials, or exfiltrating data through URL parameters. There's no audit trail, no approval workflow, no receipt proving what happened. The agent does what it thinks will accomplish the task.

Sanna is a governance plugin for OpenClaw. It intercepts every tool call through the gateway's before_tool_call hook and evaluates it against a YAML constitution before execution.

Without Sanna

> exec(command="curl -X POST https://slack.com/api/chat.postMessage -d 'channel=#prod&text=deployed'")
→ Executed. No audit trail. No receipt. No one was asked.

With Sanna

> exec(command="curl -X POST https://slack.com/api/chat.postMessage -d 'channel=#prod&text=deployed'")
✗ Blocked by Sanna governance: Block network/messaging commands routed
  through exec to prevent escalation bypass (receipt: r_528f…a3e1)

The curl in the command matched INV_NO_EXTERNAL_COMMS_VIA_EXEC. The decision was signed, persisted, and returned before the tool executed. The agent never touched the network.

What Sanna Does

Sanna is a governance layer for AI agents. It has two pillars:

Constitution enforcement. YAML constitutions define three tiers of authority: tools the agent can execute freely, actions that require human approval, and operations that are permanently blocked. But tier matching alone isn't enough — invariants inspect the full parameter context of every tool call, catching dangerous patterns regardless of which tool executes them.

Cryptographic receipts. Every governance decision — allow, escalate, or halt — produces an Ed25519-signed receipt persisted via a ReceiptSink before the response returns to the agent. Receipts use the Sanna v1.1 protocol with 14-field fingerprints, receipt chaining (parent_receipts), and per-session workflow_id tracking. Receipts are tamper-evident and independently verifiable. If it happened, there's a receipt.

The system is fail-closed. If evaluation throws or receipt persistence fails, the action is blocked in enforce mode.

Architecture

  Agent Loop                 OpenClaw Gateway
 ┌──────────┐              ┌─────────────────────────────────────┐
 │           │─ tool call ─>│  before_tool_call hook (sanna)      │
 │           │              │                                     │
 │           │              │  1. evaluateAuthority(tool, params)  │
 │           │              │     via @sanna-ai/core (in-process)  │
 │           │              │  2. Run invariant checks             │
 │           │              │  3. generateReceipt() + signReceipt()│
 │           │              │  4. ReceiptSink.store() (write-ahead)│
 │           │              │  5. OTel span export (optional)      │
 │           │              │  6. Return allow / block to Gateway  │
 │           │<─ result ────│                                     │
 └──────────┘              │  after_tool_call (observability)     │
                           └─────────────────────────────────────┘

The before_tool_call hook is the primary enforcement point. It fires for every tool call in the agent loop, evaluates authority via @sanna-ai/core, and returns { block: true } or { blocked: false } to the Gateway. (Yes, the asymmetric key names are OpenClaw's hook API, not a typo.) No wrapper tools, no tool renaming — native tools execute normally and the hook gates them transparently.

Quick Start

# Build the plugin
npm run build

# Pack and install
npm pack
openclaw plugins install @sanna-ai/openclaw

# Enable hooks in ~/.openclaw/openclaw.json
# hooks.internal.enabled must be true for governance to fire

# Restart the gateway
openclaw gateway restart

# Check readiness
openclaw sanna doctor

Constitution files ship with the plugin and are auto-discovered from the constitutions/ directory at load time — no manual path configuration or copying needed.

See docs/SETUP.md for detailed installation steps.

Governed Tools

All tool calls pass through the before_tool_call hook. The default governed tools are organized by tier:

TierToolsRisk Level
1exec, bash, write, edit, apply_patch, processModifies system state
2browser, message, nodesComposite tools with high-risk actions
3web_search, web_fetch, cron, gateway, sessions_send, sessions_spawnAudit trail

Tier 4 tools (read, image, canvas, sessions_list, sessions_history, session_status, memory_search, memory_get, agents_list) are not governed by default.

Enforcement Layers

Each tool call is evaluated through multiple layers:

  1. Authority evaluationevaluateAuthority() checks tool name and parameters against the constitution's authority_boundaries (cannot_execute, must_escalate, can_execute)
  2. Invariant checksrunAllInvariantChecks() evaluates constitution invariants against the action context, with in-process regex fallback for regex_deny rules
  3. LLM semantic checks (optional) — AI-powered invariant evaluation via enableLlmChecks()
  4. Custom evaluators (optional) — user-defined evaluator modules loaded at startup

Results from all layers are collected as CheckResult[] in the receipt, with evaluation_coverage tracking which checks ran and passed.

Enforcement Modes

ModeBehavior
enforceBlock on deny/escalate, fail-closed on errors
auditLog decisions but never block — for monitoring and tuning
passthroughNo enforcement

CLI Commands

CommandDescription
openclaw sanna doctorCheck governance readiness (hooks, constitution, receipt store, keys, LLM checks)
openclaw sanna statusConstitution info, enforcement stats
openclaw sanna auditRecent enforcement decisions as a formatted table (--limit N, --json)
openclaw sanna verify <id>Verify a receipt's integrity (--strict, --json)

Audit Output

The audit command displays a color-coded table with timestamps, tool names, verdicts (green ALLOW, yellow ESCALATE, red HALT), and reasons. Use --json for machine-readable output.

Receipt Verification

The verify command runs a 5-stage verification pipeline via verifyReceipt() from @sanna-ai/core:

  1. Schema — receipt structure validation
  2. Integrity — content hash verification
  3. Fingerprint — receipt fingerprint check
  4. Signature — Ed25519 signature verification (requires public key)
  5. Strict — all receipt checks passed (with --strict flag)

Constitution Templates

Three starter templates in constitutions/ for different use cases:

TemplateProfile
personal.yamlLenient — broad execution and browsing, messaging escalated
developer.yamlBalanced — full workspace access, communication escalated, dangerous commands escalated
team.yamlStrict — narrow execution, broad escalation requirements

All templates include 14 invariants covering: external comms bypass, HTTP request tunneling, scripted outbound connections, AppleScript execution, app launching, DNS exfiltration, bash TCP/UDP, encoded payload execution, persistence mechanisms, data exfiltration endpoints, destructive operations, credential harvesting, keychain access, and script file execution.

All templates document evaluation order and matching asymmetry. Key ordering matches evaluation priority: cannot_executemust_escalatecan_execute.

See docs/CONSTITUTION_GUIDE.md for customization.

Configuration

In openclaw.json, the plugin reads its config from the plugin block:

{
  "plugins": {
    "sanna": {
      "constitutionPath": "./constitutions",
      "enforcementMode": "enforce"
    }
  },
  "hooks": {
    "internal": {
      "enabled": true
    }
  }
}

hooks.internal.enabled must be true — without it, the before_tool_call hook never fires and governance is silently bypassed. In enforce mode, the plugin throws on startup if this is not set. In audit mode, it warns.

Full Configuration Reference

FieldTypeDefaultDescription
constitutionPathstring"" (auto-discover)Path to YAML constitution file
privateKeyPathstring""Ed25519 private key PEM for receipt signing
publicKeyPathstring""Ed25519 public key PEM for receipt verification
receiptStorePathstring~/.sanna/receipts/openclaw.dbSQLite receipt store path
governedToolsstring[]All tier 1+2+3Tool names to govern
enforcementModestring"enforce"enforce, audit, or passthrough
sinkTypestring"local_sqlite"Receipt sink: local_sqlite or null
contentModestring"full"Receipt content mode: full, redacted, or hashes_only
otelExportbooleanfalseEnable OpenTelemetry span export for receipts
otelServiceNamestring"sanna-openclaw"OTel service name for exported spans
llmChecksbooleanfalseEnable LLM semantic checks for invariant evaluation
llmChecksModelstring""Model to use for LLM checks
customEvaluatorsPathstring""Path to JS module registering custom evaluators

OpenTelemetry Integration

Set otelExport: true and install @opentelemetry/api as a peer dependency. Governance receipts are exported as spans after each tool call decision. The exporter is fire-and-forget — failures are swallowed to avoid blocking enforcement.

LLM Semantic Checks

Set llmChecks: true to enable AI-powered invariant evaluation. Optionally set llmChecksModel to specify which model to use. LLM checks are additive — they enhance but do not replace rule-based evaluation. Initialization failures are non-fatal.

Custom Evaluators

Set customEvaluatorsPath to a JS module that registers custom invariant evaluators at load time via registerInvariantEvaluator() from @sanna-ai/core. The module is require()'d at plugin startup.

Requirements

  • Node.js 22+
  • OpenClaw Gateway >= 2026.1.26

Development

# TypeScript tests (221 tests)
npm test

# Type check
npm run lint

# Build (cleans dist/ first via prebuild)
npm run build

See the full red-team writeup at sanna.dev/blog.

Troubleshooting

"Could not locate the bindings file" on first install

The plugin uses SQLite for receipt persistence via better-sqlite3, a native C++ module. OpenClaw installs plugin dependencies with --ignore-scripts, which skips the native compilation step. The fix is a one-time rebuild:

cd ~/.openclaw/extensions/sanna && npm rebuild better-sqlite3
openclaw gateway restart

After this, the native module is compiled for your platform and persists across gateway restarts. You only need to run this once per install.

"hooks.internal.enabled is not set"

Sanna requires internal hooks to intercept tool calls. In ~/.openclaw/openclaw.json, ensure:

{
  "hooks": {
    "internal": {
      "enabled": true
    }
  }
}

Then restart the gateway: openclaw gateway restart

License

AGPL-3.0 — see LICENSE

sanna.dev

源码与版本

源码仓库

sanna-ai/sanna-openclaw

打开仓库

源码提交

6c81cee569b7dd487147a10160549a85929f4e87

查看提交

安装命令

openclaw plugins install clawhub:@sanna-ai/openclaw

元数据

  • 包名: @sanna-ai/openclaw
  • 创建时间: 2026/04/08
  • 更新时间: 2026/05/21
  • 执行代码:
  • 源码标签: main

兼容性

  • 构建于 OpenClaw: 2026.1.26
  • 插件 API 范围: >=2026.1.26
  • 标签: latest
  • 文件数: 66