mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-10-27 17:03:49 +08:00
feat: configure incomplete script review path
This commit is contained in:
1 parent
97213e357b
commit
dc72b5b4a9
7 files changed
+79
-23
No files matched your search
+31
-21
@@ -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":
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 天每日序列。
|
||||
|
||||
Reference in new issue
Block a user