Skip to main content

Policy & Safety Hooks

SochDB provides policy enforcement for AI agent safety: pre-write validation, post-read filtering, rate limiting, and audit logging that keep agents inside defined boundaries.

Where this lives in each SDK

The hook-based policy engine is a real, importable module in the Rust SDK (sochdb::policy). The Node.js SDK ships a different, real module — PolicyService — which is a namespace-scoped access-control (ACL/governance) service rather than a write/read hook engine. In the Python SDK the hook-based PolicyEngine shown in older docs is not an importable class; it exists only as an example pattern (validate_user / redact_pii) in the sochdb-python-examples repo. The Go SDK has no client-side policy engine; policy on the Go side is reached through the server's gRPC PolicyService.

For server-side authentication, RBAC (Owner / Editor / Viewer), and per-namespace policy enforced by the gRPC server, see the Security guide.

Overview (Rust PolicyEngine)

The Rust PolicyEngine<C> wraps a connection and runs configurable hooks around each operation:

TriggerPurposeUse Case
before_writeValidate before writingBlock system key modifications
after_writePost-write actionsAudit logging, notifications
before_readPre-read access controlPermission checks
after_readPost-read filteringRedact sensitive data
before_deleteValidate before deleteProtect critical data
after_deletePost-delete actionsCleanup, audit

A handler receives a single &PolicyContext and returns a PolicyAction: Allow, Deny, Modify(Vec<u8>), or Log. The engine applies the highest- precedence outcome it sees, where Deny outranks Modify, which outranks Allow; Log is a side effect.

Quick Start

The Rust SDK (crate sochdb, v2.0.3) exposes the hook engine in sochdb::policy.

use sochdb::{Connection, prelude::*};
use sochdb::policy::{PolicyEngine, PolicyAction, PolicyContext};

let conn = Connection::open("./agent_data")?;
let policy = PolicyEngine::new(conn);

// Block writes to system keys when an agent_id is present
policy.before_write("system/*", |ctx: &PolicyContext| {
if ctx.agent_id.is_some() {
PolicyAction::Deny
} else {
PolicyAction::Allow
}
});

// Redact a value on read
policy.after_read("users/*/email", |_ctx: &PolicyContext| {
PolicyAction::Modify(b"[REDACTED]".to_vec())
});

// Use policy-wrapped operations (put/get/delete enforce the hooks)
let ctx = PolicyContext::new("write", b"users/alice")
.with_agent_id("agent_001");
policy.put(b"users/alice", b"data", Some(&ctx))?;

Each put / get / delete returns Result<_, sochdb::policy::PolicyViolationError> and runs the registered hooks before (and after) touching storage.

Pattern Matching (Rust engine)

The Rust PolicyEngine compiles each pattern to an anchored regex: . is escaped, * matches within a single path segment ([^/]*), and ** matches across segments (.*).

PatternMatches
system/*system/config, system/users
users/*/emailusers/alice/email, users/bob/email
users/**users/alice, users/alice/profile/photo
*.jsonconfig.json, data.json

Rate Limiting (Rust engine)

Prevent runaway agents with token-bucket rate limiting. The signature is add_rate_limit(operation: &str, max_per_minute: u32, scope: &str):

// Global limit: 1000 writes per minute
policy.add_rate_limit("write", 1000, "global");

// Per-agent limit: 100 writes per minute per agent
policy.add_rate_limit("write", 100, "agent_id");

// Per-session limit
policy.add_rate_limit("read", 500, "session_id");

Scope options:

  • global - shared limit across all operations
  • agent_id - separate limit per agent
  • session_id - separate limit per session
  • any custom key present in the PolicyContext

Audit Logging (Rust engine)

Track agent operations for compliance and debugging. enable_audit() takes no arguments; the ring buffer retains the most recent 10,000 entries:

// Enable audit logging
policy.enable_audit();

// Perform operations...
let ctx = PolicyContext::new("write", b"key").with_agent_id("agent_001");
policy.put(b"key", b"value", Some(&ctx))?;

// Get the most recent audit entries (newest last)
for entry in policy.get_audit_log(100) {
println!("{:?}", entry);
}

get_audit_log(limit) returns up to limit of the most recent AuditEntry records. There is no built-in per-agent filter argument; filter the returned slice yourself.

Built-in Policy Helpers (Rust engine)

sochdb::policy provides factory functions that return ready-made handler closures. Call them (note the parentheses) and pass the result to a hook:

use sochdb::policy::{deny_all, allow_all, require_agent_id, redact_value};

// Deny all operations matching the pattern
policy.before_write("readonly/*", deny_all());

// Require an agent_id in the context
policy.before_write("agents/*", require_agent_id());

// Redact values on read
policy.after_read("secrets/*", redact_value(b"[REDACTED]".to_vec()));

Error Handling (Rust engine)

When a policy blocks an operation, the call returns Err(PolicyViolationError):

use sochdb::policy::PolicyContext;

let ctx = PolicyContext::new("write", b"system/config").with_agent_id("rogue");
match policy.put(b"system/config", b"malicious", Some(&ctx)) {
Ok(()) => {}
Err(e) => {
eprintln!("Operation blocked: {e}");
// Log the security event
}
}

PolicyViolationError implements std::error::Error and Display (it prints as PolicyViolation: <message>).

Node.js PolicyService

The Node SDK's PolicyService is rule-based access control plus grant management, scoped per namespace. Rules carry an effect ('allow' or 'deny'), principals, resources, actions, optional conditions, and a priority; the namespace's defaultEffect applies when no rule matches.

Rules and evaluation

import { EmbeddedDatabase, PolicyService } from '@sochdb/sochdb';

const db = EmbeddedDatabase.open('./agent_data');
const policy = new PolicyService(db, { enableAudit: true });

await policy.createNamespacePolicy({
namespace: 'tenant_123',
defaultEffect: 'deny',
rules: [],
});

// Add a rule after the fact
await policy.addRule('tenant_123', {
id: 'writers',
name: 'Writers may write documents',
effect: 'allow',
principals: ['user:alice', 'service:ingest'],
resources: ['collection:documents'],
actions: ['write', 'create'],
priority: 10,
});

const decision = await policy.evaluate({
principal: 'user:alice',
action: 'write',
resource: 'collection:documents',
});
// decision: { allowed, matchedRule?, reason?, evaluationTime }

Grants and permissions

For coarse-grained, principal-to-namespace access, use grants instead of (or alongside) rules. Permissions are drawn from NamespacePermission: 'read', 'write', 'delete', 'admin', 'create_collection', 'delete_collection', 'search', 'manage_policy'.

await policy.grantAccess({
namespace: 'tenant_123',
principal: 'user:bob',
permissions: ['read', 'search'],
expiresAt: Date.now() + 86_400_000, // optional, 24h
});

const canSearch = await policy.hasPermission('tenant_123', 'user:bob', 'search');

// Inspect decisions (when enableAudit is on)
const log = await policy.getAuditLog();
note

PolicyService is an application-level ACL layer stored in the embedded database (keys prefixed _policy:). It is independent from the gRPC server's RBAC. To enforce auth and roles at the wire level, run sochdb-grpc-server with --auth — see the Security guide.

Best Practices

  1. Defense in Depth: Combine multiple policies for layered security
  2. Audit Critical Operations: Enable audit logging for sensitive namespaces
  3. Rate Limit by Agent: Prevent any single agent from overwhelming the system
  4. Redact by Default: Apply redaction policies to PII fields
  5. Test Policies: Write unit tests for policy handlers
  6. Monitor Denials: Alert on unusual denial patterns

See Also