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
13 changes: 5 additions & 8 deletions apps/daemon/internal/agent/mcode/environment_mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,10 @@ func runtimeMCP(req proto.PromptRequestPayload) ([]map[string]any, error) {
if err != nil {
return nil, err
}
var servers []map[string]any
servers := []map[string]any{}
for _, binding := range bindings {
if binding.ConnectionOrigin != "environment" || binding.AllowedTools != nil || binding.Required {
return nil, fmt.Errorf("mcode: unsupported MCP binding")
}
if binding.Transport == "http" {
server, err := environmentHTTPMCP(binding)
server, err := httpMCP(binding)
if err != nil {
return nil, err
}
Expand All @@ -37,15 +34,15 @@ func runtimeMCP(req proto.PromptRequestPayload) ([]map[string]any, error) {
// ACP session servers remain in native memory; credentials never enter argv or
// persisted native configuration. Custom headers are not qualified because the
// pinned native HTTP client can forward them across redirect origins.
func environmentHTTPMCP(item agent.MCPBinding) (map[string]any, error) {
func httpMCP(item agent.MCPBinding) (map[string]any, error) {
endpoint, err := url.Parse(item.ServerURL)
if err != nil || (endpoint.Scheme != "http" && endpoint.Scheme != "https") || endpoint.Hostname() == "" || endpoint.User != nil || endpoint.Fragment != "" || endpoint.Opaque != "" || len(item.HTTPHeaders) != 0 {
return nil, fmt.Errorf("mcode: unsupported environment HTTP MCP declaration")
return nil, fmt.Errorf("mcode: unsupported HTTP MCP declaration")
}
headers := []map[string]string{}
if item.BearerToken != nil {
if endpoint.Scheme != "https" || !agent.ValidMCPHTTPBearerToken(*item.BearerToken) {
return nil, fmt.Errorf("mcode: unsupported environment MCP bearer credential")
return nil, fmt.Errorf("mcode: unsupported MCP bearer credential")
}
headers = append(headers, map[string]string{"name": "Authorization", "value": "Bearer " + *item.BearerToken})
}
Expand Down
30 changes: 19 additions & 11 deletions apps/daemon/internal/agent/mcode/environment_mcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,11 @@ func TestEnvironmentMCPRejectsUnqualifiedAuthorityBeforePreparation(t *testing.T
case "duplicate":
req.LocalEnvironment.MCP = append(req.LocalEnvironment.MCP, environmentMCPFixture())
}
if _, err := prepareWorkspaceOptions(testStateRoot(t), c, req); err == nil || strings.Contains(err.Error(), "confidential-http-token") {
_, err := prepareWorkspaceOptions(testStateRoot(t), c, req)
if err == nil {
_, err = runtimeMCP(req)
}
if err == nil || strings.Contains(err.Error(), "confidential-http-token") {
t.Fatal("unqualified declaration accepted or credential exposed")
}
})
Expand Down Expand Up @@ -232,7 +236,11 @@ func TestPublicEnvironmentHTTPMCPKeepsCredentialTransient(t *testing.T) {
if err != nil {
t.Fatal(err)
}
raw, err := json.Marshal(opts.MCP)
servers, err := runtimeMCP(req)
if err != nil {
t.Fatal(err)
}
raw, err := json.Marshal(servers)
if err != nil || !strings.Contains(string(raw), "Bearer "+token) {
t.Fatal("selected token not supplied to native ACP")
}
Expand All @@ -255,14 +263,14 @@ func TestPublicEnvironmentHTTPMCPKeepsCredentialTransient(t *testing.T) {
if err != nil {
t.Fatal(err)
}
empty := []string{}
(*req.MCPHTTPServers)[0].AllowedTools = &empty
if _, err := prepareWorkspaceOptions(testStateRoot(t), c, req); err == nil {
t.Fatal("empty allowlist silently treated as all")
}
(*req.MCPHTTPServers)[0].AllowedTools = nil
(*req.MCPHTTPServers)[0].Required = true
if _, err := prepareWorkspaceOptions(testStateRoot(t), c, req); err == nil {
t.Fatal("required initialization silently ignored")
}

func TestServiceOriginMCPOnNonePlacement(t *testing.T) {
req := testRequest(t)
req.MCPHTTPServers = &[]proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "fixture", ServerURL: "https://mcp.example/mcp"}}
servers, err := runtimeMCP(req)
want := []map[string]any{{"name": "fixture", "type": "http", "url": "https://mcp.example/mcp", "headers": []map[string]string{}}}
if err != nil || !reflect.DeepEqual(servers, want) {
t.Fatalf("MCP = %v, %v", servers, err)
}
}
2 changes: 1 addition & 1 deletion apps/daemon/internal/agent/mcode/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func (s *Session) emitTool(update toolUpdate) error {
}
if update.mcp == nil {
var err error
update.mcp, err = s.environmentMCPIdentity(update.Name)
update.mcp, err = s.mcpIdentity(update.Name)
if err != nil {
return err
}
Expand Down
14 changes: 10 additions & 4 deletions apps/daemon/internal/agent/mcode/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ package mcode
import (
"fmt"
"os"
"strings"

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

func validateExecutionRequest(req proto.PromptRequestPayload) error {
if !req.DisableExecutionEnvironment || req.AgentStateKey == "" || req.LocalEnvironment != nil || req.RequireExistingNativeSession || len(req.FunctionTools) != 0 || (req.MCPHTTPServers != nil && len(*req.MCPHTTPServers) != 0) {
if !req.DisableExecutionEnvironment || req.AgentStateKey == "" || req.LocalEnvironment != nil || req.RequireExistingNativeSession {
return fmt.Errorf("mcode: unsupported execution configuration")
}
if req.ExecutionControls == nil || req.ExecutionControls.OutputFormat != nil {
Expand All @@ -32,8 +33,13 @@ func validateExecutionRequest(req proto.PromptRequestPayload) error {
// Harness children use the daemon user's ordinary environment.
func executionEnvironment() []string { return os.Environ() }

// ACP commands are only recognized for a single text block. A second, empty
// block keeps public input as user text.
// ACP joins text blocks with "\n\n" and parses slash commands only from a
// single block. A second, empty block keeps input that starts with "/" as user
// text; any other input is sent as one block so the model sees it unchanged.
func promptContent(text string) []map[string]string {
return []map[string]string{{"type": "text", "text": text}, {"type": "text", "text": ""}}
content := []map[string]string{{"type": "text", "text": text}}
if strings.HasPrefix(text, "/") {
content = append(content, map[string]string{"type": "text", "text": ""})
}
return content
}
5 changes: 2 additions & 3 deletions apps/daemon/internal/agent/mcode/execution_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ func TestExecutionRejectsUnqualifiedAuthority(t *testing.T) {
func(r *proto.PromptRequestPayload) { r.DisableExecutionEnvironment = false },
func(r *proto.PromptRequestPayload) { r.DisableSubagents = false },
func(r *proto.PromptRequestPayload) { r.RequireExistingNativeSession = true },
func(r *proto.PromptRequestPayload) { r.FunctionTools = []proto.FunctionTool{{Name: "f"}} },
func(r *proto.PromptRequestPayload) {
r.ExecutionControls.WebSearch = &proto.WebSearchControl{Mode: "live", ContextSize: "medium"}
},
Expand All @@ -57,9 +56,9 @@ func TestExecutionRejectsUnqualifiedAuthority(t *testing.T) {
}

func TestPublicTextDoesNotInvokeACPCommands(t *testing.T) {
for _, text := range []string{"/model", "/compact", "hello"} {
for text, want := range map[string]int{"/model": 2, "/compact": 2, "hello": 1} {
blocks := promptContent(text)
if len(blocks) != 2 || blocks[0]["text"] != text || blocks[1]["text"] != "" {
if len(blocks) != want || blocks[0]["text"] != text || (want == 2 && blocks[1]["text"] != "") {
t.Fatal(blocks)
}
}
Expand Down
3 changes: 3 additions & 0 deletions apps/daemon/internal/agent/mcode/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ func NewExecutorFactory(runtimeRoot string, config *WorkspaceConfig) agent.Execu
binary = frozen.Binary
opts, err = prepareWorkspaceOptions(runtimeRoot, *frozen, req)
}
if err == nil {
opts.MCP, err = runtimeMCP(req)
}
if err != nil {
return nil, err
}
Expand Down
7 changes: 3 additions & 4 deletions apps/daemon/internal/agent/mcode/mcp_observations.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@ type mcpToolIdentity struct{ server, tool string }

// MiniMax 0.4.12 atomically writes these assignments before exposing tools.
// Read its exact mapping instead of reversing lossy native name normalization.
func (s *Session) environmentMCPIdentity(name string) (*mcpToolIdentity, error) {
if s.req.LocalEnvironment == nil ||
!strings.HasPrefix(name, "mcp__") {
func (s *Session) mcpIdentity(name string) (*mcpToolIdentity, error) {
if !strings.HasPrefix(name, "mcp__") {
return nil, nil
}
bindings, err := agent.ResolveMCPBindings(s.req)
Expand Down Expand Up @@ -68,7 +67,7 @@ func (s *Session) environmentMCPIdentity(name string) (*mcpToolIdentity, error)
return found, nil
}

func environmentMCPObservation(update toolUpdate, stage string) (*proto.ToolObservation, error) {
func mcpObservation(update toolUpdate, stage string) (*proto.ToolObservation, error) {
if update.mcp == nil {
return nil, nil
}
Expand Down
8 changes: 6 additions & 2 deletions apps/daemon/internal/agent/mcode/model_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider"
)

func modelProviderConfig(raw any, model string) (map[string]any, error) {
func modelProviderConfig(raw any, model, effort string) (map[string]any, error) {
provider, err := harnessconfiguration.Declaration.Configuration.ParseProvider(raw)
if err != nil {
return nil, fmt.Errorf("mcode: %w", err)
Expand All @@ -21,7 +21,7 @@ func modelProviderConfig(raw any, model string) (map[string]any, error) {
case modelprovider.Responses:
api = "openai-responses"
}
native := map[string]any{"name": model, "tool_call": true}
native := map[string]any{"name": model}
// Absent limits are omitted so the native defaults apply.
limit := map[string]any{}
if provider.ContextWindow > 0 {
Expand All @@ -33,6 +33,10 @@ func modelProviderConfig(raw any, model string) (map[string]any, error) {
if len(limit) > 0 {
native["limit"] = limit
}
if effort != "" {
// The Session selects this effort through ACP thinkingEffort.
native["thinking"] = map[string]any{"effortOptions": []string{effort}}
}
return map[string]any{
"name": "Configured provider", "kind": "custom", "enabled": true, "api": api,
"options": map[string]any{"baseURL": provider.BaseURL, "apiKey": provider.APIKey},
Expand Down
29 changes: 28 additions & 1 deletion apps/daemon/internal/agent/mcode/model_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func TestOptionsModelProviderProtocols(t *testing.T) {
"name": "Configured provider", "kind": "custom", "enabled": true, "api": tc.api,
"options": map[string]any{"baseURL": "https://provider.example/v1", "apiKey": "fixture-key"},
"models": map[string]any{"chosen-model": map[string]any{
"name": "chosen-model", "tool_call": true,
"name": "chosen-model",
"limit": map[string]any{"context": float64(64000), "output": float64(4096)},
}},
}}
Expand Down Expand Up @@ -118,3 +118,30 @@ func TestOptionsAllowLoopbackProviderFixture(t *testing.T) {
t.Fatal(err)
}
}

func TestOptionsWriteNativeThinkingEffort(t *testing.T) {
req := testRequest(t)
req.ExecutionControls.ReasoningEffort = "high"
opts, err := prepareOptions(testStateRoot(t), req, nil)
if err != nil {
t.Fatal(err)
}
body, err := os.ReadFile(filepath.Join(opts.DataDir, "config.yaml"))
if err != nil {
t.Fatal(err)
}
var config struct {
CustomProvider map[string]struct {
Models map[string]struct {
Thinking map[string]any `json:"thinking"`
} `json:"models"`
} `json:"custom_provider"`
}
if err := json.Unmarshal(body, &config); err != nil {
t.Fatal(err)
}
want := map[string]any{"effortOptions": []any{"high"}}
if got := config.CustomProvider["oac"].Models["fixture"].Thinking; !reflect.DeepEqual(got, want) {
t.Fatalf("thinking = %v", got)
}
}
Loading
Loading