Skip to content

Commit ca876ff

Browse files
authored
Declare each Harness's support once (#531)
* Declare each Harness's support once Each internal/harnessconfig/<kind> returns one proto.Declaration beside its Configuration. proto.ValidateSelection is the only check of a declared rule: saved Agents with a Harness, Session creation, input admission, device selection, the final claim and delivery check it in Core, and the Runtime checks it before the Executor factory. A heartbeat only narrows the static declaration; Core closes a connection whose heartbeat widens it. Delete services/core/internal/engine, the execution policy callbacks, the adapter and bridge re-checks, the capabilities that never narrowed (WorkspaceReadPreparation, WorkspaceOutputExport, ProgrammaticToolCallingDisable, WebSearchControl, SubagentControl), ExecutionControls.WebSearch and the hand-written Harness capabilities page. * Validate a selection once in the Runtime
1 parent e51a790 commit ca876ff

204 files changed

Lines changed: 1394 additions & 2897 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CONTRIBUTING.md‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ This guide owns how to work in the repository: documentation ownership, the repo
1818
| Core implementation constraints beyond the public contracts | [Implementation constraints](services/core/IMPLEMENTATION.md) |
1919
| Environment ownership, preparation, Skills, Plugins, packages and MCP bindings | [Environments](contracts/agents-api/environments.md) |
2020
| Built-in Harness identifiers and display names | `internal/harnessconfig/builtin/catalog.json` and its [generated reference](contracts/agents-api/harness-catalog.md) |
21-
| Harness registration, service qualification and acceptance | [Harness onboarding](contracts/agents-api/harness-onboarding.md) |
22-
| Harness capabilities by placement | [Harness capabilities](contracts/agents-api/harness-capabilities.md) |
21+
| Harness registration, support declaration and acceptance | [Harness onboarding](contracts/agents-api/harness-onboarding.md) |
2322
| Harness selection, model providers and native parameters | [Model execution](contracts/agents-api/model-execution.md) |
2423
| Provider registration and lifecycle | [Sandbox Provider guide](docs/sandbox-provider.md) |
2524
| Sandbox deployment, selection and administrative transitions | [Sandbox deployment](contracts/agents-api/sandbox-deployment.md) |

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ The [installation guide](https://openagentcore.dev/docs/getting-started/install)
7171
| Build an application on the API | [Quickstart](https://openagentcore.dev/docs/getting-started/quickstart), then the [Agents API guide](https://openagentcore.dev/docs/api/public-agent-api) |
7272
| See a complete application | [Examples](https://openagentcore.dev/docs/examples) |
7373
| Run agents on my own machine | [Self-hosted execution](https://openagentcore.dev/docs/getting-started/self-hosted) |
74-
| Check Harness capabilities and limits | [Harness capabilities](https://openagentcore.dev/contracts/agents-api/harness-capabilities) |
74+
| Check Harness differences and limits | [Known gaps](https://openagentcore.dev/contracts/agents-api/#known-gaps) |
7575
| Understand the design | [Architecture](https://openagentcore.dev/docs/architecture) |
7676
| Add a sandbox, harness or other component | [Developer guide](https://openagentcore.dev/docs/development) |
7777

‎README.zh-CN.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ irm https://github.com/MiniMax-AI/OpenAgentCore/releases/latest/download/install
7171
| 基于 API 开发应用 | [快速开始](https://openagentcore.dev/zh/docs/getting-started/quickstart),然后看 [Agents API 指南](https://openagentcore.dev/zh/docs/api/public-agent-api) |
7272
| 看一个完整的应用 | [示例](https://openagentcore.dev/zh/docs/examples) |
7373
| 在自己的机器上运行 Agent | [自托管执行](https://openagentcore.dev/zh/docs/getting-started/self-hosted) |
74-
| 查看 Harness 能力和限制 | [Harness 能力](https://openagentcore.dev/zh/contracts/agents-api/harness-capabilities) |
74+
| 查看 Harness 差异和限制 | [已知缺口](https://openagentcore.dev/zh/contracts/agents-api/#known-gaps) |
7575
| 了解设计 | [架构说明](https://openagentcore.dev/zh/docs/architecture) |
7676
| 接入新的沙箱、Harness 或其他组件 | [开发指南](https://openagentcore.dev/zh/docs/development) |
7777

‎apps/daemon/internal/agent/claudesdk/cancellation_live_linux_test.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ func TestLiveClaudeSDKCancelResume(t *testing.T) {
6666
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
6767
defer cancel()
6868
out := make(chan proto.Envelope, 64)
69-
request := proto.PromptRequestPayload{RunID: uuid.NewString(), Input: proto.TextInput(prompt), AgentSessionID: resume, DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}, Model: "MiniMax-M3", ModelProvider: provider, SystemPrompt: "Follow the user's requested format. Preserve the exact verification value in conversation history. Use no tools."}
69+
request := proto.PromptRequestPayload{RunID: uuid.NewString(), Input: proto.TextInput(prompt), AgentSessionID: resume, DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium"}, Model: "MiniMax-M3", ModelProvider: provider, SystemPrompt: "Follow the user's requested format. Preserve the exact verification value in conversation history. Use no tools."}
7070
running, err := startSingleTurn(ctx, config, request, out)
7171
if err != nil {
7272
t.Fatal(err)

‎apps/daemon/internal/agent/claudesdk/declaration.go‎

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,11 @@ import (
1818
const claudeSDKEntrypointEnv = "OAC_RUNTIME_CLAUDE_SDK_ENTRYPOINT"
1919
const claudeSDKNodeEnv = "OAC_RUNTIME_CLAUDE_SDK_NODE"
2020

21-
// Declaration owns Claude SDK discovery, configuration and execution factories.
22-
var Declaration = agent.Declaration{Info: proto.SupportedAgentKind{Kind: "claude_sdk", Capabilities: proto.AgentKindCapabilities{
23-
SubagentObservations: proto.CapabilityUnsupported,
24-
NativeSessionRecovery: proto.CapabilityUnsupported,
25-
EnvironmentNone: proto.CapabilitySupported,
26-
LocalEnvironment: proto.CapabilityUnsupported,
27-
WorkspaceReadPreparation: proto.CapabilityUnsupported,
28-
WorkspaceOutputExport: proto.CapabilityUnsupported,
29-
ProgrammaticToolCallingDisable: proto.CapabilitySupported,
30-
WebSearchControl: proto.CapabilityUnsupported,
31-
TextVerbosity: proto.CapabilityUnsupported,
32-
StructuredOutput: proto.CapabilityUnsupported,
33-
ToolSearch: proto.CapabilityUnsupported,
34-
MessageImages: proto.CapabilityUnsupported,
35-
FunctionResultImages: proto.CapabilityUnsupported,
36-
SubagentControl: proto.CapabilitySupported,
37-
FunctionTools: proto.CapabilitySupported,
38-
MCPHTTPTools: proto.CapabilityUnsupported,
39-
MCPHTTPRequired: proto.CapabilityUnsupported,
40-
MCPHTTPBearerAuth: proto.CapabilityUnsupported,
41-
}}, Configuration: configuration.Configuration(), Discover: discover}
21+
// Declaration owns Claude SDK discovery, configuration and execution
22+
// factories. Discovery narrows the declared support to what the installed
23+
// bundle serves.
24+
var Declaration = agent.Declaration{Info: proto.SupportedAgentKind{Kind: "claude_sdk", Capabilities: configuration.Configuration().Declaration.Capabilities},
25+
Configuration: configuration.Configuration(), Discover: discover}
4226

4327
func discover(ctx context.Context, options agent.DiscoveryOptions, info proto.SupportedAgentKind) *agent.Runtime {
4428
return discoverWithCheck(ctx, options, info, CheckRuntime)
@@ -100,13 +84,11 @@ func discoverWithCheck(parent context.Context, options agent.DiscoveryOptions, d
10084
if err != nil {
10185
return fail(err)
10286
}
103-
caps := &out.Info.Capabilities
104-
if config.Workspace != nil {
105-
if !info.SupportsLocalRuntime() {
106-
return fail(fmt.Errorf("Claude SDK bundle does not support the local Runtime contract"))
107-
}
108-
caps.NativeSessionRecovery = proto.CapabilitySupported
87+
if config.Workspace != nil && !info.SupportsLocalRuntime() {
88+
return fail(fmt.Errorf("Claude SDK bundle does not support the local Runtime contract"))
10989
}
90+
caps := &out.Info.Capabilities
91+
caps.NativeSessionRecovery = proto.CapabilityFromBool(config.Workspace != nil)
11092
// One declaration holds for every Executor of the install: the workspace
11193
// bridge, the agent-host view and a Runtime without a workspace, so each
11294
// feature is its workspace variant, which the others also support.

‎apps/daemon/internal/agent/claudesdk/declaration_test.go‎

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,11 @@ import (
66
"io"
77
"os"
88
"path/filepath"
9-
"reflect"
109
"slices"
1110
"strings"
1211
"testing"
1312

1413
"github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent"
15-
"github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto"
1614
)
1715

1816
func TestClaudeSDKInvalidPathsFailBeforeProbe(t *testing.T) {
@@ -66,25 +64,6 @@ func TestClaudeSDKFeatureDiscovery(t *testing.T) {
6664
}
6765
}
6866

69-
// The declaration must retain the complete baseline capability descriptor.
70-
func TestDeclaredCapabilityBaseline(t *testing.T) {
71-
expected := map[string]bool{"EnvironmentNone": true, "ProgrammaticToolCallingDisable": true, "SubagentControl": true, "FunctionTools": true}
72-
value := reflect.ValueOf(Declaration.Info.Capabilities)
73-
for i := 0; i < value.NumField(); i++ {
74-
name := value.Type().Field(i).Name
75-
want := proto.CapabilityUnsupported
76-
if expected[name] {
77-
want = proto.CapabilitySupported
78-
}
79-
if got := value.Field(i).Interface(); got != want {
80-
t.Errorf("%s = %v, want %v", name, got, want)
81-
}
82-
}
83-
if err := Declaration.Info.ValidateDeclaration(); err != nil {
84-
t.Fatal(err)
85-
}
86-
}
87-
8867
func TestRuntimeDiscoveryConfigurationAndRegistration(t *testing.T) {
8968
root := t.TempDir()
9069
t.Setenv("OAC_RUNTIME_HOME", root)
@@ -118,7 +97,7 @@ func TestRuntimeDiscoveryConfigurationAndRegistration(t *testing.T) {
11897
}
11998
continue
12099
}
121-
if calls != 1 || runtime.Info.Available != ready || (runtime.Executor != nil) != ready || runtime.Info.Capabilities.LocalEnvironment.IsSupported() {
100+
if calls != 1 || runtime.Info.Available != ready || (runtime.Executor != nil) != ready || ready && runtime.Info.Capabilities.LocalEnvironment.IsSupported() {
122101
t.Fatalf("runtime: %+v", runtime)
123102
}
124103
}

‎apps/daemon/internal/agent/claudesdk/execution_controls_test.go‎

Lines changed: 10 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func TestExecutionControlsPreserveNativeDefaultsAndInstructions(t *testing.T) {
2222
if err != nil {
2323
t.Fatal(err)
2424
}
25-
request.ExecutionControls = &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}
25+
request.ExecutionControls = &proto.ExecutionControls{TextVerbosity: "medium"}
2626
before, _ := json.Marshal(request)
2727
controlled, _, err := prepareConfiguration(config, request)
2828
if err != nil {
@@ -34,33 +34,6 @@ func TestExecutionControlsPreserveNativeDefaultsAndInstructions(t *testing.T) {
3434
}
3535
}
3636

37-
func TestExecutionControlsRejectUnsupportedProfilesBeforeLaunch(t *testing.T) {
38-
cases := map[string]proto.ExecutionControls{
39-
"empty": {}, "missing-search": {TextVerbosity: "medium"}, "missing-verbosity": {WebSearch: "disabled"},
40-
"cached-search": {WebSearch: "cached", TextVerbosity: "medium"},
41-
"live-search": {WebSearch: "live", TextVerbosity: "medium"},
42-
"unknown-search": {WebSearch: "invalid", TextVerbosity: "medium"},
43-
"low-verbosity": {WebSearch: "disabled", TextVerbosity: "low"},
44-
"high-verbosity": {WebSearch: "disabled", TextVerbosity: "high"},
45-
"unknown-verbosity": {WebSearch: "disabled", TextVerbosity: "invalid"},
46-
}
47-
for name, controls := range cases {
48-
t.Run(name, func(t *testing.T) {
49-
root := t.TempDir()
50-
t.Setenv("OAC_RUNTIME_HOME", root)
51-
config := Config{Node: "must-not-run", Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state")}
52-
request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "run", Input: proto.TextInput("Original input."), ExecutionControls: &controls, Model: "native-model"}
53-
_, err := startSingleTurn(t.Context(), config, request, make(chan proto.Envelope, 1))
54-
if err == nil || !strings.Contains(err.Error(), "execution controls require") {
55-
t.Fatal("unsupported controls did not fail at admission", err)
56-
}
57-
if _, err := os.Stat(config.StateDir); !os.IsNotExist(err) {
58-
t.Fatal("unsupported controls reached native setup", err)
59-
}
60-
})
61-
}
62-
}
63-
6437
func TestMCPWithoutEnvironmentNoneRejectedBeforeSetup(t *testing.T) {
6538
root := t.TempDir()
6639
t.Setenv("OAC_RUNTIME_HOME", root)
@@ -81,44 +54,32 @@ func TestStructuredOutputConfigurationReachesNativeUnchanged(t *testing.T) {
8154
t.Setenv("OAC_RUNTIME_HOME", root)
8255
config := Config{Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state")}
8356
schema := json.RawMessage(`{"type":"object","properties":{"n":{"const":9007199254740992}}}`)
84-
request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), DisableSubagents: true, Model: "model", SystemPrompt: "Original instructions.", ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium", OutputFormat: &proto.OutputFormat{Type: "json_schema", Schema: schema}}}
57+
request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), DisableSubagents: true, Model: "model", SystemPrompt: "Original instructions.", ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium", OutputFormat: &proto.OutputFormat{Type: "json_schema", Schema: schema}}}
8558
start, _, err := prepareConfiguration(config, request)
8659
if err != nil {
8760
t.Fatal(err)
8861
}
8962
if start.OutputFormat == nil || string(start.OutputFormat.Schema) != string(schema) || start.SystemPrompt != "Original instructions." {
9063
t.Fatal("native configuration changed")
9164
}
92-
request.ExecutionControls.OutputFormat.Schema = json.RawMessage(`{"type":"object","const":9007199254740993}`)
93-
if _, _, err := prepareConfiguration(config, request); err == nil {
94-
t.Fatal("lossy schema accepted")
95-
}
96-
request.ExecutionControls.OutputFormat.Schema = schema
97-
request.DisableSubagents = false
98-
if _, _, err := prepareConfiguration(config, request); err == nil {
99-
t.Fatal("unqualified subagent combination accepted")
100-
}
10165
}
10266

103-
func TestToolDiscoveryPreservesFrozenFunctionsAndRejectsOtherProfiles(t *testing.T) {
67+
// Tool discovery keeps the frozen definitions; a typeless parameters root
68+
// becomes an object root, which admits the same arguments.
69+
func TestToolDiscoveryPreservesFrozenFunctions(t *testing.T) {
10470
root := t.TempDir()
10571
t.Setenv("OAC_RUNTIME_HOME", root)
10672
config := Config{Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state")}
10773
request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), DisableSubagents: true, ToolSearch: true, Model: "model", FunctionTools: []proto.FunctionTool{
10874
{Name: "lookup", Description: "Lookup", Parameters: json.RawMessage(`{"type":"object","properties":{"ticket":{"const":"original"}}}`), DeferLoading: true},
109-
{Name: "clock", Description: "Clock", Parameters: json.RawMessage(`{"type":"object"}`)},
75+
{Name: "clock", Description: "Clock", Parameters: json.RawMessage(`{"properties":{}}`)},
76+
{Name: "note", Description: "Note", Parameters: json.RawMessage(`{"type":["object","null"]}`)},
11077
}}
11178
start, _, err := prepareConfiguration(config, request)
112-
if err != nil || !start.ToolSearch || !reflect.DeepEqual(start.Functions, request.FunctionTools) {
79+
if err != nil || !start.ToolSearch || !reflect.DeepEqual(start.Functions[0], request.FunctionTools[0]) || string(start.Functions[1].Parameters) != `{"properties":{},"type":"object"}` || string(start.Functions[2].Parameters) != `{"type":"object"}` {
11380
t.Fatal("function discovery changed native definitions", err)
11481
}
115-
request.DisableSubagents = false
116-
if _, _, err := prepareConfiguration(config, request); err == nil {
117-
t.Fatal("unqualified combination admitted")
118-
}
119-
request.DisableSubagents = true
120-
request.ToolSearch = false
121-
if _, _, err := prepareConfiguration(config, request); err == nil {
122-
t.Fatal("deferred definitions became eager")
82+
if string(request.FunctionTools[1].Parameters) != `{"properties":{}}` {
83+
t.Fatal("typing the root changed the frozen request")
12384
}
12485
}

‎apps/daemon/internal/agent/claudesdk/executor_live_linux_test.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func TestLiveClaudeExecutorReuseAndCancel(t *testing.T) {
8383
_ = os.WriteFile(filepath.Join(proof, "executor-evidence.json"), raw, 0600)
8484
}
8585
defer persist()
86-
request := proto.PromptRequestPayload{DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}, Model: model, ModelProvider: provider, SystemPrompt: "Follow requested formats briefly. Remember the exact verification marker across the conversation. Use no tools."}
86+
request := proto.PromptRequestPayload{DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium"}, Model: model, ModelProvider: provider, SystemPrompt: "Follow requested formats briefly. Remember the exact verification marker across the conversation. Use no tools."}
8787
factory := NewExecutorFactory(config)
8888
prepared := time.Now()
8989
owner, err := factory(ctx, request)

‎apps/daemon/internal/agent/claudesdk/functions.go‎

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"slices"
78
"strings"
89
"sync"
910
"time"
@@ -25,18 +26,34 @@ type functionState struct {
2526
closed bool
2627
}
2728

28-
func validateFunctions(tools []proto.FunctionTool) error {
29+
// functionTools types each parameters root as an object, which the native SDK
30+
// requires. A typeless root or a union with object admits the same tool
31+
// arguments, since these are always objects.
32+
func functionTools(tools []proto.FunctionTool) ([]proto.FunctionTool, error) {
2933
names := map[string]bool{}
30-
for _, tool := range tools {
31-
var schema struct {
32-
Type string `json:"type"`
33-
}
34-
if strings.TrimSpace(tool.Name) == "" || names[tool.Name] || json.Unmarshal(tool.Parameters, &schema) != nil || schema.Type != "object" {
35-
return fmt.Errorf("claudesdk: functions require unique names and object-root JSON schemas")
34+
typed := slices.Clone(tools)
35+
for i, tool := range typed {
36+
var schema map[string]json.RawMessage
37+
var root string
38+
var union []string
39+
if strings.TrimSpace(tool.Name) == "" || names[tool.Name] || json.Unmarshal(tool.Parameters, &schema) != nil || schema == nil {
40+
return nil, fmt.Errorf("claudesdk: functions require unique names and object JSON schemas")
3641
}
3742
names[tool.Name] = true
43+
switch t := schema["type"]; {
44+
case json.Unmarshal(t, &root) == nil && root == "object":
45+
case t == nil || json.Unmarshal(t, &union) == nil && slices.Contains(union, "object"):
46+
schema["type"] = json.RawMessage(`"object"`)
47+
parameters, err := json.Marshal(schema)
48+
if err != nil {
49+
return nil, err
50+
}
51+
typed[i].Parameters = parameters
52+
default:
53+
return nil, fmt.Errorf("claudesdk: functions require unique names and object JSON schemas")
54+
}
3855
}
39-
return nil
56+
return typed, nil
4057
}
4158

4259
func (s *session) receiveFunction(event bridgeEvent, start startRequest, emit func(string, any)) error {
@@ -104,14 +121,6 @@ func (s *session) SubmitFunctionResult(ctx context.Context, result proto.Functio
104121
if err := result.ValidateContent(); err != nil {
105122
return err
106123
}
107-
for _, part := range result.Content {
108-
if part.Type == "input_image" && !result.Success {
109-
return fmt.Errorf("claudesdk: native error results cannot retain images")
110-
}
111-
}
112-
if err := (proto.MessageInput{{Content: result.Content}}).ValidateInlineImages(); err != nil {
113-
return err
114-
}
115124
data, err := json.Marshal(struct {
116125
Type string `json:"type"`
117126
TurnID string `json:"turn_id"`

‎apps/daemon/internal/agent/claudesdk/functions_test.go‎

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,6 @@ func TestFunctionTurnNativeReceipts(t *testing.T) {
4949
if err := running.SubmitFunctionResult(ctx, invalid); err == nil {
5050
t.Fatal("missing content consumed call")
5151
}
52-
image := "https://example.invalid/image"
53-
invalid.Content = []proto.InputContent{{Type: "input_image", ImageURL: &image}}
54-
if err := running.SubmitFunctionResult(ctx, invalid); err == nil {
55-
t.Fatal("image should fail before delivery")
56-
}
5752
first, second := "first-"+call.CallID, "second-"+call.CallID
5853
value := proto.FunctionResultPayload{DeliveryID: "delivery-" + call.CallID, CallID: call.CallID, Success: call.CallID == "b", Content: []proto.InputContent{{Type: "input_text", Text: &first}, {Type: "input_text", Text: &second}}}
5954
go func() { submissions <- running.SubmitFunctionResult(ctx, value) }()

0 commit comments

Comments
 (0)