fix(orchestration): enforce approval gates on heartbeat and recovery paths
The heartbeat path (canRunAgent → startAgentHeartbeatRun) only checked isAgentPendingApproval (hire gate), completely bypassing plan_review, draft_review, strategy_review and other blocking approval types. Agents in Co-Founder mode continued running despite pending approvals. Root cause: task-runner.prepareTaskRun had the full getBlockingApproval check, but heartbeat runs never went through prepareTaskRun — they created runs directly, skipping the approval gate entirely. Three fixes: - heartbeat.canRunAgent: add getBlockingApproval(companyId, agentId, null) for agent-level blocking approvals - heartbeat.startAgentHeartbeatRun: add getBlockingApproval with task id after task resolution, before run creation - task-runner.resumeQueuedRuns: add getBlockingApproval check so app restart does not dispatch runs past pending approvals Also replaced the hardcoded type list in hasPendingReviewApprovalForAgent with a dynamic join on approval_policies.blocks_execution, eliminating the sync risk when new blocking approval types are added.
This commit is contained in:
1 parent
c9deca5454
commit
1e36259c5d
3 files changed
+27
-1
No files matched your search
@@ -415,9 +415,10 @@ export function buildApprovalMethods(
|
||||
.prepare(
|
||||
`SELECT 1 as hit FROM approvals a
|
||||
JOIN tasks t ON a.related_task_id = t.id
|
||||
JOIN approval_policies p ON p.approval_type = a.type
|
||||
WHERE a.company_id = ? AND a.state = 'pending'
|
||||
AND t.assignee_agent_id = ?
|
||||
AND a.type IN ('plan_review','scope_review','draft_review','strategy_review','deliverable_review','exception_review')
|
||||
AND p.blocks_execution = 1
|
||||
LIMIT 1`,
|
||||
)
|
||||
.get(companyId, agentId) as { hit: number } | undefined;
|
||||
|
||||
@@ -196,6 +196,8 @@ export function createHeartbeat(
|
||||
return BLOCKED("policy_blocked", "wake_on_demand_disabled");
|
||||
}
|
||||
if (db.core.agents.isAgentPendingApproval(agentId)) return BLOCKED("human_blocked", "pending_approval");
|
||||
const agentLevelBlock = db.core.approvals.getBlockingApproval(companyId, agentId, null);
|
||||
if (agentLevelBlock) return BLOCKED("human_blocked", `approval_gate:${agentLevelBlock.type}`);
|
||||
|
||||
const companyMode = db.core.company.getCompanyExecutionMode(companyId);
|
||||
const approvalSettings = db.core.company.getCompanyApprovalSettings(companyId);
|
||||
@@ -268,6 +270,11 @@ export function createHeartbeat(
|
||||
logger.info(`[heartbeat] Starting taskless heartbeat run for ${agent.name}`);
|
||||
}
|
||||
|
||||
if (task) {
|
||||
const taskBlock = db.core.approvals.getBlockingApproval(companyId, agent.id, task.id);
|
||||
if (taskBlock) return SKIPPED(BLOCKED("human_blocked", `approval_gate:${taskBlock.type}`));
|
||||
}
|
||||
|
||||
let workspaceResolution: ReturnType<typeof resolveWorkspace>;
|
||||
try {
|
||||
workspaceResolution = resolveWorkspace(task, agent, companyId);
|
||||
|
||||
@@ -270,6 +270,24 @@ export function createTaskRunner(
|
||||
const task = run.taskId ? db.core.entity.getTask(run.taskId) : null;
|
||||
const agent = db.core.entity.getAgent(run.agentId);
|
||||
const connector = db.core.connectors.getConnector(run.connectorId);
|
||||
const blockingApproval = db.core.approvals.getBlockingApproval(run.companyId, agent.id, run.taskId);
|
||||
if (blockingApproval) {
|
||||
finishQueuedRecoveryRun(
|
||||
run,
|
||||
"cancelled",
|
||||
`Queued run blocked by pending approval: ${blockingApproval.type}`,
|
||||
null,
|
||||
);
|
||||
db.core.activity.addActivity({
|
||||
companyId: run.companyId,
|
||||
actor: "system",
|
||||
action: "run.recovery_blocked_approval",
|
||||
entityType: "run",
|
||||
entityId: run.id,
|
||||
detail: `Recovered queued run blocked by ${blockingApproval.type} for task "${task?.title ?? "(heartbeat)"}"`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const eligibilityFailure = getRunEligibilityFailure(agent, connector, task?.projectId ?? null);
|
||||
if (eligibilityFailure) {
|
||||
if (eligibilityFailure === "CONNECTOR_MODEL_NOT_ALLOWED") {
|
||||
|
||||
Reference in new issue
Block a user