Step 1 - Define the authority certificate
Start with a small certificate type. Keep it serializable so it can be logged, signed, stored, and replayed in tests.
type SandboxClass = "read_only" | "dry_run" | "isolated_write" | "approved_side_effect";
type AgentAuthorityCertificate = {
id: string;
actorId: string;
taskId: string;
taskKind: "research" | "data_query" | "code_change" | "browser_action";
allowedTools: string[];
resourceScopes: string[];
sandboxClass: SandboxClass;
verifierRef: string;
expiresAt: string;
issuedAt: string;
signature: string;
};
Reasoning rule: if a field changes whether a tool call is allowed, it belongs in the certificate or a policy object, not in hidden prompt text.
Step 2 - Issue the certificate at the task boundary
Issue certificates when a human, scheduler, or trusted service creates the task. Do not let the model mint its own authority.
function issueCertificate(task: {
id: string;
actorId: string;
kind: AgentAuthorityCertificate["taskKind"];
risk: "low" | "medium" | "high";
}): AgentAuthorityCertificate {
const sandboxClass: SandboxClass = task.risk === "high" ? "dry_run" : "read_only";
return signCertificate({
id: crypto.randomUUID(),
actorId: task.actorId,
taskId: task.id,
taskKind: task.kind,
allowedTools: task.kind === "data_query" ? ["schema.search", "sql.preflight", "sql.read"] : ["web.search"],
resourceScopes: task.kind === "data_query" ? ["warehouse:readonly", "catalog:approved"] : ["sources:allowlist"],
sandboxClass,
verifierRef: `${task.kind}.default`,
issuedAt: new Date().toISOString(),
expiresAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
signature: "",
});
}
Keep the original unsigned payload in the audit trace. That makes signature failures debuggable without exposing secrets.
Step 3 - Require a preflight check for every tool call
Every adapter should receive a preflight decision before execution. The model may propose a tool call, but the policy layer decides whether it can run.
type ToolRequest = {
certificateId: string;
toolName: string;
input: unknown;
requestedScope: string;
};
type ToolDecision =
| { status: "allow"; sandboxClass: SandboxClass; reason: string }
| { status: "deny"; reason: string }
| { status: "needs_approval"; reason: string };
function preflightToolCall(cert: AgentAuthorityCertificate, request: ToolRequest): ToolDecision {
if (new Date(cert.expiresAt).getTime() < Date.now()) {
return { status: "deny", reason: "certificate expired" };
}
if (!cert.allowedTools.includes(request.toolName)) {
return { status: "deny", reason: `tool ${request.toolName} is outside certificate scope` };
}
if (!cert.resourceScopes.includes(request.requestedScope)) {
return { status: "deny", reason: `scope ${request.requestedScope} is not delegated` };
}
if (cert.sandboxClass !== "approved_side_effect" && request.toolName.endsWith(".write")) {
return { status: "needs_approval", reason: "write tools require explicit side-effect authority" };
}
return { status: "allow", sandboxClass: cert.sandboxClass, reason: "certificate and request scope match" };
}
The important property is not the exact TypeScript shape. The important property is that adapter code cannot run unless this decision is allow.
Step 4 - Bind verification to the certificate
A certificate should point to the verifier that defines "done." Example verifier contracts:
| Task kind | Verifier contract |
|---|
research | every claim includes a source URL from the allowlist |
data_query | numeric claims match read-only SQL output and include limitations |
code_change | diff touches approved paths and tests pass |
browser_action | action log contains target URL, selector, and confirmation state |
Use verifier failures to revoke or downgrade authority. For example, a data_query task that cannot cite a metric definition should lose SQL execution authority until the planner retrieves the missing semantic context.
Step 5 - Add sandbox harm tests
Before shipping, write tests that separate three failure classes:
- Semantic failure: the agent misunderstands the task.
- Audit-evidence failure: the agent cannot prove why a tool call was allowed.
- Sandbox harm: the tool call could mutate data, leak secrets, or trigger an external action.
const blocked = preflightToolCall(readOnlyCert, {
certificateId: readOnlyCert.id,
toolName: "sql.write",
requestedScope: "warehouse:readonly",
input: "delete from customers",
});
expect(blocked.status).toBe("needs_approval");
Tests like this are more reliable than asking the model to remember "never delete data."
Step 6 - Add the audit record
Persist one event per decision:
{
"run_id": "run_123",
"certificate_id": "cert_456",
"tool": "sql.preflight",
"requested_scope": "warehouse:readonly",
"decision": "allow",
"sandbox_class": "read_only",
"verifier_ref": "data_query.default",
"reason": "certificate and request scope match"
}
At review time, you should be able to answer: who delegated authority, what was allowed, which tool ran, what evidence was produced, and why the run stopped.