update
This commit is contained in:
1 parent
66158e222e
commit
67c757f481
314 files changed
+37945
-22962
No files matched your search
@@ -0,0 +1,213 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { DatabaseSync, SQLInputValue } from "node:sqlite";
|
||||
import type { ApprovalRecord } from "@shared/types";
|
||||
import type { ReadAll, Transaction } from "./helpers";
|
||||
import { normalizeOptionalId, nowIso, parseJson } from "./helpers";
|
||||
|
||||
export function buildApprovalMethods(
|
||||
getDb: () => DatabaseSync,
|
||||
readAll: ReadAll,
|
||||
transaction: Transaction,
|
||||
deps: {
|
||||
getAgent: (id: string) => { companyId: string; status: string };
|
||||
},
|
||||
) {
|
||||
return {
|
||||
getApprovalRecord(id: string) {
|
||||
const row = getDb()
|
||||
.prepare(
|
||||
"select id, company_id as companyId, related_task_id as relatedTaskId, requested_by_agent_id as requestedByAgentId, related_agent_id as relatedAgentId, type, payload_summary as payloadSummary, impact_summary as impactSummary, payload_json as payloadJson, decision_note as decisionNote, state, created_at as createdAt, updated_at as updatedAt from approvals where id = ?",
|
||||
)
|
||||
.get(id) as (ApprovalRecord & { payloadJson: string | null; relatedAgentId: string | null }) | undefined;
|
||||
if (!row) throw new Error(`Approval not found: ${id}`);
|
||||
return row;
|
||||
},
|
||||
|
||||
requestApproval(input: {
|
||||
companyId: string;
|
||||
relatedTaskId?: string | null;
|
||||
requestedByAgentId?: string | null;
|
||||
relatedAgentId?: string | null;
|
||||
type: string;
|
||||
payloadSummary: string;
|
||||
impactSummary: string;
|
||||
payloadJson?: string | null;
|
||||
}) {
|
||||
const id = randomUUID();
|
||||
const ts = nowIso();
|
||||
let relatedAgentId = normalizeOptionalId(input.relatedAgentId);
|
||||
if (!relatedAgentId && input.type === "hire_agent" && input.payloadJson) {
|
||||
try {
|
||||
const payload = JSON.parse(input.payloadJson) as { agentId?: string };
|
||||
relatedAgentId = payload.agentId ?? null;
|
||||
} catch { /* ignore malformed payload */ }
|
||||
}
|
||||
if (input.type === "hire_agent") {
|
||||
if (!relatedAgentId) {
|
||||
throw new Error("hire_agent approvals require a linked pending agent.");
|
||||
}
|
||||
const agent = deps.getAgent(relatedAgentId);
|
||||
if (agent.companyId !== input.companyId) {
|
||||
throw new Error(`Agent ${relatedAgentId} does not belong to company ${input.companyId}.`);
|
||||
}
|
||||
if (agent.status !== "pending_approval") {
|
||||
throw new Error(`hire_agent approvals can only target agents pending approval. Current status: ${agent.status}.`);
|
||||
}
|
||||
}
|
||||
getDb()
|
||||
.prepare(
|
||||
"insert into approvals (id, company_id, related_task_id, requested_by_agent_id, related_agent_id, type, payload_summary, impact_summary, payload_json, decision_note, state, created_at, updated_at) values (?, ?, ?, ?, ?, ?, ?, ?, ?, '', 'pending', ?, ?)",
|
||||
)
|
||||
.run(
|
||||
id, input.companyId, normalizeOptionalId(input.relatedTaskId),
|
||||
normalizeOptionalId(input.requestedByAgentId), relatedAgentId,
|
||||
input.type, input.payloadSummary, input.impactSummary,
|
||||
input.payloadJson ?? null, ts, ts,
|
||||
);
|
||||
return id;
|
||||
},
|
||||
|
||||
decideApproval(input: { companyId?: string | null; approvalId: string; state: string; decisionNote: string }) {
|
||||
const approval = getDb()
|
||||
.prepare(
|
||||
"select id, company_id as companyId, related_task_id as relatedTaskId, requested_by_agent_id as requestedByAgentId, related_agent_id as relatedAgentId, type, payload_summary as payloadSummary, impact_summary as impactSummary, payload_json as payloadJson, decision_note as decisionNote, state, created_at as createdAt, updated_at as updatedAt from approvals where id = ?",
|
||||
)
|
||||
.get(input.approvalId) as (ApprovalRecord & { payloadJson: string | null; relatedAgentId: string | null }) | undefined;
|
||||
if (!approval) throw new Error(`Approval not found: ${input.approvalId}`);
|
||||
if (input.companyId && approval.companyId !== input.companyId) {
|
||||
throw new Error(`Approval ${input.approvalId} does not belong to company ${input.companyId}.`);
|
||||
}
|
||||
if (!["pending", "revision_requested"].includes(approval.state)) {
|
||||
throw new Error(`Approval ${input.approvalId} is already ${approval.state} and cannot be re-decided.`);
|
||||
}
|
||||
const ts = nowIso();
|
||||
transaction(() => {
|
||||
getDb()
|
||||
.prepare("update approvals set state = ?, decision_note = ?, updated_at = ? where id = ?")
|
||||
.run(input.state, input.decisionNote, ts, input.approvalId);
|
||||
if (approval.type === "hire_agent") {
|
||||
const agentId = approval.relatedAgentId
|
||||
?? parseJson<{ agentId?: string } | null>(approval.payloadJson, null)?.agentId
|
||||
?? null;
|
||||
if (!agentId) {
|
||||
throw new Error(`hire_agent approval ${input.approvalId} has no linked agent — cannot resolve hire target. Ensure relatedAgentId or payloadJson.agentId is set.`);
|
||||
}
|
||||
if (input.state === "approved") {
|
||||
const result = getDb()
|
||||
.prepare("update agents set status = 'idle', updated_at = ? where id = ? and company_id = ? and status = 'pending_approval'")
|
||||
.run(ts, agentId, approval.companyId);
|
||||
if (result.changes === 0) {
|
||||
throw new Error(`hire_agent approval ${input.approvalId} could not activate agent ${agentId}; the agent is missing or no longer pending approval.`);
|
||||
}
|
||||
}
|
||||
if (input.state === "rejected") {
|
||||
const result = getDb()
|
||||
.prepare("update agents set status = 'terminated', updated_at = ? where id = ? and company_id = ? and status = 'pending_approval'")
|
||||
.run(ts, agentId, approval.companyId);
|
||||
if (result.changes === 0) {
|
||||
throw new Error(`hire_agent approval ${input.approvalId} could not reject agent ${agentId}; the agent is missing or no longer pending approval.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
deleteApproval(id: string, companyId: string) {
|
||||
getDb().prepare("delete from approvals where id = ? and company_id = ?").run(id, companyId);
|
||||
},
|
||||
|
||||
getBlockingApproval(companyId: string, agentId: string, taskId: string | null): ApprovalRecord | null {
|
||||
const taskCondition = taskId != null
|
||||
? "(related_task_id = ? or requested_by_agent_id = ?)"
|
||||
: "requested_by_agent_id = ?";
|
||||
const taskParams: SQLInputValue[] = taskId != null ? [taskId, agentId] : [agentId];
|
||||
const rows = readAll<ApprovalRecord>(
|
||||
`select id, company_id as companyId, related_task_id as relatedTaskId,
|
||||
requested_by_agent_id as requestedByAgentId, related_agent_id as relatedAgentId,
|
||||
type, payload_summary as payloadSummary, impact_summary as impactSummary,
|
||||
payload_json as payloadJson, decision_note as decisionNote, state,
|
||||
created_at as createdAt, updated_at as updatedAt
|
||||
from approvals
|
||||
where company_id = ? and state = 'pending'
|
||||
and (
|
||||
(type = 'hire_agent' and related_agent_id = ?)
|
||||
or (type = 'dangerous_command' and ${taskCondition})
|
||||
or (type = 'secret_access' and ${taskCondition})
|
||||
or (type = 'approve_ceo_strategy' and ${taskCondition})
|
||||
or (type = 'deliverable_review' and (requested_by_agent_id = ? or (related_task_id is not null and ${taskCondition})))
|
||||
)
|
||||
order by created_at asc limit 1`,
|
||||
companyId, agentId,
|
||||
...taskParams, ...taskParams, ...taskParams,
|
||||
agentId, ...taskParams,
|
||||
);
|
||||
return rows.length > 0 ? rows[0] : null;
|
||||
},
|
||||
|
||||
resubmitApproval(approvalId: string, companyId: string): void {
|
||||
const ts = nowIso();
|
||||
getDb().prepare(
|
||||
"UPDATE approvals SET state = 'pending', decision_note = '', updated_at = ? WHERE id = ? AND company_id = ? AND state = 'revision_requested'",
|
||||
).run(ts, approvalId, companyId);
|
||||
},
|
||||
|
||||
addApprovalComment(input: {
|
||||
companyId: string; approvalId: string; authorAgentId?: string | null;
|
||||
authorUserId?: string | null; body: string;
|
||||
}): string {
|
||||
const id = randomUUID();
|
||||
const ts = nowIso();
|
||||
getDb().prepare(
|
||||
"INSERT INTO approval_comments (id, company_id, approval_id, author_agent_id, author_user_id, body, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
).run(id, input.companyId, input.approvalId, input.authorAgentId ?? null, input.authorUserId ?? null, input.body, ts, ts);
|
||||
return id;
|
||||
},
|
||||
|
||||
listApprovalComments(approvalId: string, companyId: string) {
|
||||
return readAll<{
|
||||
id: string; company_id: string; approval_id: string; author_agent_id: string | null;
|
||||
author_user_id: string | null; body: string; created_at: string; updated_at: string;
|
||||
}>(
|
||||
"SELECT id, company_id, approval_id, author_agent_id, author_user_id, body, created_at, updated_at FROM approval_comments WHERE approval_id = ? AND company_id = ? ORDER BY created_at ASC",
|
||||
approvalId, companyId,
|
||||
).map((r) => ({
|
||||
id: r.id, companyId: r.company_id, approvalId: r.approval_id,
|
||||
authorAgentId: r.author_agent_id, authorUserId: r.author_user_id,
|
||||
body: r.body, createdAt: r.created_at, updatedAt: r.updated_at,
|
||||
}));
|
||||
},
|
||||
|
||||
linkTaskToApproval(taskId: string, approvalId: string, companyId: string, linkedByAgentId?: string | null): void {
|
||||
getDb().prepare(
|
||||
"INSERT OR IGNORE INTO task_approval_links (task_id, approval_id, company_id, linked_by_agent_id, created_at) VALUES (?, ?, ?, ?, ?)",
|
||||
).run(taskId, approvalId, companyId, linkedByAgentId ?? null, nowIso());
|
||||
},
|
||||
|
||||
unlinkTaskFromApproval(taskId: string, approvalId: string): void {
|
||||
getDb().prepare(
|
||||
"DELETE FROM task_approval_links WHERE task_id = ? AND approval_id = ?",
|
||||
).run(taskId, approvalId);
|
||||
},
|
||||
|
||||
listApprovalsForTask(taskId: string): string[] {
|
||||
return readAll<{ approval_id: string }>(
|
||||
"SELECT approval_id FROM task_approval_links WHERE task_id = ?", taskId,
|
||||
).map((r) => r.approval_id);
|
||||
},
|
||||
|
||||
listTasksForApproval(approvalId: string): string[] {
|
||||
return readAll<{ task_id: string }>(
|
||||
"SELECT task_id FROM task_approval_links WHERE approval_id = ?", approvalId,
|
||||
).map((r) => r.task_id);
|
||||
},
|
||||
|
||||
linkManyTasksToApproval(approvalId: string, taskIds: string[], companyId: string, linkedByAgentId?: string | null): void {
|
||||
const ts = nowIso();
|
||||
for (const taskId of taskIds) {
|
||||
getDb().prepare(
|
||||
"INSERT OR IGNORE INTO task_approval_links (task_id, approval_id, company_id, linked_by_agent_id, created_at) VALUES (?, ?, ?, ?, ?)",
|
||||
).run(taskId, approvalId, companyId, linkedByAgentId ?? null, ts);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user