Skip to content
Back to docs

§ DOCUMENTATION

Custom Validators (BYOV)

Bring-your-own HTTPS endpoint that Execlave calls during policy enforcement. Run your own business logic — finance caps, data-room ACLs, cross-system lookups — inside the allow / deny decision.

§ 01

When to reach for a custom validator

Built-in policy types (prompt injection, PII, rate limits, tool scope, spend caps) cover most governance needs. Custom validators are the escape hatch when your policy depends on state Execlave does not store.

  • Per-customer spend limits held in your billing system.
  • Entitlement lookups (role, org, region) against your IdP or LDAP.
  • Row-level access checks against an internal data catalog.
  • Business-hours / freeze-window gates driven by your ops calendar.
  • Multi-system approvals where Execlave approvals alone are insufficient.

A validator is just an HTTPS endpoint. Execlave signs each request with an HMAC-SHA256 secret, enforces SSRF protections, and respects your configured fail mode (fail_closed by default).

§ 02

Request contract

Execlave sends a signed POST to your endpoint before the agent executes. The body mirrors the enforcement context.
POST https://validators.internal.acme.com/enforceContent-Type: application/jsonX-Execlave-Signature: sha256=<hex HMAC of the raw body>X-Execlave-Timestamp: 2026-04-22T09:00:00ZX-Execlave-Validator-Id: 5f3a…X-Execlave-Delivery-Id: 9c21… {  "organizationId": "org_...",  "agentId": "agt_...",  "agentName": "finance-assistant",  "environment": "production",  "input": "Transfer $50,000 to vendor-9923",  "tools": ["payments.transfer"],  "metadata": { "userId": "u_42" },  "estimatedCost": 0.02,  "timestamp": "2026-04-22T09:00:00Z"}
§ 03

Response contract

Respond with 200 and a JSON body containing one of three decisions. Any other status code is treated as a failure and handled by the configured fail mode.
HTTP/1.1 200 OKContent-Type: application/json {  "decision": "deny",  "reason": "Transfer exceeds $10,000 daily cap for finance-assistant",  "metadata": { "policyRef": "FIN-TX-0003" }}
DecisionBehaviour
allowValidator is happy — enforcement continues with remaining policies.
denyRaises PolicyBlockedError / ValidatorDeniedError in the SDK — LLM call is prevented.
require_approvalCreates an approval request and suspends the call until a human approves.
§ 04

Security guarantees

  • HTTPS only — the SSRF guard rejects non-HTTPS URLs and any target that resolves to a private, loopback, link-local, or cloud-metadata address.
  • HMAC-SHA256 signed — a 32-byte validator-scoped secret, generated server-side and shown once. Rotate at any time; the old secret stops working immediately.
  • Replay protected — requests include an RFC3339 timestamp; reject anything older than 15 s.
  • Circuit breaker — after 10 consecutive failures the validator opens; Execlave stops calling it for 30 s and applies the configured fail mode during the cool-down.
  • Size capped — responses larger than VALIDATOR_MAX_RESPONSE_BYTES (default 64 KiB) are aborted and treated as failures.
  • Encrypted at rest — the HMAC secret is AES-256-GCM encrypted using VALIDATOR_ENCRYPTION_KEY.
§ 05

Reference implementations

Drop-in examples implementing signature verification, replay protection, and a decision response.
Express (Node.js)
import express from 'express';import crypto from 'node:crypto'; const app = express();const SECRET = process.env.EXECLAVE_VALIDATOR_SECRET!; // 32-byte hex // Preserve the raw body for HMAC verificationapp.use(express.json({ verify: (req, _res, buf) => ((req as any).rawBody = buf) })); app.post('/enforce', (req, res) => {  const sig = (req.header('X-Execlave-Signature') ?? '').replace('sha256=', '');  const ts = req.header('X-Execlave-Timestamp') ?? '';  const raw = (req as any).rawBody as Buffer;   // 1. Reject requests older than 15 s (replay protection)  if (Math.abs(Date.now() - Date.parse(ts)) > 15_000) {    return res.status(400).json({ error: 'stale timestamp' });  }   // 2. Constant-time HMAC verification  const expected = crypto.createHmac('sha256', SECRET).update(raw).digest('hex');  const ok = sig.length === expected.length &&    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));  if (!ok) return res.status(401).json({ error: 'invalid signature' });   // 3. Your business logic  const body = req.body as { input: string; estimatedCost: number };  if (body.estimatedCost > 1.0) {    return res.json({ decision: 'deny', reason: 'estimated cost exceeds $1.00' });  }   return res.json({ decision: 'allow' });}); app.listen(8080);
FastAPI (Python)
import hmac, hashlib, os, timefrom datetime import datetime, timezonefrom fastapi import FastAPI, Request, HTTPException SECRET = os.environ["EXECLAVE_VALIDATOR_SECRET"].encode() app = FastAPI() @app.post("/enforce")async def enforce(request: Request):    raw = await request.body()    sig = (request.headers.get("x-execlave-signature", "")           .removeprefix("sha256="))    ts = request.headers.get("x-execlave-timestamp", "")     # 1. Reject stale requests (>15 s skew)    try:        delta = abs(time.time() - datetime.fromisoformat(            ts.replace("Z", "+00:00")).timestamp())    except ValueError:        raise HTTPException(400, "invalid timestamp")    if delta > 15:        raise HTTPException(400, "stale timestamp")     # 2. Constant-time HMAC verification    expected = hmac.new(SECRET, raw, hashlib.sha256).hexdigest()    if not hmac.compare_digest(sig, expected):        raise HTTPException(401, "invalid signature")     # 3. Your business logic    body = await request.json()    if body.get("estimatedCost", 0) > 1.0:        return {"decision": "deny", "reason": "cost exceeds $1.00 cap"}    return {"decision": "allow"}
§ 06

CLI

# List validatorsexeclave validators list # Create a validator (secret shown once)execlave validators create \  --name finance-spend-cap \  --url https://validators.internal.acme.com/enforce \  --fail-mode fail_closed \  --timeout-ms 2000 # Probe a validatorexeclave validators test <id> # Rotate the HMAC secretexeclave validators rotate-secret <id>
§ 07

Fail modes

Every validator declares what Execlave should do when the call fails (timeout, network error, invalid response, circuit open):

  • fail_closed (default, recommended for security-critical policies) — treat failure as a deny. The LLM call is blocked.
  • fail_open — treat failure as an allow. Use only for non-critical, availability-sensitive policies.

Pick the mode per validator from the dashboard or via the API. Failures are surfaced via audit logs and the custom_validator.failed webhook event.

§ 08

SDK error handling

Both sdk-js and execlave-sdk (Python) raise ValidatorDeniedError — a subclass of PolicyBlockedError — when the denial originates from a custom validator. Catch the general class for uniform UX, or the specific class for validator-aware messaging.

import { ValidatorDeniedError, PolicyBlockedError } from '@execlave/sdk'; try {  await agent.run(input);} catch (err) {  if (err instanceof ValidatorDeniedError) {    return `Blocked by ${err.validatorViolations[0].policyName}: ${err.validatorViolations[0].message}`;  }  if (err instanceof PolicyBlockedError) {    return `Blocked by policy: ${err.violations[0].message}`;  }  throw err;}