返回 DeepSeek-Reasonix
config_write_approval_test.go
根目录 / internal / control / config_write_approval_test.go
1 package control
2
3 import (
4 "encoding/json"
5 "testing"
6 "time"
7
8 "reasonix/internal/permission"
9 )
10
11 // TestManagedConfigWriteApprovalIsFreshHuman pins the security contract of the
12 // managed-config write prompt: it is a fresh human decision, so YOLO/auto
13 // approval postures must never answer it, while an explicit session grant for
14 // the same subject may.
15 func TestManagedConfigWriteApprovalIsFreshHuman(t *testing.T) {
16 if !RequiresFreshHumanApprovalTool(ManagedConfigWriteApprovalTool) {
17 t.Fatal("config_write must require a fresh human approval")
18 }
19 if !allowsFreshSessionGrantTool(ManagedConfigWriteApprovalTool) {
20 t.Fatal("config_write should allow explicit session grants for one repair flow")
21 }
22
23 a := newApprovalManager(permission.Policy{}, ToolApprovalYolo, time.Minute)
24 subject := "write Reasonix config: /home/u/.reasonix/config.toml"
25 if a.preApprovedForDecision(ManagedConfigWriteApprovalTool, subject, nil, true) {
26 t.Fatal("YOLO posture must not pre-approve a managed config write")
27 }
28 a.grantSession(ManagedConfigWriteApprovalTool, subject)
29 if !a.preApprovedForDecision(ManagedConfigWriteApprovalTool, subject, nil, true) {
30 t.Fatal("an explicit session grant should cover the same subject")
31 }
32 // Session grants for fresh decisions are tool-wide (mirroring
33 // sandbox_escape): one "allow for this session" covers the rest of the
34 // repair flow across the handful of managed config files.
35 if !a.preApprovedForDecision(ManagedConfigWriteApprovalTool, "write Reasonix config: /other/path", nil, true) {
36 t.Fatal("session grant should cover the repair flow tool-wide")
37 }
38 // But it must never leak to a different fresh-decision tool.
39 if a.preApprovedForDecision(SandboxEscapeApprovalTool, "run unconfined once: rm -rf /", nil, true) {
40 t.Fatal("config_write session grant must not answer sandbox_escape decisions")
41 }
42 }
43
44 func TestApprovedPlanAutoAllowsFallbackButPreservesExplicitRules(t *testing.T) {
45 a := newApprovalManager(
46 permission.New("ask", nil, []string{"sensitive_writer", "Edit(secret.txt)"}, []string{"denied_writer"}),
47 ToolApprovalAsk,
48 time.Minute,
49 )
50 a.setPlanAutoApprove(true)
51
52 if !a.preApproved("ordinary_writer", "ordinary.txt", json.RawMessage(`{"path":"ordinary.txt"}`)) {
53 t.Fatal("an approved plan should auto-allow the ordinary writer fallback")
54 }
55 if a.preApproved("sensitive_writer", "sensitive.txt", json.RawMessage(`{"path":"sensitive.txt"}`)) {
56 t.Fatal("an approved plan must not bypass an explicit ask rule")
57 }
58 moveArgs := json.RawMessage(`{"source_path":"ordinary.txt","destination_path":"secret.txt"}`)
59 if a.preApproved("move_file", "ordinary.txt", moveArgs) {
60 t.Fatal("an approved plan must evaluate every subject before bypassing an explicit ask rule")
61 }
62 if a.preApproved("denied_writer", "denied.txt", json.RawMessage(`{"path":"denied.txt"}`)) {
63 t.Fatal("an approved plan must not pre-approve an explicit deny rule")
64 }
65 }
66
67 // TestHeadlessGateRefusesManagedConfigApproval pins that the non-interactive
68 // gate cannot silently answer the config_write decision the way it resolves
69 // ordinary Ask permissions.
70 func TestHeadlessGateRefusesManagedConfigApproval(t *testing.T) {
71 gate := NewHeadlessPermissionGate(permission.Policy{Mode: permission.Ask})
72 allow, _, err := gate.Check(t.Context(), ManagedConfigWriteApprovalTool, nil, false)
73 if err != nil {
74 t.Fatal(err)
75 }
76 if allow {
77 t.Fatal("headless gate must refuse fresh-human config_write approvals")
78 }
79 }
80
80 lines GO