diff --git a/packages/db/src/schema/agents.ts b/packages/db/src/schema/agents.ts index fe1d910e..2b711e70 100644 --- a/packages/db/src/schema/agents.ts +++ b/packages/db/src/schema/agents.ts @@ -26,6 +26,7 @@ export const agents = pgTable( runtimeConfig: jsonb("runtime_config").$type>().notNull().default({}), budgetMonthlyCents: integer("budget_monthly_cents").notNull().default(0), spentMonthlyCents: integer("spent_monthly_cents").notNull().default(0), + permissions: jsonb("permissions").$type>().notNull().default({}), lastHeartbeatAt: timestamp("last_heartbeat_at", { withTimezone: true }), metadata: jsonb("metadata").$type>(), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), diff --git a/packages/db/src/schema/approval_comments.ts b/packages/db/src/schema/approval_comments.ts new file mode 100644 index 00000000..4314882f --- /dev/null +++ b/packages/db/src/schema/approval_comments.ts @@ -0,0 +1,26 @@ +import { pgTable, uuid, text, timestamp, index } from "drizzle-orm/pg-core"; +import { companies } from "./companies.js"; +import { approvals } from "./approvals.js"; +import { agents } from "./agents.js"; + +export const approvalComments = pgTable( + "approval_comments", + { + id: uuid("id").primaryKey().defaultRandom(), + companyId: uuid("company_id").notNull().references(() => companies.id), + approvalId: uuid("approval_id").notNull().references(() => approvals.id), + authorAgentId: uuid("author_agent_id").references(() => agents.id), + authorUserId: text("author_user_id"), + body: text("body").notNull(), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), + }, + (table) => ({ + companyIdx: index("approval_comments_company_idx").on(table.companyId), + approvalIdx: index("approval_comments_approval_idx").on(table.approvalId), + approvalCreatedIdx: index("approval_comments_approval_created_idx").on( + table.approvalId, + table.createdAt, + ), + }), +); diff --git a/packages/db/src/schema/index.ts b/packages/db/src/schema/index.ts index 2aaa737e..d14f2a2e 100644 --- a/packages/db/src/schema/index.ts +++ b/packages/db/src/schema/index.ts @@ -11,4 +11,5 @@ export { heartbeatRuns } from "./heartbeat_runs.js"; export { heartbeatRunEvents } from "./heartbeat_run_events.js"; export { costEvents } from "./cost_events.js"; export { approvals } from "./approvals.js"; +export { approvalComments } from "./approval_comments.js"; export { activityLog } from "./activity_log.js"; diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index c50117b6..235df85b 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -7,6 +7,7 @@ export const AGENT_STATUSES = [ "idle", "running", "error", + "pending_approval", "terminated", ] as const; export type AgentStatus = (typeof AGENT_STATUSES)[number]; @@ -61,7 +62,13 @@ export type ProjectStatus = (typeof PROJECT_STATUSES)[number]; export const APPROVAL_TYPES = ["hire_agent", "approve_ceo_strategy"] as const; export type ApprovalType = (typeof APPROVAL_TYPES)[number]; -export const APPROVAL_STATUSES = ["pending", "approved", "rejected", "cancelled"] as const; +export const APPROVAL_STATUSES = [ + "pending", + "revision_requested", + "approved", + "rejected", + "cancelled", +] as const; export type ApprovalStatus = (typeof APPROVAL_STATUSES)[number]; export const HEARTBEAT_INVOCATION_SOURCES = [ diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 9c020613..84d30996 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -36,12 +36,14 @@ export { export type { Company, Agent, + AgentPermissions, AgentKeyCreated, Project, Issue, IssueComment, Goal, Approval, + ApprovalComment, CostEvent, CostSummary, HeartbeatRun, @@ -62,10 +64,13 @@ export { updateAgentSchema, createAgentKeySchema, wakeAgentSchema, + agentPermissionsSchema, + updateAgentPermissionsSchema, type CreateAgent, type UpdateAgent, type CreateAgentKey, type WakeAgent, + type UpdateAgentPermissions, createProjectSchema, updateProjectSchema, type CreateProject, @@ -84,8 +89,14 @@ export { type UpdateGoal, createApprovalSchema, resolveApprovalSchema, + requestApprovalRevisionSchema, + resubmitApprovalSchema, + addApprovalCommentSchema, type CreateApproval, type ResolveApproval, + type RequestApprovalRevision, + type ResubmitApproval, + type AddApprovalComment, createCostEventSchema, updateBudgetSchema, type CreateCostEvent, diff --git a/packages/shared/src/types/agent.ts b/packages/shared/src/types/agent.ts index 7464ea9b..7f4daefe 100644 --- a/packages/shared/src/types/agent.ts +++ b/packages/shared/src/types/agent.ts @@ -4,6 +4,10 @@ import type { AgentStatus, } from "../constants.js"; +export interface AgentPermissions { + canCreateAgents: boolean; +} + export interface Agent { id: string; companyId: string; @@ -18,6 +22,7 @@ export interface Agent { runtimeConfig: Record; budgetMonthlyCents: number; spentMonthlyCents: number; + permissions: AgentPermissions; lastHeartbeatAt: Date | null; metadata: Record | null; createdAt: Date; diff --git a/packages/shared/src/types/approval.ts b/packages/shared/src/types/approval.ts index d1954914..4554d36f 100644 --- a/packages/shared/src/types/approval.ts +++ b/packages/shared/src/types/approval.ts @@ -14,3 +14,14 @@ export interface Approval { createdAt: Date; updatedAt: Date; } + +export interface ApprovalComment { + id: string; + companyId: string; + approvalId: string; + authorAgentId: string | null; + authorUserId: string | null; + body: string; + createdAt: Date; + updatedAt: Date; +} diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index f9dc25e5..4e404b5f 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -3,7 +3,7 @@ export type { Agent, AgentKeyCreated } from "./agent.js"; export type { Project } from "./project.js"; export type { Issue, IssueComment, IssueAncestor } from "./issue.js"; export type { Goal } from "./goal.js"; -export type { Approval } from "./approval.js"; +export type { Approval, ApprovalComment } from "./approval.js"; export type { CostEvent, CostSummary } from "./cost.js"; export type { HeartbeatRun, diff --git a/packages/shared/src/validators/agent.ts b/packages/shared/src/validators/agent.ts index 4f1384f1..10d2bd04 100644 --- a/packages/shared/src/validators/agent.ts +++ b/packages/shared/src/validators/agent.ts @@ -5,6 +5,10 @@ import { AGENT_STATUSES, } from "../constants.js"; +export const agentPermissionsSchema = z.object({ + canCreateAgents: z.boolean().optional().default(false), +}); + export const createAgentSchema = z.object({ name: z.string().min(1), role: z.enum(AGENT_ROLES).optional().default("general"), @@ -15,6 +19,7 @@ export const createAgentSchema = z.object({ adapterConfig: z.record(z.unknown()).optional().default({}), runtimeConfig: z.record(z.unknown()).optional().default({}), budgetMonthlyCents: z.number().int().nonnegative().optional().default(0), + permissions: agentPermissionsSchema.optional(), metadata: z.record(z.unknown()).optional().nullable(), }); @@ -44,3 +49,9 @@ export const wakeAgentSchema = z.object({ }); export type WakeAgent = z.infer; + +export const updateAgentPermissionsSchema = z.object({ + canCreateAgents: z.boolean(), +}); + +export type UpdateAgentPermissions = z.infer; diff --git a/packages/shared/src/validators/approval.ts b/packages/shared/src/validators/approval.ts index f76507f4..f9228000 100644 --- a/packages/shared/src/validators/approval.ts +++ b/packages/shared/src/validators/approval.ts @@ -15,3 +15,22 @@ export const resolveApprovalSchema = z.object({ }); export type ResolveApproval = z.infer; + +export const requestApprovalRevisionSchema = z.object({ + decisionNote: z.string().optional().nullable(), + decidedByUserId: z.string().optional().default("board"), +}); + +export type RequestApprovalRevision = z.infer; + +export const resubmitApprovalSchema = z.object({ + payload: z.record(z.unknown()).optional(), +}); + +export type ResubmitApproval = z.infer; + +export const addApprovalCommentSchema = z.object({ + body: z.string().min(1), +}); + +export type AddApprovalComment = z.infer; diff --git a/packages/shared/src/validators/company.ts b/packages/shared/src/validators/company.ts index 75eed6bf..9ae242d0 100644 --- a/packages/shared/src/validators/company.ts +++ b/packages/shared/src/validators/company.ts @@ -14,6 +14,7 @@ export const updateCompanySchema = createCompanySchema .extend({ status: z.enum(COMPANY_STATUSES).optional(), spentMonthlyCents: z.number().int().nonnegative().optional(), + requireBoardApprovalForNewAgents: z.boolean().optional(), }); export type UpdateCompany = z.infer; diff --git a/packages/shared/src/validators/index.ts b/packages/shared/src/validators/index.ts index ebc94e6b..7e498f80 100644 --- a/packages/shared/src/validators/index.ts +++ b/packages/shared/src/validators/index.ts @@ -10,10 +10,13 @@ export { updateAgentSchema, createAgentKeySchema, wakeAgentSchema, + agentPermissionsSchema, + updateAgentPermissionsSchema, type CreateAgent, type UpdateAgent, type CreateAgentKey, type WakeAgent, + type UpdateAgentPermissions, } from "./agent.js"; export { @@ -44,8 +47,14 @@ export { export { createApprovalSchema, resolveApprovalSchema, + requestApprovalRevisionSchema, + resubmitApprovalSchema, + addApprovalCommentSchema, type CreateApproval, type ResolveApproval, + type RequestApprovalRevision, + type ResubmitApproval, + type AddApprovalComment, } from "./approval.js"; export { diff --git a/server/src/services/agent-permissions.ts b/server/src/services/agent-permissions.ts new file mode 100644 index 00000000..15500de6 --- /dev/null +++ b/server/src/services/agent-permissions.ts @@ -0,0 +1,27 @@ +export interface NormalizedAgentPermissions { + canCreateAgents: boolean; +} + +export function defaultPermissionsForRole(role: string): NormalizedAgentPermissions { + return { + canCreateAgents: role === "ceo", + }; +} + +export function normalizeAgentPermissions( + permissions: unknown, + role: string, +): NormalizedAgentPermissions { + const defaults = defaultPermissionsForRole(role); + if (typeof permissions !== "object" || permissions === null || Array.isArray(permissions)) { + return defaults; + } + + const record = permissions as Record; + return { + canCreateAgents: + typeof record.canCreateAgents === "boolean" + ? record.canCreateAgents + : defaults.canCreateAgents, + }; +}