feat: configure incomplete script review path

This commit is contained in:
ji ruo committed 2026-10-12 18:01:49 +08:00
1 parent 97213e357b
commit dc72b5b4a9
7 files changed
+79 -23

No files matched your search

+31 -21
View File
@@ -308,6 +308,7 @@ const (
settingJudgeFailAction = "llm_judge_fail_action"
settingJudgeAskTimeoutSecs = "llm_judge_ask_timeout_seconds"
settingJudgeAskTimeoutAction = "llm_judge_ask_timeout_action"
settingJudgeIncompleteScript = "llm_judge_incomplete_script_action"
)
// Judge default values.
@@ -321,13 +322,14 @@ const (
// JudgeConfig is the resolved LLM-fallback-judge configuration. Prompt is always
// non-empty (falls back to DefaultJudgePrompt).
type JudgeConfig struct {
Enabled bool `json:"enabled"`
ProfileID int64 `json:"profile_id"` // 0 = follow active/default
Prompt string `json:"prompt"`
TimeoutSeconds int `json:"timeout_seconds"`
FailAction string `json:"fail_action"` // allow|ask|deny
AskTimeoutSeconds int `json:"ask_timeout_seconds"`
AskTimeoutAction string `json:"ask_timeout_action"` // allow|deny
Enabled bool `json:"enabled"`
ProfileID int64 `json:"profile_id"` // 0 = follow active/default
Prompt string `json:"prompt"`
TimeoutSeconds int `json:"timeout_seconds"`
FailAction string `json:"fail_action"` // allow|ask|deny
AskTimeoutSeconds int `json:"ask_timeout_seconds"`
AskTimeoutAction string `json:"ask_timeout_action"` // allow|deny
IncompleteScriptAction string `json:"incomplete_script_action"` // ask|model
}
// judgeConfig reads the judge configuration from settings, applying defaults for
@@ -335,12 +337,13 @@ type JudgeConfig struct {
// follows dwarfs a few KV reads, and freshness avoids a cache-invalidation path.
func (i *Interceptor) judgeConfig() JudgeConfig {
c := JudgeConfig{
Enabled: i.db.GetBool(settingJudgeEnabled, false),
ProfileID: int64(i.getSettingInt(settingJudgeProfileID, 0)),
TimeoutSeconds: i.getSettingInt(settingJudgeTimeoutSecs, defaultJudgeTimeoutSecs),
FailAction: i.getSettingChoice(settingJudgeFailAction, defaultJudgeFailAction, "allow", "ask", "deny"),
AskTimeoutSeconds: i.getSettingInt(settingJudgeAskTimeoutSecs, defaultJudgeAskTimeoutSecs),
AskTimeoutAction: i.getSettingChoice(settingJudgeAskTimeoutAction, defaultJudgeAskTimeoutAction, "allow", "deny"),
Enabled: i.db.GetBool(settingJudgeEnabled, false),
ProfileID: int64(i.getSettingInt(settingJudgeProfileID, 0)),
TimeoutSeconds: i.getSettingInt(settingJudgeTimeoutSecs, defaultJudgeTimeoutSecs),
FailAction: i.getSettingChoice(settingJudgeFailAction, defaultJudgeFailAction, "allow", "ask", "deny"),
AskTimeoutSeconds: i.getSettingInt(settingJudgeAskTimeoutSecs, defaultJudgeAskTimeoutSecs),
AskTimeoutAction: i.getSettingChoice(settingJudgeAskTimeoutAction, defaultJudgeAskTimeoutAction, "allow", "deny"),
IncompleteScriptAction: i.getSettingChoice(settingJudgeIncompleteScript, "ask", "ask", "model"),
}
// Prompt: stored value if non-empty, else the built-in template.
if v, ok, _ := i.db.GetSetting(settingJudgePrompt); ok && strings.TrimSpace(v) != "" {
@@ -417,6 +420,9 @@ func (i *Interceptor) SetJudgeConfig(c JudgeConfig) error {
if err := i.db.SetSetting(settingJudgeAskTimeoutAction, c.AskTimeoutAction); err != nil {
return err
}
if err := i.db.SetSetting(settingJudgeIncompleteScript, c.IncompleteScriptAction); err != nil {
return err
}
return nil
}
@@ -458,14 +464,10 @@ func (i *Interceptor) Judge(ctx context.Context, tool string, arguments json.Raw
if err != nil {
out = Decision{ProfileID: out.ProfileID, ModelFallback: true, Action: cfg.FailAction, Message: "模型审批失败,按失败策略处理: " + err.Error()}
}
// A script invocation whose source could not be captured is never safe to
// auto-allow: the model must not infer that an opaque or partial script has
// no side effects. Preserve an explicit deny, but convert an allow (including
// a configured fail-open response) into human review.
if input.ScriptCaptureStatus == "incomplete" && out.Action == "allow" {
out.Action = "ask"
out.Message = "脚本未能完整附加,无法排除隐藏副作用,需要人工确认"
}
// By default an incomplete script snapshot requires human review. Operators
// may explicitly choose model review; in that mode the model receives the
// read errors/partial snapshot and its verdict is honored.
out = applyIncompleteScriptPolicy(input, out, cfg.IncompleteScriptAction)
switch out.Action {
case "allow", "ask", "deny":
// valid verdict
@@ -497,6 +499,14 @@ func (i *Interceptor) Judge(ctx context.Context, tool string, arguments json.Raw
return out, true
}
func applyIncompleteScriptPolicy(input ReviewInput, out Decision, policy string) Decision {
if input.ScriptCaptureStatus == "incomplete" && policy != "model" && out.Action == "allow" {
out.Action = "ask"
out.Message = "脚本未能完整附加,无法排除隐藏副作用,需要人工确认"
}
return out
}
func judgeActionLabel(action string) string {
switch action {
case "allow":
+23
View File
@@ -179,6 +179,29 @@ func TestReviewInputShellCommandCombinations(t *testing.T) {
}
}
func TestIncompleteScriptPolicy(t *testing.T) {
incomplete := ReviewInput{ScriptCaptureStatus: "incomplete"}
for _, tc := range []struct {
name, policy, action, wantAction string
}{
{name: "default human review", policy: "ask", action: "allow", wantAction: "ask"},
{name: "model review", policy: "model", action: "allow", wantAction: "allow"},
{name: "model deny remains deny", policy: "model", action: "deny", wantAction: "deny"},
{name: "complete allow remains allow", policy: "ask", action: "allow", wantAction: "allow"},
} {
t.Run(tc.name, func(t *testing.T) {
input := incomplete
if tc.name == "complete allow remains allow" {
input.ScriptCaptureStatus = "complete"
}
got := applyIncompleteScriptPolicy(input, Decision{Action: tc.action}, tc.policy)
if got.Action != tc.wantAction {
t.Fatalf("action=%q want %q", got.Action, tc.wantAction)
}
})
}
}
func TestReviewInputResolvesCdAndSourceScript(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "login.sh")
+8
View File
@@ -466,6 +466,14 @@ func (s *Server) interceptSetJudgeConfig(w http.ResponseWriter, r *http.Request)
writeErr(w, 400, "ask_timeout_action 必须是 allow 或 deny")
return
}
switch req.IncompleteScriptAction {
case "":
req.IncompleteScriptAction = "ask"
case "ask", "model":
default:
writeErr(w, 400, "incomplete_script_action 必须是 ask 或 model")
return
}
if err := s.m.interceptor.SetJudgeConfig(req); err != nil {
writeErr(w, 500, err.Error())
return
@@ -203,6 +203,7 @@ const defaultJudge = (): JudgeConfig => ({
fail_action: "allow",
ask_timeout_seconds: 300,
ask_timeout_action: "deny",
incomplete_script_action: "ask",
});
function JudgeCard() {
@@ -396,6 +397,18 @@ function JudgeCard() {
</SelectContent>
</Select>
</Field>
<Field label="脚本快照不完整时">
<Select
value={cfg.incomplete_script_action}
onValueChange={(v) => patch({ incomplete_script_action: v as JudgeConfig["incomplete_script_action"] })}
>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="ask">转人工审查(默认)</SelectItem>
<SelectItem value="model">继续交给模型审查</SelectItem>
</SelectContent>
</Select>
</Field>
</div>
<Separator />
+2 -2
View File
@@ -209,7 +209,7 @@ function ScriptSnapshotSummary({ input }: { input: InterceptReviewInput }) {
<div className="flex flex-wrap items-center justify-between gap-2">
<h3 className="font-medium text-sm">审查脚本快照</h3>
<Badge variant={input.script_capture_status === "incomplete" ? "destructive" : "secondary"}>
{input.script_capture_status === "incomplete" ? "读取不完整 · 需人工确认" : "已附加"}
{input.script_capture_status === "incomplete" ? "读取不完整" : "已附加"}
</Badge>
</div>
{files.map((file) => (
@@ -311,7 +311,7 @@ function ModelReviewContext({ input }: { input: InterceptReviewInput }) {
))}
{input.script_capture_status === "incomplete" ? (
<Alert variant="destructive">
<AlertDescription>无法完整读取当前执行脚本,平台要求人工确认。</AlertDescription>
<AlertDescription>无法完整读取当前执行脚本;默认转人工审查,也可以在模型设置中继续交给审查模型。</AlertDescription>
</Alert>
) : null}
</section>
+1
View File
@@ -2514,6 +2514,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
fail_action: "allow",
ask_timeout_seconds: 300,
ask_timeout_action: "deny",
incomplete_script_action: "ask",
};
if (path === "/intercept/judge" && m === "PUT") return { ok: true };
if (path === "/intercept/judge/usage" && m === "GET")
+1
View File
@@ -1365,6 +1365,7 @@ export interface JudgeConfig {
fail_action: "allow" | "ask" | "deny"; // 模型出错/超时/不可解析时的回退
ask_timeout_seconds: number; // 模型判 ask 转人工后的审批等待超时
ask_timeout_action: "allow" | "deny"; // 审批超时后的默认动作
incomplete_script_action: "ask" | "model"; // 脚本快照不完整时:人工审查或继续模型审查
}
// JudgeUsage: 模型兜底审批(judge 通道)的累计 token 用量 + 近 N 天每日序列。