Enforcement & pre-execution authorization
Monitor mode records what agents did. Enforcement decides whether they may do it. The agent asks ProofLedger before a protected action, and the protected tool refuses to run without a valid, single-use token. A modified or compromised agent SDK cannot skip that check.
How it works
- The agent signs an authorization request with its own Ed25519 key. The request covers the action, the tool, the MCP server, a SHA-256 hash of the exact tool arguments, a one-time nonce, and a timestamp. Only the hash is sent, never the raw arguments.
- ProofLedger checks the agent's signature against its registered public key. It rejects requests older than five minutes and reused nonces (replays). It confirms the agent is active or warning. It resolves the tool against your verified registry (unknown, unapproved, wrong-MCP-server and not-allowed-for-this-agent tools are blocked) and then evaluates your policies. Every attempt lands in the agent's audit chain and affects its trust score.
- On allow, the agent receives a token signed by your project's key. It is valid for 120 seconds, can be used once, and is bound to that agent, tool, server, and argument hash. On require_approval, no token exists until a human approves in the dashboard (30-minute window).
- The tool verifies and burns the token before executing. If the token is missing, expired, already used, forged, or issued for different arguments, the tool does not run.
Agent side
import { ProofLedger, AuthorizationDeniedError } from "@proofledger/sdk";
const pl = ProofLedger.enable({
apiKey: process.env.PROOFLEDGER_API_KEY,
projectId: "proj_...",
agentName: "billing-agent",
policyMode: "enforce", // refuses to start without an apiKey
});
// First run: persist result.privateKey in your secret store.
// Later runs: pass it back so the agent keeps the same identity.
await pl.registerAgent({ privateKey: process.env.AGENT_SIGNING_KEY });
try {
const grant = await pl.authorize({
action: "Refund order 1042",
toolName: "issue_refund",
args: { orderId: 1042, amountUsd: 125 }, // hashed, never sent
});
await callRefundService({ orderId: 1042, amountUsd: 125 }, grant.token);
} catch (err) {
if (err instanceof AuthorizationDeniedError) {
// err.decision: "block" | "require_approval" | "approval_timeout" | "expired" ...
// Fail closed. Do not run the action.
}
throw err;
}import os
from proofledger import enable, AuthorizationDeniedError
pl = enable(api_key=os.environ["PROOFLEDGER_API_KEY"], project_id="proj_...",
agent_name="billing-agent", policy_mode="enforce")
pl.register_agent(private_key=os.environ["AGENT_SIGNING_KEY"])
try:
grant = pl.authorize("Refund order 1042", tool_name="issue_refund",
args={"orderId": 1042, "amountUsd": 125})
call_refund_service({"orderId": 1042, "amountUsd": 125}, grant["token"])
except AuthorizationDeniedError as err:
... # fail closed: err.decision, err.reasonTool side (the part that makes it enforcement)
The protected service verifies the token with its own ProofLedger API key before it does anything. Pass the arguments it is actually about to execute. If they differ from what was authorized, verification fails.
const check = await ProofLedger.verifyAuthorization({
token,
toolName: "issue_refund",
args: { orderId, amountUsd },
});
if (!check.valid) {
// check.reason: already_used | expired | tool_mismatch | arguments_mismatch |
// invalid_signature | agent_not_eligible | authorization_denied ...
return new Response("Forbidden", { status: 403 });
}
// ...execute exactly onceMCP: in enforce mode, wrapMCPClient calls authorize() before every callTool and attaches the token at params._meta["proofledger.dev/authorization"]. On the MCP server, read it with mcpAuthorizationToken(params) and verify it as shown above before the handler runs.
Security
Revocation & identity
- Blocking or deactivating an agent in the dashboard revokes its outstanding authorizations immediately, so tokens that were already issued are rejected at the tool. The tool also re-checks the agent's status on every verification (agent_not_eligible).
- A project API key alone cannot change an agent's status or replace its signing key. Key rotation must be signed by the current key (rotateAgentKey() / rotate_agent_key()).
- Lost the key? An operator can reset it from the agent's page in the dashboard (Reset signing key). This also revokes the agent's outstanding authorizations.
Endpoints
| Method | Path | Description |
|---|---|---|
| POST | /api/authorize | Agent-signed authorization request → allow (token) / require_approval / block. |
| GET | /api/authorizations/:id | Poll a pending authorization; returns the token once approved. |
| POST | /api/authorizations/consume | Tool side: verify + burn a token (single use). |
| GET | /api/authorizations/public-key | Project signing key for offline token verification. |
| POST | /api/agents/:id/rotate-key | Replace an agent key, signed by the current key. |
Current limits
Warning
Questions about the threat model? See Trust & security.