feat(intercept): 模型审查补充任务上下文与裁决说明 (#124)
审查模型改为接收版本化 JSON:任务描述/目标/操作约束、工作目录、Worker 意图、当前轮输入,以及按 tool_use_id 配对的近期工具执行记录;末尾是完整的 tool_name 与 arguments。 - 任务约束每次审查从数据库重新读取;读取失败或约束超预算转人工。 - 历史只保留唯一配对的执行事实,排除 Agent 自述、未完成请求与平台拒绝反馈。 - 裁决改为 JSON decision(allow/ask/deny) + comment,三种结果均须说明实际操作、成功后果与命中规则。 - 所有模型裁决(含自动放行)保存实际模型输入与 SHA-256 指纹,详情页可展开。 - 对话审批卡片支持展开完整请求、模型输入、裁决与执行结果。 - 修复父目录 lockfile 导致的 Turbopack 根目录误判。 官方默认拦截规则、工具范围与优先级不变。
This commit is contained in:
1 parent
234bd8bf8e
commit
c0d1aa6f37
26 files changed
+1159
-199
No files matched your search
@@ -24,7 +24,7 @@ import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { api } from "@/lib/api";
|
||||
import type { InterceptApprovalRow, InterceptAudit, InterceptDetail } from "@/lib/types";
|
||||
import type { InterceptApprovalRow, InterceptAudit, InterceptDetail, InterceptReviewInput } from "@/lib/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function fmtTime(value?: string) {
|
||||
@@ -44,6 +44,12 @@ function source(row: InterceptApprovalRow) {
|
||||
return row.reason?.startsWith("[模型]") ? "model" : "unknown";
|
||||
}
|
||||
|
||||
function originLabel(row: InterceptApprovalRow) {
|
||||
if (row.task_id) return row.task_id;
|
||||
if (row.conversation_id) return `对话 #${row.conversation_id}`;
|
||||
return "—";
|
||||
}
|
||||
|
||||
function StatusBadge({ status }: { status: string }) {
|
||||
const labels: Record<string, string> = { pending: "待审批", allowed: "已允许", denied: "已拒绝", timeout: "已超时" };
|
||||
let variant: "default" | "destructive" | "secondary" | "outline" = "outline";
|
||||
@@ -119,6 +125,66 @@ const executionLabels: Record<InterceptAudit["execution_status"], string> = {
|
||||
unknown: "执行结果未知",
|
||||
};
|
||||
|
||||
function ModelReviewContext({ input }: { input: InterceptReviewInput }) {
|
||||
return (
|
||||
<section className="flex min-w-0 flex-col gap-4" aria-label="模型审查上下文">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h3 className="font-medium text-sm">模型审查上下文</h3>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
以下为本次实际发送给审查模型的输入快照,包含当时可用的任务背景与执行记录。
|
||||
{input.task ? "当前轮输入可能由调度器生成,不等同于原始用户消息。" : "当前轮输入为本次 Agent 收到的消息。"}
|
||||
</p>
|
||||
</div>
|
||||
{input.turn_input ? (
|
||||
<CodeBlock label="当前轮输入" text={input.turn_input} truncated={input.background_truncated} />
|
||||
) : (
|
||||
<p className="text-muted-foreground text-xs">本次审查输入未包含当前轮消息。</p>
|
||||
)}
|
||||
{input.task ? (
|
||||
<>
|
||||
<CodeBlock label="任务描述" text={input.task.description} truncated={input.task.truncated} />
|
||||
<CodeBlock label="任务目标" text={input.task.goal} truncated={input.task.truncated} />
|
||||
<CodeBlock label="任务操作约束" text={JSON.stringify(input.task.constraints, null, 2)} />
|
||||
</>
|
||||
) : null}
|
||||
{input.worker_intent ? (
|
||||
<CodeBlock label="Worker 意图" text={input.worker_intent} truncated={input.background_truncated} />
|
||||
) : null}
|
||||
{input.working_directory ? <CodeBlock label="工作目录" text={input.working_directory} /> : null}
|
||||
<div className="flex min-w-0 flex-col gap-3">
|
||||
<h4 className="font-medium text-muted-foreground text-xs">提供给模型的近期工具执行记录</h4>
|
||||
{input.history?.length ? (
|
||||
input.history.map((entry) => (
|
||||
<div key={entry.tool_use_id} className="flex min-w-0 flex-col gap-2 rounded-lg border p-3">
|
||||
<p className="break-words font-medium text-xs">
|
||||
{entry.tool} · {entry.status === "succeeded" ? "成功" : "失败(可能有部分副作用)"}
|
||||
</p>
|
||||
<CodeBlock label="历史调用参数" text={entry.arguments_preview} truncated={entry.truncated} />
|
||||
<CodeBlock label="历史执行结果" text={entry.result} truncated={entry.truncated} />
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-muted-foreground text-xs">本次未提供可配对的历史工具执行记录。</p>
|
||||
)}
|
||||
{input.history_truncated ? (
|
||||
<p className="text-muted-foreground text-xs">历史为有限窗口,部分内容已截断。</p>
|
||||
) : null}
|
||||
</div>
|
||||
<Collapsible>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="self-start">
|
||||
<ChevronDownIcon data-icon="inline-start" />
|
||||
查看完整模型审查输入 JSON
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="pt-3">
|
||||
<CodeBlock label="模型审查输入" text={JSON.stringify(input, null, 2)} />
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
type Decide = (id: number, decision: "allowed" | "denied") => Promise<void>;
|
||||
|
||||
function DecisionActions({ row, busy, decide }: { row: InterceptApprovalRow; busy: boolean; decide: Decide }) {
|
||||
@@ -137,21 +203,27 @@ function DecisionActions({ row, busy, decide }: { row: InterceptApprovalRow; bus
|
||||
);
|
||||
}
|
||||
|
||||
function ApprovalDetail({
|
||||
export function ApprovalDetail({
|
||||
row,
|
||||
busy,
|
||||
decide,
|
||||
revision,
|
||||
readOnly = false,
|
||||
defaultExpanded = false,
|
||||
onResolved,
|
||||
}: {
|
||||
row: InterceptApprovalRow;
|
||||
busy: boolean;
|
||||
decide: Decide;
|
||||
revision: number;
|
||||
readOnly?: boolean;
|
||||
defaultExpanded?: boolean;
|
||||
onResolved?: (status: "allowed" | "denied" | "timeout") => void;
|
||||
}) {
|
||||
const [detail, setDetail] = React.useState<InterceptDetail | null>(null);
|
||||
const [error, setError] = React.useState("");
|
||||
const [retry, setRetry] = React.useState(0);
|
||||
const [more, setMore] = React.useState(false);
|
||||
const [more, setMore] = React.useState(defaultExpanded);
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: Status, refresh and retry invalidate details without closing the panel.
|
||||
React.useEffect(() => {
|
||||
@@ -163,6 +235,7 @@ function ApprovalDetail({
|
||||
if (cancelled) return;
|
||||
setDetail(next);
|
||||
setError("");
|
||||
if (next.status !== "pending") onResolved?.(next.status);
|
||||
if (next.status === "pending" || next.audit?.execution_status === "awaiting_result")
|
||||
timer = setTimeout(() => void load(), 5000);
|
||||
} catch (e) {
|
||||
@@ -174,7 +247,7 @@ function ApprovalDetail({
|
||||
cancelled = true;
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [row.id, row.status, revision, retry]);
|
||||
}, [row.id, row.status, revision, retry, onResolved]);
|
||||
|
||||
// Row updates are authoritative until the lazy detail has caught up.
|
||||
const current = detail?.status === row.status ? detail : row;
|
||||
@@ -191,7 +264,7 @@ function ApprovalDetail({
|
||||
<div className="grid min-w-0 gap-5 rounded-xl border bg-muted/20 p-4 lg:grid-cols-2">
|
||||
<div className="flex min-w-0 flex-col gap-3">
|
||||
<CodeBlock
|
||||
label={`${row.agent_name || row.conv_agent_key || "Agent"} · 工具请求`}
|
||||
label={`${current.agent_name || current.conv_agent_key || "Agent"} · 工具请求`}
|
||||
text={JSON.stringify(row.tool_input ?? {}, null, 2)}
|
||||
/>
|
||||
{command ? (
|
||||
@@ -226,9 +299,7 @@ function ApprovalDetail({
|
||||
{audit?.effective_action ? <p className="text-sm">最终动作:{actionLabels[audit.effective_action]}</p> : null}
|
||||
<dl className="grid grid-cols-[auto_minmax(0,1fr)] gap-x-4 gap-y-2 text-xs">
|
||||
<dt className="text-muted-foreground">来源</dt>
|
||||
<dd className="break-words">
|
||||
{row.conv_title || (row.task_id ? `任务 ${row.task_id}` : row.agent_name || "未记录")}
|
||||
</dd>
|
||||
<dd className="break-words">{current.task_id ? `任务 ${current.task_id}` : originLabel(current)}</dd>
|
||||
<dt className="text-muted-foreground">申请时间</dt>
|
||||
<dd>{fmtTime(row.created_at)}</dd>
|
||||
<dt className="text-muted-foreground">决定时间</dt>
|
||||
@@ -246,7 +317,7 @@ function ApprovalDetail({
|
||||
</>
|
||||
) : null}
|
||||
</dl>
|
||||
<DecisionActions row={current} busy={busy} decide={decide} />
|
||||
{!readOnly ? <DecisionActions row={current} busy={busy} decide={decide} /> : null}
|
||||
</div>
|
||||
</div>
|
||||
{error ? (
|
||||
@@ -277,28 +348,59 @@ function ApprovalDetail({
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="pt-4">
|
||||
<div className="flex min-w-0 flex-col gap-5">
|
||||
<CodeBlock label="用户消息 / 当前轮输入" text={audit.user_message} truncated={audit.user_truncated} />
|
||||
<section className="flex min-w-0 flex-col gap-3">
|
||||
<h3 className="font-medium text-muted-foreground text-xs">可见会话上下文</h3>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
保存于 {fmtTime(audit.captured_at)} 的会话记录片段。模型审批目前仅接收工具名称和参数。
|
||||
</p>
|
||||
{audit.context_truncated ? (
|
||||
<p className="text-muted-foreground text-xs">仅保存最近的上下文,部分内容已截断。</p>
|
||||
) : null}
|
||||
{audit.context?.length ? (
|
||||
audit.context.map((entry, index) => (
|
||||
<CodeBlock
|
||||
key={`${entry.kind}-${entry.tool_use_id || index}`}
|
||||
label={`${contextLabels[entry.kind] ?? entry.kind}${entry.tool ? ` · ${entry.tool}` : ""}${entry.is_error ? " · 异常" : ""}`}
|
||||
text={entry.text}
|
||||
truncated={entry.truncated}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<p className="text-muted-foreground text-sm">未记录可关联的上下文</p>
|
||||
)}
|
||||
</section>
|
||||
{audit.model_input ? (
|
||||
<ModelReviewContext input={audit.model_input} />
|
||||
) : (
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
{audit.model_input_digest
|
||||
? "此历史记录仅保存审查输入指纹,未保存输入原文,无法还原当时发给模型的上下文。这不代表审查时没有上下文;新产生的模型裁决会保留输入快照。"
|
||||
: "此记录没有保存模型审查输入,可能由规则直接裁决、模型调用前发生异常或产生于旧版本。"}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
{audit.user_message || audit.context?.length ? (
|
||||
<Collapsible>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button variant="ghost" size="sm">
|
||||
<ChevronDownIcon data-icon="inline-start" />
|
||||
查看会话审计片段
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="pt-3">
|
||||
<div className="flex min-w-0 flex-col gap-3">
|
||||
{audit.user_message ? (
|
||||
<CodeBlock
|
||||
label="会话当前轮输入(审计片段)"
|
||||
text={audit.user_message}
|
||||
truncated={audit.user_truncated}
|
||||
/>
|
||||
) : null}
|
||||
<section className="flex min-w-0 flex-col gap-3">
|
||||
<h3 className="font-medium text-muted-foreground text-xs">可见会话上下文</h3>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
保存于 {fmtTime(audit.captured_at)} 的会话记录片段。模型实际使用的内容以“模型审查输入”为准。
|
||||
</p>
|
||||
{audit.context_truncated ? (
|
||||
<p className="text-muted-foreground text-xs">仅保存最近的上下文,部分内容已截断。</p>
|
||||
) : null}
|
||||
{audit.context?.length ? (
|
||||
audit.context.map((entry, index) => (
|
||||
<CodeBlock
|
||||
key={`${entry.kind}-${entry.tool_use_id || index}`}
|
||||
label={`${contextLabels[entry.kind] ?? entry.kind}${entry.tool ? ` · ${entry.tool}` : ""}${entry.is_error ? " · 异常" : ""}`}
|
||||
text={entry.text}
|
||||
truncated={entry.truncated}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<p className="text-muted-foreground text-sm">未记录可关联的上下文</p>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
) : null}
|
||||
<CodeBlock
|
||||
label={`${initialLabel}:${actionLabels[audit.initial_action] ?? audit.initial_action}`}
|
||||
text={audit.initial_reason.replace(/^\[模型\]\s*/, "")}
|
||||
@@ -321,6 +423,7 @@ function ApprovalDetail({
|
||||
<div>工具调用 ID:{audit.tool_use_id || "未记录"}</div>
|
||||
<div>参数摘要 SHA-256:{audit.input_digest}</div>
|
||||
<div>审查配置指纹 SHA-256:{audit.config_digest || "未记录"}</div>
|
||||
{audit.model_input_digest ? <div>模型审查输入 SHA-256:{audit.model_input_digest}</div> : null}
|
||||
{audit.execution_ended_at ? <div>结果记录时间:{fmtTime(audit.execution_ended_at)}</div> : null}
|
||||
</dl>
|
||||
</div>
|
||||
@@ -422,8 +525,8 @@ function ApprovalTable({
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="truncate" title={row.conv_title || row.task_id}>
|
||||
{row.conv_title || row.task_id || "—"}
|
||||
<span className="truncate" title={originLabel(row)}>
|
||||
{originLabel(row)}
|
||||
</span>
|
||||
<span className="truncate text-muted-foreground text-xs">
|
||||
{row.agent_name || row.conv_agent_key}
|
||||
|
||||
@@ -20,7 +20,9 @@ import { api } from "@/lib/api";
|
||||
import { Markdown } from "@/components/markdown";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { Activity } from "@/lib/types";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import { ApprovalDetail } from "@/components/approval-records";
|
||||
import type { Activity, InterceptPending } from "@/lib/types";
|
||||
|
||||
// ---- per-agent lane color (planner + work#1/#2/#3 …) ---------------------------
|
||||
const workerColors = [
|
||||
@@ -203,6 +205,10 @@ function InterceptCard({
|
||||
const [detail, setDetail] = React.useState<Record<string, unknown> | null>(null);
|
||||
const [decided, setDecided] = React.useState<"allowed" | "denied" | "timeout" | null>(null);
|
||||
const [deciding, setDeciding] = React.useState(false);
|
||||
const [expanded, setExpanded] = React.useState(false);
|
||||
const [pending, setPending] = React.useState<InterceptPending | null>(null);
|
||||
const [statusError, setStatusError] = React.useState("");
|
||||
const [retry, setRetry] = React.useState(0);
|
||||
|
||||
// Load persisted detail JSON + check the real current status from the backend
|
||||
// so that a page refresh shows the already-decided state instead of re-offering buttons.
|
||||
@@ -217,17 +223,20 @@ function InterceptCard({
|
||||
return () => { live = false; };
|
||||
}, [step.seq, getDetail]);
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: Retry explicitly reloads the same approval after a failed request.
|
||||
React.useEffect(() => {
|
||||
if (!pendingId) return;
|
||||
let live = true;
|
||||
api.interceptGetOne(pendingId)
|
||||
.then((p) => {
|
||||
if (!live) return;
|
||||
setPending(p);
|
||||
setStatusError("");
|
||||
if (p.status !== "pending") setDecided(p.status as "allowed" | "denied" | "timeout");
|
||||
})
|
||||
.catch(() => {/* ignore */});
|
||||
.catch((error) => { if (live) setStatusError((error as Error).message || "审批详情加载失败"); });
|
||||
return () => { live = false; };
|
||||
}, [pendingId]);
|
||||
}, [pendingId, retry]);
|
||||
|
||||
async function decide(decision: "allowed" | "denied") {
|
||||
if (step.inherited || !pendingId || deciding) return;
|
||||
@@ -247,9 +256,11 @@ function InterceptCard({
|
||||
? JSON.stringify(detail.input).slice(0, 200)
|
||||
: null;
|
||||
|
||||
const row = pending ? { ...pending, status: decided || pending.status, conv_title: "", conv_agent_key: "", rule_name: "" } : null;
|
||||
|
||||
return (
|
||||
<div className="my-2 rounded-lg border border-amber-400/50 bg-amber-50/40 dark:bg-amber-950/15 p-3 text-xs">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div className="flex items-start gap-2 min-w-0">
|
||||
<ShieldAlertIcon className="mt-0.5 h-3.5 w-3.5 shrink-0 text-amber-500" />
|
||||
<div className="min-w-0 space-y-0.5">
|
||||
@@ -305,6 +316,34 @@ function InterceptCard({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{pendingId ? (
|
||||
<Collapsible open={expanded} onOpenChange={setExpanded} className="mt-2 min-w-0">
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button variant="ghost" size="sm" aria-label={expanded ? "收起审批详情" : "展开审批详情"}>
|
||||
{expanded ? <ChevronDown data-icon="inline-start" /> : <ChevronRight data-icon="inline-start" />}
|
||||
{expanded ? "收起审批详情" : "展开审批详情"}
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
{expanded && row ? (
|
||||
<ApprovalDetail
|
||||
row={row}
|
||||
busy={deciding}
|
||||
decide={(_id, decision) => decide(decision)}
|
||||
revision={retry}
|
||||
readOnly={Boolean(step.inherited)}
|
||||
defaultExpanded
|
||||
onResolved={setDecided}
|
||||
/>
|
||||
) : statusError ? (
|
||||
<div className="flex flex-wrap items-center gap-2 p-3" role="alert">
|
||||
<span>{statusError}</span>
|
||||
<Button variant="outline" size="sm" onClick={() => setRetry((value) => value + 1)}>重试详情</Button>
|
||||
</div>
|
||||
) : <p className="p-3 text-muted-foreground">正在加载审批详情…</p>}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -351,7 +390,7 @@ function ToolBlock({
|
||||
const segs: { label: string; seq: number }[] = [];
|
||||
if (use) segs.push({ label: "命令", seq: use.seq });
|
||||
if (result) segs.push({ label: "输出" + (result.is_error ? " ✕" : " ✓"), seq: result.seq });
|
||||
Promise.all(
|
||||
void Promise.all(
|
||||
segs.map((x) =>
|
||||
getDetail(x.seq)
|
||||
.then((d) => d || "(空)")
|
||||
@@ -369,7 +408,7 @@ function ToolBlock({
|
||||
return () => {
|
||||
live = false;
|
||||
};
|
||||
}, [open, detailKey, use, result, getDetail]);
|
||||
}, [open, detailKey, use, result, getDetail, toolName]);
|
||||
|
||||
function toggle() {
|
||||
setOpen((o) => !o);
|
||||
@@ -377,7 +416,7 @@ function ToolBlock({
|
||||
|
||||
return (
|
||||
<div className="text-xs">
|
||||
<button onClick={toggle} className="flex w-full items-start gap-2 py-1 text-left hover:bg-muted/40">
|
||||
<button type="button" onClick={toggle} className="flex w-full items-start gap-2 py-1 text-left hover:bg-muted/40">
|
||||
<span className="mt-0.5 text-muted-foreground">
|
||||
{open ? <ChevronDown className="size-3" /> : <ChevronRight className="size-3" />}
|
||||
</span>
|
||||
@@ -423,7 +462,7 @@ function MessageBlock({
|
||||
React.useEffect(() => {
|
||||
if (!open || loadedKey.current === detailKey) return;
|
||||
let live = true;
|
||||
Promise.all(
|
||||
void Promise.all(
|
||||
group.steps.map((s) =>
|
||||
getDetail(s.seq)
|
||||
.then((d) => d || s.summary)
|
||||
@@ -445,7 +484,7 @@ function MessageBlock({
|
||||
|
||||
return (
|
||||
<div className="text-xs">
|
||||
<button onClick={toggle} className="flex w-full items-start gap-2 py-1 text-left hover:bg-muted/40">
|
||||
<button type="button" onClick={toggle} className="flex w-full items-start gap-2 py-1 text-left hover:bg-muted/40">
|
||||
<span className="mt-0.5 text-muted-foreground">
|
||||
{open ? <ChevronDown className="size-3" /> : <ChevronRight className="size-3" />}
|
||||
</span>
|
||||
|
||||
@@ -1280,8 +1280,38 @@ export interface LLMTask {
|
||||
count: number;
|
||||
}
|
||||
|
||||
// The exact JSON sent to the review model, retained for all model verdicts.
|
||||
export interface InterceptReviewInput {
|
||||
version: number;
|
||||
task?: {
|
||||
task_id: number;
|
||||
description: string;
|
||||
goal: string;
|
||||
constraints: { id: number; kind: string; text: string; origin: string; created_at: number }[];
|
||||
truncated?: boolean;
|
||||
};
|
||||
working_directory?: string;
|
||||
worker_intent?: string;
|
||||
turn_input?: string;
|
||||
background_truncated?: boolean;
|
||||
history: {
|
||||
tool_use_id: string;
|
||||
tool: string;
|
||||
arguments_preview: string;
|
||||
result: string;
|
||||
status: "succeeded" | "failed";
|
||||
truncated?: boolean;
|
||||
}[];
|
||||
history_truncated?: boolean;
|
||||
correlation: "exact" | "ambiguous" | "unavailable";
|
||||
tool_name: string;
|
||||
arguments: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// Immutable review snapshot plus separately recorded execution outcome.
|
||||
export interface InterceptAudit {
|
||||
model_input?: InterceptReviewInput;
|
||||
model_input_digest?: string;
|
||||
run_id?: string;
|
||||
tool_use_id?: string;
|
||||
correlation: "exact" | "ambiguous" | "unavailable";
|
||||
|
||||
Reference in new issue
Block a user