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
3 changes: 2 additions & 1 deletion services/core/IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Domain owners, each with its PostgreSQL adapter under `internal/persistence/post
- `files` (`filepg`): source Files.
- `vaults` (`vaultpg`): Vaults and Credentials, the encryption of Credential secrets, OAuth access-token refresh, and the MCP credential selection that Session creation freezes and the Dispatcher's `Credentials` resolves into a bearer token.
- `environmenttemplates` (`templatepg`): Environment Templates, their validation and default network, their sealed setup, initial files, Skills and Plugins, and the resolved Template that Session creation composes into its Environment.
- `modelconfiguration` (`modelconfigurationpg`): each Harness's deployment default model configuration and its last-use observations.

## Request handling

Expand Down Expand Up @@ -105,7 +106,7 @@ Reusable Agents are tenant-scoped rows independent of Session snapshots and engi

Saved execution defaults keep a model-provider bundle whole at every replacement boundary: endpoint, key, protocol and limits are never inherited separately. Agent JSON holds only the safe provider fields and an output-only configured flag; the complete bundle is encrypted separately with a tenant and Agent binding and its own purpose, and configuration and secret changes commit together under the Agent row lock. Model-only edits need no key. Merged harness, protocol and limits are validated without reading keys. Session creation reads safe defaults and ciphertext in one snapshot, and a complete Session override does not decrypt the inherited bundle. The resolved bundle is frozen in an encrypted Session-owned row, and dispatch fails closed when that snapshot is missing or cannot be decrypted; later Agent edits, default changes, restarts and suspension never resolve it again.

Deployment-default observations use a private revision UUID generated on every PUT, identical replacements included. Session creation reads the ciphertext and revision together and freezes them; retries and older Sessions never gain or replace revision metadata. After a successful root terminal commit, one independent pool operation has at most one second to update the matching current revision. SQL verifies the tenant, root Turn and committed outcome, and only completed Turns and native provider failures with `engine_failed` count; cancelled work, Core or Runtime errors and input-policy classifications never do. The metadata-only transaction sets statement and lock timeouts within the remaining budget and issues one UPDATE that locks only the default and samples database time after the lock. Errors throttle for 30 seconds, ordinary successes throttle for 30 seconds with one immediate recovery write after each accepted error, and an unchanged revision has at most three effective writes in any 30-second window of nondecreasing database time. Observations never change `updated_at`, readiness or execution truth, and can be lost or stale; there is no queue, retry, probe or backfill.
`modelconfiguration` owns each Harness's deployment default: its service validates a replacement through the Harness declaration and seals the complete bundle, and `modelconfigurationpg` stores it under a private revision UUID generated on every PUT, identical replacements included, with its audit row in the same transaction. Session creation resolves the ciphertext and revision together and freezes them; retries and older Sessions never gain or replace revision metadata. After a successful root terminal commit, the Dispatcher's required `modelconfiguration.Observer` runs one independent pool operation with at most one second to update the matching current revision. Only completed Turns and native provider failures with `engine_failed` count; cancelled work, Core or Runtime errors and input-policy classifications never do. `ShouldObserveProvider` skips the round trip for outcomes that cannot count, and the SQL stays authoritative: it verifies the tenant, root Turn and committed outcome. Both check the same shared cases. The metadata-only transaction sets statement and lock timeouts within the remaining budget and issues one UPDATE that locks only the default and samples database time after the lock. Errors throttle for 30 seconds, ordinary successes throttle for 30 seconds with one immediate recovery write after each accepted error, and an unchanged revision has at most three effective writes in any 30-second window of nondecreasing database time. Observations never change `updated_at`, readiness or execution truth, and can be lost or stale; there is no queue, retry, probe or backfill.

Session execution-configuration reads use a separate immutable safe projection written with its provenance in the Session's creation transaction. It reads no ciphertext, never recomputes sources from current Agents or defaults, never touches activity or wakes a sandbox, and does not affect retry identity.

Expand Down
3 changes: 2 additions & 1 deletion services/core/cmd/server/http_routes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ func daemonComposition(t testing.TB) http.Handler {
}
apiHandler, err := api.NewHandler(api.Dependencies{
Engine: "codex", CoreKeys: admin, InstallationBindings: struct{ api.InstallationBindings }{},
Projects: trapProjects{keys: keys}, ModelProviders: struct{ api.ModelProviders }{},
Projects: trapProjects{keys: keys},
ModelProviders: struct{ api.ModelProviders }{}, ModelProvidersReader: struct{ api.ModelProvidersReader }{},
Vaults: struct{ api.Vaults }{}, VaultsReader: struct{ api.VaultsReader }{},
Files: struct{ api.Files }{}, FilesReader: struct{ api.FilesReader }{}, Skills: struct{ api.Skills }{},
Agents: struct{ api.Agents }{}, AgentsReader: struct{ api.AgentsReader }{}, Sessions: struct{ api.Sessions }{}, SessionEvents: struct{ api.SessionEvents }{},
Expand Down
13 changes: 11 additions & 2 deletions services/core/cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,12 @@ import (
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmenttemplates"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/files"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/modelconfiguration"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/nativeinstaller"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/agentpg"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/auditpg"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/filepg"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/modelconfigurationpg"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/templatepg"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/vaultpg"
Expand Down Expand Up @@ -132,6 +134,11 @@ func run() error {
if err != nil {
return err
}
modelConfigurationStore := modelconfigurationpg.New(units)
modelConfigurationService, err := modelconfiguration.NewService(modelConfigurationStore, credentialKey)
if err != nil {
return err
}
installation, err := installationFacts(public)
if err != nil {
return err
Expand Down Expand Up @@ -251,7 +258,8 @@ func run() error {
}
}
if registry != nil {
dispatcher := &execution.Dispatcher{Store: executionStore, Registry: registry, Credentials: vaultService,
dispatcher := &execution.Dispatcher{Store: executionStore, Registry: registry,
Credentials: vaultService, Observer: modelConfigurationStore,
ManagedRuntimes: managed, MaxConcurrentExecutions: concurrency}
lease, err := pgunit.AcquireLease(ctx, pool)
if err != nil {
Expand Down Expand Up @@ -323,7 +331,8 @@ func run() error {
deps := api.Dependencies{
Engine: engine, Harnesses: kinds, CoreKeys: keyAdmin,
Installation: installation, InstallationBindings: executionStore,
Projects: executionStore, ModelProviders: executionStore,
Projects: executionStore,
ModelProviders: modelConfigurationService, ModelProvidersReader: modelConfigurationStore,
Vaults: vaultService, VaultsReader: vaultStore,
Skills: executionStore,
EnvironmentTemplates: environmentTemplates, EnvironmentTemplatesReader: templateStore,
Expand Down
37 changes: 29 additions & 8 deletions services/core/internal/api/core_model_provider_validation_test.go
Original file line number Diff line number Diff line change
@@ -1,33 +1,54 @@
package api

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

v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/modelconfiguration"
)

// coreProviderValidationStore counts deployment model provider writes.
// coreProviderValidationStore serves the real model configuration rules over
// storage that counts the deployment defaults reaching it.
type coreProviderValidationStore struct {
t testing.TB
writes int
}

func (s *coreProviderValidationStore) SetDeploymentModelProvider(context.Context, string, v1.ModelConfigurationInput) (store.DeploymentModelProvider, error) {
func (s *coreProviderValidationStore) Replace(context.Context, modelconfiguration.Record) (modelconfiguration.Configuration, error) {
s.writes++
return store.DeploymentModelProvider{}, nil
return modelconfiguration.Configuration{}, nil
}

func (s *coreProviderValidationStore) configure(_ *Dependencies, f *testFakes) {
f.modelProviders.setDeploymentModelProvider = s.SetDeploymentModelProvider
func (s *coreProviderValidationStore) Delete(context.Context, string) error {
unexpectedCall(s.t, "Delete")
return nil
}

func (s *coreProviderValidationStore) LoadSealed(context.Context, string) (modelconfiguration.Sealed, error) {
unexpectedCall(s.t, "LoadSealed")
return modelconfiguration.Sealed{}, nil
}

func (s *coreProviderValidationStore) configure(d *Dependencies, _ *testFakes) {
cipher, err := credentialcrypto.New(bytes.Repeat([]byte{3}, 32))
if err != nil {
s.t.Fatal(err)
}
service, err := modelconfiguration.NewService(s, cipher)
if err != nil {
s.t.Fatal(err)
}
d.ModelProviders = service
}

func TestCoreModelProviderValidationFields(t *testing.T) {
s := &coreProviderValidationStore{}
s := &coreProviderValidationStore{t: t}
h, _, _ := adminTestHandler(t, s.configure)
for _, tc := range []struct {
name, harness, body, code, param string
Expand Down
4 changes: 3 additions & 1 deletion services/core/internal/api/dependencies.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type Dependencies struct {
Vaults Vaults
VaultsReader VaultsReader
ModelProviders ModelProviders
ModelProvidersReader ModelProvidersReader
Files Files
FilesReader FilesReader
Skills Skills
Expand Down Expand Up @@ -116,7 +117,8 @@ func (d Dependencies) validate() error {
if err := required(
field{"InstallationBindings", d.InstallationBindings}, field{"Projects", d.Projects},
field{"Vaults", d.Vaults}, field{"VaultsReader", d.VaultsReader},
field{"ModelProviders", d.ModelProviders}, field{"Skills", d.Skills},
field{"ModelProviders", d.ModelProviders}, field{"ModelProvidersReader", d.ModelProvidersReader},
field{"Skills", d.Skills},
field{"Files", d.Files}, field{"FilesReader", d.FilesReader},
field{"EnvironmentTemplates", d.EnvironmentTemplates}, field{"EnvironmentTemplatesReader", d.EnvironmentTemplatesReader},
field{"Agents", d.Agents}, field{"AgentsReader", d.AgentsReader},
Expand Down
11 changes: 7 additions & 4 deletions services/core/internal/api/dependencies_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import (
"strings"
"testing"

"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/modelconfiguration"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store"
)

// testExecutorURL is the daemon URL self-hosted Sessions report in tests.
Expand All @@ -19,6 +19,7 @@ type testFakes struct {
vaults *fakeVaults
vaultsReader *fakeVaultsReader
modelProviders *fakeModelProviders
modelProvidersReader *fakeModelProvidersReader
files *fakeFiles
filesReader *fakeFilesReader
skills *fakeSkills
Expand Down Expand Up @@ -58,7 +59,8 @@ type testFakes struct {
func testDependencies(t testing.TB) (Dependencies, *testFakes) {
t.Helper()
f := &testFakes{
projects: &fakeProjects{t: t}, modelProviders: &fakeModelProviders{t: t},
projects: &fakeProjects{t: t},
modelProviders: &fakeModelProviders{t: t}, modelProvidersReader: &fakeModelProvidersReader{t: t},
vaults: &fakeVaults{t: t}, vaultsReader: &fakeVaultsReader{t: t},
skills: &fakeSkills{t: t},
environmentTemplates: &fakeEnvironmentTemplates{t: t}, environmentTemplatesReader: &fakeEnvironmentTemplatesReader{t: t},
Expand All @@ -74,7 +76,8 @@ func testDependencies(t testing.TB) (Dependencies, *testFakes) {
}
return Dependencies{
Engine: "codex", CoreKeys: coreKeys(t, "admin"), InstallationBindings: f.installationBindings,
Projects: f.projects, ModelProviders: f.modelProviders, Skills: f.skills,
Projects: f.projects, Skills: f.skills,
ModelProviders: f.modelProviders, ModelProvidersReader: f.modelProvidersReader,
Vaults: f.vaults, VaultsReader: f.vaultsReader,
Files: f.files, FilesReader: f.filesReader,
EnvironmentTemplates: f.environmentTemplates, EnvironmentTemplatesReader: f.environmentTemplatesReader,
Expand Down Expand Up @@ -124,7 +127,7 @@ func newTestHandler(t testing.TB, deps Dependencies) http.Handler {
}

// noDeploymentModelProvider is a deployment without a default model provider.
func noDeploymentModelProvider(context.Context, string) (*store.DeploymentModelProviderSnapshot, error) {
func noDeploymentModelProvider(context.Context, string) (*modelconfiguration.Snapshot, error) {
return nil, nil
}

Expand Down
2 changes: 1 addition & 1 deletion services/core/internal/api/environment_creation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func environmentCreationHandler(t *testing.T, engine string, configure ...func(*
}).ResolveProjectAPIKey
fixture.serve(fakes)
fakes.sessions.findSessionCreation, fakes.sessions.createSession, fakes.sessions.createSessionStream = fixture.FindSessionCreation, fixture.CreateSession, fixture.CreateSessionStream
fakes.modelProviders.deploymentModelProvider = fixtureDeploymentProvider
fakes.modelProviders.resolve = fixtureDeploymentProvider
for _, c := range configure {
c(&deps, fakes)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func TestSelfHostedCreationReturnsInstallationWithoutWebCredential(t *testing.T)
deps, fakes := testDependencies(t)
fakes.projects.resolveProjectAPIKey = projectKeys(t, APIKey{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential("project-key"), TenantID: uuid.NewString()}).ResolveProjectAPIKey
fakes.sessions.findSessionCreation, fakes.sessions.createSession = f.FindSessionCreation, f.CreateSession
fakes.modelProviders.deploymentModelProvider = fixtureDeploymentProvider
fakes.modelProviders.resolve = fixtureDeploymentProvider
fakes.environments.authorizeEnvironmentInstallation, fakes.environments.validateEnvironmentInstallation = f.AuthorizeEnvironmentInstallation, f.ValidateEnvironmentInstallation
deps.Execution = fakes.execution()
deps.Execution.NativeInstaller = &NativeInstaller{Version: "build", Catalog: &nativeinstaller.Catalog{Version: "build"}}
Expand Down
2 changes: 1 addition & 1 deletion services/core/internal/api/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ func writeStoreError(w http.ResponseWriter, r *http.Request, err error, notFound

case errors.Is(err, store.ErrDefaultSkillVersion):
writeError(w, http.StatusBadRequest, "invalid_value", "Cannot delete the default skill version.", "version")
case errors.Is(err, store.ErrModelProviderRequired):
case errors.Is(err, execution.ErrModelProviderRequired):
writeError(w, http.StatusBadRequest, "model_provider_required", "This Session was created without a model provider and cannot run. Create a new Session with x_agents_core.model_provider or an Agent that has one saved.")
case errors.Is(err, store.ErrHostedEnvironmentFailed):
// Observed official status, type, code, null param and message.
Expand Down
26 changes: 26 additions & 0 deletions services/core/internal/api/errors_modelconfiguration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package api

import (
"errors"
"net/http"

v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1"
"github.com/go-chi/chi/v5"
)

// writeModelConfigurationError reports a failure from the deployment default
// model configuration operations. A configuration the Harness declaration
// rejects names its field; anything unmapped is an internal error.
func writeModelConfigurationError(w http.ResponseWriter, r *http.Request, err error) {
var field *v1.ModelProviderError
if errors.As(err, &field) {
if !writeCoreModelProviderError(w, err, chi.URLParam(r, "harness")) {
writeError(w, http.StatusBadRequest, "invalid_request_error", err.Error())
}
return
}
if writeAuditSourceError(w, r, err) || writeTextValueError(w, r, err) || writeCredentialUnavailableError(w, r, err) {
return
}
writeInternalError(w, r)
}
47 changes: 26 additions & 21 deletions services/core/internal/api/fakes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmenttemplates"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/files"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/modelconfiguration"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimehistory"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimeobs"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox"
Expand Down Expand Up @@ -548,39 +549,43 @@ func (f *fakeMetrics) RecordUnavailable() {
}

type fakeModelProviders struct {
t testing.TB
listDeploymentModelProviders func(context.Context) ([]store.DeploymentModelProvider, error)
setDeploymentModelProvider func(context.Context, string, v1.ModelConfigurationInput) (store.DeploymentModelProvider, error)
deleteDeploymentModelProvider func(context.Context, string) error
deploymentModelProvider func(context.Context, string) (*store.DeploymentModelProviderSnapshot, error)
t testing.TB
replace func(context.Context, modelconfiguration.Replacement) (modelconfiguration.Configuration, error)
delete func(context.Context, string) error
resolve func(context.Context, string) (*modelconfiguration.Snapshot, error)
}

func (f *fakeModelProviders) ListDeploymentModelProviders(a0 context.Context) ([]store.DeploymentModelProvider, error) {
if f.listDeploymentModelProviders == nil {
unexpectedCall(f.t, "ListDeploymentModelProviders")
func (f *fakeModelProviders) Replace(a0 context.Context, a1 modelconfiguration.Replacement) (modelconfiguration.Configuration, error) {
if f.replace == nil {
unexpectedCall(f.t, "Replace")
}
return f.listDeploymentModelProviders(a0)
return f.replace(a0, a1)
}

func (f *fakeModelProviders) SetDeploymentModelProvider(a0 context.Context, a1 string, a2 v1.ModelConfigurationInput) (store.DeploymentModelProvider, error) {
if f.setDeploymentModelProvider == nil {
unexpectedCall(f.t, "SetDeploymentModelProvider")
func (f *fakeModelProviders) Delete(a0 context.Context, a1 string) error {
if f.delete == nil {
unexpectedCall(f.t, "Delete")
}
return f.setDeploymentModelProvider(a0, a1, a2)
return f.delete(a0, a1)
}

func (f *fakeModelProviders) DeleteDeploymentModelProvider(a0 context.Context, a1 string) error {
if f.deleteDeploymentModelProvider == nil {
unexpectedCall(f.t, "DeleteDeploymentModelProvider")
func (f *fakeModelProviders) Resolve(a0 context.Context, a1 string) (*modelconfiguration.Snapshot, error) {
if f.resolve == nil {
unexpectedCall(f.t, "Resolve")
}
return f.deleteDeploymentModelProvider(a0, a1)
return f.resolve(a0, a1)
}

func (f *fakeModelProviders) DeploymentModelProvider(a0 context.Context, a1 string) (*store.DeploymentModelProviderSnapshot, error) {
if f.deploymentModelProvider == nil {
unexpectedCall(f.t, "DeploymentModelProvider")
type fakeModelProvidersReader struct {
t testing.TB
list func(context.Context) ([]modelconfiguration.Configuration, error)
}

func (f *fakeModelProvidersReader) List(a0 context.Context) ([]modelconfiguration.Configuration, error) {
if f.list == nil {
unexpectedCall(f.t, "List")
}
return f.deploymentModelProvider(a0, a1)
return f.list(a0)
}

type fakeProjects struct {
Expand Down
Loading