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

+61 -28
View File
@@ -3,11 +3,15 @@ import type { DatabaseSync, SQLInputValue } from "node:sqlite";
import log from "electron-log/main.js";
import type { ApprovalRecord } from "@shared/types";
import type { ReadAll, Transaction } from "./helpers";
import { normalizeOptionalId, nowIso, parseJson } from "./helpers";
import {
COFOUNDER_MANDATORY_APPROVAL_TYPES,
OPERATIONAL_APPROVAL_TYPES,
} from "../execution-policy";
import { normalizeOptionalId, nowIso, parseJson, toBoolean } from "./helpers";
export interface ApprovalPolicyRecord {
approvalType: string;
scopeType: string;
blocksExecution: boolean;
requiredMode: "any" | "ai_ceo" | "ai_cofounder";
autoApprovable: boolean;
}
export function buildApprovalMethods(
getDb: () => DatabaseSync,
@@ -239,36 +243,65 @@ export function buildApprovalMethods(
return readAll(sql, ...args);
},
getApprovalPolicy(approvalType: string): ApprovalPolicyRecord | null {
const row = getDb()
.prepare(
"select approval_type as approvalType, scope_type as scopeType, blocks_execution as blocksExecution, required_mode as requiredMode, auto_approvable as autoApprovable from approval_policies where approval_type = ?",
)
.get(approvalType) as {
approvalType: string;
scopeType: string;
blocksExecution: number;
requiredMode: "any" | "ai_ceo" | "ai_cofounder";
autoApprovable: number;
} | undefined;
return row
? {
approvalType: row.approvalType,
scopeType: row.scopeType,
blocksExecution: toBoolean(row.blocksExecution),
requiredMode: row.requiredMode,
autoApprovable: toBoolean(row.autoApprovable),
}
: null;
},
getBlockingApproval(companyId: string, agentId: string, taskId: string | null): ApprovalRecord | null {
const taskCondition = taskId != null
? "(related_task_id = ? or requested_by_agent_id = ?)"
: "requested_by_agent_id = ?";
const taskParams: SQLInputValue[] = taskId != null ? [taskId, agentId] : [agentId];
const operationalApprovalPlaceholders = OPERATIONAL_APPROVAL_TYPES.map(() => "?").join(", ");
const cofounderApprovalPlaceholders = COFOUNDER_MANDATORY_APPROVAL_TYPES.map(() => "?").join(", ");
const company = getDb()
.prepare("select execution_mode as executionMode from companies where id = ?")
.get(companyId) as { executionMode: "ai_ceo" | "ai_cofounder" } | undefined;
if (!company) {
throw new Error(`Company not found for approval gate: ${companyId}`);
}
const rows = readAll<ApprovalRecord>(
`select id, company_id as companyId, related_task_id as relatedTaskId,
requested_by_agent_id as requestedByAgentId, related_agent_id as relatedAgentId,
type, payload_summary as payloadSummary, impact_summary as impactSummary,
a.type, payload_summary as payloadSummary, impact_summary as impactSummary,
payload_json as payloadJson, decision_note as decisionNote, state,
created_at as createdAt, updated_at as updatedAt
from approvals
where company_id = ? and state = 'pending'
a.created_at as createdAt, a.updated_at as updatedAt
from approvals a
inner join approval_policies p on p.approval_type = a.type
where a.company_id = ?
and a.state = 'pending'
and p.blocks_execution = 1
and (p.required_mode = 'any' or p.required_mode = ?)
and (
(type = 'hire_agent' and related_agent_id = ?)
or (type = 'dangerous_command' and ${taskCondition})
or (type = 'secret_access' and ${taskCondition})
or (type = 'approve_ceo_strategy' and ${taskCondition})
or (type = 'deliverable_review' and (requested_by_agent_id = ? or (related_task_id is not null and ${taskCondition})))
or (type in (${operationalApprovalPlaceholders}) and ${taskCondition})
or (type in (${cofounderApprovalPlaceholders}) and (requested_by_agent_id = ? or (related_task_id is not null and ${taskCondition})))
(p.scope_type = 'related_agent' and a.related_agent_id = ?)
or (
p.scope_type = 'requester_or_task'
and (
a.requested_by_agent_id = ?
or (? is not null and a.related_task_id = ?)
)
)
)
order by created_at asc limit 1`,
companyId, agentId,
...taskParams, ...taskParams, ...taskParams,
agentId, ...taskParams,
...OPERATIONAL_APPROVAL_TYPES, ...taskParams,
...COFOUNDER_MANDATORY_APPROVAL_TYPES, agentId, ...taskParams,
order by a.created_at asc limit 1`,
companyId,
company.executionMode,
agentId,
agentId,
taskId,
taskId,
);
return rows.length > 0 ? rows[0] : null;
},
@@ -0,0 +1,110 @@
import type { DatabaseSync } from "node:sqlite";
import { APPROVAL_POLICY_SEED } from "../../approval-policy-definitions";
import { nowIso, tableExists } from "./types";
export const APPROVAL_POLICY_SCHEMA_VERSION = 58;
export const APPROVAL_POLICY_SCHEMA_NAME = "approval_policy_table";
export function migrateApprovalPoliciesV58(db: DatabaseSync): void {
installApprovalPoliciesTable(db);
seedApprovalPolicies(db);
db
.prepare("insert or replace into schema_migrations (version, name, applied_at) values (?, ?, ?)")
.run(APPROVAL_POLICY_SCHEMA_VERSION, APPROVAL_POLICY_SCHEMA_NAME, nowIso());
assertApprovalPoliciesV58(db);
}
export function assertApprovalPoliciesV58(db: DatabaseSync): void {
if (!tableExists(db, "approval_policies")) {
throw new Error("Schema v58 drift: approval_policies table is missing.");
}
const columns = db
.prepare("pragma table_info(approval_policies)")
.all()
.map((row) => row as { name: string; type: string; notnull: number; pk: number });
const actualColumns = columns.map((column) => [
column.name,
column.type.toUpperCase(),
column.notnull,
column.pk,
]);
const expectedColumns = [
["approval_type", "TEXT", 0, 1],
["scope_type", "TEXT", 1, 0],
["blocks_execution", "INTEGER", 1, 0],
["required_mode", "TEXT", 1, 0],
["auto_approvable", "INTEGER", 1, 0],
["created_at", "TEXT", 1, 0],
["updated_at", "TEXT", 1, 0],
];
if (JSON.stringify(actualColumns) !== JSON.stringify(expectedColumns)) {
throw new Error("Schema v58 drift: approval_policies columns do not match the canonical schema.");
}
const rows = db
.prepare(
"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",
)
.all() as Array<{
approvalType: string;
scopeType: string;
blocksExecution: number;
requiredMode: string;
autoApprovable: number;
}>;
const actual = rows.map((row) => ({
approvalType: row.approvalType,
scopeType: row.scopeType,
blocksExecution: row.blocksExecution,
requiredMode: row.requiredMode,
autoApprovable: row.autoApprovable,
}));
const expected = [...APPROVAL_POLICY_SEED]
.sort((a, b) => a.approvalType.localeCompare(b.approvalType))
.map((policy) => ({
approvalType: policy.approvalType,
scopeType: policy.scopeType,
blocksExecution: policy.blocksExecution ? 1 : 0,
requiredMode: policy.requiredMode,
autoApprovable: policy.autoApprovable ? 1 : 0,
}));
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
throw new Error("Schema v58 drift: approval_policies seed rows do not match the canonical policy.");
}
}
function installApprovalPoliciesTable(db: DatabaseSync): void {
db.exec(`
create table if not exists approval_policies (
approval_type text primary key,
scope_type text not null check (scope_type in ('requester_or_task', 'related_agent')),
blocks_execution integer not null check (blocks_execution in (0, 1)),
required_mode text not null check (required_mode in ('any', 'ai_ceo', 'ai_cofounder')),
auto_approvable integer not null check (auto_approvable in (0, 1)),
created_at text not null,
updated_at text not null
);
`);
}
function seedApprovalPolicies(db: DatabaseSync): void {
const ts = nowIso();
const stmt = db.prepare(
`insert or replace into approval_policies (
approval_type, scope_type, blocks_execution, required_mode, auto_approvable, created_at, updated_at
) values (?, ?, ?, ?, ?, coalesce((select created_at from approval_policies where approval_type = ?), ?), ?)`,
);
for (const policy of APPROVAL_POLICY_SEED) {
stmt.run(
policy.approvalType,
policy.scopeType,
policy.blocksExecution ? 1 : 0,
policy.requiredMode,
policy.autoApprovable ? 1 : 0,
policy.approvalType,
ts,
ts,
);
}
}
+19 -1
View File
@@ -14,12 +14,17 @@ import {
assertTaskLeasesV57,
migrateTaskLeasesV57,
} from "./migrations/v57-task-leases";
import {
APPROVAL_POLICY_SCHEMA_VERSION,
assertApprovalPoliciesV58,
migrateApprovalPoliciesV58,
} from "./migrations/v58-approval-policies";
import {
type ReadAll,
type Transaction,
} from "./migrations/types";
const LATEST_SCHEMA_VERSION = TASK_LEASE_SCHEMA_VERSION;
const LATEST_SCHEMA_VERSION = APPROVAL_POLICY_SCHEMA_VERSION;
export function initializeDatabase(
db: DatabaseSync,
@@ -38,6 +43,16 @@ export function initializeDatabase(
tx(() => {
seedSettings(db);
assertTaskLeasesV57(db);
assertApprovalPoliciesV58(db);
});
return;
}
if (currentVersion === TASK_LEASE_SCHEMA_VERSION) {
tx(() => {
seedSettings(db);
assertTaskLeasesV57(db);
migrateApprovalPoliciesV58(db);
});
return;
}
@@ -49,6 +64,7 @@ export function initializeDatabase(
seedSettings(db);
assertBaselineSchema(db);
migrateTaskLeasesV57(db);
migrateApprovalPoliciesV58(db);
});
return;
}
@@ -61,6 +77,7 @@ export function initializeDatabase(
seedSettings(db);
assertBaselineSchema(db);
migrateTaskLeasesV57(db);
migrateApprovalPoliciesV58(db);
});
return;
}
@@ -71,6 +88,7 @@ export function initializeDatabase(
seedSettings(db);
assertBaselineSchema(db);
migrateTaskLeasesV57(db);
migrateApprovalPoliciesV58(db);
});
}