Skip to content

Commit 997c051

Browse files
committed
perf: unify approval profile and mode
1 parent 83c1f60 commit 997c051

File tree

20 files changed

+187
-218
lines changed

20 files changed

+187
-218
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ Each layer has one job. No layer knows about the layers above it.
3333
- Model and thinking level restored per session
3434

3535
**Security**
36-
- Three profiles: `strict` / `balanced` / `off`
36+
- Four permission modes: `strict` / `balanced` / `accept-edits` / `trust`
3737
- Dangerous command blocking (rm -rf, sudo, dd, ...)
3838
- Workspace-scoped file access
3939
- JSON audit log for every tool decision
@@ -112,15 +112,15 @@ echo "explain main.go" | codebot -p
112112
codebot -c
113113

114114
# Strict security
115-
codebot -policy-profile strict
115+
codebot --mode strict
116116
```
117117

118118
## Design Principles
119119

120120
1. **Reuse before reinvent** — agentcore does the agent loop, codebot doesn't redo it
121121
2. **No premature abstraction** — every interface has at least two real callers
122122
3. **Convention over configuration** — sensible defaults, explicit overrides
123-
4. **Secure by default** — balanced policy, audit trail, workspace boundaries
123+
4. **Secure by default** — balanced mode, audit trail, workspace boundaries
124124

125125
## Configuration
126126

README_zh.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
- 模型和思考级别按会话保存
3434

3535
**安全**
36-
- 三种策略`strict` / `balanced` / `off`
36+
- 四种权限模式`strict` / `balanced` / `accept-edits` / `trust`
3737
- 危险命令拦截(rm -rf, sudo, dd, ...)
3838
- 工作区范围的文件访问控制
3939
- 每次工具决策的 JSON 审计日志
@@ -112,15 +112,15 @@ echo "explain main.go" | codebot -p
112112
codebot -c
113113

114114
# 严格安全策略
115-
codebot -policy-profile strict
115+
codebot --mode strict
116116
```
117117

118118
## 设计原则
119119

120120
1. **复用优先** — agentcore 做 Agent 循环,codebot 不重复造轮子
121121
2. **拒绝过早抽象** — 每个接口至少有两个真实调用者
122122
3. **约定优于配置** — 合理默认值,显式覆盖
123-
4. **默认安全** — balanced 策略、审计追踪、工作区边界
123+
4. **默认安全** — balanced 模式、审计追踪、工作区边界
124124

125125
## 配置
126126

cmd/codebot/main.go

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@ import (
44
"flag"
55
"fmt"
66
"os"
7-
"strings"
87

9-
"github.com/voocel/codebot/internal/approval"
108
"github.com/voocel/codebot/internal/bootstrap"
119
"github.com/voocel/codebot/internal/config"
1210
"github.com/voocel/codebot/internal/ui"
@@ -25,9 +23,7 @@ func main() {
2523
jsonFlag := flag.Bool("json", false, "JSON output mode (implies -p)")
2624
continueFlag := flag.Bool("c", false, "Continue most recent session")
2725
resumeFlag := flag.Bool("r", false, "Select a session to resume")
28-
approvalProfileFlag := flag.String("approval-profile", "balanced", "Approval profile: strict, balanced, off")
29-
modeFlag := flag.String("mode", "normal", "Permission mode: normal, accept-edits, trust")
30-
trustFlag := flag.Bool("trust", false, "Start in trust mode (skip approval prompts, deny rules still enforced)")
26+
modeFlag := flag.String("mode", "balanced", "Permission mode: strict, balanced, accept-edits, trust")
3127
flag.Parse()
3228

3329
if *versionFlag {
@@ -38,27 +34,17 @@ func main() {
3834
printMode := *printFlag || *jsonFlag
3935

4036
rt, err := bootstrap.Boot(bootstrap.Options{
41-
Continue: *continueFlag,
42-
Resume: *resumeFlag,
43-
NonTTYMode: printMode,
44-
ApprovalProfile: *approvalProfileFlag,
37+
Continue: *continueFlag,
38+
Resume: *resumeFlag,
39+
NonTTYMode: printMode,
40+
ApprovalMode: *modeFlag,
4541
})
4642
if err != nil {
4743
fmt.Fprintf(os.Stderr, "boot error: %v\n", err)
4844
os.Exit(1)
4945
}
5046
defer rt.Close()
5147

52-
if rt.ApprovalEngine != nil {
53-
switch {
54-
case *trustFlag:
55-
rt.ApprovalEngine.SetMode(approval.ModeTrust)
56-
case *modeFlag != "normal":
57-
m := strings.ReplaceAll(*modeFlag, "-", "_")
58-
rt.ApprovalEngine.SetMode(approval.Mode(m))
59-
}
60-
}
61-
6248
if printMode {
6349
if err := ui.RunPrint(rt.Session, flag.Args(), *jsonFlag); err != nil {
6450
fmt.Fprintf(os.Stderr, "error: %v\n", err)

internal/approval/engine.go

Lines changed: 24 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import (
1414
type Mode string
1515

1616
const (
17-
ModeNormal Mode = "normal"
17+
ModeStrict Mode = "strict"
18+
ModeBalanced Mode = "balanced"
1819
ModeAcceptEdits Mode = "accept_edits"
1920
ModeTrust Mode = "trust"
2021
ModePlan Mode = "plan" // 兼容旧调用;新代码应使用 SetPlanMode
@@ -82,7 +83,6 @@ type HookRequest struct {
8283

8384
type AuditEntry struct {
8485
Time time.Time
85-
Profile Profile
8686
Mode Mode
8787
PlanMode bool
8888
Tool string
@@ -101,7 +101,6 @@ type FilesystemRoots struct {
101101
type Engine struct {
102102
workspace string
103103
fsRoots FilesystemRoots
104-
profile Profile
105104
rules *RuleSet
106105

107106
mu sync.RWMutex
@@ -115,7 +114,7 @@ type Engine struct {
115114
toolMeta map[string]ToolMetadata
116115
}
117116

118-
func NewEngine(cwd string, profile Profile, rules *RuleSet, onAudit func(AuditEntry)) (*Engine, error) {
117+
func NewEngine(cwd string, mode Mode, rules *RuleSet, onAudit func(AuditEntry)) (*Engine, error) {
119118
store, err := NewStore(config.ApprovalsPath(cwd))
120119
if err != nil {
121120
return nil, fmt.Errorf("load approvals: %w", err)
@@ -126,9 +125,8 @@ func NewEngine(cwd string, profile Profile, rules *RuleSet, onAudit func(AuditEn
126125
ReadRoots: []string{cwd},
127126
WriteRoots: []string{cwd},
128127
},
129-
profile: profile,
130128
rules: rules,
131-
mode: ModeNormal,
129+
mode: mode,
132130
sessionAllow: make(map[string]storedEntry),
133131
store: store,
134132
onAudit: onAudit,
@@ -311,7 +309,7 @@ const (
311309
)
312310

313311
func (e *Engine) decide(info toolInfo, mode Mode, planMode bool) (ruleAction, string) {
314-
// Deny rules have highest priority — override even ProfileOff and cached approvals.
312+
// Deny rules have highest priority — override everything.
315313
var ruleResult ruleAction
316314
var ruleMatched bool
317315
if e.rules != nil {
@@ -321,8 +319,7 @@ func (e *Engine) decide(info toolInfo, mode Mode, planMode bool) (ruleAction, st
321319
}
322320
}
323321

324-
// Paths outside configured roots always require explicit approval,
325-
// regardless of profile, skill allows, or cached approvals.
322+
// Paths outside configured roots always require explicit approval.
326323
if info.outsideRoots {
327324
return ruleAsk, info.reason
328325
}
@@ -337,19 +334,16 @@ func (e *Engine) decide(info toolInfo, mode Mode, planMode bool) (ruleAction, st
337334
}
338335
}
339336

340-
if e.profile == ProfileOff {
341-
return ruleAllow, ""
342-
}
343-
344337
if e.allowed(info.key) || e.allowedSession(info.capability) {
345338
return ruleAllow, ""
346339
}
347340

348-
// Allow rules take precedence over mode/profile defaults.
341+
// Allow rules take precedence over mode defaults.
349342
if ruleMatched && ruleResult == ruleAllow {
350343
return ruleAllow, "allowed by permission rule"
351344
}
352345

346+
// Plan mode enforces read-only regardless of mode.
353347
if planMode {
354348
switch info.capability {
355349
case CapRead, CapInternal:
@@ -358,31 +352,33 @@ func (e *Engine) decide(info toolInfo, mode Mode, planMode bool) (ruleAction, st
358352
return ruleDeny, "plan mode is read-only"
359353
}
360354
}
361-
if mode == ModeTrust {
362-
return ruleAllow, ""
363-
}
364-
if mode == ModeAcceptEdits && info.capability == CapWrite {
365-
return ruleAllow, "auto-approved in accept-edits mode"
366-
}
367355

368-
switch e.profile {
369-
case ProfileStrict:
356+
// Mode-based decisions (strict → balanced → accept-edits → trust).
357+
switch mode {
358+
case ModeTrust:
359+
return ruleAllow, ""
360+
case ModeAcceptEdits:
361+
switch info.capability {
362+
case CapRead, CapInternal, CapWrite:
363+
return ruleAllow, ""
364+
default:
365+
return ruleAsk, "approval required for side effects"
366+
}
367+
case ModeStrict:
370368
switch info.capability {
371369
case CapRead, CapInternal:
372370
return ruleAllow, ""
373371
case CapWrite:
374-
return ruleAsk, "strict profile requires approval for writes"
372+
return ruleAsk, "strict mode requires approval for writes"
375373
default:
376-
return ruleDeny, "strict profile denies this capability"
374+
return ruleDeny, "strict mode denies this capability"
377375
}
378-
default:
376+
default: // ModeBalanced
379377
switch info.capability {
380378
case CapRead, CapInternal:
381379
return ruleAllow, ""
382-
case CapWrite, CapExec, CapHook, CapNetwork, CapSubagent, CapUnknown:
383-
return ruleAsk, "approval required for side effects"
384380
default:
385-
return ruleAsk, "approval required"
381+
return ruleAsk, "approval required for side effects"
386382
}
387383
}
388384
}
@@ -498,7 +494,6 @@ func (e *Engine) audit(info toolInfo, mode Mode, planMode bool, decision string,
498494
}
499495
e.onAudit(AuditEntry{
500496
Time: time.Now(),
501-
Profile: e.profile,
502497
Mode: mode,
503498
PlanMode: planMode,
504499
Tool: info.tool,

0 commit comments

Comments
 (0)