refactor(approvals): move execution policy into database

This commit is contained in:
ZacharyZhang-NY committed 2026-04-24 00:33:56 -04:00
1 parent f8eb57ad8c
commit 9ee1425a71
12 files changed
+433 -101

No files matched your search

+63 -3
View File
@@ -18,12 +18,12 @@ import {
type ActiveRunContext,
} from "../../src/main/agent-runtime/hook-handlers";
function seedCofounderRun(db: AppDatabase) {
function seedRun(db: AppDatabase, executionMode: "ai_ceo" | "ai_cofounder" = "ai_cofounder") {
const companyId = db.core.company.saveCompany({
name: "Gate Co",
description: "Cofounder runtime gate test",
status: "active",
executionMode: "ai_cofounder",
executionMode,
});
db.core.connectors.saveConnector({
id: "openai-codex",
@@ -87,7 +87,7 @@ describe("cofounder approval runtime gate", () => {
});
it("blocks destructive tools while a cofounder plan review is pending", () => {
const { companyId, agentId, taskId, runContext } = seedCofounderRun(db);
const { companyId, agentId, taskId, runContext } = seedRun(db, "ai_cofounder");
const approvalId = db.core.approvals.requestApproval({
companyId,
relatedTaskId: taskId,
@@ -117,4 +117,64 @@ describe("cofounder approval runtime gate", () => {
expect(result.block).toBe(true);
expect(result.reason).toContain("plan_review");
});
it("lets CEO-mode execution proceed past plan review policy", () => {
const { companyId, agentId, taskId, runContext } = seedRun(db, "ai_ceo");
db.core.approvals.requestApproval({
companyId,
relatedTaskId: taskId,
requestedByAgentId: agentId,
type: "plan_review",
payloadSummary: "Review plan",
impactSummary: "Human review requested for planning.",
payloadJson: "{}",
});
expect(db.core.approvals.getBlockingApproval(companyId, agentId, taskId)).toBeNull();
const result = handleBeforeToolCall(
{
db,
logger: { info: () => {}, warn: () => {}, error: () => {} },
},
runContext,
{
toolName: "bash",
toolCallId: "call-1",
args: { command: "touch allowed.txt" },
},
);
expect(result.block).toBe(false);
});
it("blocks operational approvals in CEO mode", () => {
const { companyId, agentId, taskId, runContext } = seedRun(db, "ai_ceo");
const approvalId = db.core.approvals.requestApproval({
companyId,
relatedTaskId: taskId,
requestedByAgentId: agentId,
type: "dangerous_command",
payloadSummary: "Approve destructive command",
impactSummary: "Host-level mutation requires explicit approval.",
payloadJson: "{}",
});
const blocking = db.core.approvals.getBlockingApproval(companyId, agentId, taskId);
expect(blocking?.id).toBe(approvalId);
const result = handleBeforeToolCall(
{
db,
logger: { info: () => {}, warn: () => {}, error: () => {} },
},
runContext,
{
toolName: "bash",
toolCallId: "call-1",
args: { command: "rm -rf dist" },
},
);
expect(result.block).toBe(true);
expect(result.reason).toContain("dangerous_command");
});
});
+22
View File
@@ -14,6 +14,14 @@ type IpcHandler = (
...args: unknown[]
) => Promise<DesktopResult<unknown>> | DesktopResult<unknown>;
type RawDb = {
db: {
prepare: (sql: string) => {
run: (...args: unknown[]) => void;
};
};
};
const tempDirs: string[] = [];
async function createTempDir(prefix: string) {
@@ -111,10 +119,23 @@ describe("company mode switching", () => {
payloadJson: "{}",
});
threadIntegration.onApprovalCreated(approvalId, companyId, taskId, "deliverable_review", "Review deliverable");
(db as unknown as RawDb).db
.prepare("update approval_policies set blocks_execution = 1, required_mode = 'ai_cofounder', updated_at = ? where approval_type = 'incident_escalation'")
.run(new Date().toISOString());
const policyDrivenApprovalId = db.core.approvals.requestApproval({
companyId,
relatedTaskId: taskId,
requestedByAgentId: engineerId,
type: "incident_escalation",
payloadSummary: "Review incident policy",
impactSummary: "Data-driven cofounder review policy.",
payloadJson: "{}",
});
expect(db.core.threads.listThreadWaitStates(companyId).length).toBeGreaterThan(0);
expect(db.core.entity.getTask(taskId).requiresUserReview).toBe(true);
expect(db.core.approvals.getApprovalRecord(approvalId).state).toBe("pending");
expect(db.core.approvals.getApprovalRecord(policyDrivenApprovalId).state).toBe("pending");
const handlers = new Map<string, IpcHandler>();
registerCoreAdminHandlers({
@@ -151,6 +172,7 @@ describe("company mode switching", () => {
expect(db.core.threads.listThreadWaitStates(companyId)).toHaveLength(0);
expect(db.core.entity.getTask(taskId).requiresUserReview).toBe(false);
expect(db.core.approvals.getApprovalRecord(approvalId).state).toBe("approved");
expect(db.core.approvals.getApprovalRecord(policyDrivenApprovalId).state).toBe("approved");
db.close();
});
+33
View File
@@ -3,6 +3,7 @@ import { tmpdir } from "node:os";
import { mkdtemp, rm } from "node:fs/promises";
import { DatabaseSync } from "node:sqlite";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { approvalTypeEnum } from "../../src/shared/types";
beforeAll(() => {
process.env.AGENTCOMPANY_ALLOW_INSECURE_VAULT = "true";
@@ -479,6 +480,7 @@ describe("AppDatabase", () => {
expect(migrations).toEqual([
{ version: 56, name: "baseline_v56" },
{ version: 57, name: "task_leases_runtime_ownership" },
{ version: 58, name: "approval_policy_table" },
]);
db.close();
@@ -491,6 +493,7 @@ describe("AppDatabase", () => {
expect(migrationsAfterReinit).toEqual([
{ version: 56, name: "baseline_v56" },
{ version: 57, name: "task_leases_runtime_ownership" },
{ version: 58, name: "approval_policy_table" },
]);
});
@@ -535,6 +538,36 @@ describe("AppDatabase", () => {
}
});
it("seeds approval policies for every approval type", () => {
const rows = db.queryAll<{
approvalType: string;
scopeType: string;
blocksExecution: number;
requiredMode: string;
autoApprovable: number;
}>(
"select approval_type as approvalType, scope_type as scopeType, blocks_execution as blocksExecution, required_mode as requiredMode, auto_approvable as autoApprovable from approval_policies order by approval_type",
);
expect(rows.map((row) => row.approvalType).sort()).toEqual([...approvalTypeEnum.options].sort());
expect(rows.find((row) => row.approvalType === "plan_review")).toMatchObject({
scopeType: "requester_or_task",
blocksExecution: 1,
requiredMode: "ai_cofounder",
autoApprovable: 0,
});
expect(rows.find((row) => row.approvalType === "dangerous_command")).toMatchObject({
scopeType: "requester_or_task",
blocksExecution: 1,
requiredMode: "any",
});
expect(rows.find((row) => row.approvalType === "hire_agent")).toMatchObject({
scopeType: "related_agent",
blocksExecution: 1,
requiredMode: "any",
autoApprovable: 1,
});
});
it("acquires and releases workspace locks", () => {
const companyId = db.core.company.saveCompany({
name: "Lock Test Co",
@@ -172,6 +172,7 @@ describe("legacy to v56 baseline migration", () => {
expect(appDb.queryAll("select version, name from schema_migrations")).toEqual([
{ version: 56, name: "baseline_v56" },
{ version: 57, name: "task_leases_runtime_ownership" },
{ version: 58, name: "approval_policy_table" },
]);
expect(appDb.queryAll<{ companyId: string }>("select company_id as companyId from secrets where id = 'secret-1'")[0])
.toEqual({ companyId: "company-1" });
@@ -204,6 +205,7 @@ describe("legacy to v56 baseline migration", () => {
expect(appDb.queryAll("select version, name from schema_migrations")).toEqual([
{ version: 56, name: "baseline_v56" },
{ version: 57, name: "task_leases_runtime_ownership" },
{ version: 58, name: "approval_policy_table" },
]);
expect(appDb.queryAll("select id from budget_policies where scope_type = 'agent' and scope_id = 'agent-1'"))
.toHaveLength(1);
@@ -363,6 +365,7 @@ describe("legacy to v56 baseline migration", () => {
expect(appDb.queryAll("select version, name from schema_migrations")).toEqual([
{ version: 56, name: "baseline_v56" },
{ version: 57, name: "task_leases_runtime_ownership" },
{ version: 58, name: "approval_policy_table" },
]);
} finally {
appDb.close();
@@ -116,11 +116,12 @@ describe("legacy to v56 baseline migration", () => {
{ id: "task-inflight", requiresFilesystem: 1 },
]);
const migrations = appDb.queryAll<{ version: number; name: string }>(
"select version, name from schema_migrations where version in (56, 57) order by version",
"select version, name from schema_migrations where version in (56, 57, 58) order by version",
);
expect(migrations).toEqual([
{ version: 56, name: "baseline_v56" },
{ version: 57, name: "task_leases_runtime_ownership" },
{ version: 58, name: "approval_policy_table" },
]);
} finally {
appDb.close();