From e7fa60d6b6b0a02fc94cfa099da38f42f0087e99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 29 May 2026 23:27:37 -0400 Subject: [PATCH] Auto-approve agent work in AI CEO mode instead of gating on the operator --- .../src/main/services/organization-service.ts | 63 ++++++++++++++++--- .../tests/unit/domain-services.test.ts | 39 ++++++++++++ .../tests/unit/org-governance-service.test.ts | 8 ++- .../tests/unit/task-execution-service.test.ts | 1 + 4 files changed, 101 insertions(+), 10 deletions(-) diff --git a/AgentCompany-New/src/main/services/organization-service.ts b/AgentCompany-New/src/main/services/organization-service.ts index 83ebf5d0..3be290f8 100644 --- a/AgentCompany-New/src/main/services/organization-service.ts +++ b/AgentCompany-New/src/main/services/organization-service.ts @@ -1,5 +1,5 @@ import type { DatabaseSync } from "node:sqlite"; -import { APPROVAL_STATE, RISK_LEVEL, TASK_STATUS } from "../../shared/domain"; +import { APPROVAL_STATE, RISK_LEVEL, TASK_STATUS, type ApprovalState } from "../../shared/domain"; import { PROVIDER_CATALOG } from "../../shared/provider-catalog"; import { createId } from "../../shared/ids"; import type { CreateAgentInput, CreateOrganizationInput, CreateTaskInput, UpdateAgentInput } from "../../shared/commands"; @@ -187,6 +187,8 @@ export class OrganizationService { this.identityAccess.createAgentServiceIdentity(input.orgId, id, input.name, now); if (hiringPolicy.reviewRequired) { const approvalId = createId("app"); + const autoApprove = this.autonomy(input.orgId).autoApproveHires; + const decision = this.approvalDecision(input.orgId, autoApprove, now); this.db.prepare(` insert into approvals( id, org_id, kind, title, details, risk, requested_by_type, @@ -203,12 +205,12 @@ export class OrganizationService { null, "agent", id, - APPROVAL_STATE.PENDING, - null, + decision.state, + decision.decidedBy, now, - null, + decision.decidedAt, ); - this.governance.routeApproval({ orgId: input.orgId, approvalId, kind: "agent_hire", risk: RISK_LEVEL.MEDIUM }); + this.settleApproval(input.orgId, approvalId, "agent_hire", RISK_LEVEL.MEDIUM, autoApprove); } return id; } @@ -276,6 +278,8 @@ export class OrganizationService { if (approvalRequired) { const approvalId = createId("app"); const approvalKind = policy.approvalRequired ? "task_risk" : "task_execution"; + const autoApprove = this.autonomy(input.orgId).executionMode === "ai_ceo"; + const decision = this.approvalDecision(input.orgId, autoApprove, now); this.db.prepare(` insert into approvals( id, org_id, kind, title, details, risk, requested_by_type, @@ -292,12 +296,12 @@ export class OrganizationService { null, "task", id, - APPROVAL_STATE.PENDING, - null, + decision.state, + decision.decidedBy, now, - null, + decision.decidedAt, ); - this.governance.routeApproval({ orgId: input.orgId, approvalId, kind: approvalKind, risk: RISK_LEVEL.HIGH }); + this.settleApproval(input.orgId, approvalId, approvalKind, RISK_LEVEL.HIGH, autoApprove); } this.audit.record({ orgId: input.orgId, @@ -311,6 +315,47 @@ export class OrganizationService { return id; } + /** + * Reads the org's autonomy settings. In `ai_ceo` mode the CEO runs without + * the operator approving each task; `autoApproveHires` clears hire reviews. + */ + private autonomy(orgId: string): { executionMode: string; autoApproveHires: boolean } { + const row = this.db.prepare("select execution_mode, auto_approve_hires from organizations where id = ?") + .get(orgId) as { execution_mode: string; auto_approve_hires: number } | undefined; + if (!row) throw new Error("Organization was not found."); + return { executionMode: row.execution_mode, autoApproveHires: row.auto_approve_hires === 1 }; + } + + /** The decision recorded on an approval: pending, or auto-approved by the board chair. */ + private approvalDecision(orgId: string, autoApprove: boolean, now: string): { state: ApprovalState; decidedBy: string | null; decidedAt: string | null } { + if (!autoApprove) return { state: APPROVAL_STATE.PENDING, decidedBy: null, decidedAt: null }; + return { state: APPROVAL_STATE.APPROVED, decidedBy: this.boardChairEmployeeId(orgId), decidedAt: now }; + } + + /** Routes a pending approval to its decider, or records the auto-approval in the audit trail. */ + private settleApproval(orgId: string, approvalId: string, kind: string, risk: typeof RISK_LEVEL[keyof typeof RISK_LEVEL], autoApproved: boolean): void { + if (!autoApproved) { + this.governance.routeApproval({ orgId, approvalId, kind, risk }); + return; + } + this.audit.record({ + orgId, + entityType: "approval", + entityId: approvalId, + actorType: "system", + actorId: null, + action: "approved", + payload: { kind, auto: true }, + }); + } + + private boardChairEmployeeId(orgId: string): string { + const row = this.db.prepare("select id from employees where org_id = ? order by created_at asc limit 1") + .get(orgId) as { id: string } | undefined; + if (!row) throw new Error("Organization has no employee to record the approval decision."); + return row.id; + } + private verifyOptionalOrgEntity(table: "agents" | "goals" | "workspaces", id: string | null, orgId: string, label: string): void { if (!id) return; const row = this.db.prepare(`select id from ${table} where id = ? and org_id = ?`).get(id, orgId); diff --git a/AgentCompany-New/tests/unit/domain-services.test.ts b/AgentCompany-New/tests/unit/domain-services.test.ts index bde38573..06733111 100644 --- a/AgentCompany-New/tests/unit/domain-services.test.ts +++ b/AgentCompany-New/tests/unit/domain-services.test.ts @@ -73,6 +73,7 @@ describe("domain services", () => { employeeName: "Dana Lee", mission: "Coordinate agents with auditable approvals.", goal: "Ship a verified control plane.", + executionMode: "ai_cofounder", }); organization.createTask({ orgId, @@ -98,6 +99,44 @@ describe("domain services", () => { connection.close(); }); + test("ai ceo mode auto-approves high risk work without operator review", () => { + const connection = createConnection(); + const organization = new OrganizationService(connection.db); + const snapshot = new SnapshotService(connection.db); + let orgId = ""; + + connection.transaction(() => { + orgId = organization.createOrganization({ + organizationName: "Autonomy Inc", + employeeName: "Robin Park", + mission: "Run the company autonomously.", + goal: "Ship without manual gates.", + executionMode: "ai_ceo", + }); + organization.createTask({ + orgId, + title: "Production deploy", + description: "Deploy a high-risk change.", + status: TASK_STATUS.BACKLOG, + assigneeAgentId: null, + risk: RISK_LEVEL.HIGH, + projectId: null, + goalId: null, + workspaceId: null, + priority: "normal", + requiresFilesystem: false, + }); + }); + + const state = snapshot.load(); + const employeeId = state.employees.find((employee) => employee.orgId === orgId)?.id; + expect(state.approvals).toHaveLength(1); + expect(state.approvals[0]?.state).toBe("approved"); + expect(state.approvals[0]?.decidedByEmployeeId).toBe(employeeId); + expect(state.approvalAssignments).toHaveLength(0); + connection.close(); + }); + test("imports a versioned skill revision from a local directory", () => { const connection = createConnection(); const organization = new OrganizationService(connection.db); diff --git a/AgentCompany-New/tests/unit/org-governance-service.test.ts b/AgentCompany-New/tests/unit/org-governance-service.test.ts index 3bffd550..0e3172e2 100644 --- a/AgentCompany-New/tests/unit/org-governance-service.test.ts +++ b/AgentCompany-New/tests/unit/org-governance-service.test.ts @@ -85,7 +85,13 @@ describe("org governance service", () => { let orgId = ""; connection.transaction(() => { - orgId = createOrganization(organization); + orgId = organization.createOrganization({ + organizationName: "Acme Operations", + employeeName: "Dana Lee", + mission: "Coordinate agents with auditable approvals.", + goal: "Ship a verified control plane.", + autoApproveHires: false, + }); organization.createAgent({ orgId, name: "Ada", diff --git a/AgentCompany-New/tests/unit/task-execution-service.test.ts b/AgentCompany-New/tests/unit/task-execution-service.test.ts index 04ef3168..dd9c9982 100644 --- a/AgentCompany-New/tests/unit/task-execution-service.test.ts +++ b/AgentCompany-New/tests/unit/task-execution-service.test.ts @@ -160,6 +160,7 @@ describe("task execution service", () => { employeeName: "Dana Lee", mission: "Coordinate agents with auditable approvals.", goal: "Ship a verified control plane.", + executionMode: "ai_cofounder", }); employeeId = (connection.db.prepare("select id from employees where org_id = ?").get(orgId) as { id: string }).id; const providerId = openAiProviderId(connection, orgId);