Enforcing Approval Gates with Paperclip AI Governance System
Paperclip AI enforces human-in-the-loop approval for high-risk operations by detecting policy decisions, creating formal board-level approval records, and blocking execution until authorized operators explicitly approve or reject the request.
The paperclipai/paperclip repository implements a robust governance layer that protects every destructive or write-classified tool invocation. When the policy engine determines that an action requires human consent, the system automatically pauses execution and escalates to board-level operators through interactive approval cards.
How Approval Gates Work in Paperclip AI
The approval gate mechanism coordinates across the Tool Gateway, Policy Engine, and Database Layer to ensure no high-risk operation executes without explicit authorization.
Policy Evaluation and Decision Detection
The governance flow begins in server/src/services/tool-access-policy.ts. When evaluating tool access, policies can return a require_approval decision that propagates to the gateway.
// server/src/services/tool-access-policy.ts (line 1249)
if (accessDecision.decision === "require_approval") return "pending";
This decision signals that the requested operation exceeds automated risk thresholds and requires human review.
Gateway Orchestration and Record Creation
The Tool Gateway (server/src/services/tool-gateway.ts at line 1711) detects the policyDecision === "require_approval" condition and initiates the approval workflow. The gateway performs three critical operations:
- Creates a formal approval record in the
approvalstable withtype: "request_board_approval" - Links the approval to the current issue via the
issue_approvalsjoin table - Posts an interactive confirmation card on the issue thread
// server/src/services/tool-gateway.ts (lines 15113-15134)
const [approval] = await db
.insert(approvals)
.values({
companyId: input.session.companyId,
type: "request_board_approval",
requestedByAgentId: input.session.agentId,
payload: {
title: `Approve high‑risk tool action: ${input.tool.name}`,
// Tool arguments, risk level, and preview data
},
})
.returning();
// Link approval to issue (lines 15138-15144)
await db.insert(issueApprovals).values({
companyId: input.session.companyId,
issueId: input.session.issueId,
approvalId: approval.id,
linkedByAgentId: input.session.agentId,
});
Interactive UI Cards and Execution Blocking
After creating the database records, the gateway posts an interactive card and sets the invocation status to awaiting_approval (line 2931).
// server/src/services/tool-gateway.ts (lines 15148-15186)
const interaction = await interactions.create(
{ id: input.session.issueId, companyId: input.session.companyId },
{
kind: "request_confirmation",
title: "Approve tool action",
summary: `${input.tool.name} requires approval before Paperclip will execute it.`,
payload: {
target: { type: "custom", key: `tool-action:${actionRequest.id}` },
// Approve/Reject buttons rendered in the UI
},
}
);
// Pause execution (line 2931)
await db.update(toolInvocations).set({
status: "awaiting_approval",
approvalState: "pending",
});
The tool remains blocked until a board operator interacts with the approval card.
Database Schema for Approval Tracking
Paperclip AI persists approval state across two primary tables that enable audit trails and cross-reference capabilities.
The Approvals Table
The approvals table in packages/db/src/schema/approvals.ts stores every request with JSON payloads and decision metadata:
// packages/db/src/schema/approvals.ts
export const approvals = pgTable("approvals", {
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull(),
type: text("type").notNull(), // "request_board_approval"
requestedByAgentId: uuid("requested_by_agent_id"),
requestedByUserId: text("requested_by_user_id"),
status: text("status").notNull().default("pending"),
payload: jsonb("payload").$type<Record<string, unknown>>().notNull(),
decisionNote: text("decision_note"),
decidedByUserId: text("decided_by_user_id"),
decidedAt: timestamp("decided_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(),
});
Issue-Approval Linking
The issue_approvals join table connects approvals to their triggering issues:
// packages/db/src/schema/issue_approvals.ts
export const issueApprovals = pgTable("issue_approvals", {
companyId: uuid("company_id").notNull(),
issueId: uuid("issue_id").notNull(),
approvalId: uuid("approval_id").notNull().references(() => approvals.id, { onDelete: "cascade" }),
linkedByAgentId: uuid("linked_by_agent_id"),
});
Implementing Approval Flows in Code
Developers can interact with the approval system through the REST API and SDK methods.
Triggering Approval-Gated Tool Calls
When invoking high-risk tools, the gateway automatically handles approval requirements. Client code can detect pending approvals via HTTP 409 responses:
import { api } from "@paperclipai/mcp-server";
async function runRiskyTool(session, toolName, args) {
const response = await api.requestJson("POST", "/tool-gateway/run", {
sessionId: session.id,
tool: { name: toolName, risk: "destructive" },
arguments: args,
});
// Detect approval requirement (tool-gateway.ts lines 15126-15134)
if (response.status === 409 && response.body?.errorCode === "approval_path_missing") {
console.log("Approval required – board will be notified.");
return;
}
return await response.json();
}
Processing Approval Decisions
Board operators approve or reject requests through the SDK, which routes to packages/plugins/sdk/src/worker-rpc-host.ts:
import { worker } from "@paperclipai/plugins-sdk";
async function handleApprovalDecision(approvalId, decision) {
await worker.approvals.decide({
approvalId,
decision: decision ? "approved" : "rejected",
decisionNote: "Reviewed and authorized by security team",
});
}
Upon approval, the gateway resumes execution (lines 16171-16180 in tool-gateway.ts) and replays the original signed arguments.
Querying Pending Approvals
Applications can fetch pending approvals via the REST API defined in packages/mcp-server/src/tools.ts:
import { client } from "@paperclipai/mcp-server";
async function fetchPendingApprovals(companyId) {
return await client.requestJson(
"GET",
`/companies/${companyId}/approvals?status=pending`
);
}
Summary
- Policy-driven enforcement: The
tool-access-policy.tsengine returnsrequire_approvaldecisions that trigger governance workflows automatically. - Immutable audit trails: Every approval creates records in the
approvalsandissue_approvalstables with full payload history and decision metadata. - Execution blocking: The Tool Gateway sets
status: "awaiting_approval"and pauses tool invocation until explicit authorization occurs. - Interactive resolution: Approval cards surface directly in issue threads via the
interactions.createAPI, enabling one-click approve/reject actions. - SDK integration: The
worker.approvals.decidemethod inpackages/plugins/sdk/src/worker-rpc-host.tsprovides programmatic access to decision handling.
Frequently Asked Questions
How does Paperclip AI determine when to require approval?
The Tool Access Policy evaluates each tool invocation against company-specific policies. When a tool is classified as destructive or write-capable, and the policy configuration mandates human oversight, the policy engine returns require_approval at line 1249 of server/src/services/tool-access-policy.ts. This decision propagates to the Tool Gateway, which initiates the approval flow.
What happens to tool execution while waiting for approval?
The gateway explicitly blocks execution by updating the toolInvocations table with status: "awaiting_approval" (line 2931 in server/src/services/tool-gateway.ts). The original signed arguments are preserved in the approval payload, and execution only resumes after the approvals.decide endpoint processes an "approved" decision and updates the status to "executing".
Can approvals be programmatically queried and managed?
Yes. The REST API in packages/mcp-server/src/tools.ts exposes endpoints for listing approvals by company and status. The SDK provides worker.approvals.list, worker.approvals.get, and worker.approvals.decide methods implemented in packages/plugins/sdk/src/worker-rpc-host.ts, enabling full programmatic control over the approval lifecycle.
Where are approval records stored and how are they linked to issues?
Approval records reside in the approvals table defined in packages/db/src/schema/approvals.ts. The system creates a corresponding entry in the issue_approvals join table (packages/db/src/schema/issue_approvals.ts) that links the approval UUID to the specific issue ID, enabling audit trails that trace every high-risk operation back to its original context.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →