You've already forked agentic-coding-workflow
feat(acp/handlers): session/request_permission auto-reject
This commit is contained in:
@@ -49,6 +49,3 @@ func NewFsHandler() *FsHandler {
|
|||||||
return &FsHandler{Read: NewFsReadHandler(), Write: NewFsWriteHandler()}
|
return &FsHandler{Read: NewFsReadHandler(), Write: NewFsWriteHandler()}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PermissionHandler is the placeholder for session/request_permission.
|
|
||||||
// Fields and methods are added in Task D5.
|
|
||||||
type PermissionHandler struct{}
|
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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{}
|
||||||
|
|
||||||
|
// NewPermissionHandler constructs a PermissionHandler.
|
||||||
|
func NewPermissionHandler() *PermissionHandler { return &PermissionHandler{} }
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type permOutcomeSelected struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type permOutcome struct {
|
||||||
|
Selected *permOutcomeSelected `json:"selected,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
if chosen == "" {
|
||||||
|
out, _ := json.Marshal(permResult{Outcome: permOutcome{}})
|
||||||
|
return FsResult{OK: out}
|
||||||
|
}
|
||||||
|
out, _ := json.Marshal(permResult{Outcome: permOutcome{Selected: &permOutcomeSelected{ID: chosen}}})
|
||||||
|
return FsResult{OK: out}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package handlers_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/yan1h/agent-coding-workflow/internal/acp/handlers"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPermission_AutoReject(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
h := handlers.NewPermissionHandler()
|
||||||
|
params := mustJSON(t, map[string]any{
|
||||||
|
"sessionId": "x",
|
||||||
|
"toolCall": map[string]any{"name": "shell.exec"},
|
||||||
|
"options": []any{
|
||||||
|
map[string]any{"id": "allow_once"},
|
||||||
|
map[string]any{"id": "reject_once"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
res := h.Handle(context.Background(), handlers.SessionContext{}, params)
|
||||||
|
require.Nil(t, res.Err)
|
||||||
|
|
||||||
|
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, "reject_once", out.Outcome.Selected.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPermission_NoRejectOption_FallbackFirst(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
h := handlers.NewPermissionHandler()
|
||||||
|
params := mustJSON(t, map[string]any{
|
||||||
|
"sessionId": "x",
|
||||||
|
"options": []any{
|
||||||
|
map[string]any{"id": "allow_once"},
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPermission_NoOptions_EmptyOutcome(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
h := handlers.NewPermissionHandler()
|
||||||
|
params := mustJSON(t, map[string]any{"sessionId": "x"})
|
||||||
|
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