Skip to content

Commit c087dbf

Browse files
authored
Give shared Core vocabulary its own packages (#311)
Environment configuration, Skill version parsing, metadata rules and JSON object normalization move out of store and api into leaf packages that the domain cutovers share: - environmentconfig: Setup, SetupCommand, Skill, SkillMetadata, Plugin, InitialFile and InitialFileMetadata with their validation, metadata projections, MaxInitialFileBytes and the strict Decode. Setup.Validate checks requested configuration and Setup.ValidateInstalled checks frozen configuration. - skills: ParseVersion replaces store.skillVersionNumber. - metadata: Validate and ValidateStorable return structured violations that api renders with its existing messages; Encode keeps the 64 KiB bound. - jsonobject: Normalize replaces store.canonicalJSONObject. Store translates the new errors into ErrInvalidInput, and api maps environmentconfig.ErrInvalid like it. The rule tests move with the rules.
1 parent a17d731 commit c087dbf

85 files changed

Lines changed: 1219 additions & 671 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎services/core/IMPLEMENTATION.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ These are the code-level rules of `services/core` that no contract states. Contr
88

99
`internal/persistence/postgres/pgunit` owns Core's PostgreSQL transaction and execution-lease mechanics: pooled read-write and snapshot transactions, the lease's dedicated connection and its gate, the ownership check, the cancellation fence, close, and the execution deadline. Persistence code runs every transaction through it, and nothing outside `persistence` and `store` imports it. `internal/persistence/postgres/pgtest` is test support: it opens the dedicated test database under the `oac_*_tests` guard, applies the migrations, and creates isolated databases for database-wide state such as the execution lease. Only test files import it.
1010

11+
Shared vocabulary has one owner each, and domains use it rather than copy it. `internal/environmentconfig` owns Environment setup, Skills, Plugins and initial files with their validation and public metadata; `Setup.Validate` checks requested configuration, where a Skill may be an unresolved reference, and `Setup.ValidateInstalled` checks frozen, installable configuration. `internal/skills` owns `ParseVersion`, the canonical positive decimal Skill version. `internal/metadata` owns the metadata rules: `Validate` for the pair, key and value limits and U+0000, `ValidateStorable` for U+0000 alone, and `Encode` with its 64 KiB bound. `internal/jsonobject` owns `Normalize`, the stable encoding of stored JSON objects that snapshots and retry identities compare. These packages import no persistence.
12+
1113
`store` is transitional. `store.New` builds a pooled Store, and `store.NewExecution` takes the lease and builds the execution writer on it. An execution-only operation on a pooled Store fails with `store.ErrExecutionAuthority`. New adapters do not copy that check: their execution repositories require a `*pgunit.Lease` at construction, their public repositories expose no execution operation, and the check goes away with `store`.
1214

1315
## Request handling
1416

1517
Every Agents API JSON route reads its body through `readJSONObject` before decoding, validation or lookup. The gate requires a JSON Content-Type, applies the route's body limit and rejects invalid UTF-8, malformed JSON (including unpaired surrogate escapes), repeated keys and non-object roots with the official messages; an empty body or `null` becomes `{}`. DELETE, multipart, Core extension and internal routes keep their own readers. Member names match exactly: decode request objects with `decodeInputObject`, or check `inexactMember` before another decoder, so `encoding/json` never matches a case variant.
1618

17-
Report a validation failure that has official evidence through the typed field error, which emits `invalid_request_error` with the observed param and message; keep other local codes until their official fields are sampled. Saved and inline Agent configuration pass one path-tracking validator of the pinned shapes before their parsers and harness admission; do not grow it into a JSON Schema engine. A malformed path identifier must produce exactly the response of a well-formed missing one on that route, including for invalid bodies, queries and storage availability: resolve it to the never-assigned maximum UUID and let the missing path run, or reject it directly only where the lookup is the next check. An `after` cursor that does not resolve inside its already resolved parent, malformed ones included, returns that list family's observed error, and foreign and missing cursors stay identical. U+0000 is rejected explicitly only in metadata (`metadata.<key>`); other stored strings rely on the PostgreSQL error mapping, so keep each request's writes in one transaction.
19+
Report a validation failure that has official evidence through the typed field error, which emits `invalid_request_error` with the observed param and message; keep other local codes until their official fields are sampled. Saved and inline Agent configuration pass one path-tracking validator of the pinned shapes before their parsers and harness admission; do not grow it into a JSON Schema engine. A malformed path identifier must produce exactly the response of a well-formed missing one on that route, including for invalid bodies, queries and storage availability: resolve it to the never-assigned maximum UUID and let the missing path run, or reject it directly only where the lookup is the next check. An `after` cursor that does not resolve inside its already resolved parent, malformed ones included, returns that list family's observed error, and foreign and missing cursors stay identical. U+0000 is rejected explicitly only in metadata (`metadata.<key>`), by the `metadata` package; other stored strings rely on the PostgreSQL error mapping, so keep each request's writes in one transaction.
1820

1921
List queries reuse the shared parser and error serializer while keeping each family's limit bounds and error fields. The Environment Files list keeps its own path and cursor parsing but follows the same unknown-key and duplicate-key rules, and still rejects malformed query encoding that the shared lists drop. Change page bounds, cursor ownership or parent lookup order only with evidence for that family, and never reproduce an observed upstream server failure as compatibility behavior.
2022

@@ -84,7 +86,7 @@ Session status and last activity use the public projection in [`internal/api/ses
8486

8587
## Agents and model providers
8688

87-
Reusable Agents are tenant-scoped rows independent of Session snapshots and engine bindings. The store persists caller-validated configuration without applying harness restrictions or model defaults, with internal limits of 512 KiB for configuration and 64 KiB for metadata. An update locks the Agent row while merging the supplied fields and enforcing the configuration bound, then commits configuration, metadata and update time together, so a stale full snapshot never overwrites another update. An empty update preserves the saved fields and advances `updated_at` through the same SQL update. Deletion is one tenant-scoped `DELETE … RETURNING id`. A Session copies the saved configuration into its immutable snapshot and never looks up its source again.
89+
Reusable Agents are tenant-scoped rows independent of Session snapshots and engine bindings. The store persists caller-validated configuration without applying harness restrictions or model defaults, with internal limits of 512 KiB for configuration and the `metadata.Encode` bound of 64 KiB for metadata. An update locks the Agent row while merging the supplied fields and enforcing the configuration bound, then commits configuration, metadata and update time together, so a stale full snapshot never overwrites another update. An empty update preserves the saved fields and advances `updated_at` through the same SQL update. Deletion is one tenant-scoped `DELETE … RETURNING id`. A Session copies the saved configuration into its immutable snapshot and never looks up its source again.
8890

8991
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.
9092

‎services/core/internal/api/configuration.go‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66

77
v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1"
8+
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/metadata"
89
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store"
910
"github.com/google/uuid"
1011
)
@@ -20,7 +21,7 @@ func resolve(input sessionRequest, tenant, key string, saved *v1.SavedAgent) (js
2021
if input.Environment == nil || (input.Environment.Type != "none" && input.Environment.Type != "self_hosted" && input.Environment.Type != "openai_hosted") {
2122
return nil, errors.New("Unsupported environment type.")
2223
}
23-
if err := validateMetadata(input.Metadata); err != nil {
24+
if err := metadataFieldError(metadata.Validate(input.Metadata)); err != nil {
2425
return nil, err
2526
}
2627
agent, err := resolveSessionAgent(input, saved)

‎services/core/internal/api/environment_plugins.go‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,19 @@ import (
44
"encoding/json"
55

66
"github.com/MiniMax-AI/OpenAgentCore/internal/agentplugin"
7+
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig"
78
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store"
89
)
910

10-
func decodeEnvironmentPlugins(raw json.RawMessage) ([]store.EnvironmentPlugin, error) {
11+
func decodeEnvironmentPlugins(raw json.RawMessage) ([]environmentconfig.Plugin, error) {
1112
if len(raw) == 0 {
1213
return nil, nil
1314
}
1415
var entries []json.RawMessage
1516
if json.Unmarshal(raw, &entries) != nil || len(entries) > 50 {
1617
return nil, store.ErrInvalidInput
1718
}
18-
result := make([]store.EnvironmentPlugin, 0, len(entries))
19+
result := make([]environmentconfig.Plugin, 0, len(entries))
1920
for _, entry := range entries {
2021
var input struct {
2122
Type string `json:"type"`
@@ -30,9 +31,9 @@ func decodeEnvironmentPlugins(raw json.RawMessage) ([]store.EnvironmentPlugin, e
3031
if err != nil {
3132
return nil, err
3233
}
33-
result = append(result, store.EnvironmentPlugin{Metadata: agentplugin.Metadata{Type: input.Type, Name: input.Name, Description: input.Description}, Archive: body})
34+
result = append(result, environmentconfig.Plugin{Metadata: agentplugin.Metadata{Type: input.Type, Name: input.Name, Description: input.Description}, Archive: body})
3435
}
35-
return result, store.ValidateEnvironmentPlugins(result)
36+
return result, environmentconfig.ValidatePlugins(result)
3637
}
3738

3839
func pluginResponse(plugins []agentplugin.Metadata) []json.RawMessage {

‎services/core/internal/api/environment_setup.go‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66

77
v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1"
8+
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig"
89
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store"
910
)
1011

@@ -23,8 +24,8 @@ func rejectSystemPackages(raw json.RawMessage) error {
2324
return nil
2425
}
2526

26-
func decodeEnvironmentSetup(fields map[string]json.RawMessage) (store.EnvironmentSetup, error) {
27-
var result store.EnvironmentSetup
27+
func decodeEnvironmentSetup(fields map[string]json.RawMessage) (environmentconfig.Setup, error) {
28+
var result environmentconfig.Setup
2829
if err := rejectSystemPackages(fields["packages"]); err != nil {
2930
return result, err
3031
}
@@ -54,7 +55,7 @@ func decodeEnvironmentSetup(fields map[string]json.RawMessage) (store.Environmen
5455
if decodeInputObject(command, &input, "command", "cwd") != nil || input.Command == nil {
5556
return result, store.ErrInvalidInput
5657
}
57-
step := store.SetupCommand{Command: *input.Command}
58+
step := environmentconfig.SetupCommand{Command: *input.Command}
5859
if input.CWD != nil {
5960
if *input.CWD == "" {
6061
return result, store.ErrInvalidInput
@@ -113,7 +114,7 @@ func decodeEnvironmentSetup(fields map[string]json.RawMessage) (store.Environmen
113114
}
114115

115116
func packageMetadata(packages *v1.EnvironmentPackages) v1.EnvironmentPackagesResponse {
116-
value := store.EnvironmentSetup{}
117+
value := environmentconfig.Setup{}
117118
if packages != nil {
118119
value.Packages = *packages
119120
}

‎services/core/internal/api/environment_skill_selectors_test.go‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"reflect"
66
"testing"
77

8+
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig"
89
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store"
910
)
1011

@@ -25,7 +26,7 @@ func TestSkillReferenceNullableSelectorAdmissionAndTemplateProjection(t *testing
2526
if err != nil || !template.SetSkills || len(template.Initialization.Skills) != 1 {
2627
t.Fatalf("template admission: %+v %v", template, err)
2728
}
28-
want := store.EnvironmentSkill{Metadata: store.EnvironmentSkillMetadata{Type: "skill_reference", SkillID: "skill-owned", Version: test.selector}}
29+
want := environmentconfig.Skill{Metadata: environmentconfig.SkillMetadata{Type: "skill_reference", SkillID: "skill-owned", Version: test.selector}}
2930
if !reflect.DeepEqual(template.Initialization.Skills[0], want) {
3031
t.Fatalf("unresolved selector changed: %+v", template.Initialization.Skills[0])
3132
}
@@ -61,8 +62,8 @@ func TestSkillReferenceNullDoesNotWidenOtherSelectors(t *testing.T) {
6162
}
6263

6364
func TestInstalledSkillReferenceRequiresConcreteVersion(t *testing.T) {
64-
metadata := store.EnvironmentSkillMetadata{Type: "skill_reference", SkillID: "skill-owned", Version: "2", Name: "proof", Description: "A proof."}
65-
public := skillResponse([]store.EnvironmentSkillMetadata{metadata})
65+
metadata := environmentconfig.SkillMetadata{Type: "skill_reference", SkillID: "skill-owned", Version: "2", Name: "proof", Description: "A proof."}
66+
public := skillResponse([]environmentconfig.SkillMetadata{metadata})
6667
var reference map[string]any
6768
if len(public) != 1 || json.Unmarshal(public[0], &reference) != nil {
6869
t.Fatalf("installed projection: %s", public)

‎services/core/internal/api/environment_skills.go‎

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,19 @@ import (
44
"bytes"
55
"encoding/json"
66

7+
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig"
78
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store"
89
)
910

10-
func decodeEnvironmentSkills(raw json.RawMessage) ([]store.EnvironmentSkill, error) {
11+
func decodeEnvironmentSkills(raw json.RawMessage) ([]environmentconfig.Skill, error) {
1112
if len(raw) == 0 {
1213
return nil, nil
1314
}
1415
var entries []json.RawMessage
1516
if json.Unmarshal(raw, &entries) != nil || len(entries) > 50 {
1617
return nil, store.ErrInvalidInput
1718
}
18-
result := make([]store.EnvironmentSkill, 0, len(entries))
19+
result := make([]environmentconfig.Skill, 0, len(entries))
1920
for _, entry := range entries {
2021
var discriminator struct {
2122
Type string `json:"type"`
@@ -32,13 +33,13 @@ func decodeEnvironmentSkills(raw json.RawMessage) ([]store.EnvironmentSkill, err
3233
if decodeInputObject(entry, &reference, "type", "skill_id", "version") != nil {
3334
return nil, store.ErrInvalidInput
3435
}
35-
metadata := store.EnvironmentSkillMetadata{Type: reference.Type, SkillID: reference.SkillID}
36+
metadata := environmentconfig.SkillMetadata{Type: reference.Type, SkillID: reference.SkillID}
3637
if len(reference.Version) > 0 && !bytes.Equal(bytes.TrimSpace(reference.Version), []byte("null")) {
3738
if json.Unmarshal(reference.Version, &metadata.Version) != nil || metadata.Version == "" {
3839
return nil, store.ErrInvalidInput
3940
}
4041
}
41-
result = append(result, store.EnvironmentSkill{Metadata: metadata})
42+
result = append(result, environmentconfig.Skill{Metadata: metadata})
4243
continue
4344
}
4445
var input struct {
@@ -54,20 +55,20 @@ func decodeEnvironmentSkills(raw json.RawMessage) ([]store.EnvironmentSkill, err
5455
if err != nil {
5556
return nil, err
5657
}
57-
result = append(result, store.EnvironmentSkill{Metadata: store.EnvironmentSkillMetadata{Type: input.Type, Name: input.Name, Description: input.Description}, Archive: body})
58+
result = append(result, environmentconfig.Skill{Metadata: environmentconfig.SkillMetadata{Type: input.Type, Name: input.Name, Description: input.Description}, Archive: body})
5859
}
59-
return result, store.ValidateEnvironmentSkills(result)
60+
return result, environmentconfig.ValidateSkills(result)
6061
}
6162

62-
func skillResponse(skills []store.EnvironmentSkillMetadata) []json.RawMessage {
63+
func skillResponse(skills []environmentconfig.SkillMetadata) []json.RawMessage {
6364
result := make([]json.RawMessage, 0, len(skills))
6465
for _, skill := range skills {
6566
var projection any = skill
6667
if skill.Type == "skill_reference" && skill.Version == "" {
6768
projection = struct {
68-
store.EnvironmentSkillMetadata
69+
environmentconfig.SkillMetadata
6970
Version *string `json:"version"`
70-
}{EnvironmentSkillMetadata: skill}
71+
}{SkillMetadata: skill}
7172
}
7273
raw, _ := json.Marshal(projection)
7374
result = append(result, raw)
@@ -85,8 +86,8 @@ func storedSkills(raw json.RawMessage) ([]json.RawMessage, error) {
8586
}
8687
seen := map[string]bool{}
8788
for _, entry := range entries {
88-
var metadata store.EnvironmentSkillMetadata
89-
if decodeInputObject(entry, &metadata, "type", "name", "description", "skill_id", "version") != nil || store.ValidateInstalledSkillMetadata(metadata) != nil || seen[metadata.Name] {
89+
var metadata environmentconfig.SkillMetadata
90+
if decodeInputObject(entry, &metadata, "type", "name", "description", "skill_id", "version") != nil || metadata.ValidateInstalled() != nil || seen[metadata.Name] {
9091
return nil, store.ErrInvalidInput
9192
}
9293
seen[metadata.Name] = true

‎services/core/internal/api/environment_skills_test.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"encoding/json"
88
"testing"
99

10-
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store"
10+
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig"
1111
)
1212

1313
func skillInput(t *testing.T, body string) json.RawMessage {
@@ -32,7 +32,7 @@ func skillInput(t *testing.T, body string) json.RawMessage {
3232
}
3333

3434
func TestSkillReferenceParsingInheritanceAndReplacement(t *testing.T) {
35-
lookup := &templateLookupStore{network: "enabled", skills: []store.EnvironmentSkill{{Metadata: store.EnvironmentSkillMetadata{Type: "skill_reference", SkillID: "skill-template", Version: "latest"}}}}
35+
lookup := &templateLookupStore{network: "enabled", skills: []environmentconfig.Skill{{Metadata: environmentconfig.SkillMetadata{Type: "skill_reference", SkillID: "skill-template", Version: "latest"}}}}
3636
h := templateHandler(t, lookup.ResolveEnvironmentTemplate)
3737
for _, fields := range []string{"", `,"skills":[]`, `,"skills":[{"type":"skill_reference","skill_id":"skill-override","version":"2"}]`} {
3838
var decoded decodedSessionRequest

‎services/core/internal/api/environment_templates.go‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ import (
77
"unicode/utf8"
88

99
v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1"
10+
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig"
1011
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store"
1112
"github.com/go-chi/chi/v5"
1213
)
1314

1415
// EnvironmentTemplates manages Environment Templates. ResolveEnvironmentTemplate
1516
// reads a Template with its initial files for Session creation.
1617
type EnvironmentTemplates interface {
17-
ResolveEnvironmentTemplate(context.Context, string, string) (store.EnvironmentTemplate, []store.InitialFile, error)
18+
ResolveEnvironmentTemplate(context.Context, string, string) (store.EnvironmentTemplate, []environmentconfig.InitialFile, error)
1819
CreateEnvironmentTemplate(context.Context, string, store.EnvironmentTemplateInput) (store.EnvironmentTemplate, error)
1920
GetEnvironmentTemplate(context.Context, string, string) (store.EnvironmentTemplate, error)
2021
UpdateEnvironmentTemplate(context.Context, string, string, store.EnvironmentTemplateInput) (store.EnvironmentTemplate, error)

0 commit comments

Comments
 (0)