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:
@@ -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(),
|
||||||
|
|||||||
26
packages/db/src/schema/approval_comments.ts
Normal file
26
packages/db/src/schema/approval_comments.ts
Normal 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,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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 = [
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
27
server/src/services/agent-permissions.ts
Normal file
27
server/src/services/agent-permissions.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user