diff --git a/apps/daemon/internal/agent/claudesdk/declaration.go b/apps/daemon/internal/agent/claudesdk/declaration.go index 04deb951c..c2756ccd2 100644 --- a/apps/daemon/internal/agent/claudesdk/declaration.go +++ b/apps/daemon/internal/agent/claudesdk/declaration.go @@ -21,29 +21,20 @@ const claudeSDKNodeEnv = "OAC_RUNTIME_CLAUDE_SDK_NODE" // Declaration owns Claude SDK discovery, configuration and execution factories. var Declaration = agent.Declaration{Info: proto.SupportedAgentKind{Kind: "claude_sdk", Capabilities: proto.AgentKindCapabilities{ SubagentObservations: proto.CapabilityUnsupported, - Streaming: proto.CapabilitySupported, - Usage: proto.CapabilitySupported, - Resume: proto.CapabilitySupported, NativeSessionRecovery: proto.CapabilityUnsupported, - Steering: proto.CapabilitySupported, MessageItems: proto.CapabilitySupported, - ToolObservations: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported, LocalEnvironment: proto.CapabilityUnsupported, - Preparation: proto.CapabilityUnsupported, WorkspaceReadPreparation: proto.CapabilityUnsupported, WorkspaceOutputExport: proto.CapabilityUnsupported, ProgrammaticToolCallingDisable: proto.CapabilitySupported, WebSearchControl: proto.CapabilityUnsupported, - ExecutionControls: proto.CapabilitySupported, TextVerbosity: proto.CapabilityUnsupported, StructuredOutput: proto.CapabilityUnsupported, ToolSearch: proto.CapabilityUnsupported, MessageImages: proto.CapabilityUnsupported, FunctionResultImages: proto.CapabilityUnsupported, SubagentControl: proto.CapabilitySupported, - DurableInputReceipts: proto.CapabilitySupported, - DurableTurns: proto.CapabilitySupported, FunctionTools: proto.CapabilitySupported, MCPHTTPTools: proto.CapabilityUnsupported, MCPHTTPRequired: proto.CapabilityUnsupported, diff --git a/apps/daemon/internal/agent/claudesdk/declaration_test.go b/apps/daemon/internal/agent/claudesdk/declaration_test.go index 9bd2721ba..dc5148eaf 100644 --- a/apps/daemon/internal/agent/claudesdk/declaration_test.go +++ b/apps/daemon/internal/agent/claudesdk/declaration_test.go @@ -66,7 +66,7 @@ func TestClaudeSDKFeatureDiscovery(t *testing.T) { // The declaration must retain the complete baseline capability descriptor. func TestDeclaredCapabilityBaseline(t *testing.T) { - expected := map[string]bool{"Streaming": true, "Usage": true, "Resume": true, "Steering": true, "MessageItems": true, "ToolObservations": true, "EnvironmentNone": true, "ProgrammaticToolCallingDisable": true, "ExecutionControls": true, "SubagentControl": true, "DurableInputReceipts": true, "DurableTurns": true, "FunctionTools": true} + expected := map[string]bool{"MessageItems": true, "EnvironmentNone": true, "ProgrammaticToolCallingDisable": true, "SubagentControl": true, "FunctionTools": true} value := reflect.ValueOf(Declaration.Info.Capabilities) for i := 0; i < value.NumField(); i++ { name := value.Type().Field(i).Name @@ -119,12 +119,6 @@ func TestRuntimeDiscoveryConfigurationAndRegistration(t *testing.T) { if calls != 1 || runtime.Info.Available != ready || (runtime.Executor != nil) != ready || runtime.Info.Capabilities.LocalEnvironment.IsSupported() { t.Fatalf("runtime: %+v", runtime) } - registry := agent.NewRegistry() - registry.Register(Declaration, *runtime) - info := registry.SupportedAgentKinds()[0] - if info.Capabilities.Preparation.IsSupported() != ready { - t.Fatal(info) - } } } } diff --git a/apps/daemon/internal/agent/codex/declaration.go b/apps/daemon/internal/agent/codex/declaration.go index 48942f2fc..809d7e718 100644 --- a/apps/daemon/internal/agent/codex/declaration.go +++ b/apps/daemon/internal/agent/codex/declaration.go @@ -15,29 +15,20 @@ var Declaration = agent.Declaration{Info: proto.SupportedAgentKind{ Kind: "codex", Capabilities: proto.AgentKindCapabilities{ SubagentObservations: proto.CapabilitySupported, - Streaming: proto.CapabilitySupported, - Usage: proto.CapabilitySupported, - Resume: proto.CapabilitySupported, NativeSessionRecovery: proto.CapabilityUnsupported, - Steering: proto.CapabilitySupported, MessageItems: proto.CapabilitySupported, - ToolObservations: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported, LocalEnvironment: proto.CapabilityUnsupported, - Preparation: proto.CapabilityUnsupported, WorkspaceReadPreparation: proto.CapabilityUnsupported, WorkspaceOutputExport: proto.CapabilityUnsupported, ProgrammaticToolCallingDisable: proto.CapabilitySupported, WebSearchControl: proto.CapabilitySupported, - ExecutionControls: proto.CapabilityFromBool(SupportsTextVerbosity), TextVerbosity: proto.CapabilityFromBool(SupportsTextVerbosity), StructuredOutput: proto.CapabilityUnsupported, ToolSearch: proto.CapabilityUnsupported, MessageImages: proto.CapabilitySupported, FunctionResultImages: proto.CapabilitySupported, SubagentControl: proto.CapabilitySupported, - DurableInputReceipts: proto.CapabilitySupported, - DurableTurns: proto.CapabilitySupported, FunctionTools: proto.CapabilitySupported, MCPHTTPTools: proto.CapabilitySupported, MCPHTTPRequired: proto.CapabilityUnsupported, @@ -50,6 +41,11 @@ func discover(ctx context.Context, options agent.DiscoveryOptions, info proto.Su } func discoverWithCheck(parent context.Context, options agent.DiscoveryOptions, info proto.SupportedAgentKind, check func(context.Context, string) (string, error)) *agent.Runtime { runtime := &agent.Runtime{Info: info} + if !SupportsTextVerbosity { + // Every execution carries a text verbosity, which needs the bounded catalog probe. + fmt.Fprintln(options.Stderr, "oac-daemon: codex unavailable: text verbosity requires Unix process-group cancellation support") + return runtime + } ctx, cancel := context.WithTimeout(parent, 15*time.Second) defer cancel() version, err := check(ctx, "") diff --git a/apps/daemon/internal/agent/codex/declaration_test.go b/apps/daemon/internal/agent/codex/declaration_test.go index 2653897ec..af90218d3 100644 --- a/apps/daemon/internal/agent/codex/declaration_test.go +++ b/apps/daemon/internal/agent/codex/declaration_test.go @@ -29,8 +29,7 @@ func TestMCPRequiredDiscoveryRequiresPinnedNative(t *testing.T) { // The declaration must retain the complete baseline capability descriptor. func TestDeclaredCapabilityBaseline(t *testing.T) { - expected := map[string]bool{"SubagentObservations": true, "Streaming": true, "Usage": true, "Resume": true, "Steering": true, "MessageItems": true, "ToolObservations": true, "EnvironmentNone": true, "ProgrammaticToolCallingDisable": true, "WebSearchControl": true, "MessageImages": true, "FunctionResultImages": true, "SubagentControl": true, "DurableInputReceipts": true, "DurableTurns": true, "FunctionTools": true, "MCPHTTPTools": true, "MCPHTTPBearerAuth": true} - expected["ExecutionControls"], expected["TextVerbosity"] = SupportsTextVerbosity, SupportsTextVerbosity + expected := map[string]bool{"SubagentObservations": true, "MessageItems": true, "EnvironmentNone": true, "ProgrammaticToolCallingDisable": true, "WebSearchControl": true, "TextVerbosity": SupportsTextVerbosity, "MessageImages": true, "FunctionResultImages": true, "SubagentControl": true, "FunctionTools": true, "MCPHTTPTools": true, "MCPHTTPBearerAuth": true} value := reflect.ValueOf(Declaration.Info.Capabilities) for i := 0; i < value.NumField(); i++ { name := value.Type().Field(i).Name diff --git a/apps/daemon/internal/agent/harness.go b/apps/daemon/internal/agent/harness.go index 712169023..719485404 100644 --- a/apps/daemon/internal/agent/harness.go +++ b/apps/daemon/internal/agent/harness.go @@ -14,9 +14,8 @@ // Registration: each adapter exports one Declaration. The Runtime discovers the // static declaration list and installs each resulting Runtime through Register. // Availability and factory selection belong to the adapter. RegisterKind resets -// the factories, so Register installs it first. RegisterExecutor derives the -// Preparation capability. The Runtime's Environment owner, not the adapter, -// serves and declares workspace operations. +// the factories, so Register installs it first. The Runtime's Environment +// owner, not the adapter, serves and declares workspace operations. // // Runtime registration and Core service qualification remain separate. A public // Harness also needs a profile in services/core/internal/engine; advertising @@ -627,16 +626,15 @@ func (r *Registry) RegisterKind(info proto.SupportedAgentKind, configuration har r.configurations[kind] = configuration delete(r.executors, kind) delete(r.views, kind) - info.Capabilities.Preparation = proto.CapabilityUnsupported r.kinds[kind] = info } -// RegisterExecutor installs the shared lifecycle after RegisterKind and derives -// the Preparation capability. It does not enable other public operations. +// RegisterExecutor installs the shared lifecycle after RegisterKind. It does +// not enable other public operations. func (r *Registry) RegisterExecutor(kind string, factory ExecutorFactory) { r.mu.Lock() defer r.mu.Unlock() - info, exists := r.kinds[kind] + _, exists := r.kinds[kind] if !exists || factory == nil { panic("agent.Registry.RegisterExecutor: registered kind and factory required") } @@ -650,6 +648,4 @@ func (r *Registry) RegisterExecutor(kind string, factory ExecutorFactory) { } return factory(ctx, req) } - info.Capabilities.Preparation = proto.CapabilitySupported - r.kinds[kind] = info } diff --git a/apps/daemon/internal/agent/mcode/declaration.go b/apps/daemon/internal/agent/mcode/declaration.go index 461cd53ed..b4a0762af 100644 --- a/apps/daemon/internal/agent/mcode/declaration.go +++ b/apps/daemon/internal/agent/mcode/declaration.go @@ -13,29 +13,20 @@ import ( // Declaration owns MiniMax Code discovery, configuration and execution factories. var Declaration = agent.Declaration{Info: proto.SupportedAgentKind{Kind: "mcode", Capabilities: proto.AgentKindCapabilities{ SubagentObservations: proto.CapabilityUnsupported, - Streaming: proto.CapabilitySupported, - Usage: proto.CapabilityUnsupported, - Resume: proto.CapabilitySupported, NativeSessionRecovery: proto.CapabilityUnsupported, - Steering: proto.CapabilityUnsupported, MessageItems: proto.CapabilityUnsupported, - ToolObservations: proto.CapabilityUnsupported, EnvironmentNone: proto.CapabilityUnsupported, LocalEnvironment: proto.CapabilityUnsupported, - Preparation: proto.CapabilityUnsupported, WorkspaceReadPreparation: proto.CapabilityUnsupported, WorkspaceOutputExport: proto.CapabilityUnsupported, ProgrammaticToolCallingDisable: proto.CapabilityUnsupported, WebSearchControl: proto.CapabilityUnsupported, - ExecutionControls: proto.CapabilityUnsupported, TextVerbosity: proto.CapabilityUnsupported, StructuredOutput: proto.CapabilityUnsupported, ToolSearch: proto.CapabilityUnsupported, MessageImages: proto.CapabilityUnsupported, FunctionResultImages: proto.CapabilityUnsupported, SubagentControl: proto.CapabilityUnsupported, - DurableInputReceipts: proto.CapabilityUnsupported, - DurableTurns: proto.CapabilityUnsupported, FunctionTools: proto.CapabilityUnsupported, MCPHTTPTools: proto.CapabilityUnsupported, MCPHTTPRequired: proto.CapabilityUnsupported, @@ -56,12 +47,7 @@ func discoverWithCheck(parent context.Context, options agent.DiscoveryOptions, r return runtime } result.Available, result.Version = true, version - result.Capabilities.Steering = proto.CapabilitySupported - result.Capabilities.DurableTurns = proto.CapabilitySupported - result.Capabilities.DurableInputReceipts = proto.CapabilitySupported - result.Capabilities.ExecutionControls = proto.CapabilitySupported result.Capabilities.ProgrammaticToolCallingDisable = proto.CapabilitySupported - result.Capabilities.ToolObservations = proto.CapabilitySupported result.Capabilities.SubagentControl = proto.CapabilitySupported // Native preparation verifies the applied admission/tool profile before input. result.Capabilities.SubagentObservations = proto.CapabilitySupported diff --git a/apps/daemon/internal/agent/mcode/declaration_test.go b/apps/daemon/internal/agent/mcode/declaration_test.go index a2a72c6f6..b9d2dacaa 100644 --- a/apps/daemon/internal/agent/mcode/declaration_test.go +++ b/apps/daemon/internal/agent/mcode/declaration_test.go @@ -24,7 +24,7 @@ func TestMCodeExecutionFollowsAvailability(t *testing.T) { if (runtime.Executor != nil) != available { t.Fatalf("factories: %+v", runtime) } - if info.Available != available || info.Capabilities.EnvironmentNone.IsSupported() != available || info.Capabilities.DurableInputReceipts.IsSupported() != available || info.Capabilities.SubagentObservations.IsSupported() != available { + if info.Available != available || info.Capabilities.EnvironmentNone.IsSupported() != available || info.Capabilities.SubagentObservations.IsSupported() != available { t.Fatalf("capabilities=%+v", info.Capabilities) } if info.Capabilities.NativeSessionRecovery.IsSupported() || info.Capabilities.LocalEnvironment.IsSupported() || info.Capabilities.FunctionTools.IsSupported() { @@ -34,18 +34,12 @@ func TestMCodeExecutionFollowsAvailability(t *testing.T) { } } -// The declaration must retain the complete baseline capability descriptor. +// The static declaration supports nothing until discovery finds the CLI. func TestDeclaredCapabilityBaseline(t *testing.T) { - expected := map[string]bool{"Streaming": true, "Resume": true} value := reflect.ValueOf(Declaration.Info.Capabilities) for i := 0; i < value.NumField(); i++ { - name := value.Type().Field(i).Name - want := proto.CapabilityUnsupported - if expected[name] { - want = proto.CapabilitySupported - } - if got := value.Field(i).Interface(); got != want { - t.Errorf("%s = %v, want %v", name, got, want) + if got := value.Field(i).Interface(); got != proto.CapabilityUnsupported { + t.Errorf("%s = %v, want unsupported", value.Type().Field(i).Name, got) } } if err := Declaration.Info.ValidateDeclaration(); err != nil { diff --git a/apps/daemon/internal/agent/registry_test.go b/apps/daemon/internal/agent/registry_test.go index e2538065a..40f9b66ef 100644 --- a/apps/daemon/internal/agent/registry_test.go +++ b/apps/daemon/internal/agent/registry_test.go @@ -55,7 +55,7 @@ func TestRegistrySupportedAgentKindsReportsDescriptors(t *testing.T) { Available: false, Version: "missing", Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{ - Streaming: proto.CapabilitySupported, + MessageItems: proto.CapabilitySupported, }), }, harnessconfig.Configuration{}) reg.RegisterKind(proto.SupportedAgentKind{ @@ -63,9 +63,8 @@ func TestRegistrySupportedAgentKindsReportsDescriptors(t *testing.T) { Available: true, Version: "1.2.3", Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{ - Streaming: proto.CapabilitySupported, - Usage: proto.CapabilitySupported, - Resume: proto.CapabilitySupported, + MessageItems: proto.CapabilitySupported, + EnvironmentNone: proto.CapabilitySupported, }), }, harnessconfig.Configuration{}) @@ -76,10 +75,10 @@ func TestRegistrySupportedAgentKindsReportsDescriptors(t *testing.T) { if got[0].Kind != "fake_alpha" || got[1].Kind != "fake_beta" { t.Fatalf("SupportedAgentKinds sort = %#v, want fake_alpha then fake_beta", got) } - if !got[0].Available || got[0].Version != "1.2.3" || !got[0].Capabilities.Usage.IsSupported() || !got[0].Capabilities.Resume.IsSupported() { + if !got[0].Available || got[0].Version != "1.2.3" || !got[0].Capabilities.MessageItems.IsSupported() || !got[0].Capabilities.EnvironmentNone.IsSupported() { t.Fatalf("fake_alpha descriptor not preserved: %#v", got[0]) } - if got[1].Available || got[1].Version != "missing" || !got[1].Capabilities.Streaming.IsSupported() { + if got[1].Available || got[1].Version != "missing" || !got[1].Capabilities.MessageItems.IsSupported() { t.Fatalf("fake_beta descriptor not preserved: %#v", got[1]) } } @@ -113,7 +112,6 @@ func TestRegistryRejectsEveryOmittedCapabilityBeforeReplacement(t *testing.T) { original := proto.SupportedAgentKind{Kind: "fixture", Available: true, Capabilities: valid} registry.RegisterKind(original, harnessconfig.Configuration{}) registry.RegisterExecutor("fixture", func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { return nil, nil }) - original.Capabilities.Preparation = proto.CapabilitySupported missing := valid reflect.ValueOf(&missing).Elem().Field(i).Set(reflect.ValueOf(proto.CapabilityUnspecified)) func() { diff --git a/apps/daemon/internal/agenthost/admit_linux_test.go b/apps/daemon/internal/agenthost/admit_linux_test.go index b9bda343a..55f69464b 100644 --- a/apps/daemon/internal/agenthost/admit_linux_test.go +++ b/apps/daemon/internal/agenthost/admit_linux_test.go @@ -204,7 +204,7 @@ func TestRegistryDescribesTheViewPath(t *testing.T) { for _, info := range (&Host{cfg: f.cfg}).Registry(nil).SupportedAgentKinds() { kinds = append(kinds, info.Kind) c, declared := info.Capabilities, info.Kind == "supporting" - if !c.Preparation.IsSupported() || !c.LocalEnvironment.IsSupported() || !c.MCPHTTPTools.IsSupported() || c.EnvironmentNone.IsSupported() != declared || + if !c.LocalEnvironment.IsSupported() || !c.MCPHTTPTools.IsSupported() || c.EnvironmentNone.IsSupported() != declared || c.FunctionTools.IsSupported() != declared || c.FunctionResultImages.IsSupported() != declared || c.ToolSearch.IsSupported() != declared || c.WorkspaceOutputExport.IsSupported() || c.WorkspaceReadPreparation.IsSupported() { t.Errorf("%s: capabilities %+v do not describe the view path", info.Kind, c) diff --git a/apps/daemon/internal/dispatch/capability_admission_test.go b/apps/daemon/internal/dispatch/capability_admission_test.go index 823aa22d3..92c0a596a 100644 --- a/apps/daemon/internal/dispatch/capability_admission_test.go +++ b/apps/daemon/internal/dispatch/capability_admission_test.go @@ -11,44 +11,27 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto/prototest" ) -func TestSteeringUsesAdmittedDeclarationAndDoesNotReplayUnsupportedImplementation(t *testing.T) { - for _, supported := range []bool{false, true} { - t.Run(fmt.Sprint(supported), func(t *testing.T) { - h := newHarness(t) - defer h.router.Shutdown(context.Background()) - var calls atomic.Int32 - factory := func(_ context.Context, _ proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { - return &steeringSession{fakeSession: &fakeSession{out: out, closeOutOnCancel: true}, steer: func(context.Context, proto.PromptSteerPayload) error { - calls.Add(1) - return fmt.Errorf("%w: fixture has no active input", agent.ErrUnsupportedOperation) - }}, nil - } - info := proto.SupportedAgentKind{Kind: "fixture", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilityFromBool(supported)})} - registerSession(h.reg, info, factory) - startRun(t, h.router, h.sender, "fixture", "run") - // A new registration cannot rewrite the already admitted owner's contract. - info.Capabilities.Steering = proto.CapabilityFromBool(!supported) - registerSession(h.reg, info, factory) - for range 2 { - if err := handleSteeringAndWait(t, h, scoped(t, "run", proto.TypePromptSteer, "run", proto.PromptSteerPayload{InputID: "input", Input: proto.TextInput("hello")})); err != nil { - t.Fatal(err) - } - ack := lastSteeringAck(t, h.sender, "run", "input") - code := "unsupported" - if supported { - code = "contract_violation" - } - if ack.Accepted || ack.Written || ack.ErrorCode != code { - t.Fatal(ack) - } - } - want := int32(0) - if supported { - want = 1 - } - if calls.Load() != want { - t.Fatalf("native calls %d, want %d", calls.Load(), want) - } - }) +func TestSteeringDoesNotReplayUnsupportedImplementation(t *testing.T) { + h := newHarness(t) + defer h.router.Shutdown(context.Background()) + var calls atomic.Int32 + factory := func(_ context.Context, _ proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + return &steeringSession{fakeSession: &fakeSession{out: out, closeOutOnCancel: true}, steer: func(context.Context, proto.PromptSteerPayload) error { + calls.Add(1) + return fmt.Errorf("%w: fixture has no active input", agent.ErrUnsupportedOperation) + }}, nil + } + registerSession(h.reg, proto.SupportedAgentKind{Kind: "fixture", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, factory) + startRun(t, h.router, h.sender, "fixture", "run") + for range 2 { + if err := handleSteeringAndWait(t, h, scoped(t, "run", proto.TypePromptSteer, "run", proto.PromptSteerPayload{InputID: "input", Input: proto.TextInput("hello")})); err != nil { + t.Fatal(err) + } + if ack := lastSteeringAck(t, h.sender, "run", "input"); ack.Accepted || ack.Written || ack.ErrorCode != "contract_violation" { + t.Fatal(ack) + } + } + if calls.Load() != 1 { + t.Fatalf("native calls %d, want 1", calls.Load()) } } diff --git a/apps/daemon/internal/dispatch/executor.go b/apps/daemon/internal/dispatch/executor.go index ca4f5087d..1a3f25c40 100644 --- a/apps/daemon/internal/dispatch/executor.go +++ b/apps/daemon/internal/dispatch/executor.go @@ -54,7 +54,7 @@ func (r *Router) handleExecutorPrepare(ctx context.Context, env proto.Envelope, return r.rejectPreparation(env, "resource_unavailable") } factory, err := r.registry.ResolveExecutor(req.AgentKind) - if err != nil || !caps.Preparation.IsSupported() { + if err != nil { return r.rejectPreparation(env, "unsupported_preparation") } environment, code := r.admittedEnvironment(env.Assignment, input.SessionID) diff --git a/apps/daemon/internal/dispatch/executor_cancel_receipt_test.go b/apps/daemon/internal/dispatch/executor_cancel_receipt_test.go index 590d8bc0b..131c9a3fb 100644 --- a/apps/daemon/internal/dispatch/executor_cancel_receipt_test.go +++ b/apps/daemon/internal/dispatch/executor_cancel_receipt_test.go @@ -125,7 +125,7 @@ func TestExecutorCancellationReachesNativeBeforeDurableReceiptJoin(t *testing.T) sender := &receiptCancelSender{recSender: &recSender{}, entered: make(chan struct{}), release: make(chan struct{})} owner := &receiptCancelExecutor{turn: make(chan *receiptCancelTurn, 2), cancelFails: mode == "cancel_failure" || mode == "close_failure_retry", closeFailsFirst: mode == "close_failure_retry", closeEntered: make(chan struct{}), closeRelease: make(chan struct{})} reg := agent.NewRegistry() - reg.RegisterKind(proto.SupportedAgentKind{Kind: "reusable", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, prototest.ModelConfiguration()) + reg.RegisterKind(proto.SupportedAgentKind{Kind: "reusable", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported})}, prototest.ModelConfiguration()) reg.RegisterExecutor("reusable", func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { return owner, nil }) r, err := dispatch.New(dispatch.Config{Registry: reg, Sender: sender, IdleTimeout: time.Hour}) if err != nil { diff --git a/apps/daemon/internal/dispatch/executor_test.go b/apps/daemon/internal/dispatch/executor_test.go index 52941687b..680fd02c5 100644 --- a/apps/daemon/internal/dispatch/executor_test.go +++ b/apps/daemon/internal/dispatch/executor_test.go @@ -86,7 +86,7 @@ func executorRouter(t *testing.T, owner *reusableExecutor, idle time.Duration) ( t.Helper() calls := &atomic.Int32{} reg := agent.NewRegistry() - registerExecutorKind(reg, proto.SupportedAgentKind{Kind: "reusable", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { + registerExecutorKind(reg, proto.SupportedAgentKind{Kind: "reusable", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported})}, func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { calls.Add(1) return owner, nil }) @@ -228,7 +228,7 @@ func TestExecutorPreInputFailureConfirmsCloseBeforeRetrySignal(t *testing.T) { func poolRouter(t *testing.T, factory agent.ExecutorFactory) (*dispatch.Router, *recSender) { t.Helper() registry := agent.NewRegistry() - registry.RegisterKind(proto.SupportedAgentKind{Kind: "reusable", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, prototest.ModelConfiguration()) + registry.RegisterKind(proto.SupportedAgentKind{Kind: "reusable", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported})}, prototest.ModelConfiguration()) registry.RegisterExecutor("reusable", factory) sender := &recSender{} router, err := dispatch.New(dispatch.Config{Registry: registry, Sender: sender, IdleTimeout: time.Minute}) diff --git a/apps/daemon/internal/dispatch/preparation.go b/apps/daemon/internal/dispatch/preparation.go index 60b15d8ea..d8a0db769 100644 --- a/apps/daemon/internal/dispatch/preparation.go +++ b/apps/daemon/internal/dispatch/preparation.go @@ -50,9 +50,6 @@ func (r *Router) handleExecutionPrepare(ctx context.Context, env proto.Envelope) if !available { return r.rejectPreparation(env, "resource_unavailable") } - if !caps.Preparation.IsSupported() { - return r.rejectPreparation(env, "unsupported_preparation") - } // The Session's owner declares read-only preparation for a kind that // supports a local Environment. environment, code := r.admittedEnvironment(env.Assignment, input.SessionID) diff --git a/apps/daemon/internal/dispatch/preparation_test.go b/apps/daemon/internal/dispatch/preparation_test.go index 54109456c..8c8b9d072 100644 --- a/apps/daemon/internal/dispatch/preparation_test.go +++ b/apps/daemon/internal/dispatch/preparation_test.go @@ -121,7 +121,7 @@ func preparationRequest() proto.ExecutionPreparePayload { func preparationRouter(t *testing.T, sender dispatch.Sender, timeout time.Duration, factory preparationFactory) *dispatch.Router { t.Helper() reg := agent.NewRegistry() - reg.RegisterKind(proto.SupportedAgentKind{Kind: "prepared", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported, FunctionTools: proto.CapabilitySupported, Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, prototest.ModelConfiguration()) + reg.RegisterKind(proto.SupportedAgentKind{Kind: "prepared", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported, FunctionTools: proto.CapabilitySupported})}, prototest.ModelConfiguration()) reg.RegisterExecutor("prepared", preparationExecutorFixture(factory)) r, err := dispatch.New(dispatch.Config{Registry: reg, Sender: sender, PreparationTimeout: timeout, Environments: dispatch.LocalEnvironments(preparationWorkspace(t))}) if err != nil { diff --git a/apps/daemon/internal/dispatch/receipt_order_test.go b/apps/daemon/internal/dispatch/receipt_order_test.go index 2250c7c4d..f3eaeeb17 100644 --- a/apps/daemon/internal/dispatch/receipt_order_test.go +++ b/apps/daemon/internal/dispatch/receipt_order_test.go @@ -50,7 +50,7 @@ func TestDurableCompletionWaitsForSteeringReceiptSend(t *testing.T) { registry := agent.NewRegistry() var session *fakeSession var calls atomic.Int32 - registerSession(registry, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, func(_ context.Context, _ proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + registerSession(registry, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, func(_ context.Context, _ proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { session = &fakeSession{out: out, closeOutOnCancel: true} return &steeringSession{fakeSession: session, steer: func(context.Context, proto.PromptSteerPayload) error { calls.Add(1) @@ -125,7 +125,7 @@ func TestShutdownReleasesSteeringWorkerAndReceiptJoin(t *testing.T) { registry := agent.NewRegistry() entered, exited := make(chan struct{}), make(chan struct{}) var session *fakeSession - registerSession(registry, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, func(_ context.Context, _ proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + registerSession(registry, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, func(_ context.Context, _ proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { session = &fakeSession{out: out, closeOutOnCancel: true} return &steeringSession{fakeSession: session, steer: func(ctx context.Context, _ proto.PromptSteerPayload) error { close(entered) diff --git a/apps/daemon/internal/dispatch/receipt_shutdown_test.go b/apps/daemon/internal/dispatch/receipt_shutdown_test.go index 76316068f..f553d7581 100644 --- a/apps/daemon/internal/dispatch/receipt_shutdown_test.go +++ b/apps/daemon/internal/dispatch/receipt_shutdown_test.go @@ -45,7 +45,7 @@ func TestShutdownCancelsCompletionErrorSend(t *testing.T) { sender := &shutdownAllSendsBlockSender{entered: make(chan struct{}), terminal: make(chan context.Context, 1), rescue: make(chan struct{})} registry := agent.NewRegistry() var session *fakeSession - registerSession(registry, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, func(_ context.Context, _ proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + registerSession(registry, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, func(_ context.Context, _ proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { session = &fakeSession{out: out, closeOutOnCancel: true} return &steeringSession{fakeSession: session, steer: func(context.Context, proto.PromptSteerPayload) error { return nil }}, nil }) diff --git a/apps/daemon/internal/dispatch/steering.go b/apps/daemon/internal/dispatch/steering.go index 5a0ab04c3..a60d6b8c7 100644 --- a/apps/daemon/internal/dispatch/steering.go +++ b/apps/daemon/internal/dispatch/steering.go @@ -95,11 +95,6 @@ func (r *Router) queueSteering(ctx context.Context, env proto.Envelope, input pr if state.session == nil { return &ack } - // The admitted declaration is immutable even if discovery changes later. - if input.DurableReceipt && !state.capabilities.DurableInputReceipts.IsSupported() || !input.DurableReceipt && !state.capabilities.Steering.IsSupported() { - ack.ErrorCode, ack.Error = "unsupported", "The runtime declaration does not support this input operation." - return &ack - } session := state.session steerer, supportsSteering := session.(agent.Steerer) if !input.DurableReceipt && !supportsSteering { diff --git a/apps/daemon/internal/dispatch/steering_lifetime_test.go b/apps/daemon/internal/dispatch/steering_lifetime_test.go index 0c4c47115..c30af7c6a 100644 --- a/apps/daemon/internal/dispatch/steering_lifetime_test.go +++ b/apps/daemon/internal/dispatch/steering_lifetime_test.go @@ -26,7 +26,7 @@ func TestDurableSteeringWaitsBeyondTransportDeadline(t *testing.T) { var session *fakeSession var calls atomic.Int32 release := make(chan struct{}) - registerSession(h.reg, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + registerSession(h.reg, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { session = &fakeSession{out: out, closeOutOnCancel: true} return &durableSteeringSession{steeringSession: &steeringSession{fakeSession: session}, phased: func(ctx context.Context, input proto.PromptSteerPayload, written func()) error { calls.Add(1) @@ -77,7 +77,7 @@ func TestDurableSteeringTransportTimeoutAndShutdown(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) exited := make(chan struct{}) - registerSession(h.reg, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, func(_ context.Context, _ proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + registerSession(h.reg, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, func(_ context.Context, _ proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { return &durableSteeringSession{steeringSession: &steeringSession{fakeSession: &fakeSession{out: out, closeOutOnCancel: true}}, phased: func(ctx context.Context, _ proto.PromptSteerPayload, written func()) error { defer close(exited) if phase == "written" { diff --git a/apps/daemon/internal/dispatch/steering_test.go b/apps/daemon/internal/dispatch/steering_test.go index d9c6eef4a..488438abc 100644 --- a/apps/daemon/internal/dispatch/steering_test.go +++ b/apps/daemon/internal/dispatch/steering_test.go @@ -31,7 +31,7 @@ func TestSteeringReceiptsAndRetries(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) calls, starts := 0, 0 - registerSession(h.reg, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + registerSession(h.reg, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { starts++ return &steeringSession{ fakeSession: &fakeSession{out: out, closeOutOnCancel: true}, @@ -86,11 +86,11 @@ func TestSteeringReceiptsAndRetries(t *testing.T) { } } -func TestSteeringReadinessAndUnsupportedRuns(t *testing.T) { +func TestSteeringReadinessAndInputValidation(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) calls := 0 - registerSession(h.reg, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + registerSession(h.reg, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { return &steeringSession{ fakeSession: &fakeSession{out: out, closeOutOnCancel: true}, steer: func(context.Context, proto.PromptSteerPayload) error { @@ -133,13 +133,7 @@ func TestSteeringReadinessAndUnsupportedRuns(t *testing.T) { if ack := lastSteeringAck(t, h.sender, "run-1", "input-1"); ack.ErrorCode != "run_inactive" { t.Fatalf("inactive: %+v", ack) } - startRun(t, h.router, h.sender, "fake_alpha", "run-2") - if err := handleSteeringAndWait(t, h, scoped(t, "run-2", proto.TypePromptSteer, "run-2", input)); err != nil { - t.Fatal(err) - } - if ack := lastSteeringAck(t, h.sender, "run-2", "input-1"); ack.ErrorCode != "unsupported" { - t.Fatalf("unsupported: %+v", ack) - } + startRun(t, h.router, h.sender, "codex", "run-2") input.Input = proto.TextInput("") if err := handleSteeringAndWait(t, h, scoped(t, "run-2", proto.TypePromptSteer, "run-2", input)); err != nil { t.Fatal(err) @@ -152,7 +146,7 @@ func TestSteeringReadinessAndUnsupportedRuns(t *testing.T) { if err := handleSteeringAndWait(t, h, scoped(t, "run-2", proto.TypePromptSteer, "run-2", input)); err != nil { t.Fatal(err) } - if ack := lastSteeringAck(t, h.sender, "run-2", "input-2"); ack.ErrorCode != "unsupported" { + if ack := lastSteeringAck(t, h.sender, "run-2", "input-2"); ack.ErrorCode != "" { t.Fatalf("whitespace: %+v", ack) } } @@ -162,7 +156,7 @@ func TestSteeringDoesNotBlockOtherRunCancellation(t *testing.T) { defer h.router.Shutdown(context.Background()) entered, release := make(chan struct{}), make(chan struct{}) defer close(release) - registerSession(h.reg, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + registerSession(h.reg, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { return &steeringSession{ fakeSession: &fakeSession{out: out, closeOutOnCancel: true}, steer: func(context.Context, proto.PromptSteerPayload) error { @@ -220,7 +214,7 @@ func TestSteeringCapacityPreservesExistingReceipts(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) calls := 0 - registerSession(h.reg, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + registerSession(h.reg, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { return &steeringSession{ fakeSession: &fakeSession{out: out, closeOutOnCancel: true}, steer: func(context.Context, proto.PromptSteerPayload) error { diff --git a/apps/daemon/testdata/onboarding/main.go b/apps/daemon/testdata/onboarding/main.go index 783c5220e..6c19b5e33 100644 --- a/apps/daemon/testdata/onboarding/main.go +++ b/apps/daemon/testdata/onboarding/main.go @@ -163,7 +163,7 @@ func run() error { registry := agent.NewRegistry() h := &harness{history: map[string]string{}} registry.RegisterKind(proto.SupportedAgentKind{Kind: "mcode", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{ - Streaming: proto.CapabilitySupported, Steering: proto.CapabilitySupported, DurableTurns: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported, ExecutionControls: proto.CapabilitySupported, ToolObservations: proto.CapabilitySupported, SubagentControl: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported, + SubagentControl: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported, })}, mcode.Configuration()) registry.RegisterExecutor("mcode", h.prepare) sink := &sender{encoder: json.NewEncoder(os.Stdout)} diff --git a/contracts/agents-api/harness-onboarding.md b/contracts/agents-api/harness-onboarding.md index 25522f1e4..96170206a 100644 --- a/contracts/agents-api/harness-onboarding.md +++ b/contracts/agents-api/harness-onboarding.md @@ -153,14 +153,14 @@ Registration is static and requires a build. Export one `agent.Declaration` from | Order | Method | Registers | | --- | --- | --- | | 1 | `RegisterKind(proto.SupportedAgentKind, harnessconfig.Configuration)` | Kind, availability, version, `AgentKindCapabilities` and the model configuration declaration. It resets the other registrations, so call it first. | -| 2 | `RegisterExecutor(kind, agent.ExecutorFactory)` | The Executor and Turn lifecycle used for execution; derives the `Preparation` capability | +| 2 | `RegisterExecutor(kind, agent.ExecutorFactory)` | The Executor and Turn lifecycle used for execution | | 3 | `RegisterView(kind, agent.View)` | Optional: the agent-host view declaration from `Runtime.View`. It panics with `ErrInvalidView` when `View.Validate` fails. Its Executor factory validates the model configuration like `RegisterExecutor` and enforces the [gateway rule](#endpoints-and-proxy). | `Runtime.View` declares how the Harness runs in an agent-host Session view, described in [Run in an agent-host view](#run-in-an-agent-host-view). Every adapter sets it explicitly; `View: nil` means the agent host rejects the kind, and `Registry.ResolveView` returns an error wrapping `ErrUnsupportedOperation`. `TestPublicHarnessContractDeclarations` requires the field in each declaration. Every `proto.AgentKindCapabilities` field must be explicitly `proto.CapabilitySupported` or `proto.CapabilityUnsupported`, even for an unavailable Harness. `proto.CapabilityUnspecified` is invalid: zero values and omitted fields never mean Unsupported. An installation probe may set an individual field with `proto.CapabilityFromBool`; it must not populate unmentioned or future fields. Availability stays separate in `SupportedAgentKind.Available`. Registration validates the complete declaration before changing the registry, and the wire carries an explicit boolean for every field, so omitted and null fields are invalid. A new field requires a decision in every production declaration. Runtime consumers use `IsSupported()` and reject unsupported requests before native operations; an interface assertion verifies implementation, never support. Every declaration must match the behavior verified for that installation; the [Core–Runtime protocol](../../docs/runtime-protocol.md#capability-declarations) owns how declarations travel and are frozen. -The admission mapping is explicit. `Steering` controls non-durable `Steerer` input. `DurableInputReceipts` controls `DurableSteerer` input and also requires the Turn settlement contract; neither implies the other, and Core's public text profile requires both. `WorkspaceReadPreparation` admits `execution_prepare` with `workspace_read_only`, which the Environment owner readies and serves without calling the Executor factory. Runtime registration does not grant Core qualification; the service profile does. +Every available Harness implements, without a declaration, the shared Turn lifecycle, including `DurableSteerer` input and the Turn settlement contract that `contracttest.TextLifecycle` checks, typed `execution_controls` and tool observations. A Harness that cannot meet them on a platform reports `Available` false there. `WorkspaceReadPreparation` admits `execution_prepare` with `workspace_read_only`, which the Environment owner readies and serves without calling the Executor factory. Runtime registration does not grant Core qualification; the service profile does. The runnable test-only example [`testdata/onboarding/main.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/testdata/onboarding/main.go) registers a text-only synthetic Harness under the `mcode` kind, because Core admits only [catalog](./harness-catalog.md) Harnesses. It shows a Session-owned Executor, fresh Turns, durable steering, cancellation and history binding, and is never shipped. diff --git a/contracts/agents-api/zh/harness-onboarding.md b/contracts/agents-api/zh/harness-onboarding.md index 28b3fc293..608b3c734 100644 --- a/contracts/agents-api/zh/harness-onboarding.md +++ b/contracts/agents-api/zh/harness-onboarding.md @@ -1,7 +1,7 @@ --- title: "将原生 Harness 添加到 OpenAgentCore" source: contracts/agents-api/harness-onboarding.md -source_hash: 146f98f62e43880478349005b7dcb8dd91d03f26579b38ef3f025f158b722a1c +source_hash: 749d48c7bcdf5dfbf2f01d21b4524ff8c7fd45943252d0ce5f2ca5ba9efae8eb --- **Harness** 是一种运行模型和工具循环的原生代理引擎(Codex、Claude Code、MiniMax Code)。**Harness 适配器**将 Runtime 的 Executor 和 Turn 契约转换到该引擎的 SDK 或协议。本文档定义 Runtime–Harness 协议:适配器接口及其生命周期义务、注册、Core 资格认定和验收。[Harness capabilities](harness-capabilities.md) 记录了当前每个 Harness 支持的功能。 @@ -155,14 +155,14 @@ MCP、公共函数、延迟函数发现、结构化输出、图像输入、详 | 顺序 | 方法 | 注册内容 | | --- | --- | --- | | 1 | `RegisterKind(proto.SupportedAgentKind, harnessconfig.Configuration)` | Kind、可用性、版本、`AgentKindCapabilities` 和模型配置声明。它会重置其他注册项,因此必须首先调用。 | -| 2 | `RegisterExecutor(kind, agent.ExecutorFactory)` | 执行所用的 Executor 和 Turn 生命周期;据此派生 `Preparation` 能力 | +| 2 | `RegisterExecutor(kind, agent.ExecutorFactory)` | 执行所用的 Executor 和 Turn 生命周期 | | 3 | `RegisterView(kind, agent.View)` | 可选:来自 `Runtime.View` 的 agent-host 视图声明。`View.Validate` 失败时以 `ErrInvalidView` panic。其 Executor 工厂像 `RegisterExecutor` 一样验证模型配置,并执行[网关规则](#endpoints-and-proxy)。 | `Runtime.View` 声明 Harness 如何在 agent-host Session 视图中运行,详见[在 agent-host 视图中运行](#run-in-an-agent-host-view)。每个适配器都显式设置它;`View: nil` 表示 agent host 拒绝该 kind,`Registry.ResolveView` 返回包装 `ErrUnsupportedOperation` 的错误。`TestPublicHarnessContractDeclarations` 要求每个声明都包含该字段。 每个 `proto.AgentKindCapabilities` 字段都必须显式设为 `proto.CapabilitySupported` 或 `proto.CapabilityUnsupported`,即使 Harness 不可用也是如此。`proto.CapabilityUnspecified` 无效:零值和省略字段绝不表示 Unsupported。安装探测可以使用 `proto.CapabilityFromBool` 设置单个字段;但不得填充未提及字段或未来字段。可用性通过 `SupportedAgentKind.Available` 单独表示。注册会在更改 registry 之前验证完整声明;线协议会为每个字段携带显式布尔值,因此省略字段和 null 字段均无效。添加新字段时,每个生产声明都必须作出决定。Runtime 使用者应调用 `IsSupported()`,并在原生操作前拒绝不受支持的请求;接口断言用于验证实现,绝不表示支持。每个声明都必须与针对该安装验证的行为一致;[Core–Runtime protocol](../../../docs/zh/runtime-protocol.md#capability-declarations) 负责声明的传输方式和冻结方式。 -准入映射是显式的。`Steering` 控制非持久化 `Steerer` 输入。`DurableInputReceipts` 控制 `DurableSteerer` 输入,并且还要求 Turn 结算契约;二者互不隐含,而且 Core 的公共文本 profile 要求同时具备二者。`WorkspaceReadPreparation` 准入带 `workspace_read_only` 的 `execution_prepare`,由 Environment owner 就绪并提供读取,不调用 Executor 工厂。Runtime 注册不会授予 Core 资格;服务 profile 才会授予。 +每个可用 Harness 都无需声明即实现共享 Turn 生命周期(包括 `DurableSteerer` 输入和由 `contracttest.TextLifecycle` 检查的 Turn 结算契约)、类型化的 `execution_controls` 和工具观测。在某个平台上无法满足这些要求的 Harness 在该平台报告 `Available` 为 false。`WorkspaceReadPreparation` 准入带 `workspace_read_only` 的 `execution_prepare`,由 Environment owner 就绪并提供读取,不调用 Executor 工厂。Runtime 注册不会授予 Core 资格;服务 profile 才会授予。 可运行的仅测试示例 [`testdata/onboarding/main.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/testdata/onboarding/main.go) 会以 `mcode` 类型注册一个仅支持文本的合成 Harness,因为 Core 只接纳[目录](harness-catalog.md)中的 Harness。它展示 Session 所有的 Executor、全新的 Turn、持久化引导、取消和历史绑定,并且绝不会发布。 diff --git a/docs/getting-started/self-hosted.md b/docs/getting-started/self-hosted.md index e5b8382a0..a2936f3fc 100644 --- a/docs/getting-started/self-hosted.md +++ b/docs/getting-started/self-hosted.md @@ -14,7 +14,7 @@ The Session's model provider resolves as for any other Session, so the installat | --- | --- | --- | --- | | Linux amd64 | Supported | Supported | Supported | | macOS arm64 | Supported | Supported | Supported | -| Windows amd64 | Supported | Supported | Not supported | +| Windows amd64 | Not supported | Supported | Not supported | The installer brings its own pinned Node.js and Harness versions (listed in [`scripts/build-native-installer.mjs`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/scripts/build-native-installer.mjs)) and leaves other installations of those tools untouched. On a platform without a matching installer, the command fails. diff --git a/docs/runtime-protocol.md b/docs/runtime-protocol.md index 48dbef51f..9dc8cd249 100644 --- a/docs/runtime-protocol.md +++ b/docs/runtime-protocol.md @@ -30,13 +30,12 @@ On the wire each field is a JSON boolean, and every field is present, including Each admitted Executor and Turn keeps the declaration it was admitted with. A later heartbeat cannot add operations to an existing owner. Optional operations check this snapshot before any native call; the presence of a Go interface never grants support. A declared operation that returns `agent.ErrUnsupportedOperation` is a contract violation, distinct from unavailability, a failed native call or an uncertain write. Uncertain operations keep their receipts and ownership and are never replayed automatically. A Runtime declares `workspace_read_preparation` and `workspace_output_export` from its [Environment owner](#session-assignments), never from a Harness adapter. -A new field requires an explicit decision in every production declaration. Contract tests enumerate every field for registration, wire round trips and the persisted boolean projection; the shared test fixture lists fields individually and supplies no defaults for future ones. [Harness onboarding](../contracts/agents-api/harness-onboarding.md) owns the adapter side of each declaration. +A new field requires an explicit decision in every production declaration. Contract tests enumerate every field for registration and wire round trips; the shared test fixture lists fields individually and supplies no defaults for future ones. [Harness onboarding](../contracts/agents-api/harness-onboarding.md) owns the adapter side of each declaration. -A declaration describes what the Runtime can do. Core admits a public feature only when the Harness's engine profile also qualifies it, and checks the declaration of the selected device during device selection and again at the final check before it claims a Turn: +A declaration describes what the Runtime can do. Core admits a public feature only when the Harness's engine profile also qualifies it. During device selection and again at the final check before it claims a Turn, Core requires the selected device to report the Harness `available` and checks its declaration: | Capability | Core requires it when | | --- | --- | -| `streaming`, `steering`, `durable_turns`, `durable_input_receipts`, `preparation`, `execution_controls`, `tool_observations` | Always, for every execution on that Harness (with `available` true) | | `environment_none` | The Environment type is `none` | | `local_environment`, `workspace_read_preparation`, `workspace_output_export` | The Environment type is `openai_hosted` or `self_hosted` | | `workspace_read_preparation` | An idle Files directory read needs a read-only preparation | @@ -51,7 +50,7 @@ A declaration describes what the Runtime can do. Core admits a public feature on | `message_images`, `function_result_images` | A message, or a function result, carries an image | | `mcp_http_tools`, `mcp_http_required`, `mcp_http_bearer_auth` | The Agent declares HTTP MCP servers; one is `required`; a Vault credential is selected for one | -Core has no admission rule for `usage` and `resume`. +Streaming, steering with durable input receipts, durable Turns, preparation, execution controls and tool observations are not declared: every available Harness implements them, and [Harness onboarding](../contracts/agents-api/harness-onboarding.md#register-the-adapter) owns that obligation. The `execution_prepare` configuration carries the Session's model configuration and the opt-ins Core sets for each Run: diff --git a/docs/zh/getting-started/self-hosted.md b/docs/zh/getting-started/self-hosted.md index 055fcb883..df6b1af3f 100644 --- a/docs/zh/getting-started/self-hosted.md +++ b/docs/zh/getting-started/self-hosted.md @@ -1,7 +1,7 @@ --- title: "自托管执行器" source: docs/getting-started/self-hosted.md -source_hash: 19506e4bab34b802e5c3c8a1818bcb5c8397aa91fdf9ace512b5aaeb99341b3a +source_hash: 6f39aff3240eec960d278d7b21665b105ce14337517eb8961424ec141bfcfbe5 --- `self_hosted` Session 在应用拥有的机器上运行:工作站、虚拟机或你管理的沙箱。应用通过 `/v1` 创建 Session,并获得安装 `oac-daemon`、启动它并连接 Core 的命令。Web 在 Session 页面展示同一命令;Web 是可选的。Core 不创建、停止或回收这台机器。 @@ -16,7 +16,7 @@ Session 的模型提供商与其他 Session 一样解析,因此当 Session 及 | --- | --- | --- | --- | | Linux amd64 | 支持 | 支持 | 支持 | | macOS arm64 | 支持 | 支持 | 支持 | -| Windows amd64 | 支持 | 支持 | 不支持 | +| Windows amd64 | 不支持 | 支持 | 不支持 | 安装程序自带固定版本的 Node.js 和 Harness(列于 [`scripts/build-native-installer.mjs`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/scripts/build-native-installer.mjs)),不修改这些工具的其他安装。在没有匹配安装程序的平台上,命令会失败。 diff --git a/docs/zh/runtime-protocol.md b/docs/zh/runtime-protocol.md index d27f71134..940f456da 100644 --- a/docs/zh/runtime-protocol.md +++ b/docs/zh/runtime-protocol.md @@ -1,7 +1,7 @@ --- title: "Core–Runtime 协议" source: docs/runtime-protocol.md -source_hash: 58f0ccc44daf024d3f8079e0f09b78dfefb2e55e558f5663c2add01ee153061a +source_hash: 667a4f20a030e5af7787a4240c14dc648ec622c3de01c7f965813fbefac55f6d --- 此协议在 Runtime daemon 获取机器凭据后连接 Core 与 daemon,定义 daemon 连接上消息的含义和顺序。wire 类型、限制和验证器仅在 [`internal/agentdaemon/proto`](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/internal/agentdaemon/proto) 中定义一次;Core 的 [gateway](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/services/core/internal/runtimegateway) 与参考 Runtime 的 [dispatcher](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/apps/daemon/internal/dispatch) 都使用它们,因此无需同步第二套 payload schema。签发凭据和打开连接的 HTTP 路由见[机器连接 API](../../contracts/agents-api/zh/machine-api.md)。 @@ -32,13 +32,12 @@ wire 上每个字段都是 JSON boolean,所有字段都必须出现,包括 ` 每个已准入的 Executor 和 Turn 保留准入时的声明。后续 heartbeat 不能给已有 owner 增加操作。可选操作在任何原生调用前检查此快照;存在 Go interface 不代表支持。已声明操作返回 `agent.ErrUnsupportedOperation` 属于契约违规,与不可用、原生调用失败或不确定写入不同。不确定操作保留回执与所有权,绝不自动重放。Runtime 根据其 [Environment owner](#session-assignments) 声明 `workspace_read_preparation` 和 `workspace_output_export`,从不由 Harness adapter 声明。 -新增字段要求每个生产声明都作出明确决定。契约测试为注册、wire 往返和持久化 boolean 投影逐一枚举字段;共享测试 fixture 单独列出字段,不为未来字段提供默认值。[Harness 接入](../../contracts/agents-api/zh/harness-onboarding.md)负责各声明的 adapter 侧规则。 +新增字段要求每个生产声明都作出明确决定。契约测试为注册和 wire 往返逐一枚举字段;共享测试 fixture 单独列出字段,不为未来字段提供默认值。[Harness 接入](../../contracts/agents-api/zh/harness-onboarding.md)负责各声明的 adapter 侧规则。 -声明描述 Runtime 能做什么。Core 仅在 Harness 的 engine profile 也通过资格验证时准入公开功能,并在设备选择及领取 Turn 前的最终检查中检查选定设备的声明: +声明描述 Runtime 能做什么。Core 仅在 Harness 的 engine profile 也通过资格验证时准入公开功能。在设备选择及领取 Turn 前的最终检查中,Core 要求选定设备报告该 Harness 为 `available`,并检查其声明: | 能力 | Core 何时要求 | | --- | --- | -| `streaming`, `steering`, `durable_turns`, `durable_input_receipts`, `preparation`, `execution_controls`, `tool_observations` | 始终要求,适用于该 Harness 上每次执行(`available` 为 true) | | `environment_none` | Environment 类型为 `none` | | `local_environment`, `workspace_read_preparation`, `workspace_output_export` | Environment 类型为 `openai_hosted` 或 `self_hosted` | | `workspace_read_preparation` | 空闲 Files 目录读取需要只读 preparation | @@ -53,7 +52,7 @@ wire 上每个字段都是 JSON boolean,所有字段都必须出现,包括 ` | `message_images`, `function_result_images` | 消息或 function result 携带图像 | | `mcp_http_tools`, `mcp_http_required`, `mcp_http_bearer_auth` | Agent 声明 HTTP MCP server;其中一个为 `required`;其中一个选用了 Vault 凭据 | -Core 对 `usage` 和 `resume` 没有准入规则。 +流式输出、带持久输入回执的 steering、持久 Turn、preparation、执行控制和工具观测不作声明:每个可用 Harness 都实现它们,该义务由 [Harness 接入](../../contracts/agents-api/zh/harness-onboarding.md#register-the-adapter)负责。 `execution_prepare` 的配置携带 Session 的模型配置和 Core 为各 Run 设置的显式启用项: diff --git a/internal/agentdaemon/proto/inbound.go b/internal/agentdaemon/proto/inbound.go index cacf1e9c8..978bd5d59 100644 --- a/internal/agentdaemon/proto/inbound.go +++ b/internal/agentdaemon/proto/inbound.go @@ -150,40 +150,30 @@ const ( // AgentKindCapabilities describes what a daemon-side agent_kind can // do inside one prompt session. Every field requires an explicit support -// decision, including for unavailable engines. Runtime lifecycle requirements -// are mandatory independently of these optional operations. +// decision, including for unavailable engines. The Runtime lifecycle +// (streaming, durable Turns and input receipts, preparation) is mandatory for +// every Harness and is not declared. type AgentKindCapabilities struct { SubagentObservations CapabilitySupport `json:"subagent_observations"` - Streaming CapabilitySupport `json:"streaming"` - Usage CapabilitySupport `json:"usage"` - Resume CapabilitySupport `json:"resume"` NativeSessionRecovery CapabilitySupport `json:"native_session_recovery"` - Steering CapabilitySupport `json:"steering"` MessageItems CapabilitySupport `json:"message_items"` - ToolObservations CapabilitySupport `json:"tool_observations"` EnvironmentNone CapabilitySupport `json:"environment_none"` LocalEnvironment CapabilitySupport `json:"local_environment"` - Preparation CapabilitySupport `json:"preparation"` WorkspaceReadPreparation CapabilitySupport `json:"workspace_read_preparation"` WorkspaceOutputExport CapabilitySupport `json:"workspace_output_export"` ProgrammaticToolCallingDisable CapabilitySupport `json:"programmatic_tool_calling_disable"` WebSearchControl CapabilitySupport `json:"web_search_control"` - // ExecutionControls supports typed search and verbosity controls. - ExecutionControls CapabilitySupport `json:"execution_controls"` - TextVerbosity CapabilitySupport `json:"text_verbosity"` - StructuredOutput CapabilitySupport `json:"structured_output"` - ToolSearch CapabilitySupport `json:"tool_search"` - MessageImages CapabilitySupport `json:"message_images"` - FunctionResultImages CapabilitySupport `json:"function_result_images"` - SubagentControl CapabilitySupport `json:"subagent_control"` - DurableInputReceipts CapabilitySupport `json:"durable_input_receipts"` - // DurableTurns includes strict resume, completion release and cancellation snapshots. - DurableTurns CapabilitySupport `json:"durable_turns"` - FunctionTools CapabilitySupport `json:"function_tools"` - MCPHTTPTools CapabilitySupport `json:"mcp_http_tools"` - MCPHTTPRequired CapabilitySupport `json:"mcp_http_required"` - MCPHTTPBearerAuth CapabilitySupport `json:"mcp_http_bearer_auth"` + TextVerbosity CapabilitySupport `json:"text_verbosity"` + StructuredOutput CapabilitySupport `json:"structured_output"` + ToolSearch CapabilitySupport `json:"tool_search"` + MessageImages CapabilitySupport `json:"message_images"` + FunctionResultImages CapabilitySupport `json:"function_result_images"` + SubagentControl CapabilitySupport `json:"subagent_control"` + FunctionTools CapabilitySupport `json:"function_tools"` + MCPHTTPTools CapabilitySupport `json:"mcp_http_tools"` + MCPHTTPRequired CapabilitySupport `json:"mcp_http_required"` + MCPHTTPBearerAuth CapabilitySupport `json:"mcp_http_bearer_auth"` } // SupportedAgentKind is one daemon-advertised agent engine. Daemons diff --git a/internal/agentdaemon/proto/prototest/capabilities.go b/internal/agentdaemon/proto/prototest/capabilities.go index 82afd39b5..9bf034797 100644 --- a/internal/agentdaemon/proto/prototest/capabilities.go +++ b/internal/agentdaemon/proto/prototest/capabilities.go @@ -13,29 +13,20 @@ import ( func Capabilities(overrides proto.AgentKindCapabilities) proto.AgentKindCapabilities { c := proto.AgentKindCapabilities{ SubagentObservations: proto.CapabilityUnsupported, - Streaming: proto.CapabilityUnsupported, - Usage: proto.CapabilityUnsupported, - Resume: proto.CapabilityUnsupported, NativeSessionRecovery: proto.CapabilityUnsupported, - Steering: proto.CapabilityUnsupported, MessageItems: proto.CapabilityUnsupported, - ToolObservations: proto.CapabilityUnsupported, EnvironmentNone: proto.CapabilityUnsupported, LocalEnvironment: proto.CapabilityUnsupported, - Preparation: proto.CapabilityUnsupported, WorkspaceReadPreparation: proto.CapabilityUnsupported, WorkspaceOutputExport: proto.CapabilityUnsupported, ProgrammaticToolCallingDisable: proto.CapabilityUnsupported, WebSearchControl: proto.CapabilityUnsupported, - ExecutionControls: proto.CapabilityUnsupported, TextVerbosity: proto.CapabilityUnsupported, StructuredOutput: proto.CapabilityUnsupported, ToolSearch: proto.CapabilityUnsupported, MessageImages: proto.CapabilityUnsupported, FunctionResultImages: proto.CapabilityUnsupported, SubagentControl: proto.CapabilityUnsupported, - DurableInputReceipts: proto.CapabilityUnsupported, - DurableTurns: proto.CapabilityUnsupported, FunctionTools: proto.CapabilityUnsupported, MCPHTTPTools: proto.CapabilityUnsupported, MCPHTTPRequired: proto.CapabilityUnsupported, diff --git a/services/core/internal/execution/disabled_tools_test.go b/services/core/internal/execution/disabled_tools_test.go index e5ca26517..3af6fd349 100644 --- a/services/core/internal/execution/disabled_tools_test.go +++ b/services/core/internal/execution/disabled_tools_test.go @@ -8,7 +8,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine/enginetest" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) @@ -38,7 +37,7 @@ func TestDisabledToolRequestPreservesIntentOnResume(t *testing.T) { } before, _ := json.Marshal(snapshot) for _, nativeID := range []string{"", "native-session"} { - request, err := (&Dispatcher{SessionsReader: frozenProvider{engine: "codex"}}).executionRequest(t.Context(), sessions.Session{ID: "session", Engine: "codex"}, snapshot, runtimedevice.KindCapabilities{}, sessions.ExecutionBinding{NativeSessionID: nativeID}) + request, err := (&Dispatcher{SessionsReader: frozenProvider{engine: "codex"}}).executionRequest(t.Context(), sessions.Session{ID: "session", Engine: "codex"}, snapshot, proto.AgentKindCapabilities{}, sessions.ExecutionBinding{NativeSessionID: nativeID}) if err != nil || request.ExecutionControls.DisableProgrammaticToolCalling != disabled || request.ExecutionControls.WebSearch != "disabled" || request.AgentSessionID != nativeID { t.Fatal(request, err) } @@ -68,7 +67,7 @@ func TestEnabledWebSearchNeverReachesDispatch(t *testing.T) { t.Fatal(tool, err) } snapshot := Snapshot{Agent: v1.Agent{Model: "model", Tools: tools}} - if _, err := (&Dispatcher{}).executionRequest(t.Context(), sessions.Session{ID: "session"}, snapshot, runtimedevice.KindCapabilities{}, sessions.ExecutionBinding{}); err == nil { + if _, err := (&Dispatcher{}).executionRequest(t.Context(), sessions.Session{ID: "session"}, snapshot, proto.AgentKindCapabilities{}, sessions.ExecutionBinding{}); err == nil { t.Fatal("dispatch request built for", tool) } raw := json.RawMessage(`{"agent":{"model":"model","tools":[` + tool + `]},"environment":{"type":"none"}}`) diff --git a/services/core/internal/execution/environment_directory.go b/services/core/internal/execution/environment_directory.go index a189005dc..d8d4d35ff 100644 --- a/services/core/internal/execution/environment_directory.go +++ b/services/core/internal/execution/environment_directory.go @@ -132,8 +132,8 @@ func (w *Worker) directoryDeviceReady(ctx context.Context, id, engine string, pl return false } info, found, known := peer.AgentKindStatus(engine) - placementReady := info.Capabilities.LocalEnvironment - return known && found && info.Available && placementReady && (!prepare || (info.Capabilities.Preparation && info.Capabilities.WorkspaceReadPreparation)) + placementReady := info.Capabilities.LocalEnvironment.IsSupported() + return known && found && info.Available && placementReady && (!prepare || info.Capabilities.WorkspaceReadPreparation.IsSupported()) } func readEnvironmentDirectory(ctx context.Context, peer *runtimegateway.Session, ref proto.AssignmentRef, request proto.WorkspaceReadPayload) directoryReadResult { diff --git a/services/core/internal/execution/environment_file_write.go b/services/core/internal/execution/environment_file_write.go index 6967eff8a..5f3e2b91f 100644 --- a/services/core/internal/execution/environment_file_write.go +++ b/services/core/internal/execution/environment_file_write.go @@ -98,7 +98,7 @@ func (w *Worker) runFileWrite(owner context.Context, request fileWriteRequest) f return unavailable } info, found, known := peer.AgentKindStatus(session.Engine) - if !known || !found || !info.Available || !info.Capabilities.LocalEnvironment { + if !known || !found || !info.Available || !info.Capabilities.LocalEnvironment.IsSupported() { return unavailable } dataDigest := sha256.Sum256(request.data) diff --git a/services/core/internal/execution/functions.go b/services/core/internal/execution/functions.go index b784c47b0..ab8429cb9 100644 --- a/services/core/internal/execution/functions.go +++ b/services/core/internal/execution/functions.go @@ -192,7 +192,7 @@ func requireFunctionResultImages(peer *runtimegateway.Session, kind string, resu return nil } info, found, known := peer.AgentKindStatus(kind) - if !found || !known || !info.Available || !info.Capabilities.FunctionResultImages { + if !found || !known || !info.Available || !info.Capabilities.FunctionResultImages.IsSupported() { return errors.New("Runtime does not support function result images") } return nil diff --git a/services/core/internal/execution/mcp_support.go b/services/core/internal/execution/mcp_support.go index bc36584f5..b59f623e2 100644 --- a/services/core/internal/execution/mcp_support.go +++ b/services/core/internal/execution/mcp_support.go @@ -4,7 +4,6 @@ import ( "errors" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/vaults" ) @@ -22,7 +21,7 @@ func (p Policy) mcpCredentialBindings(engine string, snapshot Snapshot) (map[str // Selection, final preclaim and request construction use the same combination // checks. This function never reads plaintext credentials or native configuration. -func (p Policy) mcpExecutionCredentials(engine string, snapshot Snapshot, servers []proto.MCPHTTPServer, caps runtimedevice.KindCapabilities) (map[string]vaults.MCPCredentialBinding, error) { +func (p Policy) mcpExecutionCredentials(engine string, snapshot Snapshot, servers []proto.MCPHTTPServer, caps proto.AgentKindCapabilities) (map[string]vaults.MCPCredentialBinding, error) { fail := func(message string) (map[string]vaults.MCPCredentialBinding, error) { return nil, errors.New(message) } @@ -30,7 +29,7 @@ func (p Policy) mcpExecutionCredentials(engine string, snapshot Snapshot, server if err := profile.ValidateMCPOrigins(snapshot.Environment, servers); err != nil { return nil, err } - if len(servers) > 0 && !caps.MCPHTTPTools { + if len(servers) > 0 && !caps.MCPHTTPTools.IsSupported() { return fail("device must advertise mcp_http_tools") } selected, err := p.mcpCredentialBindings(engine, snapshot) @@ -38,14 +37,14 @@ func (p Policy) mcpExecutionCredentials(engine string, snapshot Snapshot, server return nil, err } for _, server := range servers { - if server.Required && !caps.MCPHTTPRequired { + if server.Required && !caps.MCPHTTPRequired.IsSupported() { return fail("device must advertise mcp_http_required") } } - if len(selected) > 0 && !caps.MCPHTTPBearerAuth { + if len(selected) > 0 && !caps.MCPHTTPBearerAuth.IsSupported() { return fail("device must advertise mcp_http_bearer_auth") } - if len(servers) > 0 && snapshot.Environment.Type == "none" && !caps.EnvironmentNone { + if len(servers) > 0 && snapshot.Environment.Type == "none" && !caps.EnvironmentNone.IsSupported() { return fail("device must advertise environment_none") } return selected, nil diff --git a/services/core/internal/execution/mcp_support_test.go b/services/core/internal/execution/mcp_support_test.go index 27a4032ae..176ae5930 100644 --- a/services/core/internal/execution/mcp_support_test.go +++ b/services/core/internal/execution/mcp_support_test.go @@ -8,7 +8,6 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/vaults" "github.com/google/uuid" @@ -25,7 +24,7 @@ func (c *recordingCredentials) MCPBearerToken(_ context.Context, command vaults. return c.token, nil } -func mcpSupportFixture(t *testing.T) (Snapshot, []proto.MCPHTTPServer, runtimedevice.KindCapabilities) { +func mcpSupportFixture(t *testing.T) (Snapshot, []proto.MCPHTTPServer, proto.AgentKindCapabilities) { t.Helper() vault, credential := uuid.NewString(), uuid.NewString() tool := json.RawMessage(`{"type":"mcp","server_label":"tickets","connection_origin":"service","transport":{"type":"http","server_url":"https://mcp.example/tools"}}`) @@ -35,8 +34,7 @@ func mcpSupportFixture(t *testing.T) (Snapshot, []proto.MCPHTTPServer, runtimede if err != nil { t.Fatal(err) } - caps := runtimedevice.KindCapabilities{EnvironmentNone: true, MCPHTTPTools: true, MCPHTTPBearerAuth: true, MCPHTTPRequired: true, - Preparation: true} + caps := proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported, MCPHTTPTools: proto.CapabilitySupported, MCPHTTPBearerAuth: proto.CapabilitySupported, MCPHTTPRequired: proto.CapabilitySupported} return snapshot, tools.MCP, caps } @@ -78,7 +76,7 @@ func TestMCPPublicBearerPolicyIsIndependentOfRuntimeCapabilities(t *testing.T) { func TestMCPExecutionChecksRequireVerifiedCapabilityCombinations(t *testing.T) { for _, placement := range []string{"none", "self_hosted"} { - for _, missing := range []string{"", "mcp", "bearer", "placement", "required", "preparation", "environment"} { + for _, missing := range []string{"", "mcp", "bearer", "placement", "required", "environment"} { t.Run(placement+"/"+missing, func(t *testing.T) { snapshot, servers, caps := mcpSupportFixture(t) snapshot.Environment.Type = placement @@ -92,19 +90,17 @@ func TestMCPExecutionChecksRequireVerifiedCapabilityCombinations(t *testing.T) { snapshot.Agent.Tools[0], _ = json.Marshal(tool) switch missing { case "mcp": - caps.MCPHTTPTools = false + caps.MCPHTTPTools = proto.CapabilityUnsupported case "bearer": - caps.MCPHTTPBearerAuth = false + caps.MCPHTTPBearerAuth = proto.CapabilityUnsupported case "placement": - caps.EnvironmentNone = false + caps.EnvironmentNone = proto.CapabilityUnsupported case "required": - caps.MCPHTTPRequired = false - case "preparation": - caps.Preparation = false + caps.MCPHTTPRequired = proto.CapabilityUnsupported case "environment": snapshot.Environment = nil } - allowed := placement == "none" && (missing == "" || missing == "preparation") + allowed := placement == "none" && missing == "" selected, err := (Policy{}).mcpExecutionCredentials("codex", snapshot, servers, caps) if (err == nil) != allowed || allowed && len(selected) != 1 { t.Fatal("incorrect combined MCP capability decision", err) @@ -126,7 +122,7 @@ func TestMCPAnonymousExecutionPreservesFrozenDecision(t *testing.T) { for _, mode := range []string{"unattached", "frozen anonymous", "invalid binding"} { t.Run(engine+"/"+mode, func(t *testing.T) { snapshot, _, caps := mcpSupportFixture(t) - caps.MCPHTTPBearerAuth = false + caps.MCPHTTPBearerAuth = proto.CapabilityUnsupported if mode == "unattached" { snapshot.VaultIDs, snapshot.MCPCredentials = nil, nil } else { diff --git a/services/core/internal/execution/message_support.go b/services/core/internal/execution/message_support.go index 39f1a4526..fbf7883b3 100644 --- a/services/core/internal/execution/message_support.go +++ b/services/core/internal/execution/message_support.go @@ -80,7 +80,7 @@ func requireMessageImages(peer *runtimegateway.Session, kind string, input proto return nil } info, found, known := peer.AgentKindStatus(kind) - if !found || !known || !info.Available || !info.Capabilities.MessageImages { + if !found || !known || !info.Available || !info.Capabilities.MessageImages.IsSupported() { return errors.New("Runtime does not support message images") } return nil diff --git a/services/core/internal/execution/model_execution_test.go b/services/core/internal/execution/model_execution_test.go index ec08ee9a0..e33580b83 100644 --- a/services/core/internal/execution/model_execution_test.go +++ b/services/core/internal/execution/model_execution_test.go @@ -9,9 +9,9 @@ import ( "github.com/google/uuid" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) @@ -33,13 +33,13 @@ func TestSessionModelExecutionNeverFallsBack(t *testing.T) { reader, _ := testSessions(t, pgtest.Open(t), nil) d := Dispatcher{SessionsReader: reader} session := sessions.Session{TenantID: uuid.NewString(), ID: uuid.NewString(), Engine: "codex"} - if _, err := d.executionRequest(t.Context(), session, Snapshot{ModelProviderConfigured: true}, runtimedevice.KindCapabilities{}, sessions.ExecutionBinding{}); !errors.Is(err, sessions.ErrNotFound) { + if _, err := d.executionRequest(t.Context(), session, Snapshot{ModelProviderConfigured: true}, proto.AgentKindCapabilities{}, sessions.ExecutionBinding{}); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("missing Session credentials fell back", err) } // No Runtime has model configuration of its own. for _, environment := range []string{"openai_hosted", "self_hosted", "none"} { snapshot := Snapshot{Agent: v1.Agent{Model: "m"}, Environment: &v1.Environment{Type: environment}} - if _, err := d.executionRequest(t.Context(), sessions.Session{Engine: "codex"}, snapshot, runtimedevice.KindCapabilities{}, sessions.ExecutionBinding{}); !errors.Is(err, ErrModelProviderRequired) { + if _, err := d.executionRequest(t.Context(), sessions.Session{Engine: "codex"}, snapshot, proto.AgentKindCapabilities{}, sessions.ExecutionBinding{}); !errors.Is(err, ErrModelProviderRequired) { t.Fatal("provider-free Session dispatched", environment, err) } } diff --git a/services/core/internal/execution/recovery_test.go b/services/core/internal/execution/recovery_test.go index 91065c4ca..d456ea4ef 100644 --- a/services/core/internal/execution/recovery_test.go +++ b/services/core/internal/execution/recovery_test.go @@ -3,7 +3,7 @@ package execution import ( "testing" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) @@ -13,7 +13,7 @@ func TestExistingSessionRecoveryRequiresVerifiedCapability(t *testing.T) { for _, nativeID := range []string{"", "native"} { for _, capable := range []bool{false, true} { wantRecovery := started && nativeID == "" - req, err := (&Dispatcher{SessionsReader: frozenProvider{engine: engine}}).executionRequest(t.Context(), sessions.Session{ID: "session", Engine: engine}, Snapshot{ModelProviderConfigured: true}, runtimedevice.KindCapabilities{NativeSessionRecovery: capable}, sessions.ExecutionBinding{HasStartedTurn: started, NativeSessionID: nativeID}) + req, err := (&Dispatcher{SessionsReader: frozenProvider{engine: engine}}).executionRequest(t.Context(), sessions.Session{ID: "session", Engine: engine}, Snapshot{ModelProviderConfigured: true}, proto.AgentKindCapabilities{NativeSessionRecovery: proto.CapabilityFromBool(capable)}, sessions.ExecutionBinding{HasStartedTurn: started, NativeSessionID: nativeID}) if wantRecovery && !capable { if err == nil { t.Fatal("unverified recovery admitted", engine) diff --git a/services/core/internal/execution/request.go b/services/core/internal/execution/request.go index e5159e0df..77a40dc12 100644 --- a/services/core/internal/execution/request.go +++ b/services/core/internal/execution/request.go @@ -5,7 +5,6 @@ import ( "errors" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/vaults" ) @@ -14,9 +13,9 @@ import ( // provider and therefore cannot run. var ErrModelProviderRequired = errors.New("the Session has no model provider") -func (d *Dispatcher) executionRequest(ctx context.Context, session sessions.Session, snapshot Snapshot, caps runtimedevice.KindCapabilities, bound sessions.ExecutionBinding) (proto.PromptRequestPayload, error) { +func (d *Dispatcher) executionRequest(ctx context.Context, session sessions.Session, snapshot Snapshot, caps proto.AgentKindCapabilities, bound sessions.ExecutionBinding) (proto.PromptRequestPayload, error) { recoverNativeSession := bound.HasStartedTurn && bound.NativeSessionID == "" - if recoverNativeSession && !caps.NativeSessionRecovery { + if recoverNativeSession && !caps.NativeSessionRecovery.IsSupported() { return proto.PromptRequestPayload{}, errors.New("native session recovery is unavailable") } tools, err := executionTools(snapshot.Agent.Tools) @@ -50,7 +49,7 @@ func (d *Dispatcher) executionRequest(ctx context.Context, session sessions.Sess Model: snapshot.Agent.Model, SystemPrompt: instructions, ModelProvider: provider, HarnessConfig: harnessConfig, ExecutionControls: controls, AgentStateKey: "agents-api-" + session.ID, AgentSessionID: bound.NativeSessionID, RequireExistingNativeSession: recoverNativeSession, - ObserveMessages: caps.MessageItems, + ObserveMessages: caps.MessageItems.IsSupported(), ObserveSubagentIdentities: snapshot.Agent.MultiAgent.Enabled, MaxConcurrentSubagents: snapshot.Agent.MultiAgent.MaxConcurrentSubagents, DisableSubagents: !snapshot.Agent.MultiAgent.Enabled} diff --git a/services/core/internal/execution/structured_output_test.go b/services/core/internal/execution/structured_output_test.go index ec7f7408b..c5c8e7242 100644 --- a/services/core/internal/execution/structured_output_test.go +++ b/services/core/internal/execution/structured_output_test.go @@ -9,7 +9,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine/enginetest" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) @@ -32,7 +31,7 @@ func TestStructuredOutputRequestKeepsFrozenSchemaAndInstructions(t *testing.T) { schema := json.RawMessage(`{"type":"object","properties":{"number":{"const":9007199254740992}}}`) instructions := "Keep these original instructions." snapshot := Snapshot{ModelProviderConfigured: true, Agent: v1.Agent{Model: "model", Instructions: &instructions, Text: v1.TextConfig{Format: v1.TextFormat{Type: "json_schema", Schema: schema}}}} - request, err := (&Dispatcher{SessionsReader: frozenProvider{engine: "claude_sdk"}}).executionRequest(context.Background(), sessions.Session{Engine: "claude_sdk"}, snapshot, runtimedevice.KindCapabilities{MessageItems: true}, sessions.ExecutionBinding{}) + request, err := (&Dispatcher{SessionsReader: frozenProvider{engine: "claude_sdk"}}).executionRequest(context.Background(), sessions.Session{Engine: "claude_sdk"}, snapshot, proto.AgentKindCapabilities{MessageItems: proto.CapabilitySupported}, sessions.ExecutionBinding{}) if err != nil || request.ExecutionControls.OutputFormat == nil { t.Fatal(err) } diff --git a/services/core/internal/execution/support.go b/services/core/internal/execution/support.go index 388a4d544..7650347c5 100644 --- a/services/core/internal/execution/support.go +++ b/services/core/internal/execution/support.go @@ -6,7 +6,6 @@ import ( "strings" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) @@ -78,68 +77,59 @@ func (p Policy) validateEngineInputs(engine string, configuration json.RawMessag // engineCapabilities is shared by device selection and the final preclaim check. // Capability bits describe the adapter; supported values still depend on its profile. -func (p Policy) engineCapabilities(peer *runtimegateway.Session, engine string, snapshot Snapshot) (runtimedevice.KindCapabilities, error) { - fail := func(message string) (runtimedevice.KindCapabilities, error) { - return runtimedevice.KindCapabilities{}, errors.New(message) +func (p Policy) engineCapabilities(peer *runtimegateway.Session, engine string, snapshot Snapshot) (proto.AgentKindCapabilities, error) { + fail := func(message string) (proto.AgentKindCapabilities, error) { + return proto.AgentKindCapabilities{}, errors.New(message) } profile, ok := p.Engines.Lookup(engine) if !ok || (snapshot.Environment != nil && !profile.Accepts(snapshot.Environment.Type)) { return fail("execution engine placement is not supported") } if err := validateProfileConfiguration(profile, snapshot); err != nil { - return runtimedevice.KindCapabilities{}, err + return proto.AgentKindCapabilities{}, err } info, found, known := peer.AgentKindStatus(engine) caps := info.Capabilities - if !known || !found || !info.Available || !caps.Streaming || !caps.Steering || !caps.DurableTurns || !caps.DurableInputReceipts { - return fail("device must advertise streaming, steering and durable turns for this engine") + if !known || !found || !info.Available { + return fail("device must advertise this engine as available") } - if !caps.Preparation { - return fail("device must advertise executor preparation") - } - if !caps.ExecutionControls { - return fail("device must advertise execution_controls") - } - if profile.WebSearchControl.IsSupported() && !caps.WebSearchControl { + if profile.WebSearchControl.IsSupported() && !caps.WebSearchControl.IsSupported() { return fail("device must advertise web_search_control") } - if snapshot.Agent.Text.Format.Type == "json_schema" && (!caps.StructuredOutput || !caps.MessageItems) { + if snapshot.Agent.Text.Format.Type == "json_schema" && (!caps.StructuredOutput.IsSupported() || !caps.MessageItems.IsSupported()) { return fail("device must support structured output and message observations") } - if profile.TextVerbosity.IsSupported() && !caps.TextVerbosity { + if profile.TextVerbosity.IsSupported() && !caps.TextVerbosity.IsSupported() { return fail("device must advertise text_verbosity") } - if !caps.ToolObservations { - return fail("device must advertise tool_observations") - } - if snapshot.Agent.MultiAgent.Enabled && !caps.SubagentObservations { + if snapshot.Agent.MultiAgent.Enabled && !caps.SubagentObservations.IsSupported() { return fail("device must support durable subagent observations") } - if !snapshot.Agent.MultiAgent.Enabled && !caps.SubagentControl { + if !snapshot.Agent.MultiAgent.Enabled && !caps.SubagentControl.IsSupported() { return fail("device must advertise subagent_control") } tools, err := executionTools(snapshot.Agent.Tools) if err != nil { return fail("invalid execution tool configuration") } - if err := (proto.PromptRequestPayload{ToolSearch: tools.Search, FunctionTools: tools.Functions}).ValidateToolSearch(caps.ToolSearch); err != nil { + if err := (proto.PromptRequestPayload{ToolSearch: tools.Search, FunctionTools: tools.Functions}).ValidateToolSearch(caps.ToolSearch.IsSupported()); err != nil { return fail(err.Error()) } - if tools.DisableProgrammatic && !caps.ProgrammaticToolCallingDisable { + if tools.DisableProgrammatic && !caps.ProgrammaticToolCallingDisable.IsSupported() { return fail("device must support disabling programmatic tool calling") } - if len(tools.Functions) > 0 && !caps.FunctionTools { + if len(tools.Functions) > 0 && !caps.FunctionTools.IsSupported() { return fail("device must advertise function_tools") } if _, err := p.mcpExecutionCredentials(engine, snapshot, tools.MCP, caps); err != nil { - return runtimedevice.KindCapabilities{}, err + return proto.AgentKindCapabilities{}, err } if snapshot.Environment != nil && (snapshot.Environment.Type == "openai_hosted" || snapshot.Environment.Type == "self_hosted") { - if !caps.Preparation || !caps.LocalEnvironment || !caps.WorkspaceReadPreparation || !caps.WorkspaceOutputExport { + if !caps.LocalEnvironment.IsSupported() || !caps.WorkspaceReadPreparation.IsSupported() || !caps.WorkspaceOutputExport.IsSupported() { return fail("device must advertise local preparation, workspace reads and output export") } } - if environmentNone(snapshot) && !caps.EnvironmentNone { + if environmentNone(snapshot) && !caps.EnvironmentNone.IsSupported() { return fail("device must advertise environment_none") } return caps, nil diff --git a/services/core/internal/runtimedevice/state.go b/services/core/internal/runtimedevice/state.go index 5d322c826..ed95ffbf6 100644 --- a/services/core/internal/runtimedevice/state.go +++ b/services/core/internal/runtimedevice/state.go @@ -12,58 +12,10 @@ type HeartbeatStatus struct { Deleted bool } -// KindCapabilities mirrors the daemon heartbeat capability -// shape after gateway-level normalization. Persistence stays separate from wire protocol structs. -type KindCapabilities struct { - SubagentObservations bool `json:"subagent_observations,omitempty"` - Streaming bool `json:"streaming,omitempty"` - Usage bool `json:"usage,omitempty"` - Resume bool `json:"resume,omitempty"` - NativeSessionRecovery bool `json:"native_session_recovery,omitempty"` - Steering bool `json:"steering,omitempty"` - MessageItems bool `json:"message_items,omitempty"` - - ToolObservations bool `json:"tool_observations,omitempty"` - EnvironmentNone bool `json:"environment_none,omitempty"` - LocalEnvironment bool `json:"local_environment,omitempty"` - Preparation bool `json:"preparation,omitempty"` - WorkspaceReadPreparation bool `json:"workspace_read_preparation,omitempty"` - WorkspaceOutputExport bool `json:"workspace_output_export,omitempty"` - ProgrammaticToolCallingDisable bool `json:"programmatic_tool_calling_disable,omitempty"` - WebSearchControl bool `json:"web_search_control,omitempty"` - // ExecutionControls supports typed search and verbosity controls. - ExecutionControls bool `json:"execution_controls,omitempty"` - TextVerbosity bool `json:"text_verbosity,omitempty"` - StructuredOutput bool `json:"structured_output,omitempty"` - ToolSearch bool `json:"tool_search,omitempty"` - MessageImages bool `json:"message_images,omitempty"` - FunctionResultImages bool `json:"function_result_images,omitempty"` - SubagentControl bool `json:"subagent_control,omitempty"` - FunctionTools bool `json:"function_tools,omitempty"` - MCPHTTPTools bool `json:"mcp_http_tools,omitempty"` - MCPHTTPRequired bool `json:"mcp_http_required,omitempty"` - MCPHTTPBearerAuth bool `json:"mcp_http_bearer_auth,omitempty"` - DurableInputReceipts bool `json:"durable_input_receipts,omitempty"` - DurableTurns bool `json:"durable_turns,omitempty"` -} - -// SupportedAgentKind is the sanitized runtime.config view -// of one daemon-side agent_kind. -type SupportedAgentKind struct { - Kind string `json:"kind"` - Available bool `json:"available"` - Version string `json:"version,omitempty"` - Capabilities KindCapabilities `json:"capabilities,omitempty"` -} - -// Heartbeat is the WebSocket daemon heartbeat -// payload after gateway normalization. +// Heartbeat names the authenticated Runtime connection a daemon heartbeat +// refreshes. type Heartbeat struct { RuntimeID string // CredentialHash comes from gateway authentication, never a daemon frame. - CredentialHash string - DaemonVersion string - ActiveRequests int - HeartbeatTimestamp int64 - SupportedAgentKinds []SupportedAgentKind + CredentialHash string } diff --git a/services/core/internal/runtimegateway/capabilities_test.go b/services/core/internal/runtimegateway/capabilities_test.go index e7e7b66f4..6769d0af1 100644 --- a/services/core/internal/runtimegateway/capabilities_test.go +++ b/services/core/internal/runtimegateway/capabilities_test.go @@ -3,33 +3,9 @@ package runtimegateway import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto/prototest" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" - "reflect" "testing" ) -func TestCapabilityProjectionCoversEveryField(t *testing.T) { - declaration := prototest.Capabilities(proto.AgentKindCapabilities{}) - wireType, deviceType := reflect.TypeOf(declaration), reflect.TypeOf(runtimedevice.KindCapabilities{}) - if wireType.NumField() != deviceType.NumField() { - t.Fatal("wire and device capability inventories differ") - } - for i := 0; i < wireType.NumField(); i++ { - field := wireType.Field(i) - t.Run(field.Name, func(t *testing.T) { - caps := declaration - reflect.ValueOf(&caps).Elem().Field(i).Set(reflect.ValueOf(proto.CapabilitySupported)) - kinds := deviceKindsFromHeartbeat(proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported, SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "fixture", Available: true, Capabilities: caps}}}) - actual := reflect.ValueOf(kinds[0].Capabilities) - for j := 0; j < deviceType.NumField(); j++ { - if actual.Field(j).Bool() != (deviceType.Field(j).Name == field.Name) { - t.Fatal("projection lost or crossed a capability field") - } - } - }) - } -} - func TestInvalidHeartbeatDiscardsPreviousDeclarationAndClosesTransport(t *testing.T) { session := NewSession(newFakeConn(), "runtime", "workspace", proto.Version, NewRegistry(), nil) valid, err := proto.NewEnvelope(proto.TypeHeartbeat, "", proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported, SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "fixture", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}}}) diff --git a/services/core/internal/runtimegateway/functions_test.go b/services/core/internal/runtimegateway/functions_test.go deleted file mode 100644 index 03e8635d1..000000000 --- a/services/core/internal/runtimegateway/functions_test.go +++ /dev/null @@ -1,19 +0,0 @@ -package runtimegateway - -import ( - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto/prototest" - "testing" -) - -func TestFunctionCapabilitySurvivesHeartbeatMapping(t *testing.T) { - for _, supported := range []bool{false, true} { - kinds := deviceKindsFromHeartbeat(proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported, SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{FunctionTools: proto.CapabilityFromBool(supported), FunctionResultImages: proto.CapabilityFromBool(supported)})}}}) - s := &Session{} - s.setDeclarations(kinds, proto.CapabilityUnsupported) - info, found, known := s.AgentKindStatus("codex") - if !found || !known || info.Capabilities.FunctionTools != supported || info.Capabilities.FunctionResultImages != supported { - t.Fatal(info, found, known) - } - } -} diff --git a/services/core/internal/runtimegateway/mcp_bearer_live_linux_test.go b/services/core/internal/runtimegateway/mcp_bearer_live_linux_test.go index f33f76206..b77ff7707 100644 --- a/services/core/internal/runtimegateway/mcp_bearer_live_linux_test.go +++ b/services/core/internal/runtimegateway/mcp_bearer_live_linux_test.go @@ -70,7 +70,7 @@ func TestLiveMCPBearerGatewayColdContinuation(t *testing.T) { for { info, found, known := peer.AgentKindStatus("codex") if known && found && info.Available { - if !info.Capabilities.MCPHTTPTools || !info.Capabilities.MCPHTTPBearerAuth || !info.Capabilities.ToolObservations || !info.Capabilities.DurableTurns || !info.Capabilities.EnvironmentNone { + if !info.Capabilities.MCPHTTPTools.IsSupported() || !info.Capabilities.MCPHTTPBearerAuth.IsSupported() || !info.Capabilities.EnvironmentNone.IsSupported() { t.Fatal("built daemon did not advertise the required private execution capabilities") } proof["codex_descriptor"] = info diff --git a/services/core/internal/runtimegateway/mcp_test.go b/services/core/internal/runtimegateway/mcp_test.go deleted file mode 100644 index 13d7cefa6..000000000 --- a/services/core/internal/runtimegateway/mcp_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package runtimegateway - -import ( - "encoding/json" - "strings" - "testing" - - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto/prototest" -) - -func TestMCPHTTPBearerCapabilitySurvivesHeartbeatMapping(t *testing.T) { - for _, supported := range []bool{false, true} { - heartbeat := proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported, SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, - Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{MCPHTTPTools: proto.CapabilitySupported, MCPHTTPBearerAuth: proto.CapabilityFromBool(supported)})}}} - raw, err := json.Marshal(heartbeat) - if err != nil || strings.Contains(string(raw), `"mcp_http_bearer_auth":true`) != supported { - t.Fatal("wire capability changed", err) - } - var decoded proto.HeartbeatPayload - if err := json.Unmarshal(raw, &decoded); err != nil { - t.Fatal(err) - } - s := &Session{} - s.setDeclarations(deviceKindsFromHeartbeat(decoded), proto.CapabilityUnsupported) - info, found, known := s.AgentKindStatus("codex") - if !found || !known || !info.Capabilities.MCPHTTPTools || info.Capabilities.MCPHTTPBearerAuth != supported { - t.Fatal("bearer capability was lost or inferred from credential-free MCP") - } - } -} - -func TestMCPRequiredCapabilityIsExplicit(t *testing.T) { - for _, supported := range []bool{false, true} { - heartbeat := proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported, SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{MCPHTTPTools: proto.CapabilitySupported, MCPHTTPBearerAuth: proto.CapabilitySupported, MCPHTTPRequired: proto.CapabilityFromBool(supported)})}}} - raw, err := json.Marshal(heartbeat) - if err != nil || strings.Contains(string(raw), `"mcp_http_required":true`) != supported { - t.Fatal("wire capability differs", err) - } - var decoded proto.HeartbeatPayload - if err = json.Unmarshal(raw, &decoded); err != nil { - t.Fatal(err) - } - s := &Session{} - s.setDeclarations(deviceKindsFromHeartbeat(decoded), proto.CapabilityUnsupported) - info, found, known := s.AgentKindStatus("codex") - if !found || !known || info.Capabilities.MCPHTTPRequired != supported { - t.Fatal("combination capability lost or inferred") - } - } -} diff --git a/services/core/internal/runtimegateway/preparation_test.go b/services/core/internal/runtimegateway/preparation_test.go index 99eb254a3..8da8bd442 100644 --- a/services/core/internal/runtimegateway/preparation_test.go +++ b/services/core/internal/runtimegateway/preparation_test.go @@ -5,7 +5,6 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto/prototest" ) func TestPreparationSubscriptionHasNoRunIdentityAndOrdersRevisions(t *testing.T) { @@ -76,10 +75,3 @@ func TestPreparationCloseAndOverflowDoNotInventRunEvents(t *testing.T) { s.Close("test") } } - -func TestPreparationCapabilitySurvivesHeartbeatMapping(t *testing.T) { - kinds := deviceKindsFromHeartbeat(proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported, SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Preparation: proto.CapabilitySupported, LocalEnvironment: proto.CapabilitySupported, WorkspaceReadPreparation: proto.CapabilitySupported, NativeSessionRecovery: proto.CapabilitySupported})}}}) - if len(kinds) != 1 || (!kinds[0].Capabilities.Preparation || !kinds[0].Capabilities.LocalEnvironment || !kinds[0].Capabilities.WorkspaceReadPreparation || !kinds[0].Capabilities.NativeSessionRecovery) { - t.Fatal("preparation capability lost") - } -} diff --git a/services/core/internal/runtimegateway/session.go b/services/core/internal/runtimegateway/session.go index 36a923397..f6c14180c 100644 --- a/services/core/internal/runtimegateway/session.go +++ b/services/core/internal/runtimegateway/session.go @@ -98,7 +98,7 @@ type Session struct { // sending execution_prepare so unsupported engines fail on the server. kindsMu sync.RWMutex kindsSeen bool - supportedKinds []runtimedevice.SupportedAgentKind + supportedKinds []proto.SupportedAgentKind homeRemoval proto.CapabilitySupport // assignments holds each Session's reference the Runtime acknowledged @@ -205,29 +205,29 @@ func (s *Session) LastSeen() time.Time { // AgentKindStatus returns the latest advertised descriptor for kind. // found=false means the daemon has not advertised that kind; snapshotKnown // distinguishes "no heartbeat yet" from "heartbeat arrived and omitted it". -func (s *Session) AgentKindStatus(kind string) (info runtimedevice.SupportedAgentKind, found bool, snapshotKnown bool) { +func (s *Session) AgentKindStatus(kind string) (info proto.SupportedAgentKind, found bool, snapshotKnown bool) { kind = strings.TrimSpace(kind) if kind == "" { - return runtimedevice.SupportedAgentKind{}, false, false + return proto.SupportedAgentKind{}, false, false } s.kindsMu.RLock() seen := s.kindsSeen - kinds := make([]runtimedevice.SupportedAgentKind, len(s.supportedKinds)) + kinds := make([]proto.SupportedAgentKind, len(s.supportedKinds)) copy(kinds, s.supportedKinds) s.kindsMu.RUnlock() if !seen { - return runtimedevice.SupportedAgentKind{}, false, false + return proto.SupportedAgentKind{}, false, false } for _, candidate := range kinds { if candidate.Kind == kind { return candidate, true, true } } - return runtimedevice.SupportedAgentKind{}, false, true + return proto.SupportedAgentKind{}, false, true } -func (s *Session) setDeclarations(kinds []runtimedevice.SupportedAgentKind, homeRemoval proto.CapabilitySupport) { - copyKinds := make([]runtimedevice.SupportedAgentKind, len(kinds)) +func (s *Session) setDeclarations(kinds []proto.SupportedAgentKind, homeRemoval proto.CapabilitySupport) { + copyKinds := make([]proto.SupportedAgentKind, len(kinds)) copy(copyKinds, kinds) s.kindsMu.Lock() s.kindsSeen = true @@ -407,22 +407,14 @@ func (s *Session) handleHeartbeat(env proto.Envelope) { s.Close("invalid heartbeat declaration") return } - kinds := deviceKindsFromHeartbeat(p) - s.setDeclarations(kinds, p.HomeRemoval) + s.setDeclarations(p.SupportedAgentKinds, p.HomeRemoval) if s.heartbeat == nil { return } ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() - status, err := s.heartbeat.TouchAgentDaemonHeartbeat(ctx, runtimedevice.Heartbeat{ - RuntimeID: s.DeviceID, - CredentialHash: s.credentialHash, - DaemonVersion: p.DaemonVersion, - ActiveRequests: p.ActiveRequests, - HeartbeatTimestamp: p.Timestamp, - SupportedAgentKinds: kinds, - }) + status, err := s.heartbeat.TouchAgentDaemonHeartbeat(ctx, runtimedevice.Heartbeat{RuntimeID: s.DeviceID, CredentialHash: s.credentialHash}) if err != nil { s.log("agentdaemon gateway: persist heartbeat device=%s: %v", s.DeviceID, err) return @@ -441,49 +433,6 @@ func (s *Session) handleHeartbeat(env proto.Envelope) { } } -func deviceKindsFromHeartbeat(p proto.HeartbeatPayload) []runtimedevice.SupportedAgentKind { - out := make([]runtimedevice.SupportedAgentKind, 0, len(p.SupportedAgentKinds)) - for _, info := range p.SupportedAgentKinds { - out = append(out, runtimedevice.SupportedAgentKind{ - Kind: info.Kind, - Available: info.Available, - Version: info.Version, - Capabilities: runtimedevice.KindCapabilities{ - Streaming: info.Capabilities.Streaming.IsSupported(), - Usage: info.Capabilities.Usage.IsSupported(), - Resume: info.Capabilities.Resume.IsSupported(), - Steering: info.Capabilities.Steering.IsSupported(), - DurableTurns: info.Capabilities.DurableTurns.IsSupported(), - DurableInputReceipts: info.Capabilities.DurableInputReceipts.IsSupported(), - NativeSessionRecovery: info.Capabilities.NativeSessionRecovery.IsSupported(), - MessageItems: info.Capabilities.MessageItems.IsSupported(), - - ToolObservations: info.Capabilities.ToolObservations.IsSupported(), - EnvironmentNone: info.Capabilities.EnvironmentNone.IsSupported(), - LocalEnvironment: info.Capabilities.LocalEnvironment.IsSupported(), - Preparation: info.Capabilities.Preparation.IsSupported(), - WorkspaceReadPreparation: info.Capabilities.WorkspaceReadPreparation.IsSupported(), - WorkspaceOutputExport: info.Capabilities.WorkspaceOutputExport.IsSupported(), - WebSearchControl: info.Capabilities.WebSearchControl.IsSupported(), - ProgrammaticToolCallingDisable: info.Capabilities.ProgrammaticToolCallingDisable.IsSupported(), - TextVerbosity: info.Capabilities.TextVerbosity.IsSupported(), - StructuredOutput: info.Capabilities.StructuredOutput.IsSupported(), - ToolSearch: info.Capabilities.ToolSearch.IsSupported(), - MessageImages: info.Capabilities.MessageImages.IsSupported(), - FunctionResultImages: info.Capabilities.FunctionResultImages.IsSupported(), - ExecutionControls: info.Capabilities.ExecutionControls.IsSupported(), - SubagentControl: info.Capabilities.SubagentControl.IsSupported(), - SubagentObservations: info.Capabilities.SubagentObservations.IsSupported(), - FunctionTools: info.Capabilities.FunctionTools.IsSupported(), - MCPHTTPTools: info.Capabilities.MCPHTTPTools.IsSupported(), - MCPHTTPRequired: info.Capabilities.MCPHTTPRequired.IsSupported(), - MCPHTTPBearerAuth: info.Capabilities.MCPHTTPBearerAuth.IsSupported(), - }, - }) - } - return out -} - func (s *Session) dispatch(env proto.Envelope) { if !s.allowsReceiptFrame(env, false) { return diff --git a/services/core/internal/runtimegateway/session_test.go b/services/core/internal/runtimegateway/session_test.go index e746dbd2f..95d545194 100644 --- a/services/core/internal/runtimegateway/session_test.go +++ b/services/core/internal/runtimegateway/session_test.go @@ -315,7 +315,7 @@ func TestSession_SendWritesToWire(t *testing.T) { } } -func TestSession_HeartbeatPersistsSupportedAgentKinds(t *testing.T) { +func TestSession_HeartbeatTouchesRuntimeAndAdmitsDeclarations(t *testing.T) { reg := NewRegistry() conn := newFakeConn() heartbeat := newFakeHeartbeatStore() @@ -324,67 +324,21 @@ func TestSession_HeartbeatPersistsSupportedAgentKinds(t *testing.T) { sess.Start() defer sess.Close("test done") - env, _ := proto.NewEnvelope(proto.TypeHeartbeat, "", proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported, - Timestamp: 1710000000, - ActiveRequests: 2, - DaemonVersion: "0.2.0-test", - SupportedAgentKinds: []proto.SupportedAgentKind{ - { - Kind: "fake_beta", - Available: false, - Version: "missing", - Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{ - Streaming: proto.CapabilitySupported, - }), - }, - { - Kind: "fake_alpha", - Available: true, - Version: "1.2.3", - Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{ - Streaming: proto.CapabilitySupported, - Usage: proto.CapabilitySupported, - Resume: proto.CapabilitySupported, - }), - }, - { - Kind: "codex", - Available: true, - Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{MCPHTTPTools: proto.CapabilitySupported, Steering: proto.CapabilitySupported, MessageItems: proto.CapabilitySupported, ToolObservations: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported, WebSearchControl: proto.CapabilitySupported, TextVerbosity: proto.CapabilitySupported, ExecutionControls: proto.CapabilitySupported, SubagentControl: proto.CapabilitySupported}), - }, - }, - }) + kinds := []proto.SupportedAgentKind{ + {Kind: "fake_beta", Available: false, Version: "missing", Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{MessageItems: proto.CapabilitySupported})}, + {Kind: "codex", Available: true, Version: "1.2.3", Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{MCPHTTPTools: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported, TextVerbosity: proto.CapabilitySupported, SubagentControl: proto.CapabilitySupported})}, + } + env, _ := proto.NewEnvelope(proto.TypeHeartbeat, "", proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported, SupportedAgentKinds: kinds}) raw, _ := jsonMarshal(env) conn.Feed(raw) - got := heartbeat.waitDaemonHeartbeat(t) - if got.RuntimeID != "dev-1" || got.DaemonVersion != "0.2.0-test" || got.ActiveRequests != 2 || got.HeartbeatTimestamp != 1710000000 { - t.Fatalf("heartbeat metadata not preserved: %+v", got) - } - if len(got.SupportedAgentKinds) != 3 { - t.Fatalf("SupportedAgentKinds len = %d, want 3: %#v", len(got.SupportedAgentKinds), got.SupportedAgentKinds) - } - byKind := map[string]runtimedevice.SupportedAgentKind{} - for _, info := range got.SupportedAgentKinds { - byKind[info.Kind] = info - } - claude := byKind["fake_alpha"] - if !claude.Available || claude.Version != "1.2.3" || !claude.Capabilities.Streaming || !claude.Capabilities.Usage || !claude.Capabilities.Resume { - t.Fatalf("fake_alpha descriptor not converted: %#v", claude) + if got := heartbeat.waitDaemonHeartbeat(t); got.RuntimeID != "dev-1" { + t.Fatalf("heartbeat touched %+v", got) } - fake_beta := byKind["fake_beta"] - if fake_beta.Available || fake_beta.Version != "missing" || !fake_beta.Capabilities.Streaming { - t.Fatalf("fake_beta descriptor not converted: %#v", fake_beta) - } - if !byKind["codex"].Capabilities.ExecutionControls || claude.Capabilities.ExecutionControls || fake_beta.Capabilities.ExecutionControls || !byKind["codex"].Capabilities.ToolObservations || claude.Capabilities.ToolObservations || fake_beta.Capabilities.ToolObservations || !byKind["codex"].Capabilities.SubagentControl || claude.Capabilities.SubagentControl || fake_beta.Capabilities.SubagentControl || !byKind["codex"].Capabilities.TextVerbosity || claude.Capabilities.TextVerbosity || fake_beta.Capabilities.TextVerbosity || !byKind["codex"].Capabilities.WebSearchControl || claude.Capabilities.WebSearchControl || fake_beta.Capabilities.WebSearchControl || !byKind["codex"].Capabilities.EnvironmentNone || claude.Capabilities.EnvironmentNone || fake_beta.Capabilities.EnvironmentNone || !byKind["codex"].Capabilities.MessageItems || !byKind["codex"].Capabilities.Steering || claude.Capabilities.Steering || fake_beta.Capabilities.Steering { - t.Fatalf("steering capability not preserved: %#v", byKind) - } - if !byKind["codex"].Capabilities.MCPHTTPTools || claude.Capabilities.MCPHTTPTools || fake_beta.Capabilities.MCPHTTPTools { - t.Fatalf("HTTP MCP capability not preserved: %#v", byKind) - } - codex, found, known := sess.AgentKindStatus("codex") - if !found || !known || !codex.Capabilities.Steering || !codex.Capabilities.MCPHTTPTools { - t.Fatalf("steering capability absent from live session: %#v", codex) + for _, want := range kinds { + if got, found, known := sess.AgentKindStatus(want.Kind); !found || !known || got != want { + t.Fatalf("live declaration %#v, want %#v", got, want) + } } } @@ -398,9 +352,9 @@ func TestSession_HeartbeatDoesNotInferCapabilities(t *testing.T) { defer sess.Close("test done") conn.Feed([]byte(`{"type":"heartbeat","payload":{"ts":1710000100,"claude_available":true,"home_removal":false}}`)) - got := heartbeat.waitDaemonHeartbeat(t) - if len(got.SupportedAgentKinds) != 0 { - t.Fatalf("undeclared capabilities inferred: %#v", got.SupportedAgentKinds) + heartbeat.waitDaemonHeartbeat(t) + if info, found, known := sess.AgentKindStatus("claude_sdk"); found || !known { + t.Fatalf("undeclared capabilities inferred: %#v", info) } } diff --git a/services/core/tests/integration/archive_cancellation_test.go b/services/core/tests/integration/archive_cancellation_test.go index 90addbd8e..9f5352099 100644 --- a/services/core/tests/integration/archive_cancellation_test.go +++ b/services/core/tests/integration/archive_cancellation_test.go @@ -114,7 +114,7 @@ func TestArchiveWaitingCancellationReceipts(t *testing.T) { peer, err = registry.LookupDevice(owner.DeviceID) if err == nil { info, _, known := peer.AgentKindStatus("codex") - if known && info.Capabilities.FunctionTools { + if known && info.Capabilities.FunctionTools.IsSupported() { break } } diff --git a/services/core/tests/integration/claude_execution_test.go b/services/core/tests/integration/claude_execution_test.go index 25fa8035f..33657b630 100644 --- a/services/core/tests/integration/claude_execution_test.go +++ b/services/core/tests/integration/claude_execution_test.go @@ -30,13 +30,13 @@ func claudeSession(t *testing.T, h *dispatchHarness, configuration string, prebo func claudeHeartbeat(t *testing.T, h *dispatchHarness, ready bool) { t.Helper() - caps := prototest.Capabilities(proto.AgentKindCapabilities{Streaming: proto.CapabilitySupported, Steering: proto.CapabilitySupported, DurableTurns: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilityFromBool(ready), ExecutionControls: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported, SubagentControl: proto.CapabilitySupported, FunctionTools: proto.CapabilitySupported, ToolObservations: proto.CapabilitySupported, Preparation: proto.CapabilitySupported}) - h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported, SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "claude_sdk", Available: true, Capabilities: caps}}}) + caps := prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported, SubagentControl: proto.CapabilitySupported, FunctionTools: proto.CapabilitySupported}) + h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported, SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "claude_sdk", Available: ready, Capabilities: caps}}}) deadline := time.Now().Add(3 * time.Second) for { peer, _ := h.registry.LookupDevice(h.device.ID) info, found, known := peer.AgentKindStatus("claude_sdk") - if known && found && info.Capabilities.DurableInputReceipts == ready { + if known && found && info.Available == ready { return } if time.Now().After(deadline) { @@ -79,7 +79,7 @@ func TestClaudeWorkerSelectsStoredEngineAndRestrictiveCapabilities(t *testing.T) } queued() // A fully capable Codex descriptor cannot execute a Claude Session. claudeHeartbeat(t, h, false) - queued() // Durable application receipts are required for this engine too. + queued() // An unavailable Claude descriptor cannot execute it either. claudeHeartbeat(t, h, true) var prompt proto.PromptRequestPayload if h.read(testExecutionRequest).DecodePayload(&prompt) != nil { diff --git a/services/core/tests/integration/claude_mcp_test.go b/services/core/tests/integration/claude_mcp_test.go index feeea8e5c..1eb291bfb 100644 --- a/services/core/tests/integration/claude_mcp_test.go +++ b/services/core/tests/integration/claude_mcp_test.go @@ -28,12 +28,12 @@ func TestClaudeMCPWaitsForCapableRuntime(t *testing.T) { } claudeSession(t, h, configuration, prebound) // Base MCP support does not imply authentication or required initialization. - caps := prototest.Capabilities(proto.AgentKindCapabilities{Streaming: proto.CapabilitySupported, Steering: proto.CapabilitySupported, DurableTurns: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported, ExecutionControls: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported, SubagentControl: proto.CapabilitySupported, ToolObservations: proto.CapabilitySupported, MCPHTTPTools: proto.CapabilityFromBool(authenticated || required), Preparation: proto.CapabilitySupported}) + caps := prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported, SubagentControl: proto.CapabilitySupported, MCPHTTPTools: proto.CapabilityFromBool(authenticated || required)}) h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported, SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "claude_sdk", Available: true, Capabilities: caps}}}) awaitDaemonRemoteCondition(t, t.Context(), 3*time.Second, "Claude MCP heartbeat", func() bool { peer, _ := h.registry.LookupDevice(h.device.ID) info, found, known := peer.AgentKindStatus("claude_sdk") - return known && found && info.Capabilities.MCPHTTPTools == (authenticated || required) && !info.Capabilities.MCPHTTPBearerAuth && !info.Capabilities.MCPHTTPRequired + return known && found && info.Capabilities.MCPHTTPTools.IsSupported() == (authenticated || required) && !info.Capabilities.MCPHTTPBearerAuth.IsSupported() && !info.Capabilities.MCPHTTPRequired.IsSupported() }) input := h.message("start", "Use declared tools only") if prebound { @@ -101,7 +101,7 @@ func TestClaudeMCPUnsupportedSnapshotRejectedBeforeClaim(t *testing.T) { configuration = strings.Replace(configuration, `"tickets"`, `"functions"`, 1) } claudeSession(t, h, configuration, true) - caps := prototest.Capabilities(proto.AgentKindCapabilities{Streaming: proto.CapabilitySupported, Steering: proto.CapabilitySupported, DurableTurns: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported, ExecutionControls: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported, SubagentControl: proto.CapabilitySupported, ToolObservations: proto.CapabilitySupported, MCPHTTPTools: proto.CapabilitySupported, MCPHTTPRequired: proto.CapabilitySupported, MCPHTTPBearerAuth: proto.CapabilitySupported, Preparation: proto.CapabilitySupported}) + caps := prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported, SubagentControl: proto.CapabilitySupported, MCPHTTPTools: proto.CapabilitySupported, MCPHTTPRequired: proto.CapabilitySupported, MCPHTTPBearerAuth: proto.CapabilitySupported}) if profile == "missing required capability" { caps.MCPHTTPRequired = proto.CapabilityUnsupported } @@ -109,7 +109,7 @@ func TestClaudeMCPUnsupportedSnapshotRejectedBeforeClaim(t *testing.T) { deadline := time.Now().Add(3 * time.Second) for { peer, _ := h.registry.LookupDevice(h.device.ID) - if info, _, _ := peer.AgentKindStatus("claude_sdk"); info.Capabilities.MCPHTTPBearerAuth { + if info, _, _ := peer.AgentKindStatus("claude_sdk"); info.Capabilities.MCPHTTPBearerAuth.IsSupported() { break } if time.Now().After(deadline) { diff --git a/services/core/tests/integration/dispatch_test.go b/services/core/tests/integration/dispatch_test.go index 1d2ac99b6..a2002117b 100644 --- a/services/core/tests/integration/dispatch_test.go +++ b/services/core/tests/integration/dispatch_test.go @@ -102,7 +102,7 @@ func newDispatchHarnessForSession(t *testing.T, configuration []byte, local bool t.Fatal("device connection failed") } t.Cleanup(func() { h.conn.Close() }) - h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported, SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Streaming: proto.CapabilitySupported, Steering: proto.CapabilitySupported, Resume: proto.CapabilitySupported, DurableTurns: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported, WebSearchControl: proto.CapabilitySupported, TextVerbosity: proto.CapabilitySupported, ExecutionControls: proto.CapabilitySupported, SubagentControl: proto.CapabilitySupported, SubagentObservations: proto.CapabilitySupported, ToolObservations: proto.CapabilitySupported, NativeSessionRecovery: proto.CapabilitySupported, Preparation: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported})}}}) + h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported, SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{WebSearchControl: proto.CapabilitySupported, TextVerbosity: proto.CapabilitySupported, SubagentControl: proto.CapabilitySupported, SubagentObservations: proto.CapabilitySupported, NativeSessionRecovery: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported})}}}) deadline := time.Now().Add(3 * time.Second) for { peer, e := h.registry.LookupDevice(h.device.ID) @@ -428,25 +428,21 @@ func TestExecutionOutcomeAndNativeBindingCommitTogether(t *testing.T) { func TestExecutionRejectsRuntimeMissingCapabilityBeforeClaim(t *testing.T) { for _, tc := range []struct{ missing, message string }{ - {"durable_turns", "device must advertise streaming, steering and durable turns for this engine"}, - {"durable_input_receipts", "device must advertise streaming, steering and durable turns for this engine"}, - {"preparation", "device must advertise executor preparation"}, - {"execution_controls", "device must advertise execution_controls"}, {"web_search_control", "device must advertise web_search_control"}, {"text_verbosity", "device must advertise text_verbosity"}, - {"tool_observations", "device must advertise tool_observations"}, {"subagent_control", "device must advertise subagent_control"}, {"environment_none", "device must advertise environment_none"}, } { missing := tc.missing t.Run(missing, func(t *testing.T) { h := newDispatchHarness(t) - h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported, SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Streaming: proto.CapabilitySupported, Steering: proto.CapabilitySupported, Resume: proto.CapabilitySupported, DurableTurns: proto.CapabilityFromBool(missing != "durable_turns"), DurableInputReceipts: proto.CapabilityFromBool(missing != "durable_input_receipts"), Preparation: proto.CapabilityFromBool(missing != "preparation"), WebSearchControl: proto.CapabilityFromBool(missing != "web_search_control"), TextVerbosity: proto.CapabilityFromBool(missing != "text_verbosity"), ExecutionControls: proto.CapabilityFromBool(missing != "execution_controls"), SubagentControl: proto.CapabilityFromBool(missing != "subagent_control"), ToolObservations: proto.CapabilityFromBool(missing != "tool_observations"), EnvironmentNone: proto.CapabilityFromBool(missing != "environment_none")})}}}) + caps := prototest.Capabilities(proto.AgentKindCapabilities{WebSearchControl: proto.CapabilityFromBool(missing != "web_search_control"), TextVerbosity: proto.CapabilityFromBool(missing != "text_verbosity"), SubagentControl: proto.CapabilityFromBool(missing != "subagent_control"), EnvironmentNone: proto.CapabilityFromBool(missing != "environment_none")}) + h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported, SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: caps}}}) deadline := time.Now().Add(3 * time.Second) for { peer, _ := h.registry.LookupDevice(h.device.ID) info, _, _ := peer.AgentKindStatus("codex") - if info.Capabilities.DurableInputReceipts == (missing != "durable_input_receipts") && info.Capabilities.Preparation == (missing != "preparation") && info.Capabilities.ExecutionControls == (missing != "execution_controls") && info.Capabilities.DurableTurns == (missing != "durable_turns") && info.Capabilities.WebSearchControl == (missing != "web_search_control") && info.Capabilities.TextVerbosity == (missing != "text_verbosity") && info.Capabilities.SubagentControl == (missing != "subagent_control") && info.Capabilities.ToolObservations == (missing != "tool_observations") && info.Capabilities.EnvironmentNone == (missing != "environment_none") { + if info.Capabilities == caps { break } if time.Now().After(deadline) { diff --git a/services/core/tests/integration/environment_device_test.go b/services/core/tests/integration/environment_device_test.go index b348101ea..f07ed49d8 100644 --- a/services/core/tests/integration/environment_device_test.go +++ b/services/core/tests/integration/environment_device_test.go @@ -10,7 +10,7 @@ import ( ) func TestWorkerEnvironmentSelectsCapableDeviceWithoutMovingBinding(t *testing.T) { - for _, missing := range []string{"preparation", "local_environment", "durable_input_receipts"} { + for _, missing := range []string{"local_environment", "workspace_read_preparation"} { t.Run(missing, func(t *testing.T) { h := newDispatchHarness(t) _, pool := testStore(t) @@ -19,9 +19,8 @@ func TestWorkerEnvironmentSelectsCapableDeviceWithoutMovingBinding(t *testing.T) bound := workerEnvironmentReservation(t, h) originalRuntime := h.environments[bound.SessionID] caps := workerEnvironmentCapabilities() - caps.Preparation = proto.CapabilityFromBool(missing != "preparation") caps.LocalEnvironment = proto.CapabilityFromBool(missing != "local_environment") - caps.DurableInputReceipts = proto.CapabilityFromBool(missing != "durable_input_receipts") + caps.WorkspaceReadPreparation = proto.CapabilityFromBool(missing != "workspace_read_preparation") awaitFixtureCapabilities(t, originalRuntime, caps) generalFrames := workerFrames(t, h) boundFrames := workerFrames(t, originalRuntime) diff --git a/services/core/tests/integration/environment_directory_test.go b/services/core/tests/integration/environment_directory_test.go index 0fc04e95b..da44cd306 100644 --- a/services/core/tests/integration/environment_directory_test.go +++ b/services/core/tests/integration/environment_directory_test.go @@ -25,14 +25,14 @@ func directoryWorker(t *testing.T) (*dispatchHarness, *execution.Worker, session if err != nil { t.Fatal(err) } - h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported, SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported, Preparation: proto.CapabilitySupported, WorkspaceReadPreparation: proto.CapabilitySupported})}}}) + h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported, SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported, WorkspaceReadPreparation: proto.CapabilitySupported})}}}) peer, err := h.registry.LookupDevice(h.device.ID) if err != nil { t.Fatal(err) } awaitDaemonRemoteCondition(t, t.Context(), 3*time.Second, "read preparation capability", func() bool { info, _, _ := peer.AgentKindStatus("codex") - return info.Capabilities.WorkspaceReadPreparation + return info.Capabilities.WorkspaceReadPreparation.IsSupported() }) w := startWorker(t, t.Context(), h.s, h.d) ctx, cancel := context.WithCancel(t.Context()) diff --git a/services/core/tests/integration/environment_expiry_dispatch_test.go b/services/core/tests/integration/environment_expiry_dispatch_test.go index c96c13f73..aafee8a52 100644 --- a/services/core/tests/integration/environment_expiry_dispatch_test.go +++ b/services/core/tests/integration/environment_expiry_dispatch_test.go @@ -11,8 +11,8 @@ import ( func enableEnvironmentExpiryDispatch(h *dispatchHarness) { h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported, SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{ - Streaming: proto.CapabilitySupported, Steering: proto.CapabilitySupported, DurableTurns: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported, WebSearchControl: proto.CapabilitySupported, TextVerbosity: proto.CapabilitySupported, ExecutionControls: proto.CapabilitySupported, SubagentControl: proto.CapabilitySupported, ToolObservations: proto.CapabilitySupported, - Preparation: proto.CapabilitySupported})}}}) + EnvironmentNone: proto.CapabilitySupported, WebSearchControl: proto.CapabilitySupported, TextVerbosity: proto.CapabilitySupported, SubagentControl: proto.CapabilitySupported, + })}}}) } func TestWorkerEnvironmentExpiryAtFullExecutionCapacity(t *testing.T) { diff --git a/services/core/tests/integration/environment_runtime_fixture_test.go b/services/core/tests/integration/environment_runtime_fixture_test.go index f2703e39d..31dde7bfd 100644 --- a/services/core/tests/integration/environment_runtime_fixture_test.go +++ b/services/core/tests/integration/environment_runtime_fixture_test.go @@ -118,9 +118,6 @@ func awaitFixtureCapabilities(t *testing.T, h *dispatchHarness, caps proto.Agent return false } info, _, known := peer.AgentKindStatus("codex") - return known && info.Capabilities.Preparation == caps.Preparation.IsSupported() && - info.Capabilities.LocalEnvironment == caps.LocalEnvironment.IsSupported() && - info.Capabilities.DurableInputReceipts == caps.DurableInputReceipts.IsSupported() && - info.Capabilities.WorkspaceOutputExport == caps.WorkspaceOutputExport.IsSupported() + return known && info.Capabilities == caps }) } diff --git a/services/core/tests/integration/environment_worker_helpers_test.go b/services/core/tests/integration/environment_worker_helpers_test.go index 86c34df7a..9b5007a3d 100644 --- a/services/core/tests/integration/environment_worker_helpers_test.go +++ b/services/core/tests/integration/environment_worker_helpers_test.go @@ -16,18 +16,18 @@ import ( func enableWorkerEnvironment(t *testing.T, h *dispatchHarness) { t.Helper() h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported, SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: workerEnvironmentCapabilities()}}}) - awaitDaemonRemoteCondition(t, t.Context(), 3*time.Second, "worker preparation capability", func() bool { + awaitDaemonRemoteCondition(t, t.Context(), 3*time.Second, "worker capabilities", func() bool { peer, err := h.registry.LookupDevice(h.device.ID) if err != nil { return false } info, _, _ := peer.AgentKindStatus("codex") - return info.Capabilities.Preparation && info.Capabilities.EnvironmentNone + return info.Capabilities == workerEnvironmentCapabilities() }) } func workerEnvironmentCapabilities() proto.AgentKindCapabilities { - return prototest.Capabilities(proto.AgentKindCapabilities{Streaming: proto.CapabilitySupported, Steering: proto.CapabilitySupported, DurableTurns: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported, WebSearchControl: proto.CapabilitySupported, TextVerbosity: proto.CapabilitySupported, ExecutionControls: proto.CapabilitySupported, SubagentControl: proto.CapabilitySupported, ToolObservations: proto.CapabilitySupported, Preparation: proto.CapabilitySupported, LocalEnvironment: proto.CapabilitySupported, WorkspaceReadPreparation: proto.CapabilitySupported, WorkspaceOutputExport: proto.CapabilitySupported}) + return prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported, WebSearchControl: proto.CapabilitySupported, TextVerbosity: proto.CapabilitySupported, SubagentControl: proto.CapabilitySupported, LocalEnvironment: proto.CapabilitySupported, WorkspaceReadPreparation: proto.CapabilitySupported, WorkspaceOutputExport: proto.CapabilitySupported}) } func workerEnvironmentReservation(t *testing.T, h *dispatchHarness) sessions.EnvironmentInputReservation { diff --git a/services/core/tests/integration/environment_worker_scan_test.go b/services/core/tests/integration/environment_worker_scan_test.go index 5149190ff..5d0c408e2 100644 --- a/services/core/tests/integration/environment_worker_scan_test.go +++ b/services/core/tests/integration/environment_worker_scan_test.go @@ -14,7 +14,7 @@ func TestWorkerEnvironmentRetriesNewlyReadyAtNextScan(t *testing.T) { pending := workerEnvironmentReservation(t, h) runtime := h.environments[pending.SessionID] caps := workerEnvironmentCapabilities() - caps.Preparation = proto.CapabilityUnsupported + caps.WorkspaceReadPreparation = proto.CapabilityUnsupported awaitFixtureCapabilities(t, runtime, caps) frames := workerFrames(t, h, runtime) diff --git a/services/core/tests/integration/execution_messages_test.go b/services/core/tests/integration/execution_messages_test.go index 22ffa33b5..9a4b3f044 100644 --- a/services/core/tests/integration/execution_messages_test.go +++ b/services/core/tests/integration/execution_messages_test.go @@ -24,7 +24,7 @@ func TestExecutionNegotiatesAndPersistsMessageObservations(t *testing.T) { } h.write(first.TurnID, proto.TypeDone, proto.DonePayload{}) h.finished(result, sessions.TurnCompleted) - h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported, SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Streaming: proto.CapabilitySupported, Steering: proto.CapabilitySupported, Resume: proto.CapabilitySupported, DurableTurns: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported, WebSearchControl: proto.CapabilitySupported, TextVerbosity: proto.CapabilitySupported, ExecutionControls: proto.CapabilitySupported, SubagentControl: proto.CapabilitySupported, ToolObservations: proto.CapabilitySupported, MessageItems: proto.CapabilitySupported, NativeSessionRecovery: proto.CapabilitySupported, Preparation: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported})}}}) + h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported, SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{WebSearchControl: proto.CapabilitySupported, TextVerbosity: proto.CapabilitySupported, SubagentControl: proto.CapabilitySupported, MessageItems: proto.CapabilitySupported, NativeSessionRecovery: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported})}}}) deadline := time.Now().Add(3 * time.Second) for { peer, err := h.registry.LookupDevice(h.device.ID) @@ -32,7 +32,7 @@ func TestExecutionNegotiatesAndPersistsMessageObservations(t *testing.T) { t.Fatal(err) } info, _, _ := peer.AgentKindStatus("codex") - if info.Capabilities.MessageItems { + if info.Capabilities.MessageItems.IsSupported() { break } if time.Now().After(deadline) { diff --git a/services/core/tests/integration/function_execution_test.go b/services/core/tests/integration/function_execution_test.go index 76367912f..05df7bee4 100644 --- a/services/core/tests/integration/function_execution_test.go +++ b/services/core/tests/integration/function_execution_test.go @@ -27,12 +27,12 @@ func newFunctionHarness(t *testing.T) *dispatchHarness { if err := bindSessionDevice(t, h.s, h.tenant, h.session.ID, h.device.ID); err != nil { t.Fatal(err) } - h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported, SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Streaming: proto.CapabilitySupported, Steering: proto.CapabilitySupported, Resume: proto.CapabilitySupported, DurableTurns: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported, WebSearchControl: proto.CapabilitySupported, TextVerbosity: proto.CapabilitySupported, ExecutionControls: proto.CapabilitySupported, SubagentControl: proto.CapabilitySupported, ToolObservations: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported, FunctionTools: proto.CapabilitySupported, FunctionResultImages: proto.CapabilitySupported, Preparation: proto.CapabilitySupported})}}}) + h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported, SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{WebSearchControl: proto.CapabilitySupported, TextVerbosity: proto.CapabilitySupported, SubagentControl: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported, FunctionTools: proto.CapabilitySupported, FunctionResultImages: proto.CapabilitySupported})}}}) deadline := time.Now().Add(time.Second) for { peer, _ := h.registry.LookupDevice(h.device.ID) info, _, _ := peer.AgentKindStatus("codex") - if info.Capabilities.FunctionTools { + if info.Capabilities.FunctionTools.IsSupported() { return h } if time.Now().After(deadline) { diff --git a/services/core/tests/integration/function_worker_test.go b/services/core/tests/integration/function_worker_test.go index c5d28736d..13d50167d 100644 --- a/services/core/tests/integration/function_worker_test.go +++ b/services/core/tests/integration/function_worker_test.go @@ -18,7 +18,7 @@ import ( ) func TestWorkerWaitsForToolCapabilities(t *testing.T) { - for _, missing := range []string{"preparation", "durable_input_receipts", "execution_controls", "function_tools", "tool_observations", "mcp_http_tools", "mcp_http_bearer_auth", "mcp_http_required"} { + for _, missing := range []string{"function_tools", "mcp_http_tools", "mcp_http_bearer_auth", "mcp_http_required"} { for _, prebound := range []bool{false, true} { t.Run(missing+"/"+map[bool]string{false: "select", true: "bound"}[prebound], func(t *testing.T) { h := newFunctionHarness(t) @@ -46,7 +46,7 @@ func TestWorkerWaitsForToolCapabilities(t *testing.T) { t.Fatal(err) } } - caps := prototest.Capabilities(proto.AgentKindCapabilities{Preparation: proto.CapabilityFromBool(missing != "preparation"), Streaming: proto.CapabilitySupported, Steering: proto.CapabilitySupported, DurableTurns: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilityFromBool(missing != "durable_input_receipts"), EnvironmentNone: proto.CapabilitySupported, WebSearchControl: proto.CapabilitySupported, TextVerbosity: proto.CapabilitySupported, ExecutionControls: proto.CapabilityFromBool(missing != "execution_controls"), SubagentControl: proto.CapabilitySupported, ToolObservations: proto.CapabilityFromBool(missing != "tool_observations"), MCPHTTPTools: proto.CapabilityFromBool(missing != "mcp_http_tools"), MCPHTTPRequired: proto.CapabilityFromBool(missing != "mcp_http_required"), MCPHTTPBearerAuth: proto.CapabilityFromBool(missing != "mcp_http_bearer_auth"), FunctionTools: proto.CapabilityFromBool(missing != "function_tools" && !isMCP)}) + caps := prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported, WebSearchControl: proto.CapabilitySupported, TextVerbosity: proto.CapabilitySupported, SubagentControl: proto.CapabilitySupported, MCPHTTPTools: proto.CapabilityFromBool(missing != "mcp_http_tools"), MCPHTTPRequired: proto.CapabilityFromBool(missing != "mcp_http_required"), MCPHTTPBearerAuth: proto.CapabilityFromBool(missing != "mcp_http_bearer_auth"), FunctionTools: proto.CapabilityFromBool(missing != "function_tools" && !isMCP)}) heartbeat := func() { h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported, SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: caps}}}) } @@ -55,7 +55,7 @@ func TestWorkerWaitsForToolCapabilities(t *testing.T) { for { peer, _ := h.registry.LookupDevice(h.device.ID) info, _, _ := peer.AgentKindStatus("codex") - if info.Capabilities.Preparation == caps.Preparation.IsSupported() && info.Capabilities.DurableInputReceipts == caps.DurableInputReceipts.IsSupported() && info.Capabilities.ExecutionControls == caps.ExecutionControls.IsSupported() && info.Capabilities.FunctionTools == caps.FunctionTools.IsSupported() && info.Capabilities.ToolObservations == caps.ToolObservations.IsSupported() && info.Capabilities.MCPHTTPTools == caps.MCPHTTPTools.IsSupported() && info.Capabilities.MCPHTTPBearerAuth == caps.MCPHTTPBearerAuth.IsSupported() && info.Capabilities.MCPHTTPRequired == caps.MCPHTTPRequired.IsSupported() { + if info.Capabilities == caps { break } if time.Now().After(deadline) { @@ -87,8 +87,7 @@ func TestWorkerWaitsForToolCapabilities(t *testing.T) { t.Fatal("bound an incapable device", err) } } - caps.Preparation, caps.DurableInputReceipts, caps.ExecutionControls, caps.ToolObservations, caps.MCPHTTPTools = proto.CapabilitySupported, proto.CapabilitySupported, proto.CapabilitySupported, proto.CapabilitySupported, proto.CapabilitySupported - caps.MCPHTTPBearerAuth, caps.FunctionTools, caps.MCPHTTPRequired = proto.CapabilitySupported, proto.CapabilityFromBool(!isMCP), proto.CapabilitySupported + caps.MCPHTTPTools, caps.MCPHTTPBearerAuth, caps.FunctionTools, caps.MCPHTTPRequired = proto.CapabilitySupported, proto.CapabilitySupported, proto.CapabilityFromBool(!isMCP), proto.CapabilitySupported heartbeat() request := h.read(testExecutionRequest) var prompt proto.PromptRequestPayload diff --git a/services/core/tests/integration/harness_onboarding_test.go b/services/core/tests/integration/harness_onboarding_test.go index f8ad10c66..6dcef7f2a 100644 --- a/services/core/tests/integration/harness_onboarding_test.go +++ b/services/core/tests/integration/harness_onboarding_test.go @@ -122,11 +122,11 @@ func TestThirdHarnessPublicOnboarding(t *testing.T) { } request("POST", "/v1/agents/sessions/"+created.ID+"/events", `{"events":[{"type":"agent.session.input.cancel"}]}`, 202) waitTurn(t, h, next.RunID, sessions.TurnCancelled) - // A missing mandatory receipt capability must prevent claiming queued work. + // A missing required capability must prevent claiming queued work. peer, _ := h.registry.LookupDevice(h.device.ID) // Mutate the actual wire declaration, not its lossy persisted boolean projection. changed := declaration - changed.Capabilities.DurableInputReceipts = proto.CapabilityUnsupported + changed.Capabilities.SubagentControl = proto.CapabilityUnsupported // A separate unbound Session is used, without changing public handler behavior. update, _ := proto.NewEnvelope(proto.TypeHeartbeat, "", proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported, SupportedAgentKinds: []proto.SupportedAgentKind{changed}}) if err := write(update); err != nil { @@ -134,7 +134,7 @@ func TestThirdHarnessPublicOnboarding(t *testing.T) { } for deadline := time.Now().Add(3 * time.Second); ; { current, _, _ := peer.AgentKindStatus("mcode") - if !current.Capabilities.DurableInputReceipts { + if !current.Capabilities.SubagentControl.IsSupported() { break } if time.Now().After(deadline) { diff --git a/services/core/tests/integration/local_environment_worker_test.go b/services/core/tests/integration/local_environment_worker_test.go index f26451725..0348e2bde 100644 --- a/services/core/tests/integration/local_environment_worker_test.go +++ b/services/core/tests/integration/local_environment_worker_test.go @@ -25,12 +25,10 @@ func localWorker(t *testing.T, scoped, execute bool) (*dispatchHarness, *executi if err != nil { t.Fatal(err) } - caps := prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported, Preparation: proto.CapabilitySupported, WorkspaceReadPreparation: proto.CapabilitySupported}) + caps := prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported, WorkspaceReadPreparation: proto.CapabilitySupported}) if execute { caps.WorkspaceOutputExport = proto.CapabilitySupported - caps.Streaming, caps.Steering, caps.DurableTurns, caps.DurableInputReceipts = proto.CapabilitySupported, proto.CapabilitySupported, proto.CapabilitySupported, proto.CapabilitySupported - caps.WebSearchControl, caps.TextVerbosity, caps.ExecutionControls = proto.CapabilitySupported, proto.CapabilitySupported, proto.CapabilitySupported - caps.SubagentControl, caps.ToolObservations = proto.CapabilitySupported, proto.CapabilitySupported + caps.WebSearchControl, caps.TextVerbosity, caps.SubagentControl = proto.CapabilitySupported, proto.CapabilitySupported, proto.CapabilitySupported } h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported, SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: caps}}}) awaitDaemonRemoteCondition(t, t.Context(), 3*time.Second, "local capability", func() bool { @@ -39,7 +37,7 @@ func localWorker(t *testing.T, scoped, execute bool) (*dispatchHarness, *executi return false } info, _, _ := peer.AgentKindStatus("codex") - return info.Capabilities.LocalEnvironment + return info.Capabilities.LocalEnvironment.IsSupported() }) w := startWorker(t, t.Context(), h.s, h.d) ctx, cancel := context.WithCancel(t.Context()) diff --git a/services/core/tests/integration/mcode_public_native_test.go b/services/core/tests/integration/mcode_public_native_test.go index f592fc60e..84c6f6ab0 100644 --- a/services/core/tests/integration/mcode_public_native_test.go +++ b/services/core/tests/integration/mcode_public_native_test.go @@ -159,7 +159,7 @@ func startNativeEngineDaemon(t *testing.T, h *dispatchHarness, home, binary, eng deadline := time.Now().Add(45 * time.Second) for time.Now().Before(deadline) { if peer, err := h.registry.LookupDevice(h.device.ID); err == nil && peer != old { - if info, found, known := peer.AgentKindStatus(engine); found && known && info.Available && info.Capabilities.EnvironmentNone { + if info, found, known := peer.AgentKindStatus(engine); found && known && info.Available && info.Capabilities.EnvironmentNone.IsSupported() { return stop } } diff --git a/services/core/tests/integration/native_daemon_test.go b/services/core/tests/integration/native_daemon_test.go index 908f769a9..ac6cb9dee 100644 --- a/services/core/tests/integration/native_daemon_test.go +++ b/services/core/tests/integration/native_daemon_test.go @@ -92,7 +92,7 @@ func startNativeDispatchDaemon(t *testing.T, h *dispatchHarness, home, binary st for { peer, e := h.registry.LookupDevice(h.device.ID) if e == nil && peer != oldPeer { - if info, found, known := peer.AgentKindStatus("codex"); known && found && info.Available && info.Capabilities.EnvironmentNone { + if info, found, known := peer.AgentKindStatus("codex"); known && found && info.Available && info.Capabilities.EnvironmentNone.IsSupported() { break } } diff --git a/services/core/tests/integration/public_execution_test.go b/services/core/tests/integration/public_execution_test.go index 92ea75caf..b9bb6b379 100644 --- a/services/core/tests/integration/public_execution_test.go +++ b/services/core/tests/integration/public_execution_test.go @@ -23,7 +23,7 @@ func publicSession(t *testing.T, h *dispatchHarness, key string) sessions.Sessio func TestExecutionWorkerAdmissionBindingAndRecovery(t *testing.T) { h := newDispatchHarness(t) - h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported, SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Streaming: proto.CapabilitySupported, Steering: proto.CapabilitySupported, DurableTurns: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported, WebSearchControl: proto.CapabilitySupported, TextVerbosity: proto.CapabilitySupported, ExecutionControls: proto.CapabilitySupported, SubagentControl: proto.CapabilitySupported, ToolObservations: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported, Preparation: proto.CapabilitySupported})}}}) + h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported, SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{WebSearchControl: proto.CapabilitySupported, TextVerbosity: proto.CapabilitySupported, SubagentControl: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported})}}}) h.session = publicSession(t, h, "public") ctx, cancel := context.WithCancel(context.Background()) defer cancel() diff --git a/services/core/tests/integration/structured_output_dispatch_test.go b/services/core/tests/integration/structured_output_dispatch_test.go index 0b32c1194..989f49429 100644 --- a/services/core/tests/integration/structured_output_dispatch_test.go +++ b/services/core/tests/integration/structured_output_dispatch_test.go @@ -30,12 +30,12 @@ func TestStructuredOutputDispatchRechecksOperationQualification(t *testing.T) { if err = bindSessionDevice(t, h.s, h.tenant, h.session.ID, h.device.ID); err != nil { t.Fatal(err) } - caps := prototest.Capabilities(proto.AgentKindCapabilities{Streaming: proto.CapabilitySupported, Steering: proto.CapabilitySupported, DurableTurns: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported, ExecutionControls: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported, SubagentControl: proto.CapabilitySupported, ToolObservations: proto.CapabilitySupported, StructuredOutput: proto.CapabilitySupported, MessageItems: proto.CapabilitySupported, Preparation: proto.CapabilitySupported}) + caps := prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported, SubagentControl: proto.CapabilitySupported, StructuredOutput: proto.CapabilitySupported, MessageItems: proto.CapabilitySupported}) h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported, SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "fixture_harness", Available: true, Capabilities: caps}}}) peer, _ := h.registry.LookupDevice(h.device.ID) for deadline := time.Now().Add(3 * time.Second); ; { info, found, known := peer.AgentKindStatus("fixture_harness") - if known && found && info.Capabilities.StructuredOutput { + if known && found && info.Capabilities.StructuredOutput.IsSupported() { break } if time.Now().After(deadline) { diff --git a/services/core/tests/integration/worker_lease_loss_test.go b/services/core/tests/integration/worker_lease_loss_test.go index 45b5d0950..4ec4c715d 100644 --- a/services/core/tests/integration/worker_lease_loss_test.go +++ b/services/core/tests/integration/worker_lease_loss_test.go @@ -14,7 +14,7 @@ import ( func TestWorkerLeaseLossLeavesUncertainWorkForSuccessor(t *testing.T) { h := newDispatchHarness(t) _, pool := testStore(t) - h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported, SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Streaming: proto.CapabilitySupported, Steering: proto.CapabilitySupported, DurableTurns: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported, WebSearchControl: proto.CapabilitySupported, TextVerbosity: proto.CapabilitySupported, ExecutionControls: proto.CapabilitySupported, SubagentControl: proto.CapabilitySupported, ToolObservations: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported, Preparation: proto.CapabilitySupported})}}}) + h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported, SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{WebSearchControl: proto.CapabilitySupported, TextVerbosity: proto.CapabilitySupported, SubagentControl: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported})}}}) h.session = publicSession(t, h, "active") queued := publicSession(t, h, "queued") worker := startWorker(t, t.Context(), h.s, h.d)