Skip to content
Closed
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
8 changes: 8 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -3797,3 +3797,11 @@ code. Include only safe Core-owned facts; never pass submitted values, secrets,
native text or provider bodies. Invalid/empty details are omitted. Preserve the
public and machine error serializers, observer callbacks and streaming interfaces.
The Core client ignores malformed optional details and never retries a mutation.

Core operation validators preserve the original error text, sentinel identity,
and validation precedence. Package-owned typed errors carry fixed field metadata;
only the marked Core error mapper translates it to operation codes and safe
bounds/catalog details. Preserve Project/key rune limits and node byte limits
separately. Keep sandbox validation metadata through its existing store wrapper
without changing transaction or provider authority. Public Session provider
validation remains byte-compatible; cover it with handler-level golden responses.
42 changes: 39 additions & 3 deletions contracts/agents-api/core-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ strings, finite numbers, booleans, null or arrays of strings (including empty
arrays). It contains documented Core-owned facts, never submitted names, URLs,
keys, echoed request values, native error text or provider response bodies. Each
operation that adds details must document its exact keys alongside its error
code. This foundation does not add operation-specific validators, diagnostics
endpoints or new details to existing operations.
code. Operation-specific validation details are listed below; this contract does not
add diagnostics endpoints.

The typed Go `CoreErrorDetails` values have string, number, boolean, null and
string-array constructors. `writeCoreError` emits them only through the marked
Expand Down Expand Up @@ -57,7 +57,7 @@ path checks and the no-retry rule are unchanged.
Existing operation-specific codes remain documented in the [administrator
contract](admin-api.md), [sandbox deployment contract](sandbox-deployment.md),
[executor credential contract](environment-executor-credentials.md) and related
resource contracts. This change does not reclassify their validation failures.
resource contracts. The following validators refine Core operation failures only.

## E2B online changes

Expand All @@ -75,3 +75,39 @@ Missing or unsettled Create receipts are uncertainty, never evidence of a differ
team or released compute. The typed client projects these codes to fixed local
messages. It also projects `409 sandbox_configuration_error` to fixed public-URL
guidance, even when a PUT omitted its key; arbitrary upstream text is never echoed.

## Operation validation

All entries below return HTTP 400 with type `invalid_request_error`. Missing,
malformed or incorrectly typed model-provider bundles use `invalid_model_provider`
before field validation. JSON body parsing retains its existing errors. Other
malformed administration requests retain `invalid_request`.

| Code | Param | Details | Meaning |
| --- | --- | --- | --- |
| `invalid_name` | `name` | `max_length`: 128 for Projects/nodes, 80 for Project keys | Name failed the resource's existing validator |
| `invalid_node_capacity` | `max_active` or `max_retained` | `min`: 1, `max`: 1000000 | Capacity is invalid; retained capacity must also be at least active capacity |
| `invalid_model_provider` | null | omitted | A complete model-provider bundle is required |
| `model_provider_base_url_invalid` | `base_url` | omitted | Requires HTTPS without credentials, query or fragment |
| `model_provider_protocol_unsupported` | `protocol` | `harness` and `allowed_protocols`, from the build's adapter catalog | Protocol is unknown or unsupported by the selected harness |
| `model_provider_api_key_invalid` | `api_key` | `max_length`: 16384 | Key is empty, too long or contains a prohibited character |
| `model_provider_token_limits_invalid` | `context_window` or `max_output_tokens` | omitted | Limits are invalid or required positive limits are missing |
| `invalid_sandbox_configuration` | `resources.cpus` | `min`: 1, `max`: 255 | CPU count is outside the supported bounds |
| `invalid_sandbox_configuration` | `resources.memory_mib` | `min`: 512, `max`: 1048576 | Memory is outside the supported bounds |
| `invalid_sandbox_configuration` | `resources.root_disk_mib` or `resources.environment_disk_mib` | `min`: 1024 for microsandbox; `min`: 0, `max`: 0 for Docker/E2B | Disk capacity is missing or unsupported by the provider |
| `invalid_sandbox_configuration` | `runtime` | omitted | Runtime release is missing, mutable, invalid or prohibited for E2B |

Bounds describe validation constants, never submitted values. Node names retain
their existing byte limit and character rules; Project/key names retain their
trimmed Unicode character limit and control-character rules. No name rules are
widened or unified. Numeric checks retain their existing order, including
provider-dependent retained capacity validation inside the existing transaction.
Model-provider checks retain URL, protocol, key, general limits, harness protocol,
then required harness limits precedence. Resource checks retain CPU, memory, disk,
then Runtime precedence.

Shared validators preserve their original error strings and sentinel identity.
Only the marked Core router maps their typed field metadata to this catalog;
public `/v1` and machine routes retain their previous complete error bodies.
Unknown sandbox providers retain the existing untyped error. E2B provider errors
above retain their fixed redaction contract with no echoed template or key.
37 changes: 30 additions & 7 deletions contracts/agents-api/v1/model_execution.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
package v1

import (
"errors"
"strings"

"github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig"
"github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig/builtin"
)

// ModelProviderError preserves the shared validation message while allowing Core
// administration to identify a field. It contains no submitted values.
type ModelProviderError struct {
Code, Param string
message string
}

func (e *ModelProviderError) Error() string { return e.message }

// SessionExecutionInput is a write-only execution extension, not a provider resource.
type SessionExecutionInput struct {
ModelProvider *ModelProviderInput `json:"model_provider,omitempty"`
Expand All @@ -27,19 +35,23 @@ func (p *ModelProviderInput) Validate() error {

func (p *ModelProviderInput) validate(registry harnessconfig.Registry) error {
if p == nil {
return errors.New("model_provider is required")
return &ModelProviderError{Code: "invalid_model_provider", Param: "", message: "model_provider is required"}
}
if !validModelProviderBaseURL(p.BaseURL) {
return errors.New("model provider requires an HTTPS base_url without credentials, query or fragment")
return &ModelProviderError{Code: "model_provider_base_url_invalid", Param: "base_url", message: "model provider requires an HTTPS base_url without credentials, query or fragment"}
}
if !registry.SupportsProtocol(p.Protocol) {
return errors.New("unsupported model provider protocol")
return &ModelProviderError{Code: "model_provider_protocol_unsupported", Param: "protocol", message: "unsupported model provider protocol"}
}
if strings.TrimSpace(p.APIKey) == "" || len(p.APIKey) > 16384 || strings.ContainsAny(p.APIKey, "\x00\r\n") {
return errors.New("invalid model provider API key")
return &ModelProviderError{Code: "model_provider_api_key_invalid", Param: "api_key", message: "invalid model provider API key"}
}
if p.ContextWindow < 0 || p.MaxOutputTokens < 0 || (p.MaxOutputTokens > p.ContextWindow) {
return errors.New("invalid model token limits")
param := "max_output_tokens"
if p.ContextWindow < 0 {
param = "context_window"
}
return &ModelProviderError{Code: "model_provider_token_limits_invalid", Param: param, message: "invalid model token limits"}
}
return nil
}
Expand All @@ -54,7 +66,18 @@ func (p *ModelProviderInput) ValidateHarnessWithRegistry(harness string, registr
if err := p.validate(registry); err != nil {
return err
}
return p.SafeView().ValidateHarnessWithRegistry(harness, registry)
configuration, _ := registry.Lookup(harness)
if err := configuration.ValidateProtocol(p.Protocol); err != nil {
return &ModelProviderError{Code: "model_provider_protocol_unsupported", Param: "protocol", message: err.Error()}
}
if err := configuration.Validate(p.Protocol, p.ContextWindow, p.MaxOutputTokens); err != nil {
param := "max_output_tokens"
if p.ContextWindow <= 0 {
param = "context_window"
}
return &ModelProviderError{Code: "model_provider_token_limits_invalid", Param: param, message: err.Error()}
}
return nil
}

func ValidateModelProtocol(protocol, harness string) error {
Expand Down
45 changes: 45 additions & 0 deletions contracts/agents-api/v1/model_provider_error_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package v1

import (
"errors"
"testing"
)

func TestModelProviderErrorMessagesAndPrecedence(t *testing.T) {
valid := ModelProviderInput{Protocol: "responses", BaseURL: "https://example.test", APIKey: "private-key"}
for _, tc := range []struct {
name, harness, code, param, message string
change func(*ModelProviderInput)
}{
{"url first", "codex", "model_provider_base_url_invalid", "base_url", "model provider requires an HTTPS base_url without credentials, query or fragment", func(p *ModelProviderInput) {
p.BaseURL = "http://private.example"
p.Protocol = "private"
p.APIKey = ""
}},
{"protocol before key", "codex", "model_provider_protocol_unsupported", "protocol", "unsupported model provider protocol", func(p *ModelProviderInput) { p.Protocol = "private"; p.APIKey = "" }},
{"key before harness", "claude_sdk", "model_provider_api_key_invalid", "api_key", "invalid model provider API key", func(p *ModelProviderInput) { p.APIKey = "" }},
{"context before output", "codex", "model_provider_token_limits_invalid", "context_window", "invalid model token limits", func(p *ModelProviderInput) { p.ContextWindow = -1; p.MaxOutputTokens = -2 }},
{"output", "codex", "model_provider_token_limits_invalid", "max_output_tokens", "invalid model token limits", func(p *ModelProviderInput) { p.MaxOutputTokens = 1 }},
{"harness protocol", "claude_sdk", "model_provider_protocol_unsupported", "protocol", "selected harness does not support this model provider protocol", func(p *ModelProviderInput) {}},
{"required context", "mcode", "model_provider_token_limits_invalid", "context_window", "selected harness requires positive model context_window and max_output_tokens", func(p *ModelProviderInput) { p.Protocol = "anthropic" }},
{"required output", "mcode", "model_provider_token_limits_invalid", "max_output_tokens", "selected harness requires positive model context_window and max_output_tokens", func(p *ModelProviderInput) { p.Protocol = "anthropic"; p.ContextWindow = 10 }},
} {
t.Run(tc.name, func(t *testing.T) {
p := valid
tc.change(&p)
err := p.ValidateHarness(tc.harness)
var field *ModelProviderError
if !errors.As(err, &field) || field.Code != tc.code || field.Param != tc.param || err.Error() != tc.message {
t.Fatalf("wrong typed error: %#v", err)
}
})
}
var missing *ModelProviderInput
var field *ModelProviderError
if err := missing.Validate(); !errors.As(err, &field) || field.Code != "invalid_model_provider" || field.Param != "" || err.Error() != "model_provider is required" {
t.Fatal(err)
}
if err := valid.ValidateHarness("codex"); err != nil {
t.Fatal(err)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package api

import (
"context"
"encoding/json"
"net/http"
"reflect"
"strings"
"testing"

v1 "github.com/MiniMax-AI-Dev/parsar/contracts/agents-api/v1"
"github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/store"
)

type coreProviderValidationStore struct {
ResourceStore
writes int
}

func (s *coreProviderValidationStore) ListDeploymentModelProviders(context.Context) ([]store.DeploymentModelProvider, error) {
return nil, nil
}
func (s *coreProviderValidationStore) DeleteDeploymentModelProvider(context.Context, string) error {
return nil
}
func (s *coreProviderValidationStore) SetDeploymentModelProvider(context.Context, string, v1.ModelProviderInput) (store.DeploymentModelProvider, error) {
s.writes++
return store.DeploymentModelProvider{}, nil
}

func TestCoreModelProviderValidationFields(t *testing.T) {
s := &coreProviderValidationStore{}
h, _, _ := adminTestHandler(t, func(h *Handler) { h.store = s })
for _, tc := range []struct {
name, harness, body, code, param string
details map[string]any
}{
{"bundle", "codex", `{"api_key":"private-key"}`, "invalid_model_provider", "", nil},
{"negative shape", "codex", `{"protocol":"responses","base_url":"http://private-url","api_key":"private-key","context_window":-1}`, "invalid_model_provider", "", nil},
{"url first", "codex", `{"protocol":"private-protocol","base_url":"http://private-url","api_key":"private-key"}`, "model_provider_base_url_invalid", "base_url", nil},
{"protocol", "codex", `{"protocol":"private-protocol","base_url":"https://example.test","api_key":"private-key"}`, "model_provider_protocol_unsupported", "protocol", map[string]any{"harness": "codex", "allowed_protocols": []any{"responses"}}},
{"harness protocol", "claude_sdk", `{"protocol":"responses","base_url":"https://example.test","api_key":"private-key"}`, "model_provider_protocol_unsupported", "protocol", map[string]any{"harness": "claude_sdk", "allowed_protocols": []any{"anthropic"}}},
{"key", "codex", `{"protocol":"responses","base_url":"https://example.test","api_key":"private-key\n"}`, "model_provider_api_key_invalid", "api_key", map[string]any{"max_length": float64(16384)}},
{"output", "codex", `{"protocol":"responses","base_url":"https://example.test","api_key":"private-key","context_window":1,"max_output_tokens":2}`, "model_provider_token_limits_invalid", "max_output_tokens", nil},
{"context required", "mcode", `{"protocol":"anthropic","base_url":"https://example.test","api_key":"private-key"}`, "model_provider_token_limits_invalid", "context_window", nil},
{"output required", "mcode", `{"protocol":"anthropic","base_url":"https://example.test","api_key":"private-key","context_window":2}`, "model_provider_token_limits_invalid", "max_output_tokens", nil},
} {
t.Run(tc.name, func(t *testing.T) {
out := projectKeyHTTP(h, http.MethodPut, "/core/v1/harnesses/"+tc.harness+"/model-provider", "admin", tc.body)
var body struct {
Error struct {
Code string
Param *string
Details map[string]any
}
}
if err := json.Unmarshal(out.Body.Bytes(), &body); err != nil {
t.Fatal(err)
}
param := ""
if body.Error.Param != nil {
param = *body.Error.Param
}
if out.Code != 400 || body.Error.Code != tc.code || param != tc.param || !reflect.DeepEqual(body.Error.Details, tc.details) || strings.Contains(out.Body.String(), "private-") {
t.Fatalf("unexpected diagnostics: %d %s", out.Code, out.Body)
}
})
}
if s.writes != 0 {
t.Fatal("invalid input reached storage")
}
for _, tc := range []struct {
harness, token string
status int
}{{"codex", "", 401}, {"private-harness", "admin", 404}} {
out := projectKeyHTTP(h, http.MethodPut, "/core/v1/harnesses/"+tc.harness+"/model-provider", tc.token, `{}`)
if out.Code != tc.status || strings.Contains(out.Body.String(), "private-") {
t.Fatal("admission order changed", out.Code, out.Body)
}
}
}
90 changes: 90 additions & 0 deletions services/agents-api/internal/api/core_store_validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package api

import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"

"github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/device"
"github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/sandbox"
"github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/store"
)

func TestCoreStoreValidationFieldsAndPublicFallback(t *testing.T) {
s := &store.Store{}
_, nameErr := s.CreateProject(context.Background(), managementProjectID, strings.Repeat("private-name", 20))
upperCapacityErr := s.UpdateRuntimeNode(context.Background(), managementProjectID, store.RuntimeNodeUpdate{Name: "node", MaxActive: 1000001, MaxRetained: 8})
capacityErr := s.UpdateRuntimeNode(context.Background(), managementProjectID, store.RuntimeNodeUpdate{Name: "node", MaxActive: 0})
_, resourceErr := store.SandboxSetupForSelection(managementProjectID, store.SandboxDeploymentSetupRequest{Provider: "docker", DeploymentSpec: sandbox.DeploymentSpec{}})
_, runtimeErr := store.SandboxSetupForSelection(managementProjectID, store.SandboxDeploymentSetupRequest{Provider: "docker", DeploymentSpec: sandbox.DeploymentSpec{Resources: sandbox.Resources{CPUs: 1, MemoryMiB: 512}}})
for _, tc := range []struct {
err error
code, param string
details map[string]any
publicMessage, publicCode string
}{
{nameErr, "invalid_name", "name", map[string]any{"max_length": float64(128)}, "Invalid resource identifier or request limits.", "invalid_request"},
{upperCapacityErr, "invalid_node_capacity", "max_active", map[string]any{"min": float64(1), "max": float64(1000000)}, "Invalid resource identifier or request limits.", "invalid_request"},
{capacityErr, "invalid_node_capacity", "max_active", map[string]any{"min": float64(1), "max": float64(1000000)}, "Invalid resource identifier or request limits.", "invalid_request"},
{resourceErr, "invalid_sandbox_configuration", "resources.cpus", map[string]any{"min": float64(1), "max": float64(255)}, "invalid sandbox configuration: cpus must be 1..255 and memory_mib must be 512..1048576", "invalid_sandbox_configuration"},
{runtimeErr, "invalid_sandbox_configuration", "runtime", nil, "invalid sandbox configuration: managed nodes require a pinned Runtime release", "invalid_sandbox_configuration"},
} {
if tc.err == nil {
t.Fatal("missing validator error")
}
for _, core := range []bool{false, true} {
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { writeStoreError(w, r, fmt.Errorf("wrapped: %w", tc.err)) }))
if core {
handler = coreErrorResponses(handler)
}
out := httptest.NewRecorder()
handler.ServeHTTP(out, httptest.NewRequest("POST", "/core/v1/test", nil))
if out.Code != 400 || strings.Contains(out.Body.String(), "private-name") {
t.Fatal(out.Code, out.Body)
}
if !core {
golden := `{"error":{"message":"` + tc.publicMessage + `","type":"invalid_request_error","code":"` + tc.publicCode + `","param":null}}` + "\n"
if out.Body.String() != golden {
t.Fatal("public/machine fallback changed", out.Body)
}
continue
}
var body struct {
Error struct {
Code, Param string
Details map[string]any
}
}
if err := json.Unmarshal(out.Body.Bytes(), &body); err != nil {
t.Fatal(err)
}
if body.Error.Code != tc.code || body.Error.Param != tc.param || !reflect.DeepEqual(body.Error.Details, tc.details) {
t.Fatal(out.Body)
}
}
}
}

func TestCoreActiveCapacityUpperBoundNamesSubmittedField(t *testing.T) {
project, _ := NewAuthenticator([]APIKey{callerBinding()})
admin, _ := NewDeploymentAuthenticator([]string{device.HashCredential("administrator")})
h, err := NewHandler(&recordingStore{}, project, "codex", WithSandboxManager(&store.Store{}, admin))
if err != nil {
t.Fatal(err)
}
for _, tc := range []struct{ method, path, body string }{
{http.MethodPost, "/core/v1/sandbox/enrollment-tokens", `{"max_active":1000001}`},
{http.MethodPatch, "/core/v1/sandbox/nodes/11111111-1111-4111-8111-111111111111", `{"name":"node","max_active":1000001,"max_retained":8}`},
} {
out := projectKeyHTTP(h, tc.method, tc.path, "administrator", tc.body)
const golden = `{"error":{"message":"Node capacity must be positive, at most 1000000, and max_retained must be at least max_active.","type":"invalid_request_error","code":"invalid_node_capacity","param":"max_active","details":{"max":1000000,"min":1}}}` + "\n"
if out.Code != http.StatusBadRequest || out.Body.String() != golden {
t.Errorf("%s: %d %s", tc.method, out.Code, out.Body)
}
}
}
Loading
Loading