Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions apps/daemon/internal/agent/claudesdk/provider.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package claudesdk

import (
"sort"
"strings"

harnessconfiguration "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/claudesdk"
Expand All @@ -12,7 +13,17 @@ func providerEnvironment(value any) ([]string, error) {
if err != nil {
return nil, err
}
return []string{"ANTHROPIC_BASE_URL=" + provider.BaseURL, "ANTHROPIC_AUTH_TOKEN=" + provider.APIKey}, nil
names := make([]string, 0, len(provider.Headers))
for name := range provider.Headers {
names = append(names, name)
}
sort.Strings(names)
headers := make([]string, 0, len(names))
for _, name := range names {
headers = append(headers, name+": "+provider.Headers[name])
}
// An explicit empty value clears any previous native header selection.
return []string{"ANTHROPIC_BASE_URL=" + provider.BaseURL, "ANTHROPIC_AUTH_TOKEN=" + provider.APIKey, "ANTHROPIC_CUSTOM_HEADERS=" + strings.Join(headers, "\n")}, nil
}

func withProvider(env, provider []string) []string {
Expand All @@ -22,7 +33,7 @@ func withProvider(env, provider []string) []string {
out := make([]string, 0, len(env)+len(provider))
for _, entry := range env {
key, _, _ := strings.Cut(entry, "=")
if key != "ANTHROPIC_API_KEY" && key != "ANTHROPIC_AUTH_TOKEN" && key != "ANTHROPIC_BASE_URL" {
if key != "ANTHROPIC_API_KEY" && key != "ANTHROPIC_AUTH_TOKEN" && key != "ANTHROPIC_BASE_URL" && key != "ANTHROPIC_CUSTOM_HEADERS" {
out = append(out, entry)
}
}
Expand Down
63 changes: 63 additions & 0 deletions apps/daemon/internal/agent/claudesdk/provider_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package claudesdk

import (
"slices"
"strings"
"testing"
)

func TestProviderHeadersEveryPreparation(t *testing.T) {
raw := map[string]any{"protocol": "anthropic", "base_url": "https://example.test", "api_key": "fixture", "headers": map[string]string{"X-B": "two:colon", "X-A": "one", "X-Empty": ""}}
for range 3 {
provider, err := providerEnvironment(raw)
if err != nil {
t.Fatal(err)
}
env := withProvider([]string{"ANTHROPIC_CUSTOM_HEADERS=X-Stale: wrong", "OTHER=keep"}, provider)
if !slices.Contains(env, "ANTHROPIC_CUSTOM_HEADERS=X-A: one\nX-B: two:colon\nX-Empty: ") {
t.Fatal("headers missing or changed")
}
for _, entry := range env {
if strings.Contains(entry, "X-Stale") {
t.Fatal("stale headers retained")
}
}
}
delete(raw, "headers")
provider, err := providerEnvironment(raw)
if err != nil || !slices.Contains(provider, "ANTHROPIC_CUSTOM_HEADERS=") {
t.Fatal("ambient headers were not cleared")
}
}
func TestWorkspaceProviderHeadersReachNativeOptions(t *testing.T) {
config := workspaceFixture(t)
t.Setenv("ANTHROPIC_CUSTOM_HEADERS", "X-Stale: ambient")
config.Env = append(config.Env, "ANTHROPIC_CUSTOM_HEADERS=X-Stale: config")
req := workspaceRequest()
req.AgentOptions["model_provider"] = map[string]any{"protocol": "anthropic", "base_url": "https://example.test", "api_key": "fixture", "headers": map[string]string{"X-Activity": "daily-free"}}
start, env, err := prepare(config, req)
if err != nil {
t.Fatal(err)
}
if !slices.Contains(start.Workspace.EnvNames, "ANTHROPIC_CUSTOM_HEADERS") || !slices.Contains(env, "ANTHROPIC_CUSTOM_HEADERS=X-Activity: daily-free") {
t.Fatal("headers missing from native workspace projection")
}
for _, entry := range env {
if strings.Contains(entry, "X-Stale") {
t.Fatal("ambient header selection survived")
}
}
delete(req.AgentOptions["model_provider"].(map[string]any), "headers")
_, env, err = prepare(config, req)
if err != nil || !slices.Contains(env, "ANTHROPIC_CUSTOM_HEADERS=") {
t.Fatal("headerless provider did not clear ambient headers", err)
}
}
func TestProviderHeadersRejectAuthenticationAndMalformedValues(t *testing.T) {
for _, headers := range []any{map[string]string{"Authorization": "override"}, map[string]string{"Anthropic-Version": "override"}, map[string]string{"X-Activity": "bad\nvalue"}, map[string]any{"X-Activity": nil}} {
_, err := providerEnvironment(map[string]any{"protocol": "anthropic", "base_url": "https://example.test", "api_key": "fixture", "headers": headers})
if err == nil {
t.Fatal("invalid header accepted")
}
}
}
4 changes: 2 additions & 2 deletions apps/daemon/internal/agent/claudesdk/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ func workspaceEnvironment(config Config) (*workspaceProfile, []string, error) {
env := []string{}
for _, entry := range os.Environ() {
name, _, _ := strings.Cut(entry, "=")
if name != "ANTHROPIC_API_KEY" && name != "ANTHROPIC_AUTH_TOKEN" && name != "CLAUDE_CODE_OAUTH_TOKEN" && name != "CLAUDE_CONFIG_DIR" && name != "HOME" && name != "TMPDIR" {
if name != "ANTHROPIC_API_KEY" && name != "ANTHROPIC_AUTH_TOKEN" && name != "ANTHROPIC_CUSTOM_HEADERS" && name != "CLAUDE_CODE_OAUTH_TOKEN" && name != "CLAUDE_CONFIG_DIR" && name != "HOME" && name != "TMPDIR" {
env = append(env, entry)
}
}
Expand All @@ -130,7 +130,7 @@ func workspaceEnvironment(config Config) (*workspaceProfile, []string, error) {

func workspaceEnvName(name string) bool {
switch name {
case "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_BASE_URL",
case "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_BASE_URL", "ANTHROPIC_CUSTOM_HEADERS",
"ANTHROPIC_DEFAULT_SONNET_MODEL", "ANTHROPIC_DEFAULT_OPUS_MODEL", "ANTHROPIC_DEFAULT_HAIKU_MODEL",
"CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS", "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY":
return true
Expand Down
111 changes: 111 additions & 0 deletions apps/daemon/internal/agent/codex/model_headers_native_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package codex

import (
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"strings"
"sync/atomic"
"testing"
"time"

"github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto"
)

// The pinned native binary talks to a synthetic Responses HTTP endpoint.
func TestNativeModelHeadersEveryTurn(t *testing.T) {
binary := os.Getenv("OAC_TEST_CODEX_BINARY")
if binary == "" {
t.Skip("requires pinned Codex binary")
}
if version, err := exec.Command(binary, "--version").Output(); err != nil || strings.TrimSpace(string(version)) != "codex-cli 0.153.4" {
t.Fatal("requires Codex 0.153.4")
}
var attempts atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-Sandbase-Benefit") != "daily-free" || r.Header.Get("X-Literal") != "${unchanged}:value" {
t.Error("model request lost headers")
}
n := attempts.Add(1)
if n == 1 {
w.WriteHeader(500)
_, _ = w.Write([]byte(`{"error":{"message":"synthetic retry","type":"server_error"}}`))
return
}
w.Header().Set("Content-Type", "text/event-stream")
item := map[string]any{"id": fmt.Sprintf("msg_%d", n), "type": "message", "role": "assistant", "status": "completed", "content": []any{map[string]any{"type": "output_text", "text": "HEADER-OK", "annotations": []any{}}}}
if n == 2 {
item = map[string]any{"id": "fc_fixture", "type": "function_call", "call_id": "call_fixture", "name": "lookup", "arguments": "{}", "status": "completed"}
}
response := map[string]any{"id": fmt.Sprintf("resp_%d", n), "object": "response", "status": "completed", "output": []any{item}, "usage": map[string]any{"input_tokens": 10, "output_tokens": 2, "total_tokens": 12, "input_tokens_details": map[string]int{"cached_tokens": 0}}}
for _, event := range []map[string]any{{"type": "response.created", "response": map[string]any{"id": response["id"], "status": "in_progress", "output": []any{}}}, {"type": "response.output_item.added", "output_index": 0, "item": item}, {"type": "response.output_text.delta", "item_id": item["id"], "output_index": 0, "content_index": 0, "delta": "HEADER-OK"}, {"type": "response.output_item.done", "output_index": 0, "item": item}, {"type": "response.completed", "response": response}} {
body, _ := json.Marshal(event)
_, _ = fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event["type"], body)
}
}))
defer server.Close()
t.Setenv("OAC_RUNTIME_HOME", t.TempDir())
for _, name := range []string{"CODEX_EXEC_SERVER_URL", "CODEX_EXEC_SERVER_NOISE_REGISTRY_URL", "CODEX_EXEC_SERVER_NOISE_ENVIRONMENT_ID", "CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN"} {
t.Setenv(name, "")
}
cfg := defaultSessionConfig(testStateRoot(t))
cfg.codexBinary = binary
cfg.logger = slog.New(slog.DiscardHandler)
req := proto.PromptRequestPayload{AgentKind: "codex", AgentStateKey: "header-native", DisableExecutionEnvironment: true, DisableSubagents: true, FunctionTools: []proto.FunctionTool{{Name: "lookup", Description: "Fixture", Parameters: json.RawMessage(`{"type":"object","properties":{}}`)}}, AgentOptions: map[string]any{"model": "gpt-5.5", "model_provider": map[string]any{"protocol": "responses", "base_url": server.URL + "/v1", "api_key": "fixture", "headers": map[string]string{"X-Sandbase-Benefit": "daily-free", "X-Literal": "${unchanged}:value"}}}}
ctx, cancel := context.WithTimeout(t.Context(), 60*time.Second)
defer cancel()
e, err := newExecutor(ctx, req, cfg)
if err != nil {
t.Fatal(err)
}
defer func() { _ = e.Close(context.Background()) }()
for i := 0; i < 3; i++ {
output := make(chan proto.Envelope, 256)
turn, err := e.StartTurn(ctx, fmt.Sprintf("turn-%d", i), proto.TextInput("Reply HEADER-OK."), output)
if err != nil {
t.Fatal(err)
}
var done proto.DonePayload
for event := range output {
if event.Type == proto.TypeFunctionCall {
var call proto.FunctionCallPayload
_ = event.DecodePayload(&call)
resultText := "fixture result"
if err := turn.(*Session).SubmitFunctionResult(ctx, proto.FunctionResultPayload{CallID: call.CallID, Success: true, Content: []proto.InputContent{{Type: "input_text", Text: &resultText}}}); err != nil {
t.Fatal(err)
}
}
if event.Type == proto.TypeError {
t.Error("native turn failed")
}
if event.Type == proto.TypeDone {
_ = event.DecodePayload(&done)
}
}
if _, err = turn.AwaitSettlement(ctx); err != nil {
t.Fatal(err)
}
if done.Content != "HEADER-OK" {
t.Fatalf("unexpected output %q", done.Content)
}
if i == 1 {
req.AgentSessionID = turn.(*Session).currentThreadID()
req.RequireExistingNativeSession = true
if err = e.Close(ctx); err != nil {
t.Fatal(err)
}
e, err = newExecutor(ctx, req, cfg)
if err != nil {
t.Fatal(err)
}
}
}
if attempts.Load() < 5 {
t.Fatal("retry and three turns not observed")
}
}
16 changes: 14 additions & 2 deletions apps/daemon/internal/agent/codex/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os"
"path/filepath"
"slices"
"sort"
"strings"

"github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto"
Expand Down Expand Up @@ -123,7 +124,18 @@ func BuildSessionPlan(stateRoot, agentStateKey string, opts map[string]any, cont
// shell_environment_policy is applied. Do not persist provider credentials.
plan.DisableFeatures = append(plan.DisableFeatures, "shell_snapshot")
env = append(env, providerAPIKeyEnv+"="+provider.BearerToken)
plan.ExtraConfig = append(plan.ExtraConfig, [2]string{"shell_environment_policy.exclude", "[" + strconv(providerAPIKeyEnv) + "]"})
excluded := []string{strconv(providerAPIKeyEnv)}
names := make([]string, 0, len(provider.HTTPHeaders))
for name := range provider.HTTPHeaders {
names = append(names, name)
}
sort.Strings(names)
for i, name := range names {
reference := providerHeaderEnv(i)
env = append(env, reference+"="+provider.HTTPHeaders[name])
excluded = append(excluded, strconv(reference))
}
plan.ExtraConfig = append(plan.ExtraConfig, [2]string{"shell_environment_policy.exclude", "[" + strings.Join(excluded, ",") + "]"})
}

plan.Env = env
Expand Down Expand Up @@ -203,7 +215,7 @@ func normaliseProviderConfig(raw any) (providerConfig, bool, error) {
if err != nil {
return providerConfig{}, false, err
}
return providerConfig{BaseURL: provider.BaseURL, BearerToken: provider.APIKey, WireAPI: "responses"}, true, nil
return providerConfig{BaseURL: provider.BaseURL, BearerToken: provider.APIKey, WireAPI: "responses", HTTPHeaders: provider.Headers}, true, nil
}

func stringOpt(opts map[string]any, key string) string {
Expand Down
11 changes: 6 additions & 5 deletions apps/daemon/internal/agent/codex/provider_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ type providerConfig struct {
BaseURL string
// BearerToken is supplied only through the native env_key mechanism.
BearerToken string
// HTTPHeaders is forwarded as `[model_providers.oac.http_headers]`.
// Keys / values rendered as TOML basic strings.
// HTTPHeaders are supplied through native environment references.
HTTPHeaders map[string]string
// QueryParams is forwarded as `[model_providers.oac.query_params]`,
// used by Azure (api-version=2025-04-01-preview).
Expand Down Expand Up @@ -109,11 +108,11 @@ func writeCodexProviderConfig(codexHome string, cfg providerConfig) error {
sort.Strings(keys)
b.WriteString("\n[model_providers.")
b.WriteString(oacProviderSlug)
b.WriteString(".http_headers]\n")
for _, k := range keys {
b.WriteString(".env_http_headers]\n")
for i, k := range keys {
b.WriteString(tomlQuoteString(k))
b.WriteString(" = ")
b.WriteString(tomlQuoteString(cfg.HTTPHeaders[k]))
b.WriteString(tomlQuoteString(providerHeaderEnv(i)))
b.WriteByte('\n')
}
}
Expand Down Expand Up @@ -160,3 +159,5 @@ func appendConfigTOML(path string, body string) error {
}
return nil
}

func providerHeaderEnv(index int) string { return fmt.Sprintf("OAC_CODEX_MODEL_HEADER_%d", index) }
34 changes: 31 additions & 3 deletions apps/daemon/internal/agent/codex/provider_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,9 @@ func TestWriteCodexProviderConfig_FullProvider(t *testing.T) {
`name = "mygw"`,
`base_url = "https://platform-api.example.com/v1"`,
`env_key = "OAC_CODEX_PROVIDER_API_KEY"`,
`[model_providers.oac.http_headers]`,
`"X-Sub-Module" = "codex-internal"`,
`"X-Request-ID" = "abc"`,
`[model_providers.oac.env_http_headers]`,
`"X-Sub-Module" = "OAC_CODEX_MODEL_HEADER_1"`,
`"X-Request-ID" = "OAC_CODEX_MODEL_HEADER_0"`,
`[model_providers.oac.query_params]`,
`"api-version" = "2025-04-01-preview"`,
} {
Expand Down Expand Up @@ -288,3 +288,31 @@ func TestExplicitStateRootSurvivesRuntimeReplacement(t *testing.T) {
t.Fatal("unavailable explicit root fell back")
}
}

func TestProviderHeadersRebuiltForEveryTurn(t *testing.T) {
root := t.TempDir()
opts := map[string]any{"model": "fixture", "model_provider": map[string]any{"protocol": "responses", "base_url": "https://example.test/v1", "api_key": "fixture", "headers": map[string]string{"X-Activity": "private-header-canary"}}}
for range 3 {
plan, err := BuildSessionPlan(root, "persistent", opts, nil)
if err != nil {
t.Fatal(err)
}
body := mustReadFile(t, filepath.Join(nativeHomeFromPlan(plan), "config.toml"))
if strings.Contains(body, "private-header-canary") || !strings.Contains(body, "env_http_headers") {
t.Fatal("unsafe header projection")
}
if !slices.Contains(plan.Env, "OAC_CODEX_MODEL_HEADER_0=private-header-canary") {
t.Fatal("missing header environment")
}
found := false
for _, option := range plan.ExtraConfig {
if option[0] == "shell_environment_policy.exclude" && strings.Contains(option[1], "OAC_CODEX_MODEL_HEADER_0") {
found = true
}
}
if !found {
t.Fatal("header exposed to tools")
}
plan.Cleanup()
}
}
7 changes: 6 additions & 1 deletion apps/daemon/internal/agent/mcode/model_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package mcode

import (
"fmt"
"maps"

harnessconfiguration "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/mcode"
"github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider"
Expand All @@ -21,9 +22,13 @@ func modelProviderConfig(raw any, model string) (map[string]any, error) {
case modelprovider.Responses:
api = "openai-responses"
}
options := map[string]any{"baseURL": provider.BaseURL, "apiKey": provider.APIKey}
if len(provider.Headers) > 0 {
options["headers"] = maps.Clone(provider.Headers)
}
return map[string]any{
"name": "Configured provider", "kind": "custom", "enabled": true, "api": api,
"options": map[string]any{"baseURL": provider.BaseURL, "apiKey": provider.APIKey},
"options": options,
"models": map[string]any{model: map[string]any{
"name": model, "tool_call": true,
"limit": map[string]any{"context": provider.ContextWindow, "output": provider.MaxOutputTokens},
Expand Down
3 changes: 2 additions & 1 deletion apps/daemon/internal/agent/mcode/model_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ func TestOptionsModelProviderProtocols(t *testing.T) {
t.Run(tc.protocol, func(t *testing.T) {
req := testRequest(t)
req.AgentOptions["model_provider"].(map[string]any)["protocol"] = tc.protocol
req.AgentOptions["model_provider"].(map[string]any)["headers"] = map[string]string{"X-Activity": "private-header"}
req.AgentOptions["model"] = "chosen-model"
opts, err := prepareOptions(testStateRoot(t), req)
if err != nil {
Expand All @@ -30,7 +31,7 @@ func TestOptionsModelProviderProtocols(t *testing.T) {
}
want := map[string]any{"oac": map[string]any{
"name": "Configured provider", "kind": "custom", "enabled": true, "api": tc.api,
"options": map[string]any{"baseURL": "https://provider.example/v1", "apiKey": "fixture-key"},
"options": map[string]any{"baseURL": "https://provider.example/v1", "apiKey": "fixture-key", "headers": map[string]any{"X-Activity": "private-header"}},
"models": map[string]any{"chosen-model": map[string]any{
"name": "chosen-model", "tool_call": true,
"limit": map[string]any{"context": float64(64000), "output": float64(4096)},
Expand Down
Loading
Loading