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
8 changes: 7 additions & 1 deletion services/core/internal/api/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ func TestHTTPRejectsUntrustedOrUnsupportedRequests(t *testing.T) {
{"initial input", "Bearer test-api-key", "agents=v1", "/v1/agents/sessions", valid, 503},
{"stream unavailable", "Bearer test-api-key", "agents=v1", "/v1/agents/sessions", strings.Replace(valid, `"agent":`, `"stream":true,"agent":`, 1), 503},
{"unknown saved agent", "Bearer test-api-key", "agents=v1", "/v1/agents/sessions", strings.Replace(valid, `"agent":`, `"agent_id":"saved","agent":`, 1), 404},
{"saved agent without its model provider bundle", "Bearer test-api-key", "agents=v1", "/v1/agents/sessions", strings.Replace(valid, `"agent":`, `"agent_id":"saved","agent":`, 1), 500},
{"unknown agent option", "Bearer test-api-key", "agents=v1", "/v1/agents/sessions", strings.Replace(valid, `"model":`, `"tools":[{}],"model":`, 1), 400},
{"multiple objects", "Bearer test-api-key", "agents=v1", "/v1/agents/sessions", valid + `{}`, 400},
{"null body as empty object", "Bearer test-api-key", "agents=v1", "/v1/agents/sessions", `null`, 400},
Expand All @@ -158,10 +159,15 @@ func TestHTTPRejectsUntrustedOrUnsupportedRequests(t *testing.T) {
unavailable := 0
h, s, _ := testHandler(t, func(_ *Dependencies, f *testFakes) {
f.metrics.recordUnavailable = func() { unavailable++ }
if test.name == "unknown saved agent" {
switch test.name {
case "unknown saved agent":
f.agentsReader.getAgentWithModelProvider = func(context.Context, string, string) (agents.Agent, *v1.ModelProviderInput, error) {
return agents.Agent{}, nil, agents.ErrNotFound
}
case "saved agent without its model provider bundle":
f.agentsReader.getAgentWithModelProvider = func(context.Context, string, string) (agents.Agent, *v1.ModelProviderInput, error) {
return agents.Agent{ID: "saved", Configuration: json.RawMessage(`{"model":"example","x_agents_core":{"model_provider":{"protocol":"responses","base_url":"https://model.invalid"}}}`)}, nil, nil
}
}
})
r := httptest.NewRequest(http.MethodPost, test.path, strings.NewReader(test.body))
Expand Down
3 changes: 1 addition & 2 deletions services/core/internal/api/session_model_defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (

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

Expand All @@ -34,7 +33,7 @@ func (h *Handler) sessionAgentDefaults(ctx context.Context, tenant string, input
return nil, nil, err
}
if inherit && saved.XAgentsCore != nil && saved.XAgentsCore.ModelProvider != nil && provider == nil {
return nil, nil, credentialcrypto.ErrUnavailable
return nil, nil, errors.New("saved agent model provider bundle is missing")
}
return saved, provider, nil
}
Expand Down
2 changes: 1 addition & 1 deletion services/core/internal/environmenttemplates/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ var (
// the tenant. Missing, malformed and foreign IDs are indistinguishable.
ErrNotFound = errors.New("environment template not found")
// ErrInvalidInput reports input that is not a valid Template, or a saved
// Template whose configuration no longer decodes as one.
// Template whose configuration no longer passes validation.
ErrInvalidInput = errors.New("invalid environment template")
)
10 changes: 5 additions & 5 deletions services/core/internal/persistence/postgres/sessionpg/devices.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
)

func (s *Store) GetSessionDevice(ctx context.Context, tenant, session string) (sessions.ExecutionDevice, error) {
lookup, err := DeviceLookup(tenant, session)
lookup, err := ResourceLookup(tenant, session)
if err != nil {
return sessions.ExecutionDevice{}, err
}
Expand All @@ -27,7 +27,7 @@ func (s *Store) GetSessionDevice(ctx context.Context, tenant, session string) (s
}

func (s *Store) GetSessionExecutionBinding(ctx context.Context, tenant, session string) (sessions.ExecutionBinding, error) {
lookup, err := DeviceLookup(tenant, session)
lookup, err := ResourceLookup(tenant, session)
if err != nil {
return sessions.ExecutionBinding{}, err
}
Expand Down Expand Up @@ -77,7 +77,7 @@ func (s *Store) ListExecutionDevices(ctx context.Context, tenant string) ([]sess
}

func (s *Store) GetSessionRuntimeDevice(ctx context.Context, tenant, session string) (sessions.ExecutionDevice, error) {
lookup, err := DeviceLookup(tenant, session)
lookup, err := ResourceLookup(tenant, session)
if err != nil {
return sessions.ExecutionDevice{}, err
}
Expand Down Expand Up @@ -176,7 +176,7 @@ func (s *Store) CreateDevice(ctx context.Context, tenant string, registration se
}

func (s *Store) RevokeDevice(ctx context.Context, tenant, device string) error {
lookup, err := DeviceLookup(tenant, device)
lookup, err := ResourceLookup(tenant, device)
if err != nil {
return err
}
Expand Down Expand Up @@ -217,7 +217,7 @@ func (s *Store) WithEnrollment(ctx context.Context, environment, credentialHash
if err != nil {
return err
}
lookup, err := DeviceLookup(tenant, current.ID)
lookup, err := ResourceLookup(tenant, current.ID)
if err != nil {
return err
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func (e *Execution) WithConnection(ctx context.Context, tenant, environment stri
if err != nil {
return err
}
lookup, err := DeviceLookup(tenant, current.SessionID)
lookup, err := ResourceLookup(tenant, current.SessionID)
if err != nil {
return err
}
Expand All @@ -43,7 +43,7 @@ func (e *Execution) WithConnection(ctx context.Context, tenant, environment stri
}

func (e *Execution) WithDeviceBinding(ctx context.Context, tenant, session string, apply func(context.Context, sessions.DeviceBindingTx, sessions.LockedSession) error) error {
lookup, err := DeviceLookup(tenant, session)
lookup, err := ResourceLookup(tenant, session)
if err != nil {
return err
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
// data that does not open, decode or validate is corrupt stored data, never
// the caller's input, so it is an internal error.
func (s *Store) ReadEnvironmentSetup(ctx context.Context, tenant, session string) (environmentconfig.Setup, error) {
lookup, err := DeviceLookup(tenant, session)
lookup, err := ResourceLookup(tenant, session)
if err != nil {
return environmentconfig.Setup{}, sessions.ErrNotFound
}
Expand Down Expand Up @@ -51,7 +51,7 @@ func (s *Store) ReadEnvironmentSetup(ctx context.Context, tenant, session string
// position. A file that does not open or match its recorded size is corrupt
// stored data, so it is an internal error.
func (s *Store) ReadInitialEnvironmentFile(ctx context.Context, tenant, session string, position int) (environmentconfig.InitialFileMetadata, []byte, error) {
lookup, err := DeviceLookup(tenant, session)
lookup, err := ResourceLookup(tenant, session)
if err != nil {
return environmentconfig.InitialFileMetadata{}, nil, err
}
Expand Down
8 changes: 4 additions & 4 deletions services/core/internal/persistence/postgres/sessionpg/rows.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ func TurnLookup(tenantID, sessionID, turnID string) (sqlc.GetTurnParams, error)
return p, err
}

// DeviceLookup parses a tenant and the internal identifier of one of its
// ResourceLookup parses a tenant and the internal identifier of one of its
// resources, such as a device, Session or Environment; a malformed one is
// sessions.ErrInvalidInput.
func DeviceLookup(tenantID, id string) (sqlc.GetDeviceParams, error) {
func ResourceLookup(tenantID, id string) (sqlc.GetDeviceParams, error) {
var p sqlc.GetDeviceParams
var err error
if p.TenantID, err = parseID(tenantID); err != nil {
Expand Down Expand Up @@ -76,7 +76,7 @@ func sessionFromRow(row sqlc.Session) (sessions.Session, error) {
session.Creator = creator
configuration, err := jsonobject.Normalize(row.Configuration)
if err != nil {
return sessions.Session{}, fmt.Errorf("decode session configuration: %w: %w", sessions.ErrInvalidInput, err)
return sessions.Session{}, fmt.Errorf("decode session configuration: %w", err)
}
session.Configuration = configuration
if err := json.Unmarshal(row.Metadata, &session.Metadata); err != nil {
Expand Down Expand Up @@ -110,7 +110,7 @@ func environmentFromRow(row sqlc.Environment, tenant pgtype.UUID, configuration
}
configuration, err = jsonobject.Normalize(configuration)
if err != nil {
return sessions.Environment{}, fmt.Errorf("decode environment configuration: %w: %w", sessions.ErrInvalidInput, err)
return sessions.Environment{}, fmt.Errorf("decode environment configuration: %w", err)
}
return sessions.Environment{
ID: uuid.UUID(row.ID.Bytes).String(), SessionID: uuid.UUID(row.SessionID.Bytes).String(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package templatepg

import (
"encoding/json"
"errors"

"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig"
Expand Down Expand Up @@ -87,7 +88,7 @@ func (s *Store) openSetup(tenant, id pgtype.UUID, field string, ciphertext []byt
return err
}
if environmentconfig.Decode(plaintext, output) != nil {
return environmenttemplates.ErrInvalidInput
return errors.New("invalid stored environment template " + field)
}
return nil
}
Expand Down
20 changes: 14 additions & 6 deletions services/core/internal/persistence/postgres/templatepg/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,10 @@ func (s *Store) List(ctx context.Context, tenantID string, query environmenttemp
}

// Resolve reads one row, so the metadata and every confidential field come
// from the same committed version. Stored Skill and Plugin metadata must match
// the sealed archives they describe. Any malformed ID resolves to ErrNotFound.
// from the same committed version. Stored data that does not decode or match
// its sealed contents is corrupt, an internal error; configuration that decodes
// but fails current validation is ErrInvalidInput. Any malformed ID resolves to
// ErrNotFound.
func (s *Store) Resolve(ctx context.Context, tenantID, templateID string) (environmenttemplates.Resolved, error) {
tenant, err := pgunit.ParseID(tenantID)
if err != nil {
Expand Down Expand Up @@ -203,12 +205,15 @@ func (s *Store) Resolve(ctx context.Context, tenantID, templateID string) (envir
return environmenttemplates.Resolved{}, err
}
}
if !matches(metadata.Skills, resolved.Setup.SkillMetadata()) || !matches(metadata.Plugins, resolved.Setup.PluginMetadata()) || resolved.Setup.Validate() != nil {
if !matches(metadata.Skills, resolved.Setup.SkillMetadata()) || !matches(metadata.Plugins, resolved.Setup.PluginMetadata()) {
return environmenttemplates.Resolved{}, errors.New("stored environment template metadata does not match its sealed setup")
}
if resolved.Setup.Validate() != nil {
return environmenttemplates.Resolved{}, environmenttemplates.ErrInvalidInput
}
if len(row.FileContents) == 0 {
if len(metadata.Files) > 0 {
return environmenttemplates.Resolved{}, environmenttemplates.ErrInvalidInput
return environmenttemplates.Resolved{}, errors.New("stored environment template files have no contents")
}
return resolved, nil
}
Expand All @@ -219,7 +224,10 @@ func (s *Store) Resolve(ctx context.Context, tenantID, templateID string) (envir
if err != nil {
return environmenttemplates.Resolved{}, err
}
if json.Unmarshal(plaintext, &resolved.Files) != nil || environmentconfig.ValidateInitialFiles(resolved.Files) != nil {
if json.Unmarshal(plaintext, &resolved.Files) != nil {
return environmenttemplates.Resolved{}, errors.New("invalid stored environment template files")
}
if environmentconfig.ValidateInitialFiles(resolved.Files) != nil {
return environmenttemplates.Resolved{}, environmenttemplates.ErrInvalidInput
}
return resolved, nil
Expand All @@ -246,7 +254,7 @@ func template(row metadataRow) (environmenttemplates.Template, error) {
result.Name = &row.Name.String
}
if json.Unmarshal(row.Files, &result.Files) != nil || environmentconfig.Decode(row.Packages, &result.Packages) != nil || json.Unmarshal(row.Skills, &result.Skills) != nil || json.Unmarshal(row.Plugins, &result.Plugins) != nil {
return environmenttemplates.Template{}, environmenttemplates.ErrInvalidInput
return environmenttemplates.Template{}, errors.New("invalid stored environment template metadata")
}
return result, nil
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,9 +370,10 @@ func TestMissingKeyIsUnavailable(t *testing.T) {
}
}

// Stored package metadata naming the removed system manager is rejected on
// every read instead of being silently dropped.
func TestStoredSystemPackagesRejected(t *testing.T) {
// Stored package metadata naming the removed system manager no longer decodes,
// so every read fails with an internal error instead of dropping it. Packages
// that decode but fail current validation are an invalid Template.
func TestStoredPackagesRejected(t *testing.T) {
f := newFixture(t, pgtest.Open(t))
ctx := t.Context()
tenant := uuid.NewString()
Expand All @@ -383,18 +384,23 @@ func TestStoredSystemPackagesRejected(t *testing.T) {
t.Fatal(err)
}
}
internal := func(err error) bool { return err != nil && !errors.Is(err, environmenttemplates.ErrInvalidInput) }
for _, value := range []string{`null`, `[]`, `["jq"]`} {
store(`{"npm":[],"python":[],"system":` + value + `}`)
if _, err := f.keyless.Get(ctx, tenant, template.ID); !errors.Is(err, environmenttemplates.ErrInvalidInput) {
t.Fatal("get silently ignored removed system packages", value, err)
if _, err := f.keyless.Get(ctx, tenant, template.ID); !internal(err) {
t.Fatal("get did not fail internally on removed system packages", value, err)
}
if _, err := f.keyless.List(ctx, tenant, environmenttemplates.ListQuery{Limit: 1}); !errors.Is(err, environmenttemplates.ErrInvalidInput) {
t.Fatal("list silently ignored removed system packages", value, err)
if _, err := f.keyless.List(ctx, tenant, environmenttemplates.ListQuery{Limit: 1}); !internal(err) {
t.Fatal("list did not fail internally on removed system packages", value, err)
}
if _, err := f.keyed.Resolve(ctx, tenant, template.ID); !errors.Is(err, environmenttemplates.ErrInvalidInput) {
t.Fatal("resolve silently ignored removed system packages", value, err)
if _, err := f.keyed.Resolve(ctx, tenant, template.ID); !internal(err) {
t.Fatal("resolve did not fail internally on removed system packages", value, err)
}
}
store(`{"npm":["-x"],"python":[]}`)
if _, err := f.keyed.Resolve(ctx, tenant, template.ID); !errors.Is(err, environmenttemplates.ErrInvalidInput) {
t.Fatal("resolve accepted packages that fail validation", err)
}
store(`{"npm":["semver"],"python":["packaging"]}`)
if got, err := f.keyless.Get(ctx, tenant, template.ID); err != nil || !reflect.DeepEqual(got.Packages, v1.EnvironmentPackages{NPM: []string{"semver"}, Python: []string{"packaging"}}) {
t.Fatal("supported stored package managers rejected", got.Packages, err)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func FixtureEnvironmentDevice(ctx context.Context, pool *pgxpool.Pool, tenant, e
if err != nil {
return sessions.ExecutionDevice{}, err
}
lookup, err := sessionpg.DeviceLookup(tenant, current.SessionID)
lookup, err := sessionpg.ResourceLookup(tenant, current.SessionID)
if err != nil {
return sessions.ExecutionDevice{}, err
}
Expand Down
2 changes: 1 addition & 1 deletion services/core/tests/integration/turn_journal_reads_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ type SubagentIdentity struct {
// GetSubagentIdentity reads a binding in its authorized, visible Session. Only
// tests read bindings back.
func (s *Store) GetSubagentIdentity(ctx context.Context, tenantID, sessionID, nativeID string) (SubagentIdentity, error) {
p, err := sessionpg.DeviceLookup(tenantID, sessionID)
p, err := sessionpg.ResourceLookup(tenantID, sessionID)
if err != nil {
return SubagentIdentity{}, err
}
Expand Down
Loading