proxy: normalize namespaced commands in Full Access (#18331)

This commit is contained in:
Parth Sareen 2026-09-09 10:20:48 -07:00 • committed by GitHub
parent 86f7292934
commit 3738935d0c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 120 additions and 17 deletions

View file

@ -63,16 +63,53 @@ func normalizeFullAccessExecTool(body []byte) ([]byte, error) {
if !ok {
return body, nil
}
encodedTools, changed, err := normalizeFullAccessTools(rawTools)
if err != nil {
return nil, err
}
if !changed {
return body, nil
}
payload["tools"] = encodedTools
encoded, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("encode request: %w", err)
}
return encoded, nil
}
// normalizeFullAccessTools preserves namespace declarations while rewriting
// their member functions using the same contract as top-level functions.
func normalizeFullAccessTools(rawTools json.RawMessage) (json.RawMessage, bool, error) {
if len(rawTools) == 0 {
return rawTools, false, nil
}
var tools []json.RawMessage
if err := json.Unmarshal(rawTools, &tools); err != nil {
return nil, fmt.Errorf("decode tools: %w", err)
return nil, false, fmt.Errorf("decode tools: %w", err)
}
changed := false
for i, rawTool := range tools {
var tool map[string]json.RawMessage
if err := json.Unmarshal(rawTool, &tool); err != nil {
return nil, fmt.Errorf("decode tool: %w", err)
return nil, false, fmt.Errorf("decode tool: %w", err)
}
var toolType string
if err := json.Unmarshal(tool["type"], &toolType); err == nil && toolType == "namespace" {
members, membersChanged, err := normalizeFullAccessTools(tool["tools"])
if err != nil {
return nil, false, fmt.Errorf("normalize namespace tools: %w", err)
}
if membersChanged {
tool["tools"] = members
tools[i], err = json.Marshal(tool)
if err != nil {
return nil, false, fmt.Errorf("encode namespace tool: %w", err)
}
changed = true
}
continue
}
var name string
if err := json.Unmarshal(tool["name"], &name); err != nil || name != "exec_command" {
@ -81,11 +118,11 @@ func normalizeFullAccessExecTool(body []byte) ([]byte, error) {
var parameters map[string]json.RawMessage
if err := json.Unmarshal(tool["parameters"], &parameters); err != nil {
return nil, fmt.Errorf("decode exec_command parameters: %w", err)
return nil, false, fmt.Errorf("decode exec_command parameters: %w", err)
}
var properties map[string]json.RawMessage
if err := json.Unmarshal(parameters["properties"], &properties); err != nil {
return nil, fmt.Errorf("decode exec_command properties: %w", err)
return nil, false, fmt.Errorf("decode exec_command properties: %w", err)
}
toolChanged := false
for _, property := range []string{"sandbox_permissions", "justification", "prefix_rule"} {
@ -101,45 +138,40 @@ func normalizeFullAccessExecTool(body []byte) ([]byte, error) {
encodedProperties, err := json.Marshal(properties)
if err != nil {
return nil, fmt.Errorf("encode exec_command properties: %w", err)
return nil, false, fmt.Errorf("encode exec_command properties: %w", err)
}
parameters["properties"] = encodedProperties
if rawRequired, ok := parameters["required"]; ok {
var required []string
if err := json.Unmarshal(rawRequired, &required); err != nil {
return nil, fmt.Errorf("decode exec_command required properties: %w", err)
return nil, false, fmt.Errorf("decode exec_command required properties: %w", err)
}
required = slices.DeleteFunc(required, func(property string) bool {
return property == "sandbox_permissions" || property == "justification" || property == "prefix_rule"
})
parameters["required"], err = json.Marshal(required)
if err != nil {
return nil, fmt.Errorf("encode exec_command required properties: %w", err)
return nil, false, fmt.Errorf("encode exec_command required properties: %w", err)
}
}
tool["parameters"], err = json.Marshal(parameters)
if err != nil {
return nil, fmt.Errorf("encode exec_command parameters: %w", err)
return nil, false, fmt.Errorf("encode exec_command parameters: %w", err)
}
tools[i], err = json.Marshal(tool)
if err != nil {
return nil, fmt.Errorf("encode exec_command tool: %w", err)
return nil, false, fmt.Errorf("encode exec_command tool: %w", err)
}
}
if !changed {
return body, nil
return rawTools, false, nil
}
encodedTools, err := json.Marshal(tools)
if err != nil {
return nil, fmt.Errorf("encode tools: %w", err)
return nil, false, fmt.Errorf("encode tools: %w", err)
}
payload["tools"] = encodedTools
encoded, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("encode request: %w", err)
}
return encoded, nil
return encodedTools, true, nil
}
func codexSandboxMode(body []byte) string {

View file

@ -11,6 +11,7 @@ import (
"net/http/httptest"
"os"
"path/filepath"
"reflect"
"slices"
"strings"
"testing"
@ -379,6 +380,76 @@ func TestNormalizeFullAccessExecToolLeavesSandboxedTurnUnchanged(t *testing.T) {
}
}
func TestNormalizeFullAccessNamespacedExecTool(t *testing.T) {
const tools = `[{"type":"namespace","name":"functions","description":"Command tools","tools":[
{"type":"function","name":"exec_command","strict":false,"parameters":{"type":"object","properties":{"cmd":{"type":"string"},"sandbox_permissions":{"type":"string","enum":["use_default","require_escalated"]},"justification":{"type":"string"},"prefix_rule":{"type":"array","items":{"type":"string"}}},"required":["cmd","sandbox_permissions","justification","prefix_rule"],"additionalProperties":false}},
{"type":"function","name":"other_tool","parameters":{"type":"object","properties":{"sandbox_permissions":{"type":"string"}}}}
]}]`
for _, mode := range []string{"danger-full-access", "workspace-write", "read-only", ""} {
t.Run(mode, func(t *testing.T) {
metadata, err := json.Marshal(map[string]string{"sandbox_mode": mode})
if err != nil {
t.Fatal(err)
}
body := []byte(fmt.Sprintf(`{"model":"glm-5.3-flash:cloud","client_metadata":{"x-codex-turn-metadata":%q},"tools":%s}`, metadata, tools))
normalized, err := normalizeFullAccessExecTool(body)
if err != nil {
t.Fatal(err)
}
if mode != "danger-full-access" {
if !bytes.Equal(normalized, body) {
t.Fatalf("sandboxed request changed: %s", normalized)
}
return
}
var want, got map[string]any
if err := json.Unmarshal(body, &want); err != nil {
t.Fatal(err)
}
if err := json.Unmarshal(normalized, &got); err != nil {
t.Fatal(err)
}
namespace := want["tools"].([]any)[0].(map[string]any)
exec := namespace["tools"].([]any)[0].(map[string]any)
parameters := exec["parameters"].(map[string]any)
properties := parameters["properties"].(map[string]any)
for _, key := range []string{"sandbox_permissions", "justification", "prefix_rule"} {
delete(properties, key)
}
parameters["required"] = []any{"cmd"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("namespace tool normalization did not preserve the expected request: %s", normalized)
}
})
}
}
func TestNormalizeFullAccessNamespaceBoundaries(t *testing.T) {
for _, tt := range []struct {
name string
tools string
wantErr bool
}{
{"missing members", `[{"type":"namespace","name":"functions"}]`, false},
{"null members", `[{"type":"namespace","name":"functions","tools":null}]`, false},
{"empty members", `[{"type":"namespace","name":"functions","tools":[]}]`, false},
{"no escalation arguments", `[{"type":"namespace","name":"functions","tools":[{"type":"function","name":"exec_command","parameters":{"properties":{"cmd":{"type":"string"}},"required":["cmd"]}}]}]`, false},
{"malformed members", `[{"type":"namespace","name":"functions","tools":{}}]`, true},
{"malformed parameters", `[{"type":"namespace","name":"functions","tools":[{"type":"function","name":"exec_command","parameters":42}]}]`, true},
} {
t.Run(tt.name, func(t *testing.T) {
body := []byte(`{"client_metadata":{"x-codex-turn-metadata":{"sandbox_mode":"danger-full-access"}},"tools":` + tt.tools + `}`)
normalized, err := normalizeFullAccessExecTool(body)
if (err != nil) != tt.wantErr {
t.Fatalf("normalization error = %v, want error = %v", err, tt.wantErr)
}
if !tt.wantErr && !bytes.Equal(normalized, body) {
t.Fatalf("unchanged namespace was rewritten: %s", normalized)
}
})
}
}
func TestLoadCatalogModelsReadsOptionalThinkingMetadata(t *testing.T) {
path := filepath.Join(t.TempDir(), CodexDesktopModelCatalogFilename)
data := []byte(`{"models":[{"slug":"legacy"},{"slug":"binary","thinking":{"supported":true,"levels":["none","medium"],"values":{"none":false,"medium":true}}}]}`)