Gate task execution on approval source refs

This commit is contained in:
ZacharyZhang-NY committed 2026-05-23 21:52:52 -04:00
1 parent 4dbd8997c0
commit c59be97b79
12 files changed
+183 -16

No files matched your search

@@ -9,6 +9,8 @@ import { OrgGovernanceService } from "./org-governance-service";
interface ApprovalAuditContext {
sourceMessageId?: string;
sourceThreadId?: string;
sourceEntityType?: string;
sourceEntityId?: string;
}
export class ApprovalService {
@@ -35,8 +37,8 @@ export class ApprovalService {
this.db.prepare(`
insert into approvals(
id, org_id, kind, title, details, risk, requested_by_type, requested_by_id,
state, decided_by_employee_id, created_at, decided_at
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
source_entity_type, source_entity_id, state, decided_by_employee_id, created_at, decided_at
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
id,
input.orgId,
@@ -46,6 +48,8 @@ export class ApprovalService {
input.risk,
input.requestedByType,
input.requestedById,
context.sourceEntityType ?? null,
context.sourceEntityId ?? null,
APPROVAL_STATE.PENDING,
null,
now,
@@ -113,7 +113,12 @@ export class BoardInterventionService {
risk: RISK_LEVEL.HIGH,
requestedByType: requester.type,
requestedById: requester.id,
}, { sourceMessageId: input.messageId, sourceThreadId: input.threadId });
}, {
sourceMessageId: input.messageId,
sourceThreadId: input.threadId,
sourceEntityType: "chat_message",
sourceEntityId: input.messageId,
});
}
private applyBoardThreadState(input: RecordBoardInterventionInput, command: BoardCommand, target: BoardTarget): void {
@@ -229,7 +229,7 @@ export class ChatService {
risk: RISK_LEVEL.HIGH,
requestedByType: requester.type,
requestedById: requester.id,
}, { sourceMessageId: messageId, sourceThreadId: threadId });
}, { sourceMessageId: messageId, sourceThreadId: threadId, sourceEntityType: "chat_message", sourceEntityId: messageId });
}
private recordSlashInvocation(messageId: string, slash: SlashInvocation, resultRef: string | null, now: string): void {
@@ -190,8 +190,8 @@ export class OrganizationService {
this.db.prepare(`
insert into approvals(
id, org_id, kind, title, details, risk, requested_by_type,
requested_by_id, state, decided_by_employee_id, created_at, decided_at
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
requested_by_id, source_entity_type, source_entity_id, state, decided_by_employee_id, created_at, decided_at
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
approvalId,
input.orgId,
@@ -201,6 +201,8 @@ export class OrganizationService {
RISK_LEVEL.MEDIUM,
"system",
null,
"agent",
id,
APPROVAL_STATE.PENDING,
null,
now,
@@ -242,28 +244,32 @@ export class OrganizationService {
now,
);
const policy = this.policy.evaluateTaskCreation(input.orgId, input.risk);
if (policy.approvalRequired) {
const approvalRequired = policy.approvalRequired || input.requiresFilesystem;
if (approvalRequired) {
const approvalId = createId("app");
const approvalKind = policy.approvalRequired ? "task_risk" : "task_execution";
this.db.prepare(`
insert into approvals(
id, org_id, kind, title, details, risk, requested_by_type,
requested_by_id, state, decided_by_employee_id, created_at, decided_at
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
requested_by_id, source_entity_type, source_entity_id, state, decided_by_employee_id, created_at, decided_at
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
approvalId,
input.orgId,
"task_risk",
approvalKind,
input.title.trim(),
input.description.trim(),
RISK_LEVEL.HIGH,
"system",
null,
"task",
id,
APPROVAL_STATE.PENDING,
null,
now,
null,
);
this.governance.routeApproval({ orgId: input.orgId, approvalId, kind: "task_risk", risk: RISK_LEVEL.HIGH });
this.governance.routeApproval({ orgId: input.orgId, approvalId, kind: approvalKind, risk: RISK_LEVEL.HIGH });
}
this.audit.record({
orgId: input.orgId,
@@ -262,7 +262,7 @@ export class SdlcService {
{ key: "provider_adapter_coverage", passed: missing.length === 0, value: `${adapterKeys.length}/${catalogKeys.length}` },
{ key: "provider_rate_limit_registry", passed: rateLimitRegistry === 1, value: rateLimitRegistry },
{ key: "provider_runtime_contract", passed: runtimeContractReady, value: runtimeContractReady },
{ key: "schema_version", passed: schemaVersion >= 29, value: schemaVersion },
{ key: "schema_version", passed: schemaVersion >= 30, value: schemaVersion },
];
}
@@ -352,7 +352,7 @@ export class SocialService {
risk: policy.risk_level,
requestedByType: "employee",
requestedById: employeeId,
});
}, { sourceEntityType: "scheduled_post", sourceEntityId: post.id });
this.assignSocialApprovalRoute(orgId, approvalId, route);
return approvalId;
}
@@ -1,6 +1,6 @@
import type { DatabaseSync } from "node:sqlite";
import type { RunTaskInput } from "../../shared/commands";
import { PROVIDER_EXECUTION_MODE } from "../../shared/domain";
import { APPROVAL_STATE, PROVIDER_EXECUTION_MODE, RISK_LEVEL } from "../../shared/domain";
import { createId } from "../../shared/ids";
import { AuditService } from "./audit-service";
import { ProviderService } from "./provider-service";
@@ -61,6 +61,11 @@ interface WorkspaceRow {
repo_ref: string;
}
interface TaskApprovalRow {
id: string;
state: string;
}
export class TaskExecutionService {
private readonly audit: AuditService;
@@ -73,6 +78,7 @@ export class TaskExecutionService {
async runTask(input: RunTaskInput): Promise<string> {
const task = this.requireTask(input.orgId, input.taskId);
this.assertExecutionApproval(input.orgId, task);
const agent = this.requireAssignedAgent(input.orgId, task);
const provider = this.requireAgentProvider(input.orgId, agent);
const runId = this.createRun(input.orgId, task.id, agent.id, provider.id);
@@ -141,6 +147,23 @@ export class TaskExecutionService {
return agent;
}
private assertExecutionApproval(orgId: string, task: TaskExecutionTaskRow): void {
if (task.risk !== RISK_LEVEL.HIGH && task.requires_filesystem !== 1) return;
const approval = this.db.prepare(`
select id, state
from approvals
where org_id = ?
and source_entity_type = 'task'
and source_entity_id = ?
and kind in ('task_risk', 'task_execution')
order by created_at desc
limit 1
`).get(orgId, task.id) as TaskApprovalRow | undefined;
if (!approval || approval.state !== APPROVAL_STATE.APPROVED) {
throw new Error("Approved task execution approval is required.");
}
}
private requireAgentProvider(orgId: string, agent: TaskExecutionAgentRow): TaskExecutionProviderRow {
if (!agent.model_provider_id) throw new Error("Task assignee requires a model provider.");
const provider = this.db.prepare(`