Scaffold agent permissions, approval comments, and hiring governance types

Add pending_approval agent status, permissions jsonb column, and AgentPermissions
type with canCreateAgents flag. Add approval_comments table and ApprovalComment
type. Extend approval statuses with revision_requested. Add validators for
permission updates, approval revision requests, resubmission, and approval
comments. Add requireBoardApprovalForNewAgents to company update schema.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Forgotten
2026-02-19 09:10:38 -06:00
parent 9f250acf43
commit e0a878f4eb
13 changed files with 131 additions and 2 deletions

View File

@@ -26,6 +26,7 @@ export const agents = pgTable(
runtimeConfig: jsonb("runtime_config").$type<Record<string, unknown>>().notNull().default({}), runtimeConfig: jsonb("runtime_config").$type<Record<string, unknown>>().notNull().default({}),
budgetMonthlyCents: integer("budget_monthly_cents").notNull().default(0), budgetMonthlyCents: integer("budget_monthly_cents").notNull().default(0),
spentMonthlyCents: integer("spent_monthly_cents").notNull().default(0), spentMonthlyCents: integer("spent_monthly_cents").notNull().default(0),
permissions: jsonb("permissions").$type<Record<string, unknown>>().notNull().default({}),
lastHeartbeatAt: timestamp("last_heartbeat_at", { withTimezone: true }), lastHeartbeatAt: timestamp("last_heartbeat_at", { withTimezone: true }),
metadata: jsonb("metadata").$type<Record<string, unknown>>(), metadata: jsonb("metadata").$type<Record<string, unknown>>(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),

View File

@@ -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,
),
}),
);

View File

@@ -11,4 +11,5 @@ export { heartbeatRuns } from "./heartbeat_runs.js";
export { heartbeatRunEvents } from "./heartbeat_run_events.js"; export { heartbeatRunEvents } from "./heartbeat_run_events.js";
export { costEvents } from "./cost_events.js"; export { costEvents } from "./cost_events.js";
export { approvals } from "./approvals.js"; export { approvals } from "./approvals.js";
export { approvalComments } from "./approval_comments.js";
export { activityLog } from "./activity_log.js"; export { activityLog } from "./activity_log.js";

View File

@@ -7,6 +7,7 @@ export const AGENT_STATUSES = [
"idle", "idle",
"running", "running",
"error", "error",
"pending_approval",
"terminated", "terminated",
] as const; ] as const;
export type AgentStatus = (typeof AGENT_STATUSES)[number]; 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 const APPROVAL_TYPES = ["hire_agent", "approve_ceo_strategy"] as const;
export type ApprovalType = (typeof APPROVAL_TYPES)[number]; 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 type ApprovalStatus = (typeof APPROVAL_STATUSES)[number];
export const HEARTBEAT_INVOCATION_SOURCES = [ export const HEARTBEAT_INVOCATION_SOURCES = [

View File

@@ -36,12 +36,14 @@ export {
export type { export type {
Company, Company,
Agent, Agent,
AgentPermissions,
AgentKeyCreated, AgentKeyCreated,
Project, Project,
Issue, Issue,
IssueComment, IssueComment,
Goal, Goal,
Approval, Approval,
ApprovalComment,
CostEvent, CostEvent,
CostSummary, CostSummary,
HeartbeatRun, HeartbeatRun,
@@ -62,10 +64,13 @@ export {
updateAgentSchema, updateAgentSchema,
createAgentKeySchema, createAgentKeySchema,
wakeAgentSchema, wakeAgentSchema,
agentPermissionsSchema,
updateAgentPermissionsSchema,
type CreateAgent, type CreateAgent,
type UpdateAgent, type UpdateAgent,
type CreateAgentKey, type CreateAgentKey,
type WakeAgent, type WakeAgent,
type UpdateAgentPermissions,
createProjectSchema, createProjectSchema,
updateProjectSchema, updateProjectSchema,
type CreateProject, type CreateProject,
@@ -84,8 +89,14 @@ export {
type UpdateGoal, type UpdateGoal,
createApprovalSchema, createApprovalSchema,
resolveApprovalSchema, resolveApprovalSchema,
requestApprovalRevisionSchema,
resubmitApprovalSchema,
addApprovalCommentSchema,
type CreateApproval, type CreateApproval,
type ResolveApproval, type ResolveApproval,
type RequestApprovalRevision,
type ResubmitApproval,
type AddApprovalComment,
createCostEventSchema, createCostEventSchema,
updateBudgetSchema, updateBudgetSchema,
type CreateCostEvent, type CreateCostEvent,

View File

@@ -4,6 +4,10 @@ import type {
AgentStatus, AgentStatus,
} from "../constants.js"; } from "../constants.js";
export interface AgentPermissions {
canCreateAgents: boolean;
}
export interface Agent { export interface Agent {
id: string; id: string;
companyId: string; companyId: string;
@@ -18,6 +22,7 @@ export interface Agent {
runtimeConfig: Record<string, unknown>; runtimeConfig: Record<string, unknown>;
budgetMonthlyCents: number; budgetMonthlyCents: number;
spentMonthlyCents: number; spentMonthlyCents: number;
permissions: AgentPermissions;
lastHeartbeatAt: Date | null; lastHeartbeatAt: Date | null;
metadata: Record<string, unknown> | null; metadata: Record<string, unknown> | null;
createdAt: Date; createdAt: Date;

View File

@@ -14,3 +14,14 @@ export interface Approval {
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
} }
export interface ApprovalComment {
id: string;
companyId: string;
approvalId: string;
authorAgentId: string | null;
authorUserId: string | null;
body: string;
createdAt: Date;
updatedAt: Date;
}

View File

@@ -3,7 +3,7 @@ export type { Agent, AgentKeyCreated } from "./agent.js";
export type { Project } from "./project.js"; export type { Project } from "./project.js";
export type { Issue, IssueComment, IssueAncestor } from "./issue.js"; export type { Issue, IssueComment, IssueAncestor } from "./issue.js";
export type { Goal } from "./goal.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 { CostEvent, CostSummary } from "./cost.js";
export type { export type {
HeartbeatRun, HeartbeatRun,

View File

@@ -5,6 +5,10 @@ import {
AGENT_STATUSES, AGENT_STATUSES,
} from "../constants.js"; } from "../constants.js";
export const agentPermissionsSchema = z.object({
canCreateAgents: z.boolean().optional().default(false),
});
export const createAgentSchema = z.object({ export const createAgentSchema = z.object({
name: z.string().min(1), name: z.string().min(1),
role: z.enum(AGENT_ROLES).optional().default("general"), role: z.enum(AGENT_ROLES).optional().default("general"),
@@ -15,6 +19,7 @@ export const createAgentSchema = z.object({
adapterConfig: z.record(z.unknown()).optional().default({}), adapterConfig: z.record(z.unknown()).optional().default({}),
runtimeConfig: z.record(z.unknown()).optional().default({}), runtimeConfig: z.record(z.unknown()).optional().default({}),
budgetMonthlyCents: z.number().int().nonnegative().optional().default(0), budgetMonthlyCents: z.number().int().nonnegative().optional().default(0),
permissions: agentPermissionsSchema.optional(),
metadata: z.record(z.unknown()).optional().nullable(), metadata: z.record(z.unknown()).optional().nullable(),
}); });
@@ -44,3 +49,9 @@ export const wakeAgentSchema = z.object({
}); });
export type WakeAgent = z.infer<typeof wakeAgentSchema>; export type WakeAgent = z.infer<typeof wakeAgentSchema>;
export const updateAgentPermissionsSchema = z.object({
canCreateAgents: z.boolean(),
});
export type UpdateAgentPermissions = z.infer<typeof updateAgentPermissionsSchema>;

View File

@@ -15,3 +15,22 @@ export const resolveApprovalSchema = z.object({
}); });
export type ResolveApproval = z.infer<typeof resolveApprovalSchema>; export type ResolveApproval = z.infer<typeof resolveApprovalSchema>;
export const requestApprovalRevisionSchema = z.object({
decisionNote: z.string().optional().nullable(),
decidedByUserId: z.string().optional().default("board"),
});
export type RequestApprovalRevision = z.infer<typeof requestApprovalRevisionSchema>;
export const resubmitApprovalSchema = z.object({
payload: z.record(z.unknown()).optional(),
});
export type ResubmitApproval = z.infer<typeof resubmitApprovalSchema>;
export const addApprovalCommentSchema = z.object({
body: z.string().min(1),
});
export type AddApprovalComment = z.infer<typeof addApprovalCommentSchema>;

View File

@@ -14,6 +14,7 @@ export const updateCompanySchema = createCompanySchema
.extend({ .extend({
status: z.enum(COMPANY_STATUSES).optional(), status: z.enum(COMPANY_STATUSES).optional(),
spentMonthlyCents: z.number().int().nonnegative().optional(), spentMonthlyCents: z.number().int().nonnegative().optional(),
requireBoardApprovalForNewAgents: z.boolean().optional(),
}); });
export type UpdateCompany = z.infer<typeof updateCompanySchema>; export type UpdateCompany = z.infer<typeof updateCompanySchema>;

View File

@@ -10,10 +10,13 @@ export {
updateAgentSchema, updateAgentSchema,
createAgentKeySchema, createAgentKeySchema,
wakeAgentSchema, wakeAgentSchema,
agentPermissionsSchema,
updateAgentPermissionsSchema,
type CreateAgent, type CreateAgent,
type UpdateAgent, type UpdateAgent,
type CreateAgentKey, type CreateAgentKey,
type WakeAgent, type WakeAgent,
type UpdateAgentPermissions,
} from "./agent.js"; } from "./agent.js";
export { export {
@@ -44,8 +47,14 @@ export {
export { export {
createApprovalSchema, createApprovalSchema,
resolveApprovalSchema, resolveApprovalSchema,
requestApprovalRevisionSchema,
resubmitApprovalSchema,
addApprovalCommentSchema,
type CreateApproval, type CreateApproval,
type ResolveApproval, type ResolveApproval,
type RequestApprovalRevision,
type ResubmitApproval,
type AddApprovalComment,
} from "./approval.js"; } from "./approval.js";
export { export {

View File

@@ -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<string, unknown>;
return {
canCreateAgents:
typeof record.canCreateAgents === "boolean"
? record.canCreateAgents
: defaults.canCreateAgents,
};
}