From 81fe6f33a8da9bda320fae4e3b51e9487ae79f4e Mon Sep 17 00:00:00 2001 From: saladday <1203511142@qq.com> Date: Wed, 30 Sep 2026 13:29:57 +0800 Subject: [PATCH 01/12] Require explicit Runtime capability support declarations --- .../internal/agent/unsupported.go | 8 ++ internal/agentdaemon/proto/capabilities.go | 112 ++++++++++++++++++ internal/agentdaemon/proto/inbound.go | 68 +++++------ .../proto/prototest/capabilities.go | 55 +++++++++ internal/agentdaemon/proto/version.go | 2 +- 5 files changed, 210 insertions(+), 35 deletions(-) create mode 100644 apps/parsar-daemon/internal/agent/unsupported.go create mode 100644 internal/agentdaemon/proto/capabilities.go create mode 100644 internal/agentdaemon/proto/prototest/capabilities.go diff --git a/apps/parsar-daemon/internal/agent/unsupported.go b/apps/parsar-daemon/internal/agent/unsupported.go new file mode 100644 index 000000000..55cca49e6 --- /dev/null +++ b/apps/parsar-daemon/internal/agent/unsupported.go @@ -0,0 +1,8 @@ +package agent + +import "errors" + +// ErrUnsupportedOperation marks an intentional absence of optional behavior. +// Implementations wrap it with a static reason and perform no native effects. +// It is distinct from unavailable resources, failures and uncertain outcomes. +var ErrUnsupportedOperation = errors.New("agent: unsupported operation") diff --git a/internal/agentdaemon/proto/capabilities.go b/internal/agentdaemon/proto/capabilities.go new file mode 100644 index 000000000..22d79c8fc --- /dev/null +++ b/internal/agentdaemon/proto/capabilities.go @@ -0,0 +1,112 @@ +package proto + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "reflect" +) + +// CapabilitySupport requires a deliberate decision. Its zero value is invalid; +// adding a field cannot silently advertise an implementation as unsupported. +type CapabilitySupport uint8 + +const ( + CapabilityUnspecified CapabilitySupport = iota + CapabilityUnsupported + CapabilitySupported +) + +func (s CapabilitySupport) IsSupported() bool { return s == CapabilitySupported } + +// CapabilityFromBool converts one explicitly selected capability decision. +func CapabilityFromBool(supported bool) CapabilitySupport { + if supported { + return CapabilitySupported + } + return CapabilityUnsupported +} + +func (s CapabilitySupport) MarshalJSON() ([]byte, error) { + switch s { + case CapabilitySupported: + return []byte("true"), nil + case CapabilityUnsupported: + return []byte("false"), nil + default: + return nil, errors.New("capability support must be explicitly declared") + } +} + +func (s *CapabilitySupport) UnmarshalJSON(data []byte) error { + switch string(bytes.TrimSpace(data)) { + case "true": + *s = CapabilitySupported + case "false": + *s = CapabilityUnsupported + default: + return errors.New("capability support must be a boolean") + } + return nil +} + +// ValidateDeclaration covers every field, including future additions. The +// struct remains flat so new fields cannot bypass validation through nesting. +func (c AgentKindCapabilities) ValidateDeclaration() error { + v := reflect.ValueOf(c) + t := v.Type() + for i := 0; i < v.NumField(); i++ { + s, ok := v.Field(i).Interface().(CapabilitySupport) + if !ok || (s != CapabilitySupported && s != CapabilityUnsupported) { + return fmt.Errorf("capability %s must be explicitly declared", t.Field(i).Name) + } + } + return nil +} + +func (c AgentKindCapabilities) MarshalJSON() ([]byte, error) { + if err := c.ValidateDeclaration(); err != nil { + return nil, err + } + type declaration AgentKindCapabilities + return json.Marshal(declaration(c)) +} + +func (c *AgentKindCapabilities) UnmarshalJSON(data []byte) error { + type declaration AgentKindCapabilities + var decoded declaration + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&decoded); err != nil { + // Do not echo unknown keys or untrusted payload values. + return errors.New("invalid capability declaration") + } + result := AgentKindCapabilities(decoded) + if err := result.ValidateDeclaration(); err != nil { + return err + } + *c = result + return nil +} + +func (k SupportedAgentKind) ValidateDeclaration() error { + if k.Kind == "" { + return errors.New("agent kind is required") + } + return k.Capabilities.ValidateDeclaration() +} + +func (k *SupportedAgentKind) UnmarshalJSON(data []byte) error { + type descriptor SupportedAgentKind + var decoded descriptor + if err := json.Unmarshal(data, &decoded); err != nil { + return err + } + result := SupportedAgentKind(decoded) + if err := result.ValidateDeclaration(); err != nil { + return err + } + *k = result + return nil +} diff --git a/internal/agentdaemon/proto/inbound.go b/internal/agentdaemon/proto/inbound.go index 9ba27830c..3ef15cc16 100644 --- a/internal/agentdaemon/proto/inbound.go +++ b/internal/agentdaemon/proto/inbound.go @@ -216,43 +216,43 @@ const ( ) // AgentKindCapabilities describes what a daemon-side agent_kind can -// do inside one prompt session. Runtime-level capabilities such as -// cancellation belong to the daemon connector itself; these bits are -// the engine-specific surface the UI uses for filtering and copy. +// 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. type AgentKindCapabilities struct { - SubagentObservations bool `json:"subagent_observations,omitempty"` - Streaming bool `json:"streaming,omitempty"` - Permissions bool `json:"permissions,omitempty"` - Usage bool `json:"usage,omitempty"` - Resume bool `json:"resume,omitempty"` - NativeSessionRecovery bool `json:"native_session_recovery,omitempty"` - WorkspaceAuthoring bool `json:"workspace_authoring,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"` + SubagentObservations CapabilitySupport `json:"subagent_observations"` + Streaming CapabilitySupport `json:"streaming"` + Permissions CapabilitySupport `json:"permissions"` + Usage CapabilitySupport `json:"usage"` + Resume CapabilitySupport `json:"resume"` + NativeSessionRecovery CapabilitySupport `json:"native_session_recovery"` + WorkspaceAuthoring CapabilitySupport `json:"workspace_authoring"` + 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 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"` - DurableInputReceipts bool `json:"durable_input_receipts,omitempty"` + 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 bool `json:"durable_turns,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"` + 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"` } // SupportedAgentKind is one daemon-advertised agent engine. Daemons diff --git a/internal/agentdaemon/proto/prototest/capabilities.go b/internal/agentdaemon/proto/prototest/capabilities.go new file mode 100644 index 000000000..baf5c8a25 --- /dev/null +++ b/internal/agentdaemon/proto/prototest/capabilities.go @@ -0,0 +1,55 @@ +// Package prototest provides explicit capability fixtures for contract tests. +package prototest + +import ( + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "reflect" +) + +// Capabilities declares the current contract, then applies explicit overrides. +// List fields individually: a future field must remain unspecified until this +// fixture is deliberately updated, so declaration validation fails on omission. +func Capabilities(overrides proto.AgentKindCapabilities) proto.AgentKindCapabilities { + c := proto.AgentKindCapabilities{ + SubagentObservations: proto.CapabilityUnsupported, + Streaming: proto.CapabilityUnsupported, + Permissions: proto.CapabilityUnsupported, + Usage: proto.CapabilityUnsupported, + Resume: proto.CapabilityUnsupported, + NativeSessionRecovery: proto.CapabilityUnsupported, + WorkspaceAuthoring: 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, + MCPHTTPBearerAuth: proto.CapabilityUnsupported, + } + dst, src := reflect.ValueOf(&c).Elem(), reflect.ValueOf(overrides) + for i := 0; i < src.NumField(); i++ { + if !src.Field(i).IsZero() { + dst.Field(i).Set(src.Field(i)) + } + } + if err := c.ValidateDeclaration(); err != nil { + panic(err) + } + return c +} diff --git a/internal/agentdaemon/proto/version.go b/internal/agentdaemon/proto/version.go index a2214276f..9ff1c71ec 100644 --- a/internal/agentdaemon/proto/version.go +++ b/internal/agentdaemon/proto/version.go @@ -2,7 +2,7 @@ package proto // Version identifies the complete Core–Runtime wire contract. Change it when // removing or changing a payload or its semantics; deploy both endpoints together. -const Version = "0.10.0" +const Version = "0.11.0" // VersionCompatible accepts only this contract. Patch drift, prerelease suffixes // and malformed versions do not select an implicit compatibility path. From c415c572125c5d8e6db28d036f963f2cced28631 Mon Sep 17 00:00:00 2001 From: saladday <1203511142@qq.com> Date: Wed, 30 Sep 2026 13:24:20 +0800 Subject: [PATCH 02/12] Make Harness extension implementations and declarations explicit --- CONTRIBUTING.md | 4 + .../internal/agent/claudesdk/contracts.go | 27 +++ .../internal/agent/claudesdk/unsupported.go | 28 +++ .../agent/claudesdk/unsupported_test.go | 38 +++++ .../internal/agent/codex/contracts.go | 27 +++ .../internal/agent/codex/unsupported.go | 42 +++++ .../internal/agent/codex/unsupported_test.go | 48 ++++++ apps/parsar-daemon/internal/agent/harness.go | 27 ++- .../internal/agent/mcode/contracts.go | 27 +++ .../internal/agent/mcode/unsupported.go | 48 ++++++ .../internal/agent/mcode/unsupported_test.go | 51 ++++++ .../internal/cli/agent_discovery.go | 159 ++++++++++++++---- apps/parsar-daemon/internal/cli/claude_sdk.go | 66 +++++--- apps/parsar-daemon/internal/cli/mcode.go | 55 ++++-- .../internal/cli/mcode_workspace.go | 7 +- contracts/agents-api/harness-onboarding.md | 124 +++++++++++--- 16 files changed, 671 insertions(+), 107 deletions(-) create mode 100644 apps/parsar-daemon/internal/agent/claudesdk/contracts.go create mode 100644 apps/parsar-daemon/internal/agent/claudesdk/unsupported.go create mode 100644 apps/parsar-daemon/internal/agent/claudesdk/unsupported_test.go create mode 100644 apps/parsar-daemon/internal/agent/codex/contracts.go create mode 100644 apps/parsar-daemon/internal/agent/codex/unsupported.go create mode 100644 apps/parsar-daemon/internal/agent/codex/unsupported_test.go create mode 100644 apps/parsar-daemon/internal/agent/mcode/contracts.go create mode 100644 apps/parsar-daemon/internal/agent/mcode/unsupported.go create mode 100644 apps/parsar-daemon/internal/agent/mcode/unsupported_test.go diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 44fc210e7..5b8579825 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -210,6 +210,10 @@ The Runtime is not a sandbox; see - Fix shared lifecycle, admission, cancellation, reuse and performance problems in the common protocol or flow, not with Harness-, Runtime- or vendor-specific branches in Core. Adapters may differ natively but keep shared semantics. +- Public Harnesses explicitly implement every extension interface, returning the + shared Unsupported error when unqualified; required lifecycle obligations cannot + be skipped. Capability declarations must be complete. Follow the single + [Harness onboarding contract](contracts/agents-api/harness-onboarding.md). - Express compatibility through declared capabilities and validate selected combinations explicitly. Public MCP origin and credential authority follow the [Environment MCP contract](contracts/agents-api/environments.md#public-mcp-connection-origin); diff --git a/apps/parsar-daemon/internal/agent/claudesdk/contracts.go b/apps/parsar-daemon/internal/agent/claudesdk/contracts.go new file mode 100644 index 000000000..58cd240ab --- /dev/null +++ b/apps/parsar-daemon/internal/agent/claudesdk/contracts.go @@ -0,0 +1,27 @@ +package claudesdk + +import "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" + +// Every public Harness implements each small contract explicitly. Unsupported +// extensions return agent.ErrUnsupportedOperation without native effects. +var ( + _ agent.Executor = (*executor)(nil) + _ agent.Turn = (*session)(nil) + _ agent.Session = (*session)(nil) + _ agent.DurableSteerer = (*session)(nil) + _ agent.Steerer = (*session)(nil) + _ agent.FunctionResultSubmitter = (*session)(nil) + _ agent.PermissionResponder = (*session)(nil) + _ agent.UserChoiceResponder = (*session)(nil) + _ agent.WorkspaceReader = (*session)(nil) + _ agent.WorkspaceDirectoryLister = (*session)(nil) + _ agent.WorkspaceWriter = (*session)(nil) + _ agent.Prepared = (*prepared)(nil) + _ agent.PreparedCancellation = (*prepared)(nil) + _ agent.WorkspaceReader = (*executor)(nil) + _ agent.WorkspaceDirectoryLister = (*executor)(nil) + _ agent.WorkspaceWriter = (*executor)(nil) + _ agent.WorkspaceReader = (*prepared)(nil) + _ agent.WorkspaceDirectoryLister = (*prepared)(nil) + _ agent.WorkspaceWriter = (*prepared)(nil) +) diff --git a/apps/parsar-daemon/internal/agent/claudesdk/unsupported.go b/apps/parsar-daemon/internal/agent/claudesdk/unsupported.go new file mode 100644 index 000000000..e6d650711 --- /dev/null +++ b/apps/parsar-daemon/internal/agent/claudesdk/unsupported.go @@ -0,0 +1,28 @@ +package claudesdk + +import ( + "context" + "fmt" + "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" +) + +func (s *session) SubmitPermission(context.Context, string, proto.PermissionDecisionPayload) error { + return fmt.Errorf("%w: native permission responses are not exposed", agent.ErrUnsupportedOperation) +} + +func (s *session) SubmitPromptForUserChoice(context.Context, string, proto.PromptForUserChoiceDecisionPayload) error { + return fmt.Errorf("%w: native user-choice responses are not exposed", agent.ErrUnsupportedOperation) +} + +func (s *executor) WriteWorkspaceFile(context.Context, string, []byte) (agent.WorkspaceWriteResult, error) { + return agent.WorkspaceWriteResult{}, agent.ErrWorkspaceWriteUnsupported +} + +func (s *session) WriteWorkspaceFile(context.Context, string, []byte) (agent.WorkspaceWriteResult, error) { + return agent.WorkspaceWriteResult{}, agent.ErrWorkspaceWriteUnsupported +} + +func (s *prepared) WriteWorkspaceFile(context.Context, string, []byte) (agent.WorkspaceWriteResult, error) { + return agent.WorkspaceWriteResult{}, agent.ErrWorkspaceWriteUnsupported +} diff --git a/apps/parsar-daemon/internal/agent/claudesdk/unsupported_test.go b/apps/parsar-daemon/internal/agent/claudesdk/unsupported_test.go new file mode 100644 index 000000000..8b8f77c88 --- /dev/null +++ b/apps/parsar-daemon/internal/agent/claudesdk/unsupported_test.go @@ -0,0 +1,38 @@ +package claudesdk + +import ( + "context" + "errors" + "strings" + "testing" + + "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" +) + +// Nil concrete receivers prove Unsupported needs no native owner, transport, +// workspace access or input retention. Callers still preserve the separate +// nil-Turn ownership rule on required lifecycle methods. +func TestUnsupportedExtensionsHaveNoNativeEffects(t *testing.T) { + ctx := context.Background() + const secret = "private-fixture-value" + check := func(err error) { + t.Helper() + if !errors.Is(err, agent.ErrUnsupportedOperation) || err.Error() == agent.ErrUnsupportedOperation.Error() { + t.Fatalf("expected explicit unsupported result with reason, got %v", err) + } + if strings.Contains(err.Error(), secret) { + t.Fatal("unsupported error disclosed input") + } + } + var turn *session + check(turn.SubmitPermission(ctx, secret, proto.PermissionDecisionPayload{})) + check(turn.SubmitPromptForUserChoice(ctx, secret, proto.PromptForUserChoiceDecisionPayload{})) + for _, owner := range []agent.WorkspaceWriter{(*executor)(nil), (*session)(nil), (*prepared)(nil)} { + result, err := owner.WriteWorkspaceFile(ctx, secret, []byte(secret)) + check(err) + if result.SizeBytes != 0 { + t.Fatal("unsupported write fabricated receipt") + } + } +} diff --git a/apps/parsar-daemon/internal/agent/codex/contracts.go b/apps/parsar-daemon/internal/agent/codex/contracts.go new file mode 100644 index 000000000..d3fb3e356 --- /dev/null +++ b/apps/parsar-daemon/internal/agent/codex/contracts.go @@ -0,0 +1,27 @@ +package codex + +import "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" + +// Every public Harness implements each small contract explicitly. Unsupported +// extensions return agent.ErrUnsupportedOperation without native effects. +var ( + _ agent.Executor = (*Executor)(nil) + _ agent.Turn = (*Session)(nil) + _ agent.Session = (*Session)(nil) + _ agent.DurableSteerer = (*Session)(nil) + _ agent.Steerer = (*Session)(nil) + _ agent.FunctionResultSubmitter = (*Session)(nil) + _ agent.PermissionResponder = (*Session)(nil) + _ agent.UserChoiceResponder = (*Session)(nil) + _ agent.WorkspaceReader = (*Session)(nil) + _ agent.WorkspaceDirectoryLister = (*Session)(nil) + _ agent.WorkspaceWriter = (*Session)(nil) + _ agent.Prepared = (*Prepared)(nil) + _ agent.PreparedCancellation = (*Prepared)(nil) + _ agent.WorkspaceReader = (*Executor)(nil) + _ agent.WorkspaceDirectoryLister = (*Executor)(nil) + _ agent.WorkspaceWriter = (*Executor)(nil) + _ agent.WorkspaceReader = (*Prepared)(nil) + _ agent.WorkspaceDirectoryLister = (*Prepared)(nil) + _ agent.WorkspaceWriter = (*Prepared)(nil) +) diff --git a/apps/parsar-daemon/internal/agent/codex/unsupported.go b/apps/parsar-daemon/internal/agent/codex/unsupported.go new file mode 100644 index 000000000..ea120f48b --- /dev/null +++ b/apps/parsar-daemon/internal/agent/codex/unsupported.go @@ -0,0 +1,42 @@ +package codex + +import ( + "context" + "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" +) + +func (s *Executor) ReadWorkspaceFile(context.Context, string, int) (agent.WorkspaceReadResult, error) { + return agent.WorkspaceReadResult{}, agent.ErrWorkspaceReadUnsupported +} + +func (s *Executor) ListWorkspaceDirectory(context.Context, string, int) (agent.WorkspaceDirectoryResult, error) { + return agent.WorkspaceDirectoryResult{}, agent.ErrWorkspaceReadUnsupported +} + +func (s *Executor) WriteWorkspaceFile(context.Context, string, []byte) (agent.WorkspaceWriteResult, error) { + return agent.WorkspaceWriteResult{}, agent.ErrWorkspaceWriteUnsupported +} + +func (s *Session) ReadWorkspaceFile(context.Context, string, int) (agent.WorkspaceReadResult, error) { + return agent.WorkspaceReadResult{}, agent.ErrWorkspaceReadUnsupported +} + +func (s *Session) ListWorkspaceDirectory(context.Context, string, int) (agent.WorkspaceDirectoryResult, error) { + return agent.WorkspaceDirectoryResult{}, agent.ErrWorkspaceReadUnsupported +} + +func (s *Session) WriteWorkspaceFile(context.Context, string, []byte) (agent.WorkspaceWriteResult, error) { + return agent.WorkspaceWriteResult{}, agent.ErrWorkspaceWriteUnsupported +} + +func (s *Prepared) ReadWorkspaceFile(context.Context, string, int) (agent.WorkspaceReadResult, error) { + return agent.WorkspaceReadResult{}, agent.ErrWorkspaceReadUnsupported +} + +func (s *Prepared) ListWorkspaceDirectory(context.Context, string, int) (agent.WorkspaceDirectoryResult, error) { + return agent.WorkspaceDirectoryResult{}, agent.ErrWorkspaceReadUnsupported +} + +func (s *Prepared) WriteWorkspaceFile(context.Context, string, []byte) (agent.WorkspaceWriteResult, error) { + return agent.WorkspaceWriteResult{}, agent.ErrWorkspaceWriteUnsupported +} diff --git a/apps/parsar-daemon/internal/agent/codex/unsupported_test.go b/apps/parsar-daemon/internal/agent/codex/unsupported_test.go new file mode 100644 index 000000000..49dbad31a --- /dev/null +++ b/apps/parsar-daemon/internal/agent/codex/unsupported_test.go @@ -0,0 +1,48 @@ +package codex + +import ( + "context" + "errors" + "strings" + "testing" + + "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" +) + +// Nil concrete receivers prove Unsupported needs no native owner, transport, +// workspace access or input retention. Callers still preserve the separate +// nil-Turn ownership rule on required lifecycle methods. +func TestUnsupportedExtensionsHaveNoNativeEffects(t *testing.T) { + ctx := context.Background() + const secret = "private-fixture-value" + check := func(err error) { + t.Helper() + if !errors.Is(err, agent.ErrUnsupportedOperation) || err.Error() == agent.ErrUnsupportedOperation.Error() { + t.Fatalf("expected explicit unsupported result with reason, got %v", err) + } + if strings.Contains(err.Error(), secret) { + t.Fatal("unsupported error disclosed input") + } + } + for _, owner := range []agent.WorkspaceReader{(*Executor)(nil), (*Session)(nil), (*Prepared)(nil)} { + result, err := owner.ReadWorkspaceFile(ctx, secret, 1) + check(err) + if len(result.Data) != 0 || result.Truncated { + t.Fatal("unsupported read fabricated data") + } + } + for _, owner := range []agent.WorkspaceDirectoryLister{(*Executor)(nil), (*Session)(nil), (*Prepared)(nil)} { + result, err := owner.ListWorkspaceDirectory(ctx, secret, 1) + check(err) + if len(result.Entries) != 0 || result.Truncated { + t.Fatal("unsupported listing fabricated entries") + } + } + for _, owner := range []agent.WorkspaceWriter{(*Executor)(nil), (*Session)(nil), (*Prepared)(nil)} { + result, err := owner.WriteWorkspaceFile(ctx, secret, []byte(secret)) + check(err) + if result.SizeBytes != 0 { + t.Fatal("unsupported write fabricated receipt") + } + } +} diff --git a/apps/parsar-daemon/internal/agent/harness.go b/apps/parsar-daemon/internal/agent/harness.go index 4d55d11bd..4705842c5 100644 --- a/apps/parsar-daemon/internal/agent/harness.go +++ b/apps/parsar-daemon/internal/agent/harness.go @@ -4,8 +4,10 @@ // // Required lifecycle: ExecutorFactory prepares a fixed Session-owned Executor; // each StartTurn returns a fresh Turn with its own output and settlement. -// The current public text profile also requires DurableSteerer. Keep optional -// interfaces separate and implement them only for qualified operations. MCP, +// Turn includes the public text DurableSteerer requirement. Keep extension +// interfaces separate, but implement each explicitly: unsupported operations +// return ErrUnsupportedOperation before any native effects. Interface presence +// does not advertise support; the capability declaration controls admission. MCP, // images, structured output and Subagent observations use protocol messages // rather than additional Go interfaces; qualify and advertise them separately. // @@ -64,6 +66,7 @@ type Executor interface { // Turn owns one output stream and never retargets cancellation to a successor. type Turn interface { Session + DurableSteerer CancellationOutcome() proto.DonePayload // Success confirms closed output and settled native input, function, // interaction and child-work obligations. Errors cannot prove cancellation. @@ -86,15 +89,18 @@ type Session interface { Cancel(ctx context.Context) error } -// Turn capabilities. Public profiles determine which operations are required. +// Turn extension contracts. Every public Harness implements each interface; +// unsupported operations return ErrUnsupportedOperation with a fixed safe reason. // DurableSteerer reports one complete write synchronously, then waits for the native receipt. -// It is independent of Steerer; the public text path requires only this interface. +// It is independent of Steerer and is mandatory on every public Turn. +// Required input receipts cannot be implemented by returning Unsupported. type DurableSteerer interface { SteerWithReceipt(context.Context, proto.PromptSteerPayload, func()) error } -// Steerer optionally delivers additional text to the session's active turn. +// Steerer delivers non-durable input when qualified; otherwise it explicitly +// returns ErrUnsupportedOperation without submitting input. type Steerer interface { Steer(context.Context, proto.PromptSteerPayload) error } @@ -105,19 +111,24 @@ type FunctionResultSubmitter interface { SubmitFunctionResult(context.Context, proto.FunctionResultPayload) error } -// PermissionResponder optionally accepts decisions for emitted permission requests. +// PermissionResponder accepts decisions for qualified permission requests. +// An adapter that never supports these interactions returns ErrUnsupportedOperation. // Unknown or expired requests return ErrUnknownPermission. type PermissionResponder interface { SubmitPermission(context.Context, string, proto.PermissionDecisionPayload) error } -// UserChoiceResponder optionally accepts answers for emitted user-choice requests. +// UserChoiceResponder accepts answers for qualified user-choice requests. +// An adapter that never supports these interactions returns ErrUnsupportedOperation. // Unknown or expired requests return ErrUnknownAsk. type UserChoiceResponder interface { SubmitPromptForUserChoice(context.Context, string, proto.PromptForUserChoiceDecisionPayload) error } -// Optional workspace capabilities, implemented by the authorized resource owner. +// Workspace extensions, implemented by each owner explicitly. The common +// Runtime may supply an authorized workspace owner independently of the adapter. +// A resource without native access returns the corresponding Unsupported error; +// this does not disable capabilities provided by the common workspace owner. // WorkspaceReader returns success only after acknowledged native close on an existing owner. type WorkspaceReader interface { diff --git a/apps/parsar-daemon/internal/agent/mcode/contracts.go b/apps/parsar-daemon/internal/agent/mcode/contracts.go new file mode 100644 index 000000000..6fac6506c --- /dev/null +++ b/apps/parsar-daemon/internal/agent/mcode/contracts.go @@ -0,0 +1,27 @@ +package mcode + +import "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" + +// Every public Harness implements each small contract explicitly. Unsupported +// extensions return agent.ErrUnsupportedOperation without native effects. +var ( + _ agent.Executor = (*executor)(nil) + _ agent.Turn = (*Session)(nil) + _ agent.Session = (*Session)(nil) + _ agent.DurableSteerer = (*Session)(nil) + _ agent.Steerer = (*Session)(nil) + _ agent.FunctionResultSubmitter = (*Session)(nil) + _ agent.PermissionResponder = (*Session)(nil) + _ agent.UserChoiceResponder = (*Session)(nil) + _ agent.WorkspaceReader = (*Session)(nil) + _ agent.WorkspaceDirectoryLister = (*Session)(nil) + _ agent.WorkspaceWriter = (*Session)(nil) + _ agent.Prepared = (*prepared)(nil) + _ agent.PreparedCancellation = (*prepared)(nil) + _ agent.WorkspaceReader = (*executor)(nil) + _ agent.WorkspaceDirectoryLister = (*executor)(nil) + _ agent.WorkspaceWriter = (*executor)(nil) + _ agent.WorkspaceReader = (*prepared)(nil) + _ agent.WorkspaceDirectoryLister = (*prepared)(nil) + _ agent.WorkspaceWriter = (*prepared)(nil) +) diff --git a/apps/parsar-daemon/internal/agent/mcode/unsupported.go b/apps/parsar-daemon/internal/agent/mcode/unsupported.go new file mode 100644 index 000000000..2940b556a --- /dev/null +++ b/apps/parsar-daemon/internal/agent/mcode/unsupported.go @@ -0,0 +1,48 @@ +package mcode + +import ( + "context" + "fmt" + "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" +) + +func (s *Session) SubmitFunctionResult(context.Context, proto.FunctionResultPayload) error { + return fmt.Errorf("%w: native public function tools are not qualified", agent.ErrUnsupportedOperation) +} + +func (s *executor) ReadWorkspaceFile(context.Context, string, int) (agent.WorkspaceReadResult, error) { + return agent.WorkspaceReadResult{}, agent.ErrWorkspaceReadUnsupported +} + +func (s *executor) ListWorkspaceDirectory(context.Context, string, int) (agent.WorkspaceDirectoryResult, error) { + return agent.WorkspaceDirectoryResult{}, agent.ErrWorkspaceReadUnsupported +} + +func (s *executor) WriteWorkspaceFile(context.Context, string, []byte) (agent.WorkspaceWriteResult, error) { + return agent.WorkspaceWriteResult{}, agent.ErrWorkspaceWriteUnsupported +} + +func (s *Session) ReadWorkspaceFile(context.Context, string, int) (agent.WorkspaceReadResult, error) { + return agent.WorkspaceReadResult{}, agent.ErrWorkspaceReadUnsupported +} + +func (s *Session) ListWorkspaceDirectory(context.Context, string, int) (agent.WorkspaceDirectoryResult, error) { + return agent.WorkspaceDirectoryResult{}, agent.ErrWorkspaceReadUnsupported +} + +func (s *Session) WriteWorkspaceFile(context.Context, string, []byte) (agent.WorkspaceWriteResult, error) { + return agent.WorkspaceWriteResult{}, agent.ErrWorkspaceWriteUnsupported +} + +func (s *prepared) ReadWorkspaceFile(context.Context, string, int) (agent.WorkspaceReadResult, error) { + return agent.WorkspaceReadResult{}, agent.ErrWorkspaceReadUnsupported +} + +func (s *prepared) ListWorkspaceDirectory(context.Context, string, int) (agent.WorkspaceDirectoryResult, error) { + return agent.WorkspaceDirectoryResult{}, agent.ErrWorkspaceReadUnsupported +} + +func (s *prepared) WriteWorkspaceFile(context.Context, string, []byte) (agent.WorkspaceWriteResult, error) { + return agent.WorkspaceWriteResult{}, agent.ErrWorkspaceWriteUnsupported +} diff --git a/apps/parsar-daemon/internal/agent/mcode/unsupported_test.go b/apps/parsar-daemon/internal/agent/mcode/unsupported_test.go new file mode 100644 index 000000000..8f8ca34d7 --- /dev/null +++ b/apps/parsar-daemon/internal/agent/mcode/unsupported_test.go @@ -0,0 +1,51 @@ +package mcode + +import ( + "context" + "errors" + "strings" + "testing" + + "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" +) + +// Nil concrete receivers prove Unsupported needs no native owner, transport, +// workspace access or input retention. Callers still preserve the separate +// nil-Turn ownership rule on required lifecycle methods. +func TestUnsupportedExtensionsHaveNoNativeEffects(t *testing.T) { + ctx := context.Background() + const secret = "private-fixture-value" + check := func(err error) { + t.Helper() + if !errors.Is(err, agent.ErrUnsupportedOperation) || err.Error() == agent.ErrUnsupportedOperation.Error() { + t.Fatalf("expected explicit unsupported result with reason, got %v", err) + } + if strings.Contains(err.Error(), secret) { + t.Fatal("unsupported error disclosed input") + } + } + var turn *Session + check(turn.SubmitFunctionResult(ctx, proto.FunctionResultPayload{CallID: secret, DeliveryID: secret})) + for _, owner := range []agent.WorkspaceReader{(*executor)(nil), (*Session)(nil), (*prepared)(nil)} { + result, err := owner.ReadWorkspaceFile(ctx, secret, 1) + check(err) + if len(result.Data) != 0 || result.Truncated { + t.Fatal("unsupported read fabricated data") + } + } + for _, owner := range []agent.WorkspaceDirectoryLister{(*executor)(nil), (*Session)(nil), (*prepared)(nil)} { + result, err := owner.ListWorkspaceDirectory(ctx, secret, 1) + check(err) + if len(result.Entries) != 0 || result.Truncated { + t.Fatal("unsupported listing fabricated entries") + } + } + for _, owner := range []agent.WorkspaceWriter{(*executor)(nil), (*Session)(nil), (*prepared)(nil)} { + result, err := owner.WriteWorkspaceFile(ctx, secret, []byte(secret)) + check(err) + if result.SizeBytes != 0 { + t.Fatal("unsupported write fabricated receipt") + } + } +} diff --git a/apps/parsar-daemon/internal/cli/agent_discovery.go b/apps/parsar-daemon/internal/cli/agent_discovery.go index 6d9710ff6..9e647a7c0 100644 --- a/apps/parsar-daemon/internal/cli/agent_discovery.go +++ b/apps/parsar-daemon/internal/cli/agent_discovery.go @@ -71,54 +71,141 @@ func discoverAgentCLIs(parent context.Context, rc *runContext, profile string, c ClaudeCode: proto.SupportedAgentKind{ Kind: "claude_code", Capabilities: proto.AgentKindCapabilities{ - Streaming: true, - Permissions: true, - Usage: true, - Resume: true, + SubagentObservations: proto.CapabilityUnsupported, + Streaming: proto.CapabilitySupported, + Permissions: proto.CapabilitySupported, + Usage: proto.CapabilitySupported, + Resume: proto.CapabilitySupported, + NativeSessionRecovery: proto.CapabilityUnsupported, + WorkspaceAuthoring: 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, + MCPHTTPBearerAuth: proto.CapabilityUnsupported, }, }, OpenCode: proto.SupportedAgentKind{ Kind: "opencode", Capabilities: proto.AgentKindCapabilities{ - Streaming: true, - Usage: true, + SubagentObservations: proto.CapabilityUnsupported, + Streaming: proto.CapabilitySupported, + Permissions: proto.CapabilityUnsupported, + Usage: proto.CapabilitySupported, + Resume: proto.CapabilityUnsupported, + NativeSessionRecovery: proto.CapabilityUnsupported, + WorkspaceAuthoring: 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, + MCPHTTPBearerAuth: proto.CapabilityUnsupported, }, }, Codex: proto.SupportedAgentKind{ Kind: "codex", Capabilities: proto.AgentKindCapabilities{ - Streaming: true, - Permissions: true, - Usage: true, - Resume: true, - Steering: true, - DurableTurns: true, - DurableInputReceipts: true, - MessageImages: true, - FunctionResultImages: true, - FunctionTools: true, - MCPHTTPTools: true, - MCPHTTPBearerAuth: true, - MessageItems: true, - - ToolObservations: true, - EnvironmentNone: true, - WebSearchControl: true, - ProgrammaticToolCallingDisable: true, - TextVerbosity: codex.SupportsTextVerbosity, - ExecutionControls: codex.SupportsTextVerbosity, - SubagentControl: true, - SubagentObservations: true, + SubagentObservations: proto.CapabilitySupported, + Streaming: proto.CapabilitySupported, + Permissions: proto.CapabilitySupported, + Usage: proto.CapabilitySupported, + Resume: proto.CapabilitySupported, + NativeSessionRecovery: proto.CapabilityUnsupported, + WorkspaceAuthoring: 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(codex.SupportsTextVerbosity), + TextVerbosity: proto.CapabilityFromBool(codex.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, + MCPHTTPBearerAuth: proto.CapabilitySupported, }, }, Pi: proto.SupportedAgentKind{ Kind: "pi", Capabilities: proto.AgentKindCapabilities{ - // pi runs --no-approve, so no permission cards; streaming, - // usage, and --session resume are all wired. - Streaming: true, - Usage: true, - Resume: true, + SubagentObservations: proto.CapabilityUnsupported, + Streaming: proto.CapabilitySupported, + Permissions: proto.CapabilityUnsupported, + Usage: proto.CapabilitySupported, + Resume: proto.CapabilitySupported, + NativeSessionRecovery: proto.CapabilityUnsupported, + WorkspaceAuthoring: 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, + MCPHTTPBearerAuth: proto.CapabilityUnsupported, }, }, } @@ -164,9 +251,9 @@ func discoverAgentCLIs(parent context.Context, rc *runContext, profile string, c if codexErr == nil { out.Codex.Available = true out.Codex.Version = codexVersion - out.Codex.Capabilities.NativeSessionRecovery = codex.SupportsNativeSessionRecovery(codexVersion) - out.Codex.Capabilities.LocalEnvironment = codex.SupportsLocalEnvironment(codexVersion) - out.Codex.Capabilities.MCPHTTPRequired = codex.SupportsNativeSessionRecovery(codexVersion) + out.Codex.Capabilities.NativeSessionRecovery = proto.CapabilityFromBool(codex.SupportsNativeSessionRecovery(codexVersion)) + out.Codex.Capabilities.LocalEnvironment = proto.CapabilityFromBool(codex.SupportsLocalEnvironment(codexVersion)) + out.Codex.Capabilities.MCPHTTPRequired = proto.CapabilityFromBool(codex.SupportsNativeSessionRecovery(codexVersion)) fmt.Fprintf(rc.stdout, "Codex preflight ok (%s)\n", codexVersion) } else if errors.Is(codexErr, codex.ErrCLINotFound) { fmt.Fprintln(rc.stderr, "oac-daemon: Codex CLI not found on PATH; codex unavailable.") diff --git a/apps/parsar-daemon/internal/cli/claude_sdk.go b/apps/parsar-daemon/internal/cli/claude_sdk.go index 8ede63c61..6b9cc2c02 100644 --- a/apps/parsar-daemon/internal/cli/claude_sdk.go +++ b/apps/parsar-daemon/internal/cli/claude_sdk.go @@ -30,10 +30,36 @@ func discoverClaudeSDK(parent context.Context, rc *runContext, profile string, c return nil } out := &claudeSDKDiscovery{Info: proto.SupportedAgentKind{Kind: "claude_sdk", Capabilities: proto.AgentKindCapabilities{ - Streaming: true, Usage: true, Resume: true, Steering: true, MessageItems: true, - ToolObservations: true, EnvironmentNone: true, SubagentControl: true, - DurableTurns: true, DurableInputReceipts: true, FunctionTools: true, ExecutionControls: true, - ProgrammaticToolCallingDisable: true, + SubagentObservations: proto.CapabilityUnsupported, + Streaming: proto.CapabilitySupported, + Permissions: proto.CapabilityUnsupported, + Usage: proto.CapabilitySupported, + Resume: proto.CapabilitySupported, + NativeSessionRecovery: proto.CapabilityUnsupported, + WorkspaceAuthoring: 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, + MCPHTTPBearerAuth: proto.CapabilityUnsupported, }}} fail := func(err error) *claudeSDKDiscovery { fmt.Fprintf(rc.stderr, "oac-daemon: configured Claude SDK runtime unavailable: %v\n", err) @@ -92,28 +118,28 @@ func discoverClaudeSDK(parent context.Context, rc *runContext, profile string, c return fail(fmt.Errorf("Claude SDK bundle does not support the local Runtime contract")) } caps := &out.Info.Capabilities - caps.EnvironmentNone, caps.FunctionTools = false, info.SupportsWorkspaceFunctions() - caps.Preparation, caps.LocalEnvironment = true, true - caps.WorkspaceReadPreparation, caps.NativeSessionRecovery = true, true + caps.EnvironmentNone, caps.FunctionTools = proto.CapabilityUnsupported, proto.CapabilityFromBool(info.SupportsWorkspaceFunctions()) + caps.Preparation, caps.LocalEnvironment = proto.CapabilitySupported, proto.CapabilitySupported + caps.WorkspaceReadPreparation, caps.NativeSessionRecovery = proto.CapabilitySupported, proto.CapabilitySupported } out.Info.Available, out.Info.Version = true, info.SDK - out.Info.Capabilities.MessageImages = info.SupportsMessageImages() - out.Info.Capabilities.FunctionResultImages = info.SupportsFunctionResultImages() - out.Info.Capabilities.ToolSearch = info.SupportsToolSearch() + out.Info.Capabilities.MessageImages = proto.CapabilityFromBool(info.SupportsMessageImages()) + out.Info.Capabilities.FunctionResultImages = proto.CapabilityFromBool(info.SupportsFunctionResultImages()) + out.Info.Capabilities.ToolSearch = proto.CapabilityFromBool(info.SupportsToolSearch()) if out.Config.Workspace != nil { - out.Info.Capabilities.ToolSearch = info.SupportsWorkspaceToolSearch() + out.Info.Capabilities.ToolSearch = proto.CapabilityFromBool(info.SupportsWorkspaceToolSearch()) } - out.Info.Capabilities.StructuredOutput = info.SupportsStructuredOutput() + out.Info.Capabilities.StructuredOutput = proto.CapabilityFromBool(info.SupportsStructuredOutput()) if out.Config.Workspace != nil { - out.Info.Capabilities.StructuredOutput = info.SupportsWorkspaceStructuredOutput() + out.Info.Capabilities.StructuredOutput = proto.CapabilityFromBool(info.SupportsWorkspaceStructuredOutput()) } - out.Info.Capabilities.SubagentObservations = info.SupportsSubagents() - out.Info.Capabilities.MCPHTTPTools = info.SupportsHTTPMCP() - out.Info.Capabilities.MCPHTTPBearerAuth = info.SupportsHTTPMCPBearer() - out.Info.Capabilities.MCPHTTPRequired = info.SupportsHTTPMCPRequired() + out.Info.Capabilities.SubagentObservations = proto.CapabilityFromBool(info.SupportsSubagents()) + out.Info.Capabilities.MCPHTTPTools = proto.CapabilityFromBool(info.SupportsHTTPMCP()) + out.Info.Capabilities.MCPHTTPBearerAuth = proto.CapabilityFromBool(info.SupportsHTTPMCPBearer()) + out.Info.Capabilities.MCPHTTPRequired = proto.CapabilityFromBool(info.SupportsHTTPMCPRequired()) if out.Config.Workspace != nil && !info.SupportsWorkspaceMCP() { - out.Info.Capabilities.MCPHTTPTools, out.Info.Capabilities.MCPHTTPBearerAuth = false, false - out.Info.Capabilities.MCPHTTPRequired = false + out.Info.Capabilities.MCPHTTPTools, out.Info.Capabilities.MCPHTTPBearerAuth = proto.CapabilityUnsupported, proto.CapabilityUnsupported + out.Info.Capabilities.MCPHTTPRequired = proto.CapabilityUnsupported } fmt.Fprintf(rc.stdout, "Claude SDK preflight ok (SDK %s, %s)\n", info.SDK, info.Native) return out @@ -133,7 +159,7 @@ func registerClaudeSDK(registry *agent.Registry, discovery *claudeSDKDiscovery) if discovery.Info.Available { registry.RegisterExecutor("claude_sdk", claudesdk.NewExecutorFactory(discovery.Config)) } - if discovery.Info.Available && discovery.Info.Capabilities.LocalEnvironment { + if discovery.Info.Available && discovery.Info.Capabilities.LocalEnvironment.IsSupported() { registry.RegisterPreparation("claude_sdk", true, claudesdk.NewPreparationFactory(discovery.Config)) } } diff --git a/apps/parsar-daemon/internal/cli/mcode.go b/apps/parsar-daemon/internal/cli/mcode.go index 30ea20275..e2f13bd22 100644 --- a/apps/parsar-daemon/internal/cli/mcode.go +++ b/apps/parsar-daemon/internal/cli/mcode.go @@ -12,7 +12,38 @@ func discoverMCode(parent context.Context, rc *runContext, check func(context.Co if check == nil { check = mcode.CheckCLIAvailable } - result := proto.SupportedAgentKind{Kind: "mcode", Capabilities: proto.AgentKindCapabilities{Streaming: true, Permissions: true, Resume: true}} + result := proto.SupportedAgentKind{Kind: "mcode", Capabilities: proto.AgentKindCapabilities{ + SubagentObservations: proto.CapabilityUnsupported, + Streaming: proto.CapabilitySupported, + Permissions: proto.CapabilitySupported, + Usage: proto.CapabilityUnsupported, + Resume: proto.CapabilitySupported, + NativeSessionRecovery: proto.CapabilityUnsupported, + WorkspaceAuthoring: 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, + MCPHTTPBearerAuth: proto.CapabilityUnsupported, + }} ctx, cancel := context.WithTimeout(parent, cliVersionTimeout) defer cancel() version, err := check(ctx, "") @@ -22,18 +53,18 @@ func discoverMCode(parent context.Context, rc *runContext, check func(context.Co } result.Available, result.Version = true, version if mcode.SupportsExecution(version) { - result.Capabilities.Steering = true - result.Capabilities.DurableTurns = true - result.Capabilities.DurableInputReceipts = true - result.Capabilities.ExecutionControls = true - result.Capabilities.ProgrammaticToolCallingDisable = true - result.Capabilities.ToolObservations = true - result.Capabilities.SubagentControl = true + 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 = true - result.Capabilities.EnvironmentNone = true - result.Capabilities.MCPHTTPTools = true - result.Capabilities.MCPHTTPBearerAuth = true + result.Capabilities.SubagentObservations = proto.CapabilitySupported + result.Capabilities.EnvironmentNone = proto.CapabilitySupported + result.Capabilities.MCPHTTPTools = proto.CapabilitySupported + result.Capabilities.MCPHTTPBearerAuth = proto.CapabilitySupported } fmt.Fprintf(rc.stdout, "mcode preflight ok (%s)\n", version) return result diff --git a/apps/parsar-daemon/internal/cli/mcode_workspace.go b/apps/parsar-daemon/internal/cli/mcode_workspace.go index e100edb4e..e759a77d5 100644 --- a/apps/parsar-daemon/internal/cli/mcode_workspace.go +++ b/apps/parsar-daemon/internal/cli/mcode_workspace.go @@ -11,6 +11,7 @@ import ( "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent/mcode" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/localworkspace" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/paths" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" ) func discoverMCodeWorkspace(parent context.Context, rc *runContext, discovery *agentCLIDiscovery) { @@ -69,7 +70,7 @@ func discoverMCodeWorkspace(parent context.Context, rc *runContext, discovery *a } discovery.MCodeWorkspace = &c caps := &discovery.MCode.Capabilities - caps.EnvironmentNone = false - caps.Preparation, caps.LocalEnvironment = true, true - caps.WorkspaceReadPreparation = true + caps.EnvironmentNone = proto.CapabilityUnsupported + caps.Preparation, caps.LocalEnvironment = proto.CapabilitySupported, proto.CapabilitySupported + caps.WorkspaceReadPreparation = proto.CapabilitySupported } diff --git a/contracts/agents-api/harness-onboarding.md b/contracts/agents-api/harness-onboarding.md index fbb144445..27ca39740 100644 --- a/contracts/agents-api/harness-onboarding.md +++ b/contracts/agents-api/harness-onboarding.md @@ -11,7 +11,7 @@ Start from two entry points: - [`internal/harnessconfig/harness.go`](../../internal/harnessconfig/harness.go): the shared model configuration contract (declarations and preparation). - [`agent/harness.go`](../../apps/parsar-daemon/internal/agent/harness.go): the - execution lifecycle, optional interfaces and registration methods. + execution lifecycle, explicit extension contracts and registration methods. ## Ownership @@ -74,11 +74,15 @@ model communication configuration, not Turn scheduling or native process ownersh [Harness integration](harnesses.md#acceptance-checklist). Record results in the [qualification table](harnesses.md#current-qualified-operations). -Start with the mandatory text lifecycle, then qualify optional operations one at -a time. Call the reusable `agent/contracttest.TextLifecycle` assertions with the +Implement the mandatory text lifecycle and explicitly handle every extension. +Qualify supported extensions one at a time; an unqualified extension returns +`agent.ErrUnsupportedOperation` without native effects. Call the reusable `agent/contracttest.TextLifecycle` assertions with the adapter's prepared Executor and deterministic native fixture. These assertions -cover healthy reuse, durable input and cancellation; keep native fault and live -acceptance separate. Name the entry test `TestSharedTextLifecycle` so +cover healthy reuse, durable input and cancellation for adapters whose native +owner remains reusable. A native cancellation may instead require retirement: +`Reusable=false` carries a reason and the caller must confirm `Executor.Close`. +Do not force reuse to fit a test helper. Keep native fault and live acceptance +separate. Name the entry test `TestSharedTextLifecycle` so `make check-runtime-contract` includes it. Do not copy an adapter's native limitations into the shared Core protocol. @@ -91,7 +95,7 @@ limitations into the shared Core protocol. - A new engine supplies an adapter, a qualified profile, registration and an independently verified deployment. It adds no engine-name branches to API handlers, persistence, dispatch, scheduling or Environment providers. -- Keep required lifecycle declarations, optional interfaces and registration +- Keep required lifecycle declarations, extension interfaces and registration methods in `agent/harness.go`. Result types, errors and Registry storage may stay in focused files. Keep this guide linked to that entry point. - Use the existing `proto.SupportedAgentKind` and `AgentKindCapabilities` @@ -99,7 +103,8 @@ limitations into the shared Core protocol. interface. - Onboarding does not require feature equality. Verify common lifecycle obligations and use the same public assertions for each declared operation. - Optional native differences are separate capability work, not onboarding blockers. + Native differences do not block onboarding, but an omitted declaration or + missing extension implementation does. - Never equate accepted parameters with applied native behavior. ## Native model configuration @@ -153,9 +158,11 @@ ownership are in the [unified model configuration design](model-configuration-de [`agent/harness.go`](../../apps/parsar-daemon/internal/agent/harness.go) is the canonical interface entry point. Its required lifecycle is `ExecutorFactory`, -`Executor`, `Turn` and `TurnSettlement`. Optional Turn and workspace interfaces -remain separate; their result types and error values stay in the corresponding -operation files in the same package. All use the existing neutral protocol types. +`Executor`, `Turn` (including `DurableSteerer`) and `TurnSettlement`. Required +methods must perform their native obligations; returning Unsupported is not an +implementation of cancellation, receipts, settlement or cleanup. Turn and workspace +extension interfaces remain small and separate, but every public adapter implements +each explicitly. Their result types and errors stay in focused operation files. All use the existing neutral protocol types. The [Core–Runtime lifecycle contract](../../docs/runtime-protocol.md#executor-and-turn-lifetimes) owns preparation failure, partial StartTurn results, output closure, settlement, @@ -172,7 +179,7 @@ native Session. Their implementations expose the same Executor and Turn contract Implement cancellation on the exact Turn through `agent.Session`. Follow the [lifecycle and settlement rules](../../docs/runtime-protocol.md#executor-and-turn-lifetimes); the adapter must supply native completion evidence to the shared Runtime. -Permission and user-choice responses use the optional interfaces below. +Permission and user-choice responses use the explicit extension interfaces below. ## Events, inputs and optional capabilities @@ -191,20 +198,50 @@ before consuming a pending interaction. Do not map answers by header or position Resume only the exact history bound to the Session; missing or ambiguous required history fails before new model input. -| Interface or contract | When required | Obligation | +| Interface or contract | Required handling | Obligation | | --- | --- | --- | -| `agent.DurableSteerer` | Current public text execution | Distinguish write and application receipts; preserve retry identity; independent of the optional non-durable `Steerer` | -| `agent.FunctionResultSubmitter` | Public function tools | Match call/result identity and acknowledge native application | -| `agent.PermissionResponder`, `agent.UserChoiceResponder` | When emitting these interactions | Route exact identities and settle receipts | -| `agent.WorkspaceReader`, `agent.WorkspaceDirectoryLister`, `agent.WorkspaceWriter` | Qualified workspace operations | Use the fixed authorized workspace and retain accepted operations through close | -| Neutral message, image, MCP, structured-output and Subagent observations | Only when qualified and advertised | Preserve the operation-specific contract and reject unsupported combinations | - -Optional features need not match another harness. The service profile qualifies -public combinations; the Runtime advertises this installation's available support. -Neither replaces schema validation or tenant authorization. Declaring a capability -without implementing its semantics is an error. Workspace reads may use separate -read-only preparations; those do not start model work or provide another execution -lifecycle. +| `ExecutorFactory`, `Executor.StartTurn`, `Executor.Close` | Real implementation | Prepare without model input; keep failed or uncertain resource ownership; confirm cleanup | +| `Turn`, `Session.Cancel`, `CancellationOutcome`, `AwaitSettlement` | Real implementation | Cancel the exact Turn, preserve observed results and confirm settlement independently of cancellation requests | +| `DurableSteerer` | Real implementation on every Turn | Distinguish complete write from native application receipt; preserve retry identity | +| `Steerer` | Explicit implementation or Unsupported | Additional non-durable active-turn input | +| `FunctionResultSubmitter` | Explicit implementation or Unsupported | Match native call/result identity and acknowledge application | +| `PermissionResponder`, `UserChoiceResponder` | Explicit implementation or Unsupported | Respond to exact emitted identities; unknown/expired interactions remain distinct from Unsupported | +| `WorkspaceReader`, `WorkspaceDirectoryLister`, `WorkspaceWriter` | Explicit on Turn, Executor and Prepared owners | Use the authorized workspace, confirm access/commit/close, or return the operation's Unsupported error | +| `Prepared`, `PreparedCancellation` | Real implementation for an executable preparation | Preserve resource and output ownership across Start, cancellation and unused cleanup | +| Neutral messages, images, MCP, structured output and Subagent observations | Explicit capability decisions | Preserve each operation's protocol semantics; reject unsupported input before submission | + +Each adapter's `contracts.go` contains individual compile-time assertions for these +small interfaces. Do not embed a default implementation that makes future +interfaces appear implemented. Adding a contract also requires classification in +the common completeness check and an explicit assertion for every public adapter; +the check follows the authored Harness catalog. + +For a design-level refusal, implement the method directly, for example: + +```go +func (s *Session) SubmitFunctionResult(context.Context, proto.FunctionResultPayload) error { + return fmt.Errorf("%w: native public function tools are not qualified", agent.ErrUnsupportedOperation) +} +``` + +The reason is a fixed safe string, never submitted content, a credential or raw +native diagnostics. Unsupported guarantees no native side effect. It is not a +successful empty operation. Installation unavailability, unknown interaction IDs, +native failures and uncertain outcomes keep their existing errors and ownership. +A nil `Turn` still means no input was submitted and output remains with the caller; +it must not be repurposed as an Unsupported marker. + +Workspace capability describes the actual Runtime/resource-owner combination. +Codex and MiniMax resource objects explicitly reject native workspace access while +the common authorized `localworkspace` owner provides it. Claude can expose native +read/list access; writes are provided by the common owner. Interface presence alone +must never select a resource or advertise support. + +The service profile qualifies public combinations; the Runtime advertises the +installed combination. Neither replaces schema validation or tenant authorization. +Native behavior tests must agree with supported declarations. An advertised +operation returning Unsupported is a contract violation, never success or grounds +for automatic replay. ## Register the adapter @@ -221,8 +258,19 @@ from [`cli/agent_registration.go`](../../apps/parsar-daemon/internal/cli/agent_r | 3 | `RegisterPreparation(kind, workspaceRead, agent.PreparationFactory)` | Optional. Separate read-only workspace preparation when qualified workspace operations need it. | The direct-call `agent.Factory` should delegate to the same Executor -implementation. Every other capability declaration must match behavior verified -for that installation. Runtime registration does not grant Core qualification; +implementation. Every `proto.AgentKindCapabilities` field must be explicitly +`proto.CapabilitySupported` or `proto.CapabilityUnsupported`. +`proto.CapabilityUnspecified` is invalid: zero values and omitted fields do not mean +Unsupported. Installation probes may use `proto.CapabilityFromBool` for an +individual field; they must not populate all unmentioned or future fields. +Availability remains separate in `SupportedAgentKind.Available`. + +Registration and wire decoding validate the complete declaration. The wire carries +an explicit boolean for every field; omitted and null fields are invalid. A new +field requires a decision by every production declaration. Runtime consumers use +`IsSupported()` and reject unsupported requests before native operations; an +interface assertion only verifies implementation, never support. Every declaration +must match behavior verified for that installation. Runtime registration does not grant Core qualification; that belongs to the service profile. The runnable test-only example is @@ -273,7 +321,7 @@ The [Harness selection contract](harness-selection.md) owns the public selector, deployment enablement, defaults and immutable Session binding. This guide adds no second selector or fallback rule. -## Required versus optional operations +## Required versus extension operations The current public text path requires durable turns, applied input receipts, ordered observations, cancellation and enforcement of disabled execution controls. @@ -284,7 +332,7 @@ of enforcement. MCP, public function calls, deferred function discovery, structured output, image inputs, verbosity controls and other optional operations do not need to match another engine. Reject unqualified combinations -explicitly and record the gap. Never advertise a capability to bypass selection. +with Unsupported and record the gap. Never advertise a capability to bypass selection. Structured-output adapters consume `ExecutionControls.OutputFormat` and publish confirmed native output through the existing Message contract. Register public @@ -331,6 +379,26 @@ not add routes, storage branches or a harness-specific Core scheduler. Report unsupported native facts explicitly; completing a child task is not closing its Subagent. Native background work must remain owned through settlement and cancel. +## Contract verification + +Run `make check-runtime-contract`, the three adapter test packages and `make check`. +The common completeness gate covers capability omissions and interface assertions; +adapter tests must cover actual native semantics, not only method presence. + +| Boundary | Existing focused evidence | +| --- | --- | +| Codex reuse, cancellation and unconfirmed cleanup | `codex/executor_test.go`, `terminal_cleanup_test.go`, `prepared_cancel_test.go` | +| Codex input receipts and strict recovery | `codex/function_write_receipt_test.go`, `function_receipt_test.go`, `resume_test.go`, `recovery_test.go` | +| Claude input ownership, cancellation and preparation cleanup | `claudesdk/executor_test.go`, `cancellation_test.go`, `preparation_test.go` | +| MiniMax cancellation retirement, failed Start and cleanup retry | `mcode/executor_test.go`, `executor_backpressure_test.go` | +| MiniMax native history binding | `mcode/session_test.go` | +| Explicit refusals without native effects or fabricated results | Each adapter's `unsupported_test.go` | + +These paths are relative to `apps/parsar-daemon/internal/agent`. Controlled native +transport fixtures establish failure and ownership behavior; they are not live model +qualification. Preserve the separate native acceptance requirements in +[Harness integration](harnesses.md#acceptance-checklist). + ## Native installer participation An adapter may supply `agent.Installation` from `installation.go` in its own From 5d21b9b14b043b3ab2d6c38df69da53e2a555ceb Mon Sep 17 00:00:00 2001 From: saladday <1203511142@qq.com> Date: Wed, 30 Sep 2026 13:24:52 +0800 Subject: [PATCH 03/12] Clarify capability admission mapping for Harness extensions --- contracts/agents-api/harness-onboarding.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/contracts/agents-api/harness-onboarding.md b/contracts/agents-api/harness-onboarding.md index 27ca39740..20f047237 100644 --- a/contracts/agents-api/harness-onboarding.md +++ b/contracts/agents-api/harness-onboarding.md @@ -270,7 +270,17 @@ an explicit boolean for every field; omitted and null fields are invalid. A new field requires a decision by every production declaration. Runtime consumers use `IsSupported()` and reject unsupported requests before native operations; an interface assertion only verifies implementation, never support. Every declaration -must match behavior verified for that installation. Runtime registration does not grant Core qualification; +must match behavior verified for that installation. + +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. Core's current public text profile +requires both advertised capabilities. `Permissions` qualifies permission and +user-choice responses together; a supported declaration requires both native +response paths. Workspace declarations describe the selected authorized resource +owner, including the common Runtime workspace implementation. + +Runtime registration does not grant Core qualification; that belongs to the service profile. The runnable test-only example is From f9d832ea208f12df2c7b157d5c5779e361ca53dd Mon Sep 17 00:00:00 2001 From: saladday <1203511142@qq.com> Date: Wed, 30 Sep 2026 13:27:17 +0800 Subject: [PATCH 04/12] Regenerate Harness onboarding guide --- apps/docs/content/docs/harness-onboarding.mdx | 134 ++++++++++++++---- apps/docs/content/guide-sources.json | 4 +- 2 files changed, 108 insertions(+), 30 deletions(-) diff --git a/apps/docs/content/docs/harness-onboarding.mdx b/apps/docs/content/docs/harness-onboarding.mdx index 8f8654b6f..eb4bd57b6 100644 --- a/apps/docs/content/docs/harness-onboarding.mdx +++ b/apps/docs/content/docs/harness-onboarding.mdx @@ -14,7 +14,7 @@ Start from two entry points: - [`internal/harnessconfig/harness.go`](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/internal/harnessconfig/harness.go): the shared model configuration contract (declarations and preparation). - [`agent/harness.go`](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/apps/parsar-daemon/internal/agent/harness.go): the - execution lifecycle, optional interfaces and registration methods. + execution lifecycle, explicit extension contracts and registration methods. ## Ownership @@ -77,11 +77,15 @@ model communication configuration, not Turn scheduling or native process ownersh [Harness integration](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/harnesses.md#acceptance-checklist). Record results in the [qualification table](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/harnesses.md#current-qualified-operations). -Start with the mandatory text lifecycle, then qualify optional operations one at -a time. Call the reusable `agent/contracttest.TextLifecycle` assertions with the +Implement the mandatory text lifecycle and explicitly handle every extension. +Qualify supported extensions one at a time; an unqualified extension returns +`agent.ErrUnsupportedOperation` without native effects. Call the reusable `agent/contracttest.TextLifecycle` assertions with the adapter's prepared Executor and deterministic native fixture. These assertions -cover healthy reuse, durable input and cancellation; keep native fault and live -acceptance separate. Name the entry test `TestSharedTextLifecycle` so +cover healthy reuse, durable input and cancellation for adapters whose native +owner remains reusable. A native cancellation may instead require retirement: +`Reusable=false` carries a reason and the caller must confirm `Executor.Close`. +Do not force reuse to fit a test helper. Keep native fault and live acceptance +separate. Name the entry test `TestSharedTextLifecycle` so `make check-runtime-contract` includes it. Do not copy an adapter's native limitations into the shared Core protocol. @@ -94,7 +98,7 @@ limitations into the shared Core protocol. - A new engine supplies an adapter, a qualified profile, registration and an independently verified deployment. It adds no engine-name branches to API handlers, persistence, dispatch, scheduling or Environment providers. -- Keep required lifecycle declarations, optional interfaces and registration +- Keep required lifecycle declarations, extension interfaces and registration methods in `agent/harness.go`. Result types, errors and Registry storage may stay in focused files. Keep this guide linked to that entry point. - Use the existing `proto.SupportedAgentKind` and `AgentKindCapabilities` @@ -102,7 +106,8 @@ limitations into the shared Core protocol. interface. - Onboarding does not require feature equality. Verify common lifecycle obligations and use the same public assertions for each declared operation. - Optional native differences are separate capability work, not onboarding blockers. + Native differences do not block onboarding, but an omitted declaration or + missing extension implementation does. - Never equate accepted parameters with applied native behavior. ## Native model configuration @@ -156,9 +161,11 @@ ownership are in the [unified model configuration design](https://github.com/Min [`agent/harness.go`](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/apps/parsar-daemon/internal/agent/harness.go) is the canonical interface entry point. Its required lifecycle is `ExecutorFactory`, -`Executor`, `Turn` and `TurnSettlement`. Optional Turn and workspace interfaces -remain separate; their result types and error values stay in the corresponding -operation files in the same package. All use the existing neutral protocol types. +`Executor`, `Turn` (including `DurableSteerer`) and `TurnSettlement`. Required +methods must perform their native obligations; returning Unsupported is not an +implementation of cancellation, receipts, settlement or cleanup. Turn and workspace +extension interfaces remain small and separate, but every public adapter implements +each explicitly. Their result types and errors stay in focused operation files. All use the existing neutral protocol types. The [Core–Runtime lifecycle contract](/runtime-protocol#executor-and-turn-lifetimes) owns preparation failure, partial StartTurn results, output closure, settlement, @@ -175,7 +182,7 @@ native Session. Their implementations expose the same Executor and Turn contract Implement cancellation on the exact Turn through `agent.Session`. Follow the [lifecycle and settlement rules](/runtime-protocol#executor-and-turn-lifetimes); the adapter must supply native completion evidence to the shared Runtime. -Permission and user-choice responses use the optional interfaces below. +Permission and user-choice responses use the explicit extension interfaces below. ## Events, inputs and optional capabilities @@ -194,20 +201,50 @@ before consuming a pending interaction. Do not map answers by header or position Resume only the exact history bound to the Session; missing or ambiguous required history fails before new model input. -| Interface or contract | When required | Obligation | +| Interface or contract | Required handling | Obligation | | --- | --- | --- | -| `agent.DurableSteerer` | Current public text execution | Distinguish write and application receipts; preserve retry identity; independent of the optional non-durable `Steerer` | -| `agent.FunctionResultSubmitter` | Public function tools | Match call/result identity and acknowledge native application | -| `agent.PermissionResponder`, `agent.UserChoiceResponder` | When emitting these interactions | Route exact identities and settle receipts | -| `agent.WorkspaceReader`, `agent.WorkspaceDirectoryLister`, `agent.WorkspaceWriter` | Qualified workspace operations | Use the fixed authorized workspace and retain accepted operations through close | -| Neutral message, image, MCP, structured-output and Subagent observations | Only when qualified and advertised | Preserve the operation-specific contract and reject unsupported combinations | - -Optional features need not match another harness. The service profile qualifies -public combinations; the Runtime advertises this installation's available support. -Neither replaces schema validation or tenant authorization. Declaring a capability -without implementing its semantics is an error. Workspace reads may use separate -read-only preparations; those do not start model work or provide another execution -lifecycle. +| `ExecutorFactory`, `Executor.StartTurn`, `Executor.Close` | Real implementation | Prepare without model input; keep failed or uncertain resource ownership; confirm cleanup | +| `Turn`, `Session.Cancel`, `CancellationOutcome`, `AwaitSettlement` | Real implementation | Cancel the exact Turn, preserve observed results and confirm settlement independently of cancellation requests | +| `DurableSteerer` | Real implementation on every Turn | Distinguish complete write from native application receipt; preserve retry identity | +| `Steerer` | Explicit implementation or Unsupported | Additional non-durable active-turn input | +| `FunctionResultSubmitter` | Explicit implementation or Unsupported | Match native call/result identity and acknowledge application | +| `PermissionResponder`, `UserChoiceResponder` | Explicit implementation or Unsupported | Respond to exact emitted identities; unknown/expired interactions remain distinct from Unsupported | +| `WorkspaceReader`, `WorkspaceDirectoryLister`, `WorkspaceWriter` | Explicit on Turn, Executor and Prepared owners | Use the authorized workspace, confirm access/commit/close, or return the operation's Unsupported error | +| `Prepared`, `PreparedCancellation` | Real implementation for an executable preparation | Preserve resource and output ownership across Start, cancellation and unused cleanup | +| Neutral messages, images, MCP, structured output and Subagent observations | Explicit capability decisions | Preserve each operation's protocol semantics; reject unsupported input before submission | + +Each adapter's `contracts.go` contains individual compile-time assertions for these +small interfaces. Do not embed a default implementation that makes future +interfaces appear implemented. Adding a contract also requires classification in +the common completeness check and an explicit assertion for every public adapter; +the check follows the authored Harness catalog. + +For a design-level refusal, implement the method directly, for example: + +```go +func (s *Session) SubmitFunctionResult(context.Context, proto.FunctionResultPayload) error { + return fmt.Errorf("%w: native public function tools are not qualified", agent.ErrUnsupportedOperation) +} +``` + +The reason is a fixed safe string, never submitted content, a credential or raw +native diagnostics. Unsupported guarantees no native side effect. It is not a +successful empty operation. Installation unavailability, unknown interaction IDs, +native failures and uncertain outcomes keep their existing errors and ownership. +A nil `Turn` still means no input was submitted and output remains with the caller; +it must not be repurposed as an Unsupported marker. + +Workspace capability describes the actual Runtime/resource-owner combination. +Codex and MiniMax resource objects explicitly reject native workspace access while +the common authorized `localworkspace` owner provides it. Claude can expose native +read/list access; writes are provided by the common owner. Interface presence alone +must never select a resource or advertise support. + +The service profile qualifies public combinations; the Runtime advertises the +installed combination. Neither replaces schema validation or tenant authorization. +Native behavior tests must agree with supported declarations. An advertised +operation returning Unsupported is a contract violation, never success or grounds +for automatic replay. ## Register the adapter @@ -224,8 +261,29 @@ from [`cli/agent_registration.go`](https://github.com/MiniMax-AI/parsar-core/blo | 3 | `RegisterPreparation(kind, workspaceRead, agent.PreparationFactory)` | Optional. Separate read-only workspace preparation when qualified workspace operations need it. | The direct-call `agent.Factory` should delegate to the same Executor -implementation. Every other capability declaration must match behavior verified -for that installation. Runtime registration does not grant Core qualification; +implementation. Every `proto.AgentKindCapabilities` field must be explicitly +`proto.CapabilitySupported` or `proto.CapabilityUnsupported`. +`proto.CapabilityUnspecified` is invalid: zero values and omitted fields do not mean +Unsupported. Installation probes may use `proto.CapabilityFromBool` for an +individual field; they must not populate all unmentioned or future fields. +Availability remains separate in `SupportedAgentKind.Available`. + +Registration and wire decoding validate the complete declaration. The wire carries +an explicit boolean for every field; omitted and null fields are invalid. A new +field requires a decision by every production declaration. Runtime consumers use +`IsSupported()` and reject unsupported requests before native operations; an +interface assertion only verifies implementation, never support. Every declaration +must match behavior verified for that installation. + +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. Core's current public text profile +requires both advertised capabilities. `Permissions` qualifies permission and +user-choice responses together; a supported declaration requires both native +response paths. Workspace declarations describe the selected authorized resource +owner, including the common Runtime workspace implementation. + +Runtime registration does not grant Core qualification; that belongs to the service profile. The runnable test-only example is @@ -276,7 +334,7 @@ The [Harness selection contract](https://github.com/MiniMax-AI/parsar-core/blob/ deployment enablement, defaults and immutable Session binding. This guide adds no second selector or fallback rule. -## Required versus optional operations +## Required versus extension operations The current public text path requires durable turns, applied input receipts, ordered observations, cancellation and enforcement of disabled execution controls. @@ -287,7 +345,7 @@ of enforcement. MCP, public function calls, deferred function discovery, structured output, image inputs, verbosity controls and other optional operations do not need to match another engine. Reject unqualified combinations -explicitly and record the gap. Never advertise a capability to bypass selection. +with Unsupported and record the gap. Never advertise a capability to bypass selection. Structured-output adapters consume `ExecutionControls.OutputFormat` and publish confirmed native output through the existing Message contract. Register public @@ -334,6 +392,26 @@ not add routes, storage branches or a harness-specific Core scheduler. Report unsupported native facts explicitly; completing a child task is not closing its Subagent. Native background work must remain owned through settlement and cancel. +## Contract verification + +Run `make check-runtime-contract`, the three adapter test packages and `make check`. +The common completeness gate covers capability omissions and interface assertions; +adapter tests must cover actual native semantics, not only method presence. + +| Boundary | Existing focused evidence | +| --- | --- | +| Codex reuse, cancellation and unconfirmed cleanup | `codex/executor_test.go`, `terminal_cleanup_test.go`, `prepared_cancel_test.go` | +| Codex input receipts and strict recovery | `codex/function_write_receipt_test.go`, `function_receipt_test.go`, `resume_test.go`, `recovery_test.go` | +| Claude input ownership, cancellation and preparation cleanup | `claudesdk/executor_test.go`, `cancellation_test.go`, `preparation_test.go` | +| MiniMax cancellation retirement, failed Start and cleanup retry | `mcode/executor_test.go`, `executor_backpressure_test.go` | +| MiniMax native history binding | `mcode/session_test.go` | +| Explicit refusals without native effects or fabricated results | Each adapter's `unsupported_test.go` | + +These paths are relative to `apps/parsar-daemon/internal/agent`. Controlled native +transport fixtures establish failure and ownership behavior; they are not live model +qualification. Preserve the separate native acceptance requirements in +[Harness integration](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/harnesses.md#acceptance-checklist). + ## Native installer participation An adapter may supply `agent.Installation` from `installation.go` in its own diff --git a/apps/docs/content/guide-sources.json b/apps/docs/content/guide-sources.json index 88522a088..65a082355 100644 --- a/apps/docs/content/guide-sources.json +++ b/apps/docs/content/guide-sources.json @@ -25,7 +25,7 @@ "docs/user-guide.md": "190bea5bfe23b71db3d9437065ee270e07226a89a6e98c668853e5c7f7555529", "docs/assets/development-architecture.png": "24e6d0145d4f16ad70b07b6bc643808a6455aaf6398434d199cf74def200fca6", "docs/development.md": "f5c340253036a2cabc43e22aecdf70522d90280d6511e7649278ae93712ab8b6", - "contracts/agents-api/harness-onboarding.md": "e8fceb236e7fb0794eade26639d86b0622ec7dbb1fad90c30f41742c9d029232", + "contracts/agents-api/harness-onboarding.md": "acbc9bf8c1365e1242d4c9a934b068a19ef3450a4aef2fc08356624380222266", "docs/runtime-bootstrap.md": "0d49aed73b298039e04453e6f465b0e925fb2227fa35d4206820bb3acbd8df39", "docs/runtime-protocol.md": "1d48186fa84140403358d2d01ddaca56d7122e1b3606cb6855ddefffeb3b8ed6", "docs/sandbox-provider.md": "4d47b234a457f874f3ff7e61a7f5da6fc8b75b0c6df0ce0ce9ead1c07afdfb3e", @@ -56,7 +56,7 @@ "content/docs/user-guide.mdx": "7c099b2d787ce3d7876889c7cefd621840ec083f267eadf278214a83f11ba274", "public/images/source/docs/assets/development-architecture.png": "24e6d0145d4f16ad70b07b6bc643808a6455aaf6398434d199cf74def200fca6", "content/docs/development.mdx": "2e5249ddfca571d264e93fd1e0e390a4bd5b0b623bda100cd02f2982929850bb", - "content/docs/harness-onboarding.mdx": "8959febf77b2e9e4b16d301eabd8027f09ebd635f2c2713a40d22884afe95d12", + "content/docs/harness-onboarding.mdx": "f0d91fd8f4cbaaa9475a25e24b08d9bfb0c25a8549af47b81073886444e5ac44", "content/docs/runtime-bootstrap.mdx": "58982955811c9ad46a9fc04d8d2aef5a762fc9d25d5833f88aa75ee3fc46523f", "content/docs/runtime-protocol.mdx": "caa7700e442b0666340c091f3ec0bff32e6217b3be4bc845c3b9c23ee0c8d0f6", "content/docs/sandbox-provider.mdx": "9db833fe9ff3f30a65451f80d7251348695a08e7830f9e95871739a710529818" From 88570e6cdee95891c8943b7d72232f97ef32f034 Mon Sep 17 00:00:00 2001 From: saladday <1203511142@qq.com> Date: Wed, 30 Sep 2026 13:32:32 +0800 Subject: [PATCH 05/12] Migrate shared registration and capability consumers to explicit declarations --- .../internal/agent/claudecode/session.go | 2 +- .../agent/codex/preparation_router_test.go | 3 +- .../internal/agent/configuration_test.go | 5 +- apps/parsar-daemon/internal/agent/harness.go | 19 +++--- apps/parsar-daemon/internal/agent/registry.go | 3 - .../internal/agent/registry_test.go | 43 ++++++------- .../internal/agent/workspace_read.go | 3 +- .../internal/agent/workspace_write.go | 2 +- .../internal/cli/agent_registration.go | 4 +- apps/parsar-daemon/internal/cli/authoring.go | 4 +- .../internal/cli/authoring_test.go | 3 +- .../internal/cli/claude_sdk_test.go | 12 ++-- apps/parsar-daemon/internal/cli/connect.go | 2 +- .../internal/cli/connect_cleanup_test.go | 3 +- .../internal/cli/connect_suspend.go | 2 +- .../internal/cli/connect_test.go | 63 ++++++++++--------- .../internal/cli/mcode_execution_test.go | 4 +- apps/parsar-daemon/internal/cli/mcp_test.go | 10 +-- .../internal/cli/preparation_test.go | 7 ++- .../internal/contracttest/wire_test.go | 3 +- .../internal/dispatch/cancellation_test.go | 5 +- .../internal/dispatch/environment.go | 8 +-- .../internal/dispatch/environment_test.go | 7 ++- .../internal/dispatch/executor.go | 4 +- .../dispatch/executor_cancel_receipt_test.go | 3 +- .../dispatch/executor_handoff_test.go | 3 +- .../internal/dispatch/executor_test.go | 5 +- .../dispatch/functions_native_test.go | 3 +- .../internal/dispatch/functions_test.go | 7 ++- .../internal/dispatch/local_directory_test.go | 5 +- .../internal/dispatch/mcp_http.go | 6 +- .../internal/dispatch/mcp_http_test.go | 15 ++--- .../dispatch/optional_interactions_test.go | 3 +- .../internal/dispatch/preparation.go | 6 +- .../internal/dispatch/preparation_test.go | 3 +- .../internal/dispatch/receipt_order_test.go | 5 +- .../dispatch/receipt_shutdown_test.go | 3 +- .../internal/dispatch/router_test.go | 3 +- .../dispatch/steering_lifetime_test.go | 7 ++- .../internal/dispatch/steering_test.go | 9 +-- .../parsar-daemon/testdata/onboarding/main.go | 7 ++- .../agentdaemon/gateway/functions_test.go | 3 +- internal/agentdaemon/gateway/mcp_test.go | 5 +- .../agentdaemon/gateway/preparation_test.go | 3 +- internal/agentdaemon/gateway/session.go | 62 +++++++++--------- internal/agentdaemon/gateway/session_test.go | 21 ++++--- .../internal/store/claude_execution_test.go | 3 +- .../internal/store/claude_mcp_test.go | 5 +- .../internal/store/dispatch_test.go | 5 +- .../store/environment_directory_test.go | 3 +- .../store/environment_expiry_dispatch_test.go | 7 ++- .../store/environment_worker_helpers_test.go | 3 +- .../internal/store/execution_messages_test.go | 3 +- .../internal/store/function_execution_test.go | 3 +- .../internal/store/function_worker_test.go | 3 +- .../store/local_environment_worker_test.go | 3 +- .../internal/store/public_execution_test.go | 3 +- .../store/runtime_compute_lifecycle_test.go | 3 +- .../store/runtime_initialization_peer_test.go | 3 +- .../store/structured_output_dispatch_test.go | 3 +- .../internal/store/worker_lease_loss_test.go | 3 +- 61 files changed, 250 insertions(+), 211 deletions(-) diff --git a/apps/parsar-daemon/internal/agent/claudecode/session.go b/apps/parsar-daemon/internal/agent/claudecode/session.go index 93b3e40fb..7eac59cf4 100644 --- a/apps/parsar-daemon/internal/agent/claudecode/session.go +++ b/apps/parsar-daemon/internal/agent/claudecode/session.go @@ -60,7 +60,7 @@ func defaultConfig() sessionConfig { // Factory implements agent.Factory for agent_kind="claude_code". // Register during daemon startup: // -// registry.Register("claude_code", harnessconfig.Configuration{}, claudecode.Factory) +// Register the factory with an explicit capability descriptor using Registry.RegisterKind. func Factory(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { return newSession(ctx, req, out, defaultConfig()) } diff --git a/apps/parsar-daemon/internal/agent/codex/preparation_router_test.go b/apps/parsar-daemon/internal/agent/codex/preparation_router_test.go index 270dba28f..472a3a3bf 100644 --- a/apps/parsar-daemon/internal/agent/codex/preparation_router_test.go +++ b/apps/parsar-daemon/internal/agent/codex/preparation_router_test.go @@ -7,6 +7,7 @@ import ( "errors" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/localworkspace" "github.com/MiniMax-AI-Dev/parsar/internal/agentcapabilities" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "github.com/google/uuid" "os" "path/filepath" @@ -59,7 +60,7 @@ func TestPreparationRouterRetainsActualNativeChild(t *testing.T) { req.DisableExecutionEnvironment = false req.LocalEnvironment = &proto.LocalEnvironment{ID: environment, WorkspaceDirectory: "/workspace", NetworkAccess: "enabled", CapabilitySources: &agentcapabilities.Input{}} registry := agent.NewRegistry() - registry.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: proto.AgentKindCapabilities{LocalEnvironment: true, FunctionTools: true}}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + registry.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported, FunctionTools: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { return nil, errors.New("ordinary Factory must not run") }) prepared := make(chan *Prepared, 1) diff --git a/apps/parsar-daemon/internal/agent/configuration_test.go b/apps/parsar-daemon/internal/agent/configuration_test.go index 617601cd9..299834e0a 100644 --- a/apps/parsar-daemon/internal/agent/configuration_test.go +++ b/apps/parsar-daemon/internal/agent/configuration_test.go @@ -3,6 +3,7 @@ package agent_test import ( "context" "errors" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "strings" "testing" @@ -16,7 +17,7 @@ func TestEveryRegistryEntryPreparesTheBoundModelConfiguration(t *testing.T) { configuration := harnessconfig.Configuration{Providers: []harnessconfig.Provider{{Protocol: "responses"}}} calls := 0 expected := errors.New("native entry reached") - registry.Register("fixture", configuration, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + registry.RegisterKind(proto.SupportedAgentKind{Kind: "fixture", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, configuration, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { calls++ return nil, expected }) @@ -73,5 +74,5 @@ func TestRegistryRejectsInvalidConfigurationDeclaration(t *testing.T) { t.Fatal("invalid configuration registered") } }() - agent.NewRegistry().Register("fixture", harnessconfig.Configuration{Providers: []harnessconfig.Provider{{Protocol: "unknown"}}}, stubFactory("fixture")) + agent.NewRegistry().RegisterKind(proto.SupportedAgentKind{Kind: "fixture", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{Providers: []harnessconfig.Provider{{Protocol: "unknown"}}}, stubFactory("fixture")) } diff --git a/apps/parsar-daemon/internal/agent/harness.go b/apps/parsar-daemon/internal/agent/harness.go index 4705842c5..c5b995677 100644 --- a/apps/parsar-daemon/internal/agent/harness.go +++ b/apps/parsar-daemon/internal/agent/harness.go @@ -203,6 +203,9 @@ func (r *Registry) RegisterKind(info proto.SupportedAgentKind, configuration har if err := configuration.ValidateDeclaration(); err != nil { panic(err) } + if err := info.ValidateDeclaration(); err != nil { + panic(err) + } configuration = configuration.Clone() r.mu.Lock() defer r.mu.Unlock() @@ -215,8 +218,8 @@ func (r *Registry) RegisterKind(info proto.SupportedAgentKind, configuration har } delete(r.preparers, kind) delete(r.executors, kind) - info.Capabilities.Preparation = false - info.Capabilities.WorkspaceReadPreparation = false + info.Capabilities.Preparation = proto.CapabilityUnsupported + info.Capabilities.WorkspaceReadPreparation = proto.CapabilityUnsupported r.kinds[kind] = info } @@ -239,7 +242,7 @@ func (r *Registry) RegisterExecutor(kind string, factory ExecutorFactory) { } return factory(ctx, req) } - info.Capabilities.Preparation = true + info.Capabilities.Preparation = proto.CapabilitySupported r.kinds[kind] = info } @@ -262,13 +265,7 @@ func (r *Registry) RegisterPreparation(kind string, workspaceRead bool, prepare } return prepare(ctx, req) } - info.Capabilities.Preparation = true - info.Capabilities.WorkspaceReadPreparation = workspaceRead + info.Capabilities.Preparation = proto.CapabilitySupported + info.Capabilities.WorkspaceReadPreparation = proto.CapabilityFromBool(workspaceRead) r.kinds[kind] = info } - -// Register installs f as the factory for kind with a basic available -// descriptor. Panics on empty kind or nil factory. -func (r *Registry) Register(kind string, configuration harnessconfig.Configuration, f Factory) { - r.RegisterKind(proto.SupportedAgentKind{Kind: kind, Available: true}, configuration, f) -} diff --git a/apps/parsar-daemon/internal/agent/registry.go b/apps/parsar-daemon/internal/agent/registry.go index 18396433c..0185e0c2e 100644 --- a/apps/parsar-daemon/internal/agent/registry.go +++ b/apps/parsar-daemon/internal/agent/registry.go @@ -74,9 +74,6 @@ func (r *Registry) SupportedAgentKinds() []proto.SupportedAgentKind { out := make([]proto.SupportedAgentKind, 0, len(r.factories)) for kind := range r.factories { info := r.kinds[kind] - if info.Kind == "" { - info = proto.SupportedAgentKind{Kind: kind, Available: true} - } out = append(out, info) } slices.SortFunc(out, func(a, b proto.SupportedAgentKind) int { diff --git a/apps/parsar-daemon/internal/agent/registry_test.go b/apps/parsar-daemon/internal/agent/registry_test.go index c875a2175..08fc0885d 100644 --- a/apps/parsar-daemon/internal/agent/registry_test.go +++ b/apps/parsar-daemon/internal/agent/registry_test.go @@ -5,6 +5,7 @@ import "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig" import ( "context" "errors" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "slices" "testing" @@ -30,7 +31,7 @@ func (stubSession) SubmitPromptForUserChoice(context.Context, string, proto.Prom func TestRegistryResolveReturnsRegisteredFactory(t *testing.T) { reg := agent.NewRegistry() - reg.Register("claude_code", harnessconfig.Configuration{}, stubFactory("cc")) + reg.RegisterKind(proto.SupportedAgentKind{Kind: "claude_code", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, stubFactory("cc")) f, err := reg.Resolve("claude_code") if err != nil { @@ -48,7 +49,7 @@ func TestRegistryResolveReturnsRegisteredFactory(t *testing.T) { func TestRegistryResolveUnknownKindReturnsTypedError(t *testing.T) { reg := agent.NewRegistry() - reg.Register("claude_code", harnessconfig.Configuration{}, stubFactory("cc")) + reg.RegisterKind(proto.SupportedAgentKind{Kind: "claude_code", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, stubFactory("cc")) _, err := reg.Resolve("opencode") if !errors.Is(err, agent.ErrUnsupportedKind) { @@ -58,8 +59,8 @@ func TestRegistryResolveUnknownKindReturnsTypedError(t *testing.T) { func TestRegistryRegisterOverwrites(t *testing.T) { reg := agent.NewRegistry() - reg.Register("k", harnessconfig.Configuration{}, stubFactory("v1")) - reg.Register("k", harnessconfig.Configuration{}, stubFactory("v2")) + reg.RegisterKind(proto.SupportedAgentKind{Kind: "k", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, stubFactory("v1")) + reg.RegisterKind(proto.SupportedAgentKind{Kind: "k", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, stubFactory("v2")) f, err := reg.Resolve("k") if err != nil { @@ -73,8 +74,8 @@ func TestRegistryRegisterOverwrites(t *testing.T) { func TestRegistryKindsReportsRegistered(t *testing.T) { reg := agent.NewRegistry() - reg.Register("claude_code", harnessconfig.Configuration{}, stubFactory("cc")) - reg.Register("opencode", harnessconfig.Configuration{}, stubFactory("oc")) + reg.RegisterKind(proto.SupportedAgentKind{Kind: "claude_code", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, stubFactory("cc")) + reg.RegisterKind(proto.SupportedAgentKind{Kind: "opencode", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, stubFactory("oc")) got := reg.Kinds() slices.Sort(got) @@ -90,7 +91,7 @@ func TestRegistryRegisterPanicsOnEmptyKind(t *testing.T) { t.Fatal("Register(\"\", ...) did not panic") } }() - agent.NewRegistry().Register("", harnessconfig.Configuration{}, stubFactory("x")) + agent.NewRegistry().RegisterKind(proto.SupportedAgentKind{Kind: "", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, stubFactory("x")) } func TestRegistryRegisterPanicsOnNilFactory(t *testing.T) { @@ -99,7 +100,7 @@ func TestRegistryRegisterPanicsOnNilFactory(t *testing.T) { t.Fatal("Register(kind, nil) did not panic") } }() - agent.NewRegistry().Register("k", harnessconfig.Configuration{}, nil) + agent.NewRegistry().RegisterKind(proto.SupportedAgentKind{Kind: "k", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, nil) } func TestRegistrySupportedAgentKindsReportsDescriptors(t *testing.T) { @@ -108,20 +109,20 @@ func TestRegistrySupportedAgentKindsReportsDescriptors(t *testing.T) { Kind: "opencode", Available: false, Version: "missing", - Capabilities: proto.AgentKindCapabilities{ - Streaming: true, - }, + Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{ + Streaming: proto.CapabilitySupported, + }), }, harnessconfig.Configuration{}, stubFactory("oc")) reg.RegisterKind(proto.SupportedAgentKind{ Kind: "claude_code", Available: true, Version: "1.2.3", - Capabilities: proto.AgentKindCapabilities{ - Streaming: true, - Permissions: true, - Usage: true, - Resume: true, - }, + Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{ + Streaming: proto.CapabilitySupported, + Permissions: proto.CapabilitySupported, + Usage: proto.CapabilitySupported, + Resume: proto.CapabilitySupported, + }), }, harnessconfig.Configuration{}, stubFactory("cc")) got := reg.SupportedAgentKinds() @@ -131,17 +132,17 @@ func TestRegistrySupportedAgentKindsReportsDescriptors(t *testing.T) { if got[0].Kind != "claude_code" || got[1].Kind != "opencode" { t.Fatalf("SupportedAgentKinds sort = %#v, want claude_code then opencode", got) } - if !got[0].Available || got[0].Version != "1.2.3" || !got[0].Capabilities.Permissions || !got[0].Capabilities.Resume { + if !got[0].Available || got[0].Version != "1.2.3" || !got[0].Capabilities.Permissions.IsSupported() || !got[0].Capabilities.Resume.IsSupported() { t.Fatalf("claude_code descriptor not preserved: %#v", got[0]) } - if got[1].Available || got[1].Version != "missing" || !got[1].Capabilities.Streaming { + if got[1].Available || got[1].Version != "missing" || !got[1].Capabilities.Streaming.IsSupported() { t.Fatalf("opencode descriptor not preserved: %#v", got[1]) } } func TestRegistryExecutorRequiresExplicitRegistration(t *testing.T) { registry := agent.NewRegistry() - registry.Register("native", harnessconfig.Configuration{}, stubFactory("native")) + registry.RegisterKind(proto.SupportedAgentKind{Kind: "native", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, stubFactory("native")) if _, err := registry.ResolveExecutor("native"); err == nil { t.Fatal("legacy factory implied reusable execution") } @@ -154,7 +155,7 @@ func TestRegistryExecutorRequiresExplicitRegistration(t *testing.T) { if _, err := factory(t.Context(), proto.PromptRequestPayload{}); !errors.Is(err, expected) { t.Fatal(err) } - registry.Register("native", harnessconfig.Configuration{}, stubFactory("replacement")) + registry.RegisterKind(proto.SupportedAgentKind{Kind: "native", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, stubFactory("replacement")) if _, err := registry.ResolveExecutor("native"); err == nil { t.Fatal("replacing a kind retained its old executor capability") } diff --git a/apps/parsar-daemon/internal/agent/workspace_read.go b/apps/parsar-daemon/internal/agent/workspace_read.go index 03e270d5c..71f1db3e5 100644 --- a/apps/parsar-daemon/internal/agent/workspace_read.go +++ b/apps/parsar-daemon/internal/agent/workspace_read.go @@ -2,6 +2,7 @@ package agent import ( "errors" + "fmt" ) type WorkspaceReadResult struct { @@ -10,7 +11,7 @@ type WorkspaceReadResult struct { } var ( - ErrWorkspaceReadUnsupported = errors.New("workspace read unsupported") + ErrWorkspaceReadUnsupported = fmt.Errorf("%w: workspace read", ErrUnsupportedOperation) ErrWorkspaceReadUnavailable = errors.New("workspace read unavailable") ErrWorkspaceReadBusy = errors.New("workspace read busy") ErrWorkspaceReadInvalid = errors.New("workspace read invalid") diff --git a/apps/parsar-daemon/internal/agent/workspace_write.go b/apps/parsar-daemon/internal/agent/workspace_write.go index 11130d377..e584843f8 100644 --- a/apps/parsar-daemon/internal/agent/workspace_write.go +++ b/apps/parsar-daemon/internal/agent/workspace_write.go @@ -10,7 +10,7 @@ type WorkspaceWriteResult struct { } var ( - ErrWorkspaceWriteUnsupported = errors.New("workspace write unsupported") + ErrWorkspaceWriteUnsupported = fmt.Errorf("%w: workspace write", ErrUnsupportedOperation) ErrWorkspaceWriteUnavailable = errors.New("workspace write unavailable") ErrWorkspaceWriteBusy = errors.New("workspace write busy") ErrWorkspaceWriteInvalid = errors.New("workspace write invalid") diff --git a/apps/parsar-daemon/internal/cli/agent_registration.go b/apps/parsar-daemon/internal/cli/agent_registration.go index 1cd70ecef..1197e2cb1 100644 --- a/apps/parsar-daemon/internal/cli/agent_registration.go +++ b/apps/parsar-daemon/internal/cli/agent_registration.go @@ -28,7 +28,7 @@ func registerAgentKinds(registry *agent.Registry, agentCLIs agentCLIDiscovery, s if agentCLIs.Codex.Available { registry.RegisterExecutor("codex", withExecutorCapabilities(codex.NewExecutorFactory(), serverURL)) } - if agentCLIs.Codex.Available && agentCLIs.Codex.Capabilities.LocalEnvironment { + if agentCLIs.Codex.Available && agentCLIs.Codex.Capabilities.LocalEnvironment.IsSupported() { registry.RegisterPreparation("codex", true, func(ctx context.Context, req proto.PromptRequestPayload) (agent.Prepared, error) { prepared, err := codex.Prepare(ctx, req) if prepared == nil { @@ -51,6 +51,6 @@ func registerAgentKinds(registry *agent.Registry, agentCLIs agentCLIDiscovery, s } func registerProductAgentKind(registry *agent.Registry, info proto.SupportedAgentKind, configuration harnessconfig.Configuration, factory agent.Factory) { - info.Capabilities.WorkspaceAuthoring = true + info.Capabilities.WorkspaceAuthoring = proto.CapabilitySupported registry.RegisterKind(info, configuration, factory) } diff --git a/apps/parsar-daemon/internal/cli/authoring.go b/apps/parsar-daemon/internal/cli/authoring.go index f75917b90..01e336faa 100644 --- a/apps/parsar-daemon/internal/cli/authoring.go +++ b/apps/parsar-daemon/internal/cli/authoring.go @@ -58,7 +58,7 @@ func authoringRegistry(registry *agent.Registry, bridge *authoring.Bridge) *agen wrapped := agent.NewRegistry() for _, info := range registry.SupportedAgentKinds() { factory, _ := registry.Resolve(info.Kind) - if info.Capabilities.WorkspaceAuthoring { + if info.Capabilities.WorkspaceAuthoring.IsSupported() { factory = withAuthoringBridge(factory, bridge) } configuration, err := registry.Configuration(info.Kind) @@ -70,7 +70,7 @@ func authoringRegistry(registry *agent.Registry, bridge *authoring.Bridge) *agen wrapped.RegisterExecutor(info.Kind, executor) } if prepare, err := registry.ResolvePreparation(info.Kind); err == nil { - wrapped.RegisterPreparation(info.Kind, info.Capabilities.WorkspaceReadPreparation, prepare) + wrapped.RegisterPreparation(info.Kind, info.Capabilities.WorkspaceReadPreparation.IsSupported(), prepare) } } return wrapped diff --git a/apps/parsar-daemon/internal/cli/authoring_test.go b/apps/parsar-daemon/internal/cli/authoring_test.go index ffdb411f5..8081469b9 100644 --- a/apps/parsar-daemon/internal/cli/authoring_test.go +++ b/apps/parsar-daemon/internal/cli/authoring_test.go @@ -5,6 +5,7 @@ import "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig" import ( "context" "errors" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "net" "os" "testing" @@ -71,7 +72,7 @@ func TestAuthoringRegistryRequiresExplicitCapability(t *testing.T) { called := false stop := errors.New("controlled factory stop") out := make(chan proto.Envelope, 1) - reg.RegisterKind(proto.SupportedAgentKind{Kind: "engine", Available: true, Capabilities: proto.AgentKindCapabilities{WorkspaceAuthoring: optIn}}, harnessconfig.Configuration{}, func(_ context.Context, req proto.PromptRequestPayload, events chan<- proto.Envelope) (agent.Session, error) { + reg.RegisterKind(proto.SupportedAgentKind{Kind: "engine", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{WorkspaceAuthoring: proto.CapabilityFromBool(optIn)})}, harnessconfig.Configuration{}, func(_ context.Context, req proto.PromptRequestPayload, events chan<- proto.Envelope) (agent.Session, error) { called = true env, _ := req.AgentOptions["env"].(map[string]any) socket, _ := env[proto.AuthoringSocketEnv].(string) diff --git a/apps/parsar-daemon/internal/cli/claude_sdk_test.go b/apps/parsar-daemon/internal/cli/claude_sdk_test.go index 6eb2c21a5..67812af4f 100644 --- a/apps/parsar-daemon/internal/cli/claude_sdk_test.go +++ b/apps/parsar-daemon/internal/cli/claude_sdk_test.go @@ -79,7 +79,7 @@ func TestClaudeSDKDiscoveryAndRegistration(t *testing.T) { } info := discovery.ClaudeSDK.Info if info.Available { - info.Capabilities.Preparation = true + info.Capabilities.Preparation = proto.CapabilitySupported } if info.Available != tc.ready { t.Fatal(info) @@ -88,12 +88,12 @@ func TestClaudeSDKDiscoveryAndRegistration(t *testing.T) { if registered.Kind == "claude_sdk" && registered != info { t.Fatalf("SDK descriptor changed: %+v", registered) } - if registered.Kind != "claude_sdk" && !registered.Capabilities.WorkspaceAuthoring { + if registered.Kind != "claude_sdk" && !registered.Capabilities.WorkspaceAuthoring.IsSupported() { t.Fatalf("product authoring lost: %+v", registered) } } caps := info.Capabilities - if caps.WorkspaceAuthoring || caps.Permissions || caps.WebSearchControl || caps.TextVerbosity || !caps.DurableTurns || !caps.DurableInputReceipts || !caps.FunctionTools || !caps.EnvironmentNone { + if caps.WorkspaceAuthoring.IsSupported() || caps.Permissions.IsSupported() || caps.WebSearchControl.IsSupported() || caps.TextVerbosity.IsSupported() || !caps.DurableTurns.IsSupported() || !caps.DurableInputReceipts.IsSupported() || !caps.FunctionTools.IsSupported() || !caps.EnvironmentNone.IsSupported() { t.Fatalf("incorrect SDK capability scope: %+v", caps) } // Even a ready SDK must not acquire product write access through the wrapper. @@ -142,13 +142,13 @@ func TestClaudeSDKFeatureDiscovery(t *testing.T) { return info, nil }) supported := len(features) > 0 && features[0] == "mcp_http_tools" - if out == nil || !out.Info.Available || out.Info.Capabilities.MCPHTTPTools != supported || out.Info.Capabilities.MCPHTTPBearerAuth != (supported && slices.Contains(features, "mcp_http_bearer_auth")) || out.Info.Capabilities.MCPHTTPRequired != (supported && slices.Contains(features, "mcp_http_required")) { + if out == nil || !out.Info.Available || out.Info.Capabilities.MCPHTTPTools.IsSupported() != supported || out.Info.Capabilities.MCPHTTPBearerAuth.IsSupported() != (supported && slices.Contains(features, "mcp_http_bearer_auth")) || out.Info.Capabilities.MCPHTTPRequired.IsSupported() != (supported && slices.Contains(features, "mcp_http_required")) { t.Fatal("MCP feature discovery widened the runtime profile") } - if out.Info.Capabilities.StructuredOutput != slices.Contains(features, "structured_output") { + if out.Info.Capabilities.StructuredOutput.IsSupported() != slices.Contains(features, "structured_output") { t.Fatal("structured output feature does not match the installed runtime") } - if out.Info.Capabilities.SubagentObservations != slices.Contains(features, "subagent_resources") { + if out.Info.Capabilities.SubagentObservations.IsSupported() != slices.Contains(features, "subagent_resources") { t.Fatal("Subagent feature discovery does not match the runtime contract") } } diff --git a/apps/parsar-daemon/internal/cli/connect.go b/apps/parsar-daemon/internal/cli/connect.go index 037ee086a..7ea1d4830 100644 --- a/apps/parsar-daemon/internal/cli/connect.go +++ b/apps/parsar-daemon/internal/cli/connect.go @@ -451,7 +451,7 @@ func pumpConn(parentCtx context.Context, conn *transport.Conn, registry *agent.R kinds := registry.SupportedAgentKinds() for i := range kinds { caps := &kinds[i].Capabilities - caps.WorkspaceOutputExport = local.CanExport() && caps.LocalEnvironment && caps.WorkspaceReadPreparation + caps.WorkspaceOutputExport = proto.CapabilityFromBool(local.CanExport() && caps.LocalEnvironment.IsSupported() && caps.WorkspaceReadPreparation.IsSupported()) } return proto.HeartbeatPayload{ Timestamp: time.Now().Unix(), diff --git a/apps/parsar-daemon/internal/cli/connect_cleanup_test.go b/apps/parsar-daemon/internal/cli/connect_cleanup_test.go index fa963eac0..cd253530e 100644 --- a/apps/parsar-daemon/internal/cli/connect_cleanup_test.go +++ b/apps/parsar-daemon/internal/cli/connect_cleanup_test.go @@ -5,6 +5,7 @@ import "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig" import ( "context" "errors" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "net/http" "net/http/httptest" "os" @@ -114,7 +115,7 @@ func testDisconnectedPumpCleanup(t *testing.T, suspend bool) { } owner := &cleanupExecutor{retry: make(chan struct{}), confirm: make(chan struct{})} registry := agent.NewRegistry() - registry.RegisterKind(proto.SupportedAgentKind{Kind: "cleanup", Available: true, Capabilities: proto.AgentKindCapabilities{EnvironmentNone: true}}, + registry.RegisterKind(proto.SupportedAgentKind{Kind: "cleanup", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { return nil, errors.New("unexpected legacy factory") }) diff --git a/apps/parsar-daemon/internal/cli/connect_suspend.go b/apps/parsar-daemon/internal/cli/connect_suspend.go index 0253f0a5f..149716ce1 100644 --- a/apps/parsar-daemon/internal/cli/connect_suspend.go +++ b/apps/parsar-daemon/internal/cli/connect_suspend.go @@ -159,7 +159,7 @@ func (s *suspendedRouter) heartbeats(ctx context.Context, conn *transport.Conn, kinds := s.registry.SupportedAgentKinds() for i := range kinds { caps := &kinds[i].Capabilities - caps.WorkspaceOutputExport = s.local.CanExport() && caps.LocalEnvironment && caps.WorkspaceReadPreparation + caps.WorkspaceOutputExport = proto.CapabilityFromBool(s.local.CanExport() && caps.LocalEnvironment.IsSupported() && caps.WorkspaceReadPreparation.IsSupported()) } return proto.HeartbeatPayload{Timestamp: time.Now().Unix(), ActiveRequests: s.router.ActiveRuns(), DaemonVersion: Version, SupportedAgentKinds: kinds} }, obslog.Bg()) diff --git a/apps/parsar-daemon/internal/cli/connect_test.go b/apps/parsar-daemon/internal/cli/connect_test.go index ab3ada305..9d910ba8e 100644 --- a/apps/parsar-daemon/internal/cli/connect_test.go +++ b/apps/parsar-daemon/internal/cli/connect_test.go @@ -2,6 +2,7 @@ package cli import ( "context" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "os" "reflect" "strings" @@ -102,7 +103,7 @@ func TestDiscoverAgentCLIsAllowsOpenCodeWithoutClaude(t *testing.T) { if got.Pi.Available { t.Fatalf("Pi.Available = true, want false: %#v", got.Pi) } - if !got.OpenCode.Capabilities.Streaming || !got.OpenCode.Capabilities.Usage || got.OpenCode.Capabilities.Permissions { + if !got.OpenCode.Capabilities.Streaming.IsSupported() || !got.OpenCode.Capabilities.Usage.IsSupported() || got.OpenCode.Capabilities.Permissions.IsSupported() { t.Fatalf("OpenCode capabilities = %#v", got.OpenCode.Capabilities) } if !strings.Contains(stdout.String(), "OpenCode preflight ok") { @@ -174,19 +175,19 @@ func TestDiscoverAgentCLIsBothAvailable(t *testing.T) { if !got.Codex.Available || got.Codex.Version != "codex 0.141.0" { t.Fatalf("Codex descriptor = %#v", got.Codex) } - if !got.ClaudeCode.Capabilities.Permissions || !got.ClaudeCode.Capabilities.Resume { + if !got.ClaudeCode.Capabilities.Permissions.IsSupported() || !got.ClaudeCode.Capabilities.Resume.IsSupported() { t.Fatalf("ClaudeCode capabilities = %#v", got.ClaudeCode.Capabilities) } - if got.Codex.Capabilities.ExecutionControls != codex.SupportsTextVerbosity || got.ClaudeCode.Capabilities.ExecutionControls || got.OpenCode.Capabilities.ExecutionControls || !got.Codex.Capabilities.ToolObservations || !got.Codex.Capabilities.SubagentControl || got.Codex.Capabilities.TextVerbosity != codex.SupportsTextVerbosity || !got.Codex.Capabilities.WebSearchControl || !got.Codex.Capabilities.EnvironmentNone || !got.Codex.Capabilities.MessageItems || !got.Codex.Capabilities.Streaming || !got.Codex.Capabilities.Permissions || !got.Codex.Capabilities.Resume { + if got.Codex.Capabilities.ExecutionControls.IsSupported() != codex.SupportsTextVerbosity || got.ClaudeCode.Capabilities.ExecutionControls.IsSupported() || got.OpenCode.Capabilities.ExecutionControls.IsSupported() || !got.Codex.Capabilities.ToolObservations.IsSupported() || !got.Codex.Capabilities.SubagentControl.IsSupported() || got.Codex.Capabilities.TextVerbosity.IsSupported() != codex.SupportsTextVerbosity || !got.Codex.Capabilities.WebSearchControl.IsSupported() || !got.Codex.Capabilities.EnvironmentNone.IsSupported() || !got.Codex.Capabilities.MessageItems.IsSupported() || !got.Codex.Capabilities.Streaming.IsSupported() || !got.Codex.Capabilities.Permissions.IsSupported() || !got.Codex.Capabilities.Resume.IsSupported() { t.Fatalf("Codex capabilities = %#v (want Streaming+Permissions+Resume)", got.Codex.Capabilities) } if !got.Pi.Available || got.Pi.Version != "pi 0.1.0" { t.Fatalf("Pi descriptor = %#v", got.Pi) } - if !got.Pi.Capabilities.Streaming || !got.Pi.Capabilities.Usage || !got.Pi.Capabilities.Resume || got.Pi.Capabilities.Permissions { + if !got.Pi.Capabilities.Streaming.IsSupported() || !got.Pi.Capabilities.Usage.IsSupported() || !got.Pi.Capabilities.Resume.IsSupported() || got.Pi.Capabilities.Permissions.IsSupported() { t.Fatalf("Pi capabilities = %#v (want Streaming+Usage+Resume, no Permissions)", got.Pi.Capabilities) } - if !got.MCode.Available || !got.MCode.Capabilities.Resume || got.MCode.Capabilities.Usage { + if !got.MCode.Available || !got.MCode.Capabilities.Resume.IsSupported() || got.MCode.Capabilities.Usage.IsSupported() { t.Fatalf("mcode descriptor = %#v", got.MCode) } if stderr.Len() != 0 { @@ -197,47 +198,47 @@ func TestDiscoverAgentCLIsBothAvailable(t *testing.T) { func TestRegisterAgentKindsPreservesDescriptors(t *testing.T) { reg := agent.NewRegistry() registerAgentKinds(reg, agentCLIDiscovery{ - MCode: proto.SupportedAgentKind{Kind: "mcode", Available: true, Version: "0.4.12", Capabilities: proto.AgentKindCapabilities{Streaming: true, Permissions: true, Resume: true}}, + MCode: proto.SupportedAgentKind{Kind: "mcode", Available: true, Version: "0.4.12", Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Streaming: proto.CapabilitySupported, Permissions: proto.CapabilitySupported, Resume: proto.CapabilitySupported})}, ClaudeCode: proto.SupportedAgentKind{ Kind: "claude_code", Available: true, Version: "claude 2.0.0", - Capabilities: proto.AgentKindCapabilities{ - Streaming: true, - Permissions: true, - Usage: true, - Resume: true, - }, + Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{ + Streaming: proto.CapabilitySupported, + Permissions: proto.CapabilitySupported, + Usage: proto.CapabilitySupported, + Resume: proto.CapabilitySupported, + }), }, OpenCode: proto.SupportedAgentKind{ Kind: "opencode", Available: false, Version: "missing", - Capabilities: proto.AgentKindCapabilities{ - Streaming: true, - Usage: true, - }, + Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{ + Streaming: proto.CapabilitySupported, + Usage: proto.CapabilitySupported, + }), }, Codex: proto.SupportedAgentKind{ Kind: "codex", Available: true, Version: "codex 0.141.0", - Capabilities: proto.AgentKindCapabilities{ - Streaming: true, - Permissions: true, - Usage: true, - Resume: true, - }, + Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{ + Streaming: proto.CapabilitySupported, + Permissions: proto.CapabilitySupported, + Usage: proto.CapabilitySupported, + Resume: proto.CapabilitySupported, + }), }, Pi: proto.SupportedAgentKind{ Kind: "pi", Available: true, Version: "pi 0.1.0", - Capabilities: proto.AgentKindCapabilities{ - Streaming: true, - Usage: true, - Resume: true, - }, + Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{ + Streaming: proto.CapabilitySupported, + Usage: proto.CapabilitySupported, + Resume: proto.CapabilitySupported, + }), }, }, "https://core.example.test") @@ -249,16 +250,16 @@ func TestRegisterAgentKindsPreservesDescriptors(t *testing.T) { if kinds[0].Kind != "claude_code" || kinds[1].Kind != "codex" || kinds[2].Kind != "mcode" || kinds[3].Kind != "opencode" || kinds[4].Kind != "pi" { t.Fatalf("SupportedAgentKinds sort = %#v", kinds) } - if !kinds[0].Available || kinds[0].Version != "claude 2.0.0" || !kinds[0].Capabilities.Permissions { + if !kinds[0].Available || kinds[0].Version != "claude 2.0.0" || !kinds[0].Capabilities.Permissions.IsSupported() { t.Fatalf("claude descriptor not preserved: %#v", kinds[0]) } - if !kinds[1].Available || kinds[1].Version != "codex 0.141.0" || !kinds[1].Capabilities.Resume { + if !kinds[1].Available || kinds[1].Version != "codex 0.141.0" || !kinds[1].Capabilities.Resume.IsSupported() { t.Fatalf("codex descriptor not preserved: %#v", kinds[1]) } - if kinds[3].Available || kinds[3].Version != "missing" || !kinds[3].Capabilities.Streaming || !kinds[3].Capabilities.Usage { + if kinds[3].Available || kinds[3].Version != "missing" || !kinds[3].Capabilities.Streaming.IsSupported() || !kinds[3].Capabilities.Usage.IsSupported() { t.Fatalf("opencode descriptor not preserved: %#v", kinds[3]) } - if !kinds[4].Available || kinds[4].Version != "pi 0.1.0" || !kinds[4].Capabilities.Resume || kinds[4].Capabilities.Permissions { + if !kinds[4].Available || kinds[4].Version != "pi 0.1.0" || !kinds[4].Capabilities.Resume.IsSupported() || kinds[4].Capabilities.Permissions.IsSupported() { t.Fatalf("pi descriptor not preserved: %#v", kinds[4]) } if _, err := reg.Resolve("opencode"); err != nil { diff --git a/apps/parsar-daemon/internal/cli/mcode_execution_test.go b/apps/parsar-daemon/internal/cli/mcode_execution_test.go index 1f03f381f..cbf8bab97 100644 --- a/apps/parsar-daemon/internal/cli/mcode_execution_test.go +++ b/apps/parsar-daemon/internal/cli/mcode_execution_test.go @@ -15,10 +15,10 @@ func TestMCodeExecutionOptInIsVersionBound(t *testing.T) { t.Setenv("OAC_RUNTIME_MCODE_AGENTS_API", tc.enabled) rc := &runContext{stdout: io.Discard, stderr: io.Discard} info := discoverMCode(t.Context(), rc, func(context.Context, string) (string, error) { return tc.version, nil }) - if !info.Available || info.Capabilities.EnvironmentNone != tc.qualified || info.Capabilities.DurableInputReceipts != tc.qualified || info.Capabilities.SubagentObservations != tc.qualified { + if !info.Available || info.Capabilities.EnvironmentNone.IsSupported() != tc.qualified || info.Capabilities.DurableInputReceipts.IsSupported() != tc.qualified || info.Capabilities.SubagentObservations.IsSupported() != tc.qualified { t.Fatalf("capabilities=%+v", info.Capabilities) } - if info.Capabilities.NativeSessionRecovery || info.Capabilities.LocalEnvironment || info.Capabilities.FunctionTools { + if info.Capabilities.NativeSessionRecovery.IsSupported() || info.Capabilities.LocalEnvironment.IsSupported() || info.Capabilities.FunctionTools.IsSupported() { t.Fatal("unqualified capability advertised") } }) diff --git a/apps/parsar-daemon/internal/cli/mcp_test.go b/apps/parsar-daemon/internal/cli/mcp_test.go index aee418290..7c756cba6 100644 --- a/apps/parsar-daemon/internal/cli/mcp_test.go +++ b/apps/parsar-daemon/internal/cli/mcp_test.go @@ -19,10 +19,10 @@ func TestMCPHTTPBearerDiscoveryExcludesUnconfiguredSDK(t *testing.T) { registry := agent.NewRegistry() registerAgentKinds(registry, discovery, "https://service.example") for _, kind := range registry.SupportedAgentKinds() { - if kind.Capabilities.MCPHTTPBearerAuth != (kind.Kind == "codex") { + if kind.Capabilities.MCPHTTPBearerAuth.IsSupported() != (kind.Kind == "codex") { t.Fatal("bearer capability missing or advertised for another adapter") } - if kind.Kind == "codex" && (!kind.Available || !kind.Capabilities.MCPHTTPTools || !kind.Capabilities.EnvironmentNone) { + if kind.Kind == "codex" && (!kind.Available || !kind.Capabilities.MCPHTTPTools.IsSupported() || !kind.Capabilities.EnvironmentNone.IsSupported()) { t.Fatal("bearer capability lacks prerequisite profile") } } @@ -36,13 +36,13 @@ func TestMCPRequiredDiscoveryRequiresPinnedNative(t *testing.T) { if err != nil { t.Fatal(err) } - if got.Codex.Capabilities.MCPHTTPRequired != (version == "codex-cli 0.153.4") { + if got.Codex.Capabilities.MCPHTTPRequired.IsSupported() != (version == "codex-cli 0.153.4") { t.Fatal("unverified native combination advertised") } - if got.Codex.Capabilities.NativeSessionRecovery != (version == "codex-cli 0.153.4") { + if got.Codex.Capabilities.NativeSessionRecovery.IsSupported() != (version == "codex-cli 0.153.4") { t.Fatal("unverified native recovery advertised") } - if got.ClaudeCode.Capabilities.MCPHTTPRequired || got.OpenCode.Capabilities.MCPHTTPRequired || got.Pi.Capabilities.MCPHTTPRequired { + if got.ClaudeCode.Capabilities.MCPHTTPRequired.IsSupported() || got.OpenCode.Capabilities.MCPHTTPRequired.IsSupported() || got.Pi.Capabilities.MCPHTTPRequired.IsSupported() { t.Fatal("other engine advertised combination") } } diff --git a/apps/parsar-daemon/internal/cli/preparation_test.go b/apps/parsar-daemon/internal/cli/preparation_test.go index 4f6c5b7a4..05b868e2c 100644 --- a/apps/parsar-daemon/internal/cli/preparation_test.go +++ b/apps/parsar-daemon/internal/cli/preparation_test.go @@ -5,6 +5,7 @@ import "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig" import ( "context" "errors" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "testing" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" @@ -15,7 +16,7 @@ import ( func TestPreparationRegistrationBypassesProductWrappers(t *testing.T) { for _, supported := range []bool{false, true} { reg := agent.NewRegistry() - registerAgentKinds(reg, agentCLIDiscovery{Codex: proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: proto.AgentKindCapabilities{LocalEnvironment: supported}}, ClaudeCode: proto.SupportedAgentKind{Kind: "claude_code"}, OpenCode: proto.SupportedAgentKind{Kind: "opencode"}, Pi: proto.SupportedAgentKind{Kind: "pi"}, MCode: proto.SupportedAgentKind{Kind: "mcode"}}, "http://unreachable.invalid") + registerAgentKinds(reg, agentCLIDiscovery{Codex: proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilityFromBool(supported)})}, ClaudeCode: proto.SupportedAgentKind{Kind: "claude_code"}, OpenCode: proto.SupportedAgentKind{Kind: "opencode"}, Pi: proto.SupportedAgentKind{Kind: "pi"}, MCode: proto.SupportedAgentKind{Kind: "mcode"}}, "http://unreachable.invalid") _, err := reg.ResolvePreparation("codex") if (err == nil) != supported { t.Fatal("unverified native version advertised preparation") @@ -39,11 +40,11 @@ func TestPreparationRegistrationBypassesProductWrappers(t *testing.T) { t.Fatal("raw preparation was lost or wrapped", err) } for _, info := range wrapped.SupportedAgentKinds() { - if info.Kind == "codex" && (!info.Capabilities.Preparation || !info.Capabilities.WorkspaceReadPreparation) { + if info.Kind == "codex" && (!info.Capabilities.Preparation.IsSupported() || !info.Capabilities.WorkspaceReadPreparation.IsSupported()) { t.Fatal("real heartbeat registry lost preparation") } } - wrapped.Register("codex", harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + wrapped.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { return nil, stop }) if _, err := wrapped.ResolvePreparation("codex"); err == nil { diff --git a/apps/parsar-daemon/internal/contracttest/wire_test.go b/apps/parsar-daemon/internal/contracttest/wire_test.go index 9cf58ace5..070c16514 100644 --- a/apps/parsar-daemon/internal/contracttest/wire_test.go +++ b/apps/parsar-daemon/internal/contracttest/wire_test.go @@ -7,6 +7,7 @@ import "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig" import ( "context" "errors" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "net/http" "net/http/httptest" "strings" @@ -85,7 +86,7 @@ func connectFixture(t *testing.T, factory agent.ExecutorFactory) *wireFixture { t.Fatal(err) } kinds := agent.NewRegistry() - kinds.RegisterKind(proto.SupportedAgentKind{Kind: "contract", Available: true, Capabilities: proto.AgentKindCapabilities{EnvironmentNone: true}}, + kinds.RegisterKind(proto.SupportedAgentKind{Kind: "contract", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { return nil, errors.New("prepared execution must not use prompt_request") }) diff --git a/apps/parsar-daemon/internal/dispatch/cancellation_test.go b/apps/parsar-daemon/internal/dispatch/cancellation_test.go index f25280d05..3553dcc6b 100644 --- a/apps/parsar-daemon/internal/dispatch/cancellation_test.go +++ b/apps/parsar-daemon/internal/dispatch/cancellation_test.go @@ -5,6 +5,7 @@ import "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig" import ( "context" "errors" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "testing" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" @@ -26,7 +27,7 @@ func TestCompletionWaitsForNativeWriterRelease(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) sess := &cancelReceiptSession{entered: make(chan struct{}), release: make(chan struct{})} - h.reg.Register("codex", harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { sess.fakeSession = &fakeSession{out: out, closeOutOnCancel: true} return sess, nil }) @@ -65,7 +66,7 @@ func TestCancellationReceiptFollowsAdapterOutcome(t *testing.T) { if fails { sess.err = errors.New("adapter could not cancel") } - h.reg.Register("codex", harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { sess.fakeSession = &fakeSession{out: out, closeOutOnCancel: true} return sess, nil }) diff --git a/apps/parsar-daemon/internal/dispatch/environment.go b/apps/parsar-daemon/internal/dispatch/environment.go index fef75ac38..7b826397d 100644 --- a/apps/parsar-daemon/internal/dispatch/environment.go +++ b/apps/parsar-daemon/internal/dispatch/environment.go @@ -7,16 +7,16 @@ import ( ) func validateExecutionEnvironment(req proto.PromptRequestPayload, caps proto.AgentKindCapabilities) error { - if err := req.ValidateProgrammaticToolCallingDisable(caps.ProgrammaticToolCallingDisable); err != nil { + if err := req.ValidateProgrammaticToolCallingDisable(caps.ProgrammaticToolCallingDisable.IsSupported()); err != nil { return err } - if err := req.ValidateToolSearch(caps.ToolSearch); err != nil { + if err := req.ValidateToolSearch(caps.ToolSearch.IsSupported()); err != nil { return err } - if req.LocalEnvironment != nil && (req.DisableExecutionEnvironment || !caps.LocalEnvironment) { + if req.LocalEnvironment != nil && (req.DisableExecutionEnvironment || !caps.LocalEnvironment.IsSupported()) { return errors.New("engine does not support this local Environment configuration") } - if req.DisableExecutionEnvironment && !caps.EnvironmentNone { + if req.DisableExecutionEnvironment && !caps.EnvironmentNone.IsSupported() { return errors.New("engine does not support execution environment none") } return validateMCPHTTP(req, caps) diff --git a/apps/parsar-daemon/internal/dispatch/environment_test.go b/apps/parsar-daemon/internal/dispatch/environment_test.go index 4d2469c04..22692b61a 100644 --- a/apps/parsar-daemon/internal/dispatch/environment_test.go +++ b/apps/parsar-daemon/internal/dispatch/environment_test.go @@ -5,6 +5,7 @@ import "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig" import ( "context" "errors" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "testing" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" @@ -15,7 +16,7 @@ func TestNoEnvironmentRejectsOtherEngineBeforeFactory(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) called := false - h.reg.Register("claude_code", harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "claude_code", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { called = true return nil, nil }) @@ -34,7 +35,7 @@ func TestNoEnvironmentUsesAvailableCapability(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) called := false - h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "claude_sdk", Available: available, Capabilities: proto.AgentKindCapabilities{EnvironmentNone: true}}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "claude_sdk", Available: available, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { called = true return nil, errors.New("controlled factory stop") }) @@ -52,7 +53,7 @@ func TestLocalEnvironmentRequiresAvailableCapability(t *testing.T) { defer h.router.Shutdown(context.Background()) called := false h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: mode != "unavailable", - Capabilities: proto.AgentKindCapabilities{LocalEnvironment: mode != "unsupported"}}, + Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilityFromBool(mode != "unsupported")})}, harnessconfig.Configuration{}, func(_ context.Context, req proto.PromptRequestPayload, _ chan<- proto.Envelope) (agent.Session, error) { called = true if req.LocalEnvironment == nil || req.LocalEnvironment.ID != preparationEnvironmentID { diff --git a/apps/parsar-daemon/internal/dispatch/executor.go b/apps/parsar-daemon/internal/dispatch/executor.go index afe959a66..30cc47a8b 100644 --- a/apps/parsar-daemon/internal/dispatch/executor.go +++ b/apps/parsar-daemon/internal/dispatch/executor.go @@ -49,14 +49,14 @@ func (r *Router) handleExecutorPrepare(ctx context.Context, env proto.Envelope, } caps := r.availableCapabilities(req.AgentKind) factory, err := r.registry.ResolveExecutor(req.AgentKind) - if err != nil || !caps.Preparation { + if err != nil || !caps.Preparation.IsSupported() { return r.rejectPreparation(env, "unsupported_preparation") } req, err = r.localWorkspace.Configure(req) if err != nil { return r.rejectPreparation(env, "invalid_configuration") } - if validateExecutionEnvironment(req, caps) != nil || len(req.FunctionTools) > 0 && !caps.FunctionTools { + if validateExecutionEnvironment(req, caps) != nil || len(req.FunctionTools) > 0 && !caps.FunctionTools.IsSupported() { return r.rejectPreparation(env, "unsupported_configuration") } fingerprint, err := executorFingerprint(req) diff --git a/apps/parsar-daemon/internal/dispatch/executor_cancel_receipt_test.go b/apps/parsar-daemon/internal/dispatch/executor_cancel_receipt_test.go index d799c60c9..9d83f86c4 100644 --- a/apps/parsar-daemon/internal/dispatch/executor_cancel_receipt_test.go +++ b/apps/parsar-daemon/internal/dispatch/executor_cancel_receipt_test.go @@ -5,6 +5,7 @@ import "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig" import ( "context" "errors" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "sync" "sync/atomic" "testing" @@ -126,7 +127,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: proto.AgentKindCapabilities{EnvironmentNone: true}}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + reg.RegisterKind(proto.SupportedAgentKind{Kind: "reusable", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { return nil, errors.New("legacy factory forbidden") }) reg.RegisterExecutor("reusable", func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { return owner, nil }) diff --git a/apps/parsar-daemon/internal/dispatch/executor_handoff_test.go b/apps/parsar-daemon/internal/dispatch/executor_handoff_test.go index 2a3d73d4d..53a1b59cb 100644 --- a/apps/parsar-daemon/internal/dispatch/executor_handoff_test.go +++ b/apps/parsar-daemon/internal/dispatch/executor_handoff_test.go @@ -5,6 +5,7 @@ import "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig" import ( "context" "errors" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "sync" "sync/atomic" "testing" @@ -92,7 +93,7 @@ func TestPreparedDonePublishesAfterExecutorHandoff(t *testing.T) { } owner := &terminalHandoffExecutor{turns: make(chan *terminalHandoffTurn, 3)} registry := agent.NewRegistry() - registry.RegisterKind(proto.SupportedAgentKind{Kind: "handoff", Available: true, Capabilities: proto.AgentKindCapabilities{EnvironmentNone: true}}, + registry.RegisterKind(proto.SupportedAgentKind{Kind: "handoff", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { return nil, errors.New("legacy path forbidden") }) diff --git a/apps/parsar-daemon/internal/dispatch/executor_test.go b/apps/parsar-daemon/internal/dispatch/executor_test.go index f04a7f35d..8c80a4a10 100644 --- a/apps/parsar-daemon/internal/dispatch/executor_test.go +++ b/apps/parsar-daemon/internal/dispatch/executor_test.go @@ -6,6 +6,7 @@ import ( "context" "errors" "fmt" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "sync" "sync/atomic" "testing" @@ -75,7 +76,7 @@ func executorRouter(t *testing.T, owner *reusableExecutor, idle time.Duration) ( t.Helper() calls := &atomic.Int32{} reg := agent.NewRegistry() - reg.RegisterKind(proto.SupportedAgentKind{Kind: "reusable", Available: true, Capabilities: proto.AgentKindCapabilities{EnvironmentNone: true}}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + reg.RegisterKind(proto.SupportedAgentKind{Kind: "reusable", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { return nil, errors.New("ordinary factory is forbidden") }) reg.RegisterExecutor("reusable", func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { @@ -219,7 +220,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: proto.AgentKindCapabilities{EnvironmentNone: true}}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + registry.RegisterKind(proto.SupportedAgentKind{Kind: "reusable", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { return nil, errors.New("unexpected legacy factory") }) registry.RegisterExecutor("reusable", factory) diff --git a/apps/parsar-daemon/internal/dispatch/functions_native_test.go b/apps/parsar-daemon/internal/dispatch/functions_native_test.go index e9cea60c1..e428cee35 100644 --- a/apps/parsar-daemon/internal/dispatch/functions_native_test.go +++ b/apps/parsar-daemon/internal/dispatch/functions_native_test.go @@ -6,6 +6,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "net/http" "net/http/httptest" "os" @@ -109,7 +110,7 @@ func TestNativeFunctionBridge(t *testing.T) { })) defer model.Close() reg := agent.NewRegistry() - reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: proto.AgentKindCapabilities{FunctionTools: true, EnvironmentNone: true}}, harnessconfig.Configuration{}, codex.Factory) + reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{FunctionTools: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported})}, harnessconfig.Configuration{}, codex.Factory) sender := make(nativeFunctionSender, 256) router, err := dispatch.New(dispatch.Config{Registry: reg, Sender: sender}) if err != nil { diff --git a/apps/parsar-daemon/internal/dispatch/functions_test.go b/apps/parsar-daemon/internal/dispatch/functions_test.go index 1d6c59ccd..60761edb3 100644 --- a/apps/parsar-daemon/internal/dispatch/functions_test.go +++ b/apps/parsar-daemon/internal/dispatch/functions_test.go @@ -7,6 +7,7 @@ import ( "context" "encoding/base64" "encoding/json" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "image" "image/color" "image/png" @@ -37,7 +38,7 @@ func TestFunctionReceiptsScopeRetriesAndConflicts(t *testing.T) { reg := agent.NewRegistry() sender := &recSender{} sessions := map[string]*functionSession{} - reg.RegisterKind(proto.SupportedAgentKind{Kind: "function-test", Available: true, Capabilities: proto.AgentKindCapabilities{FunctionTools: true}}, harnessconfig.Configuration{}, func(ctx context.Context, p proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + reg.RegisterKind(proto.SupportedAgentKind{Kind: "function-test", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{FunctionTools: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(ctx context.Context, p proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { s := &functionSession{fakeSession: &fakeSession{out: out, ctx: ctx, closeOutOnCancel: true}} sessions[p.RunID] = s return s, nil @@ -137,7 +138,7 @@ func TestFunctionReceiptsScopeRetriesAndConflicts(t *testing.T) { func TestFunctionToolsRequireAdvertisedSupport(t *testing.T) { reg := agent.NewRegistry() called := false - reg.Register("unsupported", harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + reg.RegisterKind(proto.SupportedAgentKind{Kind: "unsupported", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { called = true return nil, nil }) @@ -165,7 +166,7 @@ func functionResultContent(text string) []proto.InputContent { func TestDiscoveryCannotReachAnEagerOnlyAdapter(t *testing.T) { reg := agent.NewRegistry() called := false - reg.RegisterKind(proto.SupportedAgentKind{Kind: "eager-only", Available: true, Capabilities: proto.AgentKindCapabilities{FunctionTools: true}}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + reg.RegisterKind(proto.SupportedAgentKind{Kind: "eager-only", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{FunctionTools: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { called = true return nil, nil }) diff --git a/apps/parsar-daemon/internal/dispatch/local_directory_test.go b/apps/parsar-daemon/internal/dispatch/local_directory_test.go index b7ed94508..67188ba50 100644 --- a/apps/parsar-daemon/internal/dispatch/local_directory_test.go +++ b/apps/parsar-daemon/internal/dispatch/local_directory_test.go @@ -5,6 +5,7 @@ import "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig" import ( "context" "errors" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "os" "path/filepath" "sync/atomic" @@ -28,7 +29,7 @@ func TestLocalDirectoryPreparationNeedsNoHarnessAndRejectsOtherOwners(t *testing } var harnessCalls atomic.Int32 reg := agent.NewRegistry() - reg.RegisterKind(proto.SupportedAgentKind{Kind: "native", Available: true, Capabilities: proto.AgentKindCapabilities{LocalEnvironment: true}}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + reg.RegisterKind(proto.SupportedAgentKind{Kind: "native", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { harnessCalls.Add(1) return nil, errors.New("must not start a model") }) @@ -110,7 +111,7 @@ func TestLocalDirectoryKeepsNotDirectorySeparateFromFailures(t *testing.T) { t.Fatal(err) } reg := agent.NewRegistry() - reg.RegisterKind(proto.SupportedAgentKind{Kind: "native", Available: true, Capabilities: proto.AgentKindCapabilities{LocalEnvironment: true}}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + reg.RegisterKind(proto.SupportedAgentKind{Kind: "native", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { return nil, errors.New("must not start a model") }) reg.RegisterPreparation("native", true, func(context.Context, proto.PromptRequestPayload) (agent.Prepared, error) { diff --git a/apps/parsar-daemon/internal/dispatch/mcp_http.go b/apps/parsar-daemon/internal/dispatch/mcp_http.go index 894d9e759..8f0495f53 100644 --- a/apps/parsar-daemon/internal/dispatch/mcp_http.go +++ b/apps/parsar-daemon/internal/dispatch/mcp_http.go @@ -16,16 +16,16 @@ func validateMCPHTTP(req proto.PromptRequestPayload, caps proto.AgentKindCapabil if err := server.ValidateConnectionOrigin(req); err != nil { return err } - if !caps.MCPHTTPTools { + if !caps.MCPHTTPTools.IsSupported() { return errors.New("engine does not support HTTP MCP") } - if server.Required && !caps.MCPHTTPRequired { + if server.Required && !caps.MCPHTTPRequired.IsSupported() { return errors.New("engine does not support required HTTP MCP initialization") } if server.BearerToken == nil { continue } - if !caps.MCPHTTPTools || !caps.MCPHTTPBearerAuth { + if !caps.MCPHTTPTools.IsSupported() || !caps.MCPHTTPBearerAuth.IsSupported() { return errors.New("engine does not support authenticated HTTP MCP") } endpoint, err := url.Parse(server.ServerURL) diff --git a/apps/parsar-daemon/internal/dispatch/mcp_http_test.go b/apps/parsar-daemon/internal/dispatch/mcp_http_test.go index f9021c275..db1538a6c 100644 --- a/apps/parsar-daemon/internal/dispatch/mcp_http_test.go +++ b/apps/parsar-daemon/internal/dispatch/mcp_http_test.go @@ -6,6 +6,7 @@ import ( "context" "encoding/json" "errors" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "strings" "testing" "time" @@ -22,24 +23,24 @@ func TestMCPHTTPBearerRejectsUnsupportedRequestsBeforeFactory(t *testing.T) { token := "synthetic-private-token" servers := []proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "tools", ServerURL: "https://tools.example/mcp", BearerToken: &token}} req := proto.PromptRequestPayload{AgentKind: "codex", DisableExecutionEnvironment: true, MCPHTTPServers: &servers} - caps := proto.AgentKindCapabilities{EnvironmentNone: true, MCPHTTPTools: true, MCPHTTPBearerAuth: true} + caps := prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported, MCPHTTPTools: proto.CapabilitySupported, MCPHTTPBearerAuth: proto.CapabilitySupported}) switch mode { case "claude", "claude old peer", "claude local": req.AgentKind = "claude_sdk" - caps.MCPHTTPBearerAuth = mode != "claude old peer" + caps.MCPHTTPBearerAuth = proto.CapabilityFromBool(mode != "claude old peer") if mode == "claude local" { req.DisableExecutionEnvironment = false } case "required", "required old peer", "optional old peer": servers[0].Required = mode != "optional old peer" servers[0].BearerToken = nil - caps.MCPHTTPRequired = mode == "required" + caps.MCPHTTPRequired = proto.CapabilityFromBool(mode == "required") case "no bearer capability", "credential-free", "product": - caps.MCPHTTPBearerAuth = false + caps.MCPHTTPBearerAuth = proto.CapabilityUnsupported case "no MCP capability": - caps.MCPHTTPTools = false + caps.MCPHTTPTools = proto.CapabilityUnsupported case "no none capability": - caps.EnvironmentNone = false + caps.EnvironmentNone = proto.CapabilityUnsupported case "local": req.DisableExecutionEnvironment = false case "other engine": @@ -104,7 +105,7 @@ func TestLocalMCPOriginAndCapabilityAdmission(t *testing.T) { req.Configuration.MCPHTTPServers = nil } entered := make(chan struct{}, 1) - h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "prepared", Available: true, Capabilities: proto.AgentKindCapabilities{LocalEnvironment: true, MCPHTTPTools: mode != "environment missing capability", MCPHTTPBearerAuth: true, MCPHTTPRequired: true}}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "prepared", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported, MCPHTTPTools: proto.CapabilityFromBool(mode != "environment missing capability"), MCPHTTPBearerAuth: proto.CapabilitySupported, MCPHTTPRequired: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { t.Error("ordinary factory called") return nil, errors.New("unexpected") }) diff --git a/apps/parsar-daemon/internal/dispatch/optional_interactions_test.go b/apps/parsar-daemon/internal/dispatch/optional_interactions_test.go index f53a80708..baedc7f25 100644 --- a/apps/parsar-daemon/internal/dispatch/optional_interactions_test.go +++ b/apps/parsar-daemon/internal/dispatch/optional_interactions_test.go @@ -4,6 +4,7 @@ import "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig" import ( "context" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "testing" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" @@ -21,7 +22,7 @@ func TestOptionalInteractionResponders(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) var output chan<- proto.Envelope - h.reg.Register("minimal", harnessconfig.Configuration{}, func(_ context.Context, _ proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "minimal", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, func(_ context.Context, _ proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { output = out s := &fakeSession{out: out, closeOutOnCancel: true} return lifecycleOnly{cancel: s.Cancel}, nil diff --git a/apps/parsar-daemon/internal/dispatch/preparation.go b/apps/parsar-daemon/internal/dispatch/preparation.go index dd218cd88..073d740d2 100644 --- a/apps/parsar-daemon/internal/dispatch/preparation.go +++ b/apps/parsar-daemon/internal/dispatch/preparation.go @@ -50,10 +50,10 @@ func (r *Router) handleExecutionPrepare(ctx context.Context, env proto.Envelope) } caps := r.availableCapabilities(req.AgentKind) prepare, err := r.registry.ResolvePreparation(req.AgentKind) - if err != nil || !caps.Preparation { + if err != nil || !caps.Preparation.IsSupported() { return r.rejectPreparation(env, "unsupported_preparation") } - if req.WorkspaceReadOnly && (!caps.WorkspaceReadPreparation || !proto.ValidWorkspaceReadPreparation(req)) { + if req.WorkspaceReadOnly && (!caps.WorkspaceReadPreparation.IsSupported() || !proto.ValidWorkspaceReadPreparation(req)) { return r.rejectPreparation(env, "unsupported_read_preparation") } if req, err = r.localWorkspace.Configure(req); err != nil { @@ -62,7 +62,7 @@ func (r *Router) handleExecutionPrepare(ctx context.Context, env proto.Envelope) if req.RunID != "" || len(req.Input) != 0 || req.ConversationID != "" || req.WorkspaceAuthoring || req.EnvironmentID() == "" || strings.TrimSpace(req.AgentStateKey) == "" || !req.StrictResume || !req.ReleaseOnCompletion { return r.rejectPreparation(env, "invalid_configuration") } - if validateExecutionEnvironment(req, caps) != nil || (len(req.FunctionTools) > 0 && !caps.FunctionTools) { + if validateExecutionEnvironment(req, caps) != nil || (len(req.FunctionTools) > 0 && !caps.FunctionTools.IsSupported()) { return r.rejectPreparation(env, "unsupported_configuration") } if req.LocalEnvironment != nil && req.WorkspaceReadOnly { diff --git a/apps/parsar-daemon/internal/dispatch/preparation_test.go b/apps/parsar-daemon/internal/dispatch/preparation_test.go index 7e0dcbacd..8f7260522 100644 --- a/apps/parsar-daemon/internal/dispatch/preparation_test.go +++ b/apps/parsar-daemon/internal/dispatch/preparation_test.go @@ -5,6 +5,7 @@ import "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig" import ( "context" "errors" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "os" "sync" "sync/atomic" @@ -118,7 +119,7 @@ func preparationRequest() proto.ExecutionPreparePayload { func preparationRouter(t *testing.T, sender dispatch.Sender, timeout time.Duration, factory agent.PreparationFactory) *dispatch.Router { t.Helper() reg := agent.NewRegistry() - reg.RegisterKind(proto.SupportedAgentKind{Kind: "prepared", Available: true, Capabilities: proto.AgentKindCapabilities{LocalEnvironment: true}}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + reg.RegisterKind(proto.SupportedAgentKind{Kind: "prepared", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { return nil, errors.New("ordinary Factory must not be used for preparation") }) reg.RegisterPreparation("prepared", true, factory) diff --git a/apps/parsar-daemon/internal/dispatch/receipt_order_test.go b/apps/parsar-daemon/internal/dispatch/receipt_order_test.go index d5cea0ee0..307f20990 100644 --- a/apps/parsar-daemon/internal/dispatch/receipt_order_test.go +++ b/apps/parsar-daemon/internal/dispatch/receipt_order_test.go @@ -5,6 +5,7 @@ import "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig" import ( "context" "errors" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "sync" "sync/atomic" "testing" @@ -51,7 +52,7 @@ func TestDurableCompletionWaitsForSteeringReceiptSend(t *testing.T) { registry := agent.NewRegistry() var session *fakeSession var calls atomic.Int32 - registry.Register("codex", harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + registry.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, func(ctx context.Context, req 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) @@ -124,7 +125,7 @@ func TestShutdownReleasesSteeringWorkerAndCompletionBarrier(t *testing.T) { registry := agent.NewRegistry() entered, exited := make(chan struct{}), make(chan struct{}) var session *fakeSession - registry.Register("codex", harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + registry.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, func(ctx context.Context, req 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/parsar-daemon/internal/dispatch/receipt_shutdown_test.go b/apps/parsar-daemon/internal/dispatch/receipt_shutdown_test.go index 28d4eefee..0ef93f662 100644 --- a/apps/parsar-daemon/internal/dispatch/receipt_shutdown_test.go +++ b/apps/parsar-daemon/internal/dispatch/receipt_shutdown_test.go @@ -8,6 +8,7 @@ import ( "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/dispatch" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "testing" "time" ) @@ -46,7 +47,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 - registry.Register("codex", harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + registry.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, func(ctx context.Context, req 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/parsar-daemon/internal/dispatch/router_test.go b/apps/parsar-daemon/internal/dispatch/router_test.go index 07de29e24..d80d457f8 100644 --- a/apps/parsar-daemon/internal/dispatch/router_test.go +++ b/apps/parsar-daemon/internal/dispatch/router_test.go @@ -5,6 +5,7 @@ import "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig" import ( "context" "errors" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "slices" "sync" "testing" @@ -153,7 +154,7 @@ func newHarnessWithIdleTimeout(t *testing.T, idleTimeout time.Duration) *harness gotReq: make(chan proto.PromptRequestPayload, 16), gotSess: make(chan *fakeSession, 16), } - h.reg.Register("claude_code", harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "claude_code", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { sess := &fakeSession{out: out, ctx: ctx} h.gotReq <- req h.gotSess <- sess diff --git a/apps/parsar-daemon/internal/dispatch/steering_lifetime_test.go b/apps/parsar-daemon/internal/dispatch/steering_lifetime_test.go index 038839250..27dfa0952 100644 --- a/apps/parsar-daemon/internal/dispatch/steering_lifetime_test.go +++ b/apps/parsar-daemon/internal/dispatch/steering_lifetime_test.go @@ -4,6 +4,7 @@ import "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig" import ( "context" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "sync/atomic" "testing" "time" @@ -27,7 +28,7 @@ func TestDurableSteeringWaitsBeyondTransportDeadline(t *testing.T) { var session *fakeSession var calls atomic.Int32 release := make(chan struct{}) - h.reg.Register("codex", harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, 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) @@ -81,7 +82,7 @@ func TestDurableSteeringTransportTimeoutAndShutdown(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) exited := make(chan struct{}) - h.reg.Register("codex", harnessconfig.Configuration{}, func(_ context.Context, _ proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, 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" { @@ -130,7 +131,7 @@ func TestDurableSteeringRequiresOptInAndAdapter(t *testing.T) { t.Run(map[bool]string{false: "old-adapter", true: "retained-run"}[supported], func(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) - h.reg.Register("codex", harnessconfig.Configuration{}, func(_ context.Context, _ proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, func(_ context.Context, _ proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { s := &steeringSession{fakeSession: &fakeSession{out: out, closeOutOnCancel: true}, steer: func(context.Context, proto.PromptSteerPayload) error { t.Error("unexpected legacy call"); return nil }} if !supported { return s, nil diff --git a/apps/parsar-daemon/internal/dispatch/steering_test.go b/apps/parsar-daemon/internal/dispatch/steering_test.go index d78872020..c064a9681 100644 --- a/apps/parsar-daemon/internal/dispatch/steering_test.go +++ b/apps/parsar-daemon/internal/dispatch/steering_test.go @@ -6,6 +6,7 @@ import ( "context" "errors" "fmt" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "testing" "time" @@ -32,7 +33,7 @@ func TestSteeringReceiptsAndRetries(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) calls, starts := 0, 0 - h.reg.Register("codex", harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { starts++ return &steeringSession{ fakeSession: &fakeSession{out: out, closeOutOnCancel: true}, @@ -93,7 +94,7 @@ func TestSteeringReadinessAndUnsupportedRuns(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) calls := 0 - h.reg.Register("codex", harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, 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 { @@ -171,7 +172,7 @@ func TestSteeringDoesNotBlockOtherRunCancellation(t *testing.T) { defer h.router.Shutdown(context.Background()) entered, release := make(chan struct{}), make(chan struct{}) defer close(release) - h.reg.Register("codex", harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, 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 { @@ -234,7 +235,7 @@ func TestSteeringCapacityPreservesExistingReceipts(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) calls := 0 - h.reg.Register("codex", harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, 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/parsar-daemon/testdata/onboarding/main.go b/apps/parsar-daemon/testdata/onboarding/main.go index 7aff33e35..05ac3fb3a 100644 --- a/apps/parsar-daemon/testdata/onboarding/main.go +++ b/apps/parsar-daemon/testdata/onboarding/main.go @@ -9,6 +9,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "io" "log/slog" "os" @@ -160,9 +161,9 @@ func (s *session) AwaitSettlement(ctx context.Context) (agent.TurnSettlement, er func run() error { registry := agent.NewRegistry() h := &harness{history: map[string]string{}} - registry.RegisterKind(proto.SupportedAgentKind{Kind: "fixture_harness", Available: true, Capabilities: proto.AgentKindCapabilities{ - Streaming: true, Steering: true, DurableTurns: true, DurableInputReceipts: true, ExecutionControls: true, ToolObservations: true, SubagentControl: true, EnvironmentNone: true, - }}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + registry.RegisterKind(proto.SupportedAgentKind{Kind: "fixture_harness", 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, + })}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { return nil, errors.New("fixture execution requires an Executor") }) registry.RegisterExecutor("fixture_harness", h.prepare) diff --git a/internal/agentdaemon/gateway/functions_test.go b/internal/agentdaemon/gateway/functions_test.go index c77c9daf1..c38022db7 100644 --- a/internal/agentdaemon/gateway/functions_test.go +++ b/internal/agentdaemon/gateway/functions_test.go @@ -2,12 +2,13 @@ package gateway import ( "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "testing" ) func TestFunctionCapabilitySurvivesHeartbeatMapping(t *testing.T) { for _, supported := range []bool{false, true} { - kinds := deviceKindsFromHeartbeat(proto.HeartbeatPayload{SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: proto.AgentKindCapabilities{FunctionTools: supported, FunctionResultImages: supported}}}}) + kinds := deviceKindsFromHeartbeat(proto.HeartbeatPayload{SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{FunctionTools: proto.CapabilityFromBool(supported), FunctionResultImages: proto.CapabilityFromBool(supported)})}}}) s := &Session{} s.setSupportedAgentKinds(kinds) info, found, known := s.AgentKindStatus("codex") diff --git a/internal/agentdaemon/gateway/mcp_test.go b/internal/agentdaemon/gateway/mcp_test.go index e5a4520c4..480716fbb 100644 --- a/internal/agentdaemon/gateway/mcp_test.go +++ b/internal/agentdaemon/gateway/mcp_test.go @@ -2,6 +2,7 @@ package gateway import ( "encoding/json" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "strings" "testing" @@ -11,7 +12,7 @@ import ( func TestMCPHTTPBearerCapabilitySurvivesHeartbeatMapping(t *testing.T) { for _, supported := range []bool{false, true} { heartbeat := proto.HeartbeatPayload{SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, - Capabilities: proto.AgentKindCapabilities{MCPHTTPTools: true, MCPHTTPBearerAuth: supported}}}} + 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) @@ -31,7 +32,7 @@ func TestMCPHTTPBearerCapabilitySurvivesHeartbeatMapping(t *testing.T) { func TestMCPRequiredCapabilityIsExplicit(t *testing.T) { for _, supported := range []bool{false, true} { - heartbeat := proto.HeartbeatPayload{SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: proto.AgentKindCapabilities{MCPHTTPTools: true, MCPHTTPBearerAuth: true, MCPHTTPRequired: supported}}}} + heartbeat := proto.HeartbeatPayload{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) diff --git a/internal/agentdaemon/gateway/preparation_test.go b/internal/agentdaemon/gateway/preparation_test.go index 496a0994b..c65dcff0f 100644 --- a/internal/agentdaemon/gateway/preparation_test.go +++ b/internal/agentdaemon/gateway/preparation_test.go @@ -2,6 +2,7 @@ package gateway import ( "errors" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "testing" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" @@ -77,7 +78,7 @@ func TestPreparationCloseAndOverflowDoNotInventRunEvents(t *testing.T) { } func TestPreparationCapabilitySurvivesHeartbeatMapping(t *testing.T) { - kinds := deviceKindsFromHeartbeat(proto.HeartbeatPayload{SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: proto.AgentKindCapabilities{Preparation: true, LocalEnvironment: true, WorkspaceReadPreparation: true, NativeSessionRecovery: true}}}}) + kinds := deviceKindsFromHeartbeat(proto.HeartbeatPayload{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/internal/agentdaemon/gateway/session.go b/internal/agentdaemon/gateway/session.go index 4e742bbe0..7cf019b88 100644 --- a/internal/agentdaemon/gateway/session.go +++ b/internal/agentdaemon/gateway/session.go @@ -510,37 +510,37 @@ func deviceKindsFromHeartbeat(p proto.HeartbeatPayload) []device.SupportedAgentK Available: info.Available, Version: info.Version, Capabilities: device.KindCapabilities{ - Streaming: info.Capabilities.Streaming, - Permissions: info.Capabilities.Permissions, - Usage: info.Capabilities.Usage, - Resume: info.Capabilities.Resume, - Steering: info.Capabilities.Steering, - DurableTurns: info.Capabilities.DurableTurns, - DurableInputReceipts: info.Capabilities.DurableInputReceipts, - NativeSessionRecovery: info.Capabilities.NativeSessionRecovery, - MessageItems: info.Capabilities.MessageItems, - - ToolObservations: info.Capabilities.ToolObservations, - EnvironmentNone: info.Capabilities.EnvironmentNone, - LocalEnvironment: info.Capabilities.LocalEnvironment, - Preparation: info.Capabilities.Preparation, - WorkspaceReadPreparation: info.Capabilities.WorkspaceReadPreparation, - WorkspaceOutputExport: info.Capabilities.WorkspaceOutputExport, - WebSearchControl: info.Capabilities.WebSearchControl, - ProgrammaticToolCallingDisable: info.Capabilities.ProgrammaticToolCallingDisable, - TextVerbosity: info.Capabilities.TextVerbosity, - StructuredOutput: info.Capabilities.StructuredOutput, - ToolSearch: info.Capabilities.ToolSearch, - MessageImages: info.Capabilities.MessageImages, - FunctionResultImages: info.Capabilities.FunctionResultImages, - ExecutionControls: info.Capabilities.ExecutionControls, - SubagentControl: info.Capabilities.SubagentControl, - SubagentObservations: info.Capabilities.SubagentObservations, - FunctionTools: info.Capabilities.FunctionTools, - MCPHTTPTools: info.Capabilities.MCPHTTPTools, - MCPHTTPRequired: info.Capabilities.MCPHTTPRequired, - MCPHTTPBearerAuth: info.Capabilities.MCPHTTPBearerAuth, - WorkspaceAuthoring: info.Capabilities.WorkspaceAuthoring, + Streaming: info.Capabilities.Streaming.IsSupported(), + Permissions: info.Capabilities.Permissions.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(), + WorkspaceAuthoring: info.Capabilities.WorkspaceAuthoring.IsSupported(), }, }) } diff --git a/internal/agentdaemon/gateway/session_test.go b/internal/agentdaemon/gateway/session_test.go index c70dde3d3..f3db4ae76 100644 --- a/internal/agentdaemon/gateway/session_test.go +++ b/internal/agentdaemon/gateway/session_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "io" "sync" "testing" @@ -383,25 +384,25 @@ func TestSession_HeartbeatPersistsSupportedAgentKinds(t *testing.T) { Kind: "opencode", Available: false, Version: "missing", - Capabilities: proto.AgentKindCapabilities{ - Streaming: true, - }, + Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{ + Streaming: proto.CapabilitySupported, + }), }, { Kind: "claude_code", Available: true, Version: "1.2.3", - Capabilities: proto.AgentKindCapabilities{ - Streaming: true, - Permissions: true, - Usage: true, - Resume: true, - }, + Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{ + Streaming: proto.CapabilitySupported, + Permissions: proto.CapabilitySupported, + Usage: proto.CapabilitySupported, + Resume: proto.CapabilitySupported, + }), }, { Kind: "codex", Available: true, - Capabilities: proto.AgentKindCapabilities{MCPHTTPTools: true, Steering: true, MessageItems: true, ToolObservations: true, EnvironmentNone: true, WebSearchControl: true, TextVerbosity: true, ExecutionControls: true, SubagentControl: 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}), }, }, }) diff --git a/services/agents-api/internal/store/claude_execution_test.go b/services/agents-api/internal/store/claude_execution_test.go index deca0c94c..2c7cf66d5 100644 --- a/services/agents-api/internal/store/claude_execution_test.go +++ b/services/agents-api/internal/store/claude_execution_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "strings" "testing" "time" @@ -30,7 +31,7 @@ func claudeSession(t *testing.T, h *dispatchHarness, configuration string, prebo func claudeHeartbeat(t *testing.T, h *dispatchHarness, ready bool) { t.Helper() - caps := proto.AgentKindCapabilities{Streaming: true, Steering: true, DurableTurns: true, DurableInputReceipts: ready, ExecutionControls: true, EnvironmentNone: true, SubagentControl: true, FunctionTools: true, ToolObservations: true, Preparation: true} + 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{SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "claude_sdk", Available: true, Capabilities: caps}}}) deadline := time.Now().Add(3 * time.Second) for { diff --git a/services/agents-api/internal/store/claude_mcp_test.go b/services/agents-api/internal/store/claude_mcp_test.go index c09fed51c..dd9e8ecaf 100644 --- a/services/agents-api/internal/store/claude_mcp_test.go +++ b/services/agents-api/internal/store/claude_mcp_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "strings" "testing" "time" @@ -28,7 +29,7 @@ func TestClaudeMCPWaitsForCapableRuntime(t *testing.T) { } claudeSession(t, h, configuration, prebound) // Base MCP support does not imply authentication or required initialization. - caps := proto.AgentKindCapabilities{Streaming: true, Steering: true, DurableTurns: true, DurableInputReceipts: true, ExecutionControls: true, EnvironmentNone: true, SubagentControl: true, ToolObservations: true, MCPHTTPTools: authenticated || required, Preparation: 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.CapabilityFromBool(authenticated || required), Preparation: proto.CapabilitySupported}) h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{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) @@ -104,7 +105,7 @@ func TestClaudeMCPUnsupportedSnapshotRejectedBeforeClaim(t *testing.T) { configuration = strings.Replace(configuration, `"tickets"`, `"functions"`, 1) } claudeSession(t, h, configuration, true) - caps := proto.AgentKindCapabilities{Streaming: true, Steering: true, DurableTurns: true, DurableInputReceipts: true, ExecutionControls: true, EnvironmentNone: true, SubagentControl: true, ToolObservations: true, MCPHTTPTools: true, MCPHTTPRequired: true, MCPHTTPBearerAuth: true, Preparation: 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}) if profile == "missing required capability" { caps.MCPHTTPRequired = false } diff --git a/services/agents-api/internal/store/dispatch_test.go b/services/agents-api/internal/store/dispatch_test.go index e68ff6213..0c95af129 100644 --- a/services/agents-api/internal/store/dispatch_test.go +++ b/services/agents-api/internal/store/dispatch_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "net/http" "net/http/httptest" "net/url" @@ -94,7 +95,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{SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: proto.AgentKindCapabilities{Streaming: true, Steering: true, Resume: true, DurableTurns: true, DurableInputReceipts: true, WebSearchControl: true, TextVerbosity: true, ExecutionControls: true, SubagentControl: true, SubagentObservations: true, ToolObservations: true, NativeSessionRecovery: true, Preparation: true}}}}) + h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{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})}}}) deadline := time.Now().Add(3 * time.Second) for { peer, e := h.registry.LookupDevice(h.device.ID) @@ -378,7 +379,7 @@ func TestExecutionRejectsLegacyDaemonBeforeClaim(t *testing.T) { for _, missing := range []string{"execution_controls", "durable_input_receipts", "durable_turns", "web_search_control", "text_verbosity", "subagent_control", "tool_observations"} { t.Run(missing, func(t *testing.T) { h := newDispatchHarness(t) - h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: proto.AgentKindCapabilities{Streaming: true, Steering: true, Resume: true, DurableTurns: missing != "durable_turns", DurableInputReceipts: missing != "durable_input_receipts", WebSearchControl: missing != "web_search_control", TextVerbosity: missing != "text_verbosity", ExecutionControls: missing != "execution_controls", SubagentControl: missing != "subagent_control", ToolObservations: missing != "tool_observations"}}}}) + h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{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"), 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")})}}}) deadline := time.Now().Add(3 * time.Second) for { peer, _ := h.registry.LookupDevice(h.device.ID) diff --git a/services/agents-api/internal/store/environment_directory_test.go b/services/agents-api/internal/store/environment_directory_test.go index 903084d26..15464eb89 100644 --- a/services/agents-api/internal/store/environment_directory_test.go +++ b/services/agents-api/internal/store/environment_directory_test.go @@ -3,6 +3,7 @@ package store_test import ( "context" "errors" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "testing" "time" @@ -24,7 +25,7 @@ func directoryWorker(t *testing.T, execute ...bool) (*dispatchHarness, *executio if err != nil { t.Fatal(err) } - h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: proto.AgentKindCapabilities{LocalEnvironment: true, Preparation: true, WorkspaceReadPreparation: true}}}}) + h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported, Preparation: proto.CapabilitySupported, WorkspaceReadPreparation: proto.CapabilitySupported})}}}) peer, err := h.registry.LookupDevice(h.device.ID) if err != nil { t.Fatal(err) diff --git a/services/agents-api/internal/store/environment_expiry_dispatch_test.go b/services/agents-api/internal/store/environment_expiry_dispatch_test.go index fc2cfc462..a3200ac29 100644 --- a/services/agents-api/internal/store/environment_expiry_dispatch_test.go +++ b/services/agents-api/internal/store/environment_expiry_dispatch_test.go @@ -2,6 +2,7 @@ package store_test import ( "encoding/json" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "testing" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" @@ -9,9 +10,9 @@ import ( ) func enableEnvironmentExpiryDispatch(h *dispatchHarness) { - h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: proto.AgentKindCapabilities{ - Streaming: true, Steering: true, DurableTurns: true, DurableInputReceipts: true, EnvironmentNone: true, WebSearchControl: true, TextVerbosity: true, ExecutionControls: true, SubagentControl: true, ToolObservations: true, - Preparation: true}}}}) + h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{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})}}}) } func TestWorkerEnvironmentExpiryAtFullExecutionCapacity(t *testing.T) { diff --git a/services/agents-api/internal/store/environment_worker_helpers_test.go b/services/agents-api/internal/store/environment_worker_helpers_test.go index a5163e502..629322f11 100644 --- a/services/agents-api/internal/store/environment_worker_helpers_test.go +++ b/services/agents-api/internal/store/environment_worker_helpers_test.go @@ -3,6 +3,7 @@ package store_test import ( "context" "encoding/json" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "testing" "time" @@ -26,7 +27,7 @@ func enableWorkerEnvironment(t *testing.T, h *dispatchHarness) { } func workerEnvironmentCapabilities() proto.AgentKindCapabilities { - return proto.AgentKindCapabilities{Streaming: true, Steering: true, DurableTurns: true, DurableInputReceipts: true, EnvironmentNone: true, WebSearchControl: true, TextVerbosity: true, ExecutionControls: true, SubagentControl: true, ToolObservations: true, Preparation: true, LocalEnvironment: true, WorkspaceReadPreparation: true, WorkspaceOutputExport: true} + 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}) } func workerEnvironmentReservation(t *testing.T, h *dispatchHarness) store.EnvironmentInputReservation { diff --git a/services/agents-api/internal/store/execution_messages_test.go b/services/agents-api/internal/store/execution_messages_test.go index cbda2d87e..0e2013ed4 100644 --- a/services/agents-api/internal/store/execution_messages_test.go +++ b/services/agents-api/internal/store/execution_messages_test.go @@ -3,6 +3,7 @@ package store_test import ( "context" "encoding/json" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "testing" "time" @@ -23,7 +24,7 @@ func TestExecutionNegotiatesAndPersistsMessageObservations(t *testing.T) { } h.write(first.TurnID, proto.TypeDone, proto.DonePayload{}) h.finished(result, store.TurnCompleted) - h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: proto.AgentKindCapabilities{Streaming: true, Steering: true, Resume: true, DurableTurns: true, DurableInputReceipts: true, WebSearchControl: true, TextVerbosity: true, ExecutionControls: true, SubagentControl: true, ToolObservations: true, MessageItems: true, NativeSessionRecovery: true, Preparation: true}}}}) + h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{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})}}}) deadline := time.Now().Add(3 * time.Second) for { peer, err := h.registry.LookupDevice(h.device.ID) diff --git a/services/agents-api/internal/store/function_execution_test.go b/services/agents-api/internal/store/function_execution_test.go index 0e138b18c..37cce4ec8 100644 --- a/services/agents-api/internal/store/function_execution_test.go +++ b/services/agents-api/internal/store/function_execution_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "strings" "testing" "time" @@ -26,7 +27,7 @@ func newFunctionHarness(t *testing.T) *dispatchHarness { if err := h.s.BindSessionDevice(t.Context(), h.tenant, h.session.ID, h.device.ID); err != nil { t.Fatal(err) } - h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: proto.AgentKindCapabilities{Streaming: true, Steering: true, Resume: true, DurableTurns: true, DurableInputReceipts: true, WebSearchControl: true, TextVerbosity: true, ExecutionControls: true, SubagentControl: true, ToolObservations: true, EnvironmentNone: true, FunctionTools: true, FunctionResultImages: true, Preparation: true}}}}) + h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{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})}}}) deadline := time.Now().Add(time.Second) for { peer, _ := h.registry.LookupDevice(h.device.ID) diff --git a/services/agents-api/internal/store/function_worker_test.go b/services/agents-api/internal/store/function_worker_test.go index f3965da77..f7ca8e54d 100644 --- a/services/agents-api/internal/store/function_worker_test.go +++ b/services/agents-api/internal/store/function_worker_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "strings" "testing" "time" @@ -44,7 +45,7 @@ func TestWorkerWaitsForToolCapabilities(t *testing.T) { t.Fatal(err) } } - caps := proto.AgentKindCapabilities{Preparation: missing != "preparation", Streaming: true, Steering: true, DurableTurns: true, DurableInputReceipts: missing != "durable_input_receipts", EnvironmentNone: true, WebSearchControl: true, TextVerbosity: true, ExecutionControls: missing != "execution_controls", SubagentControl: true, ToolObservations: missing != "tool_observations", MCPHTTPTools: missing != "mcp_http_tools", MCPHTTPRequired: missing != "mcp_http_required", MCPHTTPBearerAuth: missing != "mcp_http_bearer_auth", FunctionTools: missing != "function_tools" && !isMCP} + 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)}) heartbeat := func() { h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: caps}}}) } diff --git a/services/agents-api/internal/store/local_environment_worker_test.go b/services/agents-api/internal/store/local_environment_worker_test.go index b5dfc3381..786e33c16 100644 --- a/services/agents-api/internal/store/local_environment_worker_test.go +++ b/services/agents-api/internal/store/local_environment_worker_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "testing" "time" @@ -24,7 +25,7 @@ func localWorker(t *testing.T, scoped, execute bool) (*dispatchHarness, *executi if err != nil { t.Fatal(err) } - caps := proto.AgentKindCapabilities{LocalEnvironment: true, Preparation: true, WorkspaceReadPreparation: true} + caps := prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported, Preparation: proto.CapabilitySupported, WorkspaceReadPreparation: proto.CapabilitySupported}) if execute { caps.WorkspaceOutputExport = true caps.Streaming, caps.Steering, caps.DurableTurns, caps.DurableInputReceipts = true, true, true, true diff --git a/services/agents-api/internal/store/public_execution_test.go b/services/agents-api/internal/store/public_execution_test.go index 6d9b2c28b..9355a447b 100644 --- a/services/agents-api/internal/store/public_execution_test.go +++ b/services/agents-api/internal/store/public_execution_test.go @@ -3,6 +3,7 @@ package store_test import ( "context" "encoding/json" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "testing" "time" @@ -23,7 +24,7 @@ func publicSession(t *testing.T, h *dispatchHarness, key string) store.Session { func TestExecutionWorkerAdmissionBindingAndRecovery(t *testing.T) { h := newDispatchHarness(t) - h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: proto.AgentKindCapabilities{Streaming: true, Steering: true, DurableTurns: true, DurableInputReceipts: true, WebSearchControl: true, TextVerbosity: true, ExecutionControls: true, SubagentControl: true, ToolObservations: true, EnvironmentNone: true, Preparation: true}}}}) + h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{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.session = publicSession(t, h, "public") ctx, cancel := context.WithCancel(context.Background()) defer cancel() diff --git a/services/agents-api/internal/store/runtime_compute_lifecycle_test.go b/services/agents-api/internal/store/runtime_compute_lifecycle_test.go index e2665e687..16e8b9e7f 100644 --- a/services/agents-api/internal/store/runtime_compute_lifecycle_test.go +++ b/services/agents-api/internal/store/runtime_compute_lifecycle_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "net/http" "net/http/httptest" "strings" @@ -183,7 +184,7 @@ func (p *fakeCheckpointProvider) connect(ctx context.Context, b sandbox.Bootstra if err != nil { return err } - heartbeat, _ := proto.NewEnvelope(proto.TypeHeartbeat, "", proto.HeartbeatPayload{SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true}}}) + heartbeat, _ := proto.NewEnvelope(proto.TypeHeartbeat, "", proto.HeartbeatPayload{SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}}}) if err := conn.WriteJSON(heartbeat); err != nil { conn.Close() return err diff --git a/services/agents-api/internal/store/runtime_initialization_peer_test.go b/services/agents-api/internal/store/runtime_initialization_peer_test.go index 4f63e44ca..2b0252d1a 100644 --- a/services/agents-api/internal/store/runtime_initialization_peer_test.go +++ b/services/agents-api/internal/store/runtime_initialization_peer_test.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "encoding/hex" "errors" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "net/http" "sync/atomic" "testing" @@ -43,7 +44,7 @@ func (p *initializationPeer) connect(b sandbox.Bootstrap) error { return err } p.t.Cleanup(func() { _ = c.Close() }) - heartbeat, _ := proto.NewEnvelope(proto.TypeHeartbeat, "", proto.HeartbeatPayload{SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: !p.unavailable}}}) + heartbeat, _ := proto.NewEnvelope(proto.TypeHeartbeat, "", proto.HeartbeatPayload{SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: !p.unavailable, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}}}) if err := c.WriteJSON(heartbeat); err != nil { return err } diff --git a/services/agents-api/internal/store/structured_output_dispatch_test.go b/services/agents-api/internal/store/structured_output_dispatch_test.go index 78d26936e..5168724d2 100644 --- a/services/agents-api/internal/store/structured_output_dispatch_test.go +++ b/services/agents-api/internal/store/structured_output_dispatch_test.go @@ -3,6 +3,7 @@ package store_test import ( "context" "encoding/json" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "testing" "time" @@ -28,7 +29,7 @@ func TestStructuredOutputDispatchRechecksOperationQualification(t *testing.T) { if err = h.s.BindSessionDevice(t.Context(), h.tenant, h.session.ID, h.device.ID); err != nil { t.Fatal(err) } - caps := proto.AgentKindCapabilities{Streaming: true, Steering: true, DurableTurns: true, DurableInputReceipts: true, ExecutionControls: true, EnvironmentNone: true, SubagentControl: true, ToolObservations: true, StructuredOutput: true, MessageItems: true, Preparation: 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, StructuredOutput: proto.CapabilitySupported, MessageItems: proto.CapabilitySupported, Preparation: proto.CapabilitySupported}) h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{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); ; { diff --git a/services/agents-api/internal/store/worker_lease_loss_test.go b/services/agents-api/internal/store/worker_lease_loss_test.go index f1dbf42cc..8d530c863 100644 --- a/services/agents-api/internal/store/worker_lease_loss_test.go +++ b/services/agents-api/internal/store/worker_lease_loss_test.go @@ -3,6 +3,7 @@ package store_test import ( "context" "encoding/json" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "testing" "time" @@ -14,7 +15,7 @@ import ( func TestWorkerLeaseLossLeavesUncertainWorkForSuccessor(t *testing.T) { h := newDispatchHarness(t) _, pool := store.NewTestStore(t) - h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: proto.AgentKindCapabilities{Streaming: true, Steering: true, DurableTurns: true, DurableInputReceipts: true, WebSearchControl: true, TextVerbosity: true, ExecutionControls: true, SubagentControl: true, ToolObservations: true, EnvironmentNone: true, Preparation: true}}}}) + h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{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.session = publicSession(t, h, "active") queued := publicSession(t, h, "queued") worker, err := execution.StartWorker(t.Context(), h.d) From 95c5f91bdf8e9d80f749522fee8adec44965409f Mon Sep 17 00:00:00 2001 From: saladday <1203511142@qq.com> Date: Wed, 30 Sep 2026 13:36:06 +0800 Subject: [PATCH 06/12] Freeze admitted capabilities and reject undeclared optional operations --- .../internal/dispatch/capabilities.go | 6 +- .../internal/dispatch/executor.go | 10 ++- .../internal/dispatch/functions.go | 9 ++ .../dispatch/interaction_decisions.go | 16 +++- .../internal/dispatch/preparation.go | 8 +- .../internal/dispatch/preparation_start.go | 2 +- .../parsar-daemon/internal/dispatch/prompt.go | 11 ++- .../internal/dispatch/receipt_order_test.go | 4 +- .../dispatch/receipt_shutdown_test.go | 2 +- .../parsar-daemon/internal/dispatch/router.go | 3 +- .../internal/dispatch/router_test.go | 2 +- .../internal/dispatch/steering.go | 7 ++ .../dispatch/steering_lifetime_test.go | 6 +- .../internal/dispatch/steering_test.go | 8 +- .../retired_provider_configuration_test.go | 7 +- internal/agentdaemon/gateway/session.go | 4 +- .../agentdaemon/proto/capabilities_test.go | 85 +++++++++++++++++++ 17 files changed, 163 insertions(+), 27 deletions(-) create mode 100644 internal/agentdaemon/proto/capabilities_test.go diff --git a/apps/parsar-daemon/internal/dispatch/capabilities.go b/apps/parsar-daemon/internal/dispatch/capabilities.go index c06c6d00e..f99185ef8 100644 --- a/apps/parsar-daemon/internal/dispatch/capabilities.go +++ b/apps/parsar-daemon/internal/dispatch/capabilities.go @@ -2,11 +2,11 @@ package dispatch import "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" -func (r *Router) availableCapabilities(kind string) proto.AgentKindCapabilities { +func (r *Router) availableCapabilities(kind string) (proto.AgentKindCapabilities, bool) { for _, info := range r.registry.SupportedAgentKinds() { if info.Kind == kind && info.Available { - return info.Capabilities + return info.Capabilities, true } } - return proto.AgentKindCapabilities{} + return proto.AgentKindCapabilities{}, false } diff --git a/apps/parsar-daemon/internal/dispatch/executor.go b/apps/parsar-daemon/internal/dispatch/executor.go index 30cc47a8b..9846eae02 100644 --- a/apps/parsar-daemon/internal/dispatch/executor.go +++ b/apps/parsar-daemon/internal/dispatch/executor.go @@ -16,6 +16,7 @@ import ( const executorIdleCapacity = 16 type executorState struct { + capabilities proto.AgentKindCapabilities id, sessionID, environmentID, stateKey string fingerprint [32]byte native agent.Executor @@ -47,7 +48,10 @@ func (r *Router) handleExecutorPrepare(ctx context.Context, env proto.Envelope, if strings.TrimSpace(input.SessionID) == "" || req.RunID != "" || len(req.Input) != 0 || req.ConversationID != "" || req.WorkspaceAuthoring || req.AgentStateKey != "agents-api-"+input.SessionID || !req.StrictResume { return r.rejectPreparation(env, "invalid_configuration") } - caps := r.availableCapabilities(req.AgentKind) + caps, available := r.availableCapabilities(req.AgentKind) + if !available { + return r.rejectPreparation(env, "resource_unavailable") + } factory, err := r.registry.ResolveExecutor(req.AgentKind) if err != nil || !caps.Preparation.IsSupported() { return r.rejectPreparation(env, "unsupported_preparation") @@ -137,12 +141,12 @@ func (r *Router) handleExecutorPrepare(ctx context.Context, env proto.Envelope, return r.rejectPreparation(env, "executor_capacity") } ownerCtx, cancel := context.WithCancel(context.WithoutCancel(ctx)) - owner = &executorState{id: uuid.NewString(), sessionID: input.SessionID, environmentID: req.EnvironmentID(), stateKey: req.AgentStateKey, fingerprint: fingerprint, ctx: ownerCtx, cancel: cancel, preparing: true, nativeID: req.AgentSessionID} + owner = &executorState{capabilities: caps, id: uuid.NewString(), sessionID: input.SessionID, environmentID: req.EnvironmentID(), stateKey: req.AgentStateKey, fingerprint: fingerprint, ctx: ownerCtx, cancel: cancel, preparing: true, nativeID: req.AgentSessionID} r.executors[input.SessionID] = owner r.log.Info("executor owner_created", "executor_id", owner.id, "session_id", owner.sessionID) } operation, cancel := context.WithCancel(context.WithoutCancel(ctx)) - p := &preparationState{requestID: env.ID, trace: env.Trace, fingerprint: requestFingerprint, ctx: operation, cancel: cancel, stateKey: req.AgentStateKey, environmentID: req.EnvironmentID(), executor: owner, owns: true, busy: !reused, deadline: time.Now().Add(r.preparationTimeout)} + p := &preparationState{capabilities: owner.capabilities, requestID: env.ID, trace: env.Trace, fingerprint: requestFingerprint, ctx: operation, cancel: cancel, stateKey: req.AgentStateKey, environmentID: req.EnvironmentID(), executor: owner, owns: true, busy: !reused, deadline: time.Now().Add(r.preparationTimeout)} state := "preparing" if reused { state = "ready" diff --git a/apps/parsar-daemon/internal/dispatch/functions.go b/apps/parsar-daemon/internal/dispatch/functions.go index 9e74393d7..4b721f7b6 100644 --- a/apps/parsar-daemon/internal/dispatch/functions.go +++ b/apps/parsar-daemon/internal/dispatch/functions.go @@ -50,11 +50,20 @@ func (r *Router) handleFunctionResult(ctx context.Context, env proto.Envelope) e ctx, stop = r.shutdownContext(ctx) defer stop() } + if state != nil && !state.capabilities.FunctionTools.IsSupported() { + return r.sendInteractionDecisionAck(ctx, env.ID, result.DeliveryID, false, "unsupported", "The runtime declaration does not support function results.") + } + if ready && submitter == nil { + return r.sendInteractionDecisionAck(ctx, env.ID, result.DeliveryID, false, "contract_violation", "Declared function capability has no implementation.") + } if submitter == nil { return r.sendInteractionDecisionAck(ctx, env.ID, result.DeliveryID, false, "not_pending", "function call is no longer pending") } if err := submitter.SubmitFunctionResult(ctx, result); err != nil { code := "runtime_error" + if errors.Is(err, agent.ErrUnsupportedOperation) { + code = "contract_violation" + } if errors.Is(err, agent.ErrUnknownFunctionCall) { code = "not_pending" } diff --git a/apps/parsar-daemon/internal/dispatch/interaction_decisions.go b/apps/parsar-daemon/internal/dispatch/interaction_decisions.go index 711593393..7bef0a732 100644 --- a/apps/parsar-daemon/internal/dispatch/interaction_decisions.go +++ b/apps/parsar-daemon/internal/dispatch/interaction_decisions.go @@ -58,12 +58,18 @@ func (r *Router) handlePermissionDecision(ctx context.Context, env proto.Envelop ctx, stop := r.shutdownContext(ctx) defer stop() + if !state.capabilities.Permissions.IsSupported() { + return r.sendInteractionDecisionAck(ctx, env.ID, payload.DeliveryID, false, "unsupported", "The runtime declaration does not support interaction decisions.") + } responder, supported := session.(agent.PermissionResponder) if !supported { r.dropPermission(state, env.ID) - return r.sendInteractionDecisionAck(ctx, env.ID, payload.DeliveryID, false, "unsupported", "runtime does not support permission responses") + return r.sendInteractionDecisionAck(ctx, env.ID, payload.DeliveryID, false, "contract_violation", "Declared runtime capability does not implement permission responses") } if err := responder.SubmitPermission(ctx, env.ID, payload); err != nil { + if errors.Is(err, agent.ErrUnsupportedOperation) { + return r.sendInteractionDecisionAck(ctx, env.ID, payload.DeliveryID, false, "contract_violation", "Declared permission capability has no implementation.") + } if errors.Is(err, agent.ErrUnknownPermission) { r.log.InfoContext(ctx, "agent reports unknown perm (race with cancel)", "perm_id", env.ID, "run_id", runID) r.dropPermission(state, env.ID) @@ -139,13 +145,19 @@ func (r *Router) handlePromptForUserChoiceDecision(ctx context.Context, env prot ctx, stop := r.shutdownContext(ctx) defer stop() + if !state.capabilities.Permissions.IsSupported() { + return r.sendInteractionDecisionAck(ctx, env.ID, payload.DeliveryID, false, "unsupported", "The runtime declaration does not support interaction decisions.") + } responder, supported := session.(agent.UserChoiceResponder) if !supported { r.dropAsk(state, env.ID) - return r.sendInteractionDecisionAck(ctx, env.ID, payload.DeliveryID, false, "unsupported", "runtime does not support user-choice responses") + return r.sendInteractionDecisionAck(ctx, env.ID, payload.DeliveryID, false, "contract_violation", "Declared runtime capability does not implement user-choice responses") } err = responder.SubmitPromptForUserChoice(ctx, env.ID, payload) if err != nil { + if errors.Is(err, agent.ErrUnsupportedOperation) { + return r.sendInteractionDecisionAck(ctx, env.ID, payload.DeliveryID, false, "contract_violation", "Declared user-choice capability has no implementation.") + } if errors.Is(err, agent.ErrUnknownAsk) { r.log.InfoContext(ctx, "agent reports unknown ask (race with cancel)", "ask_id", env.ID, "run_id", runID) r.dropAsk(state, env.ID) diff --git a/apps/parsar-daemon/internal/dispatch/preparation.go b/apps/parsar-daemon/internal/dispatch/preparation.go index 073d740d2..51c959c84 100644 --- a/apps/parsar-daemon/internal/dispatch/preparation.go +++ b/apps/parsar-daemon/internal/dispatch/preparation.go @@ -19,6 +19,7 @@ const preparationRecords = 64 // All mutable fields are protected by Router.mu. owns includes resources whose // cancellation is underway; a slow close cannot bypass the capacity bound. type preparationState struct { + capabilities proto.AgentKindCapabilities executor *executorState requestID string trace string @@ -48,7 +49,10 @@ func (r *Router) handleExecutionPrepare(ctx context.Context, env proto.Envelope) if !req.WorkspaceReadOnly { return r.handleExecutorPrepare(ctx, env, input) } - caps := r.availableCapabilities(req.AgentKind) + caps, available := r.availableCapabilities(req.AgentKind) + if !available { + return r.rejectPreparation(env, "resource_unavailable") + } prepare, err := r.registry.ResolvePreparation(req.AgentKind) if err != nil || !caps.Preparation.IsSupported() { return r.rejectPreparation(env, "unsupported_preparation") @@ -104,7 +108,7 @@ func (r *Router) handleExecutionPrepare(ctx context.Context, env proto.Envelope) return r.rejectPreparation(env, "preparation_capacity") } owner, cancel := context.WithCancel(context.WithoutCancel(ctx)) - p := &preparationState{requestID: env.ID, trace: env.Trace, fingerprint: fingerprint, ctx: owner, cancel: cancel, stateKey: req.AgentStateKey, environmentID: req.EnvironmentID(), workspaceReadOnly: req.WorkspaceReadOnly, busy: true, owns: true, deadline: time.Now().Add(r.preparationTimeout)} + p := &preparationState{capabilities: caps, requestID: env.ID, trace: env.Trace, fingerprint: fingerprint, ctx: owner, cancel: cancel, stateKey: req.AgentStateKey, environmentID: req.EnvironmentID(), workspaceReadOnly: req.WorkspaceReadOnly, busy: true, owns: true, deadline: time.Now().Add(r.preparationTimeout)} p.status = proto.PreparationStatusPayload{Handle: uuid.NewString(), Revision: 1, State: "preparing", ExpiresAt: p.deadline.UnixMilli()} r.preparations[p.status.Handle], r.preparationRequests[p.requestID] = p, p p.timer = time.AfterFunc(r.preparationTimeout, func() { r.releasePreparation(p, "expired", "", true, true) }) diff --git a/apps/parsar-daemon/internal/dispatch/preparation_start.go b/apps/parsar-daemon/internal/dispatch/preparation_start.go index 74d267cac..99ff3759e 100644 --- a/apps/parsar-daemon/internal/dispatch/preparation_start.go +++ b/apps/parsar-daemon/internal/dispatch/preparation_start.go @@ -70,7 +70,7 @@ func (r *Router) handleExecutionStart(_ context.Context, env proto.Envelope) err } p.status.State, p.status.RunID, p.status.Revision = "starting", input.RunID, p.status.Revision+1 p.startFingerprint, p.busy = fingerprint, true - state := &sessionState{runID: input.RunID, stateKey: p.stateKey, environmentID: p.environmentID, out: make(chan proto.Envelope, 64), ctx: p.ctx, ctxCancel: p.cancel, pendingIDs: make(map[string]struct{}), pendingAsks: make(map[string]struct{}), traceparent: env.Trace} + state := &sessionState{capabilities: p.capabilities, runID: input.RunID, stateKey: p.stateKey, environmentID: p.environmentID, out: make(chan proto.Envelope, 64), ctx: p.ctx, ctxCancel: p.cancel, pendingIDs: make(map[string]struct{}), pendingAsks: make(map[string]struct{}), traceparent: env.Trace} state.preparedHandoff = newPreparedHandoff(p, owner.native) p.handoff, owner.run = state.preparedHandoff, state r.sessions[input.RunID] = state diff --git a/apps/parsar-daemon/internal/dispatch/prompt.go b/apps/parsar-daemon/internal/dispatch/prompt.go index e0b4c07d4..b0ced933a 100644 --- a/apps/parsar-daemon/internal/dispatch/prompt.go +++ b/apps/parsar-daemon/internal/dispatch/prompt.go @@ -41,12 +41,18 @@ func (r *Router) handlePromptRequest(callerCtx context.Context, env proto.Envelo r.emitTerminalError(callerCtx, runID, err.Error()) return err } - if len(req.FunctionTools) > 0 && !r.availableCapabilities(req.AgentKind).FunctionTools { + caps, available := r.availableCapabilities(req.AgentKind) + if !available { + err := errors.New("engine is unavailable on this runtime") + r.emitTerminalError(callerCtx, runID, err.Error()) + return err + } + if len(req.FunctionTools) > 0 && !caps.FunctionTools.IsSupported() { err := errors.New("engine does not support function tools") r.emitTerminalError(callerCtx, runID, err.Error()) return err } - if err := validateExecutionEnvironment(req, r.availableCapabilities(req.AgentKind)); err != nil { + if err := validateExecutionEnvironment(req, caps); err != nil { r.emitTerminalError(callerCtx, runID, err.Error()) return err } @@ -97,6 +103,7 @@ func (r *Router) handlePromptRequest(callerCtx context.Context, env proto.Envelo out := make(chan proto.Envelope, 64) state := &sessionState{ runID: runID, + capabilities: caps, environmentID: req.EnvironmentID(), stateKey: stateKey, out: out, diff --git a/apps/parsar-daemon/internal/dispatch/receipt_order_test.go b/apps/parsar-daemon/internal/dispatch/receipt_order_test.go index 307f20990..c20e5e02d 100644 --- a/apps/parsar-daemon/internal/dispatch/receipt_order_test.go +++ b/apps/parsar-daemon/internal/dispatch/receipt_order_test.go @@ -52,7 +52,7 @@ func TestDurableCompletionWaitsForSteeringReceiptSend(t *testing.T) { registry := agent.NewRegistry() var session *fakeSession var calls atomic.Int32 - registry.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + registry.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(ctx context.Context, req 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 TestShutdownReleasesSteeringWorkerAndCompletionBarrier(t *testing.T) { registry := agent.NewRegistry() entered, exited := make(chan struct{}), make(chan struct{}) var session *fakeSession - registry.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + registry.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(ctx context.Context, req 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/parsar-daemon/internal/dispatch/receipt_shutdown_test.go b/apps/parsar-daemon/internal/dispatch/receipt_shutdown_test.go index 0ef93f662..5f66820e4 100644 --- a/apps/parsar-daemon/internal/dispatch/receipt_shutdown_test.go +++ b/apps/parsar-daemon/internal/dispatch/receipt_shutdown_test.go @@ -47,7 +47,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 - registry.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + registry.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(ctx context.Context, req 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/parsar-daemon/internal/dispatch/router.go b/apps/parsar-daemon/internal/dispatch/router.go index 0a8a4ca9d..a3a58a4ef 100644 --- a/apps/parsar-daemon/internal/dispatch/router.go +++ b/apps/parsar-daemon/internal/dispatch/router.go @@ -51,7 +51,7 @@ type Router struct { preparations map[string]*preparationState preparationRequests map[string]*preparationState preparationTimeout time.Duration - runtimePreparation *runtimePreparationTransfer + runtimePreparation *runtimePreparationTransfer workspaceWrite *workspaceUpload workspaceExport *workspaceExport workspaceReads map[string]struct{} @@ -71,6 +71,7 @@ type appliedInteractionDecision struct { // outbound frame stamps env.Trace with the same value, completing // frontend → server → daemon → agent → server attribution. type sessionState struct { + capabilities proto.AgentKindCapabilities runID string environmentID string stateKey string diff --git a/apps/parsar-daemon/internal/dispatch/router_test.go b/apps/parsar-daemon/internal/dispatch/router_test.go index d80d457f8..1e26b7fda 100644 --- a/apps/parsar-daemon/internal/dispatch/router_test.go +++ b/apps/parsar-daemon/internal/dispatch/router_test.go @@ -154,7 +154,7 @@ func newHarnessWithIdleTimeout(t *testing.T, idleTimeout time.Duration) *harness gotReq: make(chan proto.PromptRequestPayload, 16), gotSess: make(chan *fakeSession, 16), } - h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "claude_code", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "claude_code", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Permissions: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { sess := &fakeSession{out: out, ctx: ctx} h.gotReq <- req h.gotSess <- sess diff --git a/apps/parsar-daemon/internal/dispatch/steering.go b/apps/parsar-daemon/internal/dispatch/steering.go index 5baa092be..f3df33dd3 100644 --- a/apps/parsar-daemon/internal/dispatch/steering.go +++ b/apps/parsar-daemon/internal/dispatch/steering.go @@ -91,6 +91,11 @@ 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 { @@ -145,6 +150,8 @@ func (r *Router) queueSteering(ctx context.Context, env proto.Envelope, input pr func steeringResult(inputID string, err error) proto.PromptSteerAckPayload { ack := proto.PromptSteerAckPayload{InputID: inputID} switch { + case errors.Is(err, agent.ErrUnsupportedOperation): + ack.ErrorCode, ack.Error = "contract_violation", "Declared input capability has no implementation." case errors.Is(err, agent.ErrSteeringNotReady): ack.ErrorCode, ack.Error = "not_ready", err.Error() case errors.Is(err, agent.ErrSteeringInactive): diff --git a/apps/parsar-daemon/internal/dispatch/steering_lifetime_test.go b/apps/parsar-daemon/internal/dispatch/steering_lifetime_test.go index 27dfa0952..e384027ce 100644 --- a/apps/parsar-daemon/internal/dispatch/steering_lifetime_test.go +++ b/apps/parsar-daemon/internal/dispatch/steering_lifetime_test.go @@ -28,7 +28,7 @@ func TestDurableSteeringWaitsBeyondTransportDeadline(t *testing.T) { var session *fakeSession var calls atomic.Int32 release := make(chan struct{}) - h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}, 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) @@ -82,7 +82,7 @@ func TestDurableSteeringTransportTimeoutAndShutdown(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) exited := make(chan struct{}) - h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, func(_ context.Context, _ proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}, 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" { @@ -131,7 +131,7 @@ func TestDurableSteeringRequiresOptInAndAdapter(t *testing.T) { t.Run(map[bool]string{false: "old-adapter", true: "retained-run"}[supported], func(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) - h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, func(_ context.Context, _ proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(_ context.Context, _ proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { s := &steeringSession{fakeSession: &fakeSession{out: out, closeOutOnCancel: true}, steer: func(context.Context, proto.PromptSteerPayload) error { t.Error("unexpected legacy call"); return nil }} if !supported { return s, nil diff --git a/apps/parsar-daemon/internal/dispatch/steering_test.go b/apps/parsar-daemon/internal/dispatch/steering_test.go index c064a9681..4364984ac 100644 --- a/apps/parsar-daemon/internal/dispatch/steering_test.go +++ b/apps/parsar-daemon/internal/dispatch/steering_test.go @@ -33,7 +33,7 @@ func TestSteeringReceiptsAndRetries(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) calls, starts := 0, 0 - h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { starts++ return &steeringSession{ fakeSession: &fakeSession{out: out, closeOutOnCancel: true}, @@ -94,7 +94,7 @@ func TestSteeringReadinessAndUnsupportedRuns(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) calls := 0 - h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}, 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 { @@ -172,7 +172,7 @@ func TestSteeringDoesNotBlockOtherRunCancellation(t *testing.T) { defer h.router.Shutdown(context.Background()) entered, release := make(chan struct{}), make(chan struct{}) defer close(release) - h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}, 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 { @@ -235,7 +235,7 @@ func TestSteeringCapacityPreservesExistingReceipts(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) calls := 0 - h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}, 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/internal/agentdaemon/gateway/retired_provider_configuration_test.go b/internal/agentdaemon/gateway/retired_provider_configuration_test.go index 052e4c13b..e49f6f4c4 100644 --- a/internal/agentdaemon/gateway/retired_provider_configuration_test.go +++ b/internal/agentdaemon/gateway/retired_provider_configuration_test.go @@ -2,6 +2,7 @@ package gateway import ( "encoding/json" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "strings" "testing" @@ -9,7 +10,11 @@ import ( ) func TestRetiredProviderConfigurationDoesNotAffectHeartbeatAdmission(t *testing.T) { - const oldHeartbeat = `{"supported_agent_kinds":[{"kind":"codex","available":true,"capabilities":{"function_tools":true},"provider_configuration":{"schema_version":1,"providers":[{"protocol":"responses"}]}}]}` + caps, err := json.Marshal(prototest.Capabilities(proto.AgentKindCapabilities{FunctionTools: proto.CapabilitySupported})) + if err != nil { + t.Fatal(err) + } + oldHeartbeat := `{"supported_agent_kinds":[{"kind":"codex","available":true,"capabilities":` + string(caps) + `,"provider_configuration":{"schema_version":1,"providers":[{"protocol":"responses"}]}}]}` var heartbeat proto.HeartbeatPayload if err := json.Unmarshal([]byte(oldHeartbeat), &heartbeat); err != nil { t.Fatal(err) diff --git a/internal/agentdaemon/gateway/session.go b/internal/agentdaemon/gateway/session.go index 7cf019b88..604ddd5be 100644 --- a/internal/agentdaemon/gateway/session.go +++ b/internal/agentdaemon/gateway/session.go @@ -465,7 +465,9 @@ func (s *Session) markOfflineOnClose() { func (s *Session) handleHeartbeat(env proto.Envelope) { var p proto.HeartbeatPayload if err := env.DecodePayload(&p); err != nil { - s.log("agentdaemon gateway: decode heartbeat payload device=%s: %v", s.DeviceID, err) + s.log("agentdaemon gateway: invalid heartbeat declaration device=%s", s.DeviceID) + s.setSupportedAgentKinds(nil) + s.Close("invalid heartbeat declaration") return } kinds := deviceKindsFromHeartbeat(p) diff --git a/internal/agentdaemon/proto/capabilities_test.go b/internal/agentdaemon/proto/capabilities_test.go new file mode 100644 index 000000000..707c2c887 --- /dev/null +++ b/internal/agentdaemon/proto/capabilities_test.go @@ -0,0 +1,85 @@ +package proto_test + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" +) + +func TestCapabilityDeclarationIsCompleteOnBothSides(t *testing.T) { + caps := prototest.Capabilities(proto.AgentKindCapabilities{}) + raw, err := json.Marshal(caps) + if err != nil { + t.Fatal(err) + } + var fields map[string]json.RawMessage + if err := json.Unmarshal(raw, &fields); err != nil { + t.Fatal(err) + } + typ := reflect.TypeOf(caps) + if len(fields) != typ.NumField() { + t.Fatal("wire projection lost capability fields") + } + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + key := field.Tag.Get("json") + t.Run(key, func(t *testing.T) { + if string(fields[key]) != "false" { + t.Fatalf("explicit unsupported omitted: %s", raw) + } + for _, value := range []proto.CapabilitySupport{proto.CapabilityUnspecified, proto.CapabilitySupport(255)} { + invalid := caps + reflect.ValueOf(&invalid).Elem().Field(i).Set(reflect.ValueOf(value)) + if invalid.ValidateDeclaration() == nil { + t.Fatal("undeclared/invalid capability accepted") + } + if _, err := json.Marshal(invalid); err == nil { + t.Fatal("invalid declaration encoded") + } + } + original := fields[key] + for _, replacement := range []json.RawMessage{nil, []byte("null"), []byte("0"), []byte(`"true"`)} { + if replacement == nil { + delete(fields, key) + } else { + fields[key] = replacement + } + wire, _ := json.Marshal(fields) + decoded := caps // Decode must not preserve old values for omitted fields. + if json.Unmarshal(wire, &decoded) == nil { + t.Fatal("incomplete declaration decoded") + } + } + fields[key] = []byte("true") + wire, _ := json.Marshal(fields) + var decoded proto.AgentKindCapabilities + if err := json.Unmarshal(wire, &decoded); err != nil { + t.Fatal(err) + } + if reflect.ValueOf(decoded).Field(i).Interface() != proto.CapabilitySupported { + t.Fatal("supported field lost") + } + fields[key] = original + }) + } + fields["future_private_value"] = []byte("false") + wire, _ := json.Marshal(fields) + if json.Unmarshal(wire, &caps) == nil { + t.Fatal("unknown declaration accepted") + } +} + +func TestHeartbeatRejectsMissingCapabilityObject(t *testing.T) { + for _, raw := range []string{ + `{"supported_agent_kinds":[{"kind":"codex","available":true}]}`, + `{"supported_agent_kinds":[{"kind":"codex","available":false,"capabilities":null}]}`, + } { + var heartbeat proto.HeartbeatPayload + if json.Unmarshal([]byte(raw), &heartbeat) == nil { + t.Fatal("missing declaration accepted") + } + } +} From 1577196bd4577c60da6bb7d5cbb6c14f7cc7df6c Mon Sep 17 00:00:00 2001 From: saladday <1203511142@qq.com> Date: Wed, 30 Sep 2026 13:39:05 +0800 Subject: [PATCH 07/12] Guard evolving Harness interfaces and complete wire projections --- .../agent/contract_declarations_test.go | 128 ++++++++++++++++++ .../internal/agent/registry_test.go | 29 ++++ .../internal/cli/preparation_test.go | 2 +- .../internal/contracttest/wire_test.go | 5 + .../dispatch/executor_handoff_test.go | 5 + .../agentdaemon/gateway/capabilities_test.go | 50 +++++++ 6 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 apps/parsar-daemon/internal/agent/contract_declarations_test.go create mode 100644 internal/agentdaemon/gateway/capabilities_test.go diff --git a/apps/parsar-daemon/internal/agent/contract_declarations_test.go b/apps/parsar-daemon/internal/agent/contract_declarations_test.go new file mode 100644 index 000000000..fc642d3f4 --- /dev/null +++ b/apps/parsar-daemon/internal/agent/contract_declarations_test.go @@ -0,0 +1,128 @@ +package agent_test + +import ( + "encoding/json" + "go/ast" + "go/parser" + "go/token" + "os" + "path/filepath" + "reflect" + "strings" + "testing" +) + +// This inventory makes a newly introduced interface require a deliberate role +// decision. Each adapter's compile assertions then enforce its actual methods. +func TestPublicHarnessContractDeclarations(t *testing.T) { + roles := map[string][]string{ + "Executor": {"executor"}, + "Turn": {"session"}, + "Session": {"session"}, + "DurableSteerer": {"session"}, + "Steerer": {"session"}, + "FunctionResultSubmitter": {"session"}, + "PermissionResponder": {"session"}, + "UserChoiceResponder": {"session"}, + "WorkspaceReader": {"executor", "prepared", "session"}, + "WorkspaceDirectoryLister": {"executor", "prepared", "session"}, + "WorkspaceWriter": {"executor", "prepared", "session"}, + "Prepared": {"prepared"}, + "PreparedCancellation": {"prepared"}, + } + files, err := filepath.Glob("*.go") + if err != nil { + t.Fatal(err) + } + found := map[string]bool{} + for _, path := range files { + if strings.HasSuffix(path, "_test.go") { + continue + } + f, err := parser.ParseFile(token.NewFileSet(), path, nil, 0) + if err != nil { + t.Fatal(err) + } + ast.Inspect(f, func(node ast.Node) bool { + spec, ok := node.(*ast.TypeSpec) + if !ok || !spec.Name.IsExported() { + return true + } + if _, ok := spec.Type.(*ast.InterfaceType); ok { + found[spec.Name.Name] = true + } + return true + }) + } + if len(found) != len(roles) { + t.Fatal("classify every public Harness interface in the role contract", found) + } + for name := range found { + if _, ok := roles[name]; !ok { + t.Fatalf("unclassified interface %s", name) + } + } + data, err := os.ReadFile("../../../../internal/harnessconfig/builtin/catalog.json") + if err != nil { + t.Fatal(err) + } + var catalog []struct { + Configuration string `json:"configuration"` + } + if err := json.Unmarshal(data, &catalog); err != nil { + t.Fatal(err) + } + for _, entry := range catalog { + t.Run(entry.Configuration, func(t *testing.T) { + f, err := parser.ParseFile(token.NewFileSet(), filepath.Join(entry.Configuration, "contracts.go"), nil, 0) + if err != nil { + t.Fatal(err) + } + assertions := map[string]map[string]bool{} + ast.Inspect(f, func(node ast.Node) bool { + spec, ok := node.(*ast.ValueSpec) + if !ok || len(spec.Names) != 1 || spec.Names[0].Name != "_" || len(spec.Values) != 1 { + return true + } + contract, ok := spec.Type.(*ast.SelectorExpr) + if !ok { + return true + } + pkg, ok := contract.X.(*ast.Ident) + if !ok || pkg.Name != "agent" { + return true + } + call, ok := spec.Values[0].(*ast.CallExpr) + if !ok { + t.Fatal("contract assertion must identify its concrete owner") + } + paren, ok := call.Fun.(*ast.ParenExpr) + if !ok { + t.Fatal("expected pointer assertion") + } + pointer, ok := paren.X.(*ast.StarExpr) + if !ok { + t.Fatal("expected pointer owner") + } + owner, ok := pointer.X.(*ast.Ident) + if !ok { + t.Fatal("expected local concrete owner") + } + if assertions[contract.Sel.Name] == nil { + assertions[contract.Sel.Name] = map[string]bool{} + } + assertions[contract.Sel.Name][strings.ToLower(owner.Name)] = true + return true + }) + for name, owners := range roles { + want := map[string]bool{} + for _, owner := range owners { + want[owner] = true + } + if !reflect.DeepEqual(assertions[name], want) { + t.Errorf("%s needs explicit compile assertions on %v; got %v", name, want, assertions[name]) + } + } + }) + } +} diff --git a/apps/parsar-daemon/internal/agent/registry_test.go b/apps/parsar-daemon/internal/agent/registry_test.go index 08fc0885d..e26dcb88e 100644 --- a/apps/parsar-daemon/internal/agent/registry_test.go +++ b/apps/parsar-daemon/internal/agent/registry_test.go @@ -6,6 +6,7 @@ import ( "context" "errors" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" + "reflect" "slices" "testing" @@ -160,3 +161,31 @@ func TestRegistryExecutorRequiresExplicitRegistration(t *testing.T) { t.Fatal("replacing a kind retained its old executor capability") } } + +func TestRegistryRejectsEveryOmittedCapabilityBeforeReplacement(t *testing.T) { + valid := prototest.Capabilities(proto.AgentKindCapabilities{}) + for i := 0; i < reflect.TypeOf(valid).NumField(); i++ { + t.Run(reflect.TypeOf(valid).Field(i).Name, func(t *testing.T) { + registry := agent.NewRegistry() + registry.RegisterKind(proto.SupportedAgentKind{Kind: "fixture", Available: true, Capabilities: valid}, harnessconfig.Configuration{}, stubFactory("original")) + missing := valid + reflect.ValueOf(&missing).Elem().Field(i).Set(reflect.ValueOf(proto.CapabilityUnspecified)) + func() { + defer func() { + if recover() == nil { + t.Error("incomplete declaration registered") + } + }() + registry.RegisterKind(proto.SupportedAgentKind{Kind: "fixture", Available: false, Capabilities: missing}, harnessconfig.Configuration{}, stubFactory("replacement")) + }() + factory, err := registry.Resolve("fixture") + if err != nil { + t.Fatal(err) + } + session, err := factory(t.Context(), proto.PromptRequestPayload{}, nil) + if err != nil || session.(stubSession).marker != "original" { + t.Fatal("failed declaration changed registry") + } + }) + } +} diff --git a/apps/parsar-daemon/internal/cli/preparation_test.go b/apps/parsar-daemon/internal/cli/preparation_test.go index 05b868e2c..45fc59dec 100644 --- a/apps/parsar-daemon/internal/cli/preparation_test.go +++ b/apps/parsar-daemon/internal/cli/preparation_test.go @@ -16,7 +16,7 @@ import ( func TestPreparationRegistrationBypassesProductWrappers(t *testing.T) { for _, supported := range []bool{false, true} { reg := agent.NewRegistry() - registerAgentKinds(reg, agentCLIDiscovery{Codex: proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilityFromBool(supported)})}, ClaudeCode: proto.SupportedAgentKind{Kind: "claude_code"}, OpenCode: proto.SupportedAgentKind{Kind: "opencode"}, Pi: proto.SupportedAgentKind{Kind: "pi"}, MCode: proto.SupportedAgentKind{Kind: "mcode"}}, "http://unreachable.invalid") + registerAgentKinds(reg, agentCLIDiscovery{Codex: proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilityFromBool(supported)})}, ClaudeCode: proto.SupportedAgentKind{Kind: "claude_code", Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, OpenCode: proto.SupportedAgentKind{Kind: "opencode", Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, Pi: proto.SupportedAgentKind{Kind: "pi", Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, MCode: proto.SupportedAgentKind{Kind: "mcode", Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}}, "http://unreachable.invalid") _, err := reg.ResolvePreparation("codex") if (err == nil) != supported { t.Fatal("unverified native version advertised preparation") diff --git a/apps/parsar-daemon/internal/contracttest/wire_test.go b/apps/parsar-daemon/internal/contracttest/wire_test.go index 070c16514..65a6fb731 100644 --- a/apps/parsar-daemon/internal/contracttest/wire_test.go +++ b/apps/parsar-daemon/internal/contracttest/wire_test.go @@ -375,3 +375,8 @@ func TestWireContractDisconnectIsUnknownAndReconnectDoesNotReplay(t *testing.T) t.Fatal("reconnect replayed input") } } + +// These fixtures exercise settlement only; active input is deliberately rejected. +func (*controlledTurn) SteerWithReceipt(context.Context, proto.PromptSteerPayload, func()) error { + return agent.ErrSteeringRejected +} diff --git a/apps/parsar-daemon/internal/dispatch/executor_handoff_test.go b/apps/parsar-daemon/internal/dispatch/executor_handoff_test.go index 53a1b59cb..e199f3f60 100644 --- a/apps/parsar-daemon/internal/dispatch/executor_handoff_test.go +++ b/apps/parsar-daemon/internal/dispatch/executor_handoff_test.go @@ -229,3 +229,8 @@ func TestPreparedDonePublishesAfterExecutorHandoff(t *testing.T) { }) } } + +// These fixtures exercise settlement only; active input is deliberately rejected. +func (*terminalHandoffTurn) SteerWithReceipt(context.Context, proto.PromptSteerPayload, func()) error { + return agent.ErrSteeringRejected +} diff --git a/internal/agentdaemon/gateway/capabilities_test.go b/internal/agentdaemon/gateway/capabilities_test.go new file mode 100644 index 000000000..da09e2af7 --- /dev/null +++ b/internal/agentdaemon/gateway/capabilities_test.go @@ -0,0 +1,50 @@ +package gateway + +import ( + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/device" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" + "reflect" + "testing" +) + +func TestCapabilityProjectionCoversEveryField(t *testing.T) { + declaration := prototest.Capabilities(proto.AgentKindCapabilities{}) + wireType, deviceType := reflect.TypeOf(declaration), reflect.TypeOf(device.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{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{SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "fixture", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}}}) + if err != nil { + t.Fatal(err) + } + session.handleHeartbeat(valid) + if _, found, _ := session.AgentKindStatus("fixture"); !found { + t.Fatal("valid declaration not admitted") + } + session.handleHeartbeat(proto.Envelope{Type: proto.TypeHeartbeat, Payload: []byte(`{"supported_agent_kinds":[{"kind":"fixture","available":true}]}`)}) + if !session.IsClosed() { + t.Fatal("invalid peer remains usable") + } + if _, found, known := session.AgentKindStatus("fixture"); found || !known { + t.Fatal("old declaration survived invalid heartbeat") + } +} From 194546a71f93da976c5c08903754d8f2a35e43bf Mon Sep 17 00:00:00 2001 From: saladday <1203511142@qq.com> Date: Wed, 30 Sep 2026 13:41:24 +0800 Subject: [PATCH 08/12] Verify declaration admission and document the Runtime extension contract --- CONTRIBUTING.md | 4 +- apps/docs/content/docs/runtime-protocol.mdx | 31 +++++ apps/docs/content/guide-sources.json | 4 +- .../agent/codex/preparation_router_test.go | 2 +- .../internal/agent/configuration_test.go | 2 +- .../internal/agent/registry_test.go | 2 +- .../internal/cli/authoring_test.go | 2 +- .../internal/cli/connect_cleanup_test.go | 2 +- .../internal/cli/connect_test.go | 2 +- .../internal/cli/preparation_test.go | 2 +- .../internal/contracttest/wire_test.go | 2 +- .../internal/dispatch/cancellation_test.go | 2 +- .../dispatch/capability_admission_test.go | 106 ++++++++++++++++++ .../internal/dispatch/environment_test.go | 2 +- .../dispatch/executor_cancel_receipt_test.go | 2 +- .../dispatch/executor_handoff_test.go | 2 +- .../internal/dispatch/executor_test.go | 6 +- .../dispatch/functions_native_test.go | 2 +- .../internal/dispatch/functions_test.go | 2 +- .../internal/dispatch/local_directory_test.go | 2 +- .../internal/dispatch/mcp_http_test.go | 2 +- .../dispatch/optional_interactions_test.go | 2 +- .../internal/dispatch/preparation_test.go | 2 +- .../internal/dispatch/receipt_order_test.go | 2 +- .../internal/dispatch/router_test.go | 2 +- .../dispatch/steering_lifetime_test.go | 2 +- .../internal/dispatch/steering_test.go | 2 +- .../parsar-daemon/testdata/onboarding/main.go | 2 +- docs/runtime-protocol.md | 31 +++++ internal/agentdaemon/gateway/mcp_test.go | 2 +- .../agentdaemon/gateway/preparation_test.go | 2 +- .../retired_provider_configuration_test.go | 2 +- internal/agentdaemon/gateway/session_test.go | 2 +- internal/agentdaemon/proto/inbound.go | 2 +- .../internal/store/claude_execution_test.go | 2 +- .../internal/store/claude_mcp_test.go | 2 +- .../internal/store/dispatch_test.go | 2 +- .../store/environment_directory_test.go | 2 +- .../store/environment_expiry_dispatch_test.go | 2 +- .../store/environment_worker_helpers_test.go | 2 +- .../internal/store/execution_messages_test.go | 2 +- .../internal/store/function_execution_test.go | 2 +- .../internal/store/function_worker_test.go | 2 +- .../store/local_environment_worker_test.go | 2 +- .../internal/store/public_execution_test.go | 2 +- .../store/runtime_compute_lifecycle_test.go | 2 +- .../store/runtime_initialization_peer_test.go | 2 +- .../store/structured_output_dispatch_test.go | 2 +- .../internal/store/worker_lease_loss_test.go | 2 +- 49 files changed, 221 insertions(+), 47 deletions(-) create mode 100644 apps/parsar-daemon/internal/dispatch/capability_admission_test.go diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5b8579825..b7b7f7caf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -242,7 +242,9 @@ The Runtime is not a sandbox; see Shared wire types and validators live only in `internal/agentdaemon/proto`. Change both peers together with an exact wire-version check; do not add a -parallel schema or a historical wire fallback. +parallel schema or a historical wire fallback. Capability completeness and +interface coverage are mandatory extension gates under the +[explicit declaration contract](docs/runtime-protocol.md#explicit-capability-declarations). ### Pre-release policy diff --git a/apps/docs/content/docs/runtime-protocol.mdx b/apps/docs/content/docs/runtime-protocol.mdx index cc7236641..9e144911c 100644 --- a/apps/docs/content/docs/runtime-protocol.mdx +++ b/apps/docs/content/docs/runtime-protocol.mdx @@ -59,6 +59,37 @@ leases and evicts old Run/interaction routes; the new connection does not inheri them. A valid credential and connection are not authority to choose another Session or Environment binding. +### Explicit capability declarations + +`AgentKindCapabilities` describes the composed Runtime and Harness, independently +of `Available` and the Core model profile. Every field uses `CapabilitySupport`: +`CapabilitySupported` or `CapabilityUnsupported`. Zero means unspecified and is +invalid even for an unavailable Harness. Registration validates the complete +struct before changing the registry; there is no implicit basic descriptor. + +The wire still uses JSON booleans and includes every field, including `false`. +Encoding incomplete declarations fails; decoding rejects omitted, null, invalid +or unknown capability fields, including a missing capability object. An invalid +heartbeat clears the connection's admission snapshot and closes its transport. +That establishes no native completion or cancellation result. Both peers use the +same exact wire version; no historical declaration format is accepted. + +Each admitted Executor and Turn retains its declaration. Rediscovery cannot add +operations to an existing owner. Optional operations check this snapshot before +native calls; interface presence alone never grants support. A declared operation +returning `agent.ErrUnsupportedOperation` is a contract violation, distinct from +unavailability, a failed native call or an uncertain write. Uncertain operations +keep their existing receipts and ownership; they are never automatically replayed. +Workspace support includes the common Runtime workspace implementation, so a +native adapter's unsupported workspace method does not disable that composition. + +New fields require an explicit decision in each production declaration. Contract +tests enumerate every field for registration, wire round trips and the persisted +boolean projection. The shared test fixture lists current fields individually; +it does not supply defaults for future fields. The Harness interface inventory +also requires a role decision and compile assertions for every public adapter; +see [Harness onboarding](/harness-onboarding). + ### Executor and Turn lifetimes This section owns the separation of execution and resource lifetimes. diff --git a/apps/docs/content/guide-sources.json b/apps/docs/content/guide-sources.json index 65a082355..d77a47c08 100644 --- a/apps/docs/content/guide-sources.json +++ b/apps/docs/content/guide-sources.json @@ -27,7 +27,7 @@ "docs/development.md": "f5c340253036a2cabc43e22aecdf70522d90280d6511e7649278ae93712ab8b6", "contracts/agents-api/harness-onboarding.md": "acbc9bf8c1365e1242d4c9a934b068a19ef3450a4aef2fc08356624380222266", "docs/runtime-bootstrap.md": "0d49aed73b298039e04453e6f465b0e925fb2227fa35d4206820bb3acbd8df39", - "docs/runtime-protocol.md": "1d48186fa84140403358d2d01ddaca56d7122e1b3606cb6855ddefffeb3b8ed6", + "docs/runtime-protocol.md": "4bce016e8ec239281969c84c8483044cf3247051eb062ec30d3effa00b661001", "docs/sandbox-provider.md": "4d47b234a457f874f3ff7e61a7f5da6fc8b75b0c6df0ce0ce9ead1c07afdfb3e", "apps/docs/scripts/guides.json": "3c3768fb94fd3d42464d4ce8c55724b9ba8360133c7cc19d513d8ec83a8e034f" }, @@ -58,7 +58,7 @@ "content/docs/development.mdx": "2e5249ddfca571d264e93fd1e0e390a4bd5b0b623bda100cd02f2982929850bb", "content/docs/harness-onboarding.mdx": "f0d91fd8f4cbaaa9475a25e24b08d9bfb0c25a8549af47b81073886444e5ac44", "content/docs/runtime-bootstrap.mdx": "58982955811c9ad46a9fc04d8d2aef5a762fc9d25d5833f88aa75ee3fc46523f", - "content/docs/runtime-protocol.mdx": "caa7700e442b0666340c091f3ec0bff32e6217b3be4bc845c3b9c23ee0c8d0f6", + "content/docs/runtime-protocol.mdx": "560f887d5fc9fa673275a6481220f5fb997c4ece30c09384b81b14dbc7aa0dd4", "content/docs/sandbox-provider.mdx": "9db833fe9ff3f30a65451f80d7251348695a08e7830f9e95871739a710529818" } } diff --git a/apps/parsar-daemon/internal/agent/codex/preparation_router_test.go b/apps/parsar-daemon/internal/agent/codex/preparation_router_test.go index 472a3a3bf..e653de49f 100644 --- a/apps/parsar-daemon/internal/agent/codex/preparation_router_test.go +++ b/apps/parsar-daemon/internal/agent/codex/preparation_router_test.go @@ -7,7 +7,6 @@ import ( "errors" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/localworkspace" "github.com/MiniMax-AI-Dev/parsar/internal/agentcapabilities" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "github.com/google/uuid" "os" "path/filepath" @@ -17,6 +16,7 @@ import ( "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/dispatch" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" ) type preparationWireSender chan proto.Envelope diff --git a/apps/parsar-daemon/internal/agent/configuration_test.go b/apps/parsar-daemon/internal/agent/configuration_test.go index 299834e0a..75a44ec39 100644 --- a/apps/parsar-daemon/internal/agent/configuration_test.go +++ b/apps/parsar-daemon/internal/agent/configuration_test.go @@ -3,12 +3,12 @@ package agent_test import ( "context" "errors" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "strings" "testing" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig" ) diff --git a/apps/parsar-daemon/internal/agent/registry_test.go b/apps/parsar-daemon/internal/agent/registry_test.go index e26dcb88e..82068c2ee 100644 --- a/apps/parsar-daemon/internal/agent/registry_test.go +++ b/apps/parsar-daemon/internal/agent/registry_test.go @@ -5,13 +5,13 @@ import "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig" import ( "context" "errors" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "reflect" "slices" "testing" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" ) func stubFactory(marker string) agent.Factory { diff --git a/apps/parsar-daemon/internal/cli/authoring_test.go b/apps/parsar-daemon/internal/cli/authoring_test.go index 8081469b9..7cb9db5f5 100644 --- a/apps/parsar-daemon/internal/cli/authoring_test.go +++ b/apps/parsar-daemon/internal/cli/authoring_test.go @@ -5,7 +5,6 @@ import "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig" import ( "context" "errors" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "net" "os" "testing" @@ -14,6 +13,7 @@ import ( "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/authoring" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" ) func TestAuthoringSocketEndsWithTurnWhileSessionIsRetained(t *testing.T) { diff --git a/apps/parsar-daemon/internal/cli/connect_cleanup_test.go b/apps/parsar-daemon/internal/cli/connect_cleanup_test.go index cd253530e..f89a3aee0 100644 --- a/apps/parsar-daemon/internal/cli/connect_cleanup_test.go +++ b/apps/parsar-daemon/internal/cli/connect_cleanup_test.go @@ -5,7 +5,6 @@ import "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig" import ( "context" "errors" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "net/http" "net/http/httptest" "os" @@ -14,6 +13,7 @@ import ( "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/transport" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "github.com/gorilla/websocket" "sync/atomic" "testing" diff --git a/apps/parsar-daemon/internal/cli/connect_test.go b/apps/parsar-daemon/internal/cli/connect_test.go index 9d910ba8e..99facf4b6 100644 --- a/apps/parsar-daemon/internal/cli/connect_test.go +++ b/apps/parsar-daemon/internal/cli/connect_test.go @@ -2,7 +2,6 @@ package cli import ( "context" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "os" "reflect" "strings" @@ -15,6 +14,7 @@ import ( opencodeagent "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent/opencode" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent/pi" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" ) func TestScrubInlineConnectArgsRemovesTokenURLAndDeviceName(t *testing.T) { diff --git a/apps/parsar-daemon/internal/cli/preparation_test.go b/apps/parsar-daemon/internal/cli/preparation_test.go index 45fc59dec..bac218f4c 100644 --- a/apps/parsar-daemon/internal/cli/preparation_test.go +++ b/apps/parsar-daemon/internal/cli/preparation_test.go @@ -5,12 +5,12 @@ import "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig" import ( "context" "errors" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "testing" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/authoring" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" ) func TestPreparationRegistrationBypassesProductWrappers(t *testing.T) { diff --git a/apps/parsar-daemon/internal/contracttest/wire_test.go b/apps/parsar-daemon/internal/contracttest/wire_test.go index 65a6fb731..8214eaf48 100644 --- a/apps/parsar-daemon/internal/contracttest/wire_test.go +++ b/apps/parsar-daemon/internal/contracttest/wire_test.go @@ -7,7 +7,6 @@ import "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig" import ( "context" "errors" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "net/http" "net/http/httptest" "strings" @@ -22,6 +21,7 @@ import ( "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/device" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/gateway" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" ) const credential = "synthetic-contract-credential" diff --git a/apps/parsar-daemon/internal/dispatch/cancellation_test.go b/apps/parsar-daemon/internal/dispatch/cancellation_test.go index 3553dcc6b..b49586ad8 100644 --- a/apps/parsar-daemon/internal/dispatch/cancellation_test.go +++ b/apps/parsar-daemon/internal/dispatch/cancellation_test.go @@ -5,11 +5,11 @@ import "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig" import ( "context" "errors" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "testing" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" ) type cancelReceiptSession struct { diff --git a/apps/parsar-daemon/internal/dispatch/capability_admission_test.go b/apps/parsar-daemon/internal/dispatch/capability_admission_test.go new file mode 100644 index 000000000..ebc1b9442 --- /dev/null +++ b/apps/parsar-daemon/internal/dispatch/capability_admission_test.go @@ -0,0 +1,106 @@ +package dispatch_test + +import ( + "context" + "fmt" + "sync/atomic" + "testing" + + "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" + "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig" +) + +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)})} + h.reg.RegisterKind(info, harnessconfig.Configuration{}, factory) + if err := h.router.Handle(t.Context(), mustEnv(t, proto.TypePromptRequest, "run", proto.PromptRequestPayload{AgentKind: "fixture"})); err != nil { + t.Fatal(err) + } + // A new registration cannot rewrite the already admitted owner's contract. + info.Capabilities.Steering = proto.CapabilityFromBool(!supported) + h.reg.RegisterKind(info, harnessconfig.Configuration{}, factory) + for range 2 { + if err := handleSteeringAndWait(t, h, mustEnv(t, 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 TestInteractionDeclarationPrecedesResponderMethods(t *testing.T) { + for _, supported := range []bool{false, true} { + for _, ask := range []bool{false, true} { + t.Run(fmt.Sprintf("supported=%v/ask=%v", supported, ask), func(t *testing.T) { + h := newHarness(t) + defer h.router.Shutdown(context.Background()) + var session *fakeSession + info := proto.SupportedAgentKind{Kind: "fixture", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Permissions: proto.CapabilityFromBool(supported)})} + h.reg.RegisterKind(info, harnessconfig.Configuration{}, func(_ context.Context, _ proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { + session = &fakeSession{out: out, closeOutOnCancel: true, submitErr: agent.ErrUnsupportedOperation, askErr: agent.ErrUnsupportedOperation} + return session, nil + }) + if err := h.router.Handle(t.Context(), mustEnv(t, proto.TypePromptRequest, "run", proto.PromptRequestPayload{AgentKind: "fixture"})); err != nil { + t.Fatal(err) + } + event := mustEnv(t, proto.TypePermissionRequest, "run", proto.PermissionRequestPayload{RequestID: "interaction", Tool: "fixture"}) + decision := mustEnv(t, proto.TypePermissionDecision, "interaction", proto.PermissionDecisionPayload{DeliveryID: "decision", Approved: true}) + if ask { + event = mustEnv(t, proto.TypePromptForUserChoice, "run", proto.PromptForUserChoicePayload{AskID: "interaction"}) + decision = mustEnv(t, proto.TypePromptForUserChoiceDecision, "interaction", proto.PromptForUserChoiceDecisionPayload{DeliveryID: "decision"}) + } + session.out <- event + waitFor(t, func() bool { return len(h.sender.snapshot()) > 0 }, "interaction indexed") + if err := h.router.Handle(t.Context(), decision); err != nil { + t.Fatal(err) + } + code := "unsupported" + if supported { + code = "contract_violation" + } + assertDecisionAck(t, h.sender, "decision", false, code) + count := len(session.submissions()) + if ask { + session.askMu.Lock() + count = len(session.askCalls) + session.askMu.Unlock() + } + want := 0 + if supported { + want = 1 + } + if count != want { + t.Fatalf("native calls %d, want %d", count, want) + } + }) + } + } +} diff --git a/apps/parsar-daemon/internal/dispatch/environment_test.go b/apps/parsar-daemon/internal/dispatch/environment_test.go index 22692b61a..f092405c5 100644 --- a/apps/parsar-daemon/internal/dispatch/environment_test.go +++ b/apps/parsar-daemon/internal/dispatch/environment_test.go @@ -5,11 +5,11 @@ import "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig" import ( "context" "errors" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "testing" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" ) func TestNoEnvironmentRejectsOtherEngineBeforeFactory(t *testing.T) { diff --git a/apps/parsar-daemon/internal/dispatch/executor_cancel_receipt_test.go b/apps/parsar-daemon/internal/dispatch/executor_cancel_receipt_test.go index 9d83f86c4..8b1a105b4 100644 --- a/apps/parsar-daemon/internal/dispatch/executor_cancel_receipt_test.go +++ b/apps/parsar-daemon/internal/dispatch/executor_cancel_receipt_test.go @@ -5,7 +5,6 @@ import "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig" import ( "context" "errors" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "sync" "sync/atomic" "testing" @@ -14,6 +13,7 @@ import ( "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/dispatch" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" ) type receiptCancelSender struct { diff --git a/apps/parsar-daemon/internal/dispatch/executor_handoff_test.go b/apps/parsar-daemon/internal/dispatch/executor_handoff_test.go index e199f3f60..38d3d938a 100644 --- a/apps/parsar-daemon/internal/dispatch/executor_handoff_test.go +++ b/apps/parsar-daemon/internal/dispatch/executor_handoff_test.go @@ -5,7 +5,6 @@ import "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig" import ( "context" "errors" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "sync" "sync/atomic" "testing" @@ -13,6 +12,7 @@ import ( "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" ) type terminalHandoffSender struct { diff --git a/apps/parsar-daemon/internal/dispatch/executor_test.go b/apps/parsar-daemon/internal/dispatch/executor_test.go index 8c80a4a10..337ffefc9 100644 --- a/apps/parsar-daemon/internal/dispatch/executor_test.go +++ b/apps/parsar-daemon/internal/dispatch/executor_test.go @@ -6,7 +6,6 @@ import ( "context" "errors" "fmt" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "sync" "sync/atomic" "testing" @@ -15,6 +14,7 @@ import ( "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/dispatch" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" ) type reusableExecutor struct { @@ -313,3 +313,7 @@ func TestExecutorRejectsOutputFromAnotherTurn(t *testing.T) { } } } + +func (*reusableTurn) SteerWithReceipt(context.Context, proto.PromptSteerPayload, func()) error { + return agent.ErrSteeringRejected +} diff --git a/apps/parsar-daemon/internal/dispatch/functions_native_test.go b/apps/parsar-daemon/internal/dispatch/functions_native_test.go index e428cee35..7c222e786 100644 --- a/apps/parsar-daemon/internal/dispatch/functions_native_test.go +++ b/apps/parsar-daemon/internal/dispatch/functions_native_test.go @@ -6,7 +6,6 @@ import ( "context" "encoding/json" "fmt" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "net/http" "net/http/httptest" "os" @@ -21,6 +20,7 @@ import ( "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent/codex" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/dispatch" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" ) type nativeFunctionSender chan proto.Envelope diff --git a/apps/parsar-daemon/internal/dispatch/functions_test.go b/apps/parsar-daemon/internal/dispatch/functions_test.go index 60761edb3..c6bc1cc0a 100644 --- a/apps/parsar-daemon/internal/dispatch/functions_test.go +++ b/apps/parsar-daemon/internal/dispatch/functions_test.go @@ -7,7 +7,6 @@ import ( "context" "encoding/base64" "encoding/json" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "image" "image/color" "image/png" @@ -17,6 +16,7 @@ import ( "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/dispatch" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" ) type functionSession struct { diff --git a/apps/parsar-daemon/internal/dispatch/local_directory_test.go b/apps/parsar-daemon/internal/dispatch/local_directory_test.go index 67188ba50..fb2caa3ac 100644 --- a/apps/parsar-daemon/internal/dispatch/local_directory_test.go +++ b/apps/parsar-daemon/internal/dispatch/local_directory_test.go @@ -5,7 +5,6 @@ import "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig" import ( "context" "errors" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "os" "path/filepath" "sync/atomic" @@ -16,6 +15,7 @@ import ( "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/dispatch" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/localworkspace" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "github.com/google/uuid" ) diff --git a/apps/parsar-daemon/internal/dispatch/mcp_http_test.go b/apps/parsar-daemon/internal/dispatch/mcp_http_test.go index db1538a6c..bac2d4c2a 100644 --- a/apps/parsar-daemon/internal/dispatch/mcp_http_test.go +++ b/apps/parsar-daemon/internal/dispatch/mcp_http_test.go @@ -6,13 +6,13 @@ import ( "context" "encoding/json" "errors" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "strings" "testing" "time" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" ) func TestMCPHTTPBearerRejectsUnsupportedRequestsBeforeFactory(t *testing.T) { diff --git a/apps/parsar-daemon/internal/dispatch/optional_interactions_test.go b/apps/parsar-daemon/internal/dispatch/optional_interactions_test.go index baedc7f25..7895b1774 100644 --- a/apps/parsar-daemon/internal/dispatch/optional_interactions_test.go +++ b/apps/parsar-daemon/internal/dispatch/optional_interactions_test.go @@ -4,11 +4,11 @@ import "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig" import ( "context" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "testing" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" ) // Wrapping only Cancel proves that no responder stubs are required for a Session. diff --git a/apps/parsar-daemon/internal/dispatch/preparation_test.go b/apps/parsar-daemon/internal/dispatch/preparation_test.go index 8f7260522..da29fdf63 100644 --- a/apps/parsar-daemon/internal/dispatch/preparation_test.go +++ b/apps/parsar-daemon/internal/dispatch/preparation_test.go @@ -5,7 +5,6 @@ import "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig" import ( "context" "errors" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "os" "sync" "sync/atomic" @@ -17,6 +16,7 @@ import ( "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/localworkspace" "github.com/MiniMax-AI-Dev/parsar/internal/agentcapabilities" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" ) type controlledPreparation struct { diff --git a/apps/parsar-daemon/internal/dispatch/receipt_order_test.go b/apps/parsar-daemon/internal/dispatch/receipt_order_test.go index c20e5e02d..b6e8a7aca 100644 --- a/apps/parsar-daemon/internal/dispatch/receipt_order_test.go +++ b/apps/parsar-daemon/internal/dispatch/receipt_order_test.go @@ -5,7 +5,6 @@ import "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig" import ( "context" "errors" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "sync" "sync/atomic" "testing" @@ -14,6 +13,7 @@ import ( "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/dispatch" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" ) type blockedReceiptSender struct { diff --git a/apps/parsar-daemon/internal/dispatch/router_test.go b/apps/parsar-daemon/internal/dispatch/router_test.go index 1e26b7fda..59290389e 100644 --- a/apps/parsar-daemon/internal/dispatch/router_test.go +++ b/apps/parsar-daemon/internal/dispatch/router_test.go @@ -5,7 +5,6 @@ import "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig" import ( "context" "errors" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "slices" "sync" "testing" @@ -14,6 +13,7 @@ import ( "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/dispatch" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" ) // --------------------------------------------------------------------- diff --git a/apps/parsar-daemon/internal/dispatch/steering_lifetime_test.go b/apps/parsar-daemon/internal/dispatch/steering_lifetime_test.go index e384027ce..b8c20c7ed 100644 --- a/apps/parsar-daemon/internal/dispatch/steering_lifetime_test.go +++ b/apps/parsar-daemon/internal/dispatch/steering_lifetime_test.go @@ -4,13 +4,13 @@ import "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig" import ( "context" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "sync/atomic" "testing" "time" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" ) type durableSteeringSession struct { diff --git a/apps/parsar-daemon/internal/dispatch/steering_test.go b/apps/parsar-daemon/internal/dispatch/steering_test.go index 4364984ac..5ec852417 100644 --- a/apps/parsar-daemon/internal/dispatch/steering_test.go +++ b/apps/parsar-daemon/internal/dispatch/steering_test.go @@ -6,12 +6,12 @@ import ( "context" "errors" "fmt" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "testing" "time" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" ) type steeringSession struct { diff --git a/apps/parsar-daemon/testdata/onboarding/main.go b/apps/parsar-daemon/testdata/onboarding/main.go index 05ac3fb3a..4d46c107e 100644 --- a/apps/parsar-daemon/testdata/onboarding/main.go +++ b/apps/parsar-daemon/testdata/onboarding/main.go @@ -9,7 +9,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "io" "log/slog" "os" @@ -18,6 +17,7 @@ import ( "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/dispatch" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" ) type sender struct { diff --git a/docs/runtime-protocol.md b/docs/runtime-protocol.md index 4828bed2a..3690a1f77 100644 --- a/docs/runtime-protocol.md +++ b/docs/runtime-protocol.md @@ -56,6 +56,37 @@ leases and evicts old Run/interaction routes; the new connection does not inheri them. A valid credential and connection are not authority to choose another Session or Environment binding. +### Explicit capability declarations + +`AgentKindCapabilities` describes the composed Runtime and Harness, independently +of `Available` and the Core model profile. Every field uses `CapabilitySupport`: +`CapabilitySupported` or `CapabilityUnsupported`. Zero means unspecified and is +invalid even for an unavailable Harness. Registration validates the complete +struct before changing the registry; there is no implicit basic descriptor. + +The wire still uses JSON booleans and includes every field, including `false`. +Encoding incomplete declarations fails; decoding rejects omitted, null, invalid +or unknown capability fields, including a missing capability object. An invalid +heartbeat clears the connection's admission snapshot and closes its transport. +That establishes no native completion or cancellation result. Both peers use the +same exact wire version; no historical declaration format is accepted. + +Each admitted Executor and Turn retains its declaration. Rediscovery cannot add +operations to an existing owner. Optional operations check this snapshot before +native calls; interface presence alone never grants support. A declared operation +returning `agent.ErrUnsupportedOperation` is a contract violation, distinct from +unavailability, a failed native call or an uncertain write. Uncertain operations +keep their existing receipts and ownership; they are never automatically replayed. +Workspace support includes the common Runtime workspace implementation, so a +native adapter's unsupported workspace method does not disable that composition. + +New fields require an explicit decision in each production declaration. Contract +tests enumerate every field for registration, wire round trips and the persisted +boolean projection. The shared test fixture lists current fields individually; +it does not supply defaults for future fields. The Harness interface inventory +also requires a role decision and compile assertions for every public adapter; +see [Harness onboarding](../contracts/agents-api/harness-onboarding.md). + ### Executor and Turn lifetimes This section owns the separation of execution and resource lifetimes. diff --git a/internal/agentdaemon/gateway/mcp_test.go b/internal/agentdaemon/gateway/mcp_test.go index 480716fbb..975b658fa 100644 --- a/internal/agentdaemon/gateway/mcp_test.go +++ b/internal/agentdaemon/gateway/mcp_test.go @@ -2,11 +2,11 @@ package gateway import ( "encoding/json" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "strings" "testing" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" ) func TestMCPHTTPBearerCapabilitySurvivesHeartbeatMapping(t *testing.T) { diff --git a/internal/agentdaemon/gateway/preparation_test.go b/internal/agentdaemon/gateway/preparation_test.go index c65dcff0f..b9d3ffb87 100644 --- a/internal/agentdaemon/gateway/preparation_test.go +++ b/internal/agentdaemon/gateway/preparation_test.go @@ -2,10 +2,10 @@ package gateway import ( "errors" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "testing" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" ) func TestPreparationSubscriptionHasNoRunIdentityAndOrdersRevisions(t *testing.T) { diff --git a/internal/agentdaemon/gateway/retired_provider_configuration_test.go b/internal/agentdaemon/gateway/retired_provider_configuration_test.go index e49f6f4c4..f104f98e4 100644 --- a/internal/agentdaemon/gateway/retired_provider_configuration_test.go +++ b/internal/agentdaemon/gateway/retired_provider_configuration_test.go @@ -2,11 +2,11 @@ package gateway import ( "encoding/json" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "strings" "testing" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" ) func TestRetiredProviderConfigurationDoesNotAffectHeartbeatAdmission(t *testing.T) { diff --git a/internal/agentdaemon/gateway/session_test.go b/internal/agentdaemon/gateway/session_test.go index f3db4ae76..3026ec85a 100644 --- a/internal/agentdaemon/gateway/session_test.go +++ b/internal/agentdaemon/gateway/session_test.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "errors" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "io" "sync" "testing" @@ -12,6 +11,7 @@ import ( "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/device" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" ) // fakeConn is the WSConn implementation used by session + registry diff --git a/internal/agentdaemon/proto/inbound.go b/internal/agentdaemon/proto/inbound.go index 3ef15cc16..14613fb6c 100644 --- a/internal/agentdaemon/proto/inbound.go +++ b/internal/agentdaemon/proto/inbound.go @@ -262,7 +262,7 @@ type SupportedAgentKind struct { Kind string `json:"kind"` Available bool `json:"available"` Version string `json:"version,omitempty"` - Capabilities AgentKindCapabilities `json:"capabilities,omitempty"` + Capabilities AgentKindCapabilities `json:"capabilities"` } // HeartbeatPayload advertises only explicit engine descriptors. Missing diff --git a/services/agents-api/internal/store/claude_execution_test.go b/services/agents-api/internal/store/claude_execution_test.go index 2c7cf66d5..4b88d3316 100644 --- a/services/agents-api/internal/store/claude_execution_test.go +++ b/services/agents-api/internal/store/claude_execution_test.go @@ -5,12 +5,12 @@ import ( "encoding/json" "errors" "fmt" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "strings" "testing" "time" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/execution" "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/store" ) diff --git a/services/agents-api/internal/store/claude_mcp_test.go b/services/agents-api/internal/store/claude_mcp_test.go index dd9e8ecaf..360c0e173 100644 --- a/services/agents-api/internal/store/claude_mcp_test.go +++ b/services/agents-api/internal/store/claude_mcp_test.go @@ -4,12 +4,12 @@ import ( "context" "errors" "fmt" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "strings" "testing" "time" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/execution" "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/store" ) diff --git a/services/agents-api/internal/store/dispatch_test.go b/services/agents-api/internal/store/dispatch_test.go index 0c95af129..a37897bf2 100644 --- a/services/agents-api/internal/store/dispatch_test.go +++ b/services/agents-api/internal/store/dispatch_test.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "errors" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "net/http" "net/http/httptest" "net/url" @@ -15,6 +14,7 @@ import ( "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/device" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/gateway" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/execution" "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/runtime" "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/store" diff --git a/services/agents-api/internal/store/environment_directory_test.go b/services/agents-api/internal/store/environment_directory_test.go index 15464eb89..f05cb4ff0 100644 --- a/services/agents-api/internal/store/environment_directory_test.go +++ b/services/agents-api/internal/store/environment_directory_test.go @@ -3,11 +3,11 @@ package store_test import ( "context" "errors" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "testing" "time" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/execution" "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/store" "github.com/google/uuid" diff --git a/services/agents-api/internal/store/environment_expiry_dispatch_test.go b/services/agents-api/internal/store/environment_expiry_dispatch_test.go index a3200ac29..49443f262 100644 --- a/services/agents-api/internal/store/environment_expiry_dispatch_test.go +++ b/services/agents-api/internal/store/environment_expiry_dispatch_test.go @@ -2,10 +2,10 @@ package store_test import ( "encoding/json" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "testing" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/store" ) diff --git a/services/agents-api/internal/store/environment_worker_helpers_test.go b/services/agents-api/internal/store/environment_worker_helpers_test.go index 629322f11..ffacdc639 100644 --- a/services/agents-api/internal/store/environment_worker_helpers_test.go +++ b/services/agents-api/internal/store/environment_worker_helpers_test.go @@ -3,11 +3,11 @@ package store_test import ( "context" "encoding/json" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "testing" "time" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/execution" "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/store" "github.com/google/uuid" diff --git a/services/agents-api/internal/store/execution_messages_test.go b/services/agents-api/internal/store/execution_messages_test.go index 0e2013ed4..e240151f0 100644 --- a/services/agents-api/internal/store/execution_messages_test.go +++ b/services/agents-api/internal/store/execution_messages_test.go @@ -3,11 +3,11 @@ package store_test import ( "context" "encoding/json" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "testing" "time" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/store" ) diff --git a/services/agents-api/internal/store/function_execution_test.go b/services/agents-api/internal/store/function_execution_test.go index 37cce4ec8..9f54808f2 100644 --- a/services/agents-api/internal/store/function_execution_test.go +++ b/services/agents-api/internal/store/function_execution_test.go @@ -4,12 +4,12 @@ import ( "context" "encoding/json" "errors" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "strings" "testing" "time" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/items" "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/store" ) diff --git a/services/agents-api/internal/store/function_worker_test.go b/services/agents-api/internal/store/function_worker_test.go index f7ca8e54d..ec2c9cf62 100644 --- a/services/agents-api/internal/store/function_worker_test.go +++ b/services/agents-api/internal/store/function_worker_test.go @@ -4,12 +4,12 @@ import ( "context" "encoding/json" "errors" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "strings" "testing" "time" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/credentialcrypto" "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/execution" "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/store" diff --git a/services/agents-api/internal/store/local_environment_worker_test.go b/services/agents-api/internal/store/local_environment_worker_test.go index 786e33c16..53b2013a5 100644 --- a/services/agents-api/internal/store/local_environment_worker_test.go +++ b/services/agents-api/internal/store/local_environment_worker_test.go @@ -4,11 +4,11 @@ import ( "context" "encoding/json" "errors" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "testing" "time" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/execution" "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/store" "github.com/google/uuid" diff --git a/services/agents-api/internal/store/public_execution_test.go b/services/agents-api/internal/store/public_execution_test.go index 9355a447b..43f6bfa14 100644 --- a/services/agents-api/internal/store/public_execution_test.go +++ b/services/agents-api/internal/store/public_execution_test.go @@ -3,11 +3,11 @@ package store_test import ( "context" "encoding/json" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "testing" "time" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/execution" "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/store" "github.com/google/uuid" diff --git a/services/agents-api/internal/store/runtime_compute_lifecycle_test.go b/services/agents-api/internal/store/runtime_compute_lifecycle_test.go index 16e8b9e7f..242549d3f 100644 --- a/services/agents-api/internal/store/runtime_compute_lifecycle_test.go +++ b/services/agents-api/internal/store/runtime_compute_lifecycle_test.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "net/http" "net/http/httptest" "strings" @@ -16,6 +15,7 @@ import ( "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/gateway" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/execution" "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/sandbox" "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/store" diff --git a/services/agents-api/internal/store/runtime_initialization_peer_test.go b/services/agents-api/internal/store/runtime_initialization_peer_test.go index 2b0252d1a..0c1a7f7b9 100644 --- a/services/agents-api/internal/store/runtime_initialization_peer_test.go +++ b/services/agents-api/internal/store/runtime_initialization_peer_test.go @@ -5,7 +5,6 @@ import ( "crypto/sha256" "encoding/hex" "errors" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "net/http" "sync/atomic" "testing" @@ -13,6 +12,7 @@ import ( "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/gateway" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/sandbox" "github.com/gorilla/websocket" ) diff --git a/services/agents-api/internal/store/structured_output_dispatch_test.go b/services/agents-api/internal/store/structured_output_dispatch_test.go index 5168724d2..c492d7662 100644 --- a/services/agents-api/internal/store/structured_output_dispatch_test.go +++ b/services/agents-api/internal/store/structured_output_dispatch_test.go @@ -3,11 +3,11 @@ package store_test import ( "context" "encoding/json" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "testing" "time" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/engine" "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/execution" "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/store" diff --git a/services/agents-api/internal/store/worker_lease_loss_test.go b/services/agents-api/internal/store/worker_lease_loss_test.go index 8d530c863..4eb7c4eab 100644 --- a/services/agents-api/internal/store/worker_lease_loss_test.go +++ b/services/agents-api/internal/store/worker_lease_loss_test.go @@ -3,11 +3,11 @@ package store_test import ( "context" "encoding/json" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "testing" "time" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto/prototest" "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/execution" "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/store" ) From 5c6b1bb9107e1067b1fc54b9e9b00e4affa84fdc Mon Sep 17 00:00:00 2001 From: saladday <1203511142@qq.com> Date: Wed, 30 Sep 2026 13:45:00 +0800 Subject: [PATCH 09/12] Declare exercised capabilities in Core and lifecycle fixtures --- .../internal/dispatch/executor_cancel_receipt_test.go | 2 +- apps/parsar-daemon/internal/dispatch/executor_test.go | 4 ++-- apps/parsar-daemon/internal/dispatch/preparation_test.go | 2 +- .../internal/store/archive_cancellation_test.go | 2 +- services/agents-api/internal/store/claude_mcp_test.go | 6 +++--- .../agents-api/internal/store/environment_device_test.go | 6 +++--- .../internal/store/environment_runtime_fixture_test.go | 8 ++++---- .../internal/store/environment_worker_scan_test.go | 2 +- .../agents-api/internal/store/function_worker_test.go | 6 +++--- .../internal/store/local_environment_worker_test.go | 8 ++++---- 10 files changed, 23 insertions(+), 23 deletions(-) diff --git a/apps/parsar-daemon/internal/dispatch/executor_cancel_receipt_test.go b/apps/parsar-daemon/internal/dispatch/executor_cancel_receipt_test.go index 8b1a105b4..8fcc8f042 100644 --- a/apps/parsar-daemon/internal/dispatch/executor_cancel_receipt_test.go +++ b/apps/parsar-daemon/internal/dispatch/executor_cancel_receipt_test.go @@ -127,7 +127,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})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + reg.RegisterKind(proto.SupportedAgentKind{Kind: "reusable", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { return nil, errors.New("legacy factory forbidden") }) reg.RegisterExecutor("reusable", func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { return owner, nil }) diff --git a/apps/parsar-daemon/internal/dispatch/executor_test.go b/apps/parsar-daemon/internal/dispatch/executor_test.go index 337ffefc9..4453a7b50 100644 --- a/apps/parsar-daemon/internal/dispatch/executor_test.go +++ b/apps/parsar-daemon/internal/dispatch/executor_test.go @@ -76,7 +76,7 @@ func executorRouter(t *testing.T, owner *reusableExecutor, idle time.Duration) ( t.Helper() calls := &atomic.Int32{} reg := agent.NewRegistry() - reg.RegisterKind(proto.SupportedAgentKind{Kind: "reusable", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + reg.RegisterKind(proto.SupportedAgentKind{Kind: "reusable", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { return nil, errors.New("ordinary factory is forbidden") }) reg.RegisterExecutor("reusable", func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { @@ -220,7 +220,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})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + registry.RegisterKind(proto.SupportedAgentKind{Kind: "reusable", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { return nil, errors.New("unexpected legacy factory") }) registry.RegisterExecutor("reusable", factory) diff --git a/apps/parsar-daemon/internal/dispatch/preparation_test.go b/apps/parsar-daemon/internal/dispatch/preparation_test.go index da29fdf63..c6a5ddbc5 100644 --- a/apps/parsar-daemon/internal/dispatch/preparation_test.go +++ b/apps/parsar-daemon/internal/dispatch/preparation_test.go @@ -119,7 +119,7 @@ func preparationRequest() proto.ExecutionPreparePayload { func preparationRouter(t *testing.T, sender dispatch.Sender, timeout time.Duration, factory agent.PreparationFactory) *dispatch.Router { t.Helper() reg := agent.NewRegistry() - reg.RegisterKind(proto.SupportedAgentKind{Kind: "prepared", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + reg.RegisterKind(proto.SupportedAgentKind{Kind: "prepared", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported, Permissions: proto.CapabilitySupported, FunctionTools: proto.CapabilitySupported, Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { return nil, errors.New("ordinary Factory must not be used for preparation") }) reg.RegisterPreparation("prepared", true, factory) diff --git a/services/agents-api/internal/store/archive_cancellation_test.go b/services/agents-api/internal/store/archive_cancellation_test.go index 1f68324bd..928c17684 100644 --- a/services/agents-api/internal/store/archive_cancellation_test.go +++ b/services/agents-api/internal/store/archive_cancellation_test.go @@ -98,7 +98,7 @@ func TestArchiveWaitingCancellationReceipts(t *testing.T) { t.Cleanup(func() { conn.Close() }) h := &dispatchHarness{t: t, s: s, tenant: project.TenantID, session: session, conn: conn, registry: registry, d: &execution.Dispatcher{Store: writer, Registry: registry}} capabilities := workerEnvironmentCapabilities() - capabilities.FunctionTools = true + capabilities.FunctionTools = proto.CapabilitySupported h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: capabilities}}}) var peer *gateway.Session for deadline := time.Now().Add(3 * time.Second); ; { diff --git a/services/agents-api/internal/store/claude_mcp_test.go b/services/agents-api/internal/store/claude_mcp_test.go index 360c0e173..41a753022 100644 --- a/services/agents-api/internal/store/claude_mcp_test.go +++ b/services/agents-api/internal/store/claude_mcp_test.go @@ -68,8 +68,8 @@ func TestClaudeMCPWaitsForCapableRuntime(t *testing.T) { t.Fatal("bound an incapable runtime", err) } } - caps.MCPHTTPTools, caps.MCPHTTPBearerAuth = true, authenticated - caps.MCPHTTPRequired = required + caps.MCPHTTPTools, caps.MCPHTTPBearerAuth = proto.CapabilitySupported, proto.CapabilityFromBool(authenticated) + caps.MCPHTTPRequired = proto.CapabilityFromBool(required) h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "claude_sdk", Available: true, Capabilities: caps}}}) var prompt proto.PromptRequestPayload if h.read(testExecutionRequest).DecodePayload(&prompt) != nil || prompt.AgentKind != "claude_sdk" || prompt.MCPHTTPServers == nil || len(*prompt.MCPHTTPServers) != 1 { @@ -107,7 +107,7 @@ func TestClaudeMCPUnsupportedSnapshotRejectedBeforeClaim(t *testing.T) { 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}) if profile == "missing required capability" { - caps.MCPHTTPRequired = false + caps.MCPHTTPRequired = proto.CapabilityUnsupported } h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "claude_sdk", Available: true, Capabilities: caps}}}) deadline := time.Now().Add(3 * time.Second) diff --git a/services/agents-api/internal/store/environment_device_test.go b/services/agents-api/internal/store/environment_device_test.go index 0901946b1..a8b31aede 100644 --- a/services/agents-api/internal/store/environment_device_test.go +++ b/services/agents-api/internal/store/environment_device_test.go @@ -19,9 +19,9 @@ func TestWorkerEnvironmentSelectsCapableDeviceWithoutMovingBinding(t *testing.T) bound := workerEnvironmentReservation(t, h) originalRuntime := h.environments[bound.SessionID] caps := workerEnvironmentCapabilities() - caps.Preparation = missing != "preparation" - caps.LocalEnvironment = missing != "local_environment" - caps.DurableInputReceipts = missing != "durable_input_receipts" + caps.Preparation = proto.CapabilityFromBool(missing != "preparation") + caps.LocalEnvironment = proto.CapabilityFromBool(missing != "local_environment") + caps.DurableInputReceipts = proto.CapabilityFromBool(missing != "durable_input_receipts") awaitFixtureCapabilities(t, originalRuntime, caps) generalFrames := workerFrames(t, h) boundFrames := workerFrames(t, originalRuntime) diff --git a/services/agents-api/internal/store/environment_runtime_fixture_test.go b/services/agents-api/internal/store/environment_runtime_fixture_test.go index 6187bec58..010546b9f 100644 --- a/services/agents-api/internal/store/environment_runtime_fixture_test.go +++ b/services/agents-api/internal/store/environment_runtime_fixture_test.go @@ -117,9 +117,9 @@ func awaitFixtureCapabilities(t *testing.T, h *dispatchHarness, caps proto.Agent return false } info, _, known := peer.AgentKindStatus("codex") - return known && info.Capabilities.Preparation == caps.Preparation && - info.Capabilities.LocalEnvironment == caps.LocalEnvironment && - info.Capabilities.DurableInputReceipts == caps.DurableInputReceipts && - info.Capabilities.WorkspaceOutputExport == caps.WorkspaceOutputExport + 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() }) } diff --git a/services/agents-api/internal/store/environment_worker_scan_test.go b/services/agents-api/internal/store/environment_worker_scan_test.go index bca895242..e2f1bd770 100644 --- a/services/agents-api/internal/store/environment_worker_scan_test.go +++ b/services/agents-api/internal/store/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 = false + caps.Preparation = proto.CapabilityUnsupported awaitFixtureCapabilities(t, runtime, caps) frames := workerFrames(t, h, runtime) diff --git a/services/agents-api/internal/store/function_worker_test.go b/services/agents-api/internal/store/function_worker_test.go index ec2c9cf62..db8e9cc3a 100644 --- a/services/agents-api/internal/store/function_worker_test.go +++ b/services/agents-api/internal/store/function_worker_test.go @@ -54,7 +54,7 @@ func TestWorkerWaitsForToolCapabilities(t *testing.T) { for { peer, _ := h.registry.LookupDevice(h.device.ID) info, _, _ := peer.AgentKindStatus("codex") - if info.Capabilities.Preparation == caps.Preparation && info.Capabilities.DurableInputReceipts == caps.DurableInputReceipts && info.Capabilities.ExecutionControls == caps.ExecutionControls && info.Capabilities.FunctionTools == caps.FunctionTools && info.Capabilities.ToolObservations == caps.ToolObservations && info.Capabilities.MCPHTTPTools == caps.MCPHTTPTools && info.Capabilities.MCPHTTPBearerAuth == caps.MCPHTTPBearerAuth && info.Capabilities.MCPHTTPRequired == caps.MCPHTTPRequired { + 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() { break } if time.Now().After(deadline) { @@ -89,8 +89,8 @@ func TestWorkerWaitsForToolCapabilities(t *testing.T) { t.Fatal("bound an incapable device", err) } } - caps.Preparation, caps.DurableInputReceipts, caps.ExecutionControls, caps.ToolObservations, caps.MCPHTTPTools = true, true, true, true, true - caps.MCPHTTPBearerAuth, caps.FunctionTools, caps.MCPHTTPRequired = true, !isMCP, true + 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 heartbeat() request := h.read(testExecutionRequest) var prompt proto.PromptRequestPayload diff --git a/services/agents-api/internal/store/local_environment_worker_test.go b/services/agents-api/internal/store/local_environment_worker_test.go index 53b2013a5..5b8f7642d 100644 --- a/services/agents-api/internal/store/local_environment_worker_test.go +++ b/services/agents-api/internal/store/local_environment_worker_test.go @@ -27,10 +27,10 @@ func localWorker(t *testing.T, scoped, execute bool) (*dispatchHarness, *executi } caps := prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported, Preparation: proto.CapabilitySupported, WorkspaceReadPreparation: proto.CapabilitySupported}) if execute { - caps.WorkspaceOutputExport = true - caps.Streaming, caps.Steering, caps.DurableTurns, caps.DurableInputReceipts = true, true, true, true - caps.WebSearchControl, caps.TextVerbosity, caps.ExecutionControls = true, true, true - caps.SubagentControl, caps.ToolObservations = true, true + 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 } else { h.d.Options = func(context.Context, store.Session) (map[string]any, error) { t.Error("directory read requested model credentials") From 9905ac11cc6a127f9ce856700f98880e2a55d713 Mon Sep 17 00:00:00 2001 From: saladday <1203511142@qq.com> Date: Wed, 30 Sep 2026 13:46:10 +0800 Subject: [PATCH 10/12] Preserve unknown Harness classification during availability admission --- apps/parsar-daemon/internal/dispatch/prompt.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/parsar-daemon/internal/dispatch/prompt.go b/apps/parsar-daemon/internal/dispatch/prompt.go index b0ced933a..56bedf83d 100644 --- a/apps/parsar-daemon/internal/dispatch/prompt.go +++ b/apps/parsar-daemon/internal/dispatch/prompt.go @@ -43,7 +43,10 @@ func (r *Router) handlePromptRequest(callerCtx context.Context, env proto.Envelo } caps, available := r.availableCapabilities(req.AgentKind) if !available { - err := errors.New("engine is unavailable on this runtime") + _, err := r.registry.Resolve(req.AgentKind) + if err == nil { + err = errors.New("engine is unavailable on this runtime") + } r.emitTerminalError(callerCtx, runID, err.Error()) return err } From 65cfa71be58253294376bab518b41281ccbe069e Mon Sep 17 00:00:00 2001 From: saladday <1203511142@qq.com> Date: Wed, 30 Sep 2026 13:47:31 +0800 Subject: [PATCH 11/12] Run declaration coverage in the focused gate and parameterize live provider selection --- Makefile | 2 +- .../agent/claudesdk/executor_live_linux_test.go | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 7396e4bdf..28faf0b73 100644 --- a/Makefile +++ b/Makefile @@ -56,7 +56,7 @@ check-go: check-runtime-contract: go test ./internal/agentdaemon/proto ./internal/agentdaemon/gateway ./apps/parsar-daemon/internal/transport ./apps/parsar-daemon/internal/dispatch ./apps/parsar-daemon/internal/contracttest -count=1 go test ./services/agents-api/internal/execution -run '^TestRuntimeProtocol' -count=1 - go test ./apps/parsar-daemon/internal/agent/... -run '^TestSharedTextLifecycle$$' -count=1 + go test ./apps/parsar-daemon/internal/agent/... -run '^(TestSharedTextLifecycle|TestPublicHarnessContractDeclarations|TestRegistryRejectsEveryOmittedCapabilityBeforeReplacement|TestUnsupportedExtensionsHaveNoNativeEffects)$$' -count=1 build-daemon: @set -e; output="$${OAC_DEV_HOME:-$$HOME/.oac}/build/daemon"; \ diff --git a/apps/parsar-daemon/internal/agent/claudesdk/executor_live_linux_test.go b/apps/parsar-daemon/internal/agent/claudesdk/executor_live_linux_test.go index a2ac5f536..4d3a2aec2 100644 --- a/apps/parsar-daemon/internal/agent/claudesdk/executor_live_linux_test.go +++ b/apps/parsar-daemon/internal/agent/claudesdk/executor_live_linux_test.go @@ -26,6 +26,10 @@ func TestLiveClaudeExecutorReuseAndCancel(t *testing.T) { if entry == "" || keyPath == "" || proof == "" { t.Skip("explicit installed runtime, private key file and proof directory required") } + endpoint, model := os.Getenv("OAC_TEST_CLAUDE_EXECUTOR_BASE_URL"), os.Getenv("OAC_TEST_CLAUDE_EXECUTOR_MODEL") + if endpoint == "" || model == "" { + t.Fatal("explicit provider endpoint and model required") + } if !filepath.IsAbs(proof) { t.Fatal("absolute proof directory required") } @@ -38,8 +42,8 @@ func TestLiveClaudeExecutorReuseAndCancel(t *testing.T) { } t.Setenv("OAC_RUNTIME_HOME", proof) config := Config{Node: os.Getenv("OAC_TEST_CLAUDE_EXECUTOR_NODE"), Entrypoint: entry, StateDir: filepath.Join(proof, "state"), Env: []string{ - "ANTHROPIC_BASE_URL=https://api.moonshot.cn/anthropic", "ANTHROPIC_AUTH_TOKEN=" + strings.TrimSpace(string(key)), "ANTHROPIC_API_KEY=", "CLAUDE_CODE_OAUTH_TOKEN=", "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1", - "ANTHROPIC_DEFAULT_SONNET_MODEL=kimi-k3", "ANTHROPIC_DEFAULT_OPUS_MODEL=kimi-k3", "ANTHROPIC_DEFAULT_HAIKU_MODEL=kimi-k3", + "ANTHROPIC_BASE_URL=" + endpoint, "ANTHROPIC_AUTH_TOKEN=" + strings.TrimSpace(string(key)), "ANTHROPIC_API_KEY=", "CLAUDE_CODE_OAUTH_TOKEN=", "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1", + "ANTHROPIC_DEFAULT_SONNET_MODEL=" + model, "ANTHROPIC_DEFAULT_OPUS_MODEL=" + model, "ANTHROPIC_DEFAULT_HAIKU_MODEL=" + model, }} if proxy := os.Getenv("OAC_TEST_CLAUDE_EXECUTOR_HTTP_PROXY"); proxy != "" { config.Env = append(config.Env, "HTTP_PROXY="+proxy, "HTTPS_PROXY="+proxy, "http_proxy="+proxy, "https_proxy="+proxy) @@ -76,7 +80,7 @@ func TestLiveClaudeExecutorReuseAndCancel(t *testing.T) { _ = os.WriteFile(filepath.Join(proof, "executor-evidence.json"), raw, 0600) } defer persist() - request := proto.PromptRequestPayload{StrictResume: true, DisableExecutionEnvironment: true, DisableSubagents: true, ObserveMessages: true, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}, AgentOptions: map[string]any{"model": "kimi-k3", "system_prompt": "Follow requested formats briefly. Remember the exact verification marker across the conversation. Use no tools."}} + request := proto.PromptRequestPayload{StrictResume: true, DisableExecutionEnvironment: true, DisableSubagents: true, ObserveMessages: true, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}, AgentOptions: map[string]any{"model": model, "system_prompt": "Follow requested formats briefly. Remember the exact verification marker across the conversation. Use no tools."}} factory := NewExecutorFactory(config) prepared := time.Now() owner, err := factory(ctx, request) From 2496ba0770659fc4f26cbf7b4ec1bfb230f07933 Mon Sep 17 00:00:00 2001 From: saladday <1203511142@qq.com> Date: Wed, 30 Sep 2026 14:00:53 +0800 Subject: [PATCH 12/12] Mutate the onboarding peer wire declaration instead of its storage projection --- .../internal/store/harness_onboarding_test.go | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/services/agents-api/internal/store/harness_onboarding_test.go b/services/agents-api/internal/store/harness_onboarding_test.go index c3655ddde..543387313 100644 --- a/services/agents-api/internal/store/harness_onboarding_test.go +++ b/services/agents-api/internal/store/harness_onboarding_test.go @@ -42,7 +42,7 @@ func TestThirdHarnessPublicOnboarding(t *testing.T) { h.d.Policy = policy // The fixture only supplies an adapter and registration to the real daemon router. // Core sees its ordinary authenticated gateway connection and neutral frames. - started, write := startOnboardingPeer(t, h) + started, write, declaration := startOnboardingPeer(t, h) ctx, cancel := context.WithCancel(t.Context()) defer cancel() worker, err := execution.StartWorker(ctx, h.d) @@ -126,14 +126,10 @@ func TestThirdHarnessPublicOnboarding(t *testing.T) { waitTurn(t, h, next.RunID, store.TurnCancelled) // A missing mandatory receipt capability must prevent claiming queued work. peer, _ := h.registry.LookupDevice(h.device.ID) - info, _, _ := peer.AgentKindStatus("fixture_harness") - info.Capabilities.DurableInputReceipts = false + // Mutate the actual wire declaration, not its lossy persisted boolean projection. + changed := declaration + changed.Capabilities.DurableInputReceipts = proto.CapabilityUnsupported // A separate unbound Session is used, without changing public handler behavior. - wire, _ := json.Marshal(info) - var changed proto.SupportedAgentKind - if err := json.Unmarshal(wire, &changed); err != nil { - t.Fatal(err) - } update, _ := proto.NewEnvelope(proto.TypeHeartbeat, "", proto.HeartbeatPayload{SupportedAgentKinds: []proto.SupportedAgentKind{changed}}) if err := write(update); err != nil { t.Fatal(err) @@ -174,7 +170,7 @@ func awaitOnboardingPrompt(t *testing.T, c <-chan proto.PromptRequestPayload) pr } } -func startOnboardingPeer(t *testing.T, h *dispatchHarness) (<-chan proto.PromptRequestPayload, func(proto.Envelope) error) { +func startOnboardingPeer(t *testing.T, h *dispatchHarness) (<-chan proto.PromptRequestPayload, func(proto.Envelope) error, proto.SupportedAgentKind) { t.Helper() root, err := filepath.Abs("../../../..") if err != nil { @@ -201,6 +197,7 @@ func startOnboardingPeer(t *testing.T, h *dispatchHarness) (<-chan proto.PromptR t.Fatal(err) } started := make(chan proto.PromptRequestPayload, 4) + declarations := make(chan proto.SupportedAgentKind, 1) up := make(chan error, 1) down := make(chan error, 1) var writeMu sync.Mutex @@ -213,6 +210,21 @@ func startOnboardingPeer(t *testing.T, h *dispatchHarness) (<-chan proto.PromptR up <- err return } + if e.Type == proto.TypeHeartbeat { + var heartbeat proto.HeartbeatPayload + if err := e.DecodePayload(&heartbeat); err != nil { + up <- err + return + } + for _, info := range heartbeat.SupportedAgentKinds { + if info.Kind == "fixture_harness" { + select { + case declarations <- info: + default: + } + } + } + } if err := write(e); err != nil { up <- err return @@ -290,7 +302,7 @@ func startOnboardingPeer(t *testing.T, h *dispatchHarness) (<-chan proto.PromptR if err == nil { _, found, known := peer.AgentKindStatus("fixture_harness") if found && known { - return started, write + return started, write, <-declarations } } if time.Now().After(deadline) {