@@ -949,15 +1087,85 @@ export function ApprovalRecords({ taskId }: { taskId?: string }) {
Failed to load pending approvals: {pendingError}. Click Refresh to retry.
) : null}
+ {batchErrors.length ? (
+
+
+ {batchErrors.length} selected request(s) were not decided and stay pending:
+
+ {batchErrors.map((f) => (
+ -
+ #{f.id}: {f.error}
+
+ ))}
+
+
+
+ ) : null}
{pending.length ? (
Pending ({pending.length})expand to allow or deny
-
+
+
+
{selectedRows.length} selected
+
+
+
+
+
) : null}
+
{
+ if (!open && !deciding) setBatchDecision(null);
+ }}
+ >
+
+
+
+ {batchDecision === "denied" ? "Deny" : "Approve"} {selectedRows.length} request(s)?
+
+
+ {selectedHigh ? `${selectedHigh} of them are high risk.` : "None of them is high risk."} Each request is
+ decided on its own; one that cannot be decided stays pending and is listed.
+
+
+
+ Cancel
+ {
+ event.preventDefault();
+ if (batchDecision) void batchDecide(batchDecision);
+ }}
+ >
+ {batchDecision === "denied" ? "Deny selected" : "Approve selected"}
+
+
+
+
{filtered ? "Filtered results" : "All records"} ({total})
diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts
index 59e5fd4..178fad5 100644
--- a/web/src/lib/api.ts
+++ b/web/src/lib/api.ts
@@ -1232,6 +1232,9 @@ export const api = {
interceptGetOne: (id: number) => get(`/intercept/pending/${id}`),
interceptDecide: (id: number, decision: "allowed" | "denied") =>
post<{ ok: boolean }>(`/intercept/pending/${id}/decide`, { decision }),
+ // 200 even on partial failure: each id carries its own ok/error.
+ interceptBatchDecide: (ids: number[], decision: "allowed" | "denied") =>
+ post("/intercept/pending/batch-decide", { ids, decision }),
// No body: the server derives the rule from the stored request, which stays pending.
interceptAlwaysAllow: (id: number) =>
post<{ rule: InterceptRule; created: boolean }>(`/intercept/pending/${id}/always-allow`),
diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts
index 854792a..ec3bf3d 100644
--- a/web/src/lib/types.ts
+++ b/web/src/lib/types.ts
@@ -1368,6 +1368,20 @@ export interface InterceptPending {
reason: string; // the rule's message or the model's verdict reason (model verdicts carry a [model] prefix)
decided_at?: string;
created_at: string;
+ // Deterministic risk score, computed by the server when a pending row is read.
+ risk?: InterceptRisk;
+}
+
+export interface InterceptRisk {
+ score: number; // 0..100
+ level: "low" | "medium" | "high";
+ reasons: string[];
+}
+
+export interface InterceptBatchDecideResult {
+ decided: number;
+ failed: number;
+ results: { id: number; ok: boolean; error?: string }[];
}
// JudgeConfig: global config for model fallback approval (the model decides only when no interception rule matches).