Skip to content

Commit 02f6b0a

Browse files
committed
acp: treat disabled mode negotiation as read-only permissions
1 parent d8c048f commit 02f6b0a

File tree

2 files changed

+56
-2
lines changed

2 files changed

+56
-2
lines changed

internal/agent/acp.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -869,10 +869,13 @@ func (c *acpClient) RequestPermission(ctx context.Context, params acp.RequestPer
869869
}, nil
870870
}
871871

872-
// Apply permission logic based on agent mode
872+
// Apply permission logic based on effective permission mode.
873+
// When session mode negotiation is disabled (Mode == ""), keep
874+
// permission behavior in read-only mode by default.
875+
effectiveMode := c.agent.effectivePermissionMode()
873876

874877
// In read-only mode, deny all destructive operations.
875-
if c.agent.Mode == c.agent.ReadOnlyMode {
878+
if effectiveMode == c.agent.ReadOnlyMode {
876879
if isDestructive {
877880
return acp.RequestPermissionResponse{
878881
Outcome: selectPermissionOutcome(params.Options, false),
@@ -1410,6 +1413,13 @@ func (a *ACPAgent) mutatingOperationsAllowed() bool {
14101413
return a.AutoApproveMode != "" && a.Mode == a.AutoApproveMode
14111414
}
14121415

1416+
func (a *ACPAgent) effectivePermissionMode() string {
1417+
if strings.TrimSpace(a.Mode) != "" {
1418+
return a.Mode
1419+
}
1420+
return a.ReadOnlyMode
1421+
}
1422+
14131423
func defaultACPAgentConfig() *config.ACPAgentConfig {
14141424
return &config.ACPAgentConfig{
14151425
Name: defaultACPName,

internal/agent/acp_auth_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,50 @@ func TestACPAuthPermissionModes(t *testing.T) {
124124
}
125125
})
126126

127+
t.Run("RequestPermission: Empty mode defaults to read-only behavior for permissions", func(t *testing.T) {
128+
agent := &ACPAgent{
129+
agentName: "test-acp",
130+
Command: "test-command",
131+
ReadOnlyMode: "plan",
132+
AutoApproveMode: "auto-approve",
133+
Mode: "",
134+
Model: "test-model",
135+
Timeout: 30 * time.Second,
136+
}
137+
138+
client := &acpClient{
139+
agent: agent,
140+
}
141+
142+
readKind := acp.ToolKind("read")
143+
readResponse, err := client.RequestPermission(context.Background(), acp.RequestPermissionRequest{
144+
Options: authTestPermissionOptions(),
145+
ToolCall: acp.RequestPermissionToolCall{
146+
Kind: &readKind,
147+
},
148+
})
149+
if err != nil {
150+
t.Fatalf("RequestPermission failed for read kind: %v", err)
151+
}
152+
if readResponse.Outcome.Selected == nil || readResponse.Outcome.Selected.OptionId != authAllowAlwaysOptionID {
153+
t.Errorf("Expected %q for non-destructive operation when mode is disabled, got '%v'", authAllowAlwaysOptionID, readResponse.Outcome)
154+
}
155+
156+
editKind := acp.ToolKind("edit")
157+
editResponse, err := client.RequestPermission(context.Background(), acp.RequestPermissionRequest{
158+
Options: authTestPermissionOptions(),
159+
ToolCall: acp.RequestPermissionToolCall{
160+
Kind: &editKind,
161+
},
162+
})
163+
if err != nil {
164+
t.Fatalf("RequestPermission failed for edit kind: %v", err)
165+
}
166+
if editResponse.Outcome.Selected == nil || editResponse.Outcome.Selected.OptionId != authRejectAlwaysOptionID {
167+
t.Errorf("Expected %q for destructive operation when mode is disabled, got '%v'", authRejectAlwaysOptionID, editResponse.Outcome)
168+
}
169+
})
170+
127171
t.Run("RequestPermission: AutoApproveMode allows all known operations", func(t *testing.T) {
128172
// Create agent in auto-approve mode (agentic)
129173
agent := &ACPAgent{

0 commit comments

Comments
 (0)