You've already forked agentic-coding-workflow
修改
This commit is contained in:
@@ -22,7 +22,8 @@ type SessionContext struct {
|
||||
WorkspaceID uuid.UUID
|
||||
UserID uuid.UUID
|
||||
CwdPath string
|
||||
AgentSessionID *string // 可能 nil(handshake 未完成时不应触发 fs/* 但兜底)
|
||||
AgentSessionID *string // 可能 nil(handshake 未完成时不应触发 fs/* 但兜底)
|
||||
ToolAllowlist []string // agent kind 配置的工具白名单,命中则自动放行
|
||||
}
|
||||
|
||||
// FsResult 是 fs/* method 处理后返回给 relay 的结果。要么 OK(带 result),
|
||||
@@ -48,4 +49,3 @@ type FsHandler struct {
|
||||
func NewFsHandler() *FsHandler {
|
||||
return &FsHandler{Read: NewFsReadHandler(), Write: NewFsWriteHandler()}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,23 +8,38 @@ import (
|
||||
|
||||
// PermissionHandler handles session/request_permission (spec §5.1).
|
||||
//
|
||||
// MVP behaviour (decision §1 #8 + §10 misc #1): fs.* tool calls are answered
|
||||
// directly by the server (so they never reach this handler). Other tool calls
|
||||
// default to "reject_once" — fully automated, no human-in-the-loop.
|
||||
type PermissionHandler struct{}
|
||||
// 行为分两层:
|
||||
// - fs.* 工具调用由服务端直接应答(不经过本 handler)。
|
||||
// - 其他工具调用:若注入了 PermissionDecider(生产路径),交由其裁决——命中
|
||||
// agent kind 白名单则自动放行,否则挂起等待人工审批;未注入 decider 时
|
||||
// (部分单测/向后兼容)回退到「默认拒绝」的纯本地逻辑。
|
||||
type PermissionHandler struct {
|
||||
decider PermissionDecider
|
||||
}
|
||||
|
||||
// NewPermissionHandler constructs a PermissionHandler.
|
||||
func NewPermissionHandler() *PermissionHandler { return &PermissionHandler{} }
|
||||
// PermOption 是 session/request_permission 提供的一个候选选项。
|
||||
type PermOption struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
// PermissionDecider 是权限裁决器契约。实现放在 internal/acp 主包(PermissionService),
|
||||
// 以接口注入避免 handlers 子包反向 import 主包形成循环。返回选中的 option id;
|
||||
// 空串表示拒绝。
|
||||
type PermissionDecider interface {
|
||||
Decide(ctx context.Context, sess SessionContext, agentRequestID string,
|
||||
toolCall json.RawMessage, options []PermOption) (chosenOptionID string)
|
||||
}
|
||||
|
||||
// NewPermissionHandler 构造 PermissionHandler。decider 可为 nil(回退到本地默认拒绝)。
|
||||
func NewPermissionHandler(decider PermissionDecider) *PermissionHandler {
|
||||
return &PermissionHandler{decider: decider}
|
||||
}
|
||||
|
||||
type permissionParams struct {
|
||||
SessionID string `json:"sessionId"`
|
||||
ToolCall json.RawMessage `json:"toolCall"`
|
||||
Options []permOption `json:"options"`
|
||||
}
|
||||
|
||||
type permOption struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Options []PermOption `json:"options"`
|
||||
}
|
||||
|
||||
type permOutcomeSelected struct {
|
||||
@@ -39,25 +54,21 @@ type permResult struct {
|
||||
Outcome permOutcome `json:"outcome"`
|
||||
}
|
||||
|
||||
// Handle picks the first option whose ID looks like a "reject" choice; if none
|
||||
// matches, falls back to the first option. Aligns with spec §10 misc #4
|
||||
// ("default reject").
|
||||
func (h *PermissionHandler) Handle(_ context.Context, _ SessionContext, paramsJSON json.RawMessage) FsResult {
|
||||
// Handle 解析参数后委托 decider 裁决;无 decider 时回退到本地默认拒绝逻辑。
|
||||
// agentRequestID 是代理侧 JSON-RPC request id,用于持久化与排障。
|
||||
func (h *PermissionHandler) Handle(ctx context.Context, sess SessionContext, agentRequestID string, paramsJSON json.RawMessage) FsResult {
|
||||
var p permissionParams
|
||||
if err := json.Unmarshal(paramsJSON, &p); err != nil {
|
||||
return FsResult{Err: &FsError{Code: -32602, Message: "invalid params: " + err.Error()}}
|
||||
}
|
||||
chosen := ""
|
||||
for _, opt := range p.Options {
|
||||
lc := strings.ToLower(opt.ID)
|
||||
if strings.Contains(lc, "reject") || strings.Contains(lc, "deny") || strings.Contains(lc, "no") {
|
||||
chosen = opt.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
if chosen == "" && len(p.Options) > 0 {
|
||||
chosen = p.Options[0].ID
|
||||
|
||||
var chosen string
|
||||
if h.decider != nil {
|
||||
chosen = h.decider.Decide(ctx, sess, agentRequestID, p.ToolCall, p.Options)
|
||||
} else {
|
||||
chosen = defaultRejectChoice(p.Options)
|
||||
}
|
||||
|
||||
if chosen == "" {
|
||||
out, _ := json.Marshal(permResult{Outcome: permOutcome{}})
|
||||
return FsResult{OK: out}
|
||||
@@ -65,3 +76,14 @@ func (h *PermissionHandler) Handle(_ context.Context, _ SessionContext, paramsJS
|
||||
out, _ := json.Marshal(permResult{Outcome: permOutcome{Selected: &permOutcomeSelected{ID: chosen}}})
|
||||
return FsResult{OK: out}
|
||||
}
|
||||
|
||||
// defaultRejectChoice 是无 decider 时的回退:优先选 reject 类选项,否则返回空(拒绝)。
|
||||
func defaultRejectChoice(options []PermOption) string {
|
||||
for _, opt := range options {
|
||||
lc := strings.ToLower(opt.ID)
|
||||
if strings.Contains(lc, "reject") || strings.Contains(lc, "deny") || strings.Contains(lc, "no") {
|
||||
return opt.ID
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -11,9 +11,10 @@ import (
|
||||
"github.com/yan1h/agent-coding-workflow/internal/acp/handlers"
|
||||
)
|
||||
|
||||
// 无 decider(nil)时回退到「默认拒绝」逻辑:优先选 reject 类选项。
|
||||
func TestPermission_AutoReject(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := handlers.NewPermissionHandler()
|
||||
h := handlers.NewPermissionHandler(nil)
|
||||
params := mustJSON(t, map[string]any{
|
||||
"sessionId": "x",
|
||||
"toolCall": map[string]any{"name": "shell.exec"},
|
||||
@@ -22,7 +23,7 @@ func TestPermission_AutoReject(t *testing.T) {
|
||||
map[string]any{"id": "reject_once"},
|
||||
},
|
||||
})
|
||||
res := h.Handle(context.Background(), handlers.SessionContext{}, params)
|
||||
res := h.Handle(context.Background(), handlers.SessionContext{}, "", params)
|
||||
require.Nil(t, res.Err)
|
||||
|
||||
var out struct {
|
||||
@@ -36,9 +37,10 @@ func TestPermission_AutoReject(t *testing.T) {
|
||||
assert.Equal(t, "reject_once", out.Outcome.Selected.ID)
|
||||
}
|
||||
|
||||
func TestPermission_NoRejectOption_FallbackFirst(t *testing.T) {
|
||||
// 无 reject 类选项且无 decider 时,默认拒绝(返回空 outcome,不再误选 allow)。
|
||||
func TestPermission_NoRejectOption_DefaultsToReject(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := handlers.NewPermissionHandler()
|
||||
h := handlers.NewPermissionHandler(nil)
|
||||
params := mustJSON(t, map[string]any{
|
||||
"sessionId": "x",
|
||||
"options": []any{
|
||||
@@ -46,23 +48,16 @@ func TestPermission_NoRejectOption_FallbackFirst(t *testing.T) {
|
||||
map[string]any{"id": "allow_session"},
|
||||
},
|
||||
})
|
||||
res := h.Handle(context.Background(), handlers.SessionContext{}, params)
|
||||
var out struct {
|
||||
Outcome struct {
|
||||
Selected struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"selected"`
|
||||
} `json:"outcome"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(res.OK, &out))
|
||||
assert.Equal(t, "allow_once", out.Outcome.Selected.ID)
|
||||
res := h.Handle(context.Background(), handlers.SessionContext{}, "", params)
|
||||
require.Nil(t, res.Err)
|
||||
assert.JSONEq(t, `{"outcome":{}}`, string(res.OK))
|
||||
}
|
||||
|
||||
func TestPermission_NoOptions_EmptyOutcome(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := handlers.NewPermissionHandler()
|
||||
h := handlers.NewPermissionHandler(nil)
|
||||
params := mustJSON(t, map[string]any{"sessionId": "x"})
|
||||
res := h.Handle(context.Background(), handlers.SessionContext{}, params)
|
||||
res := h.Handle(context.Background(), handlers.SessionContext{}, "", params)
|
||||
require.Nil(t, res.Err)
|
||||
assert.JSONEq(t, `{"outcome":{}}`, string(res.OK))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user