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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ This guide owns how to work in the repository: documentation ownership, the repo
| Harness qualification and acceptance | [Harness integration](contracts/agents-api/harnesses.md) |
| Harness service qualification declarations and registration | [Explicit service qualification](contracts/agents-api/harness-onboarding.md#explicit-service-qualification) and `services/core/internal/engine/profile.go` |
| Harness selection and Agent defaults | [Harness selection](contracts/agents-api/harness-selection.md) |
| Provider registration validation | [Sandbox Provider guide](docs/sandbox-provider.md#registration-validation) |
| Provider selection, sandbox deployment and E2B setup | [Sandbox deployment](contracts/agents-api/sandbox-deployment.md) |
| Hosted sandbox nodes | [Nodes guide](docs/getting-started/nodes.md) and [sandbox deployment contract](contracts/agents-api/sandbox-deployment.md) |
| Claude private bridge and Runtime artifact | [Claude SDK adapter](packages/claude-sdk-adapter/README.md) |
Expand Down
13 changes: 13 additions & 0 deletions docs/sandbox-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,19 @@ mode, defaults, the adapter-owned operation declaration, and local or direct con
direct adapters. Neither allocates compute. There is no init-time registration or
runtime plugin loading.

### Registration validation

`providers.ValidateRegistration` is the single wiring check. Lookup, constructor binding and the installer projection use it before configuration callbacks or constructors can run. Unknown provider names remain invalid input; malformed registered adapters return a safe `providercontract.ErrContract`, without including submitted configuration or native diagnostics.

- A `nodes` registration requires only `BuildLocal`; a `direct` registration requires only `BuildDirect`. Missing, mixed or unknown modes are rejected.
- Specification/resource validators, the configuration adapter and the complete operation declaration are mandatory. An incomplete registration cannot publish a partial installer projection.
- The Runtime input policy must either accept the pinned Runtime or give the adapter's fixed reason for rejecting it; it cannot do both.
- Current checkpoint suspension requires node mode and positive idle/retention defaults that fit Runtime durations. The two durations are independent. Providers without checkpoint support must not configure suspension defaults.

The configuration adapter must be non-nil, including its concrete value. Each `ConfigurationRequirements` field needs an explicit valid decision: `Credential` and `PublicOrigin` use `Required` or `NotRequired`; `Discovery` uses the shared supported/Unsupported declaration with its safe reason. `ConfigurationDiscoverer` must be implemented even when discovery is unsupported. New requirement fields or discovery methods require an explicit validation update; they cannot inherit an existing decision. Configuration discovery is distinct from resource selection discovery. Requiring a credential does not itself promise the resource operation `VerifyCredential`.

These checks establish registration completeness, not the correctness of native SDK behavior. Constructor and adapter contract tests still apply.

For a new implementation:

1. Implement the operation contracts above in the adapter package and add native contract tests.
Expand Down
37 changes: 37 additions & 0 deletions services/core/cmd/server/managed_setup_preflight_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"testing"
"time"

"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/e2b"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store"
Expand Down Expand Up @@ -148,3 +149,39 @@ func TestInitialE2BPublicTemplateOutsideTeamIsRejected(t *testing.T) {
t.Fatal("public readability accepted as team ownership", err)
}
}

func TestManagedSetupRejectsUnknownRegistrationBeforePreparationOrRouting(t *testing.T) {
// No store or node hub is available: rejection must precede any use of them.
s := &managedSetup{installationID: "installation", publicURL: "http://127.0.0.1"}
setup := store.SandboxSetup{InstallationID: "installation", Provider: "missing-registration"}
for _, call := range []struct {
name string
run func() (execution.PreparedRuntimeDeployment, error)
}{
{"prepare", func() (execution.PreparedRuntimeDeployment, error) {
return s.prepare(t.Context(), setup)
}},
{"route", func() (execution.PreparedRuntimeDeployment, error) {
return s.routeGenerations(execution.PreparedRuntimeDeployment{}, setup)
}},
} {
t.Run(call.name, func(t *testing.T) {
candidate, err := call.run()
if !errors.Is(err, sandbox.ErrInvalid) || candidate.Config != nil || s.selected.Load() != nil {
t.Fatalf("invalid registration reached preparation or publication: %v", err)
}
})
}
}

func TestManagedSetupRoutesProviderWithoutCredentialRequirement(t *testing.T) {
s := &managedSetup{}
config := &execution.RuntimeProvider{ProviderKind: "docker"}
candidate, err := s.routeGenerations(
execution.PreparedRuntimeDeployment{Config: config},
store.SandboxSetup{Provider: "docker"},
)
if err != nil || candidate.Config != config || candidate.FenceCredential != nil || candidate.VerifyCredential != nil {
t.Fatalf("explicit no-credential provider required credential routing: %v", err)
}
}
6 changes: 5 additions & 1 deletion services/core/cmd/specification-contract/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import (
func main() {
write := flag.Bool("write", false, "update deploy/install/node_spec.py from the repository root")
flag.Parse()
projection := providers.PythonDeploymentContract()
projection, err := providers.PythonDeploymentContract()
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
if !*write {
fmt.Println(projection)
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ func TestInstallerDeploymentProjectionIsCurrent(t *testing.T) {
if err != nil {
t.Fatal(err)
}
expected := PythonDeploymentContract()
expected, err := PythonDeploymentContract()
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(raw), expected) {
t.Fatal("node_spec.py contract is stale; regenerate with go run ./services/core/cmd/specification-contract -write")
}
Expand Down
3 changes: 3 additions & 0 deletions services/core/internal/sandbox/providers/operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import (

// ValidateBinding catches construction that disagrees with its registration.
func ValidateBinding(adapter Adapter, provider sandbox.SandboxProvider) error {
if err := ValidateRegistration(adapter); err != nil {
return err
}
if err := sandbox.ValidateProvider(provider); err != nil {
return err
}
Expand Down
3 changes: 1 addition & 2 deletions services/core/internal/sandbox/providers/operations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"errors"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/providercontract"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/docker"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/e2b"
"testing"
)

Expand All @@ -16,7 +15,7 @@ func TestRegistrationRejectsMissingAndMismatchedDeclarations(t *testing.T) {
}
}
delete(adapters, "invalid-contract-fixture")
if err := ValidateBinding(Adapter{Operations: e2b.Operations}, &docker.Provider{}); !errors.Is(err, providercontract.ErrContract) {
if err := ValidateBinding(adapters["e2b"], &docker.Provider{}); !errors.Is(err, providercontract.ErrContract) {
t.Fatal("registration differs from instance", err)
}
}
60 changes: 60 additions & 0 deletions services/core/internal/sandbox/providers/registration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package providers

import (
"fmt"
"time"

"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/providercontract"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox"
)

// ValidateRegistration checks wiring before configuration parsing or construction.
// Only configuration requirements and operation declarations are read.
func ValidateRegistration(a Adapter) error {
invalid := func(field string) error {
return fmt.Errorf("%w: invalid registration %s", providercontract.ErrContract, field)
}
switch a.Mode {
case "nodes":
if a.BuildLocal == nil || a.BuildDirect != nil {
return invalid("node constructor")
}
case "direct":
if a.BuildDirect == nil || a.BuildLocal != nil {
return invalid("direct constructor")
}
default:
return invalid("mode")
}
if a.ValidateSpecification == nil {
return invalid("specification validator")
}
if a.ValidateResources == nil {
return invalid("resource validator")
}
if err := validateConfigurationAdapter(a.Configuration); err != nil {
return err
}
if a.Policy.Runtime == (a.Policy.RuntimeError != "") {
return invalid("Runtime input policy")
}
if a.Operations == nil {
return invalid("operation declaration")
}
operations := a.Operations()
if err := sandbox.ValidateOperations(operations); err != nil {
return err
}
// The current common lifecycle admits checkpoint suspension only on nodes,
// and creates its policy whenever checkpoint support is declared.
if operations["Initial"].State == providercontract.Supported {
const maximumSeconds = int64((1<<63 - 1) / time.Second)
if a.Mode != "nodes" || a.IdleSeconds < 1 || a.RetentionSeconds < 1 ||
a.IdleSeconds > maximumSeconds || a.RetentionSeconds > maximumSeconds {
return invalid("checkpoint policy")
}
} else if a.IdleSeconds != 0 || a.RetentionSeconds != 0 {
return invalid("non-checkpoint policy")
}
return nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package providers

import (
"errors"
"fmt"
"reflect"

"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/providercontract"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox"
)

func configurationRegistrationError() error {
return fmt.Errorf("%w: invalid configuration registration", providercontract.ErrContract)
}

func validateConfigurationAdapter(configuration sandbox.ConfigurationAdapter) error {
if configuration == nil {
return configurationRegistrationError()
}
value := reflect.ValueOf(configuration)
switch value.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
if value.IsNil() {
return configurationRegistrationError()
}
}
discovery := reflect.TypeFor[sandbox.ConfigurationDiscoverer]()
if !value.Type().Implements(discovery) {
return configurationRegistrationError()
}
if err := validateConfigurationDiscoveryInterface(discovery); err != nil {
return err
}
return validateConfigurationRequirements(reflect.ValueOf(configuration.Requirements()))
}

// Every discovery method needs its own authored requirement. A future method
// cannot inherit the existing Discovery decision merely by being implemented.
func validateConfigurationDiscoveryInterface(discovery reflect.Type) error {
if discovery.NumMethod() != 1 {
return configurationRegistrationError()
}
if _, exists := discovery.MethodByName("DiscoverConfiguration"); !exists {
return configurationRegistrationError()
}
return nil
}

// Check field names as well as values so new requirements cannot bypass the
// gate. This owns only configuration requirements, not resource operations.
func validateConfigurationRequirements(value reflect.Value) error {
if value.Kind() != reflect.Struct || value.NumField() != 3 {
return configurationRegistrationError()
}
for i := 0; i < value.NumField(); i++ {
switch value.Type().Field(i).Name {
case "Credential", "PublicOrigin":
requirement, ok := value.Field(i).Interface().(sandbox.Requirement)
if !ok || (requirement != sandbox.Required && requirement != sandbox.NotRequired) {
return configurationRegistrationError()
}
case "Discovery":
support, ok := value.Field(i).Interface().(providercontract.Support)
if !ok {
return configurationRegistrationError()
}
if err := support.Check("DiscoverConfiguration"); err != nil && !errors.Is(err, providercontract.ErrUnsupported) {
return configurationRegistrationError()
}
default:
return configurationRegistrationError()
}
}
return nil
}
Loading