From dc72b5b4a9e834ed3d2d357f9175af863af80931 Mon Sep 17 00:00:00 2001 From: ji ruo <79234113+RuoJi6@users.noreply.github.com> Date: Mon, 12 Oct 2026 18:01:49 +0800 Subject: [PATCH] feat: configure incomplete script review path --- intercept/intercept.go | 52 ++++++++++++-------- intercept/review_context_test.go | 23 +++++++++ server/intercept.go | 8 +++ web/src/app/(main)/system/intercept/page.tsx | 13 +++++ web/src/components/approval-records.tsx | 4 +- web/src/lib/mock/handler.ts | 1 + web/src/lib/types.ts | 1 + 7 files changed, 79 insertions(+), 23 deletions(-) diff --git a/intercept/intercept.go b/intercept/intercept.go index cdeff9c..a7b6a27 100644 --- a/intercept/intercept.go +++ b/intercept/intercept.go @@ -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": diff --git a/intercept/review_context_test.go b/intercept/review_context_test.go index 01da246..fc4833c 100644 --- a/intercept/review_context_test.go +++ b/intercept/review_context_test.go @@ -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") diff --git a/server/intercept.go b/server/intercept.go index 809eac2..d5fc6af 100644 --- a/server/intercept.go +++ b/server/intercept.go @@ -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 diff --git a/web/src/app/(main)/system/intercept/page.tsx b/web/src/app/(main)/system/intercept/page.tsx index 3075472..dc0d1d6 100644 --- a/web/src/app/(main)/system/intercept/page.tsx +++ b/web/src/app/(main)/system/intercept/page.tsx @@ -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() { + + + diff --git a/web/src/components/approval-records.tsx b/web/src/components/approval-records.tsx index ef613f3..e44f9cd 100644 --- a/web/src/components/approval-records.tsx +++ b/web/src/components/approval-records.tsx @@ -209,7 +209,7 @@ function ScriptSnapshotSummary({ input }: { input: InterceptReviewInput }) {

审查脚本快照

- {input.script_capture_status === "incomplete" ? "读取不完整 · 需人工确认" : "已附加"} + {input.script_capture_status === "incomplete" ? "读取不完整" : "已附加"}
{files.map((file) => ( @@ -311,7 +311,7 @@ function ModelReviewContext({ input }: { input: InterceptReviewInput }) { ))} {input.script_capture_status === "incomplete" ? ( - 无法完整读取当前执行脚本,平台要求人工确认。 + 无法完整读取当前执行脚本;默认转人工审查,也可以在模型设置中继续交给审查模型。 ) : null} diff --git a/web/src/lib/mock/handler.ts b/web/src/lib/mock/handler.ts index 90a9241..4f359bb 100644 --- a/web/src/lib/mock/handler.ts +++ b/web/src/lib/mock/handler.ts @@ -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") diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 365d779..7cc09fa 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -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 天每日序列。