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

-10
View File
@@ -27,10 +27,6 @@ import {
appendArtifactProjectionEvent,
appendArtifactProjectionFailureEvent,
} from "./artifact-projection-events";
import {
isCofounderMandatoryApproval,
isOperationalApproval,
} from "../execution-policy";
/**
* Lifecycle phase of an active pi run as seen by main.
@@ -120,12 +116,6 @@ function checkOperationalApprovalBlock(
try {
const blocking = deps.db.core.approvals.getBlockingApproval(ctx.companyId, ctx.agentId, ctx.taskId);
if (!blocking) return { block: false, reason: null };
const companyMode = deps.db.core.company.getCompanyExecutionMode(ctx.companyId);
const cofounderCheckpointBlocks =
companyMode === "ai_cofounder" && isCofounderMandatoryApproval(blocking.type);
if (!isOperationalApproval(blocking.type) && !cofounderCheckpointBlocks) {
return { block: false, reason: null };
}
return {
block: true,
reason: `Approval ${blocking.id} (${blocking.type}, state=${blocking.state}) is outstanding: ${blocking.payloadSummary}. Wait for the decision before retrying ${toolName}.`,
+97
View File
@@ -0,0 +1,97 @@
import { approvalTypeEnum, type ApprovalType, type ExecutionMode } from "@shared/types";
export type ApprovalPolicyScope = "requester_or_task" | "related_agent";
export type ApprovalPolicyRequiredMode = "any" | ExecutionMode;
export interface ApprovalPolicyDefinition {
approvalType: ApprovalType;
scopeType: ApprovalPolicyScope;
blocksExecution: boolean;
requiredMode: ApprovalPolicyRequiredMode;
autoApprovable: boolean;
}
const DEFAULT_POLICY: Omit<ApprovalPolicyDefinition, "approvalType"> = {
scopeType: "requester_or_task",
blocksExecution: false,
requiredMode: "any",
autoApprovable: false,
};
const POLICY_OVERRIDES: Record<ApprovalType, Partial<Omit<ApprovalPolicyDefinition, "approvalType">>> = {
hire_agent: {
scopeType: "related_agent",
blocksExecution: true,
autoApprovable: true,
},
approve_ceo_strategy: {
blocksExecution: true,
requiredMode: "ai_cofounder",
autoApprovable: true,
},
dangerous_command: {
blocksExecution: true,
},
secret_access: {
blocksExecution: true,
},
budget_request: {},
promotion: {},
termination: {},
vendor_approval: {},
document_review: {
blocksExecution: true,
requiredMode: "ai_cofounder",
},
production_deploy: {
blocksExecution: true,
},
incident_escalation: {},
social_post: {
blocksExecution: true,
},
deliverable_review: {
blocksExecution: true,
requiredMode: "ai_cofounder",
},
budget_override: {
blocksExecution: true,
},
plan_review: {
blocksExecution: true,
requiredMode: "ai_cofounder",
},
scope_review: {
blocksExecution: true,
requiredMode: "ai_cofounder",
},
draft_review: {
blocksExecution: true,
requiredMode: "ai_cofounder",
},
strategy_review: {
blocksExecution: true,
requiredMode: "ai_cofounder",
},
exception_review: {
blocksExecution: true,
requiredMode: "ai_cofounder",
},
};
export const APPROVAL_POLICY_SEED: readonly ApprovalPolicyDefinition[] = approvalTypeEnum.options.map((approvalType) => ({
approvalType,
...DEFAULT_POLICY,
...POLICY_OVERRIDES[approvalType],
}));
export const COFOUNDER_MANDATORY_APPROVAL_TYPES: readonly ApprovalType[] = APPROVAL_POLICY_SEED
.filter((policy) => policy.blocksExecution && policy.requiredMode === "ai_cofounder")
.map((policy) => policy.approvalType);
export const OPERATIONAL_APPROVAL_TYPES: readonly ApprovalType[] = APPROVAL_POLICY_SEED
.filter((policy) =>
policy.blocksExecution &&
policy.requiredMode === "any" &&
policy.scopeType === "requester_or_task")
.map((policy) => policy.approvalType);
+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);
});
}
+7 -17
View File
@@ -12,17 +12,6 @@ type ReconcileExecutionModeChangeInput = {
handleApprovalResolved: (approvalId: string, companyId: string, decision: string) => void;
};
const COFOUNDER_TRANSITION_APPROVAL_TYPES = [
"deliverable_review",
"document_review",
"plan_review",
"scope_review",
"draft_review",
"strategy_review",
"exception_review",
"approve_ceo_strategy",
] as const;
const REVIEW_WAIT_STATE_TYPES = new Set(["user_reply", "user_review", "user_approval"]);
function collectImpactedAgentIdsFromWaitStates(db: AppDatabase, companyId: string): Set<string> {
@@ -122,13 +111,14 @@ export function reconcileExecutionModeChange({
if (leavingCofounderMode) {
const pendingApprovals = db.queryAll<{ id: string; type: string }>(
`select id, type
from approvals
where company_id = ?
and state = 'pending'
and type in (${COFOUNDER_TRANSITION_APPROVAL_TYPES.map(() => "?").join(", ")})`,
`select a.id, a.type
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 = 'ai_cofounder'`,
companyId,
...COFOUNDER_TRANSITION_APPROVAL_TYPES,
);
for (const approval of pendingApprovals) {
resolveApprovalDecision({
+16 -41
View File
@@ -1,17 +1,22 @@
/**
* Execution Policy — Single Source of Truth
* Execution Policy
*
* Centralizes all execution-mode-aware policy decisions:
* - Which approval types require human review in co-founder mode
* - Which approval types can be auto-approved in CEO mode
* - Whether a given approval should be auto-approved based on mode + company settings
*
* checkpoint-engine.ts and approval-policy.ts both delegate here
* instead of maintaining separate rule sets.
* Runtime decisions read the seeded approval_policies table. The exported
* constants below exist for prompt/docs contracts that need the same seed
* vocabulary without opening SQLite.
*/
import type { AppDatabase } from "./database";
import type { ApprovalType, ExecutionMode } from "@shared/types";
export {
APPROVAL_POLICY_SEED,
COFOUNDER_MANDATORY_APPROVAL_TYPES,
OPERATIONAL_APPROVAL_TYPES,
} from "./approval-policy-definitions";
import {
COFOUNDER_MANDATORY_APPROVAL_TYPES,
OPERATIONAL_APPROVAL_TYPES,
} from "./approval-policy-definitions";
function assertExecutionMode(value: string): asserts value is ExecutionMode {
if (value !== "ai_ceo" && value !== "ai_cofounder") {
@@ -19,46 +24,18 @@ function assertExecutionMode(value: string): asserts value is ExecutionMode {
}
}
/** Approval types that always require human review in co-founder mode. */
export const COFOUNDER_MANDATORY_APPROVAL_TYPES = [
"deliverable_review",
"document_review",
"plan_review",
"scope_review",
"draft_review",
"strategy_review",
"exception_review",
"approve_ceo_strategy",
"production_deploy",
] as const satisfies readonly ApprovalType[];
const COFOUNDER_MANDATORY_APPROVALS = new Set<ApprovalType>(COFOUNDER_MANDATORY_APPROVAL_TYPES);
export function isCofounderMandatoryApproval(approvalType: string): approvalType is ApprovalType {
return COFOUNDER_MANDATORY_APPROVALS.has(approvalType as ApprovalType);
}
/** Approval types that always gate destructive tools while pending. */
export const OPERATIONAL_APPROVAL_TYPES = [
"dangerous_command",
"secret_access",
"production_deploy",
"social_post",
"budget_override",
] as const satisfies readonly ApprovalType[];
const OPERATIONAL_APPROVALS = new Set<ApprovalType>(OPERATIONAL_APPROVAL_TYPES);
export function isOperationalApproval(approvalType: string): approvalType is ApprovalType {
return OPERATIONAL_APPROVALS.has(approvalType as ApprovalType);
}
/** Approval types eligible for auto-approval in CEO mode (when company settings allow). */
const CEO_AUTO_APPROVABLE = new Set<ApprovalType>([
"hire_agent",
"approve_ceo_strategy",
]);
/** Task types that get plan_review checkpoints in co-founder mode. */
export const CHECKPOINT_TASK_TYPES = new Set(["research", "content", "management"]);
@@ -78,14 +55,12 @@ export function shouldAutoApprove(
const rawMode = db.core.company.getCompanyExecutionMode(companyId);
assertExecutionMode(rawMode);
const mode = rawMode;
// Co-founder mode: mandatory types always require human decision
if (mode === "ai_cofounder" && isCofounderMandatoryApproval(approvalType)) {
const policy = db.core.approvals.getApprovalPolicy(approvalType);
if (!policy?.autoApprovable) {
return false;
}
// Only specific types are ever auto-approvable
if (!CEO_AUTO_APPROVABLE.has(approvalType as ApprovalType)) {
if (mode === "ai_cofounder" && policy.requiredMode === "ai_cofounder") {
return false;
}
+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();