Auto-approve agent work in AI CEO mode instead of gating on the operator
This commit is contained in:
1 parent
9996b89f72
commit
e7fa60d6b6
4 files changed
+101
-10
No files matched your search
@@ -1,5 +1,5 @@
|
|||||||
import type { DatabaseSync } from "node:sqlite";
|
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 { PROVIDER_CATALOG } from "../../shared/provider-catalog";
|
||||||
import { createId } from "../../shared/ids";
|
import { createId } from "../../shared/ids";
|
||||||
import type { CreateAgentInput, CreateOrganizationInput, CreateTaskInput, UpdateAgentInput } from "../../shared/commands";
|
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);
|
this.identityAccess.createAgentServiceIdentity(input.orgId, id, input.name, now);
|
||||||
if (hiringPolicy.reviewRequired) {
|
if (hiringPolicy.reviewRequired) {
|
||||||
const approvalId = createId("app");
|
const approvalId = createId("app");
|
||||||
|
const autoApprove = this.autonomy(input.orgId).autoApproveHires;
|
||||||
|
const decision = this.approvalDecision(input.orgId, autoApprove, now);
|
||||||
this.db.prepare(`
|
this.db.prepare(`
|
||||||
insert into approvals(
|
insert into approvals(
|
||||||
id, org_id, kind, title, details, risk, requested_by_type,
|
id, org_id, kind, title, details, risk, requested_by_type,
|
||||||
@@ -203,12 +205,12 @@ export class OrganizationService {
|
|||||||
null,
|
null,
|
||||||
"agent",
|
"agent",
|
||||||
id,
|
id,
|
||||||
APPROVAL_STATE.PENDING,
|
decision.state,
|
||||||
null,
|
decision.decidedBy,
|
||||||
now,
|
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;
|
return id;
|
||||||
}
|
}
|
||||||
@@ -276,6 +278,8 @@ export class OrganizationService {
|
|||||||
if (approvalRequired) {
|
if (approvalRequired) {
|
||||||
const approvalId = createId("app");
|
const approvalId = createId("app");
|
||||||
const approvalKind = policy.approvalRequired ? "task_risk" : "task_execution";
|
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(`
|
this.db.prepare(`
|
||||||
insert into approvals(
|
insert into approvals(
|
||||||
id, org_id, kind, title, details, risk, requested_by_type,
|
id, org_id, kind, title, details, risk, requested_by_type,
|
||||||
@@ -292,12 +296,12 @@ export class OrganizationService {
|
|||||||
null,
|
null,
|
||||||
"task",
|
"task",
|
||||||
id,
|
id,
|
||||||
APPROVAL_STATE.PENDING,
|
decision.state,
|
||||||
null,
|
decision.decidedBy,
|
||||||
now,
|
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({
|
this.audit.record({
|
||||||
orgId: input.orgId,
|
orgId: input.orgId,
|
||||||
@@ -311,6 +315,47 @@ export class OrganizationService {
|
|||||||
return id;
|
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 {
|
private verifyOptionalOrgEntity(table: "agents" | "goals" | "workspaces", id: string | null, orgId: string, label: string): void {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
const row = this.db.prepare(`select id from ${table} where id = ? and org_id = ?`).get(id, orgId);
|
const row = this.db.prepare(`select id from ${table} where id = ? and org_id = ?`).get(id, orgId);
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ describe("domain services", () => {
|
|||||||
employeeName: "Dana Lee",
|
employeeName: "Dana Lee",
|
||||||
mission: "Coordinate agents with auditable approvals.",
|
mission: "Coordinate agents with auditable approvals.",
|
||||||
goal: "Ship a verified control plane.",
|
goal: "Ship a verified control plane.",
|
||||||
|
executionMode: "ai_cofounder",
|
||||||
});
|
});
|
||||||
organization.createTask({
|
organization.createTask({
|
||||||
orgId,
|
orgId,
|
||||||
@@ -98,6 +99,44 @@ describe("domain services", () => {
|
|||||||
connection.close();
|
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", () => {
|
test("imports a versioned skill revision from a local directory", () => {
|
||||||
const connection = createConnection();
|
const connection = createConnection();
|
||||||
const organization = new OrganizationService(connection.db);
|
const organization = new OrganizationService(connection.db);
|
||||||
|
|||||||
@@ -85,7 +85,13 @@ describe("org governance service", () => {
|
|||||||
let orgId = "";
|
let orgId = "";
|
||||||
|
|
||||||
connection.transaction(() => {
|
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({
|
organization.createAgent({
|
||||||
orgId,
|
orgId,
|
||||||
name: "Ada",
|
name: "Ada",
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ describe("task execution service", () => {
|
|||||||
employeeName: "Dana Lee",
|
employeeName: "Dana Lee",
|
||||||
mission: "Coordinate agents with auditable approvals.",
|
mission: "Coordinate agents with auditable approvals.",
|
||||||
goal: "Ship a verified control plane.",
|
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;
|
employeeId = (connection.db.prepare("select id from employees where org_id = ?").get(orgId) as { id: string }).id;
|
||||||
const providerId = openAiProviderId(connection, orgId);
|
const providerId = openAiProviderId(connection, orgId);
|
||||||
|
|||||||
Reference in new issue
Block a user