Compare commits

...

1 Commits

Author SHA1 Message Date
J
d7e39b3466 refactor(codex): make permission approval auto-grant observable
The daemon auto-grants Codex item/permissions/requestApproval requests by
echoing back the network / fileSystem profile scoped to the current turn.
Previously a malformed params payload and any permission key outside
network / fileSystem were dropped silently, so a future app-server
protocol that adds a new permission shape would be narrowed away with no
trace in daemon logs.

Log both cases (parse failure and dropped keys) without changing the
granted response. Addresses review nits on #4346 / MUL-3451.

Co-authored-by: multica-agent <github@multica.ai>
2026-06-22 13:17:46 +08:00
2 changed files with 78 additions and 9 deletions

View File

@@ -1467,7 +1467,7 @@ func (c *codexClient) handleServerRequest(raw map[string]json.RawMessage) {
case "item/fileChange/requestApproval", "applyPatchApproval":
c.respond(id, map[string]any{"decision": "accept"})
case "item/permissions/requestApproval":
c.respond(id, codexPermissionsApprovalResponse(raw["params"]))
c.respond(id, codexPermissionsApprovalResponse(raw["params"], c.cfg.Logger))
case "mcpServer/elicitation/request":
c.respond(id, map[string]any{"action": "accept", "content": nil, "_meta": nil})
default:
@@ -1478,21 +1478,40 @@ func (c *codexClient) handleServerRequest(raw map[string]json.RawMessage) {
}
}
func codexPermissionsApprovalResponse(params json.RawMessage) map[string]any {
// codexPermissionsApprovalResponse builds the auto-grant reply for a Codex
// item/permissions/requestApproval server request. In daemon mode there is no
// human to approve, so we echo back the requested network / fileSystem profile
// and scope it to the current turn, mirroring the other auto-accept branches in
// handleServerRequest.
//
// The grant is intentionally limited to the network / fileSystem keys we
// understand. A parse failure and any dropped key are logged so that a future
// app-server protocol that adds a new permission shape is visible in daemon
// logs instead of being silently narrowed away.
func codexPermissionsApprovalResponse(params json.RawMessage, logger *slog.Logger) map[string]any {
var payload struct {
Permissions map[string]any `json:"permissions"`
}
_ = json.Unmarshal(params, &payload)
if err := json.Unmarshal(params, &payload); err != nil && logger != nil {
logger.Warn("codex: failed to parse permission approval request; granting empty turn-scoped profile", "error", err)
}
granted := map[string]any{}
if payload.Permissions != nil {
if network, ok := payload.Permissions["network"]; ok && network != nil {
granted["network"] = network
}
if fileSystem, ok := payload.Permissions["fileSystem"]; ok && fileSystem != nil {
granted["fileSystem"] = fileSystem
var dropped []string
for key, value := range payload.Permissions {
switch key {
case "network", "fileSystem":
if value != nil {
granted[key] = value
}
default:
dropped = append(dropped, key)
}
}
if len(dropped) > 0 && logger != nil {
sort.Strings(dropped)
logger.Warn("codex: dropping unrecognized permission keys from approval request; add explicit handling if the app-server protocol expanded", "keys", dropped)
}
return map[string]any{
"permissions": granted,

View File

@@ -1,6 +1,7 @@
package agent
import (
"bytes"
"context"
"encoding/json"
"errors"
@@ -249,6 +250,55 @@ func TestCodexHandleServerRequestPermissionsApproval(t *testing.T) {
}
}
func TestCodexPermissionsApprovalResponseDropsUnknownKeysAndLogs(t *testing.T) {
t.Parallel()
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, nil))
resp := codexPermissionsApprovalResponse(
json.RawMessage(`{"permissions":{"network":{"enabled":true},"gpu":{"enabled":true}}}`),
logger,
)
if resp["scope"] != "turn" {
t.Fatalf("expected scope=turn, got %v", resp["scope"])
}
perms, ok := resp["permissions"].(map[string]any)
if !ok {
t.Fatalf("expected permissions object, got %v", resp["permissions"])
}
if _, ok := perms["network"]; !ok {
t.Fatalf("expected network permission to be granted, got %v", perms)
}
if _, ok := perms["gpu"]; ok {
t.Fatalf("expected unrecognized key gpu to be dropped, got %v", perms)
}
if !strings.Contains(buf.String(), "gpu") {
t.Fatalf("expected dropped key to be logged, got %q", buf.String())
}
}
func TestCodexPermissionsApprovalResponseMalformedParamsLogs(t *testing.T) {
t.Parallel()
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, nil))
resp := codexPermissionsApprovalResponse(json.RawMessage(`{"permissions":"not-an-object"}`), logger)
if resp["scope"] != "turn" {
t.Fatalf("expected scope=turn, got %v", resp["scope"])
}
perms, ok := resp["permissions"].(map[string]any)
if !ok || len(perms) != 0 {
t.Fatalf("expected empty permissions on malformed params, got %v", resp["permissions"])
}
if !strings.Contains(buf.String(), "failed to parse") {
t.Fatalf("expected parse failure to be logged, got %q", buf.String())
}
}
func TestCodexHandleServerRequestUnknownReturnsError(t *testing.T) {
t.Parallel()