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
138 changes: 69 additions & 69 deletions contracts/agents-api/core.openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,69 @@ definitions:
service:
$ref: '#/definitions/coremetrics.ServiceState'
type: object
projects.APIKey:
properties:
created_at:
type: string
id:
type: string
name:
type: string
prefix:
type: string
project_id:
type: string
revoked_at:
type: string
type: object
projects.IssuedAPIKey:
properties:
created_at:
type: string
id:
type: string
key:
type: string
name:
type: string
prefix:
type: string
project_id:
type: string
revoked_at:
type: string
type: object
projects.KeyPage:
properties:
data:
items:
$ref: '#/definitions/projects.APIKey'
type: array
has_more:
type: boolean
type: object
projects.Page:
properties:
data:
items:
$ref: '#/definitions/projects.Project'
type: array
has_more:
type: boolean
type: object
projects.Project:
properties:
active_key_count:
type: integer
archived_at:
type: string
created_at:
type: string
id:
type: string
name:
type: string
type: object
sandbox.ConfigurationDiscoveryInput:
properties:
configuration:
Expand Down Expand Up @@ -990,23 +1053,6 @@ definitions:
key_id:
type: string
type: object
store.IssuedProjectAPIKey:
properties:
created_at:
type: string
id:
type: string
key:
type: string
name:
type: string
prefix:
type: string
project_id:
type: string
revoked_at:
type: string
type: object
store.ManagedSessionArchive:
properties:
environment_id:
Expand Down Expand Up @@ -1039,52 +1085,6 @@ definitions:
start:
type: string
type: object
store.Project:
properties:
active_key_count:
type: integer
archived_at:
type: string
created_at:
type: string
id:
type: string
name:
type: string
type: object
store.ProjectAPIKey:
properties:
created_at:
type: string
id:
type: string
name:
type: string
prefix:
type: string
project_id:
type: string
revoked_at:
type: string
type: object
store.ProjectAPIKeyPage:
properties:
data:
items:
$ref: '#/definitions/store.ProjectAPIKey'
type: array
has_more:
type: boolean
type: object
store.ProjectPage:
properties:
data:
items:
$ref: '#/definitions/store.Project'
type: array
has_more:
type: boolean
type: object
store.RuntimeDeploymentView:
properties:
configuration:
Expand Down Expand Up @@ -3673,7 +3673,7 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/store.ProjectPage'
$ref: '#/definitions/projects.Page'
"400":
description: Bad Request
schema:
Expand Down Expand Up @@ -3711,7 +3711,7 @@ paths:
"201":
description: Created
schema:
$ref: '#/definitions/store.Project'
$ref: '#/definitions/projects.Project'
"400":
description: Bad Request
schema:
Expand Down Expand Up @@ -3755,7 +3755,7 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/store.Project'
$ref: '#/definitions/projects.Project'
"400":
description: Bad Request
schema:
Expand Down Expand Up @@ -3937,7 +3937,7 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/store.Project'
$ref: '#/definitions/projects.Project'
"401":
description: Unauthorized
schema:
Expand Down Expand Up @@ -4444,7 +4444,7 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/store.ProjectAPIKeyPage'
$ref: '#/definitions/projects.KeyPage'
"400":
description: Bad Request
schema:
Expand Down Expand Up @@ -4487,7 +4487,7 @@ paths:
"201":
description: Created
schema:
$ref: '#/definitions/store.IssuedProjectAPIKey'
$ref: '#/definitions/projects.IssuedAPIKey'
"400":
description: Bad Request
schema:
Expand Down
3 changes: 2 additions & 1 deletion services/core/IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Domain owners, each with its PostgreSQL adapter under `internal/persistence/post

- `agents` (`agentpg`): saved Agents, their configuration merge and bounds, and the encrypted model-provider bundle bound to each Agent.
- `files` (`filepg`): source Files.
- `projects` (`projectpg`): Projects, Project API keys and key resolution for authentication. `projects` validates Project and key names by rune count, separately from the byte limits on node names, and decides key issuance over the share-locked Project so an archive and an issuance never both commit.
- `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.
Expand Down Expand Up @@ -188,7 +189,7 @@ The [managed lifecycle](../../docs/sandbox-provider.md#managed-lifecycle) descri
## Core administration errors, metrics and write provenance

- Core error details are scoped by the `/core/v1` router's writer mark, never by a request path test. Write Core errors with `writeCoreError` and typed `CoreErrorDetails` values (string, number, null and string-array constructors); invalid or empty details are omitted as a whole. The mark preserves error observation, flushing and `http.ResponseController` access. A shared handler or a Core-looking path alone never changes a public or machine error envelope. Core authentication runs before operation configuration checks, and unknown paths keep their status and admission rules. When adding a code with details, document its fixed keys in `contracts/agents-api/core-errors.md`, and pass only safe Core-owned facts: never submitted values, secrets, native text or provider bodies.
- Operation validators keep their original error text, sentinel identity and validation precedence. Package-owned typed errors carry fixed field metadata; only the marked Core error mapper translates it into operation codes and safe bound or catalog details. Keep Project and key rune limits separate from node byte limits. Sandbox validation metadata travels through its store wrapper without changing transaction or provider authority. Public Session provider validation stays byte-for-byte unchanged; cover it with handler-level golden responses. The Core clients ignore malformed optional details and never retry a write.
- Operation validators keep their original error text, sentinel identity and validation precedence. Package-owned typed errors carry fixed field metadata; only the marked Core error mapper translates it into operation codes and safe bound or catalog details. Sandbox validation metadata travels through its store wrapper without changing transaction or provider authority. Public Session provider validation stays byte-for-byte unchanged; cover it with handler-level golden responses. The Core clients ignore malformed optional details and never retry a write.
- Core metrics instrument the existing worker and job owners without changing scheduling, lease or retention behavior. Count `execution_unavailable` at the HTTP error writer, once per rejected response; never capture request or response bodies and never infer the count from other 503s or failed Turns. Process CPU, RSS and cgroup limits are sampled by the 30-second Core metrics loop into the same bounded in-memory ring; the first CPU interval and restart gaps stay null, and host usage never substitutes for process usage. Root Turn history is queried read-only from PostgreSQL with native timestamps. Builds inject the source commit with `-ldflags` into `main.buildRevision`. Keep the response shape aligned with `packages/agents-client/src/core-metrics.ts`.
- Public resource writes carry the authenticated key's provenance separately from the execution principal. Record the operation and any creation ownership with `auditpg.RecordWriteAudit` in the business transaction, never in response middleware or an asynchronous queue; a failed record rolls back the write. Internal lifecycle and refresh work never acquires public provenance, and retries never replace ownership. Environment uploads persist the safe request origin before dispatch and record success with the confirmed Runtime receipt, not the native filesystem call. Never put payloads, paths or secrets in audit metadata. Do not confuse key identity with the Session creator identity used for retries.
- Administrator writes reuse the public resource deletion and serialization code and record their administrator audit entry with `auditpg.RecordAdminMutation`, or `RecordDeploymentMutation` for a deployment-wide write, in the same transaction. Administrator provenance takes precedence over a key's.
19 changes: 11 additions & 8 deletions services/core/cmd/server/auth_fixture_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,27 @@ package main

import (
"context"
"crypto/sha256"
"encoding/hex"
"testing"

"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/projects"
"github.com/google/uuid"
)

type testAPIKey struct{ Name, TokenSHA256, TenantID, OrganizationID, ProjectID, SubjectKind, SubjectID string }
type fixtureKeyResolver map[string]store.ProjectAPIKeyBinding
type fixtureKeyResolver map[[sha256.Size]byte]projects.KeyBinding

func (f fixtureKeyResolver) ResolveProjectAPIKey(_ context.Context, digest string) (store.ProjectAPIKeyBinding, error) {
func (f fixtureKeyResolver) ResolveAPIKey(_ context.Context, digest [sha256.Size]byte) (projects.KeyBinding, error) {
if b, ok := f[digest]; ok {
return b, nil
}
return store.ProjectAPIKeyBinding{}, store.ErrNotFound
return projects.KeyBinding{}, projects.ErrNotFound
}

// newTestAuthenticator binds each key's digest to its Principal, as the Project
// store does.
// reader does.
func newTestAuthenticator(t testing.TB, keys []testAPIKey) fixtureKeyResolver {
t.Helper()
resolver := fixtureKeyResolver{}
Expand All @@ -30,13 +31,15 @@ func newTestAuthenticator(t testing.TB, keys []testAPIKey) fixtureKeyResolver {
if err := p.Validate(); err != nil {
t.Fatalf("invalid fixture principal: %v", err)
}
if digest, err := hex.DecodeString(k.TokenSHA256); err != nil || len(digest) != 32 {
raw, err := hex.DecodeString(k.TokenSHA256)
if err != nil || len(raw) != sha256.Size {
t.Fatalf("invalid fixture digest %q", k.TokenSHA256)
}
if _, exists := resolver[k.TokenSHA256]; exists {
digest := [sha256.Size]byte(raw)
if _, exists := resolver[digest]; exists {
t.Fatalf("duplicate fixture digest %q", k.TokenSHA256)
}
resolver[k.TokenSHA256] = store.ProjectAPIKeyBinding{Key: store.ProjectAPIKey{ID: uuid.NewSHA1(uuid.NameSpaceOID, []byte(k.TokenSHA256)).String(), Name: k.Name, Prefix: "pc_" + k.TokenSHA256[:8]}, Principal: p}
resolver[digest] = projects.KeyBinding{Key: projects.APIKey{ID: uuid.NewSHA1(uuid.NameSpaceOID, []byte(k.TokenSHA256)).String(), Name: k.Name, Prefix: "pc_" + k.TokenSHA256[:8]}, Principal: p}
}
return resolver
}
15 changes: 8 additions & 7 deletions services/core/cmd/server/http_routes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"bufio"
"context"
"crypto/sha256"
"fmt"
"io"
"net"
Expand All @@ -15,8 +16,8 @@ import (
"testing"

"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/projects"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store"
"github.com/google/uuid"
)

Expand Down Expand Up @@ -65,14 +66,14 @@ func TestServerHandlerRoutesCanonicalPaths(t *testing.T) {
}
}

// trapProjects resolves the fixture Project keys; every other call panics.
type trapProjects struct {
api.Projects
// trapProjectsReader resolves the fixture Project keys; every other call panics.
type trapProjectsReader struct {
api.ProjectsReader
keys fixtureKeyResolver
}

func (p trapProjects) ResolveProjectAPIKey(ctx context.Context, digest string) (store.ProjectAPIKeyBinding, error) {
return p.keys.ResolveProjectAPIKey(ctx, digest)
func (p trapProjectsReader) ResolveAPIKey(ctx context.Context, digest [sha256.Size]byte) (projects.KeyBinding, error) {
return p.keys.ResolveAPIKey(ctx, digest)
}

// daemonComposition serves the real API handler beside sentinel daemon routes.
Expand All @@ -87,7 +88,7 @@ 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},
Projects: struct{ api.Projects }{}, ProjectsReader: trapProjectsReader{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 }{},
Expand Down
11 changes: 9 additions & 2 deletions services/core/cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,11 @@ import (
"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/projectpg"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/skillpg"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/templatepg"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/vaultpg"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/projects"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtime"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimeenrollment"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway"
Expand Down Expand Up @@ -146,6 +148,11 @@ func run() error {
if err != nil {
return err
}
projectStore := projectpg.New(units)
projectService, err := projects.NewService(projectStore)
if err != nil {
return err
}
installation, err := installationFacts(public)
if err != nil {
return err
Expand Down Expand Up @@ -222,7 +229,7 @@ func run() error {
return err
}
}
if err := api.ValidateCredentialSeparation(ctx, keyAdmin, executionStore); err != nil {
if err := api.ValidateCredentialSeparation(ctx, keyAdmin, projectStore); err != nil {
return err
}
historyResolver, err := historystoreresolver.NewResolver(executionStore)
Expand Down Expand Up @@ -338,7 +345,7 @@ func run() error {
deps := api.Dependencies{
Engine: engine, Harnesses: kinds, CoreKeys: keyAdmin,
Installation: installation, InstallationBindings: executionStore,
Projects: executionStore,
Projects: projectService, ProjectsReader: projectStore,
ModelProviders: modelConfigurationService, ModelProvidersReader: modelConfigurationStore,
Vaults: vaultService, VaultsReader: vaultStore,
Skills: skillService, SkillsReader: skillStore,
Expand Down
13 changes: 7 additions & 6 deletions services/core/internal/api/admin_resources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,21 @@ import (
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/adminaudit"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/agents"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/projects"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store"
)

const managementProjectID = "22222222-2222-4222-8222-222222222222"

// managementProject resolves managementProjectID to key's Project.
func managementProject(key APIKey) func(context.Context, string) (store.ProjectBinding, error) {
func managementProject(key APIKey) func(context.Context, string) (projects.Binding, error) {
principal := identity.Principal{ProjectScope: identity.ProjectScope{TenantID: key.TenantID, OrganizationID: key.OrganizationID, ProjectID: key.ProjectID}, SubjectKind: key.SubjectKind, SubjectID: key.SubjectID}
return func(_ context.Context, id string) (store.ProjectBinding, error) {
return func(_ context.Context, id string) (projects.Binding, error) {
if id != managementProjectID {
return store.ProjectBinding{}, store.ErrNotFound
return projects.Binding{}, projects.ErrNotFound
}
return store.ProjectBinding{Project: store.Project{ID: id, TenantID: principal.TenantID}, Principal: principal}, nil
return projects.Binding{Project: projects.Project{ID: id, TenantID: principal.TenantID}, Principal: principal}, nil
}
}

Expand All @@ -33,8 +34,8 @@ func managementProject(key APIKey) func(context.Context, string) (store.ProjectB
func managementFakes(t testing.TB, key APIKey) (Dependencies, *testFakes) {
t.Helper()
deps, fakes := testDependencies(t)
fakes.projects.resolveProjectAPIKey = projectKeys(t, key).ResolveProjectAPIKey
fakes.projects.getProject = managementProject(key)
fakes.projectsReader.resolveAPIKey = projectKeys(t, key).ResolveAPIKey
fakes.projectsReader.getProject = managementProject(key)
return deps, fakes
}

Expand Down
Loading
Loading