mirror of
https://github.com/ollama/ollama.git
synced 2026-10-20 18:41:17 +00:00
proxy: normalize namespaced commands in Full Access (#18331)
This commit is contained in:
parent
86f7292934
commit
3738935d0c
2 changed files with 120 additions and 17 deletions
|
|
@ -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"], ¶meters); 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 {
|
||||
|
|
|
|||
|
|
@ -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}}}]}`)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue