Enforcement
Asqav enforces agent behavior at three levels, each a different tradeoff between security guarantee and integration complexity. Most teams use all three for different tools, and marking a tool as hidden removes it from discovery entirely. Whichever level makes the call, Asqav signs the outcome server-side into a tamper-evident receipt, recorded by a party that is not the agent's operator and verifiable by anyone afterward.
Three-level model
| Tier | Guarantee | Tool |
|---|---|---|
| Strong | Agent cannot skip the check | enforced_tool_call |
| Bounded | Absence of gate signature is detectable | gate_action |
| Detectable | Tampering is cryptographically provable | sign_action |
High-risk mutations (database writes, file deletions, financial transactions) use the strong path. Routine operations use bounded. Everything gets the detectable layer regardless.
Strong enforcement
The enforced_tool_call MCP tool gates a tool call and signs the decision into the audit trail. Call it instead of the downstream tool.
# Request a tool call through the proxy
enforced_tool_call(
tool_name="sql:execute",
agent_id="agt_xxx",
arguments='{"query": "DELETE FROM users WHERE inactive = true"}'
)
# Returns APPROVED, DENIED, or PENDING_APPROVAL
When a tool_endpoint is configured, the approved call is forwarded and the response captured, producing a bilateral receipt that signs request and response together.
Bounded enforcement
The gate_action MCP tool is a pre-execution gate. Call it before an irreversible action, then call complete_action to close the bilateral receipt.
# 1. Check before acting
gate_action(
action_type="data:delete:users",
agent_id="agt_xxx",
tool_name="database",
risk_context="Bulk deletion of inactive user records"
)
# Returns: {"decision": "APPROVED", "gate_id": "...", "signature_id": "..."}
# 2. Perform the action, then report the outcome
complete_action(
gate_id="gate_xxx",
result="Deleted 42 inactive records"
)
# Returns: bilateral receipt linking approval + outcome
Detectable enforcement
The sign_action tool records what happened with cryptographic proof. Every signature is hash-chained to the previous one using ML-DSA-65 (FIPS 204), so a tampered or omitted entry breaks the chain and fails verification.
import asqav
asqav.init(api_key="sk_...")
agent = asqav.Agent.create("my-agent")
# Every action is signed and hash-chained
sig1 = agent.sign("data:read", {"table": "users"})
sig2 = agent.sign("data:write", {"table": "users", "rows": 5})
# Verify the chain is intact
result = Asqav.verify(sig2["signature_id"])
# {"valid": true, "chain_intact": true}
Three-phase signing
For high-stakes actions, split signing into three phases: intent, decision, and execution. Each phase produces a signed receipt, and execution only proceeds if the policy evaluation approves it.
# Three-phase signing: intent, decision, execution
chain = agent.sign_with_phases("data:delete", {"table": "users"})
print(chain.approved) # True if policy allowed it
print(chain.intent) # signed intent receipt
print(chain.decision) # policy evaluation result
print(chain.execution) # signed execution receipt (only if approved)
If the policy denies the action, chain.execution is None and chain.approved is False.
Tool policies
Tool policies control how enforced_tool_call and gate_action handle specific tools. Set them via the ASQAV_PROXY_TOOLS environment variable or at runtime with create_tool_policy.
export ASQAV_PROXY_TOOLS='{
"sql:execute": {"risk_level": "high", "require_approval": true, "max_calls_per_minute": 5},
"file:delete": {"blocked": true},
"api:external": {"risk_level": "medium", "max_calls_per_minute": 30}
}'
| Option | Type | Description |
|---|---|---|
risk_level |
string | "low", "medium", or "high". High-risk tools can require human approval. |
require_approval |
boolean | If true, high-risk tools trigger multi-party approval before execution. |
max_calls_per_minute |
integer | Rate limit. 0 means unlimited. |
blocked |
boolean | If true, the tool call is denied with a reason. The agent knows the tool exists but cannot use it. |
hidden |
boolean | If true, the tool is invisible to the agent. Stronger than blocked - the agent cannot discover or call the tool. |
tool_endpoint |
string | HTTP endpoint to forward approved calls to. Enables automatic bilateral receipts. |
Bilateral receipts
A bilateral receipt signs both the request and the outcome together as one record. Create one via gate_action + complete_action (bounded), or via enforced_tool_call with a configured tool_endpoint (strong).
Observe mode
Observe mode logs what an enforcement check would do without calling the API or blocking any action. Use it to validate policies before deploying them.
import asqav
from asqav.extras.langchain import AsqavCallbackHandler
asqav.init(api_key="sk_...")
# observe=True logs what would be signed without signing or blocking
handler = AsqavCallbackHandler(agent_name="my-agent", observe=True)
# Log output: OBSERVE: would sign tool:search with context {...}
Semantic action patterns
The SDK ships named semantic patterns that resolve to glob equivalents, so a policy can target a category of actions without hand-writing the glob.
# Resolve a named pattern to its glob
import asqav
asqav.resolve_pattern("sql-destructive") # "data:delete:*"
asqav.list_patterns() # every built-in pattern name and its glob
# Use the resolved glob as the policy's action_pattern
# (POST /api/v1/policies, see the Policies page for the full schema)
The available pattern names are listed in the Policies reference.
Preflight explanations
A preflight check returns a plain-English explanation alongside the cleared/blocked outcome, so a denial is never a bare boolean.
result = agent.preflight("database:delete:users")
print(result.cleared) # False
print(result.explanation) # human-readable reason
Fail-closed semantics
If an enforcement check fails for any reason (network error, API timeout, malformed policy), the action is denied by default. The error is signed into the audit trail.
Claude Code hook
The SDK ships a Claude Code hook that acts as the gateway where policies are enforced. The asqav hook CLI reads each PreToolUse and PostToolUse event, checks the policy, and signs the decision into the audit trail. A blocked action exits non-zero so the tool call never runs.
Setup
Install the SDK and run the hook from your Claude Code settings:
pip install asqav
export ASQAV_API_KEY="sk_..."
asqav hook pretool
Wire asqav hook pretool and asqav hook posttool into the matching hook events in your Claude Code settings.json. See the asqav-sdk repository for the hook reference.