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:

  1. Creates a formal approval record in the approvals table with type: "request_board_approval"
  2. Links the approval to the current issue via the issue_approvals join table
  3. 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.ts engine returns require_approval decisions that trigger governance workflows automatically.
  • Immutable audit trails: Every approval creates records in the approvals and issue_approvals tables 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.create API, enabling one-click approve/reject actions.
  • SDK integration: The worker.approvals.decide method in packages/plugins/sdk/src/worker-rpc-host.ts provides 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →