Gate task execution on approval source refs
This commit is contained in:
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(`
|
||||
|
||||
Reference in new issue
Block a user