From f176486cef9b72753c869b96c28b08e5666c6282 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 30 Sep 2026 15:10:52 +0000 Subject: [PATCH 1/3] Give api.NewHandler one explicit Dependencies value NewHandler(ResourceStore, auth, engine, options...) and its 26 With* options become NewHandler(Dependencies). Each application area is one interface field declared beside its handlers, and NewHandler rejects a missing required field. Execution (the Worker, its executor URL and the optional native installer) and Sandboxes (the managed deployment, which requires Execution) are the only optional groups; nil means disabled. The handler no longer discovers capabilities through type assertions and no longer falls back to GetAgent when a store lacks GetAgentForSession. The 503 and 409 answers that only a partially wired test handler could produce are gone. cmd/server builds the one Dependencies value. API tests use strict per-area fakes that fail on any unexpected call. --- contracts/agents-api/core-errors.md | 2 +- contracts/agents-api/core-metrics.md | 2 +- contracts/agents-api/harness-onboarding.md | 2 +- services/core/IMPLEMENTATION.md | 2 + services/core/cmd/server/auth_fixture_test.go | 8 +- .../core/cmd/server/executor_connections.go | 20 + services/core/cmd/server/http_routes_test.go | 32 +- services/core/cmd/server/main.go | 73 +- services/core/cmd/server/managed_setup.go | 7 + services/core/internal/api/admin_history.go | 2 +- services/core/internal/api/admin_resources.go | 34 +- .../core/internal/api/admin_resources_test.go | 74 +- services/core/internal/api/admin_runtime.go | 10 +- .../core/internal/api/admin_runtime_test.go | 42 +- .../internal/api/admin_session_archive.go | 13 +- .../api/admin_session_archive_test.go | 13 +- services/core/internal/api/admin_summary.go | 8 +- services/core/internal/api/agents.go | 22 +- services/core/internal/api/agents_delete.go | 2 +- services/core/internal/api/agents_list.go | 2 +- services/core/internal/api/agents_update.go | 2 +- services/core/internal/api/auth.go | 22 +- .../core/internal/api/auth_fixture_test.go | 21 +- services/core/internal/api/auth_test.go | 14 +- .../internal/api/claude_admission_test.go | 30 +- services/core/internal/api/claude_mcp_test.go | 19 +- .../api/configuration_validation_test.go | 13 +- .../core/internal/api/contract_routes_test.go | 20 +- .../core/internal/api/core_errors_test.go | 8 +- services/core/internal/api/core_metrics.go | 18 +- .../core/internal/api/core_metrics_test.go | 19 +- .../core_model_provider_validation_test.go | 14 +- services/core/internal/api/core_routes.go | 9 +- .../api/core_store_validation_test.go | 13 +- services/core/internal/api/credentials.go | 17 +- .../core/internal/api/credentials_delete.go | 2 +- .../core/internal/api/credentials_list.go | 2 +- .../core/internal/api/credentials_test.go | 11 +- .../core/internal/api/credentials_update.go | 4 +- services/core/internal/api/dependencies.go | 162 +++ .../core/internal/api/dependencies_test.go | 167 +++ .../internal/api/environment_creation_test.go | 70 +- .../api/environment_executor_management.go | 47 +- .../environment_executor_management_test.go | 76 +- .../core/internal/api/environment_files.go | 15 +- .../environment_files_completeness_test.go | 5 +- .../internal/api/environment_files_create.go | 19 +- .../api/environment_files_create_test.go | 35 +- .../api/environment_files_deadline_test.go | 2 +- .../internal/api/environment_files_test.go | 48 +- .../api/environment_files_wire_test.go | 2 +- .../api/environment_files_write_test.go | 9 +- .../core/internal/api/environment_input.go | 2 +- .../internal/api/environment_input_test.go | 16 +- .../internal/api/environment_installation.go | 84 +- .../api/environment_installation_test.go | 19 +- .../internal/api/environment_network_test.go | 2 +- .../internal/api/environment_plugins_test.go | 2 +- .../api/environment_preparation_input_test.go | 2 +- .../internal/api/environment_skills_test.go | 2 +- .../internal/api/environment_templates.go | 14 +- .../api/environment_templates_test.go | 11 +- services/core/internal/api/environments.go | 18 +- .../core/internal/api/environments_test.go | 17 +- .../core/internal/api/execution_policy.go | 9 - services/core/internal/api/fakes_test.go | 1109 +++++++++++++++++ .../api/function_configuration_test.go | 3 +- .../core/internal/api/function_inputs_test.go | 2 +- services/core/internal/api/handler.go | 93 +- services/core/internal/api/handler_test.go | 55 +- services/core/internal/api/harness.go | 14 +- .../internal/api/harness_model_providers.go | 39 +- services/core/internal/api/harness_test.go | 17 +- .../core/internal/api/hosted_environment.go | 6 - .../internal/api/hosted_environment_test.go | 8 +- .../core/internal/api/hosted_failure_test.go | 12 +- .../internal/api/hosted_structured_test.go | 2 +- services/core/internal/api/inputs.go | 16 +- services/core/internal/api/inputs_test.go | 17 +- services/core/internal/api/installation.go | 14 +- .../core/internal/api/installation_test.go | 32 +- services/core/internal/api/items.go | 2 +- services/core/internal/api/items_test.go | 4 +- .../api/model_configuration_route_test.go | 2 +- .../api/model_provider_fixture_test.go | 9 +- .../native_classification_integration_test.go | 2 +- .../core/internal/api/project_api_keys.go | 37 +- .../internal/api/project_api_keys_test.go | 22 +- .../internal/api/resource_creation_test.go | 10 +- .../core/internal/api/resource_query_test.go | 49 +- services/core/internal/api/routing.go | 19 +- services/core/internal/api/routing_test.go | 74 +- services/core/internal/api/runtime_history.go | 17 +- .../core/internal/api/runtime_history_test.go | 19 +- .../core/internal/api/runtime_observations.go | 13 +- .../internal/api/runtime_observations_test.go | 19 +- .../api/sandbox_configuration_discovery.go | 12 +- .../sandbox_configuration_discovery_test.go | 11 +- .../api/sandbox_deployment_changes_test.go | 28 +- .../internal/api/sandbox_deployment_setup.go | 41 +- .../api/sandbox_deployment_setup_test.go | 20 +- services/core/internal/api/sandbox_manager.go | 36 +- .../core/internal/api/sandbox_manager_test.go | 58 +- .../api/sandbox_node_configuration.go | 2 +- .../core/internal/api/sandbox_node_detail.go | 2 +- .../internal/api/sandbox_node_detail_test.go | 15 +- .../internal/api/sandbox_selector_test.go | 8 +- .../core/internal/api/saved_provider_test.go | 15 +- .../internal/api/session_admission_test.go | 16 +- .../core/internal/api/session_artifacts.go | 35 +- .../internal/api/session_artifacts_test.go | 8 +- .../internal/api/session_creation_identity.go | 11 +- .../internal/api/session_creation_stream.go | 40 +- .../api/session_creation_stream_test.go | 72 +- .../core/internal/api/session_credentials.go | 10 +- .../core/internal/api/session_deletion.go | 2 +- .../core/internal/api/session_diagnostics.go | 23 +- .../api/session_diagnostics_deadline_test.go | 2 +- .../session_diagnostics_public_compat_test.go | 21 +- .../internal/api/session_diagnostics_test.go | 22 +- .../api/session_environment_http_test.go | 18 +- .../api/session_execution_configuration.go | 12 +- .../session_execution_configuration_test.go | 5 +- .../api/session_initial_input_test.go | 19 +- .../core/internal/api/session_metadata.go | 2 +- .../api/session_model_configuration.go | 4 +- .../api/session_model_configuration_test.go | 9 +- .../internal/api/session_model_defaults.go | 24 +- .../core/internal/api/session_request_test.go | 3 +- .../core/internal/api/session_response.go | 5 - .../internal/api/session_semantics_test.go | 17 +- .../core/internal/api/session_template.go | 2 +- .../api/session_template_composition_test.go | 9 +- .../api/session_template_null_test.go | 4 +- .../core/internal/api/session_write_audit.go | 13 +- .../internal/api/session_write_audit_test.go | 23 +- services/core/internal/api/skills.go | 38 +- services/core/internal/api/skills_list.go | 10 +- services/core/internal/api/skills_transfer.go | 14 +- services/core/internal/api/source_files.go | 25 +- .../core/internal/api/source_files_content.go | 5 +- .../internal/api/source_files_errors_test.go | 2 +- .../core/internal/api/source_files_list.go | 5 +- .../internal/api/source_files_list_test.go | 10 +- .../core/internal/api/source_files_test.go | 14 +- .../core/internal/api/source_files_upload.go | 5 +- services/core/internal/api/stream.go | 24 +- .../internal/api/stream_authority_test.go | 20 +- services/core/internal/api/stream_test.go | 20 +- services/core/internal/api/subagent_turns.go | 13 +- services/core/internal/api/subagents.go | 27 +- services/core/internal/api/subagents_test.go | 29 +- .../internal/api/text_configuration_test.go | 3 +- services/core/internal/api/turns.go | 16 +- services/core/internal/api/turns_test.go | 6 +- .../internal/api/validation_errors_test.go | 15 +- services/core/internal/api/vaults.go | 16 +- services/core/internal/api/vaults_delete.go | 2 +- services/core/internal/api/vaults_list.go | 2 +- services/core/internal/api/vaults_test.go | 16 +- services/core/internal/api/write_audit.go | 18 +- .../core/internal/api/write_audit_test.go | 31 +- .../providers/configuration_flow_test.go | 44 +- .../admin_session_archive_worker_http_test.go | 6 +- .../agent_execution_defaults_http_test.go | 3 +- .../store/agents_delete_public_test.go | 5 +- .../store/agents_update_public_test.go | 5 +- .../core/internal/store/auth_fixture_test.go | 8 +- .../configuration_validation_public_test.go | 3 +- .../creation_stream_settlement_public_test.go | 7 +- .../store/credential_matrix_http_test.go | 6 +- .../deployment_model_providers_http_test.go | 8 +- ...onment_file_write_semantics_public_test.go | 3 +- .../store/environment_initial_public_test.go | 3 +- .../store/environment_mcp_public_test.go | 3 +- .../store/environment_retrieve_public_test.go | 5 +- .../file_resource_semantics_public_test.go | 3 +- .../store/function_images_native_test.go | 3 +- .../store/function_inputs_public_test.go | 3 +- .../store/function_public_native_test.go | 3 +- .../store/function_state_public_test.go | 3 +- .../internal/store/harness_onboarding_test.go | 3 +- ...sted_initialization_failure_public_test.go | 3 +- .../internal/store/initial_files_http_test.go | 3 +- .../store/input_conflicts_public_test.go | 3 +- .../internal/store/list_cursor_public_test.go | 3 +- .../internal/store/list_query_public_test.go | 3 +- .../store/mcode_public_native_test.go | 3 +- .../mcp_credential_selection_public_test.go | 3 +- .../store/message_images_native_test.go | 3 +- .../store/model_protocol_native_test.go | 3 +- .../store/model_provider_fixture_test.go | 5 +- .../core/internal/store/native_daemon_test.go | 4 +- .../store/native_public_execution_test.go | 3 +- .../store/path_id_semantics_public_test.go | 5 +- .../store/project_api_keys_http_test.go | 6 +- .../store/public_handler_fixture_test.go | 214 ++++ .../core/internal/store/remote_mcp_test.go | 3 +- .../store/request_body_public_test.go | 14 +- .../sandbox_node_auth_order_http_test.go | 6 +- .../store/saved_web_search_public_test.go | 3 +- .../store/self_hosted_cancel_public_test.go | 3 +- .../store/self_hosted_initial_public_test.go | 26 +- .../store/session_agent_filter_public_test.go | 5 +- .../store/session_artifacts_public_test.go | 3 +- .../session_deletion_lifecycle_public_test.go | 3 +- .../store/session_deletion_public_test.go | 5 +- .../store/session_initial_public_test.go | 5 +- .../session_model_execution_http_test.go | 3 +- .../session_reference_retry_public_test.go | 5 +- .../store/skill_selectors_public_test.go | 5 +- .../skill_version_deletion_public_test.go | 3 +- .../core/internal/store/skills_public_test.go | 5 +- .../store/source_files_errors_public_test.go | 3 +- .../store/stream_authority_http_test.go | 7 +- .../store/structured_output_native_test.go | 3 +- .../store/subagent_visibility_public_test.go | 3 +- .../store/template_composition_public_test.go | 3 +- .../template_null_selection_public_test.go | 3 +- .../internal/store/tool_policy_native_test.go | 3 +- .../internal/store/tool_search_native_test.go | 3 +- .../unified_model_configuration_http_test.go | 2 +- .../store/unstorable_text_public_test.go | 3 +- .../store/whitespace_input_public_test.go | 5 +- 224 files changed, 3027 insertions(+), 1784 deletions(-) create mode 100644 services/core/cmd/server/executor_connections.go create mode 100644 services/core/internal/api/dependencies.go create mode 100644 services/core/internal/api/dependencies_test.go delete mode 100644 services/core/internal/api/execution_policy.go create mode 100644 services/core/internal/api/fakes_test.go create mode 100644 services/core/internal/store/public_handler_fixture_test.go diff --git a/contracts/agents-api/core-errors.md b/contracts/agents-api/core-errors.md index a07923361..be9440ac4 100644 --- a/contracts/agents-api/core-errors.md +++ b/contracts/agents-api/core-errors.md @@ -101,6 +101,6 @@ The [Session and Turn diagnostics reads](session-diagnostics.md) return these ca | `environment_unavailable` | Environment unavailable for initial input | | `environment_provisioning_failed` | Hosted provisioning failure; params contain nullable `step`, `index`, `exit_code` from a sanitized receipt | -When the diagnostics reader is not configured, the reads return 503 `diagnostics_unavailable` without details. A database failure is an error, never an empty or healthy snapshot. Provisioning reasons and native messages are never parsed for categories or parameters. +A database failure is an error, never an empty or healthy snapshot. Provisioning reasons and native messages are never parsed for categories or parameters. Native categories apply only to a failed Turn whose outcome has `error_code: engine_failed`. Core accepts only the listed `engine_error_code` values; an unknown, malformed or absent value stays `harness_error`. Only `connection_failed` uses `engine_http_status`. Nested metadata and provider text never classify a failure. Core storage, incomplete-stream and cancellation failures take precedence, and cancelled or completed Turns have no failure. [Native error classification](../../docs/runtime-protocol.md#native-failure-classification) lists which adapters report each category. diff --git a/contracts/agents-api/core-metrics.md b/contracts/agents-api/core-metrics.md index f5cf8cb85..70da7bb13 100644 --- a/contracts/agents-api/core-metrics.md +++ b/contracts/agents-api/core-metrics.md @@ -2,7 +2,7 @@ `GET /core/v1/metrics?range=1h|6h|24h|7d` reports Core's own health: its process, execution queue and slots, PostgreSQL and background jobs. It requires the Core key ([Core administration API](admin-api.md)). -`range` is the only parameter, sent at most once; it defaults to `1h`. An empty, repeated or unsupported value, or any other parameter, returns 400 `invalid_request`. When Core has no metrics service, or cannot read it, the route returns 503 `core_metrics_unavailable`. When only some measurements fail, the response is still `200` with `service.status` set to `degraded` and each missing value set to null. The response never contains database or native error text, credentials, bodies, resource IDs or tenant labels. +`range` is the only parameter, sent at most once; it defaults to `1h`. An empty, repeated or unsupported value, or any other parameter, returns 400 `invalid_request`. When Core cannot read its metrics, the route returns 503 `core_metrics_unavailable`. When only some measurements fail, the response is still `200` with `service.status` set to `degraded` and each missing value set to null. The response never contains database or native error text, credentials, bodies, resource IDs or tenant labels. ## Time and missing data diff --git a/contracts/agents-api/harness-onboarding.md b/contracts/agents-api/harness-onboarding.md index ea91e96de..027d2b126 100644 --- a/contracts/agents-api/harness-onboarding.md +++ b/contracts/agents-api/harness-onboarding.md @@ -194,7 +194,7 @@ An omitted or unknown policy, missing required callback, or callback paired with Run the `engine` and `execution` tests for omission, policy, combination and error precedence coverage, and the public onboarding/store tests for admission and Runtime dispatch. Test fixtures use `engine/enginetest`, whose exhaustive literal also requires a decision when a field is added; it is not a production profile. -`execution.Policy` supplies immutable service qualification to HTTP admission, Worker device selection and final dispatch. Custom composition gives the same Policy to `api.WithExecutionPolicy` and the Core dispatcher's `Policy`. The zero value uses the built-in profiles; an explicitly empty catalog authorizes none. There is no mutable global registration. +`execution.Policy` supplies immutable service qualification to HTTP admission, Worker device selection and final dispatch. Custom composition gives the same Policy to `api.Dependencies.Policy` and the Core dispatcher's `Policy`. The zero value uses the built-in profiles; an explicitly empty catalog authorizes none. There is no mutable global registration. ## Native model configuration diff --git a/services/core/IMPLEMENTATION.md b/services/core/IMPLEMENTATION.md index d0b27e917..525bbe8c0 100644 --- a/services/core/IMPLEMENTATION.md +++ b/services/core/IMPLEMENTATION.md @@ -4,6 +4,8 @@ These are the code-level rules of `services/core` that no contract states. Contr ## Layering +`api.NewHandler` takes one `api.Dependencies` value, built only in `cmd/server`. Each application area is one field typed as an interface declared in `api` beside its handlers, listing exactly the methods they call. Every field is required and `NewHandler` rejects a missing one, except the optional groups whose comments say what nil means: `Execution` is nil without an execution Worker, `Sandboxes` is nil without a managed sandbox installation and requires `Execution`, and `Execution.NativeInstaller` is nil for a build without a source revision. Handlers never discover a capability by type assertion or fall back to another implementation. API tests use one strict fake per area, `fake`, which fails the test on any call the test did not set. + `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. `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`. diff --git a/services/core/cmd/server/auth_fixture_test.go b/services/core/cmd/server/auth_fixture_test.go index ee6a509fe..7cf61c11f 100644 --- a/services/core/cmd/server/auth_fixture_test.go +++ b/services/core/cmd/server/auth_fixture_test.go @@ -5,7 +5,6 @@ import ( "encoding/hex" "errors" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" @@ -20,7 +19,10 @@ func (f fixtureKeyResolver) ResolveProjectAPIKey(_ context.Context, digest strin } return store.ProjectAPIKeyBinding{}, store.ErrNotFound } -func newTestAuthenticator(keys []testAPIKey) (*api.Authenticator, error) { + +// newTestAuthenticator binds each key's digest to its Principal, as the Project +// store does. +func newTestAuthenticator(keys []testAPIKey) (fixtureKeyResolver, error) { resolver := fixtureKeyResolver{} for _, k := range keys { p := identity.Principal{ProjectScope: identity.ProjectScope{TenantID: k.TenantID, OrganizationID: k.OrganizationID, ProjectID: k.ProjectID}, SubjectKind: k.SubjectKind, SubjectID: k.SubjectID} @@ -36,5 +38,5 @@ func newTestAuthenticator(keys []testAPIKey) (*api.Authenticator, error) { } 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} } - return api.NewDatabaseAuthenticator(resolver) + return resolver, nil } diff --git a/services/core/cmd/server/executor_connections.go b/services/core/cmd/server/executor_connections.go new file mode 100644 index 000000000..9c8138c3f --- /dev/null +++ b/services/core/cmd/server/executor_connections.go @@ -0,0 +1,20 @@ +package main + +import ( + "context" + + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimeenrollment" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" +) + +// executorConnections observes enrolled executors through the Runtime gateway. +// registry is nil when this Core has no gateway; no executor is then connected. +type executorConnections struct { + store *store.Store + registry *runtimegateway.Registry +} + +func (c executorConnections) ExecutorConnected(ctx context.Context, environment, digest string) (bool, error) { + return runtimeenrollment.RuntimeConnected(ctx, c.store, c.registry, environment, digest) +} diff --git a/services/core/cmd/server/http_routes_test.go b/services/core/cmd/server/http_routes_test.go index 8bc464d58..eb3c9c066 100644 --- a/services/core/cmd/server/http_routes_test.go +++ b/services/core/cmd/server/http_routes_test.go @@ -65,20 +65,21 @@ func TestServerHandlerRoutesCanonicalPaths(t *testing.T) { } } -// trapStore panics on every store call, marking a request that reached a handler. -type trapStore struct{ api.ResourceStore } - -// trapKeys finds no derived project API key; key management calls panic. -type trapKeys struct{ api.ProjectAPIKeyStore } +// trapProjects resolves the fixture Project keys; every other call panics. +type trapProjects struct { + api.Projects + keys fixtureKeyResolver +} -func (trapKeys) ResolveProjectAPIKey(context.Context, string) (store.ProjectAPIKeyBinding, error) { - return store.ProjectAPIKeyBinding{}, store.ErrNotFound +func (p trapProjects) ResolveProjectAPIKey(ctx context.Context, digest string) (store.ProjectAPIKeyBinding, error) { + return p.keys.ResolveProjectAPIKey(ctx, digest) } // daemonComposition serves the real API handler beside sentinel daemon routes. +// Every dependency call panics, marking a request that reached a handler. func daemonComposition(t testing.TB) http.Handler { t.Helper() - auth, err := newTestAuthenticator([]testAPIKey{{OrganizationID: "org", ProjectID: "project", SubjectKind: "service_account", + keys, err := newTestAuthenticator([]testAPIKey{{OrganizationID: "org", ProjectID: "project", SubjectKind: "service_account", SubjectID: "runner", TokenSHA256: runtimedevice.HashCredential("project-key"), TenantID: uuid.NewString()}}) if err != nil { t.Fatal(err) @@ -87,7 +88,20 @@ func daemonComposition(t testing.TB) http.Handler { if err != nil { t.Fatal(err) } - apiHandler, err := api.NewHandler(trapStore{}, auth, "codex", api.WithSandboxManager(&store.Store{}, admin), api.WithProjectAPIKeys(trapKeys{}, admin)) + apiHandler, err := api.NewHandler(api.Dependencies{ + Engine: "codex", CoreKeys: admin, InstallationBindings: struct{ api.InstallationBindings }{}, + Projects: trapProjects{keys: keys}, Vaults: struct{ api.Vaults }{}, ModelProviders: struct{ api.ModelProviders }{}, + Files: struct{ api.Files }{}, Skills: struct{ api.Skills }{}, EnvironmentTemplates: struct{ api.EnvironmentTemplates }{}, + Agents: struct{ api.Agents }{}, Sessions: struct{ api.Sessions }{}, SessionEvents: struct{ api.SessionEvents }{}, + SessionHistory: struct{ api.SessionHistory }{}, Subagents: struct{ api.Subagents }{}, Artifacts: struct{ api.Artifacts }{}, + SessionAdmin: struct{ api.SessionAdmin }{}, Environments: struct{ api.Environments }{}, ExecutorConnections: struct{ api.ExecutorConnections }{}, + Admin: struct{ api.Admin }{}, WriteAudit: struct{ api.WriteAudit }{}, Metrics: struct{ api.Metrics }{}, + RuntimeObservations: struct{ api.RuntimeObservations }{}, RuntimeHistory: struct{ api.RuntimeHistory }{}, + Execution: &api.Execution{ExecutorURL: "wss://core.example/api/v1/agent-daemon/ws", Admission: struct{ api.Admission }{}, + SessionArchive: struct{ api.SessionArchive }{}, Workspaces: struct{ api.EnvironmentWorkspaces }{}}, + Sandboxes: &api.Sandboxes{Deployment: struct{ api.Deployment }{}, DeploymentChanges: struct{ api.DeploymentChanges }{}, + ConfigurationDiscovery: struct{ api.ConfigurationDiscovery }{}}, + }) if err != nil { t.Fatal(err) } diff --git a/services/core/cmd/server/main.go b/services/core/cmd/server/main.go index 06618e47f..7bb1c2929 100644 --- a/services/core/cmd/server/main.go +++ b/services/core/cmd/server/main.go @@ -23,7 +23,6 @@ package main import ( "context" - "encoding/json" "errors" "net/http" "os" @@ -45,8 +44,6 @@ import ( historystoreresolver "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimehistory/storeresolver" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimeobs" observationstoreresolver "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimeobs/storeresolver" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/providers" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/jackc/pgx/v5/pgxpool" ) @@ -115,10 +112,6 @@ func run() error { } metricsSource := &coreMetricsSource{store: executionStore, pool: pool} metrics := coremetrics.New(processStartedAt, buildRevision, metricsSource) - auth, err := api.NewDatabaseAuthenticator(executionStore) - if err != nil { - return err - } auditRetention, err := writeAuditRetention() if err != nil { return err @@ -188,13 +181,6 @@ func run() error { runHistoryCleanup(cleanupCtx, history.Prune, metrics) }() defer func() { cancelCleanup(); <-cleanupDone }() - options := []api.Option{api.WithCoreMetrics(metrics), api.WithSubagents(executionStore), api.WithSkills(executionStore), api.WithSourceFiles(executionStore), api.WithSessionArtifacts(executionStore), api.WithRuntimeObservations(observationService)} - if managedNodes != nil { - options = append(options, api.WithSandboxManager(executionStore, managedNodes.admin)) - options = append(options, api.WithSandboxConfigurationDiscovery(func(ctx context.Context, kind string, input sandbox.ConfigurationDiscoveryInput) (json.RawMessage, error) { - return providers.DiscoverConfiguration(ctx, kind, input, managedNodes.setup.processPaths) - })) - } var keyAdmin *api.DeploymentAuthenticator if managedNodes != nil { keyAdmin = managedNodes.admin @@ -207,27 +193,24 @@ func run() error { if err := api.ValidateCredentialSeparation(ctx, keyAdmin, executionStore); err != nil { return err } - options = append(options, api.WithProjectAPIKeys(executionStore, keyAdmin), api.WithWriteAudit(executionStore, keyAdmin), api.WithAdminManagement(executionStore), - api.WithInstallation(installation, executionStore.AddressBindings)) - if history.Reader != nil { - historyResolver, resolverErr := historystoreresolver.NewResolver(executionStore) - if resolverErr != nil { - return resolverErr - } - historyService, serviceErr := runtimehistory.NewService(historyResolver, history.Reader) - if serviceErr != nil { - return serviceErr - } - options = append(options, api.WithRuntimeHistory(historyService)) + historyResolver, err := historystoreresolver.NewResolver(executionStore) + if err != nil { + return err + } + historyService, err := runtimehistory.NewService(historyResolver, history.Reader) + if err != nil { + return err } var daemonHandler http.Handler var registry *runtimegateway.Registry + var executorURL string + var nativeInstaller *api.NativeInstaller if public != "" { - wsURL, err := runtimeWebSocketURL(public) + executorURL, err = runtimeWebSocketURL(public) if err != nil { return err } - daemonHandler, registry, err = runtime.NewGateway(executionStore, wsURL) + daemonHandler, registry, err = runtime.NewGateway(executionStore, executorURL) if err != nil { return err } @@ -245,11 +228,10 @@ func run() error { return err } } - options = append(options, api.WithEnvironmentRemoteURL(wsURL), api.WithNativeInstaller(catalog, buildRevision)) + if buildRevision != "" { + nativeInstaller = &api.NativeInstaller{Version: buildRevision, Catalog: catalog} + } } - options = append(options, api.WithExecutorConnections(func(ctx context.Context, environment, digest string) (bool, error) { - return runtimeenrollment.RuntimeConnected(ctx, executionStore, registry, environment, digest) - })) if registry != nil { dispatcher := &execution.Dispatcher{Store: executionStore, Registry: registry, ManagedRuntimes: managed, MaxConcurrentExecutions: concurrency} @@ -258,9 +240,6 @@ func run() error { if err != nil { return err } - if managedNodes != nil && managedNodes.setup != nil { - options = append(options, api.WithSandboxDeploymentSetup(worker.InitializeSandboxDeployment), api.WithSandboxDeploymentChanges(worker.UpdateSandboxDeployment, worker.StartSandboxReset, worker.CancelSandboxReset)) - } workerDone = make(chan error, 1) go func() { workerDone <- worker.Run(ctx) }() defer func() { @@ -269,11 +248,6 @@ func run() error { <-workerDone } }() - options = append(options, api.WithExecution(worker), api.WithSessionArchive(worker.ArchiveManagedSession), api.WithEnvironmentDirectoryReader(worker), api.WithEnvironmentFileWriter(worker)) - options = append(options, api.WithHarnesses(kinds), api.WithModelProviderDefaults(executionStore.DeploymentModelProvider)) - if managed != nil { - options = append(options, api.WithHostedEnvironments()) - } } if history.SampleInterval == 0 { metrics.StopJob("runtime_sampler") @@ -319,7 +293,24 @@ func run() error { metricsDone := make(chan struct{}) go func() { defer close(metricsDone); metrics.Run(metricsCtx) }() defer func() { cancelMetrics(); <-metricsDone }() - handler, err := api.NewHandler(executionStore, auth, engine, options...) + deps := api.Dependencies{ + Engine: engine, Harnesses: kinds, CoreKeys: keyAdmin, + Installation: installation, InstallationBindings: executionStore, + Projects: executionStore, Vaults: executionStore, ModelProviders: executionStore, Files: executionStore, + Skills: executionStore, EnvironmentTemplates: executionStore, Agents: executionStore, + Sessions: executionStore, SessionEvents: executionStore, SessionHistory: executionStore, + Subagents: executionStore, Artifacts: executionStore, SessionAdmin: executionStore, + Environments: executionStore, ExecutorConnections: executorConnections{store: executionStore, registry: registry}, + Admin: executionStore, WriteAudit: executionStore, Metrics: metrics, + RuntimeObservations: observationService, RuntimeHistory: historyService, + } + if worker != nil { + deps.Execution = &api.Execution{ExecutorURL: executorURL, Admission: worker, SessionArchive: worker, Workspaces: worker, NativeInstaller: nativeInstaller} + } + if managedNodes != nil { + deps.Sandboxes = &api.Sandboxes{Deployment: executionStore, DeploymentChanges: worker, ConfigurationDiscovery: managedNodes.setup} + } + handler, err := api.NewHandler(deps) if err != nil { return err } diff --git a/services/core/cmd/server/managed_setup.go b/services/core/cmd/server/managed_setup.go index 3ec771c93..5cffa21b0 100644 --- a/services/core/cmd/server/managed_setup.go +++ b/services/core/cmd/server/managed_setup.go @@ -2,6 +2,7 @@ package main import ( "context" + "encoding/json" "errors" "fmt" "sync/atomic" @@ -33,6 +34,12 @@ type managedSetup struct { providerCalls sandbox.CallFence } +// DiscoverConfiguration asks a Provider which configuration values its +// credential can use, with this installation's process paths. +func (s *managedSetup) DiscoverConfiguration(ctx context.Context, provider string, input sandbox.ConfigurationDiscoveryInput) (json.RawMessage, error) { + return providers.DiscoverConfiguration(ctx, provider, input, s.processPaths) +} + // Empty selections retain their generation so a delayed provider load cannot // republish a backend retired by reset. type managedSelection struct { diff --git a/services/core/internal/api/admin_history.go b/services/core/internal/api/admin_history.go index 819c26087..8e1a8e178 100644 --- a/services/core/internal/api/admin_history.go +++ b/services/core/internal/api/admin_history.go @@ -60,7 +60,7 @@ func (h *Handler) listAdminAudit(w http.ResponseWriter, r *http.Request) { writeStoreError(w, r, err) return } - page, err := h.adminManagement.ListAdminAudit(r.Context(), filter) + page, err := h.Admin.ListAdminAudit(r.Context(), filter) if err != nil { writeStoreError(w, r, err) return diff --git a/services/core/internal/api/admin_resources.go b/services/core/internal/api/admin_resources.go index e0fce76dd..51e310b62 100644 --- a/services/core/internal/api/admin_resources.go +++ b/services/core/internal/api/admin_resources.go @@ -12,17 +12,14 @@ import ( // Only Core-key-authenticated project resource handlers receive it. type adminTenantContextKey struct{} -type AdminManagementStore interface { - GetManagedSessionArchive(context.Context, string, string) (store.ManagedSessionArchive, error) +// Admin reads the administrator's cross-Project views: the asset summary, the +// Sessions whose Runtime is observed and the administrator audit log. +type Admin interface { ReadAdminSummary(context.Context, string, store.AdminSummaryFilter, func(store.Session, *string) error) (store.AdminAssetCounts, error) ListAdminRuntimeTargets(context.Context, []string, string, int, bool) (store.AdminRuntimeTargetPage, error) ListAdminAudit(context.Context, store.AdminAuditFilter) (store.AdminAuditPage, error) } -func WithAdminManagement(s AdminManagementStore) Option { - return func(h *Handler) { h.adminManagement = s } -} - func (h *Handler) adminResourceScope(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { binding, ok := h.adminProjectScope(w, r) @@ -34,17 +31,12 @@ func (h *Handler) adminResourceScope(next http.Handler) http.Handler { } func (h *Handler) registerAdminResourceRoutes(router chi.Router) { - if h.projectKeys == nil { - return - } router.Group(func(r chi.Router) { r.Get("/metrics", h.getCoreMetrics) - if h.adminManagement != nil { - r.Get("/summary", h.adminSummary) - r.Get("/sandbox/runtime-observations", h.adminRuntimeObservations) - r.Head("/sandbox/runtime-observations", methodNotAllowed) - r.Get("/audit-log", h.listAdminAudit) - } + r.Get("/summary", h.adminSummary) + r.Get("/sandbox/runtime-observations", h.adminRuntimeObservations) + r.Head("/sandbox/runtime-observations", methodNotAllowed) + r.Get("/audit-log", h.listAdminAudit) r.Group(func(r chi.Router) { r.Use(h.adminResourceScope) r.Get("/projects/{project_id}/agents", h.adminListAgents) @@ -77,10 +69,8 @@ func (h *Handler) registerAdminResourceRoutes(router chi.Router) { r.Get("/projects/{project_id}/sessions/{session_id}/diagnostics", h.getSessionDiagnostics) r.Get("/projects/{project_id}/sessions/{session_id}/turns/{turn_id}/diagnostics", h.getTurnDiagnostics) r.Delete("/projects/{project_id}/sessions/{session_id}", h.adminDeleteSession) - if h.adminManagement != nil { - r.Post("/projects/{project_id}/sessions/{session_id}/archive", h.adminArchiveSession) - r.Get("/projects/{project_id}/sessions/{session_id}/archive", h.adminGetSessionArchive) - } + r.Post("/projects/{project_id}/sessions/{session_id}/archive", h.adminArchiveSession) + r.Get("/projects/{project_id}/sessions/{session_id}/archive", h.adminGetSessionArchive) r.Get("/projects/{project_id}/sessions/{session_id}/turns", h.adminListTurns) r.Get("/projects/{project_id}/sessions/{session_id}/turns/{turn_id}", h.adminGetTurn) r.Get("/projects/{project_id}/sessions/{session_id}/items", h.adminListItems) @@ -94,10 +84,8 @@ func (h *Handler) registerAdminResourceRoutes(router chi.Router) { r.Head("/projects/{project_id}/sessions/{session_id}/runtime-observation", methodNotAllowed) r.Get("/projects/{project_id}/sessions/{session_id}/runtime-history", h.adminGetRuntimeHistory) r.Head("/projects/{project_id}/sessions/{session_id}/runtime-history", methodNotAllowed) - if h.writeAudit != nil { - r.Get("/projects/{project_id}/resource-owners", h.getResourceOwners) - r.Get("/projects/{project_id}/write-operations", h.listWriteOperations) - } + r.Get("/projects/{project_id}/resource-owners", h.getResourceOwners) + r.Get("/projects/{project_id}/write-operations", h.listWriteOperations) }) }) } diff --git a/services/core/internal/api/admin_resources_test.go b/services/core/internal/api/admin_resources_test.go index 365ceac94..151806cc9 100644 --- a/services/core/internal/api/admin_resources_test.go +++ b/services/core/internal/api/admin_resources_test.go @@ -10,58 +10,50 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/adminaudit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) const managementProjectID = "22222222-2222-4222-8222-222222222222" -type adminProjectFixture struct { - ProjectAPIKeyStore - principal identity.Principal -} - -func managementProjectStore(key APIKey) *adminProjectFixture { - return &adminProjectFixture{principal: identity.Principal{ProjectScope: identity.ProjectScope{TenantID: key.TenantID, OrganizationID: key.OrganizationID, ProjectID: key.ProjectID}, SubjectKind: key.SubjectKind, SubjectID: key.SubjectID}} -} - -func (s *adminProjectFixture) GetProject(_ context.Context, id string) (store.ProjectBinding, error) { - if id != managementProjectID { - return store.ProjectBinding{}, store.ErrNotFound +// managementProject resolves managementProjectID to key's Project. +func managementProject(key APIKey) func(context.Context, string) (store.ProjectBinding, 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) { + if id != managementProjectID { + return store.ProjectBinding{}, store.ErrNotFound + } + return store.ProjectBinding{Project: store.Project{ID: id, TenantID: principal.TenantID}, Principal: principal}, nil } - return store.ProjectBinding{Project: store.Project{ID: id, TenantID: s.principal.TenantID}, Principal: s.principal}, nil } -func (s *adminProjectFixture) ResolveProjectAPIKey(_ context.Context, _ string) (store.ProjectAPIKeyBinding, error) { - return store.ProjectAPIKeyBinding{}, store.ErrNotFound +// managementFakes authenticates key as a Project key and resolves +// managementProjectID to its Project. The Core key is "admin". +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) + return deps, fakes } // adminTestHandler serves the administrator routes, authenticated by "Bearer // admin", for managementProjectID over the same recording store as testHandler. // It returns that Project's tenant. -func adminTestHandler(t *testing.T, options ...Option) (http.Handler, *recordingStore, string) { +func adminTestHandler(t *testing.T, configure ...func(*Dependencies, *testFakes)) (http.Handler, *recordingStore, string) { t.Helper() key := callerBinding() - auth, err := NewAuthenticator([]APIKey{key}) - if err != nil { - t.Fatal(err) - } - admin, err := NewDeploymentAuthenticator([]string{runtimedevice.HashCredential("admin")}) - if err != nil { - t.Fatal(err) - } + deps, fakes := managementFakes(t, key) s := &recordingStore{} - h, err := NewHandler(s, auth, "codex", append([]Option{WithProjectAPIKeys(managementProjectStore(key), admin)}, options...)...) - if err != nil { - t.Fatal(err) + s.record(fakes) + for _, c := range configure { + c(&deps, fakes) } - return h, s, key.TenantID + return newTestHandler(t, deps), s, key.TenantID } const adminSessionsPath = "/core/v1/projects/" + managementProjectID + "/sessions/" type adminReadFixture struct { - ResourceStore seenTenant string administrative, impersonated bool } @@ -80,16 +72,10 @@ func (s *adminReadFixture) DeleteAgent(ctx context.Context, tenant, id string) ( } func TestAdminResourcesHaveExplicitTargetWithoutCallerImpersonation(t *testing.T) { key := callerBinding() - auth, err := NewAuthenticator([]APIKey{key}) - if err != nil { - t.Fatal(err) - } - admin, _ := NewDeploymentAuthenticator([]string{runtimedevice.HashCredential("admin")}) + deps, fakes := managementFakes(t, key) resources := &adminReadFixture{} - h, err := NewHandler(resources, auth, "codex", WithProjectAPIKeys(managementProjectStore(key), admin)) - if err != nil { - t.Fatal(err) - } + fakes.agents.listAgents, fakes.agents.deleteAgent = resources.ListAgents, resources.DeleteAgent + h := newTestHandler(t, deps) base := "/core/v1/projects/" + managementProjectID for _, test := range []struct { method, path string @@ -127,7 +113,6 @@ func TestAdminResourcesHaveExplicitTargetWithoutCallerImpersonation(t *testing.T } type summaryFixture struct { - AdminManagementStore tenant string filter store.AdminSummaryFilter } @@ -147,13 +132,10 @@ func (s *summaryFixture) ReadAdminSummary(_ context.Context, tenant string, filt } func TestAdminSummaryUsesPublicStateAndNullUsageCoverage(t *testing.T) { key := callerBinding() - auth, _ := NewAuthenticator([]APIKey{key}) - admin, _ := NewDeploymentAuthenticator([]string{runtimedevice.HashCredential("admin")}) + deps, fakes := managementFakes(t, key) fixture := &summaryFixture{} - h, err := NewHandler(&recordingStore{}, auth, "codex", WithProjectAPIKeys(managementProjectStore(key), admin), WithAdminManagement(fixture)) - if err != nil { - t.Fatal(err) - } + fakes.admin.readAdminSummary = fixture.ReadAdminSummary + h := newTestHandler(t, deps) base := "/core/v1/summary?project_id=" + managementProjectID + "&created_after=1970-01-01T00:00:00Z&created_before=2030-01-01T00:00:00Z" for _, group := range []string{"project", "key", "agent"} { w := projectKeyHTTP(h, http.MethodGet, base+"&group_by="+group, "admin", "") diff --git a/services/core/internal/api/admin_runtime.go b/services/core/internal/api/admin_runtime.go index 6b30b4048..0506e3903 100644 --- a/services/core/internal/api/admin_runtime.go +++ b/services/core/internal/api/admin_runtime.go @@ -47,10 +47,6 @@ type AdminRuntimeObservationList struct { // @Failure 400,401,404,500,503 {object} CoreErrorResponse // @Router /core/v1/sandbox/runtime-observations [get] func (h *Handler) adminRuntimeObservations(w http.ResponseWriter, r *http.Request) { - if h.runtimeObservations == nil { - writeError(w, http.StatusServiceUnavailable, "execution_unavailable", "Runtime observation is not configured on this service.") - return - } options, ok := readPage(w, r) if !ok { return @@ -61,7 +57,7 @@ func (h *Handler) adminRuntimeObservations(w http.ResponseWriter, r *http.Reques projectByTenant := map[string]string{} cursor := "" for { - projects, err := h.listAdminProjects(ctx, cursor, 100, true) + projects, err := h.Projects.ListProjects(ctx, cursor, 100, true) if err != nil { writeStoreError(w, r, err) return @@ -75,7 +71,7 @@ func (h *Handler) adminRuntimeObservations(w http.ResponseWriter, r *http.Reques break } } - page, err := h.adminManagement.ListAdminRuntimeTargets(ctx, tenants, options.after, options.limit, options.ascending) + page, err := h.Admin.ListAdminRuntimeTargets(ctx, tenants, options.after, options.limit, options.ascending) if err != nil { writeStoreError(w, r, err) return @@ -84,7 +80,7 @@ func (h *Handler) adminRuntimeObservations(w http.ResponseWriter, r *http.Reques for index, target := range page.Data { sessions[index] = runtimeobs.SessionIdentity{TenantID: target.TenantID, SessionID: target.SessionID} } - observations, errs := h.runtimeObservations.ObserveSessions(ctx, sessions, runtimeObservationPage) + observations, errs := h.RuntimeObservations.ObserveSessions(ctx, sessions, runtimeObservationPage) if ctx.Err() != nil { writeError(w, http.StatusServiceUnavailable, "execution_unavailable", "Runtime observation collection exceeded its request budget.") return diff --git a/services/core/internal/api/admin_runtime_test.go b/services/core/internal/api/admin_runtime_test.go index 8c1e0e7a1..5590d9cc1 100644 --- a/services/core/internal/api/admin_runtime_test.go +++ b/services/core/internal/api/admin_runtime_test.go @@ -9,23 +9,12 @@ import ( "testing" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimeobs" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" ) -type adminRuntimeProjects struct { - ProjectAPIKeyStore - projects []store.Project -} - -func (s adminRuntimeProjects) ListProjects(context.Context, string, int, bool) (store.ProjectPage, error) { - return store.ProjectPage{Data: s.projects}, nil -} - type adminRuntimeTargets struct { - AdminManagementStore page store.AdminRuntimeTargetPage tenants []string after string @@ -42,33 +31,16 @@ const adminRuntimeObservationsPath = "/core/v1/sandbox/runtime-observations" // adminRuntimeFixture serves the administrator observation list over one // Project per tenant and the given Session targets. -func adminRuntimeFixture(t *testing.T, projects []store.Project, targets []store.AdminRuntimeTarget, service RuntimeObservationService) (http.Handler, *adminRuntimeTargets) { +func adminRuntimeFixture(t *testing.T, projects []store.Project, targets []store.AdminRuntimeTarget, service RuntimeObservations) (http.Handler, *adminRuntimeTargets) { t.Helper() - auth, err := NewAuthenticator([]APIKey{callerBinding()}) - if err != nil { - t.Fatal(err) - } - admin, err := NewDeploymentAuthenticator([]string{runtimedevice.HashCredential("admin")}) - if err != nil { - t.Fatal(err) + deps, fakes := managementFakes(t, callerBinding()) + fakes.projects.listProjects = func(context.Context, string, int, bool) (store.ProjectPage, error) { + return store.ProjectPage{Data: projects}, nil } management := &adminRuntimeTargets{page: store.AdminRuntimeTargetPage{Data: targets, HasMore: true}} - options := []Option{WithProjectAPIKeys(adminRuntimeProjects{ProjectAPIKeyStore: managementProjectStore(callerBinding()), projects: projects}, admin), WithAdminManagement(management)} - if service != nil { - options = append(options, WithRuntimeObservations(service)) - } - h, err := NewHandler(&recordingStore{}, auth, "codex", options...) - if err != nil { - t.Fatal(err) - } - return h, management -} - -func TestAdminRuntimeObservationListRequiresConfiguredService(t *testing.T) { - handler, _ := adminRuntimeFixture(t, nil, nil, nil) - if response := runtimeObservationRequest(handler, adminRuntimeObservationsPath); response.Code != http.StatusServiceUnavailable { - t.Fatalf("unconfigured service returned %d: %s", response.Code, response.Body) - } + fakes.admin.listAdminRuntimeTargets = management.ListAdminRuntimeTargets + observeWith(service)(&deps, fakes) + return newTestHandler(t, deps), management } // HEAD never samples Runtime or queries history on the administrator routes. diff --git a/services/core/internal/api/admin_session_archive.go b/services/core/internal/api/admin_session_archive.go index 3e8816bce..f450318ff 100644 --- a/services/core/internal/api/admin_session_archive.go +++ b/services/core/internal/api/admin_session_archive.go @@ -8,9 +8,10 @@ import ( "github.com/go-chi/chi/v5" ) -// WithSessionArchive binds the execution owner; management reads use the ordinary Store. -func WithSessionArchive(archive func(context.Context, string, string, uint64) (store.ManagedSessionArchive, error)) Option { - return func(h *Handler) { h.adminArchive = archive } +// SessionArchive archives a managed Session through the execution owner; +// SessionAdmin reads its archive state. +type SessionArchive interface { + ArchiveManagedSession(context.Context, string, string, uint64) (store.ManagedSessionArchive, error) } type AdminSessionArchiveRequest struct { @@ -39,11 +40,11 @@ func (h *Handler) adminArchiveSession(w http.ResponseWriter, r *http.Request) { writeStoreError(w, r, store.ErrInvalidInput) return } - if h.adminArchive == nil { + if h.Execution == nil { writeStoreError(w, r, store.ErrEnvironmentUnavailable) return } - result, err := h.adminArchive(r.Context(), tenantID(r), chi.URLParam(r, "session_id"), input.ExpectedGeneration) + result, err := h.Execution.SessionArchive.ArchiveManagedSession(r.Context(), tenantID(r), chi.URLParam(r, "session_id"), input.ExpectedGeneration) if err != nil { writeStoreError(w, r, err) return @@ -62,7 +63,7 @@ func (h *Handler) adminArchiveSession(w http.ResponseWriter, r *http.Request) { // @Failure 400,401,404,500,503 {object} CoreErrorResponse // @Router /core/v1/projects/{project_id}/sessions/{session_id}/archive [get] func (h *Handler) adminGetSessionArchive(w http.ResponseWriter, r *http.Request) { - result, err := h.adminManagement.GetManagedSessionArchive(r.Context(), tenantID(r), chi.URLParam(r, "session_id")) + result, err := h.SessionAdmin.GetManagedSessionArchive(r.Context(), tenantID(r), chi.URLParam(r, "session_id")) if err != nil { writeStoreError(w, r, err) return diff --git a/services/core/internal/api/admin_session_archive_test.go b/services/core/internal/api/admin_session_archive_test.go index b96c3286a..68ce043fc 100644 --- a/services/core/internal/api/admin_session_archive_test.go +++ b/services/core/internal/api/admin_session_archive_test.go @@ -7,12 +7,10 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/adminaudit" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) type archiveManagementFixture struct { - AdminManagementStore tenant, session string generation uint64 calls int @@ -37,13 +35,12 @@ func (s *archiveManagementFixture) GetManagedSessionArchive(_ context.Context, t func TestAdminSessionArchiveAuthorityAndValidation(t *testing.T) { key := callerBinding() - auth, _ := NewAuthenticator([]APIKey{key}) - admin, _ := NewDeploymentAuthenticator([]string{runtimedevice.HashCredential("admin")}) + deps, fakes := managementFakes(t, key) fixture := &archiveManagementFixture{} - h, err := NewHandler(&recordingStore{}, auth, "codex", WithProjectAPIKeys(managementProjectStore(key), admin), WithAdminManagement(fixture), WithSessionArchive(fixture.ArchiveManagedSession)) - if err != nil { - t.Fatal(err) - } + deps.Execution = fakes.execution() + fakes.sessionArchive.archiveManagedSession = fixture.ArchiveManagedSession + fakes.sessionAdmin.getManagedSessionArchive = fixture.GetManagedSessionArchive + h := newTestHandler(t, deps) path := "/core/v1/projects/" + managementProjectID + "/sessions/11111111-1111-4111-8111-111111111111/archive" for _, body := range []string{`{}`, `{"expected_generation":null}`, `{"expected_generation":0}`, `{"expected_generation":-1}`, `{"expected_generation":1.5}`, `{"expected_generation":"1"}`, `{"Expected_Generation":1}`} { if w := projectKeyHTTP(h, http.MethodPost, path, "admin", body); w.Code != 400 { diff --git a/services/core/internal/api/admin_summary.go b/services/core/internal/api/admin_summary.go index 852217029..28d5192a9 100644 --- a/services/core/internal/api/admin_summary.go +++ b/services/core/internal/api/admin_summary.go @@ -108,10 +108,10 @@ func (h *Handler) adminSummary(w http.ResponseWriter, r *http.Request) { return } var binding store.ProjectBinding - binding, err = h.resolveAdminProject(ctx, projectID) + binding, err = h.Projects.GetProject(ctx, projectID) projects = store.ProjectPage{Data: []store.Project{binding.Project}} } else { - projects, err = h.listAdminProjects(ctx, options.after, options.limit, options.ascending) + projects, err = h.Projects.ListProjects(ctx, options.after, options.limit, options.ascending) } if err != nil { writeStoreError(w, r, err) @@ -123,8 +123,8 @@ func (h *Handler) adminSummary(w http.ResponseWriter, r *http.Request) { if group == "project" { groups[""] = &AdminSummaryRow{ProjectID: project.ID} } - counts, err := h.adminManagement.ReadAdminSummary(ctx, project.TenantID, store.AdminSummaryFilter{CreatedAfter: after, CreatedBefore: before}, func(session store.Session, creationKeyID *string) error { - projected, err := sessionResponse(session, h.executorURL) + counts, err := h.Admin.ReadAdminSummary(ctx, project.TenantID, store.AdminSummaryFilter{CreatedAfter: after, CreatedBefore: before}, func(session store.Session, creationKeyID *string) error { + projected, err := sessionResponse(session, h.executorURL()) if err != nil { return err } diff --git a/services/core/internal/api/agents.go b/services/core/internal/api/agents.go index e44b65d58..04d9e480e 100644 --- a/services/core/internal/api/agents.go +++ b/services/core/internal/api/agents.go @@ -11,12 +11,16 @@ import ( "github.com/google/uuid" ) -type AgentStore interface { +// Agents manages saved Agents. GetAgentForSession reads a saved Agent for +// Session creation together with its decrypted model provider when the +// Session inherits it. +type Agents interface { DeleteAgent(context.Context, string, string) (string, error) UpdateAgent(context.Context, string, string, store.UpdateAgentInput) (store.SavedAgent, error) ListAgents(context.Context, string, string, int, bool) (store.AgentPage, error) CreateAgent(context.Context, string, store.CreateAgentInput) (store.SavedAgent, error) GetAgent(context.Context, string, string) (store.SavedAgent, error) + GetAgentForSession(context.Context, string, string, bool) (store.SavedAgent, *v1.ModelProviderInput, error) } // @Summary Create a reusable Agent @@ -54,7 +58,7 @@ func (h *Handler) createAgent(w http.ResponseWriter, r *http.Request) { } return } - agent, err := h.store.CreateAgent(r.Context(), tenantID(r), input) + agent, err := h.Agents.CreateAgent(r.Context(), tenantID(r), input) if err != nil { writeStoreError(w, r, err) return @@ -73,7 +77,12 @@ func (h *Handler) createAgent(w http.ResponseWriter, r *http.Request) { // @Failure 400,401,404,500 {object} v1.ErrorResponse // @Router /agents/{agent_id} [get] func (h *Handler) getAgent(w http.ResponseWriter, r *http.Request) { - agent, err := h.lookupAgent(r.Context(), tenantID(r), chi.URLParam(r, "agent_id")) + id := chi.URLParam(r, "agent_id") + if !validAgentID(id) { + writeStoreError(w, r, store.ErrNotFound) + return + } + agent, err := h.Agents.GetAgent(r.Context(), tenantID(r), id) if err != nil { writeStoreError(w, r, err) return @@ -105,13 +114,6 @@ func agentResponse(agent store.SavedAgent) (v1.SavedAgent, error) { return response, nil } -func (h *Handler) lookupAgent(ctx context.Context, tenant, id string) (store.SavedAgent, error) { - if !validAgentID(id) { - return store.SavedAgent{}, store.ErrNotFound - } - return h.store.GetAgent(ctx, tenant, id) -} - func validAgentID(id string) bool { parsed, err := uuid.Parse(id) return err == nil && parsed != uuid.Nil diff --git a/services/core/internal/api/agents_delete.go b/services/core/internal/api/agents_delete.go index cdec0832d..b50f148dd 100644 --- a/services/core/internal/api/agents_delete.go +++ b/services/core/internal/api/agents_delete.go @@ -33,7 +33,7 @@ func (h *Handler) deleteAgent(w http.ResponseWriter, r *http.Request) { writeStoreError(w, r, store.ErrNotFound) return } - deleted, err := h.store.DeleteAgent(r.Context(), tenantID(r), id) + deleted, err := h.Agents.DeleteAgent(r.Context(), tenantID(r), id) if err != nil { writeStoreError(w, r, err) return diff --git a/services/core/internal/api/agents_list.go b/services/core/internal/api/agents_list.go index 9244293a1..699a1b03c 100644 --- a/services/core/internal/api/agents_list.go +++ b/services/core/internal/api/agents_list.go @@ -23,7 +23,7 @@ func (h *Handler) listAgents(w http.ResponseWriter, r *http.Request) { if !ok { return } - page, err := h.store.ListAgents(r.Context(), tenantID(r), options.after, options.limit, options.ascending) + page, err := h.Agents.ListAgents(r.Context(), tenantID(r), options.after, options.limit, options.ascending) if err != nil { writeStoreError(w, r, err) return diff --git a/services/core/internal/api/agents_update.go b/services/core/internal/api/agents_update.go index b49115aae..9321c0363 100644 --- a/services/core/internal/api/agents_update.go +++ b/services/core/internal/api/agents_update.go @@ -39,7 +39,7 @@ func (h *Handler) updateAgent(w http.ResponseWriter, r *http.Request) { // Storage validation precedes the lookup; follow the missing-Agent path. id = store.UnknownResourceID } - updated, err := h.store.UpdateAgent(r.Context(), tenantID(r), id, input) + updated, err := h.Agents.UpdateAgent(r.Context(), tenantID(r), id, input) if err != nil { writeStoreError(w, r, err) return diff --git a/services/core/internal/api/auth.go b/services/core/internal/api/auth.go index a9a30e343..f98d6fcd9 100644 --- a/services/core/internal/api/auth.go +++ b/services/core/internal/api/auth.go @@ -16,19 +16,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) -// ProjectAPIKeyResolver resolves current database credentials on each request. -type ProjectAPIKeyResolver interface { - ResolveProjectAPIKey(context.Context, string) (store.ProjectAPIKeyBinding, error) -} -type Authenticator struct{ keys ProjectAPIKeyResolver } - -func NewDatabaseAuthenticator(keys ProjectAPIKeyResolver) (*Authenticator, error) { - if keys == nil { - return nil, errors.New("database API key resolver is required") - } - return &Authenticator{keys: keys}, nil -} - func projectBearerDigest(r *http.Request) ([sha256.Size]byte, bool) { parts := strings.Fields(r.Header.Get("Authorization")) if len(r.Header.Values("Authorization")) != 1 || len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") { @@ -51,14 +38,13 @@ func (h *Handler) resolveCaller(r *http.Request) (identity.Principal, writeaudit if !valid { return identity.Principal{}, writeaudit.Source{}, false, nil } - if h.deploymentAuth != nil { - if _, admin := h.deploymentAuth.digests[digest]; admin { - return identity.Principal{}, writeaudit.Source{}, false, nil - } + // A Core key never authenticates as a Project key. + if _, admin := h.CoreKeys.digests[digest]; admin { + return identity.Principal{}, writeaudit.Source{}, false, nil } ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) defer cancel() - binding, err := h.auth.keys.ResolveProjectAPIKey(ctx, hex.EncodeToString(digest[:])) + binding, err := h.Projects.ResolveProjectAPIKey(ctx, hex.EncodeToString(digest[:])) if errors.Is(err, store.ErrNotFound) { return identity.Principal{}, writeaudit.Source{}, false, nil } diff --git a/services/core/internal/api/auth_fixture_test.go b/services/core/internal/api/auth_fixture_test.go index 2178daa63..a09652b92 100644 --- a/services/core/internal/api/auth_fixture_test.go +++ b/services/core/internal/api/auth_fixture_test.go @@ -3,7 +3,7 @@ package api import ( "context" "encoding/hex" - "errors" + "testing" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" @@ -11,6 +11,9 @@ import ( ) type APIKey struct{ Name, TokenSHA256, TenantID, OrganizationID, ProjectID, SubjectKind, SubjectID string } + +// fixtureKeyResolver resolves Project key digests the way the Project store +// does. Tests assign its ResolveProjectAPIKey to fakeProjects. type fixtureKeyResolver map[string]store.ProjectAPIKeyBinding func (f fixtureKeyResolver) ResolveProjectAPIKey(_ context.Context, digest string) (store.ProjectAPIKeyBinding, error) { @@ -19,21 +22,23 @@ func (f fixtureKeyResolver) ResolveProjectAPIKey(_ context.Context, digest strin } return store.ProjectAPIKeyBinding{}, store.ErrNotFound } -func NewAuthenticator(keys []APIKey) (*Authenticator, error) { + +// projectKeys binds each key's digest to its Principal. +func projectKeys(t testing.TB, keys ...APIKey) fixtureKeyResolver { + t.Helper() resolver := fixtureKeyResolver{} for _, k := range keys { p := identity.Principal{ProjectScope: identity.ProjectScope{TenantID: k.TenantID, OrganizationID: k.OrganizationID, ProjectID: k.ProjectID}, SubjectKind: k.SubjectKind, SubjectID: k.SubjectID} if err := p.Validate(); err != nil { - return nil, err + t.Fatal(err) } - digest, err := hex.DecodeString(k.TokenSHA256) - if err != nil || len(digest) != 32 { - return nil, errors.New("invalid fixture digest") + if digest, err := hex.DecodeString(k.TokenSHA256); err != nil || len(digest) != 32 { + t.Fatalf("invalid fixture digest %q", k.TokenSHA256) } if _, exists := resolver[k.TokenSHA256]; exists { - return nil, errors.New("duplicate fixture digest") + 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} } - return NewDatabaseAuthenticator(resolver) + return resolver } diff --git a/services/core/internal/api/auth_test.go b/services/core/internal/api/auth_test.go index a99b42a04..7876b15cd 100644 --- a/services/core/internal/api/auth_test.go +++ b/services/core/internal/api/auth_test.go @@ -15,21 +15,13 @@ func callerBinding() APIKey { OrganizationID: "org-one", ProjectID: "project-one", SubjectKind: "user", SubjectID: "user-one"} } -func TestAuthenticatorRequiresDatabaseResolver(t *testing.T) { - if _, err := NewDatabaseAuthenticator(nil); err == nil { - t.Fatal("missing database resolver accepted") - } -} - func TestCallerPrincipalHeadersAndKeyRotation(t *testing.T) { key := callerBinding() rotated, peer := key, key rotated.TokenSHA256 = runtimedevice.HashCredential("rotated") peer.TokenSHA256 = runtimedevice.HashCredential("peer") - auth, err := NewAuthenticator([]APIKey{key, rotated, peer}) - if err != nil { - t.Fatal(err) - } + deps, fakes := testDependencies(t) + fakes.projects.resolveProjectAPIKey = projectKeys(t, key, rotated, peer).ResolveProjectAPIKey for _, test := range []struct { name, token, subject string headers http.Header @@ -56,7 +48,7 @@ func TestCallerPrincipalHeadersAndKeyRotation(t *testing.T) { r.Header[name] = values } called := false - h := (&Handler{auth: auth}).authenticate(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + h := (&Handler{Dependencies: deps}).authenticate(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { called = true principal := r.Context().Value(principalContextKey{}).(identity.Principal) if principal.SubjectID != test.subject || tenantID(r) != key.TenantID || principal.ProjectID != key.ProjectID { diff --git a/services/core/internal/api/claude_admission_test.go b/services/core/internal/api/claude_admission_test.go index 41a5345d7..c1142d5e6 100644 --- a/services/core/internal/api/claude_admission_test.go +++ b/services/core/internal/api/claude_admission_test.go @@ -1,15 +1,15 @@ package api import ( - "crypto/sha256" - "encoding/hex" + "context" "fmt" "net/http" "net/http/httptest" "strings" "testing" - "github.com/google/uuid" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) func TestClaudeSessionConfigurationAdmission(t *testing.T) { @@ -42,16 +42,17 @@ func TestClaudeSessionConfigurationAdmission(t *testing.T) { {"MCP empty fragment", `,"tools":[` + strings.Replace(publicMCP, `/tools"`, `/tools#"`, 1) + `]`, false}, } { t.Run(fmt.Sprintf("%s/stream=%t/input=%s", test.name, stream, input), func(t *testing.T) { - digest := sha256.Sum256([]byte("test-api-key")) - auth, err := NewAuthenticator([]APIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: hex.EncodeToString(digest[:]), TenantID: uuid.NewString()}}) - if err != nil { - t.Fatal(err) - } - saved := &recordingStore{} - handler, err := NewHandler(saved, auth, "claude_sdk", WithExecution(&inputRecorder{ResourceStore: saved})) - if err != nil { - t.Fatal(err) - } + streamed := "" + handler, saved, _ := testHandler(t, func(d *Dependencies, f *testFakes) { + d.Engine = "claude_sdk" + admitSessions(d, f) + // The Worker's stream admission reports that it cannot execute. + f.admission.createSessionStream = func(_ context.Context, _ string, input store.CreateSessionInput) (store.SessionCreation, error) { + streamed = input.Engine + return store.SessionCreation{}, execution.ErrExecutionUnavailable + } + f.metrics.recordUnavailable = func() {} + }) body := fmt.Sprintf(`{"agent":{"model":"MiniMax-M3"%s},"environment":{"type":"none"},"stream":%t,"input":%s}`, test.fields, stream, input) request := httptest.NewRequest(http.MethodPost, "/v1/agents/sessions", strings.NewReader(body)) request.Header.Set("Authorization", "Bearer test-api-key") @@ -75,6 +76,9 @@ func TestClaudeSessionConfigurationAdmission(t *testing.T) { if want == http.StatusCreated && saved.input.Engine != "claude_sdk" { t.Fatal("wrong engine persisted") } + if (want == http.StatusServiceUnavailable) != (streamed == "claude_sdk") { + t.Fatalf("stream admission engine = %q", streamed) + } }) } } diff --git a/services/core/internal/api/claude_mcp_test.go b/services/core/internal/api/claude_mcp_test.go index 52214f0be..640e34dfc 100644 --- a/services/core/internal/api/claude_mcp_test.go +++ b/services/core/internal/api/claude_mcp_test.go @@ -2,8 +2,6 @@ package api import ( "context" - "crypto/sha256" - "encoding/hex" "fmt" "strings" "testing" @@ -13,7 +11,6 @@ import ( ) type claudeCredentialStore struct { - recordingStore binding store.MCPCredentialBinding calls int } @@ -35,18 +32,14 @@ func TestClaudeMCPAdmitsResolvedCredentials(t *testing.T) { if selection == "unmatched" { s.binding.VaultID, s.binding.CredentialID, s.binding.AuthType = "", "", "" } - digest := sha256.Sum256([]byte("test-api-key")) - auth, err := NewAuthenticator([]APIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: hex.EncodeToString(digest[:]), TenantID: uuid.NewString()}}) - if err != nil { - t.Fatal(err) - } - h, err := NewHandler(s, auth, "claude_sdk", WithExecution(&inputRecorder{ResourceStore: s})) - if err != nil { - t.Fatal(err) - } + h, recording, _ := testHandler(t, func(d *Dependencies, f *testFakes) { + d.Engine = "claude_sdk" + admitSessions(d, f) + f.vaults.resolveMCPCredentials = s.ResolveMCPCredentials + }) body := fmt.Sprintf(`{"agent":{"model":"model","tools":[%s]},"environment":{"type":"none"},"vault_ids":[%q],"input":"Use the configured records server."}`, tool, vault) response := credentialRequest(h, "POST", "/v1/agents/sessions", body) - if response.Code != 201 || s.calls != 1 || s.tenant == "" { + if response.Code != 201 || s.calls != 1 || recording.tenant == "" { t.Fatal("credential selection or admission failed", response.Code, response.Body, s.calls) } }) diff --git a/services/core/internal/api/configuration_validation_test.go b/services/core/internal/api/configuration_validation_test.go index b3350a453..fc1c63964 100644 --- a/services/core/internal/api/configuration_validation_test.go +++ b/services/core/internal/api/configuration_validation_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" ) @@ -27,11 +28,19 @@ func (s *savedConfigurationStore) GetAgent(_ context.Context, tenant, id string) return store.SavedAgent{ID: id, TenantID: tenant, Configuration: json.RawMessage(configuration), Metadata: map[string]string{}}, nil } +// GetAgentForSession reads the saved Agent for Session creation; these records +// carry no model provider. +func (s *savedConfigurationStore) GetAgentForSession(ctx context.Context, tenant, id string, _ bool) (store.SavedAgent, *v1.ModelProviderInput, error) { + agent, err := s.GetAgent(ctx, tenant, id) + return agent, nil, err +} + func configurationHandler(t *testing.T, agents map[string]string) (http.Handler, *savedConfigurationStore) { t.Helper() s := &savedConfigurationStore{validationStore: &validationStore{}, agents: agents} - h, recording, _ := testHandler(t, WithExecution(&inputRecorder{ResourceStore: s})) - recording.ResourceStore = s + h, _, _ := testHandler(t, s.serve, func(_ *Dependencies, f *testFakes) { + f.agents.getAgent, f.agents.getAgentForSession = s.GetAgent, s.GetAgentForSession + }) return h, s } diff --git a/services/core/internal/api/contract_routes_test.go b/services/core/internal/api/contract_routes_test.go index e0d68b6c5..09f37f194 100644 --- a/services/core/internal/api/contract_routes_test.go +++ b/services/core/internal/api/contract_routes_test.go @@ -9,8 +9,6 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/nativeinstaller" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/go-chi/chi/v5" "gopkg.in/yaml.v3" ) @@ -60,17 +58,11 @@ func contractOperations(t *testing.T, file, prefix string) map[string]bool { // The pinned upstream /v1 set is checked by TestEveryRouteAuthenticatesItsCanonicalPath // and the contract tests. func TestContractsPublishExactlyTheRegisteredCoreAndMachineRoutes(t *testing.T) { - admin, err := NewDeploymentAuthenticator([]string{runtimedevice.HashCredential(routingAdminKey)}) - if err != nil { - t.Fatal(err) - } - // Every option that gates a route registration, as the server passes them. - s := &store.Store{} - h := &Handler{store: s, engine: "codex"} - for _, option := range []Option{WithSandboxManager(s, admin), WithProjectAPIKeys(s, admin), WithWriteAudit(s, admin), WithAdminManagement(s), - WithInstallation(Installation{}, s.AddressBindings), WithNativeInstaller(&nativeinstaller.Catalog{}, "contract-test")} { - option(h) - } + // Every optional group that gates a route registration, as the server enables them. + deps, fakes := testDependencies(t) + deps.Execution, deps.Sandboxes = fakes.execution(), fakes.sandboxes() + deps.Execution.NativeInstaller = &NativeInstaller{Version: "contract-test", Catalog: &nativeinstaller.Catalog{}} + h := &Handler{Dependencies: deps} contracts := map[string]string{"/v1": "openapi.yaml", "/core/v1": "core.openapi.yaml", "/api/v1": "runtime.openapi.yaml"} published := map[string]map[string]bool{} for prefix, file := range contracts { @@ -78,7 +70,7 @@ func TestContractsPublishExactlyTheRegisteredCoreAndMachineRoutes(t *testing.T) } guard := reflect.ValueOf(methodNotAllowed).Pointer() registered, excluded := map[string]bool{}, map[string]bool{} - err = chi.Walk(h.routes(), func(method, route string, handler http.Handler, _ ...func(http.Handler) http.Handler) error { + err := chi.Walk(h.routes(), func(method, route string, handler http.Handler, _ ...func(http.Handler) http.Handler) error { for _, key := range []string{method + " " + route, "* " + route} { if _, ok := unpublishedRoutes[key]; ok { excluded[key] = true diff --git a/services/core/internal/api/core_errors_test.go b/services/core/internal/api/core_errors_test.go index e2b680d8d..84daa24c9 100644 --- a/services/core/internal/api/core_errors_test.go +++ b/services/core/internal/api/core_errors_test.go @@ -10,7 +10,6 @@ import ( "testing" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/go-chi/chi/v5" ) @@ -21,11 +20,8 @@ func TestCoreErrorDetailsAreScopedByRouterNotRequestPath(t *testing.T) { }, "expected_generation") }) router := chi.NewRouter() - admin, err := NewDeploymentAuthenticator([]string{runtimedevice.HashCredential("admin")}) - if err != nil { - t.Fatal(err) - } - (&Handler{deploymentAuth: admin}).registerCoreRoutes(router) + deps, _ := testDependencies(t) + (&Handler{Dependencies: deps}).registerCoreRoutes(router) var core chi.Router for _, route := range router.Routes() { if route.Pattern == "/core/v1/*" { diff --git a/services/core/internal/api/core_metrics.go b/services/core/internal/api/core_metrics.go index 01e43dda1..46e572304 100644 --- a/services/core/internal/api/core_metrics.go +++ b/services/core/internal/api/core_metrics.go @@ -8,15 +8,12 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/coremetrics" ) -type CoreMetricsService interface { +// Metrics reads Core's process and job metrics and counts rejected execution. +type Metrics interface { Read(context.Context, string) (coremetrics.View, error) RecordUnavailable() } -func WithCoreMetrics(service CoreMetricsService) Option { - return func(h *Handler) { h.coreMetrics = service } -} - // @Summary Retrieve Core operational metrics // @Description Core key only. Complete UTC buckets; unknown measurements are null. Samples are process-local and are not backfilled after a restart. // @Tags Core Administration @@ -42,11 +39,7 @@ func (h *Handler) getCoreMetrics(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusBadRequest, "invalid_request", "range must be 1h, 6h, 24h or 7d.") return } - if h.coreMetrics == nil { - writeError(w, http.StatusServiceUnavailable, "core_metrics_unavailable", "Core metrics are not configured.") - return - } - value, err := h.coreMetrics.Read(r.Context(), name) + value, err := h.Metrics.Read(r.Context(), name) if err != nil { writeError(w, http.StatusServiceUnavailable, "core_metrics_unavailable", "Core metrics could not be read.") return @@ -55,12 +48,9 @@ func (h *Handler) getCoreMetrics(w http.ResponseWriter, r *http.Request) { } func (h *Handler) responseHeaders(next http.Handler) http.Handler { - if h.coreMetrics == nil { - return agentsResponseHeaders(next) - } return responseHeadersWithErrors(next, func(code string) { if code == "execution_unavailable" { - h.coreMetrics.RecordUnavailable() + h.Metrics.RecordUnavailable() } }) } diff --git a/services/core/internal/api/core_metrics_test.go b/services/core/internal/api/core_metrics_test.go index 5cafee982..59ee9b241 100644 --- a/services/core/internal/api/core_metrics_test.go +++ b/services/core/internal/api/core_metrics_test.go @@ -9,7 +9,6 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/coremetrics" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" ) type metricsFixture struct { @@ -25,14 +24,10 @@ func (f *metricsFixture) Read(_ context.Context, name string) (coremetrics.View, } func (f *metricsFixture) RecordUnavailable() { f.refusals++ } func TestCoreMetricsAdministratorContract(t *testing.T) { - key := callerBinding() - auth, _ := NewAuthenticator([]APIKey{key}) - admin, _ := NewDeploymentAuthenticator([]string{runtimedevice.HashCredential("admin")}) + deps, fakes := managementFakes(t, callerBinding()) f := &metricsFixture{} - h, err := NewHandler(&recordingStore{}, auth, "codex", WithProjectAPIKeys(managementProjectStore(key), admin), WithCoreMetrics(f)) - if err != nil { - t.Fatal(err) - } + fakes.metrics.read = f.Read + h := newTestHandler(t, deps) path := "/core/v1/metrics" for _, token := range []string{"", "unknown", "caller"} { w := projectKeyHTTP(h, "GET", path, token, "") @@ -68,7 +63,9 @@ func TestCoreMetricsAdministratorContract(t *testing.T) { func TestCoreRejectionObservationPreservesResponsesAndFlush(t *testing.T) { for _, code := range []string{"execution_unavailable", "environment_unavailable", "invalid_api_key"} { f := &metricsFixture{} - h := &Handler{coreMetrics: f} + deps, fakes := testDependencies(t) + fakes.metrics.recordUnavailable = f.RecordUnavailable + h := &Handler{Dependencies: deps} out := httptest.NewRecorder() handler := h.responseHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { writeError(w, 503, code, "not recorded") @@ -84,7 +81,9 @@ func TestCoreRejectionObservationPreservesResponsesAndFlush(t *testing.T) { } } f := &metricsFixture{} - h := &Handler{coreMetrics: f} + deps, fakes := testDependencies(t) + fakes.metrics.recordUnavailable = f.RecordUnavailable + h := &Handler{Dependencies: deps} out := httptest.NewRecorder() h.responseHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if err := http.NewResponseController(w).Flush(); err != nil { diff --git a/services/core/internal/api/core_model_provider_validation_test.go b/services/core/internal/api/core_model_provider_validation_test.go index f2f8f8736..d16cda709 100644 --- a/services/core/internal/api/core_model_provider_validation_test.go +++ b/services/core/internal/api/core_model_provider_validation_test.go @@ -12,25 +12,23 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) +// coreProviderValidationStore counts deployment model provider writes. 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.ModelConfigurationInput) (store.DeploymentModelProvider, error) { s.writes++ return store.DeploymentModelProvider{}, nil } +func (s *coreProviderValidationStore) configure(_ *Dependencies, f *testFakes) { + f.modelProviders.setDeploymentModelProvider = s.SetDeploymentModelProvider +} + func TestCoreModelProviderValidationFields(t *testing.T) { s := &coreProviderValidationStore{} - h, _, _ := adminTestHandler(t, func(h *Handler) { h.store = s }) + h, _, _ := adminTestHandler(t, s.configure) for _, tc := range []struct { name, harness, body, code, param string details map[string]any diff --git a/services/core/internal/api/core_routes.go b/services/core/internal/api/core_routes.go index df1844802..f65a0d6d1 100644 --- a/services/core/internal/api/core_routes.go +++ b/services/core/internal/api/core_routes.go @@ -10,18 +10,13 @@ import ( // Every request, including one for an unknown path, must carry the Core key; // Project API keys and machine credentials are rejected here. func (h *Handler) registerCoreRoutes(router chi.Router) { - if h.deploymentAuth == nil { - return - } router.Route("/core/v1", func(r chi.Router) { r.Use(coreErrorResponses) - r.Use(h.deploymentAuth.authenticate) + r.Use(h.CoreKeys.authenticate) r.NotFound(func(w http.ResponseWriter, _ *http.Request) { writeError(w, http.StatusNotFound, "not_found", "This Core operation does not exist.") }) - if h.installation != nil { - r.Get("/installation", h.getInstallation) - } + r.Get("/installation", h.getInstallation) h.registerProjectAPIKeyRoutes(r) h.registerAdminResourceRoutes(r) h.registerExecutorCredentialRoutes(r) diff --git a/services/core/internal/api/core_store_validation_test.go b/services/core/internal/api/core_store_validation_test.go index 443cb3ebf..a437a5c2a 100644 --- a/services/core/internal/api/core_store_validation_test.go +++ b/services/core/internal/api/core_store_validation_test.go @@ -10,7 +10,6 @@ import ( "strings" "testing" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) @@ -72,12 +71,14 @@ func TestCoreStoreValidationFieldsAndPublicFallback(t *testing.T) { } func TestCoreActiveCapacityUpperBoundNamesSubmittedField(t *testing.T) { - project, _ := NewAuthenticator([]APIKey{callerBinding()}) - admin, _ := NewDeploymentAuthenticator([]string{runtimedevice.HashCredential("administrator")}) - h, err := NewHandler(&recordingStore{}, project, "codex", WithSandboxManager(&store.Store{}, admin)) - if err != nil { - t.Fatal(err) + deps, fakes := sandboxFakes(t) + fakes.deployment.createRuntimeEnrollment = func(_ context.Context, capacity store.RuntimeNodeCapacity) (store.RuntimeNodeEnrollmentToken, error) { + return store.RuntimeNodeEnrollmentToken{}, storeCapacity(capacity.MaxActive) } + fakes.deployment.updateRuntimeNode = func(_ context.Context, _ string, input store.RuntimeNodeUpdate) error { + return storeCapacity(input.MaxActive) + } + h := newTestHandler(t, deps) 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}`}, diff --git a/services/core/internal/api/credentials.go b/services/core/internal/api/credentials.go index 1c0256b4f..6d06fe108 100644 --- a/services/core/internal/api/credentials.go +++ b/services/core/internal/api/credentials.go @@ -1,7 +1,6 @@ package api import ( - "context" "encoding/json" "net/http" @@ -11,16 +10,6 @@ import ( "github.com/google/uuid" ) -type CredentialStore interface { - CreateOAuthCredential(context.Context, string, string, store.CreateOAuthCredentialInput) (store.Credential, error) - UpdateOAuthCredential(context.Context, string, string, string, store.UpdateOAuthCredentialInput) (store.Credential, error) - CreateStaticCredential(context.Context, string, string, store.CreateStaticCredentialInput) (store.Credential, error) - UpdateStaticCredential(context.Context, string, string, string, store.UpdateStaticCredentialInput) (store.Credential, error) - GetCredential(context.Context, string, string, string) (store.Credential, error) - DeleteCredential(context.Context, string, string, string) (string, error) - ListCredentials(context.Context, string, string, string, int, bool, []string) (store.CredentialPage, error) -} - // @Summary Create a Vault Credential // @Description Stores static_bearer or mcp_oauth secrets as execution-owned authenticated ciphertext without contacting any endpoint. Static bearer and OAuth access tokens must be nonempty strings; their bytes are preserved. OAuth accepts a required access token, nullable RFC3339 expiry and optional refresh configuration with none, client_secret_basic or client_secret_post authentication. Required name is trimmed to 1–256 UTF-8 bytes. Credential and token endpoints require HTTPS without userinfo or fragments. Responses contain safe metadata only, including explicit nullable OAuth expiry, refresh, resource and scope. Missing encryption configuration returns local 503. External authorization and provider revocation remain caller responsibilities; exact hosted error/default semantics remain unverified. // @Tags Credentials @@ -60,14 +49,14 @@ func (h *Handler) createCredential(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusBadRequest, "invalid_request", "static_bearer requires a nonempty string token and an absolute HTTPS mcp_server_url without userinfo or a fragment.") return } - credential, err = h.store.CreateStaticCredential(r.Context(), tenantID(r), vaultID, store.CreateStaticCredentialInput{Name: name, MCPServerURL: *auth.MCPServerURL, Token: *auth.Token}) + credential, err = h.Vaults.CreateStaticCredential(r.Context(), tenantID(r), vaultID, store.CreateStaticCredentialInput{Name: name, MCPServerURL: *auth.MCPServerURL, Token: *auth.Token}) case "mcp_oauth": input, parseErr := oauthCredentialCreate(request.Auth, name) if parseErr != nil { writeStoreError(w, r, parseErr) return } - credential, err = h.store.CreateOAuthCredential(r.Context(), tenantID(r), vaultID, input) + credential, err = h.Vaults.CreateOAuthCredential(r.Context(), tenantID(r), vaultID, input) default: writeError(w, http.StatusBadRequest, "invalid_request", "auth requires type static_bearer or mcp_oauth.") return @@ -99,7 +88,7 @@ func (h *Handler) getCredential(w http.ResponseWriter, r *http.Request) { if !ok { return } - credential, err := h.store.GetCredential(r.Context(), tenantID(r), vaultID, id) + credential, err := h.Vaults.GetCredential(r.Context(), tenantID(r), vaultID, id) if err != nil { writeStoreError(w, r, err) return diff --git a/services/core/internal/api/credentials_delete.go b/services/core/internal/api/credentials_delete.go index 63ad26fd5..758b26beb 100644 --- a/services/core/internal/api/credentials_delete.go +++ b/services/core/internal/api/credentials_delete.go @@ -35,7 +35,7 @@ func (h *Handler) deleteCredential(w http.ResponseWriter, r *http.Request) { if !ok { return } - deleted, err := h.store.DeleteCredential(r.Context(), tenantID(r), vaultID, id) + deleted, err := h.Vaults.DeleteCredential(r.Context(), tenantID(r), vaultID, id) if err != nil { writeStoreError(w, r, err) return diff --git a/services/core/internal/api/credentials_list.go b/services/core/internal/api/credentials_list.go index 7736e13eb..9a240f3d0 100644 --- a/services/core/internal/api/credentials_list.go +++ b/services/core/internal/api/credentials_list.go @@ -27,7 +27,7 @@ func (h *Handler) listCredentials(w http.ResponseWriter, r *http.Request) { if !ok { return } - page, err := h.store.ListCredentials(r.Context(), tenantID(r), vaultID, options.after, options.limit, options.ascending, statuses) + page, err := h.Vaults.ListCredentials(r.Context(), tenantID(r), vaultID, options.after, options.limit, options.ascending, statuses) if err != nil { writeStoreError(w, r, err) return diff --git a/services/core/internal/api/credentials_test.go b/services/core/internal/api/credentials_test.go index 084d00409..ccb81466f 100644 --- a/services/core/internal/api/credentials_test.go +++ b/services/core/internal/api/credentials_test.go @@ -16,7 +16,6 @@ import ( ) type credentialFixture struct { - ResourceStore credential store.Credential input store.CreateStaticCredentialInput replacement store.UpdateStaticCredentialInput @@ -41,11 +40,17 @@ func (f *credentialFixture) GetCredential(_ context.Context, tenant, vault, id s return f.credential, f.err } +// serve answers the Vault credential operations from f. +func (f *credentialFixture) serve(_ *Dependencies, fakes *testFakes) { + v := fakes.vaults + v.createStaticCredential, v.updateStaticCredential, v.getCredential, v.listCredentials, v.deleteCredential = f.CreateStaticCredential, f.UpdateStaticCredential, f.GetCredential, f.ListCredentials, f.DeleteCredential + v.createOAuthCredential, v.updateOAuthCredential = f.CreateOAuthCredential, f.UpdateOAuthCredential +} + func credentialHandler(t *testing.T) (http.Handler, *credentialFixture, string) { t.Helper() - h, s, tenant := testHandler(t) f := &credentialFixture{credential: store.Credential{ID: uuid.NewString(), VaultID: uuid.NewString(), AuthType: "static_bearer", CreatedAt: time.Unix(1700000000, 0), UpdatedAt: time.Unix(1700000000, 0)}} - s.ResourceStore = f + h, _, tenant := testHandler(t, f.serve) return h, f, tenant } diff --git a/services/core/internal/api/credentials_update.go b/services/core/internal/api/credentials_update.go index 1a62fe4ba..673e78b73 100644 --- a/services/core/internal/api/credentials_update.go +++ b/services/core/internal/api/credentials_update.go @@ -43,14 +43,14 @@ func (h *Handler) updateCredential(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusBadRequest, "invalid_request", "static_bearer auth requires a nonempty string token.") return } - credential, err = h.store.UpdateStaticCredential(r.Context(), tenantID(r), vaultID, id, store.UpdateStaticCredentialInput{Token: *auth.Token}) + credential, err = h.Vaults.UpdateStaticCredential(r.Context(), tenantID(r), vaultID, id, store.UpdateStaticCredentialInput{Token: *auth.Token}) case "mcp_oauth": input, parseErr := oauthCredentialUpdate(request.Auth) if parseErr != nil { writeStoreError(w, r, parseErr) return } - credential, err = h.store.UpdateOAuthCredential(r.Context(), tenantID(r), vaultID, id, input) + credential, err = h.Vaults.UpdateOAuthCredential(r.Context(), tenantID(r), vaultID, id, input) default: writeError(w, http.StatusBadRequest, "invalid_request", "auth requires type static_bearer or mcp_oauth.") return diff --git a/services/core/internal/api/dependencies.go b/services/core/internal/api/dependencies.go new file mode 100644 index 000000000..ef0cc5bcd --- /dev/null +++ b/services/core/internal/api/dependencies.go @@ -0,0 +1,162 @@ +package api + +import ( + "errors" + "fmt" + "net/http" + + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" +) + +// Dependencies is everything the handler uses. Each application area is one +// field typed as an interface declared next to the area's handlers, listing +// exactly the methods they call. Every field is required unless its comment +// says what nil means; NewHandler rejects a missing one. +type Dependencies struct { + // Engine is the default Harness. Harnesses lists the other Harnesses this + // deployment enables for explicit selection. + Engine string + Harnesses []string + // Policy is the immutable qualification shared with the execution + // Dispatcher. The zero value uses the built-in engine registrations. + Policy execution.Policy + // CoreKeys authenticates /core/v1 and keeps Core keys out of Project key + // authentication. + CoreKeys *DeploymentAuthenticator + // Installation holds the facts GET /core/v1/installation reports; + // InstallationBindings counts what is bound to the current public URL. + Installation Installation + InstallationBindings InstallationBindings + + Projects Projects + Vaults Vaults + ModelProviders ModelProviders + Files Files + Skills Skills + EnvironmentTemplates EnvironmentTemplates + Agents Agents + Sessions Sessions + SessionEvents SessionEvents + SessionHistory SessionHistory + Subagents Subagents + Artifacts Artifacts + SessionAdmin SessionAdmin + Environments Environments + ExecutorConnections ExecutorConnections + Admin Admin + WriteAudit WriteAudit + Metrics Metrics + RuntimeObservations RuntimeObservations + RuntimeHistory RuntimeHistory + + // Execution is nil when this Core runs without a Runtime gateway, and so + // without an execution Worker. Work that needs one then answers 503 + // execution_unavailable. + Execution *Execution + // Sandboxes is nil when this Core has no managed sandbox installation. The + // sandbox node and manager routes are then absent, and openai_hosted + // Sessions answer 503 execution_unavailable. It requires Execution. + Sandboxes *Sandboxes +} + +// Execution is the execution Worker's surface. Every field is required unless +// its comment says what nil means. +type Execution struct { + // ExecutorURL is the validated daemon WebSocket URL that self-hosted + // Sessions report and executors connect to. + ExecutorURL string + Admission Admission + SessionArchive SessionArchive + Workspaces EnvironmentWorkspaces + // NativeInstaller is nil for a build without a source revision: the native + // installation routes are then absent and Sessions carry no installation. + NativeInstaller *NativeInstaller +} + +// Sandboxes is the managed sandbox deployment's surface. Every field is +// required. +type Sandboxes struct { + Deployment Deployment + DeploymentChanges DeploymentChanges + ConfigurationDiscovery ConfigurationDiscovery +} + +type Handler struct { + Dependencies + harnesses map[string]bool +} + +// NewHandler builds the HTTP handler from complete dependencies. +func NewHandler(deps Dependencies) (http.Handler, error) { + if err := deps.validate(); err != nil { + return nil, err + } + deps.Installation.Object = "core.installation" + h := &Handler{Dependencies: deps, harnesses: make(map[string]bool, len(deps.Harnesses))} + for _, kind := range deps.Harnesses { + h.harnesses[kind] = true + } + return CanonicalPaths(h.routes()), nil +} + +func (d Dependencies) validate() error { + if !store.ValidEngine(d.Engine) { + return errors.New("api: a valid default Harness is required") + } + if d.CoreKeys == nil { + return errors.New("api: CoreKeys is required") + } + if err := required( + field{"InstallationBindings", d.InstallationBindings}, field{"Projects", d.Projects}, field{"Vaults", d.Vaults}, + field{"ModelProviders", d.ModelProviders}, field{"Files", d.Files}, field{"Skills", d.Skills}, + field{"EnvironmentTemplates", d.EnvironmentTemplates}, field{"Agents", d.Agents}, field{"Sessions", d.Sessions}, + field{"SessionEvents", d.SessionEvents}, field{"SessionHistory", d.SessionHistory}, field{"Subagents", d.Subagents}, + field{"Artifacts", d.Artifacts}, field{"SessionAdmin", d.SessionAdmin}, field{"Environments", d.Environments}, + field{"ExecutorConnections", d.ExecutorConnections}, field{"Admin", d.Admin}, field{"WriteAudit", d.WriteAudit}, + field{"Metrics", d.Metrics}, field{"RuntimeObservations", d.RuntimeObservations}, field{"RuntimeHistory", d.RuntimeHistory}, + ); err != nil { + return err + } + if e := d.Execution; e != nil { + if e.ExecutorURL == "" { + return errors.New("api: Execution.ExecutorURL is required") + } + if e.NativeInstaller != nil && e.NativeInstaller.Version == "" { + return errors.New("api: Execution.NativeInstaller.Version is required") + } + if err := required(field{"Execution.Admission", e.Admission}, field{"Execution.SessionArchive", e.SessionArchive}, field{"Execution.Workspaces", e.Workspaces}); err != nil { + return err + } + } + if s := d.Sandboxes; s != nil { + if d.Execution == nil { + return errors.New("api: Sandboxes requires Execution") + } + return required(field{"Sandboxes.Deployment", s.Deployment}, field{"Sandboxes.DeploymentChanges", s.DeploymentChanges}, field{"Sandboxes.ConfigurationDiscovery", s.ConfigurationDiscovery}) + } + return nil +} + +type field struct { + name string + value any +} + +func required(fields ...field) error { + for _, f := range fields { + if f.value == nil { + return fmt.Errorf("api: %s is required", f.name) + } + } + return nil +} + +// executorURL is the daemon URL self-hosted Sessions report, or empty when +// this Core cannot execute. +func (h *Handler) executorURL() string { + if h.Execution == nil { + return "" + } + return h.Execution.ExecutorURL +} diff --git a/services/core/internal/api/dependencies_test.go b/services/core/internal/api/dependencies_test.go new file mode 100644 index 000000000..71912e4c4 --- /dev/null +++ b/services/core/internal/api/dependencies_test.go @@ -0,0 +1,167 @@ +package api + +import ( + "context" + "net/http" + "strings" + "testing" + + "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. +const testExecutorURL = "wss://core.example/api/v1/agent-daemon/ws" + +// testFakes holds one strict fake per Dependencies area. +type testFakes struct { + projects *fakeProjects + vaults *fakeVaults + modelProviders *fakeModelProviders + files *fakeFiles + skills *fakeSkills + environmentTemplates *fakeEnvironmentTemplates + agents *fakeAgents + sessions *fakeSessions + sessionEvents *fakeSessionEvents + sessionHistory *fakeSessionHistory + subagents *fakeSubagents + artifacts *fakeArtifacts + sessionAdmin *fakeSessionAdmin + environments *fakeEnvironments + executorConnections *fakeExecutorConnections + admin *fakeAdmin + writeAudit *fakeWriteAudit + metrics *fakeMetrics + runtimeObservations *fakeRuntimeObservations + runtimeHistory *fakeRuntimeHistory + installationBindings *fakeInstallationBindings + admission *fakeAdmission + sessionArchive *fakeSessionArchive + workspaces *fakeEnvironmentWorkspaces + deployment *fakeDeployment + deploymentChanges *fakeDeploymentChanges + configurationDiscovery *fakeConfigurationDiscovery +} + +// testDependencies returns Dependencies in which every area is a strict fake. +// A test sets the funcs it expects on the returned fakes; any other call fails +// it. Engine is "codex", CoreKeys accepts "Bearer admin", and Execution and +// Sandboxes are disabled until the test sets fakes.execution() or +// fakes.sandboxes(). +func testDependencies(t testing.TB) (Dependencies, *testFakes) { + t.Helper() + f := &testFakes{ + projects: &fakeProjects{t: t}, vaults: &fakeVaults{t: t}, modelProviders: &fakeModelProviders{t: t}, + files: &fakeFiles{t: t}, skills: &fakeSkills{t: t}, environmentTemplates: &fakeEnvironmentTemplates{t: t}, + agents: &fakeAgents{t: t}, sessions: &fakeSessions{t: t}, sessionEvents: &fakeSessionEvents{t: t}, + sessionHistory: &fakeSessionHistory{t: t}, subagents: &fakeSubagents{t: t}, artifacts: &fakeArtifacts{t: t}, + sessionAdmin: &fakeSessionAdmin{t: t}, environments: &fakeEnvironments{t: t}, executorConnections: &fakeExecutorConnections{t: t}, + admin: &fakeAdmin{t: t}, writeAudit: &fakeWriteAudit{t: t}, metrics: &fakeMetrics{t: t}, + runtimeObservations: &fakeRuntimeObservations{t: t}, runtimeHistory: &fakeRuntimeHistory{t: t}, installationBindings: &fakeInstallationBindings{t: t}, + admission: &fakeAdmission{t: t}, sessionArchive: &fakeSessionArchive{t: t}, workspaces: &fakeEnvironmentWorkspaces{t: t}, + deployment: &fakeDeployment{t: t}, deploymentChanges: &fakeDeploymentChanges{t: t}, configurationDiscovery: &fakeConfigurationDiscovery{t: t}, + } + return Dependencies{ + Engine: "codex", CoreKeys: coreKeys(t, "admin"), InstallationBindings: f.installationBindings, + Projects: f.projects, Vaults: f.vaults, ModelProviders: f.modelProviders, Files: f.files, Skills: f.skills, + EnvironmentTemplates: f.environmentTemplates, Agents: f.agents, Sessions: f.sessions, SessionEvents: f.sessionEvents, + SessionHistory: f.sessionHistory, Subagents: f.subagents, Artifacts: f.artifacts, SessionAdmin: f.sessionAdmin, + Environments: f.environments, ExecutorConnections: f.executorConnections, Admin: f.admin, WriteAudit: f.writeAudit, + Metrics: f.metrics, RuntimeObservations: f.runtimeObservations, RuntimeHistory: f.runtimeHistory, + }, f +} + +// execution is an Execution group backed by f's strict fakes, reporting +// testExecutorURL and without a native installer. +func (f *testFakes) execution() *Execution { + return &Execution{ExecutorURL: testExecutorURL, Admission: f.admission, SessionArchive: f.sessionArchive, Workspaces: f.workspaces} +} + +// sandboxes is a Sandboxes group backed by f's strict fakes. It requires +// Execution. +func (f *testFakes) sandboxes() *Sandboxes { + return &Sandboxes{Deployment: f.deployment, DeploymentChanges: f.deploymentChanges, ConfigurationDiscovery: f.configurationDiscovery} +} + +// coreKeys accepts each token as a Core key. +func coreKeys(t testing.TB, tokens ...string) *DeploymentAuthenticator { + t.Helper() + digests := make([]string, len(tokens)) + for i, token := range tokens { + digests[i] = runtimedevice.HashCredential(token) + } + keys, err := NewDeploymentAuthenticator(digests) + if err != nil { + t.Fatal(err) + } + return keys +} + +// newTestHandler builds the handler from deps and fails the test when +// NewHandler rejects them. +func newTestHandler(t testing.TB, deps Dependencies) http.Handler { + t.Helper() + h, err := NewHandler(deps) + if err != nil { + t.Fatal(err) + } + return h +} + +// noDeploymentModelProvider is a deployment without a default model provider. +func noDeploymentModelProvider(context.Context, string) (*store.DeploymentModelProviderSnapshot, error) { + return nil, nil +} + +func TestNewHandlerAcceptsCompleteDependencies(t *testing.T) { + deps, f := testDependencies(t) + if _, err := NewHandler(deps); err != nil { + t.Fatal(err) + } + deps.Execution = f.execution() + deps.Execution.NativeInstaller = &NativeInstaller{Version: "build"} + deps.Sandboxes = f.sandboxes() + if _, err := NewHandler(deps); err != nil { + t.Fatal(err) + } +} + +func TestNewHandlerRejectsIncompleteDependencies(t *testing.T) { + for _, test := range []struct { + missing string + change func(*Dependencies, *testFakes) + }{ + {"default Harness", func(d *Dependencies, _ *testFakes) { d.Engine = "" }}, + {"CoreKeys", func(d *Dependencies, _ *testFakes) { d.CoreKeys = nil }}, + {"InstallationBindings", func(d *Dependencies, _ *testFakes) { d.InstallationBindings = nil }}, + {"Projects", func(d *Dependencies, _ *testFakes) { d.Projects = nil }}, + {"Sessions", func(d *Dependencies, _ *testFakes) { d.Sessions = nil }}, + {"RuntimeHistory", func(d *Dependencies, _ *testFakes) { d.RuntimeHistory = nil }}, + {"Execution.ExecutorURL", func(d *Dependencies, f *testFakes) { + d.Execution = f.execution() + d.Execution.ExecutorURL = "" + }}, + {"Execution.Admission", func(d *Dependencies, f *testFakes) { + d.Execution = f.execution() + d.Execution.Admission = nil + }}, + {"Execution.NativeInstaller.Version", func(d *Dependencies, f *testFakes) { + d.Execution = f.execution() + d.Execution.NativeInstaller = &NativeInstaller{} + }}, + {"Sandboxes requires Execution", func(d *Dependencies, f *testFakes) { d.Sandboxes = f.sandboxes() }}, + {"Sandboxes.ConfigurationDiscovery", func(d *Dependencies, f *testFakes) { + d.Execution, d.Sandboxes = f.execution(), f.sandboxes() + d.Sandboxes.ConfigurationDiscovery = nil + }}, + } { + t.Run(test.missing, func(t *testing.T) { + deps, f := testDependencies(t) + test.change(&deps, f) + if h, err := NewHandler(deps); err == nil || h != nil || !strings.Contains(err.Error(), test.missing) { + t.Fatalf("NewHandler = %v, %v", h, err) + } + }) + } +} diff --git a/services/core/internal/api/environment_creation_test.go b/services/core/internal/api/environment_creation_test.go index ca72dd392..d14c2d0a4 100644 --- a/services/core/internal/api/environment_creation_test.go +++ b/services/core/internal/api/environment_creation_test.go @@ -49,21 +49,33 @@ func (f *environmentCreationFixture) CreateSessionStream(ctx context.Context, te return store.SessionCreation{Session: session, Created: true}, err } -func environmentCreationHandler(t *testing.T, engine string, options ...Option) (http.Handler, *environmentCreationFixture) { +// environmentCreationHandler serves Session creation and reads from a fresh +// environmentCreationFixture, with engine as the default Harness and a +// deployment model provider for every harness. Each configure func adjusts +// the dependencies before the handler is built. +func environmentCreationHandler(t *testing.T, engine string, configure ...func(*Dependencies, *testFakes)) (http.Handler, *environmentCreationFixture) { t.Helper() fixture := &environmentCreationFixture{} - auth, err := NewAuthenticator([]APIKey{{ + deps, fakes := testDependencies(t) + deps.Engine = engine + fakes.projects.resolveProjectAPIKey = projectKeys(t, APIKey{ OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential("key"), TenantID: uuid.NewString(), - }}) - if err != nil { - t.Fatal(err) + }).ResolveProjectAPIKey + fixture.serve(fakes) + fakes.sessions.findSessionCreation, fakes.sessions.createSession, fakes.sessions.createSessionStream = fixture.FindSessionCreation, fixture.CreateSession, fixture.CreateSessionStream + fakes.modelProviders.deploymentModelProvider = fixtureDeploymentProvider + for _, c := range configure { + c(&deps, fakes) } - handler, err := NewHandler(fixture, auth, engine, append([]Option{withFixtureDeploymentProvider()}, options...)...) - if err != nil { - t.Fatal(err) - } - return handler, fixture + return newTestHandler(t, deps), fixture +} + +// selfHostedExecution enables Execution reporting environmentOrigin to +// self-hosted Sessions. +func selfHostedExecution(d *Dependencies, f *testFakes) { + d.Execution = f.execution() + d.Execution.ExecutorURL = environmentOrigin } func TestSelfHostedEmptyCreationAndStream(t *testing.T) { @@ -72,7 +84,7 @@ func TestSelfHostedEmptyCreationAndStream(t *testing.T) { for _, input := range []string{"", `,"input":null`} { for _, stream := range []bool{false, true} { t.Run(fmt.Sprintf("%s/%s/stream=%t", capability, input, stream), func(t *testing.T) { - handler, fixture := environmentCreationHandler(t, "codex", WithExecution(&inputRecorder{}), WithEnvironmentRemoteURL(environmentOrigin)) + handler, fixture := environmentCreationHandler(t, "codex", selfHostedExecution) server := httptest.NewServer(handler) defer server.Close() ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) @@ -176,7 +188,7 @@ func TestSelfHostedCreationRejectsBeforePersistence(t *testing.T) { if engine == "" { engine = "codex" } - handler, fixture := environmentCreationHandler(t, engine, WithExecution(&inputRecorder{}), WithEnvironmentRemoteURL(environmentOrigin)) + handler, fixture := environmentCreationHandler(t, engine, selfHostedExecution) environment := "" if tc.environment != "" { environment = `,"environment":` + tc.environment @@ -196,28 +208,30 @@ func TestSelfHostedCreationRejectsBeforePersistence(t *testing.T) { } } +// Execution, which carries the executor URL, is required; a URL without +// Execution cannot be configured (TestNewHandlerRejectsIncompleteDependencies). func TestSelfHostedCreationRequiresOperatorExecution(t *testing.T) { - for _, options := range [][]Option{nil, {WithExecution(&inputRecorder{})}, {WithEnvironmentRemoteURL(environmentOrigin)}} { - for _, stream := range []bool{false, true} { - handler, fixture := environmentCreationHandler(t, "codex", options...) - body := fmt.Sprintf(`{"agent":{"model":"MiniMax-M3"},"environment":{"type":"self_hosted","workspace_directory":"/workspace"},"stream":%t,%s}`, stream, fixtureSessionProvider) - request := httptest.NewRequest(http.MethodPost, "/v1/agents/sessions", strings.NewReader(body)) - request.Header.Set("Authorization", "Bearer key") - request.Header.Set("OpenAI-Beta", "agents=v1") - request.Header.Set("Content-Type", "application/json") - response := httptest.NewRecorder() - handler.ServeHTTP(response, request) - var failure v1.ErrorResponse - if response.Code != http.StatusServiceUnavailable || json.Unmarshal(response.Body.Bytes(), &failure) != nil || failure.Error.Code == nil || *failure.Error.Code != "execution_unavailable" || fixture.input.Engine != "" { - t.Fatal("operator prerequisites did not fail before persistence", response.Code, response.Body.String(), fixture.input) - } + for _, stream := range []bool{false, true} { + unavailable := 0 + handler, fixture := environmentCreationHandler(t, "codex", func(_ *Dependencies, f *testFakes) { f.metrics.recordUnavailable = func() { unavailable++ } }) + body := fmt.Sprintf(`{"agent":{"model":"MiniMax-M3"},"environment":{"type":"self_hosted","workspace_directory":"/workspace"},"stream":%t,%s}`, stream, fixtureSessionProvider) + request := httptest.NewRequest(http.MethodPost, "/v1/agents/sessions", strings.NewReader(body)) + request.Header.Set("Authorization", "Bearer key") + request.Header.Set("OpenAI-Beta", "agents=v1") + request.Header.Set("Content-Type", "application/json") + response := httptest.NewRecorder() + handler.ServeHTTP(response, request) + var failure v1.ErrorResponse + if response.Code != http.StatusServiceUnavailable || json.Unmarshal(response.Body.Bytes(), &failure) != nil || failure.Error.Code == nil || *failure.Error.Code != "execution_unavailable" || fixture.input.Engine != "" || unavailable != 1 { + t.Fatal("operator prerequisites did not fail before persistence", response.Code, response.Body.String(), fixture.input) } } } func TestHostedCreationRequiresOperatorExecution(t *testing.T) { for _, stream := range []bool{false, true} { - handler, fixture := environmentCreationHandler(t, "codex", WithExecution(&inputRecorder{}), WithEnvironmentRemoteURL(environmentOrigin)) + unavailable := 0 + handler, fixture := environmentCreationHandler(t, "codex", selfHostedExecution, func(_ *Dependencies, f *testFakes) { f.metrics.recordUnavailable = func() { unavailable++ } }) body := fmt.Sprintf(`{"agent":{"model":"model"},"environment":{"type":"openai_hosted"},"stream":%t,"input":"Initialize the hosted execution."}`, stream) request := httptest.NewRequest(http.MethodPost, "/v1/agents/sessions", strings.NewReader(body)) request.Header.Set("Authorization", "Bearer key") @@ -226,7 +240,7 @@ func TestHostedCreationRequiresOperatorExecution(t *testing.T) { response := httptest.NewRecorder() handler.ServeHTTP(response, request) var failure v1.ErrorResponse - if response.Code != http.StatusServiceUnavailable || json.Unmarshal(response.Body.Bytes(), &failure) != nil || failure.Error.Code == nil || *failure.Error.Code != "execution_unavailable" || fixture.input.Engine != "" { + if response.Code != http.StatusServiceUnavailable || json.Unmarshal(response.Body.Bytes(), &failure) != nil || failure.Error.Code == nil || *failure.Error.Code != "execution_unavailable" || fixture.input.Engine != "" || unavailable != 1 { t.Fatal("hosted configuration bypassed operator prerequisites", response.Code, response.Body.String()) } } diff --git a/services/core/internal/api/environment_executor_management.go b/services/core/internal/api/environment_executor_management.go index 008b37b4c..acfda8f83 100644 --- a/services/core/internal/api/environment_executor_management.go +++ b/services/core/internal/api/environment_executor_management.go @@ -8,19 +8,17 @@ import ( "net/http" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/go-chi/chi/v5" "github.com/google/uuid" ) -// EnvironmentExecutorStore manages the executor credentials of a Project's -// self_hosted Environments. The Project's principal is the credential's -// execution principal; the Core key that authorizes the request is not. -type EnvironmentExecutorStore interface { - ProjectExecutorCredentialState(context.Context, identity.Principal, string) (store.ExecutorCredentialState, error) - IssueProjectExecutorCredential(context.Context, identity.Principal, string, string, bool) (store.IssuedExecutorCredential, error) - RevokeProjectExecutorCredential(context.Context, identity.Principal, string, string) error +// ExecutorConnections observes current executor authority and its actual +// gateway peer. The observer runs after the Environments snapshot closes and +// must recheck authority after inspecting the peer. Without a gateway peer, no +// binding is connected. +type ExecutorConnections interface { + ExecutorConnected(ctx context.Context, environmentID, credentialDigest string) (bool, error) } type EnvironmentExecutorCredentialRequest struct { @@ -43,25 +41,14 @@ type ExecutorConnection struct { LastSeenAt *time.Time `json:"last_seen_at" binding:"required" format:"date-time" extensions:"x-nullable"` } -// WithExecutorConnections observes current authority and its actual gateway peer. -// The observer runs after the store snapshot closes and must recheck authority -// after inspecting the peer. Without an observer, no binding is called connected. -func WithExecutorConnections(observe func(context.Context, string, string) (bool, error)) Option { - return func(h *Handler) { h.executorConnections = observe } -} - // registerExecutorCredentialRoutes adds executor credential issuance to the // Core-key-authenticated /core/v1 router. func (h *Handler) registerExecutorCredentialRoutes(r chi.Router) { - s, ok := h.store.(EnvironmentExecutorStore) - if !ok || h.projectKeys == nil { - return - } const path = "/projects/{project_id}/environments/{environment_id}/executor-credentials" r.Get("/projects/{project_id}/environments/{environment_id}/installation", h.getEnvironmentInstallation) - r.Get(path, func(w http.ResponseWriter, r *http.Request) { h.listExecutorCredentials(w, r, s) }) - r.Post(path, func(w http.ResponseWriter, r *http.Request) { h.issueExecutorCredential(w, r, s) }) - r.Delete(path+"/{key_id}", func(w http.ResponseWriter, r *http.Request) { h.revokeExecutorCredential(w, r, s) }) + r.Get(path, h.listExecutorCredentials) + r.Post(path, h.issueExecutorCredential) + r.Delete(path+"/{key_id}", h.revokeExecutorCredential) } // @Summary List a self_hosted Environment's executor credentials @@ -74,12 +61,12 @@ func (h *Handler) registerExecutorCredentialRoutes(r chi.Router) { // @Success 200 {object} api.ExecutorCredentialList // @Failure 401,404,500 {object} CoreErrorResponse // @Router /core/v1/projects/{project_id}/environments/{environment_id}/executor-credentials [get] -func (h *Handler) listExecutorCredentials(w http.ResponseWriter, r *http.Request, s EnvironmentExecutorStore) { +func (h *Handler) listExecutorCredentials(w http.ResponseWriter, r *http.Request) { binding, ok := h.adminProjectScope(w, r) if !ok { return } - state, err := s.ProjectExecutorCredentialState(r.Context(), binding.Principal, chi.URLParam(r, "environment_id")) + state, err := h.Environments.ProjectExecutorCredentialState(r.Context(), binding.Principal, chi.URLParam(r, "environment_id")) if err != nil { writeStoreError(w, r, err) return @@ -88,8 +75,8 @@ func (h *Handler) listExecutorCredentials(w http.ResponseWriter, r *http.Request observed := state.Connection if observed.DeviceID != "" { connection = ExecutorConnection{Status: "disconnected", BoundKeyID: observed.BoundKeyID, EnrolledAt: observed.EnrolledAt, LastSeenAt: observed.LastSeenAt} - if observed.EnvironmentStatus == "connected" && observed.CredentialHash != "" && h.executorConnections != nil { - connected, err := h.executorConnections(r.Context(), state.EnvironmentID, observed.CredentialHash) + if observed.EnvironmentStatus == "connected" && observed.CredentialHash != "" { + connected, err := h.ExecutorConnections.ExecutorConnected(r.Context(), state.EnvironmentID, observed.CredentialHash) if err != nil && !errors.Is(err, store.ErrNotFound) && !errors.Is(err, store.ErrDeviceBindingConflict) { writeStoreError(w, r, err) return @@ -114,7 +101,7 @@ func (h *Handler) listExecutorCredentials(w http.ResponseWriter, r *http.Request // @Success 201 {object} store.IssuedExecutorCredential // @Failure 400,401,404,409,500 {object} CoreErrorResponse // @Router /core/v1/projects/{project_id}/environments/{environment_id}/executor-credentials [post] -func (h *Handler) issueExecutorCredential(w http.ResponseWriter, r *http.Request, s EnvironmentExecutorStore) { +func (h *Handler) issueExecutorCredential(w http.ResponseWriter, r *http.Request) { // Check order: request body (400), target (404), archived Project (409), // then the key itself (409 exists, or 404 for rotating an unknown key). raw, ok := readJSONBody(w, r) @@ -131,7 +118,7 @@ func (h *Handler) issueExecutorCredential(w http.ResponseWriter, r *http.Request if !ok { return } - credential, err := s.IssueProjectExecutorCredential(r.Context(), binding.Principal, chi.URLParam(r, "environment_id"), input.KeyID, input.Rotate) + credential, err := h.Environments.IssueProjectExecutorCredential(r.Context(), binding.Principal, chi.URLParam(r, "environment_id"), input.KeyID, input.Rotate) if err != nil { writeStoreError(w, r, err) return @@ -149,7 +136,7 @@ func (h *Handler) issueExecutorCredential(w http.ResponseWriter, r *http.Request // @Success 204 // @Failure 401,404,500 {object} CoreErrorResponse // @Router /core/v1/projects/{project_id}/environments/{environment_id}/executor-credentials/{key_id} [delete] -func (h *Handler) revokeExecutorCredential(w http.ResponseWriter, r *http.Request, s EnvironmentExecutorStore) { +func (h *Handler) revokeExecutorCredential(w http.ResponseWriter, r *http.Request) { binding, ok := h.adminProjectScope(w, r) if !ok { return @@ -159,7 +146,7 @@ func (h *Handler) revokeExecutorCredential(w http.ResponseWriter, r *http.Reques writeStoreError(w, r, store.ErrNotFound) return } - if err := s.RevokeProjectExecutorCredential(r.Context(), binding.Principal, chi.URLParam(r, "environment_id"), keyID); err != nil { + if err := h.Environments.RevokeProjectExecutorCredential(r.Context(), binding.Principal, chi.URLParam(r, "environment_id"), keyID); err != nil { writeStoreError(w, r, err) return } diff --git a/services/core/internal/api/environment_executor_management_test.go b/services/core/internal/api/environment_executor_management_test.go index e16db934c..48c5dff24 100644 --- a/services/core/internal/api/environment_executor_management_test.go +++ b/services/core/internal/api/environment_executor_management_test.go @@ -4,19 +4,18 @@ import ( "context" "encoding/json" "errors" + "net/http" "strings" "testing" "time" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/adminaudit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" ) type executorManagementFixture struct { - ResourceStore principal identity.Principal environment, key string rotate, audited bool @@ -49,21 +48,22 @@ func (f *executorManagementFixture) RevokeProjectExecutorCredential(ctx context. return f.err } +// executorManagementHandler serves f's executor credentials to the Core key +// "admin" for managementProjectID, observing live connections with connected. +func executorManagementHandler(t *testing.T, key APIKey, f *executorManagementFixture, connected func(context.Context, string, string) (bool, error)) http.Handler { + t.Helper() + deps, fakes := managementFakes(t, key) + fakes.environments.projectExecutorCredentialState = f.ProjectExecutorCredentialState + fakes.environments.issueProjectExecutorCredential = f.IssueProjectExecutorCredential + fakes.environments.revokeProjectExecutorCredential = f.RevokeProjectExecutorCredential + fakes.executorConnections.executorConnected = connected + return newTestHandler(t, deps) +} + func TestProjectExecutorCredentialsHTTP(t *testing.T) { f := &executorManagementFixture{} key := callerBinding() - auth, err := NewAuthenticator([]APIKey{key}) - if err != nil { - t.Fatal(err) - } - admin, err := NewDeploymentAuthenticator([]string{runtimedevice.HashCredential("admin")}) - if err != nil { - t.Fatal(err) - } - h, err := NewHandler(f, auth, "codex", WithProjectAPIKeys(managementProjectStore(key), admin)) - if err != nil { - t.Fatal(err) - } + h := executorManagementHandler(t, key, f, nil) environment, keyID := uuid.NewString(), uuid.NewString() path := "/core/v1/projects/" + managementProjectID + "/environments/" + environment + "/executor-credentials" body := `{"key_id":"` + keyID + `"}` @@ -142,36 +142,26 @@ func TestProjectExecutorCredentialsHTTP(t *testing.T) { func TestExecutorConnectionListObservation(t *testing.T) { for _, tc := range []struct { name string - observer bool connected bool err error want string status int }{ - {"live", true, true, nil, "connected", 200}, {"closed", true, false, nil, "disconnected", 200}, - {"no registry", false, false, nil, "disconnected", 200}, {"rotated", true, false, store.ErrDeviceBindingConflict, "disconnected", 200}, - {"revoked", true, false, store.ErrNotFound, "disconnected", 200}, {"database failure", true, false, errors.New("private-database"), "", 500}, + {"live", true, nil, "connected", 200}, {"closed", false, nil, "disconnected", 200}, + {"rotated", false, store.ErrDeviceBindingConflict, "disconnected", 200}, + {"revoked", false, store.ErrNotFound, "disconnected", 200}, {"database failure", false, errors.New("private-database"), "", 500}, } { t.Run(tc.name, func(t *testing.T) { key := callerBinding() at := time.Unix(1, 0).UTC() bound := "bound-key" f := &executorManagementFixture{connection: store.ExecutorConnectionState{DeviceID: "device", BoundKeyID: &bound, EnrolledAt: &at, CredentialHash: "private-digest", EnvironmentStatus: "connected"}} - auth, _ := NewAuthenticator([]APIKey{key}) - admin, _ := NewDeploymentAuthenticator([]string{runtimedevice.HashCredential("admin")}) - opts := []Option{WithProjectAPIKeys(managementProjectStore(key), admin)} - if tc.observer { - opts = append(opts, WithExecutorConnections(func(_ context.Context, environment, digest string) (bool, error) { - if environment != "environment" || digest != "private-digest" { - t.Fatal("wrong binding") - } - return tc.connected, tc.err - })) - } - h, err := NewHandler(f, auth, "codex", opts...) - if err != nil { - t.Fatal(err) - } + h := executorManagementHandler(t, key, f, func(_ context.Context, environment, digest string) (bool, error) { + if environment != "environment" || digest != "private-digest" { + t.Fatal("wrong binding") + } + return tc.connected, tc.err + }) w := projectKeyHTTP(h, "GET", "/core/v1/projects/"+managementProjectID+"/environments/environment/executor-credentials", "admin", "") if w.Code != tc.status { t.Fatal(w.Code, w.Body) @@ -196,20 +186,14 @@ func TestExecutorConnectionListUsesResolvedEnvironment(t *testing.T) { resolvedEnvironment: canonical, connection: store.ExecutorConnectionState{DeviceID: "device", CredentialHash: "private-digest", EnvironmentStatus: "connected"}, } - auth, _ := NewAuthenticator([]APIKey{key}) - admin, _ := NewDeploymentAuthenticator([]string{runtimedevice.HashCredential("admin")}) observations := 0 - h, err := NewHandler(f, auth, "codex", WithProjectAPIKeys(managementProjectStore(key), admin), - WithExecutorConnections(func(_ context.Context, environment, digest string) (bool, error) { - observations++ - if environment != canonical || digest != "private-digest" { - return false, store.ErrDeviceBindingConflict - } - return true, nil - })) - if err != nil { - t.Fatal(err) - } + h := executorManagementHandler(t, key, f, func(_ context.Context, environment, digest string) (bool, error) { + observations++ + if environment != canonical || digest != "private-digest" { + return false, store.ErrDeviceBindingConflict + } + return true, nil + }) for _, spelling := range []string{canonical, strings.ToUpper(canonical), strings.ReplaceAll(canonical, "-", "")} { w := projectKeyHTTP(h, "GET", "/core/v1/projects/"+managementProjectID+"/environments/"+spelling+"/executor-credentials", "admin", "") var got ExecutorCredentialList diff --git a/services/core/internal/api/environment_files.go b/services/core/internal/api/environment_files.go index f163ec73a..e57d3e0f8 100644 --- a/services/core/internal/api/environment_files.go +++ b/services/core/internal/api/environment_files.go @@ -16,12 +16,11 @@ import ( "github.com/go-chi/chi/v5" ) -type EnvironmentDirectoryReader interface { +// EnvironmentWorkspaces reads and writes a connected Environment's live +// workspace through its Runtime. +type EnvironmentWorkspaces interface { ReadEnvironmentDirectory(context.Context, store.Environment, string) (proto.WorkspaceDirectoryResult, error) -} - -func WithEnvironmentDirectoryReader(reader EnvironmentDirectoryReader) Option { - return func(h *Handler) { h.directoryReader = reader } + WriteEnvironmentFile(context.Context, store.Environment, string, []byte) (int64, error) } // @Summary List live Environment files @@ -39,7 +38,7 @@ func WithEnvironmentDirectoryReader(reader EnvironmentDirectoryReader) Option { // @Failure 400,401,404,500,503 {object} v1.ErrorResponse // @Router /agents/environments/{environment_id}/files [get] func (h *Handler) listEnvironmentFiles(w http.ResponseWriter, r *http.Request) { - environment, err := h.store.GetEnvironment(r.Context(), tenantID(r), chi.URLParam(r, "environment_id")) + environment, err := h.Environments.GetEnvironment(r.Context(), tenantID(r), chi.URLParam(r, "environment_id")) if err != nil { writeStoreError(w, r, err) return @@ -48,7 +47,7 @@ func (h *Handler) listEnvironmentFiles(w http.ResponseWriter, r *http.Request) { if !ok || !environmentFilesAccessible(w, environment) { return } - if h.directoryReader == nil || !execution.LocalWorkspaceConfiguration(environment.Configuration) { + if h.Execution == nil || !execution.LocalWorkspaceConfiguration(environment.Configuration) { writeStoreError(w, r, execution.ErrExecutionUnavailable) return } @@ -57,7 +56,7 @@ func (h *Handler) listEnvironmentFiles(w http.ResponseWriter, r *http.Request) { writeStoreError(w, r, execution.ErrExecutionUnavailable) return } - result, err := h.directoryReader.ReadEnvironmentDirectory(r.Context(), environment, options.relativeDirectory) + result, err := h.Execution.Workspaces.ReadEnvironmentDirectory(r.Context(), environment, options.relativeDirectory) if err != nil { writeStoreError(w, r, err) return diff --git a/services/core/internal/api/environment_files_completeness_test.go b/services/core/internal/api/environment_files_completeness_test.go index 24db6b44d..df17a8793 100644 --- a/services/core/internal/api/environment_files_completeness_test.go +++ b/services/core/internal/api/environment_files_completeness_test.go @@ -22,10 +22,11 @@ func TestEnvironmentFilesRejectsIncompleteOrMalformedDirectories(t *testing.T) { "oversized": {Entries: make([]proto.WorkspaceDirectoryEntry, proto.WorkspaceDirectoryMaxEntries+1)}, } { t.Run(name, func(t *testing.T) { - h, f := environmentFilesHandler(t, true) + unavailable := 0 + h, f := environmentFilesHandler(t, true, countEnvironmentFilesUnavailable(&unavailable)) f.result = result w := requestEnvironmentFiles(h, f.environment.ID, "?limit=1", "files-key") - if w.Code != 503 || strings.Contains(w.Body.String(), `"data"`) || strings.Contains(w.Body.String(), `"next"`) || strings.Contains(w.Body.String(), "secret") { + if w.Code != 503 || unavailable != 1 || strings.Contains(w.Body.String(), `"data"`) || strings.Contains(w.Body.String(), `"next"`) || strings.Contains(w.Body.String(), "secret") { t.Fatal("unsafe incomplete response", w.Code, w.Body) } }) diff --git a/services/core/internal/api/environment_files_create.go b/services/core/internal/api/environment_files_create.go index 3979f17f3..8adebd5a7 100644 --- a/services/core/internal/api/environment_files_create.go +++ b/services/core/internal/api/environment_files_create.go @@ -21,14 +21,6 @@ import ( "github.com/go-chi/chi/v5" ) -type EnvironmentFileWriter interface { - WriteEnvironmentFile(context.Context, store.Environment, string, []byte) (int64, error) -} - -func WithEnvironmentFileWriter(writer EnvironmentFileWriter) Option { - return func(h *Handler) { h.fileWriter = writer } -} - // @Summary Create an Environment file from inline bytes or a source file // @Description Uploads standard Base64 bytes to a file beneath /workspace in a qualified local Environment and returns 201. Accepts inline bytes or a project-owned source file_id through the same write path. Unknown body fields are rejected with their name as param. Basic public hosted creation requires explicit managed Runtime configuration; an openai_hosted Environment that has not connected yet returns 400. Inline data is limited to 5 MiB decoded and a file_id copy to 50 MiB. Missing parent directories are created with mode 0700 and the file with mode 0600. An existing destination is never replaced; a directory, an existing file or a path through a symlink or non-directory returns 400. Idle writes exclude execution. Missing receipts return unavailable and retain a durable mutation gate without automatic replay. Error/timing parity with upstream remains unverified. // @Tags Environments @@ -47,7 +39,7 @@ func (h *Handler) createEnvironmentFile(w http.ResponseWriter, r *http.Request) if !ok { return } - environment, err := h.store.GetEnvironment(r.Context(), tenantID(r), chi.URLParam(r, "environment_id")) + environment, err := h.Environments.GetEnvironment(r.Context(), tenantID(r), chi.URLParam(r, "environment_id")) if err != nil { writeStoreError(w, r, err) return @@ -107,12 +99,9 @@ func (h *Handler) createEnvironmentFile(w http.ResponseWriter, r *http.Request) return } if request.Type == "file_id" { - if !h.sourceFilesAvailable(w) { - return - } ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) defer cancel() - err = h.sourceFiles.ReadSourceFile(ctx, tenantID(r), *request.FileID, func(file store.SourceFile, body io.Reader) error { + err = h.Files.ReadSourceFile(ctx, tenantID(r), *request.FileID, func(file store.SourceFile, body io.Reader) error { if file.SizeBytes > proto.WorkspaceWriteMaxBytes { return store.ErrSourceFileTooLarge } @@ -127,7 +116,7 @@ func (h *Handler) createEnvironmentFile(w http.ResponseWriter, r *http.Request) return } } - if h.fileWriter == nil || !execution.LocalWorkspaceConfiguration(environment.Configuration) { + if h.Execution == nil || !execution.LocalWorkspaceConfiguration(environment.Configuration) { writeStoreError(w, r, execution.ErrExecutionUnavailable) return } @@ -135,7 +124,7 @@ func (h *Handler) createEnvironmentFile(w http.ResponseWriter, r *http.Request) writeStoreError(w, r, execution.ErrExecutionUnavailable) return } - size, err := h.fileWriter.WriteEnvironmentFile(r.Context(), environment, strings.TrimPrefix(*request.Path, "/workspace/"), data) + size, err := h.Execution.Workspaces.WriteEnvironmentFile(r.Context(), environment, strings.TrimPrefix(*request.Path, "/workspace/"), data) if err != nil { if !writeFieldError(w, environmentFileWriteError(err)) { writeStoreError(w, r, err) diff --git a/services/core/internal/api/environment_files_create_test.go b/services/core/internal/api/environment_files_create_test.go index b3e963bd4..ea54aa4fa 100644 --- a/services/core/internal/api/environment_files_create_test.go +++ b/services/core/internal/api/environment_files_create_test.go @@ -35,24 +35,25 @@ func (f *environmentFileCreateFixture) WriteEnvironmentFile(_ context.Context, e return int64(len(data)), f.err } -func environmentFileCreateHandler(t *testing.T, extra ...Option) (http.Handler, *environmentFileCreateFixture) { +// environmentFileCreateHandler serves a hosted Environment whose workspace +// the execution Worker lists and writes. +func environmentFileCreateHandler(t *testing.T, configure ...func(*Dependencies, *testFakes)) (http.Handler, *environmentFileCreateFixture) { t.Helper() - _, base := environmentFilesHandler(t, false) + base := newEnvironmentFilesFixture() base.environment.Configuration = json.RawMessage(`{"type":"openai_hosted","network":{"access":"disabled"}}`) f := &environmentFileCreateFixture{environmentFilesFixture: base} - auth, err := NewAuthenticator([]APIKey{ - {OrganizationID: "org", ProjectID: "project", SubjectKind: "user", SubjectID: "caller", TokenSHA256: runtimedevice.HashCredential("files-key"), TenantID: f.environment.TenantID}, - {OrganizationID: "org", ProjectID: "other", SubjectKind: "user", SubjectID: "other", TokenSHA256: runtimedevice.HashCredential("other-key"), TenantID: uuid.NewString()}, - }) - if err != nil { - t.Fatal(err) + deps, fakes := testDependencies(t) + fakes.projects.resolveProjectAPIKey = projectKeys(t, + APIKey{OrganizationID: "org", ProjectID: "project", SubjectKind: "user", SubjectID: "caller", TokenSHA256: runtimedevice.HashCredential("files-key"), TenantID: f.environment.TenantID}, + APIKey{OrganizationID: "org", ProjectID: "other", SubjectKind: "user", SubjectID: "other", TokenSHA256: runtimedevice.HashCredential("other-key"), TenantID: uuid.NewString()}, + ).ResolveProjectAPIKey + fakes.environments.getEnvironment = f.GetEnvironment + deps.Execution = fakes.execution() + fakes.workspaces.readEnvironmentDirectory, fakes.workspaces.writeEnvironmentFile = f.ReadEnvironmentDirectory, f.WriteEnvironmentFile + for _, c := range configure { + c(&deps, fakes) } - options := append([]Option{WithEnvironmentFileWriter(f), WithEnvironmentDirectoryReader(f)}, extra...) - h, err := NewHandler(f, auth, "codex", options...) - if err != nil { - t.Fatal(err) - } - return h, f + return newTestHandler(t, deps), f } func requestCreateEnvironmentFile(h http.Handler, id, body, key string) *httptest.ResponseRecorder { @@ -113,7 +114,8 @@ func TestEnvironmentFileCreateRejectsInvalidUnionAndPath(t *testing.T) { func TestEnvironmentFileCreateAuthorityAndUncertainResults(t *testing.T) { body := `{"type":"inline","data":"YWJj","path":"/workspace/a"}` - h, f := environmentFileCreateHandler(t) + unavailable := 0 + h, f := environmentFileCreateHandler(t, countEnvironmentFilesUnavailable(&unavailable)) for _, key := range []string{"other-key", "invalid"} { w := requestCreateEnvironmentFile(h, f.environment.ID, body, key) if (key == "other-key" && w.Code != 404) || (key == "invalid" && w.Code != 401) || f.writes != 0 { @@ -128,6 +130,9 @@ func TestEnvironmentFileCreateAuthorityAndUncertainResults(t *testing.T) { if w := requestCreateEnvironmentFile(h, f.environment.ID, body, "files-key"); w.Code != 503 { t.Fatal("wrong byte count reported success", w.Code) } + if unavailable != 2 { + t.Fatal("unavailability not counted", unavailable) + } f.environment.Configuration = json.RawMessage(`{"type":"self_hosted","workspace_directory":"/workspace"}`) f.writes, f.wrongSize = 0, false if w := requestCreateEnvironmentFile(h, f.environment.ID, body, "files-key"); w.Code != 201 || f.writes != 1 { diff --git a/services/core/internal/api/environment_files_deadline_test.go b/services/core/internal/api/environment_files_deadline_test.go index 905e195d1..6f1f2cd3b 100644 --- a/services/core/internal/api/environment_files_deadline_test.go +++ b/services/core/internal/api/environment_files_deadline_test.go @@ -13,7 +13,7 @@ import ( func TestEnvironmentFilesReadOutlivesDefaultHTTPWriteDeadline(t *testing.T) { for _, status := range []int{http.StatusOK, http.StatusServiceUnavailable} { t.Run(http.StatusText(status), func(t *testing.T) { - handler, fixture := environmentFilesHandler(t, true) + handler, fixture := environmentFilesHandler(t, true, func(_ *Dependencies, f *testFakes) { f.metrics.recordUnavailable = func() {} }) fixture.readDelay = 100 * time.Millisecond if status == http.StatusServiceUnavailable { fixture.readError = execution.ErrExecutionUnavailable diff --git a/services/core/internal/api/environment_files_test.go b/services/core/internal/api/environment_files_test.go index 93b29e544..82e8a94a4 100644 --- a/services/core/internal/api/environment_files_test.go +++ b/services/core/internal/api/environment_files_test.go @@ -21,7 +21,6 @@ import ( ) type environmentFilesFixture struct { - ResourceStore environment store.Environment result proto.WorkspaceDirectoryResult storeError, readError error @@ -55,13 +54,19 @@ func (f *environmentFilesFixture) ReadEnvironmentDirectory(ctx context.Context, return f.result, f.readError } -func environmentFilesHandler(t *testing.T, enabled bool) (http.Handler, *environmentFilesFixture) { - t.Helper() - f := &environmentFilesFixture{ +func newEnvironmentFilesFixture() *environmentFilesFixture { + return &environmentFilesFixture{ environment: store.Environment{ID: uuid.NewString(), TenantID: uuid.NewString(), SessionID: uuid.NewString(), Status: "connected", Configuration: json.RawMessage(`{"type":"self_hosted","workspace_directory":"/workspace"}`)}, result: proto.WorkspaceDirectoryResult{Entries: []proto.WorkspaceDirectoryEntry{}}, } +} + +// environmentFilesHandler serves f's Environment. Without enabled, Core has no +// execution Worker. +func environmentFilesHandler(t *testing.T, enabled bool, configure ...func(*Dependencies, *testFakes)) (http.Handler, *environmentFilesFixture) { + t.Helper() + f := newEnvironmentFilesFixture() keys := []APIKey{} for _, key := range []struct{ token, tenant, project string }{ {"files-key", f.environment.TenantID, "files-project"}, @@ -71,19 +76,23 @@ func environmentFilesHandler(t *testing.T, enabled bool) (http.Handler, *environ keys = append(keys, APIKey{OrganizationID: "files-org", ProjectID: key.project, SubjectKind: "user", SubjectID: key.project, TokenSHA256: runtimedevice.HashCredential(key.token), TenantID: key.tenant}) } - auth, err := NewAuthenticator(keys) - if err != nil { - t.Fatal(err) - } - options := []Option{} + deps, fakes := testDependencies(t) + deps.Engine = "fake_alpha" + fakes.projects.resolveProjectAPIKey = projectKeys(t, keys...).ResolveProjectAPIKey + fakes.environments.getEnvironment = f.GetEnvironment if enabled { - options = append(options, WithEnvironmentDirectoryReader(f)) + deps.Execution = fakes.execution() + fakes.workspaces.readEnvironmentDirectory = f.ReadEnvironmentDirectory } - h, err := NewHandler(f, auth, "fake_alpha", options...) - if err != nil { - t.Fatal(err) + for _, c := range configure { + c(&deps, fakes) } - return h, f + return newTestHandler(t, deps), f +} + +// countEnvironmentFilesUnavailable counts execution_unavailable responses. +func countEnvironmentFilesUnavailable(count *int) func(*Dependencies, *testFakes) { + return func(_ *Dependencies, f *testFakes) { f.metrics.recordUnavailable = func() { *count++ } } } func requestEnvironmentFiles(h http.Handler, id, query, key string) *httptest.ResponseRecorder { @@ -225,7 +234,8 @@ func TestEnvironmentFilesSafeStoreAndReaderFailures(t *testing.T) { }{ {store.ErrNotFound, 404}, {store.ErrInvalidInput, 400}, {execution.ErrExecutionUnavailable, 503}, {errors.New("private-native-secret"), 500}, } { - h, f := environmentFilesHandler(t, true) + unavailable := 0 + h, f := environmentFilesHandler(t, true, countEnvironmentFilesUnavailable(&unavailable)) if target == "store" { f.storeError = test.err } else { @@ -235,10 +245,14 @@ func TestEnvironmentFilesSafeStoreAndReaderFailures(t *testing.T) { if w.Code != test.status || strings.Contains(w.Body.String(), "private-native-secret") || strings.Contains(w.Body.String(), `"data"`) || strings.Contains(w.Body.String(), `"next"`) { t.Fatal("unsafe error", target, w.Code, w.Body) } + if unavailable != 0 != (test.status == 503) { + t.Fatal("unavailability not counted", target, unavailable) + } } } - h, f := environmentFilesHandler(t, false) - if w := requestEnvironmentFiles(h, f.environment.ID, "", "files-key"); w.Code != 503 || f.reads != 0 { + unavailable := 0 + h, f := environmentFilesHandler(t, false, countEnvironmentFilesUnavailable(&unavailable)) + if w := requestEnvironmentFiles(h, f.environment.ID, "", "files-key"); w.Code != 503 || f.reads != 0 || unavailable != 1 { t.Fatal("missing reader accepted", w.Code, f) } } diff --git a/services/core/internal/api/environment_files_wire_test.go b/services/core/internal/api/environment_files_wire_test.go index 143304fc3..b47a0dc49 100644 --- a/services/core/internal/api/environment_files_wire_test.go +++ b/services/core/internal/api/environment_files_wire_test.go @@ -232,7 +232,7 @@ func TestEnvironmentFilesHostedProvisioning(t *testing.T) { const hosted = `{"type":"openai_hosted","network":{"access":"disabled"}}` const createBody = `{"type":"inline","data":"YWJj","path":"/workspace/a"}` sources := &sourceFilesFixture{} - h, f := environmentFileCreateHandler(t, WithSourceFiles(sources)) + h, f := environmentFileCreateHandler(t, sources.wire) f.environment.Configuration, f.environment.Status = json.RawMessage(hosted), "pending" w := requestEnvironmentFiles(h, f.environment.ID, "?path=/workspace/a", "files-key") diff --git a/services/core/internal/api/environment_files_write_test.go b/services/core/internal/api/environment_files_write_test.go index 5a3cdce6b..c470aab21 100644 --- a/services/core/internal/api/environment_files_write_test.go +++ b/services/core/internal/api/environment_files_write_test.go @@ -44,7 +44,7 @@ func TestEnvironmentFileCreateInlineDecodedLimit(t *testing.T) { func TestEnvironmentFileCreateSourceCopyKeepsDestinationBound(t *testing.T) { sources := &sourceFilesFixture{} - h, f := environmentFileCreateHandler(t, WithSourceFiles(sources)) + h, f := environmentFileCreateHandler(t, sources.wire) data := bytes.Repeat([]byte{9}, 6<<20) sources.tenant, sources.data = f.environment.TenantID, data sources.file = store.SourceFile{ID: "file-" + uuid.NewString(), SizeBytes: int64(len(data)), CreatedAt: time.Unix(1, 0)} @@ -57,7 +57,8 @@ func TestEnvironmentFileCreateSourceCopyKeepsDestinationBound(t *testing.T) { func TestEnvironmentFileCreateDestinationConflicts(t *testing.T) { const conflict = "file path conflicts with an existing environment file" const unsafe = "environment.files paths must not traverse symlinks or overwrite existing files" - h, f := environmentFileCreateHandler(t) + unavailable := 0 + h, f := environmentFileCreateHandler(t, countEnvironmentFilesUnavailable(&unavailable)) body := `{"type":"inline","data":"YWJj","path":"/workspace/n1"}` for _, tc := range []struct { err error @@ -73,7 +74,7 @@ func TestEnvironmentFileCreateDestinationConflicts(t *testing.T) { assertListQueryError(t, requestCreateEnvironmentFile(h, f.environment.ID, body, "files-key"), tc.code, nil, tc.message) } f.err = execution.ErrExecutionUnavailable - if w := requestCreateEnvironmentFile(h, f.environment.ID, body, "files-key"); w.Code != 503 { - t.Fatal("unknown outcome changed", w.Code, w.Body) + if w := requestCreateEnvironmentFile(h, f.environment.ID, body, "files-key"); w.Code != 503 || unavailable != 1 { + t.Fatal("unknown outcome changed", w.Code, w.Body, unavailable) } } diff --git a/services/core/internal/api/environment_input.go b/services/core/internal/api/environment_input.go index 2551100cc..69c4e1ae9 100644 --- a/services/core/internal/api/environment_input.go +++ b/services/core/internal/api/environment_input.go @@ -9,7 +9,7 @@ import ( ) func (h *Handler) setEnvironmentInputWriteDeadline(w http.ResponseWriter, r *http.Request, sessionID string) error { - session, err := h.store.GetSession(r.Context(), tenantID(r), sessionID) + session, err := h.Sessions.GetSession(r.Context(), tenantID(r), sessionID) if err != nil { return err } diff --git a/services/core/internal/api/environment_input_test.go b/services/core/internal/api/environment_input_test.go index 02d908463..a8db592b2 100644 --- a/services/core/internal/api/environment_input_test.go +++ b/services/core/internal/api/environment_input_test.go @@ -28,7 +28,11 @@ func TestPublicEnvironmentInputFailureMappings(t *testing.T) { } { t.Run(tc.code, func(t *testing.T) { recorder := &inputRecorder{err: fmt.Errorf("submission: %w", tc.err)} - handler, _, _ := testHandler(t, WithExecution(recorder)) + handler, _, _ := testHandler(t, recorder.admit, func(_ *Dependencies, f *testFakes) { + if tc.code == "execution_unavailable" { + f.metrics.recordUnavailable = func() {} + } + }) request := httptest.NewRequest(http.MethodPost, "/v1/agents/sessions/session/events", strings.NewReader(`{"events":[{"type":"agent.session.input.message","input":[{"role":"user","content":[{"type":"input_text","text":"Start"}]}]}]}`)) request.Header.Set("Authorization", "Bearer test-api-key") request.Header.Set("OpenAI-Beta", "agents=v1") @@ -44,7 +48,6 @@ func TestPublicEnvironmentInputFailureMappings(t *testing.T) { } type waitingEnvironmentInput struct { - InputSubmitter entered chan struct{} release chan struct{} } @@ -63,14 +66,15 @@ func TestPreparedEnvironmentInputWaitExtendsOnlyItsResponseDeadline(t *testing.T for _, environment := range []string{"none", "self_hosted", "openai_hosted"} { t.Run(environment, func(t *testing.T) { waiting := &waitingEnvironmentInput{entered: make(chan struct{}), release: make(chan struct{})} - options := []Option{WithExecution(waiting)} environmentJSON := `{"type":"none"}` if environment == "self_hosted" { environmentJSON = `{"type":"self_hosted","workspace_directory":"/workspace"},` + fixtureSessionProvider - options = append(options, WithEnvironmentRemoteURL(environmentOrigin)) } - handler, fixture := environmentCreationHandler(t, "codex", options...) - waiting.InputSubmitter = &inputRecorder{ResourceStore: fixture} + // The Worker admits the initial input into the fixture and waits on + // the next input. + handler, fixture := environmentCreationHandler(t, "codex", selfHostedExecution, func(_ *Dependencies, f *testFakes) { + f.admission.createSession, f.admission.submitInputs = f.sessions.createSession, waiting.SubmitInputs + }) create := httptest.NewRequest(http.MethodPost, "/v1/agents/sessions", strings.NewReader(`{"agent":{"model":"MiniMax-M3"},"environment":`+environmentJSON+`,"input":"Prepare the response deadline fixture."}`)) create.Header.Set("Authorization", "Bearer key") create.Header.Set("OpenAI-Beta", "agents=v1") diff --git a/services/core/internal/api/environment_installation.go b/services/core/internal/api/environment_installation.go index 9c9cb9d80..30c102f82 100644 --- a/services/core/internal/api/environment_installation.go +++ b/services/core/internal/api/environment_installation.go @@ -13,33 +13,46 @@ import ( "github.com/go-chi/chi/v5" ) -type environmentInstallationStore interface { - AuthorizeEnvironmentInstallation(context.Context, identity.Principal, string, string) (string, int64, error) - ValidateEnvironmentInstallation(context.Context, string, string) (store.InstallationAuthorization, error) - ClaimEnvironmentInstallation(context.Context, string, string, string) error +// NativeInstaller serves the self-hosted native installation of this build. +type NativeInstaller struct { + // Version is the build revision executors install and claim. + Version string + // Catalog holds the matching installation artifacts. It is nil when the + // operator installed none: installations then report unavailable and the + // grant routes answer 503 installation_unavailable. + Catalog *nativeinstaller.Catalog } -func WithNativeInstaller(catalog *nativeinstaller.Catalog, version string) Option { - return func(h *Handler) { h.nativeInstaller, h.nativeVersion = catalog, version } +// nativeInstaller returns this Core's native installer, or nil when it serves +// none. +func (h *Handler) nativeInstaller() *NativeInstaller { + if h.Execution == nil { + return nil + } + return h.Execution.NativeInstaller } func (h *Handler) installationFor(ctx context.Context, principal identity.Principal, environment string) (*v1.EnvironmentInstallation, error) { - result := &v1.EnvironmentInstallation{Status: "unavailable", Version: h.nativeVersion, Message: "This Core has no matching native installation distribution. Ask its operator to install the qualified release artifacts."} - s, ok := h.store.(environmentInstallationStore) - if !ok || h.nativeInstaller == nil { + installer := h.nativeInstaller() + result := &v1.EnvironmentInstallation{Status: "unavailable", Message: "This Core has no matching native installation distribution. Ask its operator to install the qualified release artifacts."} + if installer == nil { + return result, nil + } + result.Version = installer.Version + if installer.Catalog == nil { return result, nil } - token, expires, err := s.AuthorizeEnvironmentInstallation(ctx, principal, environment, h.nativeVersion) + token, expires, err := h.Environments.AuthorizeEnvironmentInstallation(ctx, principal, environment, installer.Version) if err != nil { return nil, err } - origin := strings.TrimSuffix(h.executorURL, "/api/v1/agent-daemon/ws") + origin := strings.TrimSuffix(h.Execution.ExecutorURL, "/api/v1/agent-daemon/ws") origin = strings.Replace(strings.Replace(origin, "wss://", "https://", 1), "ws://", "http://", 1) - return &v1.EnvironmentInstallation{Status: "available", Version: h.nativeVersion, ExpiresAt: expires, Commands: h.nativeInstaller.Commands(origin, token)}, nil + return &v1.EnvironmentInstallation{Status: "available", Version: installer.Version, ExpiresAt: expires, Commands: installer.Catalog.Commands(origin, token)}, nil } func (h *Handler) addSessionInstallation(w http.ResponseWriter, r *http.Request, response *v1.Session) error { - if response.Environment.Type != "self_hosted" || h.nativeVersion == "" { + if response.Environment.Type != "self_hosted" || h.nativeInstaller() == nil { return nil } principal, ok := r.Context().Value(principalContextKey{}).(identity.Principal) @@ -56,34 +69,36 @@ func (h *Handler) addSessionInstallation(w http.ResponseWriter, r *http.Request, } func (h *Handler) registerNativeInstallationRoutes(r chi.Router) { - if h.nativeVersion == "" { + installer := h.nativeInstaller() + if installer == nil { return } - if h.nativeInstaller != nil { - r.Handle("/api/v1/agent-daemon/install/*", h.nativeInstaller) + if installer.Catalog != nil { + r.Handle("/api/v1/agent-daemon/install/*", installer.Catalog) } r.Post("/api/v1/agent-daemon/installation", h.prepareNativeInstallation) r.Post("/api/v1/agent-daemon/installation/claim", h.claimNativeInstallation) } -func (h *Handler) installationAuthorization(w http.ResponseWriter, r *http.Request) (environmentInstallationStore, store.InstallationAuthorization, string, bool) { +// installationAuthorization validates a grant route's bearer grant. The routes +// are registered only when this Core serves a native installer. +func (h *Handler) installationAuthorization(w http.ResponseWriter, r *http.Request) (store.InstallationAuthorization, string, bool) { w.Header().Set("Cache-Control", "no-store") - s, ok := h.store.(environmentInstallationStore) - if !ok || h.nativeInstaller == nil { + if h.Execution.NativeInstaller.Catalog == nil { writeError(w, 503, "installation_unavailable", "Matching native installation artifacts are unavailable.") - return nil, store.InstallationAuthorization{}, "", false + return store.InstallationAuthorization{}, "", false } parts := strings.Fields(r.Header.Get("Authorization")) if len(r.Header.Values("Authorization")) != 1 || len(parts) != 2 || parts[0] != "Bearer" { writeStoreError(w, r, store.ErrInstallationAuthorization) - return nil, store.InstallationAuthorization{}, "", false + return store.InstallationAuthorization{}, "", false } - claim, err := s.ValidateEnvironmentInstallation(r.Context(), parts[1], h.nativeVersion) + claim, err := h.Environments.ValidateEnvironmentInstallation(r.Context(), parts[1], h.Execution.NativeInstaller.Version) if err != nil { writeStoreError(w, r, err) - return nil, claim, "", false + return claim, "", false } - return s, claim, parts[1], true + return claim, parts[1], true } // @Summary Resolve a native installation authorization @@ -94,26 +109,26 @@ func (h *Handler) installationAuthorization(w http.ResponseWriter, r *http.Reque // @Failure 401,404,503 {object} CoreErrorResponse // @Router /api/v1/agent-daemon/installation [post] func (h *Handler) prepareNativeInstallation(w http.ResponseWriter, r *http.Request) { - _, claim, _, ok := h.installationAuthorization(w, r) + claim, _, ok := h.installationAuthorization(w, r) if !ok { return } - environment, err := h.store.GetEnvironment(r.Context(), claim.Principal.TenantID, claim.Environment) + environment, err := h.Environments.GetEnvironment(r.Context(), claim.Principal.TenantID, claim.Environment) if err != nil { writeStoreError(w, r, err) return } - session, err := h.store.GetSession(r.Context(), claim.Principal.TenantID, environment.SessionID) + session, err := h.Sessions.GetSession(r.Context(), claim.Principal.TenantID, environment.SessionID) if err != nil { writeStoreError(w, r, err) return } - response, err := sessionResponse(session, h.executorURL) + response, err := sessionResponse(session, h.executorURL()) if err != nil { writeStoreError(w, r, err) return } - writeJSON(w, http.StatusOK, v1.NativeInstallationContext{Version: h.nativeVersion, ProtocolVersion: proto.Version, EnvironmentID: claim.Environment, RemoteURL: h.executorURL, Workspace: response.Environment.WorkspaceDirectory, Harness: session.Engine}) + writeJSON(w, http.StatusOK, v1.NativeInstallationContext{Version: h.Execution.NativeInstaller.Version, ProtocolVersion: proto.Version, EnvironmentID: claim.Environment, RemoteURL: h.Execution.ExecutorURL, Workspace: response.Environment.WorkspaceDirectory, Harness: session.Engine}) } type NativeInstallationClaim struct { @@ -129,7 +144,7 @@ type NativeInstallationClaim struct { // @Failure 400,401,409,503 {object} CoreErrorResponse // @Router /api/v1/agent-daemon/installation/claim [post] func (h *Handler) claimNativeInstallation(w http.ResponseWriter, r *http.Request) { - s, _, token, ok := h.installationAuthorization(w, r) + _, token, ok := h.installationAuthorization(w, r) if !ok { return } @@ -142,7 +157,7 @@ func (h *Handler) claimNativeInstallation(w http.ResponseWriter, r *http.Request writeStoreError(w, r, store.ErrInvalidInput) return } - if err := s.ClaimEnvironmentInstallation(r.Context(), token, h.nativeVersion, input.ExecutorToken); err != nil { + if err := h.Environments.ClaimEnvironmentInstallation(r.Context(), token, h.Execution.NativeInstaller.Version, input.ExecutorToken); err != nil { writeStoreError(w, r, err) return } @@ -164,12 +179,7 @@ func (h *Handler) getEnvironmentInstallation(w http.ResponseWriter, r *http.Requ return } environment := chi.URLParam(r, "environment_id") - s, ok := h.store.(EnvironmentExecutorStore) - if !ok { - writeStoreError(w, r, store.ErrNotFound) - return - } - if _, err := s.ProjectExecutorCredentialState(r.Context(), binding.Principal, environment); err != nil { + if _, err := h.Environments.ProjectExecutorCredentialState(r.Context(), binding.Principal, environment); err != nil { writeStoreError(w, r, err) return } diff --git a/services/core/internal/api/environment_installation_test.go b/services/core/internal/api/environment_installation_test.go index 9196eb64c..122ecb31e 100644 --- a/services/core/internal/api/environment_installation_test.go +++ b/services/core/internal/api/environment_installation_test.go @@ -31,20 +31,17 @@ func (f *installationFixture) AuthorizeEnvironmentInstallation(_ context.Context func (f *installationFixture) ValidateEnvironmentInstallation(context.Context, string, string) (store.InstallationAuthorization, error) { return store.InstallationAuthorization{}, store.ErrInstallationAuthorization } -func (f *installationFixture) ClaimEnvironmentInstallation(context.Context, string, string, string) error { - return store.ErrInstallationAuthorization -} func TestSelfHostedCreationReturnsInstallationWithoutWebCredential(t *testing.T) { f := &installationFixture{} - auth, err := NewAuthenticator([]APIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential("project-key"), TenantID: uuid.NewString()}}) - if err != nil { - t.Fatal(err) - } - handler, err := NewHandler(f, auth, "codex", withFixtureDeploymentProvider(), WithExecution(&inputRecorder{}), WithEnvironmentRemoteURL("wss://core.example/api/v1/agent-daemon/ws"), WithNativeInstaller(&nativeinstaller.Catalog{Version: "build"}, "build")) - if err != nil { - t.Fatal(err) - } + 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.environments.authorizeEnvironmentInstallation, fakes.environments.validateEnvironmentInstallation = f.AuthorizeEnvironmentInstallation, f.ValidateEnvironmentInstallation + deps.Execution = fakes.execution() + deps.Execution.NativeInstaller = &NativeInstaller{Version: "build", Catalog: &nativeinstaller.Catalog{Version: "build"}} + handler := newTestHandler(t, deps) body := `{"agent":{"model":"model"},"environment":{"type":"self_hosted","workspace_directory":"/workspace"},"x_agents_core":{"model_provider":{"protocol":"responses","base_url":"https://model.example/v1","api_key":"fixture-model"}}}` r := httptest.NewRequest(http.MethodPost, "/v1/agents/sessions", strings.NewReader(body)) r.Header.Set("Authorization", "Bearer project-key") diff --git a/services/core/internal/api/environment_network_test.go b/services/core/internal/api/environment_network_test.go index 4c3da0ac9..71b0a7222 100644 --- a/services/core/internal/api/environment_network_test.go +++ b/services/core/internal/api/environment_network_test.go @@ -47,7 +47,7 @@ func TestRestrictedNetworkPublicMetadataPreservesInput(t *testing.T) { func TestTemplateNetworkOverridesOnlyNarrowAndRetainIntent(t *testing.T) { lookup := &templateLookupStore{network: "restricted", domains: []string{"Example.com", "api.example.com", "example.com"}} - h := Handler{store: lookup} + h := templateHandler(t, lookup.ResolveEnvironmentTemplate) for _, test := range []struct { name, override string want []string diff --git a/services/core/internal/api/environment_plugins_test.go b/services/core/internal/api/environment_plugins_test.go index 794bfb190..106b12c63 100644 --- a/services/core/internal/api/environment_plugins_test.go +++ b/services/core/internal/api/environment_plugins_test.go @@ -80,7 +80,7 @@ func TestPluginsSharedParsingConfidentialMetadataAndOverrides(t *testing.T) { t.Fatal("session response", err) } lookup := &templateLookupStore{network: "enabled", plugins: template.Initialization.Plugins, directories: template.Initialization.CapabilityDirectories} - h := Handler{store: lookup} + h := templateHandler(t, lookup.ResolveEnvironmentTemplate) for _, override := range []string{"", `,"plugins":null,"capability_directories":null`, `,"plugins":[],"capability_directories":[]`} { if err = json.Unmarshal([]byte(`{"agent":{"model":"test"},"environment":{"type":"openai_hosted","environment_template_id":"template"`+override+`}}`), &decoded); err != nil { t.Fatal(err) diff --git a/services/core/internal/api/environment_preparation_input_test.go b/services/core/internal/api/environment_preparation_input_test.go index 2c693bbd2..80f247dc4 100644 --- a/services/core/internal/api/environment_preparation_input_test.go +++ b/services/core/internal/api/environment_preparation_input_test.go @@ -82,7 +82,7 @@ func TestPreparationTemplateSharedAcrossPlacements(t *testing.T) { if err != nil { t.Fatal(err) } - h := Handler{store: compositionFixture()} + h := templateHandler(t, compositionFixture().ResolveEnvironmentTemplate) for _, input := range []*sessionRequest{&hosted, &own} { if err := h.resolveTemplateEnvironment(t.Context(), "tenant", input); err != nil { t.Fatal(err) diff --git a/services/core/internal/api/environment_skills_test.go b/services/core/internal/api/environment_skills_test.go index 7d5aba6d5..6561215c7 100644 --- a/services/core/internal/api/environment_skills_test.go +++ b/services/core/internal/api/environment_skills_test.go @@ -33,7 +33,7 @@ func skillInput(t *testing.T, body string) json.RawMessage { func TestSkillReferenceParsingInheritanceAndReplacement(t *testing.T) { lookup := &templateLookupStore{network: "enabled", skills: []store.EnvironmentSkill{{Metadata: store.EnvironmentSkillMetadata{Type: "skill_reference", SkillID: "skill-template", Version: "latest"}}}} - h := Handler{store: lookup} + h := templateHandler(t, lookup.ResolveEnvironmentTemplate) for _, fields := range []string{"", `,"skills":[]`, `,"skills":[{"type":"skill_reference","skill_id":"skill-override","version":"2"}]`} { var decoded decodedSessionRequest if err := json.Unmarshal([]byte(`{"agent":{"model":"test"},"environment":{"type":"openai_hosted","environment_template_id":"template"`+fields+`}}`), &decoded); err != nil { diff --git a/services/core/internal/api/environment_templates.go b/services/core/internal/api/environment_templates.go index 3dbfc3005..28198e213 100644 --- a/services/core/internal/api/environment_templates.go +++ b/services/core/internal/api/environment_templates.go @@ -11,7 +11,9 @@ import ( "github.com/go-chi/chi/v5" ) -type EnvironmentTemplateStore interface { +// EnvironmentTemplates manages Environment Templates. ResolveEnvironmentTemplate +// reads a Template with its initial files for Session creation. +type EnvironmentTemplates interface { ResolveEnvironmentTemplate(context.Context, string, string) (store.EnvironmentTemplate, []store.InitialFile, error) CreateEnvironmentTemplate(context.Context, string, store.EnvironmentTemplateInput) (store.EnvironmentTemplate, error) GetEnvironmentTemplate(context.Context, string, string) (store.EnvironmentTemplate, error) @@ -101,7 +103,7 @@ func (h *Handler) createEnvironmentTemplate(w http.ResponseWriter, r *http.Reque if !ok { return } - value, err := h.store.CreateEnvironmentTemplate(r.Context(), tenantID(r), in) + value, err := h.EnvironmentTemplates.CreateEnvironmentTemplate(r.Context(), tenantID(r), in) if err != nil { writeStoreError(w, r, err) return @@ -120,7 +122,7 @@ func (h *Handler) createEnvironmentTemplate(w http.ResponseWriter, r *http.Reque // @Failure 400,401,404,500 {object} v1.ErrorResponse // @Router /agents/environments/templates/{environment_template_id} [get] func (h *Handler) getEnvironmentTemplate(w http.ResponseWriter, r *http.Request) { - value, err := h.store.GetEnvironmentTemplate(r.Context(), tenantID(r), chi.URLParam(r, "environment_template_id")) + value, err := h.EnvironmentTemplates.GetEnvironmentTemplate(r.Context(), tenantID(r), chi.URLParam(r, "environment_template_id")) if err != nil { writeStoreError(w, r, err) return @@ -145,7 +147,7 @@ func (h *Handler) updateEnvironmentTemplate(w http.ResponseWriter, r *http.Reque if !ok { return } - value, err := h.store.UpdateEnvironmentTemplate(r.Context(), tenantID(r), chi.URLParam(r, "environment_template_id"), in) + value, err := h.EnvironmentTemplates.UpdateEnvironmentTemplate(r.Context(), tenantID(r), chi.URLParam(r, "environment_template_id"), in) if err != nil { writeStoreError(w, r, err) return @@ -164,7 +166,7 @@ func (h *Handler) updateEnvironmentTemplate(w http.ResponseWriter, r *http.Reque // @Failure 400,401,404,500 {object} v1.ErrorResponse // @Router /agents/environments/templates/{environment_template_id} [delete] func (h *Handler) deleteEnvironmentTemplate(w http.ResponseWriter, r *http.Request) { - id, err := h.store.DeleteEnvironmentTemplate(r.Context(), tenantID(r), chi.URLParam(r, "environment_template_id")) + id, err := h.EnvironmentTemplates.DeleteEnvironmentTemplate(r.Context(), tenantID(r), chi.URLParam(r, "environment_template_id")) if err != nil { writeStoreError(w, r, err) return @@ -189,7 +191,7 @@ func (h *Handler) listEnvironmentTemplates(w http.ResponseWriter, r *http.Reques if !ok { return } - page, err := h.store.ListEnvironmentTemplates(r.Context(), tenantID(r), options.after, options.limit, options.ascending) + page, err := h.EnvironmentTemplates.ListEnvironmentTemplates(r.Context(), tenantID(r), options.after, options.limit, options.ascending) if err != nil { writeStoreError(w, r, err) return diff --git a/services/core/internal/api/environment_templates_test.go b/services/core/internal/api/environment_templates_test.go index b905320fe..be1475d2f 100644 --- a/services/core/internal/api/environment_templates_test.go +++ b/services/core/internal/api/environment_templates_test.go @@ -35,7 +35,6 @@ func TestTemplateConfigurationRejectsUnqualifiedInputs(t *testing.T) { } type templateLookupStore struct { - ResourceStore network string domains []string tenant string @@ -49,9 +48,17 @@ func (s *templateLookupStore) ResolveEnvironmentTemplate(_ context.Context, tena return store.EnvironmentTemplate{ID: id, NetworkAccess: s.network, AllowedDomains: s.domains, Initialization: store.EnvironmentSetup{Skills: s.skills, Plugins: s.plugins, CapabilityDirectories: s.directories}}, nil, nil } +// templateHandler serves Environment template lookups from resolve. +func templateHandler(t *testing.T, resolve func(context.Context, string, string) (store.EnvironmentTemplate, []store.InitialFile, error)) Handler { + t.Helper() + deps, fakes := testDependencies(t) + fakes.environmentTemplates.resolveEnvironmentTemplate = resolve + return Handler{Dependencies: deps} +} + func TestTemplateResolutionAndCreationIntent(t *testing.T) { lookup := &templateLookupStore{network: "disabled"} - h := Handler{store: lookup} + h := templateHandler(t, lookup.ResolveEnvironmentTemplate) request := func(raw string) sessionRequest { t.Helper() var decoded decodedSessionRequest diff --git a/services/core/internal/api/environments.go b/services/core/internal/api/environments.go index 303cde3fd..e75649e57 100644 --- a/services/core/internal/api/environments.go +++ b/services/core/internal/api/environments.go @@ -1,15 +1,31 @@ package api import ( + "context" "encoding/json" "errors" "net/http" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/go-chi/chi/v5" ) +// Environments reads Environments, grants and claims native installations, and +// manages the executor credentials of a Project's self_hosted Environments. The +// Project's principal is an executor credential's execution principal; the +// Core key that authorizes the request is not. +type Environments interface { + GetEnvironment(context.Context, string, string) (store.Environment, error) + AuthorizeEnvironmentInstallation(context.Context, identity.Principal, string, string) (string, int64, error) + ValidateEnvironmentInstallation(context.Context, string, string) (store.InstallationAuthorization, error) + ClaimEnvironmentInstallation(context.Context, string, string, string) error + ProjectExecutorCredentialState(context.Context, identity.Principal, string) (store.ExecutorCredentialState, error) + IssueProjectExecutorCredential(context.Context, identity.Principal, string, string, bool) (store.IssuedExecutorCredential, error) + RevokeProjectExecutorCredential(context.Context, identity.Principal, string, string) error +} + // @Summary Retrieve an execution Environment // @Description Returns durable connection status and safe installed metadata for supported self_hosted and basic openai_hosted profiles. Initial files expose frozen safe metadata without content; Plugin/Skill entries expose only safe configured installation metadata. Capability-directory discoveries are not added to those arrays. Unsupported installation configurations remain implementation gaps. This read does not prepare execution, start compute or require an enabled execution worker. Session deletion removes the associated Environment from public reads; project-shared read authorization is unchanged. Connection status does not prove native readiness or process quiescence. // @Tags Environments @@ -21,7 +37,7 @@ import ( // @Failure 400,401,404,500 {object} v1.ErrorResponse // @Router /agents/environments/{environment_id} [get] func (h *Handler) getEnvironment(w http.ResponseWriter, r *http.Request) { - environment, err := h.store.GetEnvironment(r.Context(), tenantID(r), chi.URLParam(r, "environment_id")) + environment, err := h.Environments.GetEnvironment(r.Context(), tenantID(r), chi.URLParam(r, "environment_id")) if err != nil { writeStoreError(w, r, err) return diff --git a/services/core/internal/api/environments_test.go b/services/core/internal/api/environments_test.go index c8260b9d7..6e9083839 100644 --- a/services/core/internal/api/environments_test.go +++ b/services/core/internal/api/environments_test.go @@ -16,7 +16,6 @@ import ( ) type environmentResourceFixture struct { - ResourceStore environment store.Environment err error tenant, id string @@ -35,18 +34,14 @@ func environmentResourceHandler(t *testing.T) (http.Handler, *environmentResourc ID: uuid.NewString(), TenantID: uuid.NewString(), SessionID: uuid.NewString(), Status: "pending", Configuration: json.RawMessage(`{"type":"self_hosted","workspace_directory":"/private/workspace"}`), }} - auth, err := NewAuthenticator([]APIKey{{ + deps, fakes := testDependencies(t) + deps.Engine = "fake_alpha" + fakes.projects.resolveProjectAPIKey = projectKeys(t, APIKey{ OrganizationID: "resource-org", ProjectID: "resource-project", SubjectKind: "user", SubjectID: "resource-reader", TokenSHA256: runtimedevice.HashCredential("resource-key"), TenantID: f.environment.TenantID, - }}) - if err != nil { - t.Fatal(err) - } - h, err := NewHandler(f, auth, "fake_alpha") - if err != nil { - t.Fatal(err) - } - return h, f + }).ResolveProjectAPIKey + fakes.environments.getEnvironment = f.GetEnvironment + return newTestHandler(t, deps), f } func TestEnvironmentResourceExactProjectionWithoutExecution(t *testing.T) { diff --git a/services/core/internal/api/execution_policy.go b/services/core/internal/api/execution_policy.go deleted file mode 100644 index b7c5ec472..000000000 --- a/services/core/internal/api/execution_policy.go +++ /dev/null @@ -1,9 +0,0 @@ -package api - -import "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" - -// WithExecutionPolicy supplies the same immutable qualification used by the -// execution Dispatcher. Omission uses the built-in engine registrations. -func WithExecutionPolicy(policy execution.Policy) Option { - return func(h *Handler) { h.policy = policy } -} diff --git a/services/core/internal/api/fakes_test.go b/services/core/internal/api/fakes_test.go new file mode 100644 index 000000000..55e2bcaa7 --- /dev/null +++ b/services/core/internal/api/fakes_test.go @@ -0,0 +1,1109 @@ +package api + +import ( + "context" + "encoding/json" + "io" + "testing" + + v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/coremetrics" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" + "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" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" +) + +// Strict fakes: one per Dependencies area, with a func field per method. A +// test sets only the funcs it expects; calling any other method fails the test. + +func unexpectedCall(t testing.TB, method string) { + t.Helper() + t.Fatalf("unexpected call to %s", method) +} + +type fakeAdmin struct { + t testing.TB + readAdminSummary func(context.Context, string, store.AdminSummaryFilter, func(store.Session, *string) error) (store.AdminAssetCounts, error) + listAdminRuntimeTargets func(context.Context, []string, string, int, bool) (store.AdminRuntimeTargetPage, error) + listAdminAudit func(context.Context, store.AdminAuditFilter) (store.AdminAuditPage, error) +} + +func (f *fakeAdmin) ReadAdminSummary(a0 context.Context, a1 string, a2 store.AdminSummaryFilter, a3 func(store.Session, *string) error) (store.AdminAssetCounts, error) { + if f.readAdminSummary == nil { + unexpectedCall(f.t, "ReadAdminSummary") + } + return f.readAdminSummary(a0, a1, a2, a3) +} + +func (f *fakeAdmin) ListAdminRuntimeTargets(a0 context.Context, a1 []string, a2 string, a3 int, a4 bool) (store.AdminRuntimeTargetPage, error) { + if f.listAdminRuntimeTargets == nil { + unexpectedCall(f.t, "ListAdminRuntimeTargets") + } + return f.listAdminRuntimeTargets(a0, a1, a2, a3, a4) +} + +func (f *fakeAdmin) ListAdminAudit(a0 context.Context, a1 store.AdminAuditFilter) (store.AdminAuditPage, error) { + if f.listAdminAudit == nil { + unexpectedCall(f.t, "ListAdminAudit") + } + return f.listAdminAudit(a0, a1) +} + +type fakeAdmission struct { + t testing.TB + createSession func(context.Context, string, store.CreateSessionInput) (store.Session, error) + createSessionStream func(context.Context, string, store.CreateSessionInput) (store.SessionCreation, error) + submitInputs func(context.Context, string, string, string, []store.Input) ([]store.InputReceipt, error) +} + +func (f *fakeAdmission) CreateSession(a0 context.Context, a1 string, a2 store.CreateSessionInput) (store.Session, error) { + if f.createSession == nil { + unexpectedCall(f.t, "CreateSession") + } + return f.createSession(a0, a1, a2) +} + +func (f *fakeAdmission) CreateSessionStream(a0 context.Context, a1 string, a2 store.CreateSessionInput) (store.SessionCreation, error) { + if f.createSessionStream == nil { + unexpectedCall(f.t, "CreateSessionStream") + } + return f.createSessionStream(a0, a1, a2) +} + +func (f *fakeAdmission) SubmitInputs(a0 context.Context, a1 string, a2 string, a3 string, a4 []store.Input) ([]store.InputReceipt, error) { + if f.submitInputs == nil { + unexpectedCall(f.t, "SubmitInputs") + } + return f.submitInputs(a0, a1, a2, a3, a4) +} + +type fakeAgents struct { + t testing.TB + deleteAgent func(context.Context, string, string) (string, error) + updateAgent func(context.Context, string, string, store.UpdateAgentInput) (store.SavedAgent, error) + listAgents func(context.Context, string, string, int, bool) (store.AgentPage, error) + createAgent func(context.Context, string, store.CreateAgentInput) (store.SavedAgent, error) + getAgent func(context.Context, string, string) (store.SavedAgent, error) + getAgentForSession func(context.Context, string, string, bool) (store.SavedAgent, *v1.ModelProviderInput, error) +} + +func (f *fakeAgents) DeleteAgent(a0 context.Context, a1 string, a2 string) (string, error) { + if f.deleteAgent == nil { + unexpectedCall(f.t, "DeleteAgent") + } + return f.deleteAgent(a0, a1, a2) +} + +func (f *fakeAgents) UpdateAgent(a0 context.Context, a1 string, a2 string, a3 store.UpdateAgentInput) (store.SavedAgent, error) { + if f.updateAgent == nil { + unexpectedCall(f.t, "UpdateAgent") + } + return f.updateAgent(a0, a1, a2, a3) +} + +func (f *fakeAgents) ListAgents(a0 context.Context, a1 string, a2 string, a3 int, a4 bool) (store.AgentPage, error) { + if f.listAgents == nil { + unexpectedCall(f.t, "ListAgents") + } + return f.listAgents(a0, a1, a2, a3, a4) +} + +func (f *fakeAgents) CreateAgent(a0 context.Context, a1 string, a2 store.CreateAgentInput) (store.SavedAgent, error) { + if f.createAgent == nil { + unexpectedCall(f.t, "CreateAgent") + } + return f.createAgent(a0, a1, a2) +} + +func (f *fakeAgents) GetAgent(a0 context.Context, a1 string, a2 string) (store.SavedAgent, error) { + if f.getAgent == nil { + unexpectedCall(f.t, "GetAgent") + } + return f.getAgent(a0, a1, a2) +} + +func (f *fakeAgents) GetAgentForSession(a0 context.Context, a1 string, a2 string, a3 bool) (store.SavedAgent, *v1.ModelProviderInput, error) { + if f.getAgentForSession == nil { + unexpectedCall(f.t, "GetAgentForSession") + } + return f.getAgentForSession(a0, a1, a2, a3) +} + +type fakeArtifacts struct { + t testing.TB + getSessionArtifact func(context.Context, string, string, string) (store.SessionArtifact, error) + listSessionArtifacts func(context.Context, string, string, string, string, int, bool) (store.ArtifactPage, error) + readSessionArtifact func(context.Context, string, string, string, func(store.SessionArtifact, io.Reader) error) error + deleteSessionArtifact func(context.Context, string, string, string) error +} + +func (f *fakeArtifacts) GetSessionArtifact(a0 context.Context, a1 string, a2 string, a3 string) (store.SessionArtifact, error) { + if f.getSessionArtifact == nil { + unexpectedCall(f.t, "GetSessionArtifact") + } + return f.getSessionArtifact(a0, a1, a2, a3) +} + +func (f *fakeArtifacts) ListSessionArtifacts(a0 context.Context, a1 string, a2 string, a3 string, a4 string, a5 int, a6 bool) (store.ArtifactPage, error) { + if f.listSessionArtifacts == nil { + unexpectedCall(f.t, "ListSessionArtifacts") + } + return f.listSessionArtifacts(a0, a1, a2, a3, a4, a5, a6) +} + +func (f *fakeArtifacts) ReadSessionArtifact(a0 context.Context, a1 string, a2 string, a3 string, a4 func(store.SessionArtifact, io.Reader) error) error { + if f.readSessionArtifact == nil { + unexpectedCall(f.t, "ReadSessionArtifact") + } + return f.readSessionArtifact(a0, a1, a2, a3, a4) +} + +func (f *fakeArtifacts) DeleteSessionArtifact(a0 context.Context, a1 string, a2 string, a3 string) error { + if f.deleteSessionArtifact == nil { + unexpectedCall(f.t, "DeleteSessionArtifact") + } + return f.deleteSessionArtifact(a0, a1, a2, a3) +} + +type fakeConfigurationDiscovery struct { + t testing.TB + discoverConfiguration func(ctx context.Context, provider string, input sandbox.ConfigurationDiscoveryInput) (json.RawMessage, error) +} + +func (f *fakeConfigurationDiscovery) DiscoverConfiguration(a0 context.Context, a1 string, a2 sandbox.ConfigurationDiscoveryInput) (json.RawMessage, error) { + if f.discoverConfiguration == nil { + unexpectedCall(f.t, "DiscoverConfiguration") + } + return f.discoverConfiguration(a0, a1, a2) +} + +type fakeDeployment struct { + t testing.TB + getRuntimeDeployment func(context.Context) (store.RuntimeDeploymentView, error) + listRuntimeNodes func(context.Context) ([]store.RuntimeNode, error) + getRuntimeNodeDetail func(context.Context, string, string) (store.RuntimeNodeDetail, error) + updateRuntimeNode func(context.Context, string, store.RuntimeNodeUpdate) error + removeRuntimeNode func(context.Context, string) error + listNodeRuntimeAllocations func(context.Context, string) ([]store.RuntimeNodeAllocation, error) + createRuntimeEnrollment func(context.Context, store.RuntimeNodeCapacity) (store.RuntimeNodeEnrollmentToken, error) + enrollRuntimeNode func(context.Context, string, store.RuntimeNodeEnrollment) (store.RuntimeNodeIdentity, error) + runtimeNodeGenerationConfiguration func(context.Context, string, string, uint64) (store.RuntimeNodeConfiguration, error) + runtimeNodeStatus func(context.Context, string, string) (store.RuntimeNodeStatus, error) +} + +func (f *fakeDeployment) GetRuntimeDeployment(a0 context.Context) (store.RuntimeDeploymentView, error) { + if f.getRuntimeDeployment == nil { + unexpectedCall(f.t, "GetRuntimeDeployment") + } + return f.getRuntimeDeployment(a0) +} + +func (f *fakeDeployment) ListRuntimeNodes(a0 context.Context) ([]store.RuntimeNode, error) { + if f.listRuntimeNodes == nil { + unexpectedCall(f.t, "ListRuntimeNodes") + } + return f.listRuntimeNodes(a0) +} + +func (f *fakeDeployment) GetRuntimeNodeDetail(a0 context.Context, a1 string, a2 string) (store.RuntimeNodeDetail, error) { + if f.getRuntimeNodeDetail == nil { + unexpectedCall(f.t, "GetRuntimeNodeDetail") + } + return f.getRuntimeNodeDetail(a0, a1, a2) +} + +func (f *fakeDeployment) UpdateRuntimeNode(a0 context.Context, a1 string, a2 store.RuntimeNodeUpdate) error { + if f.updateRuntimeNode == nil { + unexpectedCall(f.t, "UpdateRuntimeNode") + } + return f.updateRuntimeNode(a0, a1, a2) +} + +func (f *fakeDeployment) RemoveRuntimeNode(a0 context.Context, a1 string) error { + if f.removeRuntimeNode == nil { + unexpectedCall(f.t, "RemoveRuntimeNode") + } + return f.removeRuntimeNode(a0, a1) +} + +func (f *fakeDeployment) ListNodeRuntimeAllocations(a0 context.Context, a1 string) ([]store.RuntimeNodeAllocation, error) { + if f.listNodeRuntimeAllocations == nil { + unexpectedCall(f.t, "ListNodeRuntimeAllocations") + } + return f.listNodeRuntimeAllocations(a0, a1) +} + +func (f *fakeDeployment) CreateRuntimeEnrollment(a0 context.Context, a1 store.RuntimeNodeCapacity) (store.RuntimeNodeEnrollmentToken, error) { + if f.createRuntimeEnrollment == nil { + unexpectedCall(f.t, "CreateRuntimeEnrollment") + } + return f.createRuntimeEnrollment(a0, a1) +} + +func (f *fakeDeployment) EnrollRuntimeNode(a0 context.Context, a1 string, a2 store.RuntimeNodeEnrollment) (store.RuntimeNodeIdentity, error) { + if f.enrollRuntimeNode == nil { + unexpectedCall(f.t, "EnrollRuntimeNode") + } + return f.enrollRuntimeNode(a0, a1, a2) +} + +func (f *fakeDeployment) RuntimeNodeGenerationConfiguration(a0 context.Context, a1 string, a2 string, a3 uint64) (store.RuntimeNodeConfiguration, error) { + if f.runtimeNodeGenerationConfiguration == nil { + unexpectedCall(f.t, "RuntimeNodeGenerationConfiguration") + } + return f.runtimeNodeGenerationConfiguration(a0, a1, a2, a3) +} + +func (f *fakeDeployment) RuntimeNodeStatus(a0 context.Context, a1 string, a2 string) (store.RuntimeNodeStatus, error) { + if f.runtimeNodeStatus == nil { + unexpectedCall(f.t, "RuntimeNodeStatus") + } + return f.runtimeNodeStatus(a0, a1, a2) +} + +type fakeDeploymentChanges struct { + t testing.TB + initializeSandboxDeployment func(context.Context, store.SandboxDeploymentSetupRequest) (store.RuntimeDeploymentView, error) + updateSandboxDeployment func(context.Context, store.SandboxDeploymentUpdateRequest) (store.RuntimeDeploymentView, error) + startSandboxReset func(context.Context, store.SandboxResetRequest) (store.RuntimeDeploymentView, error) + cancelSandboxReset func(context.Context, uint64) (store.RuntimeDeploymentView, error) +} + +func (f *fakeDeploymentChanges) InitializeSandboxDeployment(a0 context.Context, a1 store.SandboxDeploymentSetupRequest) (store.RuntimeDeploymentView, error) { + if f.initializeSandboxDeployment == nil { + unexpectedCall(f.t, "InitializeSandboxDeployment") + } + return f.initializeSandboxDeployment(a0, a1) +} + +func (f *fakeDeploymentChanges) UpdateSandboxDeployment(a0 context.Context, a1 store.SandboxDeploymentUpdateRequest) (store.RuntimeDeploymentView, error) { + if f.updateSandboxDeployment == nil { + unexpectedCall(f.t, "UpdateSandboxDeployment") + } + return f.updateSandboxDeployment(a0, a1) +} + +func (f *fakeDeploymentChanges) StartSandboxReset(a0 context.Context, a1 store.SandboxResetRequest) (store.RuntimeDeploymentView, error) { + if f.startSandboxReset == nil { + unexpectedCall(f.t, "StartSandboxReset") + } + return f.startSandboxReset(a0, a1) +} + +func (f *fakeDeploymentChanges) CancelSandboxReset(a0 context.Context, a1 uint64) (store.RuntimeDeploymentView, error) { + if f.cancelSandboxReset == nil { + unexpectedCall(f.t, "CancelSandboxReset") + } + return f.cancelSandboxReset(a0, a1) +} + +type fakeEnvironmentTemplates struct { + t testing.TB + resolveEnvironmentTemplate func(context.Context, string, string) (store.EnvironmentTemplate, []store.InitialFile, error) + createEnvironmentTemplate func(context.Context, string, store.EnvironmentTemplateInput) (store.EnvironmentTemplate, error) + getEnvironmentTemplate func(context.Context, string, string) (store.EnvironmentTemplate, error) + updateEnvironmentTemplate func(context.Context, string, string, store.EnvironmentTemplateInput) (store.EnvironmentTemplate, error) + deleteEnvironmentTemplate func(context.Context, string, string) (string, error) + listEnvironmentTemplates func(context.Context, string, string, int, bool) (store.EnvironmentTemplatePage, error) +} + +func (f *fakeEnvironmentTemplates) ResolveEnvironmentTemplate(a0 context.Context, a1 string, a2 string) (store.EnvironmentTemplate, []store.InitialFile, error) { + if f.resolveEnvironmentTemplate == nil { + unexpectedCall(f.t, "ResolveEnvironmentTemplate") + } + return f.resolveEnvironmentTemplate(a0, a1, a2) +} + +func (f *fakeEnvironmentTemplates) CreateEnvironmentTemplate(a0 context.Context, a1 string, a2 store.EnvironmentTemplateInput) (store.EnvironmentTemplate, error) { + if f.createEnvironmentTemplate == nil { + unexpectedCall(f.t, "CreateEnvironmentTemplate") + } + return f.createEnvironmentTemplate(a0, a1, a2) +} + +func (f *fakeEnvironmentTemplates) GetEnvironmentTemplate(a0 context.Context, a1 string, a2 string) (store.EnvironmentTemplate, error) { + if f.getEnvironmentTemplate == nil { + unexpectedCall(f.t, "GetEnvironmentTemplate") + } + return f.getEnvironmentTemplate(a0, a1, a2) +} + +func (f *fakeEnvironmentTemplates) UpdateEnvironmentTemplate(a0 context.Context, a1 string, a2 string, a3 store.EnvironmentTemplateInput) (store.EnvironmentTemplate, error) { + if f.updateEnvironmentTemplate == nil { + unexpectedCall(f.t, "UpdateEnvironmentTemplate") + } + return f.updateEnvironmentTemplate(a0, a1, a2, a3) +} + +func (f *fakeEnvironmentTemplates) DeleteEnvironmentTemplate(a0 context.Context, a1 string, a2 string) (string, error) { + if f.deleteEnvironmentTemplate == nil { + unexpectedCall(f.t, "DeleteEnvironmentTemplate") + } + return f.deleteEnvironmentTemplate(a0, a1, a2) +} + +func (f *fakeEnvironmentTemplates) ListEnvironmentTemplates(a0 context.Context, a1 string, a2 string, a3 int, a4 bool) (store.EnvironmentTemplatePage, error) { + if f.listEnvironmentTemplates == nil { + unexpectedCall(f.t, "ListEnvironmentTemplates") + } + return f.listEnvironmentTemplates(a0, a1, a2, a3, a4) +} + +type fakeEnvironmentWorkspaces struct { + t testing.TB + readEnvironmentDirectory func(context.Context, store.Environment, string) (proto.WorkspaceDirectoryResult, error) + writeEnvironmentFile func(context.Context, store.Environment, string, []byte) (int64, error) +} + +func (f *fakeEnvironmentWorkspaces) ReadEnvironmentDirectory(a0 context.Context, a1 store.Environment, a2 string) (proto.WorkspaceDirectoryResult, error) { + if f.readEnvironmentDirectory == nil { + unexpectedCall(f.t, "ReadEnvironmentDirectory") + } + return f.readEnvironmentDirectory(a0, a1, a2) +} + +func (f *fakeEnvironmentWorkspaces) WriteEnvironmentFile(a0 context.Context, a1 store.Environment, a2 string, a3 []byte) (int64, error) { + if f.writeEnvironmentFile == nil { + unexpectedCall(f.t, "WriteEnvironmentFile") + } + return f.writeEnvironmentFile(a0, a1, a2, a3) +} + +type fakeEnvironments struct { + t testing.TB + getEnvironment func(context.Context, string, string) (store.Environment, error) + authorizeEnvironmentInstallation func(context.Context, identity.Principal, string, string) (string, int64, error) + validateEnvironmentInstallation func(context.Context, string, string) (store.InstallationAuthorization, error) + claimEnvironmentInstallation func(context.Context, string, string, string) error + projectExecutorCredentialState func(context.Context, identity.Principal, string) (store.ExecutorCredentialState, error) + issueProjectExecutorCredential func(context.Context, identity.Principal, string, string, bool) (store.IssuedExecutorCredential, error) + revokeProjectExecutorCredential func(context.Context, identity.Principal, string, string) error +} + +func (f *fakeEnvironments) GetEnvironment(a0 context.Context, a1 string, a2 string) (store.Environment, error) { + if f.getEnvironment == nil { + unexpectedCall(f.t, "GetEnvironment") + } + return f.getEnvironment(a0, a1, a2) +} + +func (f *fakeEnvironments) AuthorizeEnvironmentInstallation(a0 context.Context, a1 identity.Principal, a2 string, a3 string) (string, int64, error) { + if f.authorizeEnvironmentInstallation == nil { + unexpectedCall(f.t, "AuthorizeEnvironmentInstallation") + } + return f.authorizeEnvironmentInstallation(a0, a1, a2, a3) +} + +func (f *fakeEnvironments) ValidateEnvironmentInstallation(a0 context.Context, a1 string, a2 string) (store.InstallationAuthorization, error) { + if f.validateEnvironmentInstallation == nil { + unexpectedCall(f.t, "ValidateEnvironmentInstallation") + } + return f.validateEnvironmentInstallation(a0, a1, a2) +} + +func (f *fakeEnvironments) ClaimEnvironmentInstallation(a0 context.Context, a1 string, a2 string, a3 string) error { + if f.claimEnvironmentInstallation == nil { + unexpectedCall(f.t, "ClaimEnvironmentInstallation") + } + return f.claimEnvironmentInstallation(a0, a1, a2, a3) +} + +func (f *fakeEnvironments) ProjectExecutorCredentialState(a0 context.Context, a1 identity.Principal, a2 string) (store.ExecutorCredentialState, error) { + if f.projectExecutorCredentialState == nil { + unexpectedCall(f.t, "ProjectExecutorCredentialState") + } + return f.projectExecutorCredentialState(a0, a1, a2) +} + +func (f *fakeEnvironments) IssueProjectExecutorCredential(a0 context.Context, a1 identity.Principal, a2 string, a3 string, a4 bool) (store.IssuedExecutorCredential, error) { + if f.issueProjectExecutorCredential == nil { + unexpectedCall(f.t, "IssueProjectExecutorCredential") + } + return f.issueProjectExecutorCredential(a0, a1, a2, a3, a4) +} + +func (f *fakeEnvironments) RevokeProjectExecutorCredential(a0 context.Context, a1 identity.Principal, a2 string, a3 string) error { + if f.revokeProjectExecutorCredential == nil { + unexpectedCall(f.t, "RevokeProjectExecutorCredential") + } + return f.revokeProjectExecutorCredential(a0, a1, a2, a3) +} + +type fakeExecutorConnections struct { + t testing.TB + executorConnected func(ctx context.Context, environmentID, credentialDigest string) (bool, error) +} + +func (f *fakeExecutorConnections) ExecutorConnected(a0 context.Context, a1 string, a2 string) (bool, error) { + if f.executorConnected == nil { + unexpectedCall(f.t, "ExecutorConnected") + } + return f.executorConnected(a0, a1, a2) +} + +type fakeFiles struct { + t testing.TB + createSourceFile func(context.Context, string, func(io.Writer) (store.SourceFileUpload, error)) (store.SourceFile, error) + getSourceFile func(context.Context, string, string) (store.SourceFile, error) + listSourceFiles func(context.Context, string, string, int, bool, *string) (store.SourceFilePage, error) + readSourceFile func(context.Context, string, string, func(store.SourceFile, io.Reader) error) error + deleteSourceFile func(context.Context, string, string) error +} + +func (f *fakeFiles) CreateSourceFile(a0 context.Context, a1 string, a2 func(io.Writer) (store.SourceFileUpload, error)) (store.SourceFile, error) { + if f.createSourceFile == nil { + unexpectedCall(f.t, "CreateSourceFile") + } + return f.createSourceFile(a0, a1, a2) +} + +func (f *fakeFiles) GetSourceFile(a0 context.Context, a1 string, a2 string) (store.SourceFile, error) { + if f.getSourceFile == nil { + unexpectedCall(f.t, "GetSourceFile") + } + return f.getSourceFile(a0, a1, a2) +} + +func (f *fakeFiles) ListSourceFiles(a0 context.Context, a1 string, a2 string, a3 int, a4 bool, a5 *string) (store.SourceFilePage, error) { + if f.listSourceFiles == nil { + unexpectedCall(f.t, "ListSourceFiles") + } + return f.listSourceFiles(a0, a1, a2, a3, a4, a5) +} + +func (f *fakeFiles) ReadSourceFile(a0 context.Context, a1 string, a2 string, a3 func(store.SourceFile, io.Reader) error) error { + if f.readSourceFile == nil { + unexpectedCall(f.t, "ReadSourceFile") + } + return f.readSourceFile(a0, a1, a2, a3) +} + +func (f *fakeFiles) DeleteSourceFile(a0 context.Context, a1 string, a2 string) error { + if f.deleteSourceFile == nil { + unexpectedCall(f.t, "DeleteSourceFile") + } + return f.deleteSourceFile(a0, a1, a2) +} + +type fakeInstallationBindings struct { + t testing.TB + addressBindings func(context.Context) (store.AddressBindings, error) +} + +func (f *fakeInstallationBindings) AddressBindings(a0 context.Context) (store.AddressBindings, error) { + if f.addressBindings == nil { + unexpectedCall(f.t, "AddressBindings") + } + return f.addressBindings(a0) +} + +type fakeMetrics struct { + t testing.TB + read func(context.Context, string) (coremetrics.View, error) + recordUnavailable func() +} + +func (f *fakeMetrics) Read(a0 context.Context, a1 string) (coremetrics.View, error) { + if f.read == nil { + unexpectedCall(f.t, "Read") + } + return f.read(a0, a1) +} + +func (f *fakeMetrics) RecordUnavailable() { + if f.recordUnavailable == nil { + unexpectedCall(f.t, "RecordUnavailable") + } + f.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) +} + +func (f *fakeModelProviders) ListDeploymentModelProviders(a0 context.Context) ([]store.DeploymentModelProvider, error) { + if f.listDeploymentModelProviders == nil { + unexpectedCall(f.t, "ListDeploymentModelProviders") + } + return f.listDeploymentModelProviders(a0) +} + +func (f *fakeModelProviders) SetDeploymentModelProvider(a0 context.Context, a1 string, a2 v1.ModelConfigurationInput) (store.DeploymentModelProvider, error) { + if f.setDeploymentModelProvider == nil { + unexpectedCall(f.t, "SetDeploymentModelProvider") + } + return f.setDeploymentModelProvider(a0, a1, a2) +} + +func (f *fakeModelProviders) DeleteDeploymentModelProvider(a0 context.Context, a1 string) error { + if f.deleteDeploymentModelProvider == nil { + unexpectedCall(f.t, "DeleteDeploymentModelProvider") + } + return f.deleteDeploymentModelProvider(a0, a1) +} + +func (f *fakeModelProviders) DeploymentModelProvider(a0 context.Context, a1 string) (*store.DeploymentModelProviderSnapshot, error) { + if f.deploymentModelProvider == nil { + unexpectedCall(f.t, "DeploymentModelProvider") + } + return f.deploymentModelProvider(a0, a1) +} + +type fakeProjects struct { + t testing.TB + createProject func(context.Context, string, string) (store.Project, error) + getProject func(context.Context, string) (store.ProjectBinding, error) + listProjects func(context.Context, string, int, bool) (store.ProjectPage, error) + renameProject func(context.Context, string, string) (store.Project, error) + archiveProject func(context.Context, string) (store.Project, error) + createProjectAPIKey func(context.Context, string, string, string) (store.IssuedProjectAPIKey, error) + listProjectAPIKeys func(context.Context, string, string, int, bool) (store.ProjectAPIKeyPage, error) + revokeProjectAPIKey func(context.Context, string, string) error + resolveProjectAPIKey func(context.Context, string) (store.ProjectAPIKeyBinding, error) +} + +func (f *fakeProjects) CreateProject(a0 context.Context, a1 string, a2 string) (store.Project, error) { + if f.createProject == nil { + unexpectedCall(f.t, "CreateProject") + } + return f.createProject(a0, a1, a2) +} + +func (f *fakeProjects) GetProject(a0 context.Context, a1 string) (store.ProjectBinding, error) { + if f.getProject == nil { + unexpectedCall(f.t, "GetProject") + } + return f.getProject(a0, a1) +} + +func (f *fakeProjects) ListProjects(a0 context.Context, a1 string, a2 int, a3 bool) (store.ProjectPage, error) { + if f.listProjects == nil { + unexpectedCall(f.t, "ListProjects") + } + return f.listProjects(a0, a1, a2, a3) +} + +func (f *fakeProjects) RenameProject(a0 context.Context, a1 string, a2 string) (store.Project, error) { + if f.renameProject == nil { + unexpectedCall(f.t, "RenameProject") + } + return f.renameProject(a0, a1, a2) +} + +func (f *fakeProjects) ArchiveProject(a0 context.Context, a1 string) (store.Project, error) { + if f.archiveProject == nil { + unexpectedCall(f.t, "ArchiveProject") + } + return f.archiveProject(a0, a1) +} + +func (f *fakeProjects) CreateProjectAPIKey(a0 context.Context, a1 string, a2 string, a3 string) (store.IssuedProjectAPIKey, error) { + if f.createProjectAPIKey == nil { + unexpectedCall(f.t, "CreateProjectAPIKey") + } + return f.createProjectAPIKey(a0, a1, a2, a3) +} + +func (f *fakeProjects) ListProjectAPIKeys(a0 context.Context, a1 string, a2 string, a3 int, a4 bool) (store.ProjectAPIKeyPage, error) { + if f.listProjectAPIKeys == nil { + unexpectedCall(f.t, "ListProjectAPIKeys") + } + return f.listProjectAPIKeys(a0, a1, a2, a3, a4) +} + +func (f *fakeProjects) RevokeProjectAPIKey(a0 context.Context, a1 string, a2 string) error { + if f.revokeProjectAPIKey == nil { + unexpectedCall(f.t, "RevokeProjectAPIKey") + } + return f.revokeProjectAPIKey(a0, a1, a2) +} + +func (f *fakeProjects) ResolveProjectAPIKey(a0 context.Context, a1 string) (store.ProjectAPIKeyBinding, error) { + if f.resolveProjectAPIKey == nil { + unexpectedCall(f.t, "ResolveProjectAPIKey") + } + return f.resolveProjectAPIKey(a0, a1) +} + +type fakeRuntimeHistory struct { + t testing.TB + capabilities func() runtimehistory.Capabilities + querySession func(context.Context, string, string, runtimehistory.Range) (runtimehistory.Response, error) +} + +func (f *fakeRuntimeHistory) Capabilities() runtimehistory.Capabilities { + if f.capabilities == nil { + unexpectedCall(f.t, "Capabilities") + } + return f.capabilities() +} + +func (f *fakeRuntimeHistory) QuerySession(a0 context.Context, a1 string, a2 string, a3 runtimehistory.Range) (runtimehistory.Response, error) { + if f.querySession == nil { + unexpectedCall(f.t, "QuerySession") + } + return f.querySession(a0, a1, a2, a3) +} + +type fakeRuntimeObservations struct { + t testing.TB + observeSession func(context.Context, string, string) (runtimeobs.Observation, error) + observeSessions func(context.Context, []runtimeobs.SessionIdentity, runtimeobs.PageOptions) ([]runtimeobs.Observation, []error) +} + +func (f *fakeRuntimeObservations) ObserveSession(a0 context.Context, a1 string, a2 string) (runtimeobs.Observation, error) { + if f.observeSession == nil { + unexpectedCall(f.t, "ObserveSession") + } + return f.observeSession(a0, a1, a2) +} + +func (f *fakeRuntimeObservations) ObserveSessions(a0 context.Context, a1 []runtimeobs.SessionIdentity, a2 runtimeobs.PageOptions) ([]runtimeobs.Observation, []error) { + if f.observeSessions == nil { + unexpectedCall(f.t, "ObserveSessions") + } + return f.observeSessions(a0, a1, a2) +} + +type fakeSessionAdmin struct { + t testing.TB + getSessionDiagnosticsSnapshot func(context.Context, string, string) (store.Session, error) + getTurnDiagnosticsSnapshot func(context.Context, string, string, string) (store.TurnDiagnosticsSnapshot, error) + getSessionExecutionConfiguration func(context.Context, string, string) (v1.SessionExecutionConfiguration, error) + getManagedSessionArchive func(context.Context, string, string) (store.ManagedSessionArchive, error) +} + +func (f *fakeSessionAdmin) GetSessionDiagnosticsSnapshot(a0 context.Context, a1 string, a2 string) (store.Session, error) { + if f.getSessionDiagnosticsSnapshot == nil { + unexpectedCall(f.t, "GetSessionDiagnosticsSnapshot") + } + return f.getSessionDiagnosticsSnapshot(a0, a1, a2) +} + +func (f *fakeSessionAdmin) GetTurnDiagnosticsSnapshot(a0 context.Context, a1 string, a2 string, a3 string) (store.TurnDiagnosticsSnapshot, error) { + if f.getTurnDiagnosticsSnapshot == nil { + unexpectedCall(f.t, "GetTurnDiagnosticsSnapshot") + } + return f.getTurnDiagnosticsSnapshot(a0, a1, a2, a3) +} + +func (f *fakeSessionAdmin) GetSessionExecutionConfiguration(a0 context.Context, a1 string, a2 string) (v1.SessionExecutionConfiguration, error) { + if f.getSessionExecutionConfiguration == nil { + unexpectedCall(f.t, "GetSessionExecutionConfiguration") + } + return f.getSessionExecutionConfiguration(a0, a1, a2) +} + +func (f *fakeSessionAdmin) GetManagedSessionArchive(a0 context.Context, a1 string, a2 string) (store.ManagedSessionArchive, error) { + if f.getManagedSessionArchive == nil { + unexpectedCall(f.t, "GetManagedSessionArchive") + } + return f.getManagedSessionArchive(a0, a1, a2) +} + +type fakeSessionArchive struct { + t testing.TB + archiveManagedSession func(context.Context, string, string, uint64) (store.ManagedSessionArchive, error) +} + +func (f *fakeSessionArchive) ArchiveManagedSession(a0 context.Context, a1 string, a2 string, a3 uint64) (store.ManagedSessionArchive, error) { + if f.archiveManagedSession == nil { + unexpectedCall(f.t, "ArchiveManagedSession") + } + return f.archiveManagedSession(a0, a1, a2, a3) +} + +type fakeSessionEvents struct { + t testing.TB + sessionEventCursor func(context.Context, string, string) (int64, error) + listSessionEvents func(context.Context, string, string, int64) ([]store.SessionChange, error) + sessionStreamSnapshot func(context.Context, string, string) (store.Session, int64, error) +} + +func (f *fakeSessionEvents) SessionEventCursor(a0 context.Context, a1 string, a2 string) (int64, error) { + if f.sessionEventCursor == nil { + unexpectedCall(f.t, "SessionEventCursor") + } + return f.sessionEventCursor(a0, a1, a2) +} + +func (f *fakeSessionEvents) ListSessionEvents(a0 context.Context, a1 string, a2 string, a3 int64) ([]store.SessionChange, error) { + if f.listSessionEvents == nil { + unexpectedCall(f.t, "ListSessionEvents") + } + return f.listSessionEvents(a0, a1, a2, a3) +} + +func (f *fakeSessionEvents) SessionStreamSnapshot(a0 context.Context, a1 string, a2 string) (store.Session, int64, error) { + if f.sessionStreamSnapshot == nil { + unexpectedCall(f.t, "SessionStreamSnapshot") + } + return f.sessionStreamSnapshot(a0, a1, a2) +} + +type fakeSessionHistory struct { + t testing.TB + getTurn func(context.Context, string, string, string) (store.Turn, error) + listTurns func(context.Context, string, string, string, int, bool) (store.TurnPage, error) + listItems func(context.Context, string, string, string, int, bool) (store.ItemPage, error) +} + +func (f *fakeSessionHistory) GetTurn(a0 context.Context, a1 string, a2 string, a3 string) (store.Turn, error) { + if f.getTurn == nil { + unexpectedCall(f.t, "GetTurn") + } + return f.getTurn(a0, a1, a2, a3) +} + +func (f *fakeSessionHistory) ListTurns(a0 context.Context, a1 string, a2 string, a3 string, a4 int, a5 bool) (store.TurnPage, error) { + if f.listTurns == nil { + unexpectedCall(f.t, "ListTurns") + } + return f.listTurns(a0, a1, a2, a3, a4, a5) +} + +func (f *fakeSessionHistory) ListItems(a0 context.Context, a1 string, a2 string, a3 string, a4 int, a5 bool) (store.ItemPage, error) { + if f.listItems == nil { + unexpectedCall(f.t, "ListItems") + } + return f.listItems(a0, a1, a2, a3, a4, a5) +} + +type fakeSessions struct { + t testing.TB + createSession func(context.Context, string, store.CreateSessionInput) (store.Session, error) + createSessionStream func(context.Context, string, store.CreateSessionInput) (store.SessionCreation, error) + findSessionCreation func(context.Context, string, string, json.RawMessage, identity.Subject) (store.SessionCreation, error) + getSession func(context.Context, string, string) (store.Session, error) + listSessions func(context.Context, string, string, int, bool, *string) (store.SessionPage, error) + updateSessionMetadata func(context.Context, string, string, map[string]string) (store.Session, error) + deleteSession func(context.Context, string, string) error + auditSessionOperation func(context.Context, string, string, string) error +} + +func (f *fakeSessions) CreateSession(a0 context.Context, a1 string, a2 store.CreateSessionInput) (store.Session, error) { + if f.createSession == nil { + unexpectedCall(f.t, "CreateSession") + } + return f.createSession(a0, a1, a2) +} + +func (f *fakeSessions) CreateSessionStream(a0 context.Context, a1 string, a2 store.CreateSessionInput) (store.SessionCreation, error) { + if f.createSessionStream == nil { + unexpectedCall(f.t, "CreateSessionStream") + } + return f.createSessionStream(a0, a1, a2) +} + +func (f *fakeSessions) FindSessionCreation(a0 context.Context, a1 string, a2 string, a3 json.RawMessage, a4 identity.Subject) (store.SessionCreation, error) { + if f.findSessionCreation == nil { + unexpectedCall(f.t, "FindSessionCreation") + } + return f.findSessionCreation(a0, a1, a2, a3, a4) +} + +func (f *fakeSessions) GetSession(a0 context.Context, a1 string, a2 string) (store.Session, error) { + if f.getSession == nil { + unexpectedCall(f.t, "GetSession") + } + return f.getSession(a0, a1, a2) +} + +func (f *fakeSessions) ListSessions(a0 context.Context, a1 string, a2 string, a3 int, a4 bool, a5 *string) (store.SessionPage, error) { + if f.listSessions == nil { + unexpectedCall(f.t, "ListSessions") + } + return f.listSessions(a0, a1, a2, a3, a4, a5) +} + +func (f *fakeSessions) UpdateSessionMetadata(a0 context.Context, a1 string, a2 string, a3 map[string]string) (store.Session, error) { + if f.updateSessionMetadata == nil { + unexpectedCall(f.t, "UpdateSessionMetadata") + } + return f.updateSessionMetadata(a0, a1, a2, a3) +} + +func (f *fakeSessions) DeleteSession(a0 context.Context, a1 string, a2 string) error { + if f.deleteSession == nil { + unexpectedCall(f.t, "DeleteSession") + } + return f.deleteSession(a0, a1, a2) +} + +func (f *fakeSessions) AuditSessionOperation(a0 context.Context, a1 string, a2 string, a3 string) error { + if f.auditSessionOperation == nil { + unexpectedCall(f.t, "AuditSessionOperation") + } + return f.auditSessionOperation(a0, a1, a2, a3) +} + +type fakeSkills struct { + t testing.TB + createSkill func(context.Context, string, []byte) (store.Skill, error) + getSkill func(context.Context, string, string) (store.Skill, error) + updateSkillDefault func(context.Context, string, string, string) (store.Skill, error) + deleteSkill func(context.Context, string, string) error + listSkills func(context.Context, string, string, int, bool) (store.SkillPage, error) + createSkillVersion func(context.Context, string, string, []byte, bool) (store.SkillVersion, error) + getSkillVersion func(context.Context, string, string, string) (store.SkillVersion, error) + readSkillVersion func(context.Context, string, string, string) (store.SkillVersion, []byte, error) + readDefaultSkillVersion func(context.Context, string, string) (store.SkillVersion, []byte, error) + deleteSkillVersion func(context.Context, string, string, string) (store.SkillVersion, error) + listSkillVersions func(context.Context, string, string, string, int, bool) (store.SkillVersionPage, error) +} + +func (f *fakeSkills) CreateSkill(a0 context.Context, a1 string, a2 []byte) (store.Skill, error) { + if f.createSkill == nil { + unexpectedCall(f.t, "CreateSkill") + } + return f.createSkill(a0, a1, a2) +} + +func (f *fakeSkills) GetSkill(a0 context.Context, a1 string, a2 string) (store.Skill, error) { + if f.getSkill == nil { + unexpectedCall(f.t, "GetSkill") + } + return f.getSkill(a0, a1, a2) +} + +func (f *fakeSkills) UpdateSkillDefault(a0 context.Context, a1 string, a2 string, a3 string) (store.Skill, error) { + if f.updateSkillDefault == nil { + unexpectedCall(f.t, "UpdateSkillDefault") + } + return f.updateSkillDefault(a0, a1, a2, a3) +} + +func (f *fakeSkills) DeleteSkill(a0 context.Context, a1 string, a2 string) error { + if f.deleteSkill == nil { + unexpectedCall(f.t, "DeleteSkill") + } + return f.deleteSkill(a0, a1, a2) +} + +func (f *fakeSkills) ListSkills(a0 context.Context, a1 string, a2 string, a3 int, a4 bool) (store.SkillPage, error) { + if f.listSkills == nil { + unexpectedCall(f.t, "ListSkills") + } + return f.listSkills(a0, a1, a2, a3, a4) +} + +func (f *fakeSkills) CreateSkillVersion(a0 context.Context, a1 string, a2 string, a3 []byte, a4 bool) (store.SkillVersion, error) { + if f.createSkillVersion == nil { + unexpectedCall(f.t, "CreateSkillVersion") + } + return f.createSkillVersion(a0, a1, a2, a3, a4) +} + +func (f *fakeSkills) GetSkillVersion(a0 context.Context, a1 string, a2 string, a3 string) (store.SkillVersion, error) { + if f.getSkillVersion == nil { + unexpectedCall(f.t, "GetSkillVersion") + } + return f.getSkillVersion(a0, a1, a2, a3) +} + +func (f *fakeSkills) ReadSkillVersion(a0 context.Context, a1 string, a2 string, a3 string) (store.SkillVersion, []byte, error) { + if f.readSkillVersion == nil { + unexpectedCall(f.t, "ReadSkillVersion") + } + return f.readSkillVersion(a0, a1, a2, a3) +} + +func (f *fakeSkills) ReadDefaultSkillVersion(a0 context.Context, a1 string, a2 string) (store.SkillVersion, []byte, error) { + if f.readDefaultSkillVersion == nil { + unexpectedCall(f.t, "ReadDefaultSkillVersion") + } + return f.readDefaultSkillVersion(a0, a1, a2) +} + +func (f *fakeSkills) DeleteSkillVersion(a0 context.Context, a1 string, a2 string, a3 string) (store.SkillVersion, error) { + if f.deleteSkillVersion == nil { + unexpectedCall(f.t, "DeleteSkillVersion") + } + return f.deleteSkillVersion(a0, a1, a2, a3) +} + +func (f *fakeSkills) ListSkillVersions(a0 context.Context, a1 string, a2 string, a3 string, a4 int, a5 bool) (store.SkillVersionPage, error) { + if f.listSkillVersions == nil { + unexpectedCall(f.t, "ListSkillVersions") + } + return f.listSkillVersions(a0, a1, a2, a3, a4, a5) +} + +type fakeSubagents struct { + t testing.TB + getSubagent func(context.Context, string, string, string) (v1.Subagent, error) + listSubagents func(context.Context, string, string, string, int, bool) (v1.SubagentList, error) + listSubagentItems func(context.Context, string, string, string, string, int, bool) (v1.ItemList, error) + getSubagentTurn func(context.Context, string, string, string, string) (v1.Turn, error) + listSubagentTurns func(context.Context, string, string, string, string, int, bool) (v1.TurnList, error) + listSubagentTurnItems func(context.Context, string, string, string, string, string, int, bool) (v1.ItemList, error) +} + +func (f *fakeSubagents) GetSubagent(a0 context.Context, a1 string, a2 string, a3 string) (v1.Subagent, error) { + if f.getSubagent == nil { + unexpectedCall(f.t, "GetSubagent") + } + return f.getSubagent(a0, a1, a2, a3) +} + +func (f *fakeSubagents) ListSubagents(a0 context.Context, a1 string, a2 string, a3 string, a4 int, a5 bool) (v1.SubagentList, error) { + if f.listSubagents == nil { + unexpectedCall(f.t, "ListSubagents") + } + return f.listSubagents(a0, a1, a2, a3, a4, a5) +} + +func (f *fakeSubagents) ListSubagentItems(a0 context.Context, a1 string, a2 string, a3 string, a4 string, a5 int, a6 bool) (v1.ItemList, error) { + if f.listSubagentItems == nil { + unexpectedCall(f.t, "ListSubagentItems") + } + return f.listSubagentItems(a0, a1, a2, a3, a4, a5, a6) +} + +func (f *fakeSubagents) GetSubagentTurn(a0 context.Context, a1 string, a2 string, a3 string, a4 string) (v1.Turn, error) { + if f.getSubagentTurn == nil { + unexpectedCall(f.t, "GetSubagentTurn") + } + return f.getSubagentTurn(a0, a1, a2, a3, a4) +} + +func (f *fakeSubagents) ListSubagentTurns(a0 context.Context, a1 string, a2 string, a3 string, a4 string, a5 int, a6 bool) (v1.TurnList, error) { + if f.listSubagentTurns == nil { + unexpectedCall(f.t, "ListSubagentTurns") + } + return f.listSubagentTurns(a0, a1, a2, a3, a4, a5, a6) +} + +func (f *fakeSubagents) ListSubagentTurnItems(a0 context.Context, a1 string, a2 string, a3 string, a4 string, a5 string, a6 int, a7 bool) (v1.ItemList, error) { + if f.listSubagentTurnItems == nil { + unexpectedCall(f.t, "ListSubagentTurnItems") + } + return f.listSubagentTurnItems(a0, a1, a2, a3, a4, a5, a6, a7) +} + +type fakeVaults struct { + t testing.TB + createVault func(context.Context, string, store.CreateVaultInput) (store.Vault, error) + getVault func(context.Context, string, string) (store.Vault, error) + deleteVault func(context.Context, string, string) (string, error) + listVaults func(context.Context, string, string, int, bool, []string) (store.VaultPage, error) + createOAuthCredential func(context.Context, string, string, store.CreateOAuthCredentialInput) (store.Credential, error) + updateOAuthCredential func(context.Context, string, string, string, store.UpdateOAuthCredentialInput) (store.Credential, error) + createStaticCredential func(context.Context, string, string, store.CreateStaticCredentialInput) (store.Credential, error) + updateStaticCredential func(context.Context, string, string, string, store.UpdateStaticCredentialInput) (store.Credential, error) + getCredential func(context.Context, string, string, string) (store.Credential, error) + deleteCredential func(context.Context, string, string, string) (string, error) + listCredentials func(context.Context, string, string, string, int, bool, []string) (store.CredentialPage, error) + resolveMCPCredentials func(context.Context, string, []string, []store.MCPCredentialRequest) ([]store.MCPCredentialBinding, error) +} + +func (f *fakeVaults) CreateVault(a0 context.Context, a1 string, a2 store.CreateVaultInput) (store.Vault, error) { + if f.createVault == nil { + unexpectedCall(f.t, "CreateVault") + } + return f.createVault(a0, a1, a2) +} + +func (f *fakeVaults) GetVault(a0 context.Context, a1 string, a2 string) (store.Vault, error) { + if f.getVault == nil { + unexpectedCall(f.t, "GetVault") + } + return f.getVault(a0, a1, a2) +} + +func (f *fakeVaults) DeleteVault(a0 context.Context, a1 string, a2 string) (string, error) { + if f.deleteVault == nil { + unexpectedCall(f.t, "DeleteVault") + } + return f.deleteVault(a0, a1, a2) +} + +func (f *fakeVaults) ListVaults(a0 context.Context, a1 string, a2 string, a3 int, a4 bool, a5 []string) (store.VaultPage, error) { + if f.listVaults == nil { + unexpectedCall(f.t, "ListVaults") + } + return f.listVaults(a0, a1, a2, a3, a4, a5) +} + +func (f *fakeVaults) CreateOAuthCredential(a0 context.Context, a1 string, a2 string, a3 store.CreateOAuthCredentialInput) (store.Credential, error) { + if f.createOAuthCredential == nil { + unexpectedCall(f.t, "CreateOAuthCredential") + } + return f.createOAuthCredential(a0, a1, a2, a3) +} + +func (f *fakeVaults) UpdateOAuthCredential(a0 context.Context, a1 string, a2 string, a3 string, a4 store.UpdateOAuthCredentialInput) (store.Credential, error) { + if f.updateOAuthCredential == nil { + unexpectedCall(f.t, "UpdateOAuthCredential") + } + return f.updateOAuthCredential(a0, a1, a2, a3, a4) +} + +func (f *fakeVaults) CreateStaticCredential(a0 context.Context, a1 string, a2 string, a3 store.CreateStaticCredentialInput) (store.Credential, error) { + if f.createStaticCredential == nil { + unexpectedCall(f.t, "CreateStaticCredential") + } + return f.createStaticCredential(a0, a1, a2, a3) +} + +func (f *fakeVaults) UpdateStaticCredential(a0 context.Context, a1 string, a2 string, a3 string, a4 store.UpdateStaticCredentialInput) (store.Credential, error) { + if f.updateStaticCredential == nil { + unexpectedCall(f.t, "UpdateStaticCredential") + } + return f.updateStaticCredential(a0, a1, a2, a3, a4) +} + +func (f *fakeVaults) GetCredential(a0 context.Context, a1 string, a2 string, a3 string) (store.Credential, error) { + if f.getCredential == nil { + unexpectedCall(f.t, "GetCredential") + } + return f.getCredential(a0, a1, a2, a3) +} + +func (f *fakeVaults) DeleteCredential(a0 context.Context, a1 string, a2 string, a3 string) (string, error) { + if f.deleteCredential == nil { + unexpectedCall(f.t, "DeleteCredential") + } + return f.deleteCredential(a0, a1, a2, a3) +} + +func (f *fakeVaults) ListCredentials(a0 context.Context, a1 string, a2 string, a3 string, a4 int, a5 bool, a6 []string) (store.CredentialPage, error) { + if f.listCredentials == nil { + unexpectedCall(f.t, "ListCredentials") + } + return f.listCredentials(a0, a1, a2, a3, a4, a5, a6) +} + +func (f *fakeVaults) ResolveMCPCredentials(a0 context.Context, a1 string, a2 []string, a3 []store.MCPCredentialRequest) ([]store.MCPCredentialBinding, error) { + if f.resolveMCPCredentials == nil { + unexpectedCall(f.t, "ResolveMCPCredentials") + } + return f.resolveMCPCredentials(a0, a1, a2, a3) +} + +type fakeWriteAudit struct { + t testing.TB + getResourceOwners func(context.Context, string, string, []string) ([]store.ResourceOwner, error) + listWriteOperations func(context.Context, string, store.WriteOperationFilter) (store.WriteOperationPage, error) +} + +func (f *fakeWriteAudit) GetResourceOwners(a0 context.Context, a1 string, a2 string, a3 []string) ([]store.ResourceOwner, error) { + if f.getResourceOwners == nil { + unexpectedCall(f.t, "GetResourceOwners") + } + return f.getResourceOwners(a0, a1, a2, a3) +} + +func (f *fakeWriteAudit) ListWriteOperations(a0 context.Context, a1 string, a2 store.WriteOperationFilter) (store.WriteOperationPage, error) { + if f.listWriteOperations == nil { + unexpectedCall(f.t, "ListWriteOperations") + } + return f.listWriteOperations(a0, a1, a2) +} diff --git a/services/core/internal/api/function_configuration_test.go b/services/core/internal/api/function_configuration_test.go index 2f55ecb6a..33bb9c3a7 100644 --- a/services/core/internal/api/function_configuration_test.go +++ b/services/core/internal/api/function_configuration_test.go @@ -13,8 +13,7 @@ import ( func TestPublicFunctionConfiguration(t *testing.T) { tool := `{"type":"function","name":"lookup","description":"","parameters":{"const":9007199254740993}}` for _, suffix := range []string{"", `,"tools":null`, `,"tools":[]`, `,"tools":[` + tool + `]`, `,"tools":[` + strings.TrimSuffix(tool, "}") + `,"defer_loading":false}]`} { - s := &recordingStore{} - h, _, _ := testHandler(t, WithExecution(&inputRecorder{ResourceStore: s})) + h, s, _ := testHandler(t, admitSessions) req := httptest.NewRequest(http.MethodPost, "/v1/agents/sessions", strings.NewReader(`{"agent":{"model":"model"`+suffix+`},"environment":{"type":"none"},"input":"Use the configured function when needed."}`)) req.Header.Set("Authorization", "Bearer test-api-key") req.Header.Set("OpenAI-Beta", "agents=v1") diff --git a/services/core/internal/api/function_inputs_test.go b/services/core/internal/api/function_inputs_test.go index cba1cf796..592edef4d 100644 --- a/services/core/internal/api/function_inputs_test.go +++ b/services/core/internal/api/function_inputs_test.go @@ -15,7 +15,7 @@ import ( func submitResultRequest(t *testing.T, body string, failure error) (*httptest.ResponseRecorder, *inputRecorder) { t.Helper() recorder := &inputRecorder{err: failure} - h, _, _ := testHandler(t, WithExecution(recorder)) + h, _, _ := testHandler(t, recorder.admit) r := httptest.NewRequest(http.MethodPost, "/v1/agents/sessions/session/events", strings.NewReader(body)) r.Header.Set("Authorization", "Bearer test-api-key") r.Header.Set("OpenAI-Beta", "agents=v1") diff --git a/services/core/internal/api/handler.go b/services/core/internal/api/handler.go index 6aaf7eeb9..b430f8421 100644 --- a/services/core/internal/api/handler.go +++ b/services/core/internal/api/handler.go @@ -9,82 +9,27 @@ import ( "net/http" "reflect" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" - v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/internal/obs/log" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/nativeinstaller" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/google/uuid" ) -type ResourceStore interface { - AgentStore - EnvironmentTemplateStore - VaultStore - CredentialStore - GetEnvironment(context.Context, string, string) (store.Environment, error) - ListItems(context.Context, string, string, string, int, bool) (store.ItemPage, error) - GetTurn(context.Context, string, string, string) (store.Turn, error) - ListTurns(context.Context, string, string, string, int, bool) (store.TurnPage, error) +// Sessions creates, reads, updates and deletes Sessions without execution +// work, and records their public write audit. Creation that admits work goes +// through Execution.Admission. +type Sessions interface { CreateSession(context.Context, string, store.CreateSessionInput) (store.Session, error) + CreateSessionStream(context.Context, string, store.CreateSessionInput) (store.SessionCreation, error) FindSessionCreation(context.Context, string, string, json.RawMessage, identity.Subject) (store.SessionCreation, error) GetSession(context.Context, string, string) (store.Session, error) - DeleteSession(context.Context, string, string) error - UpdateSessionMetadata(context.Context, string, string, map[string]string) (store.Session, error) ListSessions(context.Context, string, string, int, bool, *string) (store.SessionPage, error) -} - -type Handler struct { - nativeInstaller *nativeinstaller.Catalog - nativeVersion string - executorConnections func(context.Context, string, string) (bool, error) - coreMetrics CoreMetricsService - sandboxStore *store.Store - deploymentAuth *DeploymentAuthenticator - sandboxSetup func(context.Context, store.SandboxDeploymentSetupRequest) (store.RuntimeDeploymentView, error) - sandboxConfigurationDiscover func(context.Context, string, sandbox.ConfigurationDiscoveryInput) (json.RawMessage, error) - sandboxUpdate func(context.Context, store.SandboxDeploymentUpdateRequest) (store.RuntimeDeploymentView, error) - sandboxReset func(context.Context, store.SandboxResetRequest) (store.RuntimeDeploymentView, error) - sandboxResetCancel func(context.Context, uint64) (store.RuntimeDeploymentView, error) - policy execution.Policy - store ResourceStore - auth *Authenticator - projectKeys ProjectAPIKeyStore - writeAudit WriteAuditStore - adminArchive func(context.Context, string, string, uint64) (store.ManagedSessionArchive, error) - adminManagement AdminManagementStore - harnesses map[string]bool - modelProviderDefaults ModelProviderDefaults - engine string - inputs InputSubmitter - executorURL string - hostedEnvironments bool - directoryReader EnvironmentDirectoryReader - fileWriter EnvironmentFileWriter - skills SkillStore - sourceFiles SourceFileStore - artifacts SessionArtifactStore - subagents SubagentStore - runtimeObservations RuntimeObservationService - runtimeHistory RuntimeHistoryService - installation *Installation - installationBindings func(context.Context) (store.AddressBindings, error) -} - -func NewHandler(s ResourceStore, auth *Authenticator, engine string, options ...Option) (http.Handler, error) { - if s == nil || auth == nil || !store.ValidEngine(engine) { - return nil, errors.New("resource store, authentication and a valid execution engine are required") - } - h := &Handler{store: s, auth: auth, engine: engine} - for _, option := range options { - option(h) - } - return CanonicalPaths(h.routes()), nil + UpdateSessionMetadata(context.Context, string, string, map[string]string) (store.Session, error) + DeleteSession(context.Context, string, string) error + AuditSessionOperation(context.Context, string, string, string) error } // routes builds the router. HEAD runs the GET route without a body after the @@ -256,7 +201,7 @@ func (h *Handler) createSession(w http.ResponseWriter, r *http.Request) { return } } - selectedEngine := h.engine + selectedEngine := h.Engine var provider *v1.ModelProviderInput var providerSource string var deploymentRevision uuid.UUID @@ -264,7 +209,7 @@ func (h *Handler) createSession(w http.ResponseWriter, r *http.Request) { selectedEngine, provider, providerSource, deploymentRevision, err = h.resolveSessionExecution(r.Context(), input, inheritedProvider, configuration) } if err == nil { - if invalid := h.policy.ValidateSessionConfiguration(selectedEngine, configuration); invalid != nil { + if invalid := h.Policy.ValidateSessionConfiguration(selectedEngine, configuration); invalid != nil { err = fmt.Errorf("Harness %s does not support the requested Agent/environment configuration: %w", selectedEngine, invalid) } } @@ -287,11 +232,11 @@ func (h *Handler) createSession(w http.ResponseWriter, r *http.Request) { } return } - if input.Environment.Type == "self_hosted" && (h.inputs == nil || h.executorURL == "") { + if input.Environment.Type == "self_hosted" && h.Execution == nil { writeError(w, http.StatusServiceUnavailable, "execution_unavailable", "Self-hosted execution is not configured on this service.") return } - if input.Environment.Type == "openai_hosted" && (!h.hostedEnvironments || h.inputs == nil) { + if input.Environment.Type == "openai_hosted" && h.Sandboxes == nil { writeError(w, http.StatusServiceUnavailable, "execution_unavailable", "Hosted execution is not configured on this service.") return } @@ -308,13 +253,13 @@ func (h *Handler) createSession(w http.ResponseWriter, r *http.Request) { h.createSessionStream(w, r, createInput) return } - create := h.store.CreateSession + create := h.Sessions.CreateSession if len(initialInputs) > 0 || input.Environment.Type == "openai_hosted" { - if h.inputs == nil { + if h.Execution == nil { writeError(w, http.StatusServiceUnavailable, "execution_unavailable", "Execution input is not enabled on this service.") return } - create = h.inputs.CreateSession + create = h.Execution.Admission.CreateSession } session, err := create(r.Context(), tenantID(r), createInput) if err != nil { @@ -335,7 +280,7 @@ func (h *Handler) createSession(w http.ResponseWriter, r *http.Request) { // @Failure 400,401,404,500 {object} v1.ErrorResponse // @Router /agents/sessions/{session_id} [get] func (h *Handler) getSession(w http.ResponseWriter, r *http.Request) { - session, err := h.store.GetSession(r.Context(), tenantID(r), chi.URLParam(r, "session_id")) + session, err := h.Sessions.GetSession(r.Context(), tenantID(r), chi.URLParam(r, "session_id")) if err != nil { writeStoreError(w, r, err) return @@ -348,7 +293,7 @@ func (h *Handler) respondSession(w http.ResponseWriter, r *http.Request, session } func (h *Handler) respondSessionStatus(w http.ResponseWriter, r *http.Request, session store.Session, status int) { - response, err := sessionResponse(session, h.executorURL) + response, err := sessionResponse(session, h.executorURL()) if err != nil { writeStoreError(w, r, err) return @@ -382,14 +327,14 @@ func (h *Handler) listSessions(w http.ResponseWriter, r *http.Request) { if values, present := r.URL.Query()["agent_id"]; present { agentID = &values[0] } - page, err := h.store.ListSessions(r.Context(), tenantID(r), options.after, options.limit, options.ascending, agentID) + page, err := h.Sessions.ListSessions(r.Context(), tenantID(r), options.after, options.limit, options.ascending, agentID) if err != nil { writeStoreError(w, r, err) return } response := v1.SessionList{Data: make([]v1.Session, 0, len(page.Sessions)), HasMore: page.NextCursor != ""} for _, session := range page.Sessions { - item, err := sessionResponse(session, h.executorURL) + item, err := sessionResponse(session, h.executorURL()) if err != nil { writeStoreError(w, r, err) return diff --git a/services/core/internal/api/handler_test.go b/services/core/internal/api/handler_test.go index 0bfbe52c8..df21cf040 100644 --- a/services/core/internal/api/handler_test.go +++ b/services/core/internal/api/handler_test.go @@ -17,8 +17,9 @@ import ( "github.com/google/uuid" ) +// recordingStore records Session creation and listing. Tests wire it into +// fakeSessions with record. type recordingStore struct { - ResourceStore tenant string input store.CreateSessionInput sessions []store.Session @@ -34,10 +35,7 @@ func (s *recordingStore) ListSessions(_ context.Context, tenant, after string, l return store.SessionPage{Sessions: append([]store.Session(nil), s.sessions...), NextCursor: s.nextSessionCursor}, nil } -func (s *recordingStore) GetSession(ctx context.Context, tenant, id string) (store.Session, error) { - if s.ResourceStore != nil { - return s.ResourceStore.GetSession(ctx, tenant, id) - } +func (s *recordingStore) GetSession(_ context.Context, tenant, id string) (store.Session, error) { return store.Session{ID: id, TenantID: tenant, Configuration: json.RawMessage(`{"environment":{"type":"none"}}`)}, nil } @@ -50,25 +48,40 @@ func (s *recordingStore) CreateSession(_ context.Context, tenant string, input s return store.Session{ID: uuid.NewString(), TenantID: tenant, Metadata: input.Metadata, Configuration: input.Configuration, CreatedAt: time.Unix(1700000000, 0)}, nil } -func testHandler(t *testing.T, options ...Option) (http.Handler, *recordingStore, string) { +// record answers Session creation, reads and listing from s. +func (s *recordingStore) record(f *testFakes) { + f.sessions.createSession, f.sessions.getSession, f.sessions.findSessionCreation, f.sessions.listSessions = s.CreateSession, s.GetSession, s.FindSessionCreation, s.ListSessions +} + +// testHandler serves strict fakes for a fresh tenant whose caller +// authenticates with "Bearer test-api-key". A recordingStore answers Session +// creation, reads and listing, and the deployment has no default model +// provider. Each configure func adjusts the dependencies before the handler is +// built. +func testHandler(t *testing.T, configure ...func(*Dependencies, *testFakes)) (http.Handler, *recordingStore, string) { t.Helper() tenant := uuid.NewString() hash := sha256.Sum256([]byte("test-api-key")) - auth, err := NewAuthenticator([]APIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: hex.EncodeToString(hash[:]), TenantID: tenant}}) - if err != nil { - t.Fatal(err) - } + deps, fakes := testDependencies(t) + fakes.projects.resolveProjectAPIKey = projectKeys(t, APIKey{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: hex.EncodeToString(hash[:]), TenantID: tenant}).ResolveProjectAPIKey s := &recordingStore{} - h, err := NewHandler(s, auth, "codex", options...) - if err != nil { - t.Fatal(err) + s.record(fakes) + fakes.modelProviders.deploymentModelProvider = noDeploymentModelProvider + for _, c := range configure { + c(&deps, fakes) } - return h, s, tenant + return newTestHandler(t, deps), s, tenant +} + +// admitSessions enables Execution whose Worker admits Session creation, as it +// does for a Session with initial input, into the recording store. +func admitSessions(d *Dependencies, f *testFakes) { + d.Execution = f.execution() + f.admission.createSession = f.sessions.createSession } func TestHTTPConfigurationAndTenantIdentity(t *testing.T) { - s := &recordingStore{} - h, _, tenant := testHandler(t, WithExecution(&inputRecorder{ResourceStore: s})) + h, s, tenant := testHandler(t, admitSessions) body := `{"agent":{"model":"requested-model","instructions":"Keep this."},"environment":{"type":"none"},"metadata":{"tenant_id":"untrusted-tenant"},"input":"Follow the configured instructions."}` // Unknown query keys are ignored and never select the tenant. request := httptest.NewRequest(http.MethodPost, "/v1/agents/sessions?tenant_id=untrusted-tenant", strings.NewReader(body)) @@ -94,8 +107,7 @@ func TestHTTPConfigurationAndTenantIdentity(t *testing.T) { // Session responses carry both reasoning keys (SES-23); the stored // configuration and creation retry identity keep their original encoding. func TestSessionResponseReasoningKeysAreExplicit(t *testing.T) { - s := &recordingStore{} - h, _, _ := testHandler(t, WithExecution(&inputRecorder{ResourceStore: s})) + h, s, _ := testHandler(t, admitSessions) request := httptest.NewRequest(http.MethodPost, "/v1/agents/sessions", strings.NewReader(`{"agent":{"model":"requested-model"},"environment":{"type":"none"},"input":"hello"}`)) request.Header.Set("Authorization", "Bearer test-api-key") request.Header.Set("OpenAI-Beta", "agents=v1") @@ -141,7 +153,8 @@ func TestHTTPRejectsUntrustedOrUnsupportedRequests(t *testing.T) { {"large body", "Bearer test-api-key", "agents=v1", "/v1/agents/sessions", `{"agent":{"model":"` + strings.Repeat("x", 16*1024*1024) + `"}}`, 413}, } { t.Run(test.name, func(t *testing.T) { - h, s, _ := testHandler(t) + unavailable := 0 + h, s, _ := testHandler(t, func(_ *Dependencies, f *testFakes) { f.metrics.recordUnavailable = func() { unavailable++ } }) r := httptest.NewRequest(http.MethodPost, test.path, strings.NewReader(test.body)) r.Header.Set("Authorization", test.auth) r.Header.Set("OpenAI-Beta", test.beta) @@ -157,6 +170,10 @@ func TestHTTPRejectsUntrustedOrUnsupportedRequests(t *testing.T) { if w.Code != test.status || coded == (test.status == http.StatusUnauthorized) || s.tenant != "" { t.Fatalf("response = %d %s, stored tenant = %s", w.Code, w.Body, s.tenant) } + // Core counts each execution_unavailable response. + if unavailable != 0 != (test.status == http.StatusServiceUnavailable) { + t.Fatalf("recorded unavailability %d times for %d", unavailable, w.Code) + } }) } } diff --git a/services/core/internal/api/harness.go b/services/core/internal/api/harness.go index d07eaba0e..e60e128f7 100644 --- a/services/core/internal/api/harness.go +++ b/services/core/internal/api/harness.go @@ -5,26 +5,16 @@ import ( "fmt" ) -// WithHarnesses enables explicit selections qualified by this deployment. -func WithHarnesses(kinds []string) Option { - return func(h *Handler) { - h.harnesses = make(map[string]bool, len(kinds)) - for _, kind := range kinds { - h.harnesses[kind] = true - } - } -} - func (h *Handler) sessionHarness(raw json.RawMessage) (string, error) { var cfg configuration if err := json.Unmarshal(raw, &cfg); err != nil { return "", err } if cfg.Agent.XAgentsCore == nil || cfg.Agent.XAgentsCore.Harness == "" { - return h.engine, nil + return h.Engine, nil } kind := cfg.Agent.XAgentsCore.Harness - if kind != h.engine && !h.harnesses[kind] { + if kind != h.Engine && !h.harnesses[kind] { return "", fmt.Errorf("Harness %s is not enabled on this Core deployment.", kind) } return kind, nil diff --git a/services/core/internal/api/harness_model_providers.go b/services/core/internal/api/harness_model_providers.go index 2e87e65ab..6ab5f40a6 100644 --- a/services/core/internal/api/harness_model_providers.go +++ b/services/core/internal/api/harness_model_providers.go @@ -13,12 +13,15 @@ import ( "github.com/go-chi/chi/v5" ) -// DeploymentModelProviderStore holds one deployment default model provider per -// harness. Keys are write-only and encrypted. -type DeploymentModelProviderStore interface { +// ModelProviders holds one deployment default model provider per harness. Keys +// are write-only and encrypted. DeploymentModelProvider decrypts a harness's +// default at Session creation, before the encrypted Session snapshot is +// committed; it returns nil when the harness has no default. +type ModelProviders interface { ListDeploymentModelProviders(context.Context) ([]store.DeploymentModelProvider, error) SetDeploymentModelProvider(context.Context, string, v1.ModelConfigurationInput) (store.DeploymentModelProvider, error) DeleteDeploymentModelProvider(context.Context, string) error + DeploymentModelProvider(context.Context, string) (*store.DeploymentModelProviderSnapshot, error) } // HarnessModelConfiguration is a harness's deployment default model provider. It @@ -58,14 +61,10 @@ func harnessModelConfiguration(value store.DeploymentModelProvider) *HarnessMode // registerHarnessRoutes adds harness and deployment model provider management // to the Core-key-authenticated /core/v1 router. func (h *Handler) registerHarnessRoutes(r chi.Router) { - s, ok := h.store.(DeploymentModelProviderStore) - if !ok { - return - } - r.Get("/harnesses", func(w http.ResponseWriter, r *http.Request) { h.listHarnesses(w, r, s) }) - r.Get("/harnesses/{harness}/model-configuration", func(w http.ResponseWriter, r *http.Request) { h.getHarnessModelConfiguration(w, r, s) }) - r.Put("/harnesses/{harness}/model-configuration", func(w http.ResponseWriter, r *http.Request) { h.setHarnessModelConfiguration(w, r, s) }) - r.Delete("/harnesses/{harness}/model-configuration", func(w http.ResponseWriter, r *http.Request) { h.deleteHarnessModelConfiguration(w, r, s) }) + r.Get("/harnesses", h.listHarnesses) + r.Get("/harnesses/{harness}/model-configuration", h.getHarnessModelConfiguration) + r.Put("/harnesses/{harness}/model-configuration", h.setHarnessModelConfiguration) + r.Delete("/harnesses/{harness}/model-configuration", h.deleteHarnessModelConfiguration) } // knownHarness reports the path harness, writing 404 for one this build lacks. @@ -86,8 +85,8 @@ func knownHarness(w http.ResponseWriter, r *http.Request) (string, bool) { // @Success 200 {object} api.CoreHarnessList // @Failure 401,500 {object} CoreErrorResponse // @Router /core/v1/harnesses [get] -func (h *Handler) listHarnesses(w http.ResponseWriter, r *http.Request, s DeploymentModelProviderStore) { - providers, err := s.ListDeploymentModelProviders(r.Context()) +func (h *Handler) listHarnesses(w http.ResponseWriter, r *http.Request) { + providers, err := h.ModelProviders.ListDeploymentModelProviders(r.Context()) if err != nil { writeStoreError(w, r, err) return @@ -100,7 +99,7 @@ func (h *Handler) listHarnesses(w http.ResponseWriter, r *http.Request, s Deploy support.Protocols = append(support.Protocols, provider.Protocol) support.TokenLimitsRequired = support.TokenLimitsRequired || provider.RequiresTokenLimits } - harness := CoreHarness{ModelConfigurationSupport: support, Object: "core.harness", ID: kind, Enabled: kind == h.engine || h.harnesses[kind], Default: kind == h.engine} + harness := CoreHarness{ModelConfigurationSupport: support, Object: "core.harness", ID: kind, Enabled: kind == h.Engine || h.harnesses[kind], Default: kind == h.Engine} for _, provider := range providers { if provider.Harness == kind { harness.ModelConfiguration = harnessModelConfiguration(provider) @@ -120,12 +119,12 @@ func (h *Handler) listHarnesses(w http.ResponseWriter, r *http.Request, s Deploy // @Success 200 {object} api.HarnessModelConfiguration // @Failure 401,404,500 {object} CoreErrorResponse // @Router /core/v1/harnesses/{harness}/model-configuration [get] -func (h *Handler) getHarnessModelConfiguration(w http.ResponseWriter, r *http.Request, s DeploymentModelProviderStore) { +func (h *Handler) getHarnessModelConfiguration(w http.ResponseWriter, r *http.Request) { harness, ok := knownHarness(w, r) if !ok { return } - providers, err := s.ListDeploymentModelProviders(r.Context()) + providers, err := h.ModelProviders.ListDeploymentModelProviders(r.Context()) if err != nil { writeStoreError(w, r, err) return @@ -167,7 +166,7 @@ func requiredModelProviderShape() shape { // @Success 200 {object} api.HarnessModelConfiguration // @Failure 400,401,404,413,500,503 {object} CoreErrorResponse // @Router /core/v1/harnesses/{harness}/model-configuration [put] -func (h *Handler) setHarnessModelConfiguration(w http.ResponseWriter, r *http.Request, s DeploymentModelProviderStore) { +func (h *Handler) setHarnessModelConfiguration(w http.ResponseWriter, r *http.Request) { harness, ok := knownHarness(w, r) if !ok { return @@ -190,7 +189,7 @@ func (h *Handler) setHarnessModelConfiguration(w http.ResponseWriter, r *http.Re return } setAdminAuditSource(r, "") - provider, err := s.SetDeploymentModelProvider(r.Context(), harness, input) + provider, err := h.ModelProviders.SetDeploymentModelProvider(r.Context(), harness, input) if err != nil { writeStoreError(w, r, err) return @@ -206,13 +205,13 @@ func (h *Handler) setHarnessModelConfiguration(w http.ResponseWriter, r *http.Re // @Success 204 // @Failure 401,404,500 {object} CoreErrorResponse // @Router /core/v1/harnesses/{harness}/model-configuration [delete] -func (h *Handler) deleteHarnessModelConfiguration(w http.ResponseWriter, r *http.Request, s DeploymentModelProviderStore) { +func (h *Handler) deleteHarnessModelConfiguration(w http.ResponseWriter, r *http.Request) { harness, ok := knownHarness(w, r) if !ok { return } setAdminAuditSource(r, "") - if err := s.DeleteDeploymentModelProvider(r.Context(), harness); err != nil { + if err := h.ModelProviders.DeleteDeploymentModelProvider(r.Context(), harness); err != nil { writeStoreError(w, r, err) return } diff --git a/services/core/internal/api/harness_test.go b/services/core/internal/api/harness_test.go index 5a9fac422..047f869bd 100644 --- a/services/core/internal/api/harness_test.go +++ b/services/core/internal/api/harness_test.go @@ -29,13 +29,16 @@ func TestSessionHarnessAdmission(t *testing.T) { {"mcode self-hosted requires executor configuration", `,"x_agents_core":{"harness":"mcode"}`, "", `{"type":"self_hosted","workspace_directory":"/workspace"},` + fixtureAnthropicSessionProvider, "", true, 503}, } { t.Run(tc.name, func(t *testing.T) { - var options []Option - if tc.enabled { - options = append(options, WithHarnesses([]string{"claude_sdk", "mcode"})) - } - s := &recordingStore{} - options = append(options, WithExecution(&inputRecorder{ResourceStore: s})) - h, _, _ := testHandler(t, options...) + h, s, _ := testHandler(t, func(d *Dependencies, f *testFakes) { + if tc.enabled { + d.Harnesses = []string{"claude_sdk", "mcode"} + } + // Self-hosted execution needs an executor, which this Core lacks. + if !strings.Contains(tc.environment, "self_hosted") { + admitSessions(d, f) + } + f.metrics.recordUnavailable = func() {} + }) r := httptest.NewRequest(http.MethodPost, "/v1/agents/sessions", strings.NewReader(`{"agent":{"model":"fixture"`+tc.extension+tc.extra+`},"environment":`+tc.environment+`,"input":"Run on the selected harness."}`)) r.Header.Set("Authorization", "Bearer test-api-key") r.Header.Set("OpenAI-Beta", "agents=v1") diff --git a/services/core/internal/api/hosted_environment.go b/services/core/internal/api/hosted_environment.go index 844143049..7058e6fdb 100644 --- a/services/core/internal/api/hosted_environment.go +++ b/services/core/internal/api/hosted_environment.go @@ -99,12 +99,6 @@ func hostedSessionEnvironment(environment store.Environment) (v1.SessionEnvironm Packages: func() *v1.EnvironmentPackagesResponse { value := packageMetadata(cfg.Packages); return &value }(), Files: &files, Plugins: &cfg.Plugins, Skills: &cfg.Skills}, nil } -// WithHostedEnvironments enables admission only for an operator-composed, -// qualified managed Runtime deployment. Native capability flags cannot enable it. -func WithHostedEnvironments() Option { - return func(h *Handler) { h.hostedEnvironments = true } -} - func storedEnvironment(raw json.RawMessage) (*v1.Environment, error) { var fields map[string]json.RawMessage if json.Unmarshal(raw, &fields) != nil { diff --git a/services/core/internal/api/hosted_environment_test.go b/services/core/internal/api/hosted_environment_test.go index a275ec14a..b1facbd71 100644 --- a/services/core/internal/api/hosted_environment_test.go +++ b/services/core/internal/api/hosted_environment_test.go @@ -88,7 +88,10 @@ func TestHostedEnvironmentResponseHasPinnedShapeAndNoConnectionAction(t *testing func TestHostedCreationUsesExecutionAdmission(t *testing.T) { for _, stream := range []bool{false, true} { recorder := &hostedCreationRecorder{} - handler, fixture := environmentCreationHandler(t, "codex", WithHostedEnvironments(), WithExecution(recorder)) + handler, fixture := environmentCreationHandler(t, "codex", func(d *Dependencies, f *testFakes) { + d.Execution, d.Sandboxes = f.execution(), f.sandboxes() + f.admission.createSession, f.admission.createSessionStream = recorder.CreateSession, recorder.CreateSessionStream + }) input := "" if stream { input = `,"input":"Initialize the streamed hosted execution."` @@ -108,8 +111,9 @@ func TestHostedCreationUsesExecutionAdmission(t *testing.T) { } } +// hostedCreationRecorder is the Worker's hosted admission; it counts +// creations. type hostedCreationRecorder struct { - inputRecorder calls int } diff --git a/services/core/internal/api/hosted_failure_test.go b/services/core/internal/api/hosted_failure_test.go index 6929698df..e1d63de32 100644 --- a/services/core/internal/api/hosted_failure_test.go +++ b/services/core/internal/api/hosted_failure_test.go @@ -107,14 +107,10 @@ func TestGetStreamEndsAfterHostedProvisioningFailure(t *testing.T) { f := &streamFixture{session: hostedFailureSession()} f.session.TenantID = uuid.NewString() f.session.Environment.TenantID = f.session.TenantID - auth, err := NewAuthenticator([]APIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential("key"), TenantID: f.session.TenantID}}) - if err != nil { - t.Fatal(err) - } - h, err := NewHandler(f, auth, "codex") - if err != nil { - t.Fatal(err) - } + 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("key"), TenantID: f.session.TenantID}).ResolveProjectAPIKey + f.serve(fakes) + h := newTestHandler(t, deps) server := httptest.NewServer(h) defer server.Close() failed := store.SessionChange{Sequence: 13, Event: v1.SessionEvent{Type: "agent.session.failed", EventID: "failed"}} diff --git a/services/core/internal/api/hosted_structured_test.go b/services/core/internal/api/hosted_structured_test.go index c387aa3f7..4b4e2ebc7 100644 --- a/services/core/internal/api/hosted_structured_test.go +++ b/services/core/internal/api/hosted_structured_test.go @@ -20,7 +20,7 @@ func TestHostedStructuredConfigurationQualification(t *testing.T) { } lookup := &templateLookupStore{network: "enabled", skills: template.Initialization.Skills, plugins: template.Initialization.Plugins, directories: template.Initialization.CapabilityDirectories} - h := Handler{store: lookup} + h := templateHandler(t, lookup.ResolveEnvironmentTemplate) for _, environment := range []string{ `{"type":"openai_hosted",` + fields + `}`, `{"type":"openai_hosted","environment_template_id":"saved"}`, diff --git a/services/core/internal/api/inputs.go b/services/core/internal/api/inputs.go index 3892dd705..74a9a8c1d 100644 --- a/services/core/internal/api/inputs.go +++ b/services/core/internal/api/inputs.go @@ -13,16 +13,14 @@ import ( "github.com/google/uuid" ) -type InputSubmitter interface { +// Admission creates Sessions that admit work and submits Session input through +// the execution Worker, which validates execution support first. +type Admission interface { CreateSession(context.Context, string, store.CreateSessionInput) (store.Session, error) + CreateSessionStream(context.Context, string, store.CreateSessionInput) (store.SessionCreation, error) SubmitInputs(context.Context, string, string, string, []store.Input) ([]store.InputReceipt, error) } -type Option func(*Handler) - -// WithExecution enables durable admission when the service owns an execution worker. -func WithExecution(s InputSubmitter) Option { return func(h *Handler) { h.inputs = s } } - // @Summary Submit Session input events // @Description An empty events array is a resource-authorized no-op; it creates no execution retry identity, Turn, Item or input receipt. For environment none, atomically accepts text messages, cancellation and function results. Messages steer active work or start a queued Turn. Qualified Codex and Claude SDK workspace profiles accept text and inline PNG/JPEG messages, independently of managed or self_hosted ownership. Under the Session lock, matching retries retain their original target; new active messages append to the current Turn, while idle messages reserve work and wait up to the original five-minute connection/admission deadline. Return 202 only after durable admission, without claiming native application; active messages create no Turn or reservation. Cancellation-only prepared-environment batches use existing durable cancellation admission and return 202 without waiting for native exit; a new cancellation conflicts while a pre-Turn reservation is pending. Homogeneous tool_result-only prepared-environment batches reuse existing scoped result admission and application receipts without creating a Turn or bypassing a pending reservation. Mixed prepared-environment batches remain unsupported. HTTP expiry/cancellation use local 409 environment_input_expired/environment_input_cancelled errors. New input on a Session whose hosted Environment failed to provision returns the observed 409 conflict_error "the hosted environment failed to provision"; input already waiting when it fails and expired Environments keep the local 409 environment_unavailable. Input the Session cannot accept in its current state, such as a result after cancellation or a batch while earlier input is pending, and a result that differs from the call's saved result return 409 with type and code conflict_error; reusing an Idempotency-Key with a different batch returns the local 409 idempotency_conflict. Inside an owned Session, a result for an unknown call or for a call of another Turn returns 400 invalid_request_error and changes nothing; missing and foreign Sessions return 404. Losing execution ownership returns 503. The response write deadline accommodates the admission window for either prepared Environment, independently of new-hosted-admission and executor URL settings. Disconnecting the waiting HTTP request does not cancel retained work or restart its deadline. Retry keys identify the whole ordered batch. Function output accepts text or ordered text/image parts subject to engine support; Claude SDK accepts text results and, on none and qualified workspace profiles, successful inline PNG/JPEG results, preserving ordered content; error images and remote references reject before admission. Native image resizing may change bytes. Runtime image-result support is checked only for image-bearing delivery. Codex and Claude SDK on none and qualified managed or self_hosted workspace profiles accept ordered inline PNG/JPEG image messages. Other engines remain text-only; remote image URLs are unsupported. Image references are retained unchanged without service-side downloads. // @Tags Sessions @@ -62,7 +60,7 @@ func (h *Handler) createEvents(w http.ResponseWriter, r *http.Request) { if request.Events != nil && len(request.Events) == 0 { // Empty batches have no execution identity to reserve or replay. // Authorize the resource even when no executor is configured. - if _, err := h.store.GetSession(r.Context(), tenantID(r), chi.URLParam(r, "session_id")); err != nil { + if _, err := h.Sessions.GetSession(r.Context(), tenantID(r), chi.URLParam(r, "session_id")); err != nil { writeStoreError(w, r, err) return } @@ -73,7 +71,7 @@ func (h *Handler) createEvents(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusAccepted) return } - if h.inputs == nil { + if h.Execution == nil { writeError(w, http.StatusServiceUnavailable, "execution_unavailable", "Execution is not enabled on this service.") return } @@ -87,7 +85,7 @@ func (h *Handler) createEvents(w http.ResponseWriter, r *http.Request) { writeStoreError(w, r, err) return } - if _, err := h.inputs.SubmitInputs(r.Context(), tenantID(r), sessionID, key, inputs); err != nil { + if _, err := h.Execution.Admission.SubmitInputs(r.Context(), tenantID(r), sessionID, key, inputs); err != nil { writeInputError(w, r, err) return } diff --git a/services/core/internal/api/inputs_test.go b/services/core/internal/api/inputs_test.go index 7b89a1f82..8bd111f7e 100644 --- a/services/core/internal/api/inputs_test.go +++ b/services/core/internal/api/inputs_test.go @@ -12,8 +12,9 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) +// inputRecorder is the Worker's input admission. It records submitted inputs +// and answers with err. type inputRecorder struct { - ResourceStore tenant, session, key string inputs []store.Input err error @@ -24,9 +25,15 @@ func (s *inputRecorder) SubmitInputs(_ context.Context, tenant, session, key str return nil, s.err } +// admit enables Execution whose Worker records submitted inputs in s. +func (s *inputRecorder) admit(d *Dependencies, f *testFakes) { + d.Execution = f.execution() + f.admission.submitInputs = s.SubmitInputs +} + func TestPublicInputAdmission(t *testing.T) { recorder := &inputRecorder{} - h, _, tenant := testHandler(t, WithExecution(recorder)) + h, _, tenant := testHandler(t, recorder.admit) body := `{"events":[{"type":"agent.session.input.message","input":[{"role":"user","content":[{"type":"input_text","text":"First"}]},{"role":"user","content":[{"type":"input_text","text":"Second"}]}]},{"type":"agent.session.input.cancel"}]}` r := httptest.NewRequest(http.MethodPost, "/v1/agents/sessions/session-id/events", strings.NewReader(body)) r.Header.Set("Authorization", "Bearer test-api-key") @@ -62,7 +69,7 @@ func TestPublicInputRejectsUnsupportedOrMalformedBatch(t *testing.T) { `{"events":[{"type":"agent.session.input.cancel"}]} {}`, } { recorder := &inputRecorder{} - h, _, _ := testHandler(t, WithExecution(recorder)) + h, _, _ := testHandler(t, recorder.admit) r := httptest.NewRequest(http.MethodPost, "/v1/agents/sessions/id/events", strings.NewReader(body)) r.Header.Set("Authorization", "Bearer test-api-key") r.Header.Set("OpenAI-Beta", "agents=v1") @@ -87,7 +94,7 @@ func TestPublicInputWhitespaceTextAdmission(t *testing.T) { `[{"role":"user","content":[{"type":"input_text","text":""},{"type":"input_text","text":"Reply only OK."}]}]`, } { recorder := &inputRecorder{} - h, _, _ := testHandler(t, WithExecution(recorder)) + h, _, _ := testHandler(t, recorder.admit) body := `{"events":[{"type":"agent.session.input.message","input":` + input + `}]}` r := httptest.NewRequest(http.MethodPost, "/v1/agents/sessions/session-id/events", strings.NewReader(body)) r.Header.Set("Authorization", "Bearer test-api-key") @@ -113,7 +120,7 @@ func TestPublicInputWhitespaceTextAdmission(t *testing.T) { `{"events":[{"type":"agent.session.input.message","input":[]}]}`, } { recorder := &inputRecorder{} - h, _, _ := testHandler(t, WithExecution(recorder)) + h, _, _ := testHandler(t, recorder.admit) r := httptest.NewRequest(http.MethodPost, "/v1/agents/sessions/session-id/events", strings.NewReader(body)) r.Header.Set("Authorization", "Bearer test-api-key") r.Header.Set("OpenAI-Beta", "agents=v1") diff --git a/services/core/internal/api/installation.go b/services/core/internal/api/installation.go index 58f09a0f8..e39cfe22d 100644 --- a/services/core/internal/api/installation.go +++ b/services/core/internal/api/installation.go @@ -97,13 +97,9 @@ func ParseInstallationConfiguration(raw []byte) (*InstallationConfiguration, err return &value, nil } -// WithInstallation serves GET /core/v1/installation. bindings counts what is -// bound to the current public URL. -func WithInstallation(value Installation, bindings func(context.Context) (store.AddressBindings, error)) Option { - return func(h *Handler) { - value.Object = "core.installation" - h.installation, h.installationBindings = &value, bindings - } +// InstallationBindings counts what is bound to the current public URL. +type InstallationBindings interface { + AddressBindings(context.Context) (store.AddressBindings, error) } // @Summary Retrieve installation facts and process settings @@ -115,12 +111,12 @@ func WithInstallation(value Installation, bindings func(context.Context) (store. // @Failure 401,500 {object} CoreErrorResponse // @Router /core/v1/installation [get] func (h *Handler) getInstallation(w http.ResponseWriter, r *http.Request) { - bindings, err := h.installationBindings(r.Context()) + bindings, err := h.InstallationBindings.AddressBindings(r.Context()) if err != nil { writeStoreError(w, r, err) return } - value := *h.installation + value := h.Installation value.AddressBindings = bindings writeJSON(w, http.StatusOK, value) } diff --git a/services/core/internal/api/installation_test.go b/services/core/internal/api/installation_test.go index 24a7f9778..cc25fcc87 100644 --- a/services/core/internal/api/installation_test.go +++ b/services/core/internal/api/installation_test.go @@ -8,13 +8,13 @@ import ( "strings" "testing" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) func TestInstallationReadNeedsOnlyTheCoreKey(t *testing.T) { - project, _ := NewAuthenticator([]APIKey{callerBinding()}) - admin, _ := NewDeploymentAuthenticator([]string{runtimedevice.HashCredential("administrator")}) + deps, fakes := testDependencies(t) + fakes.projects.resolveProjectAPIKey = projectKeys(t, callerBinding()).ResolveProjectAPIKey + deps.CoreKeys = coreKeys(t, "administrator") public, id := "https://core.example", "5b7c0f3e-0000-4000-8000-000000000001" settings, err := ParseInstallationConfiguration([]byte(`{"path":"/home/alice/.oac/core/config.json","apply_command":"/home/alice/.oac/core/oac apply", "applied_at":"2026-09-25T09:30:00Z","settings":[{"key":"ports.core","value":8091,"default":8091,"changeable":true,"sensitive":false,"restarts":["core"]}, @@ -22,15 +22,12 @@ func TestInstallationReadNeedsOnlyTheCoreKey(t *testing.T) { if err != nil { t.Fatal(err) } - bindings := func(context.Context) (store.AddressBindings, error) { + fakes.installationBindings.addressBindings = func(context.Context) (store.AddressBindings, error) { return store.AddressBindings{Nodes: 2, NodesOnOtherAddress: 1}, nil } - // No sandbox manager: the read is available before any deployment exists. - h, err := NewHandler(&recordingStore{}, project, "codex", WithProjectAPIKeys(nil, admin), - WithInstallation(Installation{InstallationID: &id, PublicURL: &public, Configuration: settings}, bindings)) - if err != nil { - t.Fatal(err) - } + // No sandbox deployment: the read is available before any deployment exists. + deps.Installation = Installation{InstallationID: &id, PublicURL: &public, Configuration: settings} + h := newTestHandler(t, deps) get := func(token string) *httptest.ResponseRecorder { request := httptest.NewRequest(http.MethodGet, "/core/v1/installation", nil) request.Header.Set("Authorization", "Bearer "+token) @@ -64,8 +61,7 @@ func TestInstallationSnapshotCannotCarryASensitiveValue(t *testing.T) { } func TestDeploymentAddressIsNotInput(t *testing.T) { - project, _ := NewAuthenticator([]APIKey{callerBinding()}) - admin, _ := NewDeploymentAuthenticator([]string{runtimedevice.HashCredential("administrator")}) + deps, fakes := sandboxFakes(t) initializations := 0 initialize := func(_ context.Context, input store.SandboxDeploymentSetupRequest) (store.RuntimeDeploymentView, error) { initializations++ @@ -74,15 +70,9 @@ func TestDeploymentAddressIsNotInput(t *testing.T) { } return store.RuntimeDeploymentView{}, store.ErrSandboxPublicURLUnreachable } - update := func(context.Context, store.SandboxDeploymentUpdateRequest) (store.RuntimeDeploymentView, error) { - t.Fatal("core_url reached the update") - return store.RuntimeDeploymentView{}, nil - } - h, err := NewHandler(&recordingStore{}, project, "codex", WithSandboxManager(&store.Store{}, admin), - WithSandboxDeploymentSetup(initialize), WithSandboxDeploymentChanges(update, nil, nil)) - if err != nil { - t.Fatal(err) - } + // The strict fake fails the test if core_url reaches the update. + fakes.deploymentChanges.initializeSandboxDeployment = initialize + h := newTestHandler(t, deps) for _, test := range []struct { method, body, code string status int diff --git a/services/core/internal/api/items.go b/services/core/internal/api/items.go index 119ad16a0..5ee75c336 100644 --- a/services/core/internal/api/items.go +++ b/services/core/internal/api/items.go @@ -23,7 +23,7 @@ func (h *Handler) listItems(w http.ResponseWriter, r *http.Request) { if !ok { return } - page, err := h.store.ListItems(r.Context(), tenantID(r), chi.URLParam(r, "session_id"), options.after, options.limit, options.ascending) + page, err := h.SessionHistory.ListItems(r.Context(), tenantID(r), chi.URLParam(r, "session_id"), options.after, options.limit, options.ascending) if err != nil { writeStoreError(w, r, err) return diff --git a/services/core/internal/api/items_test.go b/services/core/internal/api/items_test.go index 546a33f96..676c43714 100644 --- a/services/core/internal/api/items_test.go +++ b/services/core/internal/api/items_test.go @@ -12,7 +12,6 @@ import ( ) type itemReadStore struct { - ResourceStore tenant, session, cursor string limit int ascending bool @@ -23,9 +22,8 @@ func (s *itemReadStore) ListItems(_ context.Context, tenant, session, cursor str return store.ItemPage{Items: []v1.Item{}, HasMore: false}, nil } func TestItemRouteUsesAuthenticationAndSharedPagination(t *testing.T) { - h, record, tenant := testHandler(t) s := &itemReadStore{} - record.ResourceStore = s + h, _, tenant := testHandler(t, func(_ *Dependencies, f *testFakes) { f.sessionHistory.listItems = s.ListItems }) request := func(query, token string) *httptest.ResponseRecorder { r := httptest.NewRequest(http.MethodGet, "/v1/agents/sessions/session/items"+query, nil) r.Header.Set("Authorization", "Bearer "+token) diff --git a/services/core/internal/api/model_configuration_route_test.go b/services/core/internal/api/model_configuration_route_test.go index 94281df54..e8356171b 100644 --- a/services/core/internal/api/model_configuration_route_test.go +++ b/services/core/internal/api/model_configuration_route_test.go @@ -17,7 +17,7 @@ func TestModelConfigurationRouteAdmission(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { s := &coreProviderValidationStore{} - h, _, _ := adminTestHandler(t, func(h *Handler) { h.store = s }) + h, _, _ := adminTestHandler(t, s.configure) body := `{"model":` + mustJSONForTest(tc.model) + `,"model_provider":{"protocol":"` + tc.protocol + `","base_url":"https://example.test","api_key":"provider-secret"},"harness_config":` + tc.native + `}` out := projectKeyHTTP(h, http.MethodPut, "/core/v1/harnesses/codex/model-configuration", "admin", body) var result struct { diff --git a/services/core/internal/api/model_provider_fixture_test.go b/services/core/internal/api/model_provider_fixture_test.go index c7bf955a2..4de3f94ab 100644 --- a/services/core/internal/api/model_provider_fixture_test.go +++ b/services/core/internal/api/model_provider_fixture_test.go @@ -19,11 +19,10 @@ func fixtureModelProvider(harness string) *v1.ModelProviderInput { return &v1.ModelProviderInput{Protocol: "anthropic", BaseURL: "https://model.fixture.example/anthropic", APIKey: "fixture-model-key", ContextWindow: 200000, MaxOutputTokens: 8000} } -// withFixtureDeploymentProvider configures a deployment default for every harness. -func withFixtureDeploymentProvider() Option { - return WithModelProviderDefaults(func(_ context.Context, harness string) (*store.DeploymentModelProviderSnapshot, error) { - return &store.DeploymentModelProviderSnapshot{Model: "fixture", Provider: fixtureModelProvider(harness), Revision: uuid.New()}, nil - }) +// fixtureDeploymentProvider is a deployment default for every harness, for +// fakeModelProviders.deploymentModelProvider. +func fixtureDeploymentProvider(_ context.Context, harness string) (*store.DeploymentModelProviderSnapshot, error) { + return &store.DeploymentModelProviderSnapshot{Model: "fixture", Provider: fixtureModelProvider(harness), Revision: uuid.New()}, nil } // fixtureSessionProvider is a top-level Session request member for Codex. diff --git a/services/core/internal/api/native_classification_integration_test.go b/services/core/internal/api/native_classification_integration_test.go index 4eaabd9d6..5e5aa8742 100644 --- a/services/core/internal/api/native_classification_integration_test.go +++ b/services/core/internal/api/native_classification_integration_test.go @@ -15,7 +15,7 @@ import ( func TestNativeClassificationPostgresRoundTripAndPublicPrivacy(t *testing.T) { s, pool := diagnosticDatabase(t) - h, _, tenant := adminTestHandler(t, func(h *Handler) { h.store = s }) + h, _, tenant := adminTestHandler(t, databaseSessionReads(s)) for _, code := range []string{"authentication_error", "connection_failed", "secret-canary"} { t.Run(code, func(t *testing.T) { session, err := s.CreateSession(t.Context(), tenant, store.CreateSessionInput{Creator: identity.Subject{Kind: "service_account", ID: "native-classification"}, Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"id":"agent_root","model":"test"},"environment":{"type":"none"}}`)}) diff --git a/services/core/internal/api/project_api_keys.go b/services/core/internal/api/project_api_keys.go index a6dac8fc3..c794d83ad 100644 --- a/services/core/internal/api/project_api_keys.go +++ b/services/core/internal/api/project_api_keys.go @@ -13,7 +13,9 @@ import ( "github.com/google/uuid" ) -type ProjectAPIKeyStore interface { +// Projects manages Projects and their API keys, and resolves a Project API key +// digest to its current binding for authentication. +type Projects interface { CreateProject(context.Context, string, string) (store.Project, error) GetProject(context.Context, string) (store.ProjectBinding, error) ListProjects(context.Context, string, int, bool) (store.ProjectPage, error) @@ -31,18 +33,7 @@ type ProjectAPIKeyRequest struct { Name string `json:"name"` } -func WithProjectAPIKeys(s ProjectAPIKeyStore, auth *DeploymentAuthenticator) Option { - return func(h *Handler) { - h.projectKeys = s - if auth != nil { - h.deploymentAuth = auth - } - } -} func (h *Handler) registerProjectAPIKeyRoutes(r chi.Router) { - if h.projectKeys == nil { - return - } r.Get("/projects", h.listProjects) r.Post("/projects", h.createProject) r.Post("/projects/{project_id}", h.renameProject) @@ -51,14 +42,8 @@ func (h *Handler) registerProjectAPIKeyRoutes(r chi.Router) { r.Post("/projects/{project_id}/keys", h.createProjectAPIKey) r.Delete("/projects/{project_id}/keys/{key_id}", h.revokeProjectAPIKey) } -func (h *Handler) resolveAdminProject(ctx context.Context, id string) (store.ProjectBinding, error) { - return h.projectKeys.GetProject(ctx, id) -} -func (h *Handler) listAdminProjects(ctx context.Context, after string, limit int, ascending bool) (store.ProjectPage, error) { - return h.projectKeys.ListProjects(ctx, after, limit, ascending) -} func (h *Handler) adminProjectScope(w http.ResponseWriter, r *http.Request) (store.ProjectBinding, bool) { - p, err := h.resolveAdminProject(r.Context(), chi.URLParam(r, "project_id")) + p, err := h.Projects.GetProject(r.Context(), chi.URLParam(r, "project_id")) if err != nil { writeStoreError(w, r, err) return store.ProjectBinding{}, false @@ -116,7 +101,7 @@ func (h *Handler) listProjects(w http.ResponseWriter, r *http.Request) { writeStoreError(w, r, err) return } - page, err := h.listAdminProjects(r.Context(), after, limit, ascending) + page, err := h.Projects.ListProjects(r.Context(), after, limit, ascending) if err != nil { writeStoreError(w, r, err) return @@ -145,7 +130,7 @@ func (h *Handler) createProject(w http.ResponseWriter, r *http.Request) { } id := uuid.NewString() setAdminAuditSource(r, id) - p, err := h.projectKeys.CreateProject(r.Context(), id, input.Name) + p, err := h.Projects.CreateProject(r.Context(), id, input.Name) if err != nil { writeStoreError(w, r, err) return @@ -177,7 +162,7 @@ func (h *Handler) renameProject(w http.ResponseWriter, r *http.Request) { writeStoreError(w, r, store.ErrInvalidInput) return } - p, err := h.projectKeys.RenameProject(r.Context(), binding.Project.ID, input.Name) + p, err := h.Projects.RenameProject(r.Context(), binding.Project.ID, input.Name) if err != nil { writeStoreError(w, r, err) return @@ -198,7 +183,7 @@ func (h *Handler) archiveProject(w http.ResponseWriter, r *http.Request) { if !ok { return } - p, err := h.projectKeys.ArchiveProject(r.Context(), binding.Project.ID) + p, err := h.Projects.ArchiveProject(r.Context(), binding.Project.ID) if err != nil { writeStoreError(w, r, err) return @@ -227,7 +212,7 @@ func (h *Handler) listProjectAPIKeys(w http.ResponseWriter, r *http.Request) { writeStoreError(w, r, err) return } - page, err := h.projectKeys.ListProjectAPIKeys(r.Context(), binding.Project.ID, after, limit, ascending) + page, err := h.Projects.ListProjectAPIKeys(r.Context(), binding.Project.ID, after, limit, ascending) if err != nil { writeStoreError(w, r, err) return @@ -259,7 +244,7 @@ func (h *Handler) createProjectAPIKey(w http.ResponseWriter, r *http.Request) { writeStoreError(w, r, store.ErrInvalidInput) return } - key, err := h.projectKeys.CreateProjectAPIKey(r.Context(), binding.Project.ID, uuid.NewString(), input.Name) + key, err := h.Projects.CreateProjectAPIKey(r.Context(), binding.Project.ID, uuid.NewString(), input.Name) if err != nil { writeStoreError(w, r, err) return @@ -282,7 +267,7 @@ func (h *Handler) revokeProjectAPIKey(w http.ResponseWriter, r *http.Request) { return } id := chi.URLParam(r, "key_id") - if err := h.projectKeys.RevokeProjectAPIKey(r.Context(), binding.Project.ID, id); err != nil { + if err := h.Projects.RevokeProjectAPIKey(r.Context(), binding.Project.ID, id); err != nil { writeStoreError(w, r, err) return } diff --git a/services/core/internal/api/project_api_keys_test.go b/services/core/internal/api/project_api_keys_test.go index ff264afc3..2293954b4 100644 --- a/services/core/internal/api/project_api_keys_test.go +++ b/services/core/internal/api/project_api_keys_test.go @@ -13,7 +13,6 @@ import ( ) type projectKeyStoreFixture struct { - ProjectAPIKeyStore binding store.ProjectAPIKeyBinding project store.ProjectBinding resolve error @@ -48,15 +47,10 @@ func projectKeyHTTP(h http.Handler, method, path, token, body string) *httptest. } func TestAdminCredentialNeverAuthenticatesPublicAPI(t *testing.T) { key := callerBinding() - auth, err := NewAuthenticator([]APIKey{key}) - if err != nil { - t.Fatal(err) - } - admin, err := NewDeploymentAuthenticator([]string{key.TokenSHA256}) - if err != nil { - t.Fatal(err) - } - h := &Handler{auth: auth, deploymentAuth: admin} + deps, fakes := testDependencies(t) + fakes.projects.resolveProjectAPIKey = projectKeys(t, key).ResolveProjectAPIKey + deps.CoreKeys = coreKeys(t, "caller") + h := &Handler{Dependencies: deps} r := httptest.NewRequest("GET", "/v1/files", nil) r.Header.Set("Authorization", "Bearer caller") _, _, ok, err := h.resolveCaller(r) @@ -66,11 +60,11 @@ func TestAdminCredentialNeverAuthenticatesPublicAPI(t *testing.T) { } func TestDatabaseResolverControlsAuthentication(t *testing.T) { p := callerBinding() - fixture, _ := NewAuthenticator([]APIKey{p}) - binding, _ := fixture.keys.ResolveProjectAPIKey(t.Context(), p.TokenSHA256) + binding, _ := projectKeys(t, p).ResolveProjectAPIKey(t.Context(), p.TokenSHA256) keys := &projectKeyStoreFixture{binding: binding} - auth, _ := NewDatabaseAuthenticator(keys) - h := &Handler{auth: auth} + deps, fakes := testDependencies(t) + fakes.projects.resolveProjectAPIKey = keys.ResolveProjectAPIKey + h := &Handler{Dependencies: deps} r := httptest.NewRequest("GET", "/v1/files", nil) r.Header.Set("Authorization", "Bearer issued-project-key") got, _, ok, err := h.resolveCaller(r) diff --git a/services/core/internal/api/resource_creation_test.go b/services/core/internal/api/resource_creation_test.go index 05048ad53..86498b662 100644 --- a/services/core/internal/api/resource_creation_test.go +++ b/services/core/internal/api/resource_creation_test.go @@ -10,9 +10,7 @@ import ( "github.com/google/uuid" ) -type resourceCreationStore struct { - ResourceStore -} +type resourceCreationStore struct{} func (*resourceCreationStore) CreateAgent(_ context.Context, tenant string, input store.CreateAgentInput) (store.SavedAgent, error) { return store.SavedAgent{ID: uuid.NewString(), TenantID: tenant, Configuration: input.Configuration, Metadata: input.Metadata}, nil @@ -23,8 +21,10 @@ func (*resourceCreationStore) CreateEnvironmentTemplate(context.Context, string, } func TestAgentAndTemplateCreationStatus(t *testing.T) { - h, recording, _ := testHandler(t) - recording.ResourceStore = &resourceCreationStore{} + s := &resourceCreationStore{} + h, _, _ := testHandler(t, func(_ *Dependencies, f *testFakes) { + f.agents.createAgent, f.environmentTemplates.createEnvironmentTemplate = s.CreateAgent, s.CreateEnvironmentTemplate + }) for _, tc := range []struct{ path, body, object string }{ {"/v1/agents", `{"model":"resource-model"}`, "agent"}, {"/v1/agents/environments/templates", `{}`, "agent.environment.template"}, diff --git a/services/core/internal/api/resource_query_test.go b/services/core/internal/api/resource_query_test.go index fc61d3d2e..09fb5453a 100644 --- a/services/core/internal/api/resource_query_test.go +++ b/services/core/internal/api/resource_query_test.go @@ -21,7 +21,6 @@ import ( // missingResourceStore reports every resource as missing, recording the tenant // each lookup used. type missingResourceStore struct { - ResourceStore tenants []string } @@ -66,23 +65,27 @@ func (s *missingResourceStore) DeleteEnvironmentTemplate(_ context.Context, tena return "", s.missing(tenant) } +// wire serves the Agent, Session and Environment template lookups from s. +func (s *missingResourceStore) wire(_ *Dependencies, f *testFakes) { + f.agents.getAgent, f.agents.deleteAgent, f.agents.updateAgent = s.GetAgent, s.DeleteAgent, s.UpdateAgent + f.sessions.getSession, f.sessions.deleteSession, f.sessions.updateSessionMetadata = s.GetSession, s.DeleteSession, s.UpdateSessionMetadata + f.environmentTemplates.getEnvironmentTemplate, f.environmentTemplates.updateEnvironmentTemplate, f.environmentTemplates.deleteEnvironmentTemplate = s.GetEnvironmentTemplate, s.UpdateEnvironmentTemplate, s.DeleteEnvironmentTemplate +} + // twoTenantHandler authenticates "test-api-key" as the owner and "foreign-key" as // another project. -func twoTenantHandler(t *testing.T, s ResourceStore, options ...Option) (http.Handler, string, string) { +func twoTenantHandler(t *testing.T, configure ...func(*Dependencies, *testFakes)) (http.Handler, string, string) { t.Helper() owner, foreign := uuid.NewString(), uuid.NewString() - auth, err := NewAuthenticator([]APIKey{ - {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "owner", TokenSHA256: runtimedevice.HashCredential("test-api-key"), TenantID: owner}, - {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "foreign", TokenSHA256: runtimedevice.HashCredential("foreign-key"), TenantID: foreign}, - }) - if err != nil { - t.Fatal(err) + deps, fakes := testDependencies(t) + fakes.projects.resolveProjectAPIKey = projectKeys(t, + APIKey{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "owner", TokenSHA256: runtimedevice.HashCredential("test-api-key"), TenantID: owner}, + APIKey{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "foreign", TokenSHA256: runtimedevice.HashCredential("foreign-key"), TenantID: foreign}, + ).ResolveProjectAPIKey + for _, c := range configure { + c(&deps, fakes) } - h, err := NewHandler(s, auth, "codex", options...) - if err != nil { - t.Fatal(err) - } - return h, owner, foreign + return newTestHandler(t, deps), owner, foreign } // Unknown query keys on single-resource routes are ignored: a missing or foreign @@ -102,7 +105,7 @@ func TestSingleResourceRoutesIgnoreUnknownQueryKeys(t *testing.T) { } { t.Run(route.method+" "+route.path, func(t *testing.T) { s := &missingResourceStore{} - h, tenant, _ := twoTenantHandler(t, s) + h, tenant, _ := twoTenantHandler(t, s.wire) var bodies []string for _, query := range []string{"", "?tenant_id=foreign&include=files&unknown=1&unknown=2"} { r := httptest.NewRequest(route.method, route.path+query, strings.NewReader(route.body)) @@ -125,7 +128,6 @@ func TestSingleResourceRoutesIgnoreUnknownQueryKeys(t *testing.T) { // missingSkillStore reports every Skill as missing and records list parameters. type missingSkillStore struct { - SkillStore tenants []string limit int hasMore bool @@ -156,9 +158,12 @@ func (s *missingSkillStore) ListSkillVersions(_ context.Context, tenant, _, _ st return store.SkillVersionPage{HasMore: s.hasMore}, nil } -func skillQueryHandler(t *testing.T, s SkillStore) (http.Handler, string) { +func skillQueryHandler(t *testing.T, s *missingSkillStore) (http.Handler, string) { t.Helper() - h, tenant, _ := twoTenantHandler(t, &missingResourceStore{}, WithSkills(s)) + h, tenant, _ := twoTenantHandler(t, func(_ *Dependencies, f *testFakes) { + f.skills.getSkill, f.skills.deleteSkill, f.skills.getSkillVersion = s.GetSkill, s.DeleteSkill, s.GetSkillVersion + f.skills.listSkills, f.skills.listSkillVersions = s.ListSkills, s.ListSkillVersions + }) return h, tenant } @@ -235,7 +240,6 @@ func TestEnvironmentFileCreateIgnoresUnknownQueryKeys(t *testing.T) { } type ownedArtifactStore struct { - SessionArtifactStore owner string tenants []string deleted int @@ -252,7 +256,7 @@ func (s *ownedArtifactStore) DeleteSessionArtifact(_ context.Context, tenant, se func TestArtifactDeletionIgnoresUnknownQueryKeys(t *testing.T) { s := &ownedArtifactStore{} - h, owner, foreign := twoTenantHandler(t, &missingResourceStore{}, WithSessionArtifacts(s)) + h, owner, foreign := twoTenantHandler(t, func(_ *Dependencies, f *testFakes) { f.artifacts.deleteSessionArtifact = s.DeleteSessionArtifact }) s.owner = owner request := func(id, key string) *httptest.ResponseRecorder { r := httptest.NewRequest(http.MethodDelete, "/v1/agents/sessions/session/artifacts/"+id+"?tenant_id="+owner+"&unknown=1", nil) @@ -275,7 +279,7 @@ func TestArtifactDeletionIgnoresUnknownQueryKeys(t *testing.T) { func TestSourceFileUploadIgnoresUnknownQueryKeys(t *testing.T) { f := &sourceFilesFixture{} - h, env := environmentFileCreateHandler(t, WithSourceFiles(f)) + h, env := environmentFileCreateHandler(t, f.wire) server := newSourceFileServer(t, h) query := "?purpose=assistants&tenant_id=" + env.environment.TenantID + "&unknown=1" // A query purpose is not a form field: the body purpose is still validated. @@ -300,7 +304,6 @@ func TestSourceFileUploadIgnoresUnknownQueryKeys(t *testing.T) { // ownedSkillStore accepts uploads and knows one owned Skill. type ownedSkillStore struct { - SkillStore owner string tenants []string defaults []bool @@ -342,7 +345,9 @@ func skillUpload(t *testing.T, include bool) ([]byte, string) { func TestSkillUploadsIgnoreUnknownQueryKeys(t *testing.T) { s := &ownedSkillStore{} - h, owner, foreign := twoTenantHandler(t, &missingResourceStore{}, WithSkills(s)) + h, owner, foreign := twoTenantHandler(t, func(_ *Dependencies, f *testFakes) { + f.skills.createSkill, f.skills.createSkillVersion = s.CreateSkill, s.CreateSkillVersion + }) s.owner = owner server := newSourceFileServer(t, h) query := "?tenant_id=" + owner + "&default=true&unknown=1" diff --git a/services/core/internal/api/routing.go b/services/core/internal/api/routing.go index e5cf3e325..5f2cef602 100644 --- a/services/core/internal/api/routing.go +++ b/services/core/internal/api/routing.go @@ -115,17 +115,14 @@ func cleanPath(p string) string { return cleaned } -// agentsResponseHeaders adds the observed official response headers to every -// response of this handler, including errors, 401, 404, 405 and SSE streams -// (HP-23/HP-24): a fresh random X-Request-Id, attached to the request log -// context next to the trace carrier, OpenAI-Version, OpenAI-Processing-Ms at -// the time headers are written, and nosniff. Core's traceparent and +// responseHeadersWithErrors adds the observed official response headers to +// every response of this handler, including errors, 401, 404, 405 and SSE +// streams (HP-23/HP-24): a fresh random X-Request-Id, attached to the request +// log context next to the trace carrier, OpenAI-Version, OpenAI-Processing-Ms +// at the time headers are written, and nosniff. Core's traceparent and // Cache-Control extensions remain. Organization and project headers are not -// reported: Core's project scope is configured, not account-derived. -func agentsResponseHeaders(next http.Handler) http.Handler { - return responseHeadersWithErrors(next, nil) -} - +// reported: Core's project scope is configured, not account-derived. report +// observes each emitted API error code. func responseHeadersWithErrors(next http.Handler, report func(string)) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { id := newRequestID() @@ -156,7 +153,7 @@ type processingTimeWriter struct { // reportAPIError observes the emitted code without reading or retaining bodies. func (w *processingTimeWriter) reportAPIError(code string) { - if !w.stamped && w.report != nil { + if !w.stamped { w.report(code) } } diff --git a/services/core/internal/api/routing_test.go b/services/core/internal/api/routing_test.go index 90b948a84..bd0d6e9c2 100644 --- a/services/core/internal/api/routing_test.go +++ b/services/core/internal/api/routing_test.go @@ -19,7 +19,6 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/internal/obs/log" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/go-chi/chi/v5" @@ -37,12 +36,8 @@ const ( var requestIDPattern = regexp.MustCompile(`^req_[0-9a-f]{32}$`) -// routingStore serves one saved Agent and records Agent lookups. Every other -// store method, including Environment executor credentials, panics through -// the nil embedded interfaces, so a test fails if a handler reaches it. +// routingStore serves one saved Agent and records Agent lookups. type routingStore struct { - ResourceStore - EnvironmentExecutorStore tenant string agent store.SavedAgent lookups, updates []string @@ -74,59 +69,34 @@ func (s *routingStore) UpdateAgent(_ context.Context, tenant, id string, input s return s.agent, nil } -// routingKeys resolves a second key for the same Project. -// Key management methods panic, so administrator handlers are traps. -type routingKeys struct { - ProjectAPIKeyStore - principal identity.Principal -} +// trapTB turns an unexpected call to a strict fake into a panic, which +// outcome reports as "handler reached". +type trapTB struct{ testing.TB } -func (k routingKeys) ResolveProjectAPIKey(_ context.Context, digest string) (store.ProjectAPIKeyBinding, error) { - if digest != runtimedevice.HashCredential(routingDerivedKey) { - return store.ProjectAPIKeyBinding{}, store.ErrNotFound - } - return store.ProjectAPIKeyBinding{Principal: k.principal}, nil -} - -// missingFiles reports every File as missing. -type missingFiles struct{ SourceFileStore } - -func (missingFiles) GetSourceFile(context.Context, string, string) (store.SourceFile, error) { - return store.SourceFile{}, store.ErrNotFound -} +func (trapTB) Fatalf(format string, args ...any) { panic(fmt.Sprintf(format, args...)) } // routingFixture returns the served handler and, for route enumeration, a -// router built from an identically configured Handler. Sandbox administration -// uses a zero Store and project API key management a trap store, which panic -// if a handler is ever reached; derived project keys resolve normally. +// router built from the same Dependencies. They answer only Agent reads and +// updates, Project key resolution, including a derived key for the same +// Project, and File lookups, which report every File as missing. Any other +// call, including sandbox administration and Project key management, panics +// if a handler is ever reached. func routingFixture(t *testing.T) (http.Handler, *chi.Mux, *routingStore) { t.Helper() tenant := uuid.NewString() - auth, err := NewAuthenticator([]APIKey{{OrganizationID: "test-org", ProjectID: "test-project", SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(routingKey), TenantID: tenant}}) - if err != nil { - t.Fatal(err) - } - admin, err := NewDeploymentAuthenticator([]string{runtimedevice.HashCredential(routingAdminKey)}) - if err != nil { - t.Fatal(err) - } + keys := projectKeys(t, APIKey{OrganizationID: "test-org", ProjectID: "test-project", SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(routingKey), TenantID: tenant}) + keys[runtimedevice.HashCredential(routingDerivedKey)] = keys[runtimedevice.HashCredential(routingKey)] s := &routingStore{tenant: tenant, agent: store.SavedAgent{ID: uuid.NewString(), TenantID: tenant, Metadata: map[string]string{}, Configuration: json.RawMessage(`{"model":"fixture"}`), CreatedAt: time.Unix(1700000000, 0), UpdatedAt: time.Unix(1700000000, 0)}} - binding, err := auth.keys.ResolveProjectAPIKey(t.Context(), runtimedevice.HashCredential(routingKey)) - if err != nil { - t.Fatal(err) - } - auth.keys.(fixtureKeyResolver)[runtimedevice.HashCredential(routingDerivedKey)] = binding - options := []Option{WithSandboxManager(&store.Store{}, admin), WithProjectAPIKeys(routingKeys{principal: binding.Principal}, admin), WithSourceFiles(missingFiles{})} - handler, err := NewHandler(s, auth, "codex", options...) - if err != nil { - t.Fatal(err) - } - h := &Handler{store: s, auth: auth, engine: "codex"} - for _, option := range options { - option(h) + deps, fakes := testDependencies(trapTB{t}) + fakes.projects.resolveProjectAPIKey = keys.ResolveProjectAPIKey + fakes.agents.getAgent, fakes.agents.listAgents, fakes.agents.updateAgent = s.GetAgent, s.ListAgents, s.UpdateAgent + fakes.files.getSourceFile = func(context.Context, string, string) (store.SourceFile, error) { + return store.SourceFile{}, store.ErrNotFound } - return handler, h.routes(), s + deps.CoreKeys = coreKeys(t, routingAdminKey) + deps.Execution, deps.Sandboxes = fakes.execution(), fakes.sandboxes() + return newTestHandler(t, deps), (&Handler{Dependencies: deps}).routes(), s } func routingHeaders(pairs ...string) http.Header { @@ -646,7 +616,7 @@ func TestAgentsResponseHeaders(t *testing.T) { // The ID is attached to the request log context, and a stream that flushes // before writing still reports its processing time. var logged string - stream := agentsResponseHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + stream := responseHeadersWithErrors(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { logged, _ = log.RequestIDFromContext(r.Context()) w.Header().Set("Content-Type", "text/event-stream") controller := http.NewResponseController(w) @@ -657,7 +627,7 @@ func TestAgentsResponseHeaders(t *testing.T) { t.Error(err) } _, _ = io.WriteString(w, ": connected\n\n") - })) + }), func(string) {}) server := httptest.NewServer(stream) defer server.Close() response, err := server.Client().Get(server.URL) diff --git a/services/core/internal/api/runtime_history.go b/services/core/internal/api/runtime_history.go index 10d4cd8a1..752ffe798 100644 --- a/services/core/internal/api/runtime_history.go +++ b/services/core/internal/api/runtime_history.go @@ -18,22 +18,17 @@ const ( runtimeHistoryRequestBudget = 15 * time.Second ) -type RuntimeHistoryService interface { +// RuntimeHistory queries durable Runtime history. Its capabilities declare +// whether this Core collects any; one that does not answers 503 +// runtime_history_unavailable. +type RuntimeHistory interface { Capabilities() runtimehistory.Capabilities QuerySession(context.Context, string, string, runtimehistory.Range) (runtimehistory.Response, error) } -func WithRuntimeHistory(service RuntimeHistoryService) Option { - return func(h *Handler) { h.runtimeHistory = service } -} - // getRuntimeHistory serves the administrator per-Session history read. func (h *Handler) getRuntimeHistory(w http.ResponseWriter, r *http.Request) { - if h.runtimeHistory == nil { - writeError(w, http.StatusServiceUnavailable, "runtime_history_unavailable", "Durable Runtime history is not configured on this service.") - return - } - capabilities := h.runtimeHistory.Capabilities() + capabilities := h.RuntimeHistory.Capabilities() if capabilities.Validate() != nil || !capabilities.Durable() { writeError(w, http.StatusServiceUnavailable, "runtime_history_unavailable", "Durable Runtime history is not configured on this service.") return @@ -46,7 +41,7 @@ func (h *Handler) getRuntimeHistory(w http.ResponseWriter, r *http.Request) { expectedSessionID := chi.URLParam(r, "session_id") ctx, cancel := context.WithTimeout(r.Context(), runtimeHistoryRequestBudget) defer cancel() - value, err := h.runtimeHistory.QuerySession(ctx, expectedTenantID, expectedSessionID, requested) + value, err := h.RuntimeHistory.QuerySession(ctx, expectedTenantID, expectedSessionID, requested) if err != nil { switch { case errors.Is(err, runtimehistory.ErrInvalidRange): diff --git a/services/core/internal/api/runtime_history_test.go b/services/core/internal/api/runtime_history_test.go index 933f474af..404ea2966 100644 --- a/services/core/internal/api/runtime_history_test.go +++ b/services/core/internal/api/runtime_history_test.go @@ -32,6 +32,13 @@ func (f *runtimeHistoryFixture) QuerySession(_ context.Context, tenant, session return f.response, f.err } +// historyWith answers Runtime history from service. +func historyWith(service *runtimeHistoryFixture) func(*Dependencies, *testFakes) { + return func(_ *Dependencies, f *testFakes) { + f.runtimeHistory.capabilities, f.runtimeHistory.querySession = service.Capabilities, service.QuerySession + } +} + func historyCapabilities(mode runtimehistory.CollectionMode) runtimehistory.Capabilities { value := runtimehistory.Capabilities{ CollectionMode: mode, Retention: 7 * 24 * time.Hour, MinimumStep: 30 * time.Second, @@ -46,7 +53,7 @@ func historyCapabilities(mode runtimehistory.CollectionMode) runtimehistory.Capa func TestRuntimeHistoryRequiresQualifiedPeriodicCollection(t *testing.T) { service := &runtimeHistoryFixture{capabilities: historyCapabilities(runtimehistory.CollectionOnRead)} - handler, _, _ := adminTestHandler(t, WithRuntimeHistory(service)) + handler, _, _ := adminTestHandler(t, historyWith(service)) response := runtimeObservationRequest(handler, adminSessionsPath+uuid.NewString()+"/runtime-history?start=1&end=2") if response.Code != http.StatusServiceUnavailable || service.calls != 0 { t.Fatalf("on-read history reached query service: %d calls=%d body=%s", response.Code, service.calls, response.Body) @@ -56,7 +63,7 @@ func TestRuntimeHistoryRequiresQualifiedPeriodicCollection(t *testing.T) { func TestRuntimeHistoryFailsClosedForMalformedCapabilities(t *testing.T) { service := &runtimeHistoryFixture{capabilities: historyCapabilities(runtimehistory.CollectionPeriodic)} service.capabilities.Retention = 0 - handler, _, _ := adminTestHandler(t, WithRuntimeHistory(service)) + handler, _, _ := adminTestHandler(t, historyWith(service)) response := runtimeObservationRequest(handler, adminSessionsPath+uuid.NewString()+"/runtime-history?start=1&end=2") if response.Code != http.StatusServiceUnavailable || service.calls != 0 { t.Fatalf("malformed capabilities reached query service: %d calls=%d body=%s", response.Code, service.calls, response.Body) @@ -82,7 +89,7 @@ func TestRuntimeHistoryRouteBindsAuthenticatedSessionAndPreservesCoverage(t *tes CPUUtilizationRatio: &zeroRatio, CPUCapacityCores: &capacity, MemoryUsageBytes: &zeroMemory, MemoryLimitBytes: &limit, } service := &runtimeHistoryFixture{capabilities: historyCapabilities(runtimehistory.CollectionPeriodic)} - handler, _, tenant := adminTestHandler(t, WithRuntimeHistory(service)) + handler, _, tenant := adminTestHandler(t, historyWith(service)) scope.TenantID = tenant service.response = runtimehistory.Response{ Capabilities: service.capabilities, Scope: scope, @@ -110,7 +117,7 @@ func TestRuntimeHistoryRouteBindsAuthenticatedSessionAndPreservesCoverage(t *tes func TestRuntimeHistoryRejectsUnsafeQueriesAndFailures(t *testing.T) { service := &runtimeHistoryFixture{capabilities: historyCapabilities(runtimehistory.CollectionPeriodic)} - handler, _, _ := adminTestHandler(t, WithRuntimeHistory(service)) + handler, _, _ := adminTestHandler(t, historyWith(service)) sessionID := uuid.NewString() for _, query := range []string{ "", "?start=1", "?start=2&end=1", "?start=x&end=2", "?start=1&end=2&provider=docker", "?start=1&start=1&end=2", "?start=1&end=2&max_points=x", "?start=1&end=2&max_points=1", "?start=1&end=2&max_points=1001", "?start=1&end=90002", "?start=1&end=9223372036854775807", @@ -155,7 +162,7 @@ func TestRuntimeHistoryRejectsMismatchedServiceResponses(t *testing.T) { start := now.Add(-time.Hour) sessionID := uuid.NewString() service := &runtimeHistoryFixture{capabilities: historyCapabilities(runtimehistory.CollectionPeriodic)} - handler, _, tenant := adminTestHandler(t, WithRuntimeHistory(service)) + handler, _, tenant := adminTestHandler(t, historyWith(service)) base := runtimehistory.Response{ Capabilities: service.capabilities, Scope: runtimehistory.Scope{ @@ -188,7 +195,7 @@ func TestRuntimeHistoryDefaultPointBudgetRespectsCapabilities(t *testing.T) { capabilities := historyCapabilities(runtimehistory.CollectionPeriodic) capabilities.MaximumPoints = 60 service := &runtimeHistoryFixture{capabilities: capabilities, err: runtimehistory.ErrUnavailable} - handler, _, _ := adminTestHandler(t, WithRuntimeHistory(service)) + handler, _, _ := adminTestHandler(t, historyWith(service)) now := time.Now().UTC().Truncate(time.Second) response := runtimeObservationRequest(handler, adminSessionsPath+uuid.NewString()+"/runtime-history?start="+timeString(now.Add(-time.Hour))+"&end="+timeString(now)) if response.Code != http.StatusServiceUnavailable || service.calls != 1 || service.requested.MaxPoints != 60 { diff --git a/services/core/internal/api/runtime_observations.go b/services/core/internal/api/runtime_observations.go index b6a59585b..aab0b20be 100644 --- a/services/core/internal/api/runtime_observations.go +++ b/services/core/internal/api/runtime_observations.go @@ -20,7 +20,8 @@ const ( var runtimeProviderTypePattern = regexp.MustCompile(`^[a-z][a-z0-9_]{0,31}$`) -type RuntimeObservationService interface { +// RuntimeObservations samples the current Runtime of one or more Sessions. +type RuntimeObservations interface { ObserveSession(context.Context, string, string) (runtimeobs.Observation, error) ObserveSessions(context.Context, []runtimeobs.SessionIdentity, runtimeobs.PageOptions) ([]runtimeobs.Observation, []error) } @@ -37,23 +38,15 @@ func firstRuntimeObservationError(errs []error) error { return nil } -func WithRuntimeObservations(service RuntimeObservationService) Option { - return func(h *Handler) { h.runtimeObservations = service } -} - // getRuntimeObservation serves the administrator per-Session observation read. func (h *Handler) getRuntimeObservation(w http.ResponseWriter, r *http.Request) { if len(r.URL.Query()) != 0 { writeError(w, http.StatusBadRequest, "unsupported_parameter", "Runtime observation retrieval does not accept query parameters.") return } - if h.runtimeObservations == nil { - writeError(w, http.StatusServiceUnavailable, "execution_unavailable", "Runtime observation is not configured on this service.") - return - } ctx, cancel := context.WithTimeout(r.Context(), runtimeObservationSourceBudget) defer cancel() - observation, err := h.runtimeObservations.ObserveSession(ctx, tenantID(r), chi.URLParam(r, "session_id")) + observation, err := h.RuntimeObservations.ObserveSession(ctx, tenantID(r), chi.URLParam(r, "session_id")) if err != nil { writeStoreError(w, r, err) return diff --git a/services/core/internal/api/runtime_observations_test.go b/services/core/internal/api/runtime_observations_test.go index 235a66607..227db91ac 100644 --- a/services/core/internal/api/runtime_observations_test.go +++ b/services/core/internal/api/runtime_observations_test.go @@ -57,6 +57,13 @@ func (r runtimeObservationPageRecorder) ObserveSessions(ctx context.Context, ses return observeEach(ctx, sessions, r.runtimeObservationServiceFunc) } +// observeWith answers runtime observations from service. +func observeWith(service RuntimeObservations) func(*Dependencies, *testFakes) { + return func(_ *Dependencies, f *testFakes) { + f.runtimeObservations.observeSession, f.runtimeObservations.observeSessions = service.ObserveSession, service.ObserveSessions + } +} + func runtimeObservationRequest(handler http.Handler, path string) *httptest.ResponseRecorder { request := httptest.NewRequest(http.MethodGet, path, nil) request.Header.Set("Authorization", "Bearer admin") @@ -71,7 +78,7 @@ func TestRuntimeObservationRoutesUseSessionIdentityAndExactNullability(t *testin service := runtimeObservationFixture{values: map[string]runtimeobs.Observation{ sessionID: {Target: runtimeobs.Target{SessionID: sessionID, Mode: runtimeobs.ModeNone}, Status: runtimeobs.StatusUnsupported, Reason: "runtime_mode_not_observable", ResolvedAt: now}, }} - handler, _, _ := adminTestHandler(t, WithRuntimeObservations(service)) + handler, _, _ := adminTestHandler(t, observeWith(service)) response := runtimeObservationRequest(handler, adminSessionsPath+sessionID+"/runtime-observation") if response.Code != http.StatusOK { @@ -83,19 +90,13 @@ func TestRuntimeObservationRoutesUseSessionIdentityAndExactNullability(t *testin } } -func TestRuntimeObservationRoutesRequireConfiguredServiceAndRejectQueries(t *testing.T) { - handler, _, _ := adminTestHandler(t) - missing := runtimeObservationRequest(handler, adminSessionsPath+uuid.NewString()+"/runtime-observation") - if missing.Code != http.StatusServiceUnavailable { - t.Fatalf("unconfigured service returned %d: %s", missing.Code, missing.Body) - } - +func TestRuntimeObservationRoutesRejectQueries(t *testing.T) { sessionID := uuid.NewString() now := time.Now().UTC() service := runtimeObservationFixture{values: map[string]runtimeobs.Observation{ sessionID: {Target: runtimeobs.Target{SessionID: sessionID, Mode: runtimeobs.ModeNone}, Status: runtimeobs.StatusUnsupported, Reason: "runtime_mode_not_observable", ResolvedAt: now}, }} - handler, _, _ = adminTestHandler(t, WithRuntimeObservations(service)) + handler, _, _ := adminTestHandler(t, observeWith(service)) invalid := runtimeObservationRequest(handler, adminSessionsPath+sessionID+"/runtime-observation?provider=docker") if invalid.Code != http.StatusBadRequest { t.Fatalf("unsupported query returned %d: %s", invalid.Code, invalid.Body) diff --git a/services/core/internal/api/sandbox_configuration_discovery.go b/services/core/internal/api/sandbox_configuration_discovery.go index 6971cb5ec..ce69c15c6 100644 --- a/services/core/internal/api/sandbox_configuration_discovery.go +++ b/services/core/internal/api/sandbox_configuration_discovery.go @@ -11,8 +11,10 @@ import ( "github.com/go-chi/chi/v5" ) -func WithSandboxConfigurationDiscovery(discover func(context.Context, string, sandbox.ConfigurationDiscoveryInput) (json.RawMessage, error)) Option { - return func(h *Handler) { h.sandboxConfigurationDiscover = discover } +// ConfigurationDiscovery asks a Sandbox Provider which configuration values +// its credential can use. +type ConfigurationDiscovery interface { + DiscoverConfiguration(ctx context.Context, provider string, input sandbox.ConfigurationDiscoveryInput) (json.RawMessage, error) } // @Summary Discover sandbox provider configuration @@ -36,13 +38,9 @@ func (h *Handler) discoverSandboxConfiguration(w http.ResponseWriter, r *http.Re writeStoreError(w, r, store.ErrInvalidInput) return } - if h.sandboxConfigurationDiscover == nil { - writeStoreError(w, r, store.ErrSandboxDeploymentConflict) - return - } ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) defer cancel() - result, err := h.sandboxConfigurationDiscover(ctx, chi.URLParam(r, "provider"), input) + result, err := h.Sandboxes.ConfigurationDiscovery.DiscoverConfiguration(ctx, chi.URLParam(r, "provider"), input) if err != nil { writeStoreError(w, r, err) return diff --git a/services/core/internal/api/sandbox_configuration_discovery_test.go b/services/core/internal/api/sandbox_configuration_discovery_test.go index 96269f057..4111a5bf7 100644 --- a/services/core/internal/api/sandbox_configuration_discovery_test.go +++ b/services/core/internal/api/sandbox_configuration_discovery_test.go @@ -8,14 +8,11 @@ import ( "strings" "testing" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) func TestSandboxE2BDiscoveryAuthenticationAndCredentialPrivacy(t *testing.T) { - project, _ := NewAuthenticator([]APIKey{callerBinding()}) - admin, _ := NewDeploymentAuthenticator([]string{runtimedevice.HashCredential("administrator")}) + deps, fakes := sandboxFakes(t) calls := 0 discover := func(ctx context.Context, kind string, input sandbox.ConfigurationDiscoveryInput) (json.RawMessage, error) { calls++ @@ -33,10 +30,8 @@ func TestSandboxE2BDiscoveryAuthenticationAndCredentialPrivacy(t *testing.T) { } return json.RawMessage(`{"templates":[]}`), nil } - h, err := NewHandler(&recordingStore{}, project, "codex", WithSandboxManager(&store.Store{}, admin), WithSandboxConfigurationDiscovery(discover)) - if err != nil { - t.Fatal(err) - } + fakes.configurationDiscovery.discoverConfiguration = discover + h := newTestHandler(t, deps) for _, tc := range []struct { token, path, body string status int diff --git a/services/core/internal/api/sandbox_deployment_changes_test.go b/services/core/internal/api/sandbox_deployment_changes_test.go index fb5fe1e18..b1a2fe445 100644 --- a/services/core/internal/api/sandbox_deployment_changes_test.go +++ b/services/core/internal/api/sandbox_deployment_changes_test.go @@ -9,13 +9,11 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/e2b" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) func TestSandboxDeploymentChangesAuthenticateAndDecode(t *testing.T) { - project, _ := NewAuthenticator([]APIKey{callerBinding()}) - admin, _ := NewDeploymentAuthenticator([]string{runtimedevice.HashCredential("administrator")}) + deps, fakes := sandboxFakes(t) updates, resets := 0, 0 update := func(_ context.Context, in store.SandboxDeploymentUpdateRequest) (store.RuntimeDeploymentView, error) { updates++ @@ -31,12 +29,11 @@ func TestSandboxDeploymentChangesAuthenticateAndDecode(t *testing.T) { } return store.RuntimeDeploymentView{Reset: &store.SandboxResetView{Clear: in.Clear}}, nil } - h, err := NewHandler(&recordingStore{}, project, "codex", WithSandboxManager(&store.Store{}, admin), WithSandboxDeploymentChanges(update, maintain, func(context.Context, uint64) (store.RuntimeDeploymentView, error) { + fakes.deploymentChanges.updateSandboxDeployment, fakes.deploymentChanges.startSandboxReset = update, maintain + fakes.deploymentChanges.cancelSandboxReset = func(context.Context, uint64) (store.RuntimeDeploymentView, error) { return store.RuntimeDeploymentView{}, nil - })) - if err != nil { - t.Fatal(err) } + h := newTestHandler(t, deps) const selection = `{"provider":"e2b","expected_generation":2,"credential":{"api_key":"synthetic-private-key"},"configuration":{"template":"qualified:build"}}` for _, tc := range []struct { method, path, token, body string @@ -73,23 +70,6 @@ func TestSandboxDeploymentChangesAuthenticateAndDecode(t *testing.T) { } } -func TestSandboxDeploymentChangesUnavailableWithoutOwner(t *testing.T) { - h := &Handler{} - for _, tc := range []struct { - body string - handler http.HandlerFunc - }{ - {`{"provider":"docker","expected_generation":1}`, h.updateSandboxDeployment}, - {`{"clear":"auto","expected_generation":1}`, h.startSandboxReset}, - } { - w := httptest.NewRecorder() - tc.handler(w, httptest.NewRequest("PUT", "/", strings.NewReader(tc.body))) - if w.Code != http.StatusConflict { - t.Fatal(w.Code) - } - } -} - func TestSandboxMutationErrorsExposeOnlyTypedCoreFacts(t *testing.T) { for _, tc := range []struct { err error diff --git a/services/core/internal/api/sandbox_deployment_setup.go b/services/core/internal/api/sandbox_deployment_setup.go index bd68951a9..15ed035d5 100644 --- a/services/core/internal/api/sandbox_deployment_setup.go +++ b/services/core/internal/api/sandbox_deployment_setup.go @@ -36,16 +36,13 @@ func (v SandboxDeploymentInput) request() (store.SandboxDeploymentSetupRequest, return store.SandboxDeploymentSetupRequest{ExpectedGeneration: *v.ExpectedGeneration, Provider: v.Provider, DeploymentSpec: sandbox.DeploymentSpec{Resources: v.Resources, Runtime: v.Runtime}, Configuration: c}, nil } -func WithSandboxDeploymentSetup(initialize func(context.Context, store.SandboxDeploymentSetupRequest) (store.RuntimeDeploymentView, error)) Option { - return func(h *Handler) { h.sandboxSetup = initialize } -} - -func WithSandboxDeploymentChanges( - update func(context.Context, store.SandboxDeploymentUpdateRequest) (store.RuntimeDeploymentView, error), - reset func(context.Context, store.SandboxResetRequest) (store.RuntimeDeploymentView, error), - cancel func(context.Context, uint64) (store.RuntimeDeploymentView, error), -) Option { - return func(h *Handler) { h.sandboxUpdate = update; h.sandboxReset = reset; h.sandboxResetCancel = cancel } +// DeploymentChanges sets up, updates and resets the sandbox deployment through +// the execution owner. +type DeploymentChanges interface { + InitializeSandboxDeployment(context.Context, store.SandboxDeploymentSetupRequest) (store.RuntimeDeploymentView, error) + UpdateSandboxDeployment(context.Context, store.SandboxDeploymentUpdateRequest) (store.RuntimeDeploymentView, error) + StartSandboxReset(context.Context, store.SandboxResetRequest) (store.RuntimeDeploymentView, error) + CancelSandboxReset(context.Context, uint64) (store.RuntimeDeploymentView, error) } // @Summary Initialize the deployment sandbox provider @@ -68,16 +65,12 @@ func (h *Handler) initializeSandboxDeployment(w http.ResponseWriter, r *http.Req writeStoreError(w, r, store.ErrInvalidInput) return } - if h.sandboxSetup == nil { - writeStoreError(w, r, store.ErrSandboxDeploymentConflict) - return - } selection, err := input.request() if err != nil { writeStoreError(w, r, err) return } - result, err := h.sandboxSetup(r.Context(), selection) + result, err := h.Sandboxes.DeploymentChanges.InitializeSandboxDeployment(r.Context(), selection) if err != nil { writeStoreError(w, r, err) return @@ -105,16 +98,12 @@ func (h *Handler) updateSandboxDeployment(w http.ResponseWriter, r *http.Request writeStoreError(w, r, store.ErrInvalidInput) return } - if h.sandboxUpdate == nil { - writeStoreError(w, r, store.ErrSandboxDeploymentConflict) - return - } selection, err := input.request() if err != nil { writeStoreError(w, r, err) return } - result, err := h.sandboxUpdate(r.Context(), store.SandboxDeploymentUpdateRequest{SandboxDeploymentSetupRequest: selection, ExpectedGeneration: *input.ExpectedGeneration}) + result, err := h.Sandboxes.DeploymentChanges.UpdateSandboxDeployment(r.Context(), store.SandboxDeploymentUpdateRequest{SandboxDeploymentSetupRequest: selection, ExpectedGeneration: *input.ExpectedGeneration}) if err != nil { writeStoreError(w, r, err) return @@ -160,12 +149,8 @@ func (h *Handler) startSandboxReset(w http.ResponseWriter, r *http.Request) { writeCoreError(w, http.StatusBadRequest, "invalid_request_error", "deadline_seconds applies only to auto and must be between 300 and 86400.", CoreErrorDetails{"min": CoreErrorNumber(300), "max": CoreErrorNumber(86400)}, "deadline_seconds") return } - if h.sandboxReset == nil { - writeStoreError(w, r, store.ErrSandboxDeploymentConflict) - return - } setAdminAuditSource(r, "") - result, err := h.sandboxReset(r.Context(), store.SandboxResetRequest{ExpectedGeneration: *input.ExpectedGeneration, Clear: input.Clear, DeadlineSeconds: input.DeadlineSeconds}) + result, err := h.Sandboxes.DeploymentChanges.StartSandboxReset(r.Context(), store.SandboxResetRequest{ExpectedGeneration: *input.ExpectedGeneration, Clear: input.Clear, DeadlineSeconds: input.DeadlineSeconds}) if err != nil { writeStoreError(w, r, err) return @@ -188,12 +173,8 @@ func (h *Handler) cancelSandboxReset(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusBadRequest, "invalid_request_error", "A single current expected_generation is required.", "expected_generation") return } - if h.sandboxResetCancel == nil { - writeStoreError(w, r, store.ErrSandboxDeploymentConflict) - return - } setAdminAuditSource(r, "") - result, err := h.sandboxResetCancel(r.Context(), query) + result, err := h.Sandboxes.DeploymentChanges.CancelSandboxReset(r.Context(), query) if err != nil { writeStoreError(w, r, err) return diff --git a/services/core/internal/api/sandbox_deployment_setup_test.go b/services/core/internal/api/sandbox_deployment_setup_test.go index 21656b76b..60cf5261f 100644 --- a/services/core/internal/api/sandbox_deployment_setup_test.go +++ b/services/core/internal/api/sandbox_deployment_setup_test.go @@ -7,13 +7,11 @@ import ( "strings" "testing" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) func TestSandboxDeploymentSetupRequiresAdministratorAndStrictBody(t *testing.T) { - project, _ := NewAuthenticator([]APIKey{callerBinding()}) - admin, _ := NewDeploymentAuthenticator([]string{runtimedevice.HashCredential("administrator")}) + deps, fakes := sandboxFakes(t) calls := 0 initialize := func(_ context.Context, input store.SandboxDeploymentSetupRequest) (store.RuntimeDeploymentView, error) { calls++ @@ -22,10 +20,8 @@ func TestSandboxDeploymentSetupRequiresAdministratorAndStrictBody(t *testing.T) } return store.RuntimeDeploymentView{Provider: input.Provider}, nil } - h, err := NewHandler(&recordingStore{}, project, "codex", WithSandboxManager(&store.Store{}, admin), WithSandboxDeploymentSetup(initialize)) - if err != nil { - t.Fatal(err) - } + fakes.deploymentChanges.initializeSandboxDeployment = initialize + h := newTestHandler(t, deps) for _, test := range []struct { token, body string status, calls int @@ -48,13 +44,3 @@ func TestSandboxDeploymentSetupRequiresAdministratorAndStrictBody(t *testing.T) } } } - -func TestSandboxDeploymentSetupFileModeReturnsConflict(t *testing.T) { - h := &Handler{} - request := httptest.NewRequest(http.MethodPost, "/core/v1/sandbox/deployment", strings.NewReader(`{"provider":"docker","expected_generation":0}`)) - result := httptest.NewRecorder() - h.initializeSandboxDeployment(result, request) - if result.Code != http.StatusConflict || !strings.Contains(result.Body.String(), "sandbox_deployment_conflict") { - t.Fatal(result.Code, result.Body.String()) - } -} diff --git a/services/core/internal/api/sandbox_manager.go b/services/core/internal/api/sandbox_manager.go index 6ed54d5cf..259fe9bfa 100644 --- a/services/core/internal/api/sandbox_manager.go +++ b/services/core/internal/api/sandbox_manager.go @@ -1,6 +1,7 @@ package api import ( + "context" "net/http" "time" @@ -31,14 +32,25 @@ type SandboxEnrollmentTokenRequest struct { MaxRetained *int `json:"max_retained,omitempty"` } -func WithSandboxManager(s *store.Store, auth *DeploymentAuthenticator) Option { - return func(h *Handler) { h.sandboxStore = s; h.deploymentAuth = auth } +// Deployment reads the sandbox deployment and manages its nodes: enrollment, +// identity, generation configuration, capacity and removal. +type Deployment interface { + GetRuntimeDeployment(context.Context) (store.RuntimeDeploymentView, error) + ListRuntimeNodes(context.Context) ([]store.RuntimeNode, error) + GetRuntimeNodeDetail(context.Context, string, string) (store.RuntimeNodeDetail, error) + UpdateRuntimeNode(context.Context, string, store.RuntimeNodeUpdate) error + RemoveRuntimeNode(context.Context, string) error + ListNodeRuntimeAllocations(context.Context, string) ([]store.RuntimeNodeAllocation, error) + CreateRuntimeEnrollment(context.Context, store.RuntimeNodeCapacity) (store.RuntimeNodeEnrollmentToken, error) + EnrollRuntimeNode(context.Context, string, store.RuntimeNodeEnrollment) (store.RuntimeNodeIdentity, error) + RuntimeNodeGenerationConfiguration(context.Context, string, string, uint64) (store.RuntimeNodeConfiguration, error) + RuntimeNodeStatus(context.Context, string, string) (store.RuntimeNodeStatus, error) } // registerSandboxNodeRoutes serves node machine connections. They authenticate // with an enrollment token or node credential, never the Core key. func (h *Handler) registerSandboxNodeRoutes(r chi.Router) { - if h.sandboxStore == nil { + if h.Sandboxes == nil { return } r.Post("/api/v1/sandbox-node/enroll", h.enrollSandboxNode) @@ -49,7 +61,7 @@ func (h *Handler) registerSandboxNodeRoutes(r chi.Router) { // registerSandboxManagerRoutes adds sandbox deployment and node administration // to the Core-key-authenticated /core/v1 router. func (h *Handler) registerSandboxManagerRoutes(r chi.Router) { - if h.sandboxStore == nil { + if h.Sandboxes == nil { return } r.Get("/sandbox/deployment", h.sandboxDeployment) @@ -75,7 +87,7 @@ func (h *Handler) registerSandboxManagerRoutes(r chi.Router) { // @Failure 400,401,404,409,500,503 {object} CoreErrorResponse // @Router /core/v1/sandbox/deployment [get] func (h *Handler) sandboxDeployment(w http.ResponseWriter, r *http.Request) { - value, err := h.sandboxStore.GetRuntimeDeployment(r.Context()) + value, err := h.Sandboxes.Deployment.GetRuntimeDeployment(r.Context()) if err != nil { writeStoreError(w, r, err) return @@ -92,7 +104,7 @@ func (h *Handler) sandboxDeployment(w http.ResponseWriter, r *http.Request) { // @Failure 400,401,404,409,500,503 {object} CoreErrorResponse // @Router /core/v1/sandbox/nodes [get] func (h *Handler) sandboxNodes(w http.ResponseWriter, r *http.Request) { - value, err := h.sandboxStore.ListRuntimeNodes(r.Context()) + value, err := h.Sandboxes.Deployment.ListRuntimeNodes(r.Context()) if err != nil { writeStoreError(w, r, err) return @@ -122,7 +134,7 @@ func (h *Handler) updateSandboxNode(w http.ResponseWriter, r *http.Request) { return } id := chi.URLParam(r, "node_id") - if err := h.sandboxStore.UpdateRuntimeNode(r.Context(), id, input); err != nil { + if err := h.Sandboxes.Deployment.UpdateRuntimeNode(r.Context(), id, input); err != nil { writeStoreError(w, r, err) return } @@ -140,7 +152,7 @@ func (h *Handler) updateSandboxNode(w http.ResponseWriter, r *http.Request) { // @Router /core/v1/sandbox/nodes/{node_id} [delete] func (h *Handler) removeSandboxNode(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "node_id") - if err := h.sandboxStore.RemoveRuntimeNode(r.Context(), id); err != nil { + if err := h.Sandboxes.Deployment.RemoveRuntimeNode(r.Context(), id); err != nil { writeStoreError(w, r, err) return } @@ -157,7 +169,7 @@ func (h *Handler) removeSandboxNode(w http.ResponseWriter, r *http.Request) { // @Failure 400,401,404,409,500,503 {object} CoreErrorResponse // @Router /core/v1/sandbox/nodes/{node_id}/allocations [get] func (h *Handler) sandboxAllocations(w http.ResponseWriter, r *http.Request) { - value, err := h.sandboxStore.ListNodeRuntimeAllocations(r.Context(), chi.URLParam(r, "node_id")) + value, err := h.Sandboxes.Deployment.ListNodeRuntimeAllocations(r.Context(), chi.URLParam(r, "node_id")) if err != nil { writeStoreError(w, r, err) return @@ -192,7 +204,7 @@ func (h *Handler) createSandboxEnrollment(w http.ResponseWriter, r *http.Request if input.MaxRetained != nil { capacity.MaxRetained = *input.MaxRetained } - enrollment, err := h.sandboxStore.CreateRuntimeEnrollment(r.Context(), capacity) + enrollment, err := h.Sandboxes.Deployment.CreateRuntimeEnrollment(r.Context(), capacity) if err != nil { writeStoreError(w, r, err) return @@ -229,7 +241,7 @@ func (h *Handler) enrollSandboxNode(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusBadRequest, "invalid_request_error", "Enrollment requires core_url, the Core origin this node uses. Install the node with this Core's node installer.", "core_url") return } - value, err := h.sandboxStore.EnrollRuntimeNode(r.Context(), token, input) + value, err := h.Sandboxes.Deployment.EnrollRuntimeNode(r.Context(), token, input) if err != nil { writeStoreError(w, r, err) return @@ -257,7 +269,7 @@ func (h *Handler) sandboxNodeIdentity(w http.ResponseWriter, r *http.Request) { writeStoreError(w, r, store.ErrInvalidInput) return } - value, err := h.sandboxStore.RuntimeNodeStatus(r.Context(), ids[0], token) + value, err := h.Sandboxes.Deployment.RuntimeNodeStatus(r.Context(), ids[0], token) if err != nil { writeStoreError(w, r, err) return diff --git a/services/core/internal/api/sandbox_manager_test.go b/services/core/internal/api/sandbox_manager_test.go index bc9cfd042..a78e37370 100644 --- a/services/core/internal/api/sandbox_manager_test.go +++ b/services/core/internal/api/sandbox_manager_test.go @@ -1,6 +1,7 @@ package api import ( + "context" "net/http" "net/http/httptest" "strings" @@ -10,19 +11,20 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) +// sandboxFakes authenticates callerBinding() as a Project key and +// "administrator" as the Core key, with Execution and Sandboxes enabled. +func sandboxFakes(t testing.TB) (Dependencies, *testFakes) { + t.Helper() + deps, fakes := testDependencies(t) + fakes.projects.resolveProjectAPIKey = projectKeys(t, callerBinding()).ResolveProjectAPIKey + deps.CoreKeys = coreKeys(t, "administrator") + deps.Execution, deps.Sandboxes = fakes.execution(), fakes.sandboxes() + return deps, fakes +} + func TestSandboxAdministratorIsSeparateFromProject(t *testing.T) { - project, err := NewAuthenticator([]APIKey{callerBinding()}) - if err != nil { - t.Fatal(err) - } - admin, err := NewDeploymentAuthenticator([]string{runtimedevice.HashCredential("administrator")}) - if err != nil { - t.Fatal(err) - } - h, err := NewHandler(&recordingStore{}, project, "codex", WithSandboxManager(&store.Store{}, admin)) - if err != nil { - t.Fatal(err) - } + deps, _ := sandboxFakes(t) + h := newTestHandler(t, deps) for _, path := range []string{"/core/v1/sandbox/nodes", "/core/v1/sandbox/deployment", "/core/v1/sandbox/nodes/node/allocations", "/core/v1/sandbox/enrollment-tokens"} { request := httptest.NewRequest(http.MethodGet, path, nil) if strings.HasSuffix(path, "enrollment-tokens") { @@ -43,11 +45,8 @@ func TestSandboxAdministratorIsSeparateFromProject(t *testing.T) { if result.Code != http.StatusUnauthorized { t.Fatal("admin key gained project authority", result.Code) } - reused, _ := NewDeploymentAuthenticator([]string{runtimedevice.HashCredential("caller")}) - collided, err := NewHandler(&recordingStore{}, project, "codex", WithSandboxManager(&store.Store{}, reused)) - if err != nil { - t.Fatal(err) - } + deps.CoreKeys = coreKeys(t, "caller") + collided := newTestHandler(t, deps) request.Header.Set("Authorization", "Bearer caller") result = httptest.NewRecorder() collided.ServeHTTP(result, request) @@ -79,13 +78,20 @@ func TestSandboxLocalNodeRemovalExplainsDeploymentBinding(t *testing.T) { } } +// storeCapacity rejects max_active outside the store's node capacity bounds. +func storeCapacity(active int) error { + if active < 1 || active > 1000000 { + return &store.AdminValidationError{Code: "invalid_node_capacity", Param: "max_active"} + } + return nil +} + func TestSandboxEnrollmentCapacityIsAdministratorOnly(t *testing.T) { - project, _ := NewAuthenticator([]APIKey{callerBinding()}) - admin, _ := NewDeploymentAuthenticator([]string{runtimedevice.HashCredential("administrator")}) - h, err := NewHandler(&recordingStore{}, project, "codex", WithSandboxManager(&store.Store{}, admin)) - if err != nil { - t.Fatal(err) + deps, fakes := sandboxFakes(t) + fakes.deployment.createRuntimeEnrollment = func(_ context.Context, capacity store.RuntimeNodeCapacity) (store.RuntimeNodeEnrollmentToken, error) { + return store.RuntimeNodeEnrollmentToken{}, storeCapacity(capacity.MaxActive) } + h := newTestHandler(t, deps) for _, test := range []struct{ path, token, body string }{ {"/core/v1/sandbox/enrollment-tokens", "administrator", `{"max_active":0}`}, {"/api/v1/sandbox-node/enroll", "one-use", `{"max_active":100}`}, @@ -114,12 +120,8 @@ func TestSandboxEnrollmentCapacityIsAdministratorOnly(t *testing.T) { // Enrollment names the Core address the node uses; without it the request fails // before any token is read. func TestSandboxNodeEnrollmentRequiresCoreURL(t *testing.T) { - project, _ := NewAuthenticator([]APIKey{callerBinding()}) - admin, _ := NewDeploymentAuthenticator([]string{runtimedevice.HashCredential("administrator")}) - h, err := NewHandler(&recordingStore{}, project, "codex", WithSandboxManager(&store.Store{}, admin)) - if err != nil { - t.Fatal(err) - } + deps, _ := sandboxFakes(t) + h := newTestHandler(t, deps) request := httptest.NewRequest(http.MethodPost, "/api/v1/sandbox-node/enroll", strings.NewReader(`{"node_id":"node","name":"node"}`)) request.Header.Set("Authorization", "Bearer one-use") response := httptest.NewRecorder() diff --git a/services/core/internal/api/sandbox_node_configuration.go b/services/core/internal/api/sandbox_node_configuration.go index dee7eedeb..b13a7d4d8 100644 --- a/services/core/internal/api/sandbox_node_configuration.go +++ b/services/core/internal/api/sandbox_node_configuration.go @@ -36,7 +36,7 @@ func (h *Handler) sandboxNodeConfiguration(w http.ResponseWriter, r *http.Reques return } } - value, err := h.sandboxStore.RuntimeNodeGenerationConfiguration(r.Context(), r.Header.Get("X-OAC-Node-ID"), token, generation) + value, err := h.Sandboxes.Deployment.RuntimeNodeGenerationConfiguration(r.Context(), r.Header.Get("X-OAC-Node-ID"), token, generation) if err != nil { writeStoreError(w, r, err) return diff --git a/services/core/internal/api/sandbox_node_detail.go b/services/core/internal/api/sandbox_node_detail.go index f846efa09..257d20951 100644 --- a/services/core/internal/api/sandbox_node_detail.go +++ b/services/core/internal/api/sandbox_node_detail.go @@ -32,7 +32,7 @@ func (h *Handler) sandboxNodeDetail(w http.ResponseWriter, r *http.Request) { } ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second) defer cancel() - value, err := h.sandboxStore.GetRuntimeNodeDetail(ctx, chi.URLParam(r, "node_id"), name) + value, err := h.Sandboxes.Deployment.GetRuntimeNodeDetail(ctx, chi.URLParam(r, "node_id"), name) if err != nil { writeStoreError(w, r, err) return diff --git a/services/core/internal/api/sandbox_node_detail_test.go b/services/core/internal/api/sandbox_node_detail_test.go index 5722b3423..dead3a9a2 100644 --- a/services/core/internal/api/sandbox_node_detail_test.go +++ b/services/core/internal/api/sandbox_node_detail_test.go @@ -1,18 +1,19 @@ package api import ( - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" + "context" "testing" + + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) func TestSandboxNodeDetailValidationAndAuthentication(t *testing.T) { - auth, _ := NewAuthenticator([]APIKey{callerBinding()}) - admin, _ := NewDeploymentAuthenticator([]string{runtimedevice.HashCredential("administrator")}) - h, err := NewHandler(&recordingStore{}, auth, "codex", WithSandboxManager(&store.Store{}, admin)) - if err != nil { - t.Fatal(err) + deps, fakes := sandboxFakes(t) + // The store rejects an unknown range or a malformed node ID. + fakes.deployment.getRuntimeNodeDetail = func(context.Context, string, string) (store.RuntimeNodeDetail, error) { + return store.RuntimeNodeDetail{}, store.ErrInvalidInput } + h := newTestHandler(t, deps) path := "/core/v1/sandbox/nodes/11111111-1111-4111-8111-111111111111" for _, token := range []string{"", "caller", "enrollment", "node"} { if w := projectKeyHTTP(h, "GET", path, token, ""); w.Code != 401 { diff --git a/services/core/internal/api/sandbox_selector_test.go b/services/core/internal/api/sandbox_selector_test.go index dbe6ee3a5..9c225c9bb 100644 --- a/services/core/internal/api/sandbox_selector_test.go +++ b/services/core/internal/api/sandbox_selector_test.go @@ -12,8 +12,9 @@ import ( "github.com/google/uuid" ) +// sandboxCreationRecorder is the Worker's hosted admission; it counts +// creations. type sandboxCreationRecorder struct { - inputRecorder calls int } @@ -31,7 +32,10 @@ func TestSessionCreationRejectsSandboxNodeSelector(t *testing.T) { fmt.Sprintf(`{"agent":{"model":"model","x_agents_core":{"sandbox_node_id":%q}},"environment":{"type":"openai_hosted"}}`, node), } { recorder := &sandboxCreationRecorder{} - handler, _ := environmentCreationHandler(t, "codex", WithHostedEnvironments(), WithExecution(recorder)) + handler, _ := environmentCreationHandler(t, "codex", func(d *Dependencies, f *testFakes) { + d.Execution, d.Sandboxes = f.execution(), f.sandboxes() + f.admission.createSession = recorder.CreateSession + }) request := httptest.NewRequest(http.MethodPost, "/v1/agents/sessions", strings.NewReader(body)) request.Header.Set("Authorization", "Bearer key") request.Header.Set("OpenAI-Beta", "agents=v1") diff --git a/services/core/internal/api/saved_provider_test.go b/services/core/internal/api/saved_provider_test.go index 5565d27fb..69be54e76 100644 --- a/services/core/internal/api/saved_provider_test.go +++ b/services/core/internal/api/saved_provider_test.go @@ -15,7 +15,6 @@ import ( const savedProviderFixture = `{"protocol":"responses","base_url":"https://example.test/v1","api_key":"saved-provider-secret","context_window":100000,"max_output_tokens":8000}` type savedProviderStore struct { - ResourceStore saved store.SavedAgent provider *v1.ModelProviderInput } @@ -40,10 +39,14 @@ func (s *savedProviderStore) ListAgents(context.Context, string, string, int, bo return store.AgentPage{Agents: []store.SavedAgent{s.saved}}, nil } +// serve answers the saved Agent operations from s. +func (s *savedProviderStore) serve(_ *Dependencies, f *testFakes) { + f.agents.createAgent, f.agents.updateAgent, f.agents.getAgent, f.agents.listAgents = s.CreateAgent, s.UpdateAgent, s.GetAgent, s.ListAgents +} + func TestSavedProviderReadRedaction(t *testing.T) { - h, recording, _ := testHandler(t) s := &savedProviderStore{} - recording.ResourceStore = s + h, _, _ := testHandler(t, s.serve) body := `{"model":"fixture","x_agents_core":{"harness":"codex","model_provider":` + savedProviderFixture + `}}` created := credentialRequest(h, http.MethodPost, "/v1/agents", body) if created.Code != http.StatusCreated || s.provider == nil || s.provider.APIKey != "saved-provider-secret" { @@ -75,9 +78,8 @@ func assertSavedProviderRedacted(t *testing.T, raw string) { } func TestSavedProviderWithoutHarnessDefersCompatibility(t *testing.T) { - h, recording, _ := testHandler(t) s := &savedProviderStore{} - recording.ResourceStore = s + h, _, _ := testHandler(t, s.serve) provider := strings.Replace(savedProviderFixture, `"responses"`, `"anthropic"`, 1) response := credentialRequest(h, http.MethodPost, "/v1/agents", `{"model":"fixture","x_agents_core":{"model_provider":`+provider+`}}`) if response.Code != http.StatusCreated || s.provider == nil || s.provider.Protocol != "anthropic" { @@ -148,9 +150,8 @@ func TestSavedProviderProtocolHarnessMatrix(t *testing.T) { for _, harness := range []string{"codex", "claude_sdk", "mcode"} { for _, protocol := range []string{"anthropic", "responses", "chat_completions"} { t.Run(harness+"/"+protocol, func(t *testing.T) { - h, recording, _ := testHandler(t) s := &savedProviderStore{} - recording.ResourceStore = s + h, _, _ := testHandler(t, s.serve) provider := strings.Replace(savedProviderFixture, `"responses"`, `"`+protocol+`"`, 1) body := `{"model":"fixture","x_agents_core":{"harness":"` + harness + `","model_provider":` + provider + `}}` for _, path := range []string{"/v1/agents", "/v1/agents/" + uuid.NewString()} { diff --git a/services/core/internal/api/session_admission_test.go b/services/core/internal/api/session_admission_test.go index 7b0c73700..c27c322d7 100644 --- a/services/core/internal/api/session_admission_test.go +++ b/services/core/internal/api/session_admission_test.go @@ -11,6 +11,13 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" ) +// forbidSessionAccess withdraws the Session and deployment model provider reads +// testHandler serves, so any access fails the test. +func forbidSessionAccess(_ *Dependencies, f *testFakes) { + f.sessions.createSession, f.sessions.getSession, f.sessions.findSessionCreation, f.sessions.listSessions = nil, nil, nil, nil + f.modelProviders.deploymentModelProvider = nil +} + func TestSessionAdmissionRejectsBeforeResourceOrExecutionAccess(t *testing.T) { for _, environment := range []string{"none", "openai_hosted"} { for _, input := range []string{"", `,"input":null`} { @@ -19,11 +26,8 @@ func TestSessionAdmissionRejectsBeforeResourceOrExecutionAccess(t *testing.T) { continue } t.Run(fmt.Sprintf("%s/%s/stream=%t", environment, input, stream), func(t *testing.T) { - // Any resource access, including creation retry lookup, would panic. - handler, _, _ := testHandler(t, func(h *Handler) { - h.store = &struct{ ResourceStore }{} - h.inputs = &inputRecorder{} - }) + // Any resource access, including creation retry lookup, fails the test. + handler, _, _ := testHandler(t, forbidSessionAccess, func(d *Dependencies, f *testFakes) { d.Execution = f.execution() }) body := fmt.Sprintf(`{"agent":{"model":"example"},"environment":{"type":%q},"stream":%t%s}`, environment, stream, input) for _, token := range []string{"test-api-key", "invalid"} { request := httptest.NewRequest(http.MethodPost, "/v1/agents/sessions", strings.NewReader(body)) @@ -51,7 +55,7 @@ func TestSessionAdmissionRejectsBeforeResourceOrExecutionAccess(t *testing.T) { } func TestSessionEmptyUpdateRejectsBeforeResourceAccess(t *testing.T) { - handler, _, _ := testHandler(t, func(h *Handler) { h.store = &struct{ ResourceStore }{} }) + handler, _, _ := testHandler(t, forbidSessionAccess) for _, token := range []string{"test-api-key", "invalid"} { request := httptest.NewRequest(http.MethodPost, "/v1/agents/sessions/unknown", strings.NewReader(`{}`)) request.Header.Set("Authorization", "Bearer "+token) diff --git a/services/core/internal/api/session_artifacts.go b/services/core/internal/api/session_artifacts.go index 102f2251f..c0befbfe5 100644 --- a/services/core/internal/api/session_artifacts.go +++ b/services/core/internal/api/session_artifacts.go @@ -11,25 +11,14 @@ import ( "github.com/go-chi/chi/v5" ) -type SessionArtifactStore interface { +// Artifacts reads, streams and deletes a Session's published Artifacts. +type Artifacts interface { GetSessionArtifact(context.Context, string, string, string) (store.SessionArtifact, error) ListSessionArtifacts(context.Context, string, string, string, string, int, bool) (store.ArtifactPage, error) ReadSessionArtifact(context.Context, string, string, string, func(store.SessionArtifact, io.Reader) error) error DeleteSessionArtifact(context.Context, string, string, string) error } -func WithSessionArtifacts(s SessionArtifactStore) Option { - return func(h *Handler) { h.artifacts = s } -} - -func (h *Handler) artifactsReady(w http.ResponseWriter) bool { - if h.artifacts == nil { - writeError(w, http.StatusServiceUnavailable, "artifact_storage_unavailable", "Artifact storage is unavailable.") - return false - } - return true -} - // @Summary List immutable Session artifacts // @Description Lists published outputs independently of Environment availability. Sorting uses publication time and ID. A later Turn publishes a path again only when it is new, its bytes changed, or no Artifact remains for it. A malformed environment_id matches nothing. An after value that is not an Artifact of this Session, including a malformed one, returns 400 invalid_request_error with the message "after is not a valid artifact ID". The local default page size is 20; exact upstream defaults remain unverified. // @Tags Artifacts @@ -45,14 +34,11 @@ func (h *Handler) artifactsReady(w http.ResponseWriter) bool { // @Failure 400,401,404,500,503 {object} v1.ErrorResponse // @Router /agents/sessions/{session_id}/artifacts [get] func (h *Handler) listSessionArtifacts(w http.ResponseWriter, r *http.Request) { - if !h.artifactsReady(w) { - return - } options, ok := readPage(w, r, "environment_id") if !ok { return } - page, err := h.artifacts.ListSessionArtifacts(r.Context(), tenantID(r), chi.URLParam(r, "session_id"), r.URL.Query().Get("environment_id"), options.after, options.limit, options.ascending) + page, err := h.Artifacts.ListSessionArtifacts(r.Context(), tenantID(r), chi.URLParam(r, "session_id"), r.URL.Query().Get("environment_id"), options.after, options.limit, options.ascending) if err != nil { writeStoreError(w, r, err) return @@ -76,10 +62,7 @@ func (h *Handler) listSessionArtifacts(w http.ResponseWriter, r *http.Request) { // @Failure 400,401,404,500,503 {object} v1.ErrorResponse // @Router /agents/sessions/{session_id}/artifacts/{artifact_id} [get] func (h *Handler) getSessionArtifact(w http.ResponseWriter, r *http.Request) { - if !h.artifactsReady(w) { - return - } - artifact, err := h.artifacts.GetSessionArtifact(r.Context(), tenantID(r), chi.URLParam(r, "session_id"), chi.URLParam(r, "artifact_id")) + artifact, err := h.Artifacts.GetSessionArtifact(r.Context(), tenantID(r), chi.URLParam(r, "session_id"), chi.URLParam(r, "artifact_id")) if err != nil { writeStoreError(w, r, err) return @@ -99,11 +82,8 @@ func (h *Handler) getSessionArtifact(w http.ResponseWriter, r *http.Request) { // @Failure 400,401,404,500,503 {object} v1.ErrorResponse // @Router /agents/sessions/{session_id}/artifacts/{artifact_id} [delete] func (h *Handler) deleteSessionArtifact(w http.ResponseWriter, r *http.Request) { - if !h.artifactsReady(w) { - return - } id := chi.URLParam(r, "artifact_id") - if err := h.artifacts.DeleteSessionArtifact(r.Context(), tenantID(r), chi.URLParam(r, "session_id"), id); err != nil { + if err := h.Artifacts.DeleteSessionArtifact(r.Context(), tenantID(r), chi.URLParam(r, "session_id"), id); err != nil { writeStoreError(w, r, err) return } @@ -122,11 +102,8 @@ func (h *Handler) deleteSessionArtifact(w http.ResponseWriter, r *http.Request) // @Failure 400,401,404,500,503 {object} v1.ErrorResponse // @Router /agents/sessions/{session_id}/artifacts/{artifact_id}/content [get] func (h *Handler) sessionArtifactContent(w http.ResponseWriter, r *http.Request) { - if !h.artifactsReady(w) { - return - } serveStoredContent(w, r, func(ctx context.Context, consume func(string, int64, io.Reader) error) error { - return h.artifacts.ReadSessionArtifact(ctx, tenantID(r), chi.URLParam(r, "session_id"), chi.URLParam(r, "artifact_id"), func(a store.SessionArtifact, body io.Reader) error { + return h.Artifacts.ReadSessionArtifact(ctx, tenantID(r), chi.URLParam(r, "session_id"), chi.URLParam(r, "artifact_id"), func(a store.SessionArtifact, body io.Reader) error { return consume(path.Base(a.Path), a.SizeBytes, body) }) }) diff --git a/services/core/internal/api/session_artifacts_test.go b/services/core/internal/api/session_artifacts_test.go index 4450ec585..49902c616 100644 --- a/services/core/internal/api/session_artifacts_test.go +++ b/services/core/internal/api/session_artifacts_test.go @@ -53,13 +53,19 @@ func (f *artifactFixture) DeleteSessionArtifact(ctx context.Context, tenant, ses return err } +// wire serves the Artifacts area from f. +func (f *artifactFixture) wire(_ *Dependencies, fakes *testFakes) { + fakes.artifacts.getSessionArtifact, fakes.artifacts.listSessionArtifacts = f.GetSessionArtifact, f.ListSessionArtifacts + fakes.artifacts.readSessionArtifact, fakes.artifacts.deleteSessionArtifact = f.ReadSessionArtifact, f.DeleteSessionArtifact +} + type artifactResponseRecorder struct{ *httptest.ResponseRecorder } func (*artifactResponseRecorder) SetWriteDeadline(time.Time) error { return nil } func TestSessionArtifactRoutesAndPublicProjection(t *testing.T) { f := &artifactFixture{artifact: store.SessionArtifact{ID: "artifact", SessionID: "session", EnvironmentID: "environment", TurnID: "turn", Path: "/workspace/outputs/a.bin", SizeBytes: 3, CreatedAt: time.Unix(123, 456)}} - h, _, tenant := testHandler(t, WithSessionArtifacts(f)) + h, _, tenant := testHandler(t, f.wire) request := func(method, suffix, beta string) *httptest.ResponseRecorder { r := httptest.NewRequest(method, "/v1/agents/sessions/session/artifacts"+suffix, nil) r.Header.Set("Authorization", "Bearer test-api-key") diff --git a/services/core/internal/api/session_creation_identity.go b/services/core/internal/api/session_creation_identity.go index a273a0c32..0afba228a 100644 --- a/services/core/internal/api/session_creation_identity.go +++ b/services/core/internal/api/session_creation_identity.go @@ -45,7 +45,7 @@ func (h *Handler) recoverSessionCreation(w http.ResponseWriter, r *http.Request, if len(request) == 0 { return false } - result, err := h.store.FindSessionCreation(r.Context(), tenantID(r), key, request, sessionCreator(r)) + result, err := h.Sessions.FindSessionCreation(r.Context(), tenantID(r), key, request, sessionCreator(r)) if errors.Is(err, store.ErrNotFound) { return false } @@ -54,18 +54,13 @@ func (h *Handler) recoverSessionCreation(w http.ResponseWriter, r *http.Request, return true } if stream { - events, ok := h.store.(eventStore) - if !ok { - writeError(w, http.StatusServiceUnavailable, "stream_unavailable", "Live events are unavailable.") - return true - } if !h.auditSessionOperation(w, r, result.Session.ID, "create") { return true } // Recorded-intent lookup finds an existing creation, which sends no events. - h.respondSessionCreationStream(w, r, events, nil, result) + h.respondSessionCreationStream(w, r, result) } else { - session, err := h.store.GetSession(r.Context(), tenantID(r), result.Session.ID) + session, err := h.Sessions.GetSession(r.Context(), tenantID(r), result.Session.ID) if err != nil { writeStoreError(w, r, err) } else if h.auditSessionOperation(w, r, session.ID, "create") { diff --git a/services/core/internal/api/session_creation_stream.go b/services/core/internal/api/session_creation_stream.go index 7658eee27..7d5544b4a 100644 --- a/services/core/internal/api/session_creation_stream.go +++ b/services/core/internal/api/session_creation_stream.go @@ -10,46 +10,26 @@ import ( "github.com/google/uuid" ) -type sessionStreamCreator interface { - CreateSessionStream(context.Context, string, store.CreateSessionInput) (store.SessionCreation, error) -} - func (h *Handler) createSessionStream(w http.ResponseWriter, r *http.Request, input store.CreateSessionInput) { - // Check every stream capability before the creation can commit. - events, ok := h.store.(eventStore) - snapshots, snapshotted := h.store.(sessionSnapshotStore) - if !ok || !snapshotted { - writeError(w, http.StatusServiceUnavailable, "stream_unavailable", "Live events are unavailable.") - return - } - var source any = h.store var config configuration if err := json.Unmarshal(input.Configuration, &config); err != nil { writeStoreError(w, r, store.ErrInvalidInput) return } + create := h.Sessions.CreateSessionStream if len(input.InitialInputs) > 0 || config.Environment.Type == "openai_hosted" { - if h.inputs == nil { + if h.Execution == nil { writeError(w, http.StatusServiceUnavailable, "execution_unavailable", "Execution input is not enabled on this service.") return } - source = h.inputs + create = h.Execution.Admission.CreateSessionStream } - creator, ok := source.(sessionStreamCreator) - if !ok { - writeError(w, http.StatusServiceUnavailable, "stream_unavailable", "Streaming creation is unavailable.") - return - } - result, err := creator.CreateSessionStream(r.Context(), tenantID(r), input) + result, err := create(r.Context(), tenantID(r), input) if err != nil { writeStoreError(w, r, err) return } - h.respondSessionCreationStream(w, r, events, snapshots, result) -} - -type sessionSnapshotStore interface { - SessionStreamSnapshot(context.Context, string, string) (store.Session, int64, error) + h.respondSessionCreationStream(w, r, result) } // respondSessionCreationStream renders result.Session, the committed Session @@ -60,12 +40,12 @@ type sessionSnapshotStore interface { // official same-key requests create distinct Sessions, so there is no retry // stream to follow. Recover with stream=false or the GET events stream. Only a // fresh creation uses snapshots. -func (h *Handler) respondSessionCreationStream(w http.ResponseWriter, r *http.Request, events eventStore, snapshots sessionSnapshotStore, result store.SessionCreation) { +func (h *Handler) respondSessionCreationStream(w http.ResponseWriter, r *http.Request, result store.SessionCreation) { if !result.Created { openEventStream(w, http.StatusCreated) return } - response, err := sessionResponse(result.Session, h.executorURL) + response, err := sessionResponse(result.Session, h.executorURL()) if err != nil { writeStoreError(w, r, err) return @@ -84,14 +64,14 @@ func (h *Handler) respondSessionCreationStream(w http.ResponseWriter, r *http.Re } tenant, id := tenantID(r), result.Session.ID settlement := func(ctx context.Context) (bool, int64, error) { - session, cursor, err := snapshots.SessionStreamSnapshot(ctx, tenant, id) + session, cursor, err := h.SessionEvents.SessionStreamSnapshot(ctx, tenant, id) if err != nil { return false, 0, err } - response, err := sessionResponse(session, h.executorURL) + response, err := sessionResponse(session, h.executorURL()) return err == nil && sessionSettled(session, response), cursor, err } - h.serveSessionEvents(w, r, events, result.Session, result.Cursor, &created, http.StatusCreated, settlement) + h.serveSessionEvents(w, r, result.Session, result.Cursor, &created, http.StatusCreated, settlement) } // sessionSettled reports that a committed projection has no admitted work left: diff --git a/services/core/internal/api/session_creation_stream_test.go b/services/core/internal/api/session_creation_stream_test.go index f0290f9fe..46eb33414 100644 --- a/services/core/internal/api/session_creation_stream_test.go +++ b/services/core/internal/api/session_creation_stream_test.go @@ -101,13 +101,15 @@ func (f *creationStreamFixture) FindSessionCreation(context.Context, string, str return store.SessionCreation{Session: row, Cursor: 10}, nil } -type creationStreamAdmission struct { - inputRecorder - fixture *creationStreamFixture +// AuditSessionOperation accepts the audit of a replayed creation. +func (f *creationStreamFixture) AuditSessionOperation(context.Context, string, string, string) error { + return nil } -func (a *creationStreamAdmission) CreateSessionStream(context.Context, string, store.CreateSessionInput) (store.SessionCreation, error) { - return a.fixture.creation, nil +// CreateSessionStream is the Worker's streamed admission: it returns the +// prepared creation. +func (f *creationStreamFixture) CreateSessionStream(context.Context, string, store.CreateSessionInput) (store.SessionCreation, error) { + return f.creation, nil } type sseFrame struct { @@ -139,17 +141,17 @@ func newCreationStreamHarness(t *testing.T) *creationStreamHarness { ID: uuid.NewString(), TenantID: tenant, CreatedAt: time.Unix(1700000000, 0), Metadata: map[string]string{}, Configuration: json.RawMessage(`{"agent":{"id":"agent_test","model":"model","tools":[]},"environment":{"type":"none"}}`), }}} - auth, err := NewAuthenticator([]APIKey{{ + 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("key"), TenantID: tenant, - }}) - if err != nil { - t.Fatal(err) - } - handler, err := NewHandler(fixture, auth, "codex", WithExecution(&creationStreamAdmission{fixture: fixture})) - if err != nil { - t.Fatal(err) - } + }).ResolveProjectAPIKey + fakes.sessions.getSession, fakes.sessions.findSessionCreation, fakes.sessions.auditSessionOperation = fixture.GetSession, fixture.FindSessionCreation, fixture.AuditSessionOperation + fakes.sessionEvents.sessionEventCursor, fakes.sessionEvents.sessionStreamSnapshot, fakes.sessionEvents.listSessionEvents = fixture.SessionEventCursor, fixture.SessionStreamSnapshot, fixture.ListSessionEvents + fakes.modelProviders.deploymentModelProvider = noDeploymentModelProvider + deps.Execution = fakes.execution() + fakes.admission.createSessionStream = fixture.CreateSessionStream + handler := newTestHandler(t, deps) h := &creationStreamHarness{t: t, fixture: fixture} h.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { h.active.Add(1) @@ -685,45 +687,3 @@ func TestSessionSettledProjection(t *testing.T) { } } } - -// eventOnlyStore streams events but cannot read the same-snapshot projection. -type eventOnlyStore struct{ ResourceStore } - -func (eventOnlyStore) SessionEventCursor(context.Context, string, string) (int64, error) { - return 0, nil -} - -func (eventOnlyStore) ListSessionEvents(context.Context, string, string, int64) ([]store.SessionChange, error) { - return nil, nil -} - -type countingStreamAdmission struct { - inputRecorder - calls atomic.Int32 -} - -func (a *countingStreamAdmission) CreateSessionStream(context.Context, string, store.CreateSessionInput) (store.SessionCreation, error) { - a.calls.Add(1) - return store.SessionCreation{}, store.ErrInvalidInput -} - -func TestCreationStreamCapabilityIsCheckedBeforeCreation(t *testing.T) { - auth, err := NewAuthenticator([]APIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential("key"), TenantID: uuid.NewString()}}) - if err != nil { - t.Fatal(err) - } - admission := &countingStreamAdmission{} - handler, err := NewHandler(eventOnlyStore{}, auth, "codex", WithExecution(admission)) - if err != nil { - t.Fatal(err) - } - request := httptest.NewRequest(http.MethodPost, "/v1/agents/sessions", strings.NewReader(creationBody)) - request.Header.Set("Authorization", "Bearer key") - request.Header.Set("OpenAI-Beta", "agents=v1") - request.Header.Set("Content-Type", "application/json") - response := httptest.NewRecorder() - handler.ServeHTTP(response, request) - if response.Code != http.StatusServiceUnavailable || admission.calls.Load() != 0 { - t.Fatal("unsupported stream store reached creation", response.Code, admission.calls.Load(), response.Body.String()) - } -} diff --git a/services/core/internal/api/session_credentials.go b/services/core/internal/api/session_credentials.go index 80c33036f..954e4bfd9 100644 --- a/services/core/internal/api/session_credentials.go +++ b/services/core/internal/api/session_credentials.go @@ -10,10 +10,6 @@ import ( "github.com/google/uuid" ) -type mcpCredentialResolver interface { - ResolveMCPCredentials(context.Context, string, []string, []store.MCPCredentialRequest) ([]store.MCPCredentialBinding, error) -} - func (h *Handler) bindSessionCredentials(ctx context.Context, tenant string, raw json.RawMessage) (json.RawMessage, error) { var cfg configuration if json.Unmarshal(raw, &cfg) != nil { @@ -34,11 +30,7 @@ func (h *Handler) bindSessionCredentials(ctx context.Context, tenant string, raw if !required { return raw, nil } - resolver, ok := h.store.(mcpCredentialResolver) - if !ok { - return nil, store.ErrCredentialStorageUnavailable - } - bindings, err := resolver.ResolveMCPCredentials(ctx, tenant, cfg.VaultIDs, requests) + bindings, err := h.Vaults.ResolveMCPCredentials(ctx, tenant, cfg.VaultIDs, requests) if err != nil { return nil, err } diff --git a/services/core/internal/api/session_deletion.go b/services/core/internal/api/session_deletion.go index ce0dc4f44..5812d2b45 100644 --- a/services/core/internal/api/session_deletion.go +++ b/services/core/internal/api/session_deletion.go @@ -35,7 +35,7 @@ func (h *Handler) deleteSession(w http.ResponseWriter, r *http.Request) { writeStoreError(w, r, store.ErrNotFound) return } - if err := h.store.DeleteSession(r.Context(), tenantID(r), id); err != nil { + if err := h.Sessions.DeleteSession(r.Context(), tenantID(r), id); err != nil { writeStoreError(w, r, err) return } diff --git a/services/core/internal/api/session_diagnostics.go b/services/core/internal/api/session_diagnostics.go index be6a256f5..f23b765c3 100644 --- a/services/core/internal/api/session_diagnostics.go +++ b/services/core/internal/api/session_diagnostics.go @@ -5,6 +5,7 @@ import ( "net/http" "time" + v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/go-chi/chi/v5" ) @@ -45,9 +46,13 @@ type TurnDiagnostics struct { ItemsTruncated bool `json:"items_truncated"` } -type sessionDiagnosticsStore interface { +// SessionAdmin serves the administrator's per-Session reads: diagnostics +// snapshots, the execution configuration and the managed archive state. +type SessionAdmin interface { GetSessionDiagnosticsSnapshot(context.Context, string, string) (store.Session, error) GetTurnDiagnosticsSnapshot(context.Context, string, string, string) (store.TurnDiagnosticsSnapshot, error) + GetSessionExecutionConfiguration(context.Context, string, string) (v1.SessionExecutionConfiguration, error) + GetManagedSessionArchive(context.Context, string, string) (store.ManagedSessionArchive, error) } // @Summary Retrieve root Session diagnostics @@ -61,19 +66,14 @@ type sessionDiagnosticsStore interface { // @Failure 400,401,404,500,503 {object} CoreErrorResponse // @Router /core/v1/projects/{project_id}/sessions/{session_id}/diagnostics [get] func (h *Handler) getSessionDiagnostics(w http.ResponseWriter, r *http.Request) { - source, ok := h.store.(sessionDiagnosticsStore) - if !ok { - writeError(w, http.StatusServiceUnavailable, "diagnostics_unavailable", "Session diagnostics are unavailable.") - return - } ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) defer cancel() - session, err := source.GetSessionDiagnosticsSnapshot(ctx, tenantID(r), chi.URLParam(r, "session_id")) + session, err := h.SessionAdmin.GetSessionDiagnosticsSnapshot(ctx, tenantID(r), chi.URLParam(r, "session_id")) if err != nil { writeStoreError(w, r, err) return } - public, err := sessionResponse(session, h.executorURL) + public, err := sessionResponse(session, h.executorURL()) if err != nil { writeStoreError(w, r, err) return @@ -122,14 +122,9 @@ func (h *Handler) getSessionDiagnostics(w http.ResponseWriter, r *http.Request) // @Failure 400,401,404,500,503 {object} CoreErrorResponse // @Router /core/v1/projects/{project_id}/sessions/{session_id}/turns/{turn_id}/diagnostics [get] func (h *Handler) getTurnDiagnostics(w http.ResponseWriter, r *http.Request) { - source, ok := h.store.(sessionDiagnosticsStore) - if !ok { - writeError(w, http.StatusServiceUnavailable, "diagnostics_unavailable", "Session diagnostics are unavailable.") - return - } ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) defer cancel() - snapshot, err := source.GetTurnDiagnosticsSnapshot(ctx, tenantID(r), chi.URLParam(r, "session_id"), chi.URLParam(r, "turn_id")) + snapshot, err := h.SessionAdmin.GetTurnDiagnosticsSnapshot(ctx, tenantID(r), chi.URLParam(r, "session_id"), chi.URLParam(r, "turn_id")) if err != nil { writeStoreError(w, r, err) return diff --git a/services/core/internal/api/session_diagnostics_deadline_test.go b/services/core/internal/api/session_diagnostics_deadline_test.go index ee058279b..288fc9f47 100644 --- a/services/core/internal/api/session_diagnostics_deadline_test.go +++ b/services/core/internal/api/session_diagnostics_deadline_test.go @@ -33,7 +33,7 @@ func TestDiagnosticsReadDeadline(t *testing.T) { for _, shorter := range []bool{false, true} { t.Run(suffix+map[bool]string{false: "/server", true: "/caller"}[shorter], func(t *testing.T) { source := &diagnosticDeadlineStore{diagnosticSnapshotStore: diagnosticSnapshotStore{session: session}} - h, _, _ := adminTestHandler(t, func(h *Handler) { h.store = source }) + h, _, _ := adminTestHandler(t, serveDiagnostics(source)) ctx := t.Context() var callerDeadline time.Time if shorter { diff --git a/services/core/internal/api/session_diagnostics_public_compat_test.go b/services/core/internal/api/session_diagnostics_public_compat_test.go index d01d2c7d7..25eea5fe0 100644 --- a/services/core/internal/api/session_diagnostics_public_compat_test.go +++ b/services/core/internal/api/session_diagnostics_public_compat_test.go @@ -15,19 +15,15 @@ import ( "github.com/jackc/pgx/v5/pgxpool" ) -// This test also runs unchanged against the comparison base using a private -// overlay. Fixed resource times make its public response bytes comparable. +// Fixed resource times make the logged public response bytes comparable +// between revisions. func TestDiagnosticPublicCompatibility(t *testing.T) { s, pool := diagnosticDatabase(t) key := callerBinding() - auth, err := NewAuthenticator([]APIKey{key}) - if err != nil { - t.Fatal(err) - } - h, err := NewHandler(s, auth, "codex") - if err != nil { - t.Fatal(err) - } + deps, fakes := testDependencies(t) + fakes.projects.resolveProjectAPIKey = projectKeys(t, key).ResolveProjectAPIKey + databaseSessionReads(s)(&deps, fakes) + h := newTestHandler(t, deps) session, err := s.CreateSession(t.Context(), key.TenantID, store.CreateSessionInput{Creator: identity.Subject{Kind: "service_account", ID: "compat-test"}, Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"id":"agent_root","model":"test"},"environment":{"type":"none"}}`)}) if err != nil { t.Fatal(err) @@ -63,6 +59,11 @@ func diagnosticRequest(handler http.Handler, path, token string) *httptest.Respo return w } +// databaseSessionReads serves Session, Turn, Item and diagnostic reads from s. +func databaseSessionReads(s *store.Store) func(*Dependencies, *testFakes) { + return func(d *Dependencies, _ *testFakes) { d.Sessions, d.SessionHistory, d.SessionAdmin = s, s, s } +} + func diagnosticDatabase(t *testing.T) (*store.Store, *pgxpool.Pool) { t.Helper() dsn := os.Getenv("OAC_TEST_DATABASE_URL") diff --git a/services/core/internal/api/session_diagnostics_test.go b/services/core/internal/api/session_diagnostics_test.go index 6fc5c9a20..74b86dc81 100644 --- a/services/core/internal/api/session_diagnostics_test.go +++ b/services/core/internal/api/session_diagnostics_test.go @@ -14,7 +14,7 @@ import ( func TestDiagnosticsCoreHandlerDatabaseBoundary(t *testing.T) { s, pool := diagnosticDatabase(t) - h, _, tenant := adminTestHandler(t, func(h *Handler) { h.store = s }) + h, _, tenant := adminTestHandler(t, databaseSessionReads(s)) session, err := s.CreateSession(t.Context(), tenant, store.CreateSessionInput{Creator: identity.Subject{Kind: "service_account", ID: "diagnostic-test"}, Engine: "codex", IdempotencyKey: "diagnostics", Configuration: json.RawMessage(`{"agent":{"id":"agent_root","model":"test"},"environment":{"type":"none"}}`)}) if err != nil { t.Fatal(err) @@ -62,7 +62,6 @@ func TestDiagnosticsCoreHandlerDatabaseBoundary(t *testing.T) { } type diagnosticSnapshotStore struct { - ResourceStore session store.Session } @@ -73,6 +72,19 @@ func (s diagnosticSnapshotStore) GetTurnDiagnosticsSnapshot(context.Context, str return store.TurnDiagnosticsSnapshot{Session: s.session, Turn: *s.session.LastTurn, Items: []store.ItemDiagnosticTiming{}}, nil } +// diagnosticSnapshots answers Core diagnostic reads. +type diagnosticSnapshots interface { + GetSessionDiagnosticsSnapshot(context.Context, string, string) (store.Session, error) + GetTurnDiagnosticsSnapshot(context.Context, string, string, string) (store.TurnDiagnosticsSnapshot, error) +} + +// serveDiagnostics answers Session and Turn diagnostic reads from source. +func serveDiagnostics(source diagnosticSnapshots) func(*Dependencies, *testFakes) { + return func(_ *Dependencies, f *testFakes) { + f.sessionAdmin.getSessionDiagnosticsSnapshot, f.sessionAdmin.getTurnDiagnosticsSnapshot = source.GetSessionDiagnosticsSnapshot, source.GetTurnDiagnosticsSnapshot + } +} + func TestDiagnosticsFailurePrecedenceAndUnknownTime(t *testing.T) { id, turnID := uuid.NewString(), uuid.NewString() base := store.Session{ID: id, Configuration: json.RawMessage(`{"agent":{"id":"agent_root","model":"test"},"environment":{"type":"none"}}`), LastTurn: &store.Turn{ID: turnID, SessionID: id, Status: store.TurnFailed, Outcome: json.RawMessage(`{"error_code":"engine_failed","error":"secret-canary"}`)}} @@ -82,14 +94,14 @@ func TestDiagnosticsFailurePrecedenceAndUnknownTime(t *testing.T) { }{{nil, "harness_error", "turn"}, {&store.EnvironmentInputActivity{Status: "failed"}, "environment_connection_timeout", "environment_input"}, {&store.EnvironmentInputActivity{Status: "failed", Failure: "model_provider_required"}, "model_provider_required", "environment_input"}, {&store.EnvironmentInputActivity{Status: "failed", Failure: "runtime_preparation_failed"}, "runtime_preparation_failed", "environment_input"}, {&store.EnvironmentInputActivity{Status: "failed", Failure: "secret-canary"}, "internal_error", "environment_input"}} { value := base value.EnvironmentInputActivity = tc.activity - h, _, _ := adminTestHandler(t, func(h *Handler) { h.store = diagnosticSnapshotStore{session: value} }) + h, _, _ := adminTestHandler(t, serveDiagnostics(diagnosticSnapshotStore{session: value})) w := diagnosticRequest(h, adminSessionsPath+id+"/diagnostics", "Bearer admin") if w.Code != 200 || !strings.Contains(w.Body.String(), `"code":"`+tc.code+`"`) || !strings.Contains(w.Body.String(), `"source":"`+tc.source+`"`) || !strings.Contains(w.Body.String(), `"failed_at":null`) || strings.Contains(w.Body.String(), "canary") { t.Fatal(w.Code, w.Body) } } base.EnvironmentInputActivity = &store.EnvironmentInputActivity{Status: "idle", LastActiveAt: time.Now()} - h, _, _ := adminTestHandler(t, func(h *Handler) { h.store = diagnosticSnapshotStore{session: base} }) + h, _, _ := adminTestHandler(t, serveDiagnostics(diagnosticSnapshotStore{session: base})) if w := diagnosticRequest(h, adminSessionsPath+id+"/diagnostics", "Bearer admin"); w.Code != 200 || !strings.Contains(w.Body.String(), `"failure":null`) { t.Fatal("public activity precedence changed", w.Code, w.Body) } @@ -102,7 +114,7 @@ func TestDiagnosticsHostedFailureOverridesInputWithoutParsingReason(t *testing.T step, index, exit := "setup", 2, 7 for _, detail := range []*store.ProvisioningFailureDetail{nil, {Step: &step, Index: &index, ExitCode: &exit}} { session.EnvironmentFailure = &store.EnvironmentFailure{Reason: "private-secret-canary setup_commands[99] exit 254", Detail: detail} - h, _, _ := adminTestHandler(t, func(h *Handler) { h.store = diagnosticSnapshotStore{session: session} }) + h, _, _ := adminTestHandler(t, serveDiagnostics(diagnosticSnapshotStore{session: session})) w := diagnosticRequest(h, adminSessionsPath+session.ID+"/diagnostics", "Bearer admin") if w.Code != 200 || strings.Contains(w.Body.String(), "canary") || !strings.Contains(w.Body.String(), `"code":"environment_provisioning_failed"`) || !strings.Contains(w.Body.String(), `"source":"environment"`) { t.Fatal(w.Code, w.Body) diff --git a/services/core/internal/api/session_environment_http_test.go b/services/core/internal/api/session_environment_http_test.go index 3d0cdec8e..34e14de2b 100644 --- a/services/core/internal/api/session_environment_http_test.go +++ b/services/core/internal/api/session_environment_http_test.go @@ -42,17 +42,17 @@ func TestSelfHostedSessionHTTPReadListMetadataAndLiveStream(t *testing.T) { Sequence: 11, Event: v1.SessionEvent{Type: "agent.session.requires_action", EventID: "activity", SessionID: session.ID}, EnvironmentInputActivity: session.EnvironmentInputActivity, }}}} - auth, err := NewAuthenticator([]APIKey{{ + 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("key"), TenantID: session.TenantID, - }}) - if err != nil { - t.Fatal(err) - } - handler, err := NewHandler(fixture, auth, "codex", WithEnvironmentRemoteURL(environmentOrigin)) - if err != nil { - t.Fatal(err) - } + }).ResolveProjectAPIKey + fixture.serve(fakes) + fakes.sessions.listSessions, fakes.sessions.updateSessionMetadata = fixture.ListSessions, fixture.UpdateSessionMetadata + // Self-hosted Sessions report the executor URL of the enabled Execution. + deps.Execution = fakes.execution() + deps.Execution.ExecutorURL = environmentOrigin + handler := newTestHandler(t, deps) want, err := sessionResponse(session, environmentOrigin) if err != nil { t.Fatal(err) diff --git a/services/core/internal/api/session_execution_configuration.go b/services/core/internal/api/session_execution_configuration.go index ffd2bf81f..77cfda50e 100644 --- a/services/core/internal/api/session_execution_configuration.go +++ b/services/core/internal/api/session_execution_configuration.go @@ -1,7 +1,6 @@ package api import ( - "context" "encoding/json" "net/http" @@ -9,10 +8,6 @@ import ( "github.com/go-chi/chi/v5" ) -type sessionExecutionConfigurationStore interface { - GetSessionExecutionConfiguration(context.Context, string, string) (v1.SessionExecutionConfiguration, error) -} - // Record selection sources at resolution time. Null Agent extensions reset the // harness to deployment defaults but do not clear inherited provider bundles. func sessionExecutionProjection(input sessionRequest, saved *v1.SavedAgent, inherited, provider *v1.ModelProviderInput, engine string, raw json.RawMessage) v1.SessionExecutionConfiguration { @@ -69,12 +64,7 @@ func sessionExecutionProjection(input sessionRequest, saved *v1.SavedAgent, inhe // getSessionExecutionConfiguration serves the administrator per-Session read. func (h *Handler) getSessionExecutionConfiguration(w http.ResponseWriter, r *http.Request) { - source, ok := h.store.(sessionExecutionConfigurationStore) - if !ok { - writeError(w, http.StatusServiceUnavailable, "execution_configuration_unavailable", "Session execution configuration is unavailable.") - return - } - configuration, err := source.GetSessionExecutionConfiguration(r.Context(), tenantID(r), chi.URLParam(r, "session_id")) + configuration, err := h.SessionAdmin.GetSessionExecutionConfiguration(r.Context(), tenantID(r), chi.URLParam(r, "session_id")) if err != nil { writeStoreError(w, r, err) return diff --git a/services/core/internal/api/session_execution_configuration_test.go b/services/core/internal/api/session_execution_configuration_test.go index f6148b7d9..6d590a9cf 100644 --- a/services/core/internal/api/session_execution_configuration_test.go +++ b/services/core/internal/api/session_execution_configuration_test.go @@ -52,7 +52,6 @@ func TestExecutionConfigurationSources(t *testing.T) { } type executionConfigurationReader struct { - ResourceStore calls int tenant, id string value v1.SessionExecutionConfiguration @@ -67,7 +66,9 @@ func (s *executionConfigurationReader) GetSessionExecutionConfiguration(_ contex func TestExecutionConfigurationReadBoundary(t *testing.T) { s := &executionConfigurationReader{value: v1.SessionExecutionConfiguration{Object: "agent.session.execution_configuration", SchemaVersion: 1, SessionID: "frozen"}} - h, _, tenant := adminTestHandler(t, func(h *Handler) { h.store = s }) + h, _, tenant := adminTestHandler(t, func(_ *Dependencies, f *testFakes) { + f.sessionAdmin.getSessionExecutionConfiguration = s.GetSessionExecutionConfiguration + }) for _, tc := range []struct { auth string err error diff --git a/services/core/internal/api/session_initial_input_test.go b/services/core/internal/api/session_initial_input_test.go index 44b39119b..988cb49fa 100644 --- a/services/core/internal/api/session_initial_input_test.go +++ b/services/core/internal/api/session_initial_input_test.go @@ -8,11 +8,20 @@ import ( "testing" ) +// admitInto enables Execution whose Worker admits Session creation into s, +// apart from the store that creates Sessions without execution work. Input +// submission stays unexpected. +func admitInto(s *recordingStore) func(*Dependencies, *testFakes) { + return func(d *Dependencies, f *testFakes) { + d.Execution = f.execution() + f.admission.createSession = s.CreateSession + } +} + func TestInitialInputUsesExecutionAdmissionAndSharedMessageValidation(t *testing.T) { for _, value := range []string{`"First"`, `[{"role":"user","content":[{"type":"input_text","text":"First"}]}]`} { execution := &recordingStore{} - recorder := &inputRecorder{ResourceStore: execution} - h, idle, tenant := testHandler(t, WithExecution(recorder)) + h, idle, tenant := testHandler(t, admitInto(execution)) r := httptest.NewRequest(http.MethodPost, "/v1/agents/sessions", strings.NewReader(`{"agent":{"model":"test-model"},"environment":{"type":"none"},"input":`+value+`}`)) r.Header.Set("Authorization", "Bearer test-api-key") r.Header.Set("OpenAI-Beta", "agents=v1") @@ -20,7 +29,7 @@ func TestInitialInputUsesExecutionAdmissionAndSharedMessageValidation(t *testing r.Header.Set("Idempotency-Key", "create-key") w := httptest.NewRecorder() h.ServeHTTP(w, r) - if w.Code != 201 || idle.tenant != "" || execution.tenant != tenant || execution.input.IdempotencyKey != "create-key" || len(execution.input.InitialInputs) != 1 || recorder.inputs != nil { + if w.Code != 201 || idle.tenant != "" || execution.tenant != tenant || execution.input.IdempotencyKey != "create-key" || len(execution.input.InitialInputs) != 1 { t.Fatal(w.Code, w.Body, execution.input) } expected, err := executionInputs([]json.RawMessage{json.RawMessage(`{"type":"agent.session.input.message","input":[{"role":"user","content":[{"type":"input_text","text":"First"}]}]}`)}) @@ -50,7 +59,7 @@ func TestInitialInputAdmitsWhitespaceTextVerbatim(t *testing.T) { {`[{"type":"message","role":"user","content":[{"type":"input_text","text":" "}]},{"role":"user","content":[{"type":"input_text","text":" \t\n "}]}]`, `[{"type":"message","role":"user","content":[{"type":"input_text","text":" "}]},{"role":"user","content":[{"type":"input_text","text":" \t\n "}]}]`}, } { execution := &recordingStore{} - h, _, _ := testHandler(t, WithExecution(&inputRecorder{ResourceStore: execution})) + h, _, _ := testHandler(t, admitInto(execution)) w := createWithInput(h, value.input) if w.Code != 201 || len(execution.input.InitialInputs) != 1 { t.Fatalf("%s: %d %s", value.input, w.Code, w.Body) @@ -65,7 +74,7 @@ func TestInitialInputAdmitsWhitespaceTextVerbatim(t *testing.T) { } for _, value := range []string{`""`, `[]`, `[{"role":"user","content":[]}]`, `[{"role":"user","content":[{"type":"input_text","text":""}]}]`} { execution := &recordingStore{} - h, _, _ := testHandler(t, WithExecution(&inputRecorder{ResourceStore: execution})) + h, _, _ := testHandler(t, admitInto(execution)) w := createWithInput(h, value) if w.Code != 400 || w.Body.String() != emptyInputError || execution.tenant != "" { t.Fatalf("%s: %d %s", value, w.Code, w.Body) diff --git a/services/core/internal/api/session_metadata.go b/services/core/internal/api/session_metadata.go index 718eaecd8..c658d3691 100644 --- a/services/core/internal/api/session_metadata.go +++ b/services/core/internal/api/session_metadata.go @@ -59,7 +59,7 @@ func (h *Handler) updateSession(w http.ResponseWriter, r *http.Request) { } return } - session, err := h.store.UpdateSessionMetadata(r.Context(), tenantID(r), chi.URLParam(r, "session_id"), metadata) + session, err := h.Sessions.UpdateSessionMetadata(r.Context(), tenantID(r), chi.URLParam(r, "session_id"), metadata) if err != nil { writeStoreError(w, r, err) return diff --git a/services/core/internal/api/session_model_configuration.go b/services/core/internal/api/session_model_configuration.go index ec202b148..08d89b79e 100644 --- a/services/core/internal/api/session_model_configuration.go +++ b/services/core/internal/api/session_model_configuration.go @@ -34,8 +34,8 @@ func (h *Handler) prepareSessionModelConfiguration(ctx context.Context, input *s explicitModel := input.Agent != nil && input.Agent.Model != nil explicitProvider := input.XAgentsCore != nil && input.XAgentsCore.ModelProvider != nil needsModel := !explicitModel && saved == nil - if h.modelProviderDefaults != nil && v1.ModelProviderAllowed(input.Environment.Type, v1.ModelProviderSourceDeployment) && ((inherited == nil && !explicitProvider) || needsModel) { - input.deploymentDefaults, err = h.modelProviderDefaults(ctx, engine) + if v1.ModelProviderAllowed(input.Environment.Type, v1.ModelProviderSourceDeployment) && ((inherited == nil && !explicitProvider) || needsModel) { + input.deploymentDefaults, err = h.ModelProviders.DeploymentModelProvider(ctx, engine) if err != nil { var configurationError *v1.ModelProviderError if errors.As(err, &configurationError) { diff --git a/services/core/internal/api/session_model_configuration_test.go b/services/core/internal/api/session_model_configuration_test.go index 56bd8bfac..3bc18cd2d 100644 --- a/services/core/internal/api/session_model_configuration_test.go +++ b/services/core/internal/api/session_model_configuration_test.go @@ -40,10 +40,12 @@ func TestSessionNativeConfigurationSources(t *testing.T) { t.Fatal(err) } calls := 0 - h := &Handler{engine: "codex", modelProviderDefaults: func(context.Context, string) (*store.DeploymentModelProviderSnapshot, error) { + deps, fakes := testDependencies(t) + fakes.modelProviders.deploymentModelProvider = func(context.Context, string) (*store.DeploymentModelProviderSnapshot, error) { calls++ return &store.DeploymentModelProviderSnapshot{Provider: provider, Model: "deployment-model", HarnessConfig: json.RawMessage(`{"model_reasoning_effort":"high"}`)}, nil - }} + } + h := &Handler{Dependencies: deps} err = h.prepareSessionModelConfiguration(t.Context(), &input, tc.saved, nil) if (err != nil) != tc.wantError { t.Fatalf("error=%v", err) @@ -74,7 +76,8 @@ func TestProviderReplacementDiscardsNativeConfiguration(t *testing.T) { model := "saved-model" input := sessionRequest{CreateSessionRequest: v1.CreateSessionRequest{Agent: &v1.InlineAgent{Model: nil}, Environment: &v1.Environment{Type: "self_hosted"}, XAgentsCore: &v1.SessionExecutionInput{ModelProvider: provider}}} saved := &v1.SavedAgent{SavedAgentConfiguration: v1.SavedAgentConfiguration{Model: model, XAgentsCore: &v1.SavedAgentCore{Harness: "codex", HarnessConfig: json.RawMessage(`{"model_reasoning_effort":"high"}`)}}} - h := &Handler{engine: "codex"} + deps, _ := testDependencies(t) + h := &Handler{Dependencies: deps} if err := h.prepareSessionModelConfiguration(t.Context(), &input, saved, provider); err != nil { t.Fatal(err) } diff --git a/services/core/internal/api/session_model_defaults.go b/services/core/internal/api/session_model_defaults.go index e6f6f249a..ad4739d84 100644 --- a/services/core/internal/api/session_model_defaults.go +++ b/services/core/internal/api/session_model_defaults.go @@ -10,19 +10,6 @@ import ( "github.com/google/uuid" ) -// ModelProviderDefaults decrypts the deployment default model provider for a -// harness at Session creation, before the encrypted Session snapshot is -// committed. It returns nil when the harness has no default. -type ModelProviderDefaults func(context.Context, string) (*store.DeploymentModelProviderSnapshot, error) - -func WithModelProviderDefaults(resolve ModelProviderDefaults) Option { - return func(h *Handler) { h.modelProviderDefaults = resolve } -} - -type agentDefaultsStore interface { - GetAgentForSession(context.Context, string, string, bool) (store.SavedAgent, *v1.ModelProviderInput, error) -} - func (h *Handler) sessionAgentDefaults(ctx context.Context, tenant string, input sessionRequest) (*v1.SavedAgent, *v1.ModelProviderInput, error) { if input.AgentID == nil { return nil, nil, nil @@ -31,14 +18,7 @@ func (h *Handler) sessionAgentDefaults(ctx context.Context, tenant string, input return nil, nil, store.ErrNotFound } inherit := input.XAgentsCore == nil || input.XAgentsCore.ModelProvider == nil - var resource store.SavedAgent - var provider *v1.ModelProviderInput - var err error - if source, ok := h.store.(agentDefaultsStore); ok { - resource, provider, err = source.GetAgentForSession(ctx, tenant, *input.AgentID, inherit) - } else { - resource, err = h.lookupAgent(ctx, tenant, *input.AgentID) - } + resource, provider, err := h.Agents.GetAgentForSession(ctx, tenant, *input.AgentID, inherit) if err != nil { return nil, nil, err } @@ -91,7 +71,7 @@ func (h *Handler) resolveSessionExecution(ctx context.Context, input sessionRequ } } environment := input.Environment.Type - if provider == nil && h.modelProviderDefaults != nil && v1.ModelProviderAllowed(environment, v1.ModelProviderSourceDeployment) { + if provider == nil && v1.ModelProviderAllowed(environment, v1.ModelProviderSourceDeployment) { snapshot := input.deploymentDefaults if snapshot != nil { provider, revision = snapshot.Provider, snapshot.Revision diff --git a/services/core/internal/api/session_request_test.go b/services/core/internal/api/session_request_test.go index 2bb367ada..ea17672e1 100644 --- a/services/core/internal/api/session_request_test.go +++ b/services/core/internal/api/session_request_test.go @@ -38,8 +38,7 @@ func TestSessionCreateFieldPresence(t *testing.T) { {"array metadata", `,"metadata":[]`, 400, nil, ""}, } { t.Run(tc.name, func(t *testing.T) { - saved := &recordingStore{} - handler, _, _ := testHandler(t, WithExecution(&inputRecorder{ResourceStore: saved})) + handler, saved, _ := testHandler(t, admitSessions) body := `{"agent":{"model":"example"},"environment":{"type":"none"},"input":"Confirm the session metadata."` + tc.fields + `}` request := httptest.NewRequest(http.MethodPost, "/v1/agents/sessions", strings.NewReader(body)) request.Header.Set("Authorization", "Bearer test-api-key") diff --git a/services/core/internal/api/session_response.go b/services/core/internal/api/session_response.go index 5bf35c52c..ae7fc46c2 100644 --- a/services/core/internal/api/session_response.go +++ b/services/core/internal/api/session_response.go @@ -8,11 +8,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) -// WithEnvironmentRemoteURL uses the composition's validated daemon executor URL for self-hosted requests and output. -func WithEnvironmentRemoteURL(origin string) Option { - return func(h *Handler) { h.executorURL = origin } -} - func sessionResponse(session store.Session, executorURL string) (v1.Session, error) { var cfg configuration if err := json.Unmarshal(session.Configuration, &cfg); err != nil || cfg.Agent.ID == "" || cfg.Agent.Model == "" { diff --git a/services/core/internal/api/session_semantics_test.go b/services/core/internal/api/session_semantics_test.go index a10063f02..43841f7f4 100644 --- a/services/core/internal/api/session_semantics_test.go +++ b/services/core/internal/api/session_semantics_test.go @@ -14,7 +14,6 @@ import ( ) type emptyEventSessionStore struct { - ResourceStore tenant, id string reads int } @@ -28,17 +27,21 @@ func (s *emptyEventSessionStore) GetSession(_ context.Context, tenant, id string return store.Session{ID: id, TenantID: tenant, Configuration: json.RawMessage(`{"environment":{"type":"none"}}`)}, nil } +func (s *emptyEventSessionStore) AuditSessionOperation(context.Context, string, string, string) error { + return nil +} + func TestEmptyEventBatchAuthorizesWithoutExecutionEffects(t *testing.T) { for _, executor := range []bool{false, true} { t.Run(map[bool]string{false: "without executor", true: "with executor"}[executor], func(t *testing.T) { recorder := &inputRecorder{} - var options []Option - if executor { - options = append(options, WithExecution(recorder)) - } - h, base, tenant := testHandler(t, options...) sessions := &emptyEventSessionStore{} - base.ResourceStore = sessions + h, _, tenant := testHandler(t, func(d *Dependencies, f *testFakes) { + f.sessions.getSession, f.sessions.auditSessionOperation = sessions.GetSession, sessions.AuditSessionOperation + if executor { + recorder.admit(d, f) + } + }) request := func(id, token, key string) *httptest.ResponseRecorder { r := httptest.NewRequest(http.MethodPost, "/v1/agents/sessions/"+id+"/events", strings.NewReader(`{"events":[]}`)) r.Header.Set("Authorization", "Bearer "+token) diff --git a/services/core/internal/api/session_template.go b/services/core/internal/api/session_template.go index f7bad3326..db592bde9 100644 --- a/services/core/internal/api/session_template.go +++ b/services/core/internal/api/session_template.go @@ -50,7 +50,7 @@ func (h *Handler) resolveTemplateEnvironment(ctx context.Context, tenant string, if input.templateID == "" { return nil } - template, files, err := h.store.ResolveEnvironmentTemplate(ctx, tenant, input.templateID) + template, files, err := h.EnvironmentTemplates.ResolveEnvironmentTemplate(ctx, tenant, input.templateID) if err != nil { return err } diff --git a/services/core/internal/api/session_template_composition_test.go b/services/core/internal/api/session_template_composition_test.go index 27b85ac88..62fafb3fd 100644 --- a/services/core/internal/api/session_template_composition_test.go +++ b/services/core/internal/api/session_template_composition_test.go @@ -13,7 +13,6 @@ import ( ) type compositionTemplateStore struct { - ResourceStore template store.EnvironmentTemplate files []store.InitialFile } @@ -86,7 +85,7 @@ func TestTemplateInlineCompositionRules(t *testing.T) { } beforeTemplate, _ := json.Marshal(lookup.template) beforeFiles, _ := json.Marshal(lookup.files) - h := Handler{store: lookup} + h := templateHandler(t, lookup.ResolveEnvironmentTemplate) if err := h.resolveTemplateEnvironment(t.Context(), "tenant", &input); err != nil { t.Fatal(err) } @@ -129,7 +128,7 @@ func TestTemplateCompositionDoesNotAliasChangedInputs(t *testing.T) { beforeInlineFiles, _ := json.Marshal(originalFiles) beforeTemplate, _ := json.Marshal(lookup.template) beforeFiles, _ := json.Marshal(lookup.files) - h := Handler{store: lookup} + h := templateHandler(t, lookup.ResolveEnvironmentTemplate) if err := h.resolveTemplateEnvironment(t.Context(), "tenant", &input); err != nil { t.Fatal(err) } @@ -160,7 +159,7 @@ func TestTemplateCompositionRevalidatesCombinedSetupLimit(t *testing.T) { if lookup.template.Initialization.Validate() != nil || input.initialization.Validate() != nil { t.Fatal("each side must be valid independently") } - h := Handler{store: lookup} + h := templateHandler(t, lookup.ResolveEnvironmentTemplate) if err := h.resolveTemplateEnvironment(t.Context(), "tenant", &input); err == nil { t.Fatalf("combined setup limit bypassed for %s", field) } @@ -198,7 +197,7 @@ func TestTemplateFilesReplacementDoesNotCombineCounts(t *testing.T) { } raw, _ := json.Marshal(wire) input := compositionRequest(t, `,"files":`+string(raw)) - h := Handler{store: lookup} + h := templateHandler(t, lookup.ResolveEnvironmentTemplate) if err := h.resolveTemplateEnvironment(t.Context(), "tenant", &input); err != nil || len(input.initialFiles) != 30 { t.Fatalf("file lists were combined: count=%d err=%v", len(input.initialFiles), err) } diff --git a/services/core/internal/api/session_template_null_test.go b/services/core/internal/api/session_template_null_test.go index 3f71cc70e..695789cfa 100644 --- a/services/core/internal/api/session_template_null_test.go +++ b/services/core/internal/api/session_template_null_test.go @@ -35,7 +35,7 @@ func TestTemplateNullSelectionRetainsInheritedCapabilitiesAndPolicy(t *testing.T t.Fatal(err) } before, _ := json.Marshal(template.Initialization) - h := Handler{store: lookup} + h := templateHandler(t, lookup.ResolveEnvironmentTemplate) if err := h.resolveTemplateEnvironment(t.Context(), "tenant", &input); err != nil { t.Fatal(err) } @@ -87,7 +87,7 @@ func TestTemplateNullSelectionDoesNotBypassValidation(t *testing.T) { t.Fatal("invalid nonnull override accepted", fields) } } - h := Handler{store: &templateLookupStore{network: "disabled"}} + h := templateHandler(t, (&templateLookupStore{network: "disabled"}).ResolveEnvironmentTemplate) input := compositionRequest(t, `,"network":{"access":"enabled"},"skills":null,"plugins":null,"capability_directories":null`) if err := h.resolveTemplateEnvironment(t.Context(), "tenant", &input); err == nil { t.Fatal("capability null overrides bypassed network narrowing") diff --git a/services/core/internal/api/session_write_audit.go b/services/core/internal/api/session_write_audit.go index 632e7ace6..52cb4e3be 100644 --- a/services/core/internal/api/session_write_audit.go +++ b/services/core/internal/api/session_write_audit.go @@ -1,27 +1,16 @@ package api import ( - "context" - "errors" "net/http" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/writeaudit" ) -type sessionWriteAuditor interface { - AuditSessionOperation(context.Context, string, string, string) error -} - func (h *Handler) auditSessionOperation(w http.ResponseWriter, r *http.Request, sessionID, action string) bool { if _, ok := writeaudit.FromContext(r.Context()); !ok { return true } - auditor, ok := h.store.(sessionWriteAuditor) - if !ok { - writeStoreError(w, r, errors.New("session write audit is unavailable")) - return false - } - if err := auditor.AuditSessionOperation(r.Context(), tenantID(r), sessionID, action); err != nil { + if err := h.Sessions.AuditSessionOperation(r.Context(), tenantID(r), sessionID, action); err != nil { writeStoreError(w, r, err) return false } diff --git a/services/core/internal/api/session_write_audit_test.go b/services/core/internal/api/session_write_audit_test.go index f48026f87..13002c6fe 100644 --- a/services/core/internal/api/session_write_audit_test.go +++ b/services/core/internal/api/session_write_audit_test.go @@ -16,19 +16,6 @@ import ( "github.com/google/uuid" ) -// General wire fixtures do not persist resources; dedicated audit fixtures below -// check the no-op/replay boundary independently of business Store auditing. -func (s *recordingStore) AuditSessionOperation(ctx context.Context, tenant, session, action string) error { - if auditor, ok := s.ResourceStore.(sessionWriteAuditor); ok { - return auditor.AuditSessionOperation(ctx, tenant, session, action) - } - return nil -} - -func (f *streamFixture) AuditSessionOperation(context.Context, string, string, string) error { - return nil -} - type auditedSessionFixture struct { streamFixture err error @@ -58,7 +45,15 @@ func TestSessionAuditOnlyRoutesFailClosed(t *testing.T) { if fail { f.err = errors.New("audit unavailable") } - h := &Handler{store: f} + deps, fakes := testDependencies(t) + fakes.sessions.auditSessionOperation = f.AuditSessionOperation + if route != "stream-replay" { + fakes.sessions.getSession = f.GetSession + } + if route != "empty-events" { + fakes.sessions.findSessionCreation = f.FindSessionCreation + } + h := &Handler{Dependencies: deps} ctx := context.WithValue(t.Context(), principalContextKey{}, identity.Principal{ProjectScope: identity.ProjectScope{TenantID: tenant}, SubjectKind: "service_account", SubjectID: "test"}) ctx = writeaudit.WithSource(ctx, writeaudit.Source{TenantID: tenant, RequestID: "request", TraceID: "trace"}) routeContext := chi.NewRouteContext() diff --git a/services/core/internal/api/skills.go b/services/core/internal/api/skills.go index 38a7972ad..6cb0ea051 100644 --- a/services/core/internal/api/skills.go +++ b/services/core/internal/api/skills.go @@ -10,7 +10,8 @@ import ( "github.com/go-chi/chi/v5" ) -type SkillStore interface { +// Skills manages Skills and their immutable versions. +type Skills interface { CreateSkill(context.Context, string, []byte) (store.Skill, error) GetSkill(context.Context, string, string) (store.Skill, error) UpdateSkillDefault(context.Context, string, string, string) (store.Skill, error) @@ -24,8 +25,6 @@ type SkillStore interface { ListSkillVersions(context.Context, string, string, string, int, bool) (store.SkillVersionPage, error) } -func WithSkills(s SkillStore) Option { return func(h *Handler) { h.skills = s } } - func (h *Handler) registerSkillRoutes(r chi.Router) { r.Post("/v1/skills", h.createSkill) r.Get("/v1/skills", h.listSkills) @@ -42,14 +41,6 @@ func (h *Handler) registerSkillRoutes(r chi.Router) { r.Head("/v1/skills/{skill_id}/versions/{version}/content", methodNotAllowed) } -func (h *Handler) skillsReady(w http.ResponseWriter) bool { - if h.skills == nil { - writeError(w, http.StatusServiceUnavailable, "skill_storage_unavailable", "Skill storage is unavailable.") - return false - } - return true -} - // @Summary Retrieve Skill metadata // @Description Returns tenant-owned metadata without decrypting contents or starting Runtime. No Beta header is required. // @Tags Skills @@ -59,10 +50,7 @@ func (h *Handler) skillsReady(w http.ResponseWriter) bool { // @Success 200 {object} v1.Skill // @Router /skills/{skill_id} [get] func (h *Handler) getSkill(w http.ResponseWriter, r *http.Request) { - if !h.skillsReady(w) { - return - } - value, err := h.skills.GetSkill(r.Context(), tenantID(r), chi.URLParam(r, "skill_id")) + value, err := h.Skills.GetSkill(r.Context(), tenantID(r), chi.URLParam(r, "skill_id")) if err != nil { writeStoreError(w, r, err) return @@ -81,9 +69,6 @@ func (h *Handler) getSkill(w http.ResponseWriter, r *http.Request) { // @Success 200 {object} v1.Skill // @Router /skills/{skill_id} [post] func (h *Handler) updateSkill(w http.ResponseWriter, r *http.Request) { - if !h.skillsReady(w) { - return - } body, ok := readJSONBodyLimit(w, r, 64<<10, "Request exceeds 64 KiB.") if !ok { return @@ -93,7 +78,7 @@ func (h *Handler) updateSkill(w http.ResponseWriter, r *http.Request) { writeStoreError(w, r, store.ErrInvalidInput) return } - value, err := h.skills.UpdateSkillDefault(r.Context(), tenantID(r), chi.URLParam(r, "skill_id"), input.DefaultVersion) + value, err := h.Skills.UpdateSkillDefault(r.Context(), tenantID(r), chi.URLParam(r, "skill_id"), input.DefaultVersion) if err != nil { writeStoreError(w, r, err) return @@ -110,11 +95,8 @@ func (h *Handler) updateSkill(w http.ResponseWriter, r *http.Request) { // @Success 200 {object} v1.SkillDeleted // @Router /skills/{skill_id} [delete] func (h *Handler) deleteSkill(w http.ResponseWriter, r *http.Request) { - if !h.skillsReady(w) { - return - } id := chi.URLParam(r, "skill_id") - if err := h.skills.DeleteSkill(r.Context(), tenantID(r), id); err != nil { + if err := h.Skills.DeleteSkill(r.Context(), tenantID(r), id); err != nil { writeStoreError(w, r, err) return } @@ -130,10 +112,7 @@ func (h *Handler) deleteSkill(w http.ResponseWriter, r *http.Request) { // @Success 200 {object} v1.SkillVersion // @Router /skills/{skill_id}/versions/{version} [get] func (h *Handler) getSkillVersion(w http.ResponseWriter, r *http.Request) { - if !h.skillsReady(w) { - return - } - value, err := h.skills.GetSkillVersion(r.Context(), tenantID(r), chi.URLParam(r, "skill_id"), chi.URLParam(r, "version")) + value, err := h.Skills.GetSkillVersion(r.Context(), tenantID(r), chi.URLParam(r, "skill_id"), chi.URLParam(r, "version")) if err != nil { writeStoreError(w, r, err) return @@ -151,10 +130,7 @@ func (h *Handler) getSkillVersion(w http.ResponseWriter, r *http.Request) { // @Success 200 {object} v1.SkillVersionDeleted // @Router /skills/{skill_id}/versions/{version} [delete] func (h *Handler) deleteSkillVersion(w http.ResponseWriter, r *http.Request) { - if !h.skillsReady(w) { - return - } - value, err := h.skills.DeleteSkillVersion(r.Context(), tenantID(r), chi.URLParam(r, "skill_id"), chi.URLParam(r, "version")) + value, err := h.Skills.DeleteSkillVersion(r.Context(), tenantID(r), chi.URLParam(r, "skill_id"), chi.URLParam(r, "version")) if err != nil { writeStoreError(w, r, err) return diff --git a/services/core/internal/api/skills_list.go b/services/core/internal/api/skills_list.go index 2daa63146..a54a6a9e1 100644 --- a/services/core/internal/api/skills_list.go +++ b/services/core/internal/api/skills_list.go @@ -18,14 +18,11 @@ import ( // @Success 200 {object} v1.SkillList // @Router /skills [get] func (h *Handler) listSkills(w http.ResponseWriter, r *http.Request) { - if !h.skillsReady(w) { - return - } options, ok := readPage(w, r) if !ok { return } - page, err := h.skills.ListSkills(r.Context(), tenantID(r), options.after, options.limit, options.ascending) + page, err := h.Skills.ListSkills(r.Context(), tenantID(r), options.after, options.limit, options.ascending) if err != nil { writeStoreError(w, r, err) return @@ -53,14 +50,11 @@ func (h *Handler) listSkills(w http.ResponseWriter, r *http.Request) { // @Success 200 {object} v1.SkillVersionList // @Router /skills/{skill_id}/versions [get] func (h *Handler) listSkillVersions(w http.ResponseWriter, r *http.Request) { - if !h.skillsReady(w) { - return - } options, ok := readPage(w, r) if !ok { return } - page, err := h.skills.ListSkillVersions(r.Context(), tenantID(r), chi.URLParam(r, "skill_id"), options.after, options.limit, options.ascending) + page, err := h.Skills.ListSkillVersions(r.Context(), tenantID(r), chi.URLParam(r, "skill_id"), options.after, options.limit, options.ascending) if err != nil { writeStoreError(w, r, err) return diff --git a/services/core/internal/api/skills_transfer.go b/services/core/internal/api/skills_transfer.go index 12d645301..7463d1de7 100644 --- a/services/core/internal/api/skills_transfer.go +++ b/services/core/internal/api/skills_transfer.go @@ -39,9 +39,6 @@ func (h *Handler) createSkillVersion(w http.ResponseWriter, r *http.Request) { } func (h *Handler) uploadSkill(w http.ResponseWriter, r *http.Request, version bool) { - if !h.skillsReady(w) { - return - } deadline := time.Now().Add(sourceTransferTimeout) controller := http.NewResponseController(w) if controller.SetReadDeadline(deadline) != nil || controller.SetWriteDeadline(deadline) != nil { @@ -62,14 +59,14 @@ func (h *Handler) uploadSkill(w http.ResponseWriter, r *http.Request, version bo return } if version { - result, err := h.skills.CreateSkillVersion(ctx, tenantID(r), chi.URLParam(r, "skill_id"), archive, makeDefault) + result, err := h.Skills.CreateSkillVersion(ctx, tenantID(r), chi.URLParam(r, "skill_id"), archive, makeDefault) if err != nil { writeStoreError(w, r, err) return } writeJSON(w, http.StatusOK, skillVersionResponse(result)) } else { - result, err := h.skills.CreateSkill(ctx, tenantID(r), archive) + result, err := h.Skills.CreateSkill(ctx, tenantID(r), archive) if err != nil { writeStoreError(w, r, err) return @@ -87,17 +84,14 @@ func (h *Handler) uploadSkill(w http.ResponseWriter, r *http.Request, version bo // @Success 200 {file} binary // @Router /skills/{skill_id}/content [get] func (h *Handler) skillContent(w http.ResponseWriter, r *http.Request) { - if !h.skillsReady(w) { - return - } serveStoredContent(w, r, func(ctx context.Context, consume func(string, int64, io.Reader) error) error { var value store.SkillVersion var body []byte var err error if version := chi.URLParam(r, "version"); version != "" { - value, body, err = h.skills.ReadSkillVersion(ctx, tenantID(r), chi.URLParam(r, "skill_id"), version) + value, body, err = h.Skills.ReadSkillVersion(ctx, tenantID(r), chi.URLParam(r, "skill_id"), version) } else { - value, body, err = h.skills.ReadDefaultSkillVersion(ctx, tenantID(r), chi.URLParam(r, "skill_id")) + value, body, err = h.Skills.ReadDefaultSkillVersion(ctx, tenantID(r), chi.URLParam(r, "skill_id")) } if err != nil { return err diff --git a/services/core/internal/api/source_files.go b/services/core/internal/api/source_files.go index 764380fb6..77338d275 100644 --- a/services/core/internal/api/source_files.go +++ b/services/core/internal/api/source_files.go @@ -11,7 +11,8 @@ import ( "github.com/go-chi/chi/v5" ) -type SourceFileStore interface { +// Files manages project-owned source Files and streams their bytes. +type Files interface { CreateSourceFile(context.Context, string, func(io.Writer) (store.SourceFileUpload, error)) (store.SourceFile, error) GetSourceFile(context.Context, string, string) (store.SourceFile, error) ListSourceFiles(context.Context, string, string, int, bool, *string) (store.SourceFilePage, error) @@ -19,18 +20,6 @@ type SourceFileStore interface { DeleteSourceFile(context.Context, string, string) error } -func WithSourceFiles(s SourceFileStore) Option { - return func(h *Handler) { h.sourceFiles = s } -} - -func (h *Handler) sourceFilesAvailable(w http.ResponseWriter) bool { - if h.sourceFiles == nil { - writeError(w, http.StatusServiceUnavailable, "file_storage_unavailable", "Source file storage is unavailable.") - return false - } - return true -} - // @Summary Retrieve source file metadata // @Description Returns immutable project-owned user_data file metadata. No Beta header is required. Other purposes, expiration and full hosted status/error semantics remain unimplemented or unverified. // @Tags Files @@ -41,12 +30,9 @@ func (h *Handler) sourceFilesAvailable(w http.ResponseWriter) bool { // @Failure 400,401,404,500,503 {object} v1.ErrorResponse // @Router /files/{file_id} [get] func (h *Handler) getSourceFile(w http.ResponseWriter, r *http.Request) { - if !h.sourceFilesAvailable(w) { - return - } ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) defer cancel() - file, err := h.sourceFiles.GetSourceFile(ctx, tenantID(r), chi.URLParam(r, "file_id")) + file, err := h.Files.GetSourceFile(ctx, tenantID(r), chi.URLParam(r, "file_id")) if err != nil { writeStoreError(w, r, err, "id") return @@ -64,13 +50,10 @@ func (h *Handler) getSourceFile(w http.ResponseWriter, r *http.Request) { // @Failure 400,401,404,500,503 {object} v1.ErrorResponse // @Router /files/{file_id} [delete] func (h *Handler) deleteSourceFile(w http.ResponseWriter, r *http.Request) { - if !h.sourceFilesAvailable(w) { - return - } ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) defer cancel() id := chi.URLParam(r, "file_id") - if err := h.sourceFiles.DeleteSourceFile(ctx, tenantID(r), id); err != nil { + if err := h.Files.DeleteSourceFile(ctx, tenantID(r), id); err != nil { writeStoreError(w, r, err, "id") return } diff --git a/services/core/internal/api/source_files_content.go b/services/core/internal/api/source_files_content.go index 9abf81c81..194e7486c 100644 --- a/services/core/internal/api/source_files_content.go +++ b/services/core/internal/api/source_files_content.go @@ -20,12 +20,9 @@ import ( // @Failure 400,401,404,500,503 {object} v1.ErrorResponse // @Router /files/{file_id}/content [get] func (h *Handler) sourceFileContent(w http.ResponseWriter, r *http.Request) { - if !h.sourceFilesAvailable(w) { - return - } ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) defer cancel() - _, err := h.sourceFiles.GetSourceFile(ctx, tenantID(r), chi.URLParam(r, "file_id")) + _, err := h.Files.GetSourceFile(ctx, tenantID(r), chi.URLParam(r, "file_id")) if err != nil { writeStoreError(w, r, err, "id") return diff --git a/services/core/internal/api/source_files_errors_test.go b/services/core/internal/api/source_files_errors_test.go index 3b9b2552b..d9d3b8537 100644 --- a/services/core/internal/api/source_files_errors_test.go +++ b/services/core/internal/api/source_files_errors_test.go @@ -24,7 +24,7 @@ func TestSourceFileMissingErrorParameters(t *testing.T) { } { t.Run(tc.method+tc.path, func(t *testing.T) { f := &sourceFilesFixture{listErr: fmt.Errorf("wrapped: %w", store.ErrNotFound)} - h, _ := environmentFileCreateHandler(t, WithSourceFiles(f)) + h, _ := environmentFileCreateHandler(t, f.wire) server := newSourceFileServer(t, h) status, raw := sourceRequest(t, server, tc.method, tc.path, "files-key", "", nil) var body map[string]map[string]any diff --git a/services/core/internal/api/source_files_list.go b/services/core/internal/api/source_files_list.go index 12d94eb05..14c2e95a0 100644 --- a/services/core/internal/api/source_files_list.go +++ b/services/core/internal/api/source_files_list.go @@ -19,14 +19,11 @@ import ( // @Failure 400,401,404,500,503 {object} v1.ErrorResponse // @Router /files [get] func (h *Handler) listSourceFiles(w http.ResponseWriter, r *http.Request) { - if !h.sourceFilesAvailable(w) { - return - } options, purpose, ok := readSourceFilePage(w, r) if !ok { return } - page, err := h.sourceFiles.ListSourceFiles(r.Context(), tenantID(r), options.after, options.limit, options.ascending, purpose) + page, err := h.Files.ListSourceFiles(r.Context(), tenantID(r), options.after, options.limit, options.ascending, purpose) if err != nil { writeStoreError(w, r, err, "after") return diff --git a/services/core/internal/api/source_files_list_test.go b/services/core/internal/api/source_files_list_test.go index 5106b5496..f63bae6e0 100644 --- a/services/core/internal/api/source_files_list_test.go +++ b/services/core/internal/api/source_files_list_test.go @@ -14,7 +14,7 @@ import ( func TestSourceFileListParametersAndEnvelope(t *testing.T) { f := &sourceFilesFixture{} - h, _ := environmentFileCreateHandler(t, WithSourceFiles(f)) + h, _ := environmentFileCreateHandler(t, f.wire) server := newSourceFileServer(t, h) status, raw := sourceRequest(t, server, http.MethodGet, "/v1/files", "files-key", "", nil) @@ -63,7 +63,7 @@ func TestSourceFileListRejectsInvalidQueriesBeforeStorage(t *testing.T) { } { t.Run(test.query, func(t *testing.T) { f := &sourceFilesFixture{} - h, _ := environmentFileCreateHandler(t, WithSourceFiles(f)) + h, _ := environmentFileCreateHandler(t, f.wire) w := httptest.NewRecorder() r := httptest.NewRequest(http.MethodGet, "/v1/files?"+test.query, nil) r.Header.Set("Authorization", "Bearer files-key") @@ -78,7 +78,7 @@ func TestSourceFileListRejectsInvalidQueriesBeforeStorage(t *testing.T) { func TestSourceFileListIgnoresUnknownKeysAndEmptyPurpose(t *testing.T) { f := &sourceFilesFixture{} - h, env := environmentFileCreateHandler(t, WithSourceFiles(f)) + h, env := environmentFileCreateHandler(t, f.wire) server := newSourceFileServer(t, h) want, wantBody := sourceRequest(t, server, http.MethodGet, "/v1/files?after=file-a&limit=2&order=asc", "files-key", "", nil) for _, query := range []string{"purpose=", "unknown=1", "tenant_id=foreign", "purpose[]=batch", "unknown=1&unknown=2&purpose="} { @@ -91,7 +91,7 @@ func TestSourceFileListIgnoresUnknownKeysAndEmptyPurpose(t *testing.T) { func TestSourceFileListMapsStorageErrors(t *testing.T) { f := &sourceFilesFixture{listErr: store.ErrNotFound} - h, _ := environmentFileCreateHandler(t, WithSourceFiles(f)) + h, _ := environmentFileCreateHandler(t, f.wire) server := newSourceFileServer(t, h) status, _ := sourceRequest(t, server, http.MethodGet, "/v1/files?after=file-missing", "files-key", "", nil) if status != http.StatusNotFound || f.listCalls != 1 { @@ -103,7 +103,7 @@ func TestSourceFileListPurposeValidationBeforeCursorLookup(t *testing.T) { for _, purpose := range []string{"", "user_data", "assistants", "batch", "fine-tune", "vision", "evals", "assistants_output", "batch_output", "fine-tune-results", "unknown", "USER_DATA"} { t.Run("purpose="+purpose, func(t *testing.T) { f := &sourceFilesFixture{listErr: store.ErrNotFound} - h, _ := environmentFileCreateHandler(t, WithSourceFiles(f)) + h, _ := environmentFileCreateHandler(t, f.wire) server := newSourceFileServer(t, h) status, raw := sourceRequest(t, server, http.MethodGet, "/v1/files?after=file-missing&purpose="+purpose, "files-key", "", nil) wantStatus, wantParam, wantCalls := http.StatusNotFound, "after", 1 diff --git a/services/core/internal/api/source_files_test.go b/services/core/internal/api/source_files_test.go index 7a37e198d..37b69cbfa 100644 --- a/services/core/internal/api/source_files_test.go +++ b/services/core/internal/api/source_files_test.go @@ -87,6 +87,12 @@ func (f *sourceFilesFixture) DeleteSourceFile(ctx context.Context, tenant, id st return nil } +// wire serves the Files area from f. +func (f *sourceFilesFixture) wire(_ *Dependencies, fakes *testFakes) { + fakes.files.createSourceFile, fakes.files.getSourceFile, fakes.files.listSourceFiles = f.CreateSourceFile, f.GetSourceFile, f.ListSourceFiles + fakes.files.readSourceFile, fakes.files.deleteSourceFile = f.ReadSourceFile, f.DeleteSourceFile +} + func sourceMultipart(t *testing.T, fields []string, data []byte) ([]byte, string) { t.Helper() var body bytes.Buffer @@ -138,7 +144,7 @@ func sourceRequest(t *testing.T, server *httptest.Server, method, path, key, con func TestSourceFilesPublicLifecycleAndEnvironmentCopy(t *testing.T) { for _, fields := range [][]string{{"file", "purpose=user_data"}, {"purpose=user_data", "file"}} { f := &sourceFilesFixture{} - h, env := environmentFileCreateHandler(t, WithSourceFiles(f)) + h, env := environmentFileCreateHandler(t, f.wire) server := httptest.NewServer(h) t.Cleanup(server.Close) data := []byte{0, 1, 255, 7} @@ -185,7 +191,7 @@ func TestSourceFilesPublicLifecycleAndEnvironmentCopy(t *testing.T) { func TestSourceFilesRejectIncompleteOrUnsupportedMultipart(t *testing.T) { for _, fields := range [][]string{{"file"}, {"purpose=user_data"}, {"file", "file", "purpose=user_data"}, {"file", "purpose=user_data", "purpose=user_data"}, {"file", "purpose=batch"}, {"file", "purpose=user_data", "expires_after[seconds]=3600"}, {"file", "purpose=user_data", "extra=x"}} { f := &sourceFilesFixture{} - h, _ := environmentFileCreateHandler(t, WithSourceFiles(f)) + h, _ := environmentFileCreateHandler(t, f.wire) server := httptest.NewServer(h) body, contentType := sourceMultipart(t, fields, []byte("discard")) status, _ := sourceRequest(t, server, "POST", "/v1/files", "files-key", contentType, body) @@ -195,7 +201,7 @@ func TestSourceFilesRejectIncompleteOrUnsupportedMultipart(t *testing.T) { } } f := &sourceFilesFixture{} - h, _ := environmentFileCreateHandler(t, WithSourceFiles(f)) + h, _ := environmentFileCreateHandler(t, f.wire) server := httptest.NewServer(h) defer server.Close() body, contentType := sourceMultipart(t, []string{"purpose=user_data", "file"}, []byte("truncated")) @@ -209,7 +215,7 @@ func TestSourceFilesRejectIncompleteOrUnsupportedMultipart(t *testing.T) { func TestEnvironmentSourceCopyEnforcesScopeUnionAndSize(t *testing.T) { f := &sourceFilesFixture{file: store.SourceFile{ID: "file-" + uuid.NewString(), SizeBytes: proto.WorkspaceWriteMaxBytes + 1}} - h, env := environmentFileCreateHandler(t, WithSourceFiles(f)) + h, env := environmentFileCreateHandler(t, f.wire) f.tenant = env.environment.TenantID body := `{"type":"file_id","file_id":"` + f.file.ID + `","path":"/workspace/source.bin"}` if got := requestCreateEnvironmentFile(h, env.environment.ID, body, "other-key"); got.Code != 404 || f.reads != 0 { diff --git a/services/core/internal/api/source_files_upload.go b/services/core/internal/api/source_files_upload.go index 339ff5cc7..97c39b919 100644 --- a/services/core/internal/api/source_files_upload.go +++ b/services/core/internal/api/source_files_upload.go @@ -25,9 +25,6 @@ const sourceTransferTimeout = 5 * time.Minute // @Failure 400,401,413,500,503 {object} v1.ErrorResponse // @Router /files [post] func (h *Handler) createSourceFile(w http.ResponseWriter, r *http.Request) { - if !h.sourceFilesAvailable(w) { - return - } deadline := time.Now().Add(sourceTransferTimeout) controller := http.NewResponseController(w) if controller.SetReadDeadline(deadline) != nil || controller.SetWriteDeadline(deadline) != nil { @@ -37,7 +34,7 @@ func (h *Handler) createSourceFile(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithDeadline(r.Context(), deadline) defer cancel() r.Body = http.MaxBytesReader(w, r.Body, store.MaxSourceFileBytes+(64<<10)) - file, err := h.sourceFiles.CreateSourceFile(ctx, tenantID(r), func(dst io.Writer) (store.SourceFileUpload, error) { + file, err := h.Files.CreateSourceFile(ctx, tenantID(r), func(dst io.Writer) (store.SourceFileUpload, error) { return readSourceUpload(r, dst) }) if err != nil { diff --git a/services/core/internal/api/stream.go b/services/core/internal/api/stream.go index a79e043fb..75dc197ef 100644 --- a/services/core/internal/api/stream.go +++ b/services/core/internal/api/stream.go @@ -16,9 +16,12 @@ import ( "github.com/google/uuid" ) -type eventStore interface { +// SessionEvents reads a Session's committed event journal, and the Session +// projection with its event cursor from one snapshot. +type SessionEvents interface { SessionEventCursor(context.Context, string, string) (int64, error) ListSessionEvents(context.Context, string, string, int64) ([]store.SessionChange, error) + SessionStreamSnapshot(context.Context, string, string) (store.Session, int64, error) } // @Summary Stream live Session events @@ -33,27 +36,22 @@ type eventStore interface { // @Failure 400,401,404,500,503 {object} v1.ErrorResponse // @Router /agents/sessions/{session_id}/events [get] func (h *Handler) streamEvents(w http.ResponseWriter, r *http.Request) { - events, ok := h.store.(eventStore) - if !ok { - writeError(w, http.StatusServiceUnavailable, "stream_unavailable", "Live events are unavailable.") - return - } id, tenant := chi.URLParam(r, "session_id"), tenantID(r) - session, err := h.store.GetSession(r.Context(), tenant, id) + session, err := h.Sessions.GetSession(r.Context(), tenant, id) if err != nil { writeStoreError(w, r, err) return } - if _, err = sessionResponse(session, h.executorURL); err != nil { + if _, err = sessionResponse(session, h.executorURL()); err != nil { writeStoreError(w, r, err) return } - cursor, err := events.SessionEventCursor(r.Context(), tenant, id) + cursor, err := h.SessionEvents.SessionEventCursor(r.Context(), tenant, id) if err != nil { writeStoreError(w, r, err) return } - h.serveSessionEvents(w, r, events, session, cursor, nil, http.StatusOK, nil) + h.serveSessionEvents(w, r, session, cursor, nil, http.StatusOK, nil) } // streamSettlement reads a creation stream's committed Session projection and @@ -72,7 +70,7 @@ type streamSettlement func(context.Context) (settled bool, cursor int64, err err // sends only events up to that cursor before ending. Later work drained before // that read can still be sent. The projection is re-read after a sent Session // status event and otherwise at most once a second. -func (h *Handler) serveSessionEvents(w http.ResponseWriter, r *http.Request, events eventStore, session store.Session, cursor int64, initial *v1.SessionEvent, status int, settlement streamSettlement) { +func (h *Handler) serveSessionEvents(w http.ResponseWriter, r *http.Request, session store.Session, cursor int64, initial *v1.SessionEvent, status int, settlement streamSettlement) { id, tenant := session.ID, tenantID(r) write := openEventStream(w, status) if write == nil { @@ -114,7 +112,7 @@ func (h *Handler) serveSessionEvents(w http.ResponseWriter, r *http.Request, eve if !authorized() { return } - changes, err := events.ListSessionEvents(r.Context(), tenant, id, cursor) + changes, err := h.SessionEvents.ListSessionEvents(r.Context(), tenant, id, cursor) if errors.Is(err, store.ErrNotFound) { return } @@ -126,7 +124,7 @@ func (h *Handler) serveSessionEvents(w http.ResponseWriter, r *http.Request, eve if limit >= 0 && change.Sequence > limit { return } - event, err := streamResponse(session, change, h.executorURL) + event, err := streamResponse(session, change, h.executorURL()) if err != nil { writeStreamFailure(write, id) return diff --git a/services/core/internal/api/stream_authority_test.go b/services/core/internal/api/stream_authority_test.go index 19d585ef1..1573274ad 100644 --- a/services/core/internal/api/stream_authority_test.go +++ b/services/core/internal/api/stream_authority_test.go @@ -18,7 +18,7 @@ import ( ) type streamAuthorityResolver struct { - ProjectAPIKeyResolver + keys fixtureKeyResolver unavailable atomic.Bool calls atomic.Int32 } @@ -28,7 +28,7 @@ func (r *streamAuthorityResolver) ResolveProjectAPIKey(ctx context.Context, dige if r.unavailable.Load() { return store.ProjectAPIKeyBinding{}, errors.New("resolver unavailable") } - return r.ProjectAPIKeyResolver.ResolveProjectAPIKey(ctx, digest) + return r.keys.ResolveProjectAPIKey(ctx, digest) } type busyAuthorityStream struct { @@ -47,17 +47,13 @@ func (s *busyAuthorityStream) ListSessionEvents(ctx context.Context, _, _ string func TestBusyStreamRechecksAuthorityAndFailsClosed(t *testing.T) { key := callerBinding() key.TokenSHA256 = runtimedevice.HashCredential("stream") - auth, err := NewAuthenticator([]APIKey{key}) - if err != nil { - t.Fatal(err) - } - resolver := &streamAuthorityResolver{ProjectAPIKeyResolver: auth.keys} - auth.keys = resolver + resolver := &streamAuthorityResolver{keys: projectKeys(t, key)} f := &busyAuthorityStream{streamFixture: &streamFixture{session: store.Session{ID: uuid.NewString(), TenantID: key.TenantID, CreatedAt: time.Now(), Metadata: map[string]string{}, Configuration: json.RawMessage(`{"agent":{"id":"agent_fixture","model":"fixture","tools":[]},"environment":{"type":"none"}}`)}}} - h, err := NewHandler(f, auth, "codex") - if err != nil { - t.Fatal(err) - } + deps, fakes := testDependencies(t) + fakes.projects.resolveProjectAPIKey = resolver.ResolveProjectAPIKey + f.serve(fakes) + fakes.sessionEvents.listSessionEvents = f.ListSessionEvents + h := newTestHandler(t, deps) server := httptest.NewServer(h) defer server.Close() ctx, cancel := context.WithCancel(t.Context()) diff --git a/services/core/internal/api/stream_test.go b/services/core/internal/api/stream_test.go index 95e7bc174..74bc33726 100644 --- a/services/core/internal/api/stream_test.go +++ b/services/core/internal/api/stream_test.go @@ -18,8 +18,8 @@ import ( "github.com/google/uuid" ) +// streamFixture serves one Session and its event stream. type streamFixture struct { - ResourceStore session store.Session mu sync.Mutex changes []store.SessionChange @@ -59,17 +59,19 @@ func (f *streamFixture) ListSessionEvents(_ context.Context, _, _ string, cursor return changes, nil } +// serve answers Session reads and the event stream from f. +func (f *streamFixture) serve(fakes *testFakes) { + fakes.sessions.getSession = f.GetSession + fakes.sessionEvents.sessionEventCursor, fakes.sessionEvents.sessionStreamSnapshot, fakes.sessionEvents.listSessionEvents = f.SessionEventCursor, f.SessionStreamSnapshot, f.ListSessionEvents +} + func TestLiveStreamAuthDisconnectRecoveryAndServerDeadline(t *testing.T) { f := &streamFixture{session: store.Session{ID: uuid.NewString(), TenantID: uuid.NewString(), CreatedAt: time.Now(), Metadata: map[string]string{}, Configuration: json.RawMessage(`{"agent":{"id":"agent_test","model":"model","tools":[]},"environment":{"type":"none"}}`)}} - auth, err := NewAuthenticator([]APIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential("key"), TenantID: f.session.TenantID}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential("foreign"), TenantID: uuid.NewString()}}) - if err != nil { - t.Fatal(err) - } - h, err := NewHandler(f, auth, "codex") - if err != nil { - t.Fatal(err) - } + 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("key"), TenantID: f.session.TenantID}, APIKey{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential("foreign"), TenantID: uuid.NewString()}).ResolveProjectAPIKey + f.serve(fakes) + h := newTestHandler(t, deps) done := make(chan struct{}, 8) server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { done <- struct{}{} }() diff --git a/services/core/internal/api/subagent_turns.go b/services/core/internal/api/subagent_turns.go index ffea97556..ed3e863b3 100644 --- a/services/core/internal/api/subagent_turns.go +++ b/services/core/internal/api/subagent_turns.go @@ -19,10 +19,7 @@ import ( // @Failure 400,401,404,500,503 {object} v1.ErrorResponse // @Router /agents/sessions/{session_id}/subagents/{subagent_id}/turns/{turn_id} [get] func (h *Handler) getSubagentTurn(w http.ResponseWriter, r *http.Request) { - if !h.subagentsReady(w) { - return - } - value, err := h.subagents.GetSubagentTurn(r.Context(), tenantID(r), chi.URLParam(r, "session_id"), chi.URLParam(r, "subagent_id"), chi.URLParam(r, "turn_id")) + value, err := h.Subagents.GetSubagentTurn(r.Context(), tenantID(r), chi.URLParam(r, "session_id"), chi.URLParam(r, "subagent_id"), chi.URLParam(r, "turn_id")) if err != nil { writeStoreError(w, r, err) return @@ -46,10 +43,10 @@ func (h *Handler) getSubagentTurn(w http.ResponseWriter, r *http.Request) { // @Router /agents/sessions/{session_id}/subagents/{subagent_id}/turns [get] func (h *Handler) listSubagentTurns(w http.ResponseWriter, r *http.Request) { options, ok := readPage(w, r) - if !ok || !h.subagentsReady(w) { + if !ok { return } - page, err := h.subagents.ListSubagentTurns(r.Context(), tenantID(r), chi.URLParam(r, "session_id"), chi.URLParam(r, "subagent_id"), options.after, options.limit, options.ascending) + page, err := h.Subagents.ListSubagentTurns(r.Context(), tenantID(r), chi.URLParam(r, "session_id"), chi.URLParam(r, "subagent_id"), options.after, options.limit, options.ascending) if err != nil { writeStoreError(w, r, err) return @@ -74,10 +71,10 @@ func (h *Handler) listSubagentTurns(w http.ResponseWriter, r *http.Request) { // @Router /agents/sessions/{session_id}/subagents/{subagent_id}/turns/{turn_id}/items [get] func (h *Handler) listSubagentTurnItems(w http.ResponseWriter, r *http.Request) { options, ok := readClampedPage(w, r) - if !ok || !h.subagentsReady(w) { + if !ok { return } - page, err := h.subagents.ListSubagentTurnItems(r.Context(), tenantID(r), chi.URLParam(r, "session_id"), chi.URLParam(r, "subagent_id"), chi.URLParam(r, "turn_id"), options.after, options.limit, options.ascending) + page, err := h.Subagents.ListSubagentTurnItems(r.Context(), tenantID(r), chi.URLParam(r, "session_id"), chi.URLParam(r, "subagent_id"), chi.URLParam(r, "turn_id"), options.after, options.limit, options.ascending) if err != nil { writeStoreError(w, r, err) return diff --git a/services/core/internal/api/subagents.go b/services/core/internal/api/subagents.go index e0a645c48..0b8cbffa5 100644 --- a/services/core/internal/api/subagents.go +++ b/services/core/internal/api/subagents.go @@ -8,9 +8,9 @@ import ( "github.com/go-chi/chi/v5" ) -// SubagentStore reads tenant-authorized, persisted public resources. Implementations +// Subagents reads tenant-authorized, persisted public resources. Implementations // must enforce every supplied parent scope, including the pagination cursor. -type SubagentStore interface { +type Subagents interface { GetSubagent(context.Context, string, string, string) (v1.Subagent, error) ListSubagents(context.Context, string, string, string, int, bool) (v1.SubagentList, error) ListSubagentItems(context.Context, string, string, string, string, int, bool) (v1.ItemList, error) @@ -19,8 +19,6 @@ type SubagentStore interface { ListSubagentTurnItems(context.Context, string, string, string, string, string, int, bool) (v1.ItemList, error) } -func WithSubagents(s SubagentStore) Option { return func(h *Handler) { h.subagents = s } } - func (h *Handler) registerSubagentRoutes(r chi.Router) { const root = "/agents/sessions/{session_id}/subagents" r.Get(root, h.listSubagents) @@ -31,14 +29,6 @@ func (h *Handler) registerSubagentRoutes(r chi.Router) { r.Get(root+"/{subagent_id}/turns/{turn_id}/items", h.listSubagentTurnItems) } -func (h *Handler) subagentsReady(w http.ResponseWriter) bool { - if h.subagents == nil { - writeError(w, http.StatusServiceUnavailable, "subagent_storage_unavailable", "Subagent storage is unavailable.") - return false - } - return true -} - // @Summary Retrieve a Session Subagent // @Description Returns this Session's persisted Subagent. Active includes idle between Turns. Resuming preserves opened_at and clears closed_at. Unknown or inaccessible parent scopes return not found. // @Tags Subagents @@ -51,10 +41,7 @@ func (h *Handler) subagentsReady(w http.ResponseWriter) bool { // @Failure 400,401,404,500,503 {object} v1.ErrorResponse // @Router /agents/sessions/{session_id}/subagents/{subagent_id} [get] func (h *Handler) getSubagent(w http.ResponseWriter, r *http.Request) { - if !h.subagentsReady(w) { - return - } - value, err := h.subagents.GetSubagent(r.Context(), tenantID(r), chi.URLParam(r, "session_id"), chi.URLParam(r, "subagent_id")) + value, err := h.Subagents.GetSubagent(r.Context(), tenantID(r), chi.URLParam(r, "session_id"), chi.URLParam(r, "subagent_id")) if err != nil { writeStoreError(w, r, err) return @@ -77,10 +64,10 @@ func (h *Handler) getSubagent(w http.ResponseWriter, r *http.Request) { // @Router /agents/sessions/{session_id}/subagents [get] func (h *Handler) listSubagents(w http.ResponseWriter, r *http.Request) { options, ok := readPage(w, r) - if !ok || !h.subagentsReady(w) { + if !ok { return } - page, err := h.subagents.ListSubagents(r.Context(), tenantID(r), chi.URLParam(r, "session_id"), options.after, options.limit, options.ascending) + page, err := h.Subagents.ListSubagents(r.Context(), tenantID(r), chi.URLParam(r, "session_id"), options.after, options.limit, options.ascending) if err != nil { writeStoreError(w, r, err) return @@ -104,10 +91,10 @@ func (h *Handler) listSubagents(w http.ResponseWriter, r *http.Request) { // @Router /agents/sessions/{session_id}/subagents/{subagent_id}/items [get] func (h *Handler) listSubagentItems(w http.ResponseWriter, r *http.Request) { options, ok := readClampedPage(w, r) - if !ok || !h.subagentsReady(w) { + if !ok { return } - page, err := h.subagents.ListSubagentItems(r.Context(), tenantID(r), chi.URLParam(r, "session_id"), chi.URLParam(r, "subagent_id"), options.after, options.limit, options.ascending) + page, err := h.Subagents.ListSubagentItems(r.Context(), tenantID(r), chi.URLParam(r, "session_id"), chi.URLParam(r, "subagent_id"), options.after, options.limit, options.ascending) if err != nil { writeStoreError(w, r, err) return diff --git a/services/core/internal/api/subagents_test.go b/services/core/internal/api/subagents_test.go index 504400637..346a6fb61 100644 --- a/services/core/internal/api/subagents_test.go +++ b/services/core/internal/api/subagents_test.go @@ -75,6 +75,12 @@ func (s *subagentReadStore) ListSubagentTurnItems(_ context.Context, tenant, ses return s.items(), s.err } +// wire serves the Subagents area from s. +func (s *subagentReadStore) wire(_ *Dependencies, f *testFakes) { + f.subagents.getSubagent, f.subagents.listSubagents, f.subagents.listSubagentItems = s.GetSubagent, s.ListSubagents, s.ListSubagentItems + f.subagents.getSubagentTurn, f.subagents.listSubagentTurns, f.subagents.listSubagentTurnItems = s.GetSubagentTurn, s.ListSubagentTurns, s.ListSubagentTurnItems +} + func (s *subagentReadStore) items() v1.ItemList { if s.empty { return v1.ItemList{} @@ -109,7 +115,7 @@ func requestSubagents(h http.Handler, path, auth, beta string) *httptest.Respons func TestSubagentRoutesPreserveAuthenticatedParentScope(t *testing.T) { s := &subagentReadStore{} - h, _, tenant := testHandler(t, WithSubagents(s)) + h, _, tenant := testHandler(t, s.wire) for _, route := range subagentRoutes { t.Run(route.method, func(t *testing.T) { w := requestSubagents(h, route.path, "Bearer test-api-key", "agents=v1") @@ -147,7 +153,7 @@ func TestSubagentRoutesPreserveAuthenticatedParentScope(t *testing.T) { func TestSubagentRoutesRejectInvalidQueriesBeforeStore(t *testing.T) { s := &subagentReadStore{} - h, _, _ := testHandler(t, WithSubagents(s)) + h, _, _ := testHandler(t, s.wire) for _, route := range subagentRoutes { if !route.list { continue @@ -168,7 +174,7 @@ func TestSubagentRoutesRejectInvalidQueriesBeforeStore(t *testing.T) { func TestSubagentItemListsClampLimit(t *testing.T) { s := &subagentReadStore{} - h, _, tenant := testHandler(t, WithSubagents(s)) + h, _, tenant := testHandler(t, s.wire) for _, route := range subagentRoutes { if !route.clamped { continue @@ -185,7 +191,7 @@ func TestSubagentItemListsClampLimit(t *testing.T) { func TestSubagentRoutesIgnoreUnknownQueryKeys(t *testing.T) { s := &subagentReadStore{} - h, _, tenant := testHandler(t, WithSubagents(s)) + h, _, tenant := testHandler(t, s.wire) for _, route := range subagentRoutes { // Retrieval routes also ignore list keys, which carry no semantics there. for _, query := range []string{"unknown=1", "tenant_id=foreign&unknown=1&unknown=2", "limit=1&after=a&order=asc"} { @@ -206,7 +212,7 @@ func TestSubagentRoutesIgnoreUnknownQueryKeys(t *testing.T) { func TestSubagentRoutesUseExistingAuthenticationAndErrors(t *testing.T) { s := &subagentReadStore{} - h, _, _ := testHandler(t, WithSubagents(s)) + h, _, _ := testHandler(t, s.wire) for _, route := range subagentRoutes { for _, tc := range []struct { auth, beta string @@ -241,17 +247,12 @@ func TestSubagentRoutesUseExistingAuthenticationAndErrors(t *testing.T) { } } -func TestSubagentRoutesDistinguishEmptyFromUnavailable(t *testing.T) { +func TestSubagentListsReturnEmptyPages(t *testing.T) { s := &subagentReadStore{empty: true} - h, _, _ := testHandler(t, WithSubagents(s)) - unavailable, _, _ := testHandler(t) + h, _, _ := testHandler(t, s.wire) for _, route := range subagentRoutes { - w := requestSubagents(unavailable, route.path, "Bearer test-api-key", "agents=v1") - if w.Code != http.StatusServiceUnavailable { - t.Fatalf("unwired store fabricated success: %d %s", w.Code, w.Body) - } if route.list { - w = requestSubagents(h, route.path, "Bearer test-api-key", "agents=v1") + w := requestSubagents(h, route.path, "Bearer test-api-key", "agents=v1") expected := `{"object":"list","first_id":null,"last_id":null,"data":[],"has_more":false}` if w.Code != http.StatusOK || strings.TrimSpace(w.Body.String()) != expected { t.Fatalf("empty page: %d %s", w.Code, w.Body) @@ -262,7 +263,7 @@ func TestSubagentRoutesDistinguishEmptyFromUnavailable(t *testing.T) { func TestSubagentRoutesExposeOnlyOfficialReads(t *testing.T) { s := &subagentReadStore{} - h, _, _ := testHandler(t, WithSubagents(s)) + h, _, _ := testHandler(t, s.wire) for _, route := range subagentRoutes { for _, method := range []string{http.MethodPost, http.MethodPatch, http.MethodDelete} { r := httptest.NewRequest(method, "/v1/agents/sessions/session/subagents"+route.path, nil) diff --git a/services/core/internal/api/text_configuration_test.go b/services/core/internal/api/text_configuration_test.go index cbe427f42..7b2eff4a7 100644 --- a/services/core/internal/api/text_configuration_test.go +++ b/services/core/internal/api/text_configuration_test.go @@ -19,8 +19,7 @@ func TestTextConfigurationHTTP(t *testing.T) { {`,"text":{"verbosity":"high","format":{"type":"text"}}`, "high"}, } { t.Run(tc.text, func(t *testing.T) { - s := &recordingStore{} - h, _, _ := testHandler(t, WithExecution(&inputRecorder{ResourceStore: s})) + h, s, _ := testHandler(t, admitSessions) req := httptest.NewRequest(http.MethodPost, "/v1/agents/sessions", strings.NewReader(`{"agent":{"model":"example"`+tc.text+`},"environment":{"type":"none"},"input":"Describe the configured response format."}`)) req.Header.Set("Authorization", "Bearer test-api-key") req.Header.Set("OpenAI-Beta", "agents=v1") diff --git a/services/core/internal/api/turns.go b/services/core/internal/api/turns.go index 693edb502..8b53d8a08 100644 --- a/services/core/internal/api/turns.go +++ b/services/core/internal/api/turns.go @@ -1,6 +1,7 @@ package api import ( + "context" "encoding/json" "errors" "net/http" @@ -11,6 +12,13 @@ import ( "github.com/go-chi/chi/v5" ) +// SessionHistory reads a Session's root Turns and Items. +type SessionHistory interface { + GetTurn(context.Context, string, string, string) (store.Turn, error) + ListTurns(context.Context, string, string, string, int, bool) (store.TurnPage, error) + ListItems(context.Context, string, string, string, int, bool) (store.ItemPage, error) +} + // @Summary Retrieve an execution Turn // @Description Returns a root Turn of this Session. A Subagent Turn ID returns the same not found error as a missing Turn; read it through the Subagent Turn routes. // @Tags Turns @@ -24,12 +32,12 @@ import ( // @Router /agents/sessions/{session_id}/turns/{turn_id} [get] func (h *Handler) getTurn(w http.ResponseWriter, r *http.Request) { sessionID := chi.URLParam(r, "session_id") - turn, err := h.store.GetTurn(r.Context(), tenantID(r), sessionID, chi.URLParam(r, "turn_id")) + turn, err := h.SessionHistory.GetTurn(r.Context(), tenantID(r), sessionID, chi.URLParam(r, "turn_id")) if err != nil { writeStoreError(w, r, err) return } - session, err := h.store.GetSession(r.Context(), tenantID(r), sessionID) + session, err := h.Sessions.GetSession(r.Context(), tenantID(r), sessionID) if err != nil { writeStoreError(w, r, err) return @@ -61,12 +69,12 @@ func (h *Handler) listTurns(w http.ResponseWriter, r *http.Request) { return } sessionID := chi.URLParam(r, "session_id") - session, err := h.store.GetSession(r.Context(), tenantID(r), sessionID) + session, err := h.Sessions.GetSession(r.Context(), tenantID(r), sessionID) if err != nil { writeStoreError(w, r, err) return } - page, err := h.store.ListTurns(r.Context(), tenantID(r), sessionID, options.after, options.limit, options.ascending) + page, err := h.SessionHistory.ListTurns(r.Context(), tenantID(r), sessionID, options.after, options.limit, options.ascending) if err != nil { writeStoreError(w, r, err) return diff --git a/services/core/internal/api/turns_test.go b/services/core/internal/api/turns_test.go index 8d8044382..2d726bf7d 100644 --- a/services/core/internal/api/turns_test.go +++ b/services/core/internal/api/turns_test.go @@ -14,7 +14,6 @@ import ( ) type turnReadStore struct { - ResourceStore tenant, sessionID, turnID, cursor string limit int ascending bool @@ -36,9 +35,10 @@ func (s *turnReadStore) ListTurns(_ context.Context, tenant, session, cursor str } func TestTurnRoutesUseAuthenticatedScopeAndSafeProjection(t *testing.T) { - h, record, tenant := testHandler(t) s := &turnReadStore{session: store.Session{Configuration: json.RawMessage(`{"agent":{"id":"agent_snapshot"}}`)}, turn: store.Turn{ID: "turn", SessionID: "session", Status: store.TurnFailed, CreatedAt: time.Unix(1700000000, 999), Outcome: json.RawMessage(`{"error":"Bearer SECRET","done":{"metadata":{"password":"SECRET"}}}`)}} - record.ResourceStore = s + h, _, tenant := testHandler(t, func(_ *Dependencies, f *testFakes) { + f.sessions.getSession, f.sessionHistory.getTurn, f.sessionHistory.listTurns = s.GetSession, s.GetTurn, s.ListTurns + }) request := func(path string) *httptest.ResponseRecorder { r := httptest.NewRequest(http.MethodGet, path, nil) r.Header.Set("Authorization", "Bearer test-api-key") diff --git a/services/core/internal/api/validation_errors_test.go b/services/core/internal/api/validation_errors_test.go index ff69e7a70..b67552671 100644 --- a/services/core/internal/api/validation_errors_test.go +++ b/services/core/internal/api/validation_errors_test.go @@ -19,7 +19,6 @@ import ( // validationStore counts every persistence attempt so rejected requests can // prove that validation ran before any write. type validationStore struct { - ResourceStore writes int } @@ -58,11 +57,21 @@ func (s *validationStore) UpdateEnvironmentTemplate(_ context.Context, _, id str return store.EnvironmentTemplate{ID: id, NetworkAccess: input.NetworkAccess, AllowedDomains: input.AllowedDomains}, nil } +// serve takes every write from the handler, and the Worker admits Sessions +// with initial input into s. Session reads are unexpected. +func (s *validationStore) serve(d *Dependencies, f *testFakes) { + d.Execution = f.execution() + f.admission.createSession = s.CreateSession + f.agents.createAgent, f.agents.updateAgent = s.CreateAgent, s.UpdateAgent + f.vaults.createVault = s.CreateVault + f.sessions.getSession, f.sessions.updateSessionMetadata = nil, s.UpdateSessionMetadata + f.environmentTemplates.createEnvironmentTemplate, f.environmentTemplates.updateEnvironmentTemplate = s.CreateEnvironmentTemplate, s.UpdateEnvironmentTemplate +} + func validationHandler(t *testing.T) (http.Handler, *validationStore) { t.Helper() s := &validationStore{} - h, recording, _ := testHandler(t, WithExecution(&inputRecorder{ResourceStore: s})) - recording.ResourceStore = s + h, _, _ := testHandler(t, s.serve) return h, s } diff --git a/services/core/internal/api/vaults.go b/services/core/internal/api/vaults.go index e1df8c965..b3142ba43 100644 --- a/services/core/internal/api/vaults.go +++ b/services/core/internal/api/vaults.go @@ -13,11 +13,21 @@ import ( "github.com/google/uuid" ) -type VaultStore interface { +// Vaults manages Vaults and their Credentials, and selects the Credentials a +// Session's MCP servers use at creation. +type Vaults interface { CreateVault(context.Context, string, store.CreateVaultInput) (store.Vault, error) GetVault(context.Context, string, string) (store.Vault, error) DeleteVault(context.Context, string, string) (string, error) ListVaults(context.Context, string, string, int, bool, []string) (store.VaultPage, error) + CreateOAuthCredential(context.Context, string, string, store.CreateOAuthCredentialInput) (store.Credential, error) + UpdateOAuthCredential(context.Context, string, string, string, store.UpdateOAuthCredentialInput) (store.Credential, error) + CreateStaticCredential(context.Context, string, string, store.CreateStaticCredentialInput) (store.Credential, error) + UpdateStaticCredential(context.Context, string, string, string, store.UpdateStaticCredentialInput) (store.Credential, error) + GetCredential(context.Context, string, string, string) (store.Credential, error) + DeleteCredential(context.Context, string, string, string) (string, error) + ListCredentials(context.Context, string, string, string, int, bool, []string) (store.CredentialPage, error) + ResolveMCPCredentials(context.Context, string, []string, []store.MCPCredentialRequest) ([]store.MCPCredentialBinding, error) } // @Summary Create a Vault @@ -73,7 +83,7 @@ func (h *Handler) createVault(w http.ResponseWriter, r *http.Request) { } return } - vault, err := h.store.CreateVault(r.Context(), tenantID(r), input) + vault, err := h.Vaults.CreateVault(r.Context(), tenantID(r), input) if err != nil { writeStoreError(w, r, err) return @@ -97,7 +107,7 @@ func (h *Handler) getVault(w http.ResponseWriter, r *http.Request) { writeStoreError(w, r, store.ErrNotFound) return } - vault, err := h.store.GetVault(r.Context(), tenantID(r), id) + vault, err := h.Vaults.GetVault(r.Context(), tenantID(r), id) if err != nil { writeStoreError(w, r, err) return diff --git a/services/core/internal/api/vaults_delete.go b/services/core/internal/api/vaults_delete.go index 3534e6e46..0d9274bf2 100644 --- a/services/core/internal/api/vaults_delete.go +++ b/services/core/internal/api/vaults_delete.go @@ -30,7 +30,7 @@ func (h *Handler) deleteVault(w http.ResponseWriter, r *http.Request) { if !ok { return } - deleted, err := h.store.DeleteVault(r.Context(), tenantID(r), id) + deleted, err := h.Vaults.DeleteVault(r.Context(), tenantID(r), id) if err != nil { writeStoreError(w, r, err) return diff --git a/services/core/internal/api/vaults_list.go b/services/core/internal/api/vaults_list.go index f83e08d2c..ab565ca4b 100644 --- a/services/core/internal/api/vaults_list.go +++ b/services/core/internal/api/vaults_list.go @@ -25,7 +25,7 @@ func (h *Handler) listVaults(w http.ResponseWriter, r *http.Request) { if !ok { return } - page, err := h.store.ListVaults(r.Context(), tenantID(r), options.after, options.limit, options.ascending, statuses) + page, err := h.Vaults.ListVaults(r.Context(), tenantID(r), options.after, options.limit, options.ascending, statuses) if err != nil { writeStoreError(w, r, err) return diff --git a/services/core/internal/api/vaults_test.go b/services/core/internal/api/vaults_test.go index 815915e4d..a28c1552c 100644 --- a/services/core/internal/api/vaults_test.go +++ b/services/core/internal/api/vaults_test.go @@ -17,7 +17,6 @@ import ( ) type vaultResourceFixture struct { - ResourceStore vault store.Vault err error tenant, id string @@ -47,15 +46,12 @@ func (f *vaultResourceFixture) UpdateAgent(context.Context, string, string, stor func vaultResourceHandler(t *testing.T) (http.Handler, *vaultResourceFixture) { t.Helper() f := &vaultResourceFixture{vault: store.Vault{ID: uuid.NewString(), TenantID: uuid.NewString(), Metadata: map[string]string{}, CreatedAt: time.Unix(1700000000, 0)}} - auth, err := NewAuthenticator([]APIKey{{OrganizationID: "vault-org", ProjectID: "vault-project", SubjectKind: "user", SubjectID: "vault-owner", TokenSHA256: runtimedevice.HashCredential("vault-key"), TenantID: f.vault.TenantID}}) - if err != nil { - t.Fatal(err) - } - h, err := NewHandler(f, auth, "fake_alpha") - if err != nil { - t.Fatal(err) - } - return h, f + deps, fakes := testDependencies(t) + deps.Engine = "fake_alpha" + fakes.projects.resolveProjectAPIKey = projectKeys(t, APIKey{OrganizationID: "vault-org", ProjectID: "vault-project", SubjectKind: "user", SubjectID: "vault-owner", TokenSHA256: runtimedevice.HashCredential("vault-key"), TenantID: f.vault.TenantID}).ResolveProjectAPIKey + fakes.vaults.createVault, fakes.vaults.getVault, fakes.vaults.listVaults, fakes.vaults.deleteVault = f.CreateVault, f.GetVault, f.ListVaults, f.DeleteVault + fakes.agents.updateAgent = f.UpdateAgent + return newTestHandler(t, deps), f } func vaultRequest(h http.Handler, method, path, body string) *httptest.ResponseRecorder { diff --git a/services/core/internal/api/write_audit.go b/services/core/internal/api/write_audit.go index 23d598e0a..e1a311556 100644 --- a/services/core/internal/api/write_audit.go +++ b/services/core/internal/api/write_audit.go @@ -11,8 +11,9 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" ) -// WriteAuditStore exposes safe read models, never request bodies or credentials. -type WriteAuditStore interface { +// WriteAudit reads public write provenance as safe read models, never request +// bodies or credentials. +type WriteAudit interface { GetResourceOwners(context.Context, string, string, []string) ([]store.ResourceOwner, error) ListWriteOperations(context.Context, string, store.WriteOperationFilter) (store.WriteOperationPage, error) } @@ -21,15 +22,6 @@ type ResourceOwnerList struct { Data []store.ResourceOwner `json:"data"` } -func WithWriteAudit(s WriteAuditStore, auth *DeploymentAuthenticator) Option { - return func(h *Handler) { - h.writeAudit = s - if auth != nil { - h.deploymentAuth = auth - } - } -} - func (h *Handler) writeAuditScope(w http.ResponseWriter, r *http.Request, allowed ...string) (url.Values, string, bool) { values, err := url.ParseQuery(r.URL.RawQuery) if err != nil { @@ -83,7 +75,7 @@ func (h *Handler) getResourceOwners(w http.ResponseWriter, r *http.Request) { return } } - owners, err := h.writeAudit.GetResourceOwners(r.Context(), tenant, values.Get("resource_type"), ids) + owners, err := h.WriteAudit.GetResourceOwners(r.Context(), tenant, values.Get("resource_type"), ids) if err != nil { writeStoreError(w, r, err) return @@ -152,7 +144,7 @@ func (h *Handler) listWriteOperations(w http.ResponseWriter, r *http.Request) { writeStoreError(w, r, store.ErrInvalidInput) return } - page, err := h.writeAudit.ListWriteOperations(r.Context(), tenant, filter) + page, err := h.WriteAudit.ListWriteOperations(r.Context(), tenant, filter) if err != nil { writeStoreError(w, r, err) return diff --git a/services/core/internal/api/write_audit_test.go b/services/core/internal/api/write_audit_test.go index cb73c24cb..fcc5bff76 100644 --- a/services/core/internal/api/write_audit_test.go +++ b/services/core/internal/api/write_audit_test.go @@ -11,7 +11,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/obs/log" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/writeaudit" "github.com/google/uuid" @@ -43,19 +42,13 @@ func (s *auditQueryFixture) ListWriteOperations(_ context.Context, tenant string } func TestWriteAuditQueriesDeploymentScopeAndValidation(t *testing.T) { key := callerBinding() - auth, err := NewAuthenticator([]APIKey{key}) - if err != nil { - t.Fatal(err) - } - admin, err := NewDeploymentAuthenticator([]string{runtimedevice.HashCredential("admin")}) - if err != nil { - t.Fatal(err) - } + deps, fakes := testDependencies(t) queries := &auditQueryFixture{} - h, err := NewHandler(&recordingStore{}, auth, "codex", WithWriteAudit(queries, admin), WithProjectAPIKeys(&projectKeyStoreFixture{project: store.ProjectBinding{Project: store.Project{ID: key.ProjectID}, Principal: identity.Principal{ProjectScope: identity.ProjectScope{TenantID: key.TenantID}}}}, admin)) - if err != nil { - t.Fatal(err) - } + fakes.writeAudit.getResourceOwners, fakes.writeAudit.listWriteOperations = queries.GetResourceOwners, queries.ListWriteOperations + projects := &projectKeyStoreFixture{project: store.ProjectBinding{Project: store.Project{ID: key.ProjectID}, Principal: identity.Principal{ProjectScope: identity.ProjectScope{TenantID: key.TenantID}}}} + fakes.projects.resolveProjectAPIKey = projectKeys(t, key).ResolveProjectAPIKey + fakes.projects.getProject = projects.GetProject + h := newTestHandler(t, deps) owners := "/core/v1/projects/" + key.ProjectID + "/resource-owners?resource_type=agent&resource_ids=first,second" for _, token := range []string{"", "caller", "foreign"} { w := projectKeyHTTP(h, "GET", owners, token, "") @@ -100,18 +93,18 @@ func TestWriteAuditQueriesDeploymentScopeAndValidation(t *testing.T) { func TestAuthenticatedWriteProvenance(t *testing.T) { key := callerBinding() - fixture, _ := NewAuthenticator([]APIKey{key}) - binding, _ := fixture.keys.ResolveProjectAPIKey(t.Context(), key.TokenSHA256) + binding, _ := projectKeys(t, key).ResolveProjectAPIKey(t.Context(), key.TokenSHA256) binding.Key = store.ProjectAPIKey{ID: uuid.NewString(), Name: "SDK", Prefix: "pc_12345678"} keys := &projectKeyStoreFixture{binding: binding} - auth, _ := NewDatabaseAuthenticator(keys) - h := &Handler{auth: auth} + deps, fakes := testDependencies(t) + fakes.projects.resolveProjectAPIKey = keys.ResolveProjectAPIKey + h := &Handler{Dependencies: deps} var source writeaudit.Source var got bool - handler := agentsResponseHeaders(log.HTTPMiddleware(h.authenticateCaller(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handler := responseHeadersWithErrors(log.HTTPMiddleware(h.authenticateCaller(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { source, got = writeaudit.FromContext(r.Context()) w.WriteHeader(204) - }), true))) + }), true)), func(string) {}) for _, test := range []struct { method, path string present bool diff --git a/services/core/internal/sandbox/providers/configuration_flow_test.go b/services/core/internal/sandbox/providers/configuration_flow_test.go index a828b81a7..4031d5c12 100644 --- a/services/core/internal/sandbox/providers/configuration_flow_test.go +++ b/services/core/internal/sandbox/providers/configuration_flow_test.go @@ -98,13 +98,16 @@ func TestAdditionalConfigurationProviderUsesCommonAPIAndStore(t *testing.T) { if err != nil { t.Fatal(err) } - projectAuth, err := api.NewDatabaseAuthenticator(s) - if err != nil { - t.Fatal(err) - } - h, err := api.NewHandler(s, projectAuth, "codex", api.WithSandboxManager(s, auth), api.WithSandboxDeploymentSetup(func(ctx context.Context, in store.SandboxDeploymentSetupRequest) (store.RuntimeDeploymentView, error) { - return w.InitializeSandboxDeployment(ctx, installation, in) - })) + // The flow reaches only the store areas and the deployment setup; every + // other dependency panics if called. + h, err := api.NewHandler(api.Dependencies{ + Engine: "codex", CoreKeys: auth, InstallationBindings: s, Projects: s, Vaults: s, ModelProviders: s, Files: s, Skills: s, + EnvironmentTemplates: s, Agents: s, Sessions: s, SessionEvents: s, SessionHistory: s, Subagents: s, Artifacts: s, + SessionAdmin: s, Environments: s, Admin: s, WriteAudit: s, ExecutorConnections: struct{ api.ExecutorConnections }{}, + Metrics: struct{ api.Metrics }{}, RuntimeObservations: struct{ api.RuntimeObservations }{}, RuntimeHistory: struct{ api.RuntimeHistory }{}, + Execution: &api.Execution{ExecutorURL: "wss://core.example/api/v1/agent-daemon/ws", Admission: s, SessionArchive: s, Workspaces: struct{ api.EnvironmentWorkspaces }{}}, + Sandboxes: &api.Sandboxes{Deployment: s, DeploymentChanges: leaseSetup{t: t, store: w, installation: installation}, ConfigurationDiscovery: struct{ api.ConfigurationDiscovery }{}}, + }) if err != nil { t.Fatal(err) } @@ -127,3 +130,30 @@ func TestAdditionalConfigurationProviderUsesCommonAPIAndStore(t *testing.T) { t.Fatal("native fields not persisted", err) } } + +// leaseSetup initializes the deployment through the execution lease holder. +// The flow makes no other deployment change. +type leaseSetup struct { + t *testing.T + store *store.Store + installation string +} + +func (l leaseSetup) InitializeSandboxDeployment(ctx context.Context, in store.SandboxDeploymentSetupRequest) (store.RuntimeDeploymentView, error) { + return l.store.InitializeSandboxDeployment(ctx, l.installation, in) +} + +func (l leaseSetup) UpdateSandboxDeployment(context.Context, store.SandboxDeploymentUpdateRequest) (store.RuntimeDeploymentView, error) { + l.t.Fatal("unexpected call to UpdateSandboxDeployment") + return store.RuntimeDeploymentView{}, nil +} + +func (l leaseSetup) StartSandboxReset(context.Context, store.SandboxResetRequest) (store.RuntimeDeploymentView, error) { + l.t.Fatal("unexpected call to StartSandboxReset") + return store.RuntimeDeploymentView{}, nil +} + +func (l leaseSetup) CancelSandboxReset(context.Context, uint64) (store.RuntimeDeploymentView, error) { + l.t.Fatal("unexpected call to CancelSandboxReset") + return store.RuntimeDeploymentView{}, nil +} diff --git a/services/core/internal/store/admin_session_archive_worker_http_test.go b/services/core/internal/store/admin_session_archive_worker_http_test.go index ac657b750..240404c0a 100644 --- a/services/core/internal/store/admin_session_archive_worker_http_test.go +++ b/services/core/internal/store/admin_session_archive_worker_http_test.go @@ -94,11 +94,7 @@ func TestAdminSessionArchiveWorkerHTTPPostgres(t *testing.T) { if err != nil { t.Fatal(err) } - auth, err := api.NewDatabaseAuthenticator(s) - if err != nil { - t.Fatal(err) - } - handler, err := api.NewHandler(s, auth, "codex", api.WithProjectAPIKeys(s, admin), api.WithAdminManagement(s), api.WithExecution(worker), api.WithSessionArchive(worker.ArchiveManagedSession)) + handler, err := publicHandler(t, s, nil, "codex", storeKeys(s), workerExecution(worker), withCoreKeys(admin)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/agent_execution_defaults_http_test.go b/services/core/internal/store/agent_execution_defaults_http_test.go index 77f7fa72e..6e01276d2 100644 --- a/services/core/internal/store/agent_execution_defaults_http_test.go +++ b/services/core/internal/store/agent_execution_defaults_http_test.go @@ -9,7 +9,6 @@ import ( "testing" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" @@ -27,7 +26,7 @@ func TestAgentExecutionDefaultsPublicSnapshotAndPrecedence(t *testing.T) { } deployment := &v1.ModelProviderInput{Protocol: "responses", BaseURL: "https://deployment.example/v1", APIKey: "deployment-canary"} defaultsCalls := 0 - handler, err := api.NewHandler(st, auth, "codex", api.WithHarnesses([]string{"codex", "claude_sdk", "mcode"}), api.WithHostedEnvironments(), api.WithEnvironmentRemoteURL("wss://core.example/api/v1/agent-daemon/ws"), api.WithExecution(st), api.WithModelProviderDefaults(func(context.Context, string) (*store.DeploymentModelProviderSnapshot, error) { + handler, err := publicHandler(t, st, auth, "codex", storeExecution(t, st), managedSandboxes(t, st), withHarnesses([]string{"codex", "claude_sdk", "mcode"}), modelProviderDefaults(st, func(context.Context, string) (*store.DeploymentModelProviderSnapshot, error) { defaultsCalls++ copy := *deployment return &store.DeploymentModelProviderSnapshot{Model: "fixture", Provider: ©, Revision: uuid.New()}, nil diff --git a/services/core/internal/store/agents_delete_public_test.go b/services/core/internal/store/agents_delete_public_test.go index fb8b2aad7..ef1cb96fc 100644 --- a/services/core/internal/store/agents_delete_public_test.go +++ b/services/core/internal/store/agents_delete_public_test.go @@ -8,7 +8,6 @@ import ( "testing" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" @@ -28,14 +27,14 @@ func TestAgentDeletionOfficialClient(t *testing.T) { if err != nil { t.Fatal(err) } - h, err := api.NewHandler(s, auth, "codex", api.WithExecution(s)) + h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) if err != nil { t.Fatal(err) } server := httptest.NewServer(h) defer server.Close() recoveredStore := store.New(pool) - h, err = api.NewHandler(recoveredStore, auth, "codex", api.WithExecution(recoveredStore)) + h, err = publicHandler(t, recoveredStore, auth, "codex", storeExecution(t, recoveredStore)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/agents_update_public_test.go b/services/core/internal/store/agents_update_public_test.go index 9424ca826..cea01b4b7 100644 --- a/services/core/internal/store/agents_update_public_test.go +++ b/services/core/internal/store/agents_update_public_test.go @@ -8,7 +8,6 @@ import ( "testing" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" @@ -28,14 +27,14 @@ func TestAgentUpdateOfficialClient(t *testing.T) { if err != nil { t.Fatal(err) } - h, err := api.NewHandler(s, auth, "codex", api.WithExecution(s)) + h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) if err != nil { t.Fatal(err) } server := httptest.NewServer(h) defer server.Close() recoveredStore := store.New(pool) - h, err = api.NewHandler(recoveredStore, auth, "codex", api.WithExecution(recoveredStore)) + h, err = publicHandler(t, recoveredStore, auth, "codex", storeExecution(t, recoveredStore)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/auth_fixture_test.go b/services/core/internal/store/auth_fixture_test.go index 977cf4296..37acb66ed 100644 --- a/services/core/internal/store/auth_fixture_test.go +++ b/services/core/internal/store/auth_fixture_test.go @@ -5,7 +5,6 @@ import ( "encoding/hex" "errors" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" @@ -20,7 +19,10 @@ func (f fixtureKeyResolver) ResolveProjectAPIKey(_ context.Context, digest strin } return store.ProjectAPIKeyBinding{}, store.ErrNotFound } -func newTestAuthenticator(keys []testAPIKey) (*api.Authenticator, error) { + +// newTestAuthenticator binds each key's digest to its Principal, for +// publicHandler. +func newTestAuthenticator(keys []testAPIKey) (fixtureKeyResolver, error) { resolver := fixtureKeyResolver{} for _, k := range keys { p := identity.Principal{ProjectScope: identity.ProjectScope{TenantID: k.TenantID, OrganizationID: k.OrganizationID, ProjectID: k.ProjectID}, SubjectKind: k.SubjectKind, SubjectID: k.SubjectID} @@ -36,5 +38,5 @@ func newTestAuthenticator(keys []testAPIKey) (*api.Authenticator, error) { } 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} } - return api.NewDatabaseAuthenticator(resolver) + return resolver, nil } diff --git a/services/core/internal/store/configuration_validation_public_test.go b/services/core/internal/store/configuration_validation_public_test.go index ef82fe7a7..98bf37587 100644 --- a/services/core/internal/store/configuration_validation_public_test.go +++ b/services/core/internal/store/configuration_validation_public_test.go @@ -8,7 +8,6 @@ import ( "strings" "testing" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" @@ -34,7 +33,7 @@ func TestAgentConfigurationValidationRejectsWithoutWritesPostgres(t *testing.T) if err != nil { t.Fatal(err) } - h, err := api.NewHandler(s, auth, "codex", api.WithExecution(s)) + h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/creation_stream_settlement_public_test.go b/services/core/internal/store/creation_stream_settlement_public_test.go index b02e1e6b5..14d1f817b 100644 --- a/services/core/internal/store/creation_stream_settlement_public_test.go +++ b/services/core/internal/store/creation_stream_settlement_public_test.go @@ -11,16 +11,11 @@ import ( "testing" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" ) -// storeAdmission admits creation input through the Store without a Worker, so -// reservations stay pending until the test settles them. -type storeAdmission struct{ *store.Store } - type sseLines struct { lines chan string stop context.CancelFunc @@ -124,7 +119,7 @@ func TestCreationStreamPublicLifetimes(t *testing.T) { if err != nil { t.Fatal(err) } - handler, err := api.NewHandler(s, auth, "codex", api.WithEnvironmentRemoteURL("https://offline-executor.example"), api.WithExecution(storeAdmission{s})) + handler, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), executorURL("https://offline-executor.example")) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/credential_matrix_http_test.go b/services/core/internal/store/credential_matrix_http_test.go index a9e0ff94a..0b6a84c2b 100644 --- a/services/core/internal/store/credential_matrix_http_test.go +++ b/services/core/internal/store/credential_matrix_http_test.go @@ -32,11 +32,7 @@ func TestCredentialNamespaceMatrix(t *testing.T) { if err != nil { t.Fatal(err) } - auth, err := api.NewDatabaseAuthenticator(s) - if err != nil { - t.Fatal(err) - } - handler, err := api.NewHandler(s, auth, "codex", api.WithSandboxManager(s, admin), api.WithProjectAPIKeys(s, admin)) + handler, err := publicHandler(t, s, nil, "codex", storeKeys(s), storeExecution(t, s), managedSandboxes(t, s), withCoreKeys(admin)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/deployment_model_providers_http_test.go b/services/core/internal/store/deployment_model_providers_http_test.go index 116d57b67..266a5c38f 100644 --- a/services/core/internal/store/deployment_model_providers_http_test.go +++ b/services/core/internal/store/deployment_model_providers_http_test.go @@ -39,9 +39,7 @@ func TestDeploymentModelProvidersHTTP(t *testing.T) { if err != nil { t.Fatal(err) } - handler, err := api.NewHandler(st, auth, "codex", api.WithProjectAPIKeys(st, admin), api.WithHarnesses([]string{"codex", "mcode"}), - api.WithHostedEnvironments(), api.WithExecution(st), api.WithEnvironmentRemoteURL("wss://core.example/api/v1/agent-daemon/ws"), - api.WithModelProviderDefaults(st.DeploymentModelProvider)) + handler, err := publicHandler(t, st, auth, "codex", storeExecution(t, st), managedSandboxes(t, st), withCoreKeys(admin), withHarnesses([]string{"codex", "mcode"})) if err != nil { t.Fatal(err) } @@ -297,7 +295,7 @@ func TestNoneSessionRetryAfterDeploymentDefaultChanges(t *testing.T) { if err != nil { t.Fatal(err) } - handler, err := api.NewHandler(st, auth, "codex", api.WithExecution(st), api.WithModelProviderDefaults(st.DeploymentModelProvider)) + handler, err := publicHandler(t, st, auth, "codex", storeExecution(t, st)) if err != nil { t.Fatal(err) } @@ -384,7 +382,7 @@ func TestDeploymentProviderResolutionPairsRevisionDuringReplacement(t *testing.T _, err = st.SetDeploymentModelProvider(admin, harness, v1.ModelConfigurationInput{ModelProvider: replacement, Model: "fixture"}) return snapshot, err } - handler, err := api.NewHandler(st, auth, "codex", api.WithExecution(st), api.WithModelProviderDefaults(resolver)) + handler, err := publicHandler(t, st, auth, "codex", storeExecution(t, st), modelProviderDefaults(st, resolver)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/environment_file_write_semantics_public_test.go b/services/core/internal/store/environment_file_write_semantics_public_test.go index 14a9fbf48..78697d6e4 100644 --- a/services/core/internal/store/environment_file_write_semantics_public_test.go +++ b/services/core/internal/store/environment_file_write_semantics_public_test.go @@ -13,7 +13,6 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" @@ -63,7 +62,7 @@ func TestEnvironmentFileCreateRejectionsLeaveNoReceiptOrConsumption(t *testing.T if err != nil { t.Fatal(err) } - handler, err := api.NewHandler(h.s, auth, "codex", api.WithEnvironmentFileWriter(w), api.WithSourceFiles(h.s)) + handler, err := publicHandler(t, h.s, auth, "codex", workerExecution(w)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/environment_initial_public_test.go b/services/core/internal/store/environment_initial_public_test.go index 15cfe4b73..a9aa49c4c 100644 --- a/services/core/internal/store/environment_initial_public_test.go +++ b/services/core/internal/store/environment_initial_public_test.go @@ -11,7 +11,6 @@ import ( "testing" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" @@ -51,7 +50,7 @@ func TestEnvironmentInitialFailureOfficialClient(t *testing.T) { t.Error(err) } }) - handler, err := api.NewHandler(s, auth, "codex", api.WithEnvironmentRemoteURL("https://executor.example")) + handler, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), executorURL("https://executor.example")) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/environment_mcp_public_test.go b/services/core/internal/store/environment_mcp_public_test.go index 7019d29b0..2353ad1b7 100644 --- a/services/core/internal/store/environment_mcp_public_test.go +++ b/services/core/internal/store/environment_mcp_public_test.go @@ -7,7 +7,6 @@ import ( "strings" "testing" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" @@ -21,7 +20,7 @@ func TestPublicEnvironmentMCPUsesAttachedVaultSelection(t *testing.T) { if err != nil { t.Fatal(err) } - handler, err := api.NewHandler(s, auth, kind, api.WithEnvironmentRemoteURL("https://executor.example"), api.WithExecution(&execution.Worker{})) + handler, err := publicHandler(t, s, auth, kind, workerExecution(&execution.Worker{}), executorURL("https://executor.example")) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/environment_retrieve_public_test.go b/services/core/internal/store/environment_retrieve_public_test.go index c4e8a7419..fd55c908f 100644 --- a/services/core/internal/store/environment_retrieve_public_test.go +++ b/services/core/internal/store/environment_retrieve_public_test.go @@ -11,7 +11,6 @@ import ( "testing" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" @@ -52,7 +51,7 @@ func TestEnvironmentRetrievalOfficialClient(t *testing.T) { } } }() - handler, err := api.NewHandler(s, auth, "codex", api.WithExecution(s), api.WithEnvironmentRemoteURL("https://private-registry.example")) + handler, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), executorURL("https://private-registry.example")) if err != nil { t.Fatal(err) } @@ -91,7 +90,7 @@ func TestEnvironmentRetrievalOfficialClient(t *testing.T) { server.Close() pool.Close() reopened, reopenedPool := store.NewModelTestStore(t) - handler, err = api.NewHandler(reopened, auth, "codex") + handler, err = publicHandler(t, reopened, auth, "codex") if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/file_resource_semantics_public_test.go b/services/core/internal/store/file_resource_semantics_public_test.go index 1e8400f39..a30509091 100644 --- a/services/core/internal/store/file_resource_semantics_public_test.go +++ b/services/core/internal/store/file_resource_semantics_public_test.go @@ -10,7 +10,6 @@ import ( "testing" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" @@ -38,7 +37,7 @@ func TestFileResourceSemanticsOfficialClientPostgres(t *testing.T) { newServer := func() *httptest.Server { t.Helper() s := store.NewWithCredentialCipher(pool, cipher) - h, err := api.NewHandler(s, auth, "codex", api.WithSourceFiles(s), api.WithSkills(s)) + h, err := publicHandler(t, s, auth, "codex") if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/function_images_native_test.go b/services/core/internal/store/function_images_native_test.go index b3524e4e9..a1cbc18cd 100644 --- a/services/core/internal/store/function_images_native_test.go +++ b/services/core/internal/store/function_images_native_test.go @@ -10,7 +10,6 @@ import ( "testing" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/google/uuid" @@ -55,7 +54,7 @@ func TestNativeFunctionImagePublicExecution(t *testing.T) { if err != nil { t.Fatal(err) } - handler, err := api.NewHandler(h.s, auth, kind, api.WithExecution(worker), api.WithExecutionPolicy(h.d.Policy), nativeDeploymentDefaults(model, provider)) + handler, err := publicHandler(t, h.s, auth, kind, workerExecution(worker), withPolicy(h.d.Policy), nativeDeploymentDefaults(h.s, model, provider)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/function_inputs_public_test.go b/services/core/internal/store/function_inputs_public_test.go index daaaac39b..abfb2685f 100644 --- a/services/core/internal/store/function_inputs_public_test.go +++ b/services/core/internal/store/function_inputs_public_test.go @@ -11,7 +11,6 @@ import ( "testing" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" @@ -50,7 +49,7 @@ func TestFunctionInputsOfficialClientAtomicAdmission(t *testing.T) { if err != nil { t.Fatal(err) } - handler, err := api.NewHandler(s, auth, "codex", api.WithExecution(s)) + handler, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/function_public_native_test.go b/services/core/internal/store/function_public_native_test.go index 3a293b97f..4d29aa55b 100644 --- a/services/core/internal/store/function_public_native_test.go +++ b/services/core/internal/store/function_public_native_test.go @@ -11,7 +11,6 @@ import ( "time" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/google/uuid" @@ -83,7 +82,7 @@ func nativePublicFunctionServer(t *testing.T, h *dispatchHarness, ctx context.Co if err != nil { t.Fatal(err) } - handler, err := api.NewHandler(h.s, auth, "codex", api.WithExecution(worker), nativeDeploymentDefaults("gpt-5.5", provider)) + handler, err := publicHandler(t, h.s, auth, "codex", workerExecution(worker), nativeDeploymentDefaults(h.s, "gpt-5.5", provider)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/function_state_public_test.go b/services/core/internal/store/function_state_public_test.go index 35b612617..d98748895 100644 --- a/services/core/internal/store/function_state_public_test.go +++ b/services/core/internal/store/function_state_public_test.go @@ -11,7 +11,6 @@ import ( "testing" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" @@ -49,7 +48,7 @@ func TestFunctionStateOfficialClientReadsAndLiveEvents(t *testing.T) { if err != nil { t.Fatal(err) } - handler, err := api.NewHandler(s, auth, "codex") + handler, err := publicHandler(t, s, auth, "codex") if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/harness_onboarding_test.go b/services/core/internal/store/harness_onboarding_test.go index 91f4b880f..96e39cd37 100644 --- a/services/core/internal/store/harness_onboarding_test.go +++ b/services/core/internal/store/harness_onboarding_test.go @@ -17,7 +17,6 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/engine/enginetest" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" @@ -69,7 +68,7 @@ func TestThirdHarnessPublicOnboarding(t *testing.T) { if err != nil { t.Fatal(err) } - handler, err := api.NewHandler(h.s, auth, "fixture_harness", api.WithExecution(worker), api.WithExecutionPolicy(policy)) + handler, err := publicHandler(t, h.s, auth, "fixture_harness", workerExecution(worker), withPolicy(policy)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/hosted_initialization_failure_public_test.go b/services/core/internal/store/hosted_initialization_failure_public_test.go index 9a98dc524..259b9b0bb 100644 --- a/services/core/internal/store/hosted_initialization_failure_public_test.go +++ b/services/core/internal/store/hosted_initialization_failure_public_test.go @@ -18,7 +18,6 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" @@ -300,7 +299,7 @@ func TestHostedInitializationFailurePublicHTTP(t *testing.T) { if err != nil { t.Fatal(err) } - handler, err := api.NewHandler(s, auth, "codex", api.WithExecution(w)) + handler, err := publicHandler(t, s, auth, "codex", workerExecution(w)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/initial_files_http_test.go b/services/core/internal/store/initial_files_http_test.go index 135149b7b..7976009a8 100644 --- a/services/core/internal/store/initial_files_http_test.go +++ b/services/core/internal/store/initial_files_http_test.go @@ -8,7 +8,6 @@ import ( "net/http/httptest" "testing" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" @@ -28,7 +27,7 @@ func TestInitialFilesHTTPInlineLimitsAndRetry(t *testing.T) { t.Fatal(err) } // Exercise HTTP parsing and durable storage without starting a Runtime. - handler, err := api.NewHandler(s, auth, "codex", api.WithHostedEnvironments(), api.WithExecution(s), fixtureDeploymentProvider()) + handler, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), managedSandboxes(t, s), fixtureDeploymentProvider(s)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/input_conflicts_public_test.go b/services/core/internal/store/input_conflicts_public_test.go index 312927b5a..243dfc817 100644 --- a/services/core/internal/store/input_conflicts_public_test.go +++ b/services/core/internal/store/input_conflicts_public_test.go @@ -10,7 +10,6 @@ import ( "strings" "testing" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" @@ -48,7 +47,7 @@ func TestSessionInputConflictsAndResultTargetsPostgres(t *testing.T) { if err != nil { t.Fatal(err) } - h, err := api.NewHandler(s, auth, "codex", api.WithExecution(s), api.WithEnvironmentRemoteURL("https://executor.example")) + h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), executorURL("https://executor.example")) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/list_cursor_public_test.go b/services/core/internal/store/list_cursor_public_test.go index a8a1ef6d3..fb5a38027 100644 --- a/services/core/internal/store/list_cursor_public_test.go +++ b/services/core/internal/store/list_cursor_public_test.go @@ -13,7 +13,6 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" @@ -232,7 +231,7 @@ func TestListCursorErrorsPostgres(t *testing.T) { if err != nil { t.Fatal(err) } - h, err := api.NewHandler(s, auth, "codex", api.WithExecution(s), api.WithSubagents(s), api.WithSkills(s), api.WithSourceFiles(s), api.WithSessionArtifacts(s)) + h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/list_query_public_test.go b/services/core/internal/store/list_query_public_test.go index 2b8e43ed3..515a32005 100644 --- a/services/core/internal/store/list_query_public_test.go +++ b/services/core/internal/store/list_query_public_test.go @@ -9,7 +9,6 @@ import ( "testing" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" @@ -49,7 +48,7 @@ func TestListQueryOfficialClientPostgres(t *testing.T) { t.Error(err) } }) - handler, err := api.NewHandler(s, auth, "codex", api.WithExecution(worker), api.WithSkills(s), api.WithSourceFiles(s)) + handler, err := publicHandler(t, s, auth, "codex", workerExecution(worker)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/mcode_public_native_test.go b/services/core/internal/store/mcode_public_native_test.go index 4d616faf7..c160088cd 100644 --- a/services/core/internal/store/mcode_public_native_test.go +++ b/services/core/internal/store/mcode_public_native_test.go @@ -11,7 +11,6 @@ import ( "testing" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/google/uuid" @@ -54,7 +53,7 @@ func TestNativeMCodePublicExecution(t *testing.T) { if err != nil { t.Fatal(err) } - handler, err := api.NewHandler(h.s, auth, "mcode", api.WithExecution(worker), nativeDeploymentDefaults(model, provider)) + handler, err := publicHandler(t, h.s, auth, "mcode", workerExecution(worker), acceptUnavailable(t), nativeDeploymentDefaults(h.s, model, provider)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/mcp_credential_selection_public_test.go b/services/core/internal/store/mcp_credential_selection_public_test.go index 385c9f356..cb4351a67 100644 --- a/services/core/internal/store/mcp_credential_selection_public_test.go +++ b/services/core/internal/store/mcp_credential_selection_public_test.go @@ -9,7 +9,6 @@ import ( "strings" "testing" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" @@ -40,7 +39,7 @@ func TestMCPCredentialSelectionPublicPostgres(t *testing.T) { if err != nil { t.Fatal(err) } - h, err := api.NewHandler(s, auth, "codex", api.WithExecution(s)) + h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/message_images_native_test.go b/services/core/internal/store/message_images_native_test.go index ad9ce3b6d..0b8082c76 100644 --- a/services/core/internal/store/message_images_native_test.go +++ b/services/core/internal/store/message_images_native_test.go @@ -10,7 +10,6 @@ import ( "testing" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/google/uuid" @@ -55,7 +54,7 @@ func TestNativeMessageImagePublicExecution(t *testing.T) { if err != nil { t.Fatal(err) } - handler, err := api.NewHandler(h.s, auth, kind, api.WithExecution(worker), api.WithExecutionPolicy(h.d.Policy), nativeDeploymentDefaults(model, provider)) + handler, err := publicHandler(t, h.s, auth, kind, workerExecution(worker), withPolicy(h.d.Policy), nativeDeploymentDefaults(h.s, model, provider)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/model_protocol_native_test.go b/services/core/internal/store/model_protocol_native_test.go index 9dcf766a6..f820b2cfb 100644 --- a/services/core/internal/store/model_protocol_native_test.go +++ b/services/core/internal/store/model_protocol_native_test.go @@ -12,7 +12,6 @@ import ( "time" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" @@ -83,7 +82,7 @@ func TestNativeModelProtocolPublicExecution(t *testing.T) { t.Fatal("cannot create fixture authenticator") } providerRevision := uuid.New() - handler, err := api.NewHandler(h.s, auth, options.Engine, api.WithExecution(worker), api.WithExecutionPolicy(h.d.Policy), api.WithModelProviderDefaults(func(context.Context, string) (*store.DeploymentModelProviderSnapshot, error) { + handler, err := publicHandler(t, h.s, auth, options.Engine, workerExecution(worker), withPolicy(h.d.Policy), modelProviderDefaults(h.s, func(context.Context, string) (*store.DeploymentModelProviderSnapshot, error) { return &store.DeploymentModelProviderSnapshot{Model: options.Model, HarnessConfig: options.HarnessConfig, Provider: &options.Provider, Revision: providerRevision}, nil })) if err != nil { diff --git a/services/core/internal/store/model_provider_fixture_test.go b/services/core/internal/store/model_provider_fixture_test.go index 4abc35e41..448e4b651 100644 --- a/services/core/internal/store/model_provider_fixture_test.go +++ b/services/core/internal/store/model_provider_fixture_test.go @@ -3,6 +3,7 @@ package store_test import ( "context" "encoding/json" + "github.com/google/uuid" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" @@ -14,8 +15,8 @@ import ( // key (store.NewModelTestStore). // fixtureDeploymentProvider configures a deployment default for every harness. -func fixtureDeploymentProvider() api.Option { - return api.WithModelProviderDefaults(func(_ context.Context, harness string) (*store.DeploymentModelProviderSnapshot, error) { +func fixtureDeploymentProvider(s *store.Store) func(*api.Dependencies) { + return modelProviderDefaults(s, func(_ context.Context, harness string) (*store.DeploymentModelProviderSnapshot, error) { return &store.DeploymentModelProviderSnapshot{Model: "fixture", Provider: store.FixtureModelProvider(harness), Revision: uuid.New()}, nil }) } diff --git a/services/core/internal/store/native_daemon_test.go b/services/core/internal/store/native_daemon_test.go index db3e63057..cf4b8cc0d 100644 --- a/services/core/internal/store/native_daemon_test.go +++ b/services/core/internal/store/native_daemon_test.go @@ -150,9 +150,9 @@ func nativeModelProvider(model *httptest.Server) *v1.ModelProviderInput { // nativeDeploymentDefaults makes provider the deployment default model provider, // which public environment:none Sessions freeze at creation. -func nativeDeploymentDefaults(model string, provider *v1.ModelProviderInput) api.Option { +func nativeDeploymentDefaults(s *store.Store, model string, provider *v1.ModelProviderInput) func(*api.Dependencies) { revision := uuid.New() - return api.WithModelProviderDefaults(func(context.Context, string) (*store.DeploymentModelProviderSnapshot, error) { + return modelProviderDefaults(s, func(context.Context, string) (*store.DeploymentModelProviderSnapshot, error) { return &store.DeploymentModelProviderSnapshot{Model: model, Provider: provider, Revision: revision}, nil }) } diff --git a/services/core/internal/store/native_public_execution_test.go b/services/core/internal/store/native_public_execution_test.go index f8a95250c..43420f2be 100644 --- a/services/core/internal/store/native_public_execution_test.go +++ b/services/core/internal/store/native_public_execution_test.go @@ -10,7 +10,6 @@ import ( "time" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/google/uuid" @@ -44,7 +43,7 @@ func verifyNativePublicExecution(t *testing.T, h *dispatchHarness, parent contex if err != nil { t.Fatal(err) } - handler, err := api.NewHandler(h.s, auth, "codex", api.WithExecution(worker), nativeDeploymentDefaults("gpt-5.5", provider)) + handler, err := publicHandler(t, h.s, auth, "codex", workerExecution(worker), nativeDeploymentDefaults(h.s, "gpt-5.5", provider)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/path_id_semantics_public_test.go b/services/core/internal/store/path_id_semantics_public_test.go index 7978cfdb4..a57a25391 100644 --- a/services/core/internal/store/path_id_semantics_public_test.go +++ b/services/core/internal/store/path_id_semantics_public_test.go @@ -10,7 +10,6 @@ import ( "strings" "testing" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" @@ -101,7 +100,7 @@ func TestMalformedPathIDsMatchMissingPostgres(t *testing.T) { if err != nil { t.Fatal(err) } - h, err := api.NewHandler(s, auth, "codex", api.WithExecution(s), api.WithSubagents(s), api.WithSkills(s), api.WithSourceFiles(s), api.WithSessionArtifacts(s)) + h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) if err != nil { t.Fatal(err) } @@ -363,7 +362,7 @@ func TestMalformedPathIDsMatchMissingPostgres(t *testing.T) { } // Storage availability checks also run before the lookup of a missing identifier. - h, err = api.NewHandler(store.New(pool), auth, "codex") + h, err = publicHandler(t, store.New(pool), auth, "codex") if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/project_api_keys_http_test.go b/services/core/internal/store/project_api_keys_http_test.go index 91342f725..0bd8d6019 100644 --- a/services/core/internal/store/project_api_keys_http_test.go +++ b/services/core/internal/store/project_api_keys_http_test.go @@ -15,15 +15,11 @@ import ( func TestProjectAndSharedKeysHTTPManagement(t *testing.T) { st, _ := store.NewTestStore(t) adminToken := uuid.NewString() - auth, err := api.NewDatabaseAuthenticator(st) - if err != nil { - t.Fatal(err) - } admin, err := api.NewDeploymentAuthenticator([]string{runtimedevice.HashCredential(adminToken)}) if err != nil { t.Fatal(err) } - h, err := api.NewHandler(st, auth, "codex", api.WithProjectAPIKeys(st, admin)) + h, err := publicHandler(t, st, nil, "codex", storeKeys(st), withCoreKeys(admin)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/public_handler_fixture_test.go b/services/core/internal/store/public_handler_fixture_test.go new file mode 100644 index 000000000..9b03cc85a --- /dev/null +++ b/services/core/internal/store/public_handler_fixture_test.go @@ -0,0 +1,214 @@ +package store_test + +import ( + "context" + "encoding/json" + "net/http" + "testing" + + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/coremetrics" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" + "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" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" +) + +// testExecutorURL is the daemon URL self-hosted Sessions report unless a test +// sets another with executorURL. +const testExecutorURL = "wss://core.example/api/v1/agent-daemon/ws" + +// publicHandler serves s through api.NewHandler. s backs every area the Store +// implements, keys authenticate as Project keys and "admin" as the Core key. +// Metrics, Runtime observation and history, and executor connections are +// strict stand-ins. Execution and Sandboxes stay disabled unless configure +// sets them. +func publicHandler(t testing.TB, s *store.Store, keys fixtureKeyResolver, engine string, configure ...func(*api.Dependencies)) (http.Handler, error) { + t.Helper() + admin, err := api.NewDeploymentAuthenticator([]string{runtimedevice.HashCredential("admin")}) + if err != nil { + return nil, err + } + strict := strictStandIn{t} + deps := api.Dependencies{ + Engine: engine, CoreKeys: admin, InstallationBindings: s, + Projects: fixtureProjects{Store: s, keys: keys}, Vaults: s, ModelProviders: s, Files: s, Skills: s, + EnvironmentTemplates: s, Agents: s, Sessions: s, SessionEvents: s, SessionHistory: s, Subagents: s, + Artifacts: s, SessionAdmin: s, Environments: s, Admin: s, WriteAudit: s, + ExecutorConnections: strict, Metrics: strict, RuntimeObservations: strict, RuntimeHistory: strict, + } + for _, c := range configure { + c(&deps) + } + return api.NewHandler(deps) +} + +// fixtureProjects serves Projects from the Store and resolves Project keys from +// the test's fixture keys. +type fixtureProjects struct { + *store.Store + keys fixtureKeyResolver +} + +func (p fixtureProjects) ResolveProjectAPIKey(ctx context.Context, digest string) (store.ProjectAPIKeyBinding, error) { + return p.keys.ResolveProjectAPIKey(ctx, digest) +} + +// storeKeys resolves Project keys from the Store, as production does. +func storeKeys(s *store.Store) func(*api.Dependencies) { + return func(d *api.Dependencies) { d.Projects = s } +} + +// withCoreKeys replaces the Core key. +func withCoreKeys(keys *api.DeploymentAuthenticator) func(*api.Dependencies) { + return func(d *api.Dependencies) { d.CoreKeys = keys } +} + +// withHarnesses enables Harnesses besides the default for explicit selection. +func withHarnesses(kinds []string) func(*api.Dependencies) { + return func(d *api.Dependencies) { d.Harnesses = kinds } +} + +// withPolicy replaces the built-in Harness qualification. +func withPolicy(policy execution.Policy) func(*api.Dependencies) { + return func(d *api.Dependencies) { d.Policy = policy } +} + +// storeExecution admits Sessions and inputs through the Store without a +// Worker, so nothing runs them. +func storeExecution(t testing.TB, s *store.Store) func(*api.Dependencies) { + return func(d *api.Dependencies) { + d.Execution = &api.Execution{ExecutorURL: testExecutorURL, Admission: s, SessionArchive: strictStandIn{t}, Workspaces: strictStandIn{t}} + } +} + +// workerExecution runs Sessions through worker. +func workerExecution(worker *execution.Worker) func(*api.Dependencies) { + return func(d *api.Dependencies) { + d.Execution = &api.Execution{ExecutorURL: testExecutorURL, Admission: worker, SessionArchive: worker, Workspaces: worker} + } +} + +// executorURL replaces the daemon URL self-hosted Sessions report. It follows +// the option that enables Execution. +func executorURL(url string) func(*api.Dependencies) { + return func(d *api.Dependencies) { d.Execution.ExecutorURL = url } +} + +// managedSandboxes enables the Store's managed sandbox deployment: its +// administration routes and openai_hosted Environments. It follows the option +// that enables Execution. +func managedSandboxes(t testing.TB, s *store.Store) func(*api.Dependencies) { + return func(d *api.Dependencies) { + d.Sandboxes = &api.Sandboxes{Deployment: s, DeploymentChanges: strictStandIn{t}, ConfigurationDiscovery: strictStandIn{t}} + } +} + +// modelProviderDefaults resolves deployment model provider defaults with +// resolve instead of the Store's deployment configuration. +func modelProviderDefaults(s *store.Store, resolve func(context.Context, string) (*store.DeploymentModelProviderSnapshot, error)) func(*api.Dependencies) { + return func(d *api.Dependencies) { d.ModelProviders = resolvedModelProviders{Store: s, resolve: resolve} } +} + +type resolvedModelProviders struct { + *store.Store + resolve func(context.Context, string) (*store.DeploymentModelProviderSnapshot, error) +} + +func (p resolvedModelProviders) DeploymentModelProvider(ctx context.Context, harness string) (*store.DeploymentModelProviderSnapshot, error) { + return p.resolve(ctx, harness) +} + +// acceptUnavailable lets the handler count execution_unavailable responses for +// tests that expect them. +func acceptUnavailable(t testing.TB) func(*api.Dependencies) { + return func(d *api.Dependencies) { d.Metrics = unavailableMetrics{strictStandIn{t}} } +} + +type unavailableMetrics struct{ strictStandIn } + +func (unavailableMetrics) RecordUnavailable() {} + +// strictStandIn fails the test on any call. It stands in for the areas a Store +// test does not exercise. +type strictStandIn struct{ t testing.TB } + +func (s strictStandIn) unexpected(method string) { + s.t.Helper() + s.t.Fatalf("unexpected call to %s", method) +} + +func (s strictStandIn) Read(context.Context, string) (coremetrics.View, error) { + s.unexpected("Read") + return coremetrics.View{}, nil +} + +func (s strictStandIn) RecordUnavailable() { s.unexpected("RecordUnavailable") } + +func (s strictStandIn) ObserveSession(context.Context, string, string) (runtimeobs.Observation, error) { + s.unexpected("ObserveSession") + return runtimeobs.Observation{}, nil +} + +func (s strictStandIn) ObserveSessions(context.Context, []runtimeobs.SessionIdentity, runtimeobs.PageOptions) ([]runtimeobs.Observation, []error) { + s.unexpected("ObserveSessions") + return nil, nil +} + +func (s strictStandIn) Capabilities() runtimehistory.Capabilities { + s.unexpected("Capabilities") + return runtimehistory.Capabilities{} +} + +func (s strictStandIn) QuerySession(context.Context, string, string, runtimehistory.Range) (runtimehistory.Response, error) { + s.unexpected("QuerySession") + return runtimehistory.Response{}, nil +} + +func (s strictStandIn) ExecutorConnected(context.Context, string, string) (bool, error) { + s.unexpected("ExecutorConnected") + return false, nil +} + +func (s strictStandIn) ArchiveManagedSession(context.Context, string, string, uint64) (store.ManagedSessionArchive, error) { + s.unexpected("ArchiveManagedSession") + return store.ManagedSessionArchive{}, nil +} + +func (s strictStandIn) ReadEnvironmentDirectory(context.Context, store.Environment, string) (proto.WorkspaceDirectoryResult, error) { + s.unexpected("ReadEnvironmentDirectory") + return proto.WorkspaceDirectoryResult{}, nil +} + +func (s strictStandIn) WriteEnvironmentFile(context.Context, store.Environment, string, []byte) (int64, error) { + s.unexpected("WriteEnvironmentFile") + return 0, nil +} + +func (s strictStandIn) DiscoverConfiguration(context.Context, string, sandbox.ConfigurationDiscoveryInput) (json.RawMessage, error) { + s.unexpected("DiscoverConfiguration") + return nil, nil +} + +func (s strictStandIn) InitializeSandboxDeployment(context.Context, store.SandboxDeploymentSetupRequest) (store.RuntimeDeploymentView, error) { + s.unexpected("InitializeSandboxDeployment") + return store.RuntimeDeploymentView{}, nil +} + +func (s strictStandIn) UpdateSandboxDeployment(context.Context, store.SandboxDeploymentUpdateRequest) (store.RuntimeDeploymentView, error) { + s.unexpected("UpdateSandboxDeployment") + return store.RuntimeDeploymentView{}, nil +} + +func (s strictStandIn) StartSandboxReset(context.Context, store.SandboxResetRequest) (store.RuntimeDeploymentView, error) { + s.unexpected("StartSandboxReset") + return store.RuntimeDeploymentView{}, nil +} + +func (s strictStandIn) CancelSandboxReset(context.Context, uint64) (store.RuntimeDeploymentView, error) { + s.unexpected("CancelSandboxReset") + return store.RuntimeDeploymentView{}, nil +} diff --git a/services/core/internal/store/remote_mcp_test.go b/services/core/internal/store/remote_mcp_test.go index 40fe921b1..1dcf2287a 100644 --- a/services/core/internal/store/remote_mcp_test.go +++ b/services/core/internal/store/remote_mcp_test.go @@ -7,7 +7,6 @@ import ( "strings" "testing" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" @@ -99,7 +98,7 @@ func selfHostedMCPAdmissionHandler(t *testing.T, s *store.Store, tenant string) if err != nil { t.Fatal(err) } - handler, err := api.NewHandler(s, auth, "codex", api.WithEnvironmentRemoteURL("https://executor.example")) + handler, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), executorURL("https://executor.example")) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/request_body_public_test.go b/services/core/internal/store/request_body_public_test.go index a4b7fb696..478ef5005 100644 --- a/services/core/internal/store/request_body_public_test.go +++ b/services/core/internal/store/request_body_public_test.go @@ -2,6 +2,7 @@ package store_test import ( "bytes" + "context" "encoding/json" "mime/multipart" "net/http" @@ -11,6 +12,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" @@ -36,7 +38,9 @@ func TestRequestBodyGateRejectsWithoutWritesPostgres(t *testing.T) { if err != nil { t.Fatal(err) } - h, err := api.NewHandler(s, auth, "codex", api.WithExecution(s)) + // No Runtime is connected, so a file write that passes the gate is unavailable. + unavailable := func(d *api.Dependencies) { d.Execution.Workspaces = unavailableWorkspaces{strictStandIn{t}} } + h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), unavailable, acceptUnavailable(t)) if err != nil { t.Fatal(err) } @@ -175,7 +179,7 @@ func TestRequestBodyGateExcludedRoutesPostgres(t *testing.T) { if err != nil { t.Fatal(err) } - h, err := api.NewHandler(s, auth, "codex", api.WithExecution(s), api.WithSkills(s), api.WithSourceFiles(s)) + h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) if err != nil { t.Fatal(err) } @@ -233,3 +237,9 @@ func TestRequestBodyGateExcludedRoutesPostgres(t *testing.T) { } } } + +type unavailableWorkspaces struct{ strictStandIn } + +func (unavailableWorkspaces) WriteEnvironmentFile(context.Context, store.Environment, string, []byte) (int64, error) { + return 0, execution.ErrExecutionUnavailable +} diff --git a/services/core/internal/store/sandbox_node_auth_order_http_test.go b/services/core/internal/store/sandbox_node_auth_order_http_test.go index ea7855a65..85619cce0 100644 --- a/services/core/internal/store/sandbox_node_auth_order_http_test.go +++ b/services/core/internal/store/sandbox_node_auth_order_http_test.go @@ -22,11 +22,7 @@ func TestSandboxNodeRoutesAuthenticateBeforeDeploymentState(t *testing.T) { if err != nil { t.Fatal(err) } - auth, err := api.NewDatabaseAuthenticator(s) - if err != nil { - t.Fatal(err) - } - handler, err := api.NewHandler(s, auth, "codex", api.WithSandboxManager(s, admin)) + handler, err := publicHandler(t, s, nil, "codex", storeKeys(s), storeExecution(t, s), managedSandboxes(t, s), withCoreKeys(admin)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/saved_web_search_public_test.go b/services/core/internal/store/saved_web_search_public_test.go index fec0e9d3a..0e5798492 100644 --- a/services/core/internal/store/saved_web_search_public_test.go +++ b/services/core/internal/store/saved_web_search_public_test.go @@ -8,7 +8,6 @@ import ( "strings" "testing" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" @@ -29,7 +28,7 @@ func TestSavedWebSearchPostgres(t *testing.T) { if err != nil { t.Fatal(err) } - h, err := api.NewHandler(s, auth, "codex", api.WithExecution(s)) + h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/self_hosted_cancel_public_test.go b/services/core/internal/store/self_hosted_cancel_public_test.go index 47beb1a9a..359cfebf2 100644 --- a/services/core/internal/store/self_hosted_cancel_public_test.go +++ b/services/core/internal/store/self_hosted_cancel_public_test.go @@ -11,7 +11,6 @@ import ( "testing" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" @@ -35,7 +34,7 @@ func TestSelfHostedCancellationOfficialClient(t *testing.T) { serve := func() (*httptest.Server, func(bool)) { t.Helper() worker, stop := publicInitialWorker(t, s) - handler, err := api.NewHandler(s, auth, "codex", api.WithExecution(worker), api.WithEnvironmentRemoteURL("https://offline-executor.example")) + handler, err := publicHandler(t, s, auth, "codex", workerExecution(worker), executorURL("https://offline-executor.example")) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/self_hosted_initial_public_test.go b/services/core/internal/store/self_hosted_initial_public_test.go index c5bb6a3af..6f5fc030a 100644 --- a/services/core/internal/store/self_hosted_initial_public_test.go +++ b/services/core/internal/store/self_hosted_initial_public_test.go @@ -43,11 +43,16 @@ func TestSelfHostedInitialCreationOfficialClient(t *testing.T) { const origin = "https://offline-executor.example" serve := func(s *store.Store, worker *execution.Worker) *httptest.Server { t.Helper() - options := []api.Option{api.WithEnvironmentRemoteURL(origin)} + enabled := []func(*api.Dependencies){acceptUnavailable(t)} if worker != nil { - options = append(options, api.WithExecution(worker)) + enabled = append(enabled, workerExecution(worker), executorURL(origin)) + } else { + // Without a Worker, Core keeps its executor URL but admits nothing. + enabled = append(enabled, func(d *api.Dependencies) { + d.Execution = &api.Execution{ExecutorURL: origin, Admission: unavailableAdmission{}, SessionArchive: strictStandIn{t}, Workspaces: strictStandIn{t}} + }) } - handler, err := api.NewHandler(s, auth, "codex", options...) + handler, err := publicHandler(t, s, auth, "codex", enabled...) if err != nil { t.Fatal(err) } @@ -230,3 +235,18 @@ func publicInitialWorker(t *testing.T, s *store.Store) (*execution.Worker, func( t.Cleanup(func() { stop(false) }) return worker, stop } + +// unavailableAdmission admits nothing, as a Core without a running Worker. +type unavailableAdmission struct{} + +func (unavailableAdmission) CreateSession(context.Context, string, store.CreateSessionInput) (store.Session, error) { + return store.Session{}, execution.ErrExecutionUnavailable +} + +func (unavailableAdmission) CreateSessionStream(context.Context, string, store.CreateSessionInput) (store.SessionCreation, error) { + return store.SessionCreation{}, execution.ErrExecutionUnavailable +} + +func (unavailableAdmission) SubmitInputs(context.Context, string, string, string, []store.Input) ([]store.InputReceipt, error) { + return nil, execution.ErrExecutionUnavailable +} diff --git a/services/core/internal/store/session_agent_filter_public_test.go b/services/core/internal/store/session_agent_filter_public_test.go index 1ef909dcc..704e65ba0 100644 --- a/services/core/internal/store/session_agent_filter_public_test.go +++ b/services/core/internal/store/session_agent_filter_public_test.go @@ -8,7 +8,6 @@ import ( "testing" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" @@ -28,14 +27,14 @@ func TestSessionAgentFilterOfficialClient(t *testing.T) { if err != nil { t.Fatal(err) } - h, err := api.NewHandler(s, auth, "codex", api.WithExecution(s)) + h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) if err != nil { t.Fatal(err) } server := httptest.NewServer(h) defer server.Close() recoveredStore := store.New(pool) - h, err = api.NewHandler(recoveredStore, auth, "codex", api.WithExecution(recoveredStore)) + h, err = publicHandler(t, recoveredStore, auth, "codex", storeExecution(t, recoveredStore)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/session_artifacts_public_test.go b/services/core/internal/store/session_artifacts_public_test.go index 4f7afe53f..44a840615 100644 --- a/services/core/internal/store/session_artifacts_public_test.go +++ b/services/core/internal/store/session_artifacts_public_test.go @@ -16,7 +16,6 @@ import ( "testing" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" @@ -80,7 +79,7 @@ func artifactHTTPServer(t *testing.T, s *store.Store) (server *httptest.Server, if err != nil { t.Fatal(err) } - h, err := api.NewHandler(s, auth, "codex", api.WithSessionArtifacts(s)) + h, err := publicHandler(t, s, auth, "codex") if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/session_deletion_lifecycle_public_test.go b/services/core/internal/store/session_deletion_lifecycle_public_test.go index 2b9931134..2ba0263e0 100644 --- a/services/core/internal/store/session_deletion_lifecycle_public_test.go +++ b/services/core/internal/store/session_deletion_lifecycle_public_test.go @@ -9,7 +9,6 @@ import ( "testing" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" @@ -33,7 +32,7 @@ func TestSessionDeletionLifecyclePostgres(t *testing.T) { if err != nil { t.Fatal(err) } - h, err := api.NewHandler(s, auth, "codex", api.WithEnvironmentRemoteURL("https://executor.example")) + h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), executorURL("https://executor.example")) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/session_deletion_public_test.go b/services/core/internal/store/session_deletion_public_test.go index 939ade92e..ee55b5750 100644 --- a/services/core/internal/store/session_deletion_public_test.go +++ b/services/core/internal/store/session_deletion_public_test.go @@ -8,7 +8,6 @@ import ( "testing" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" @@ -28,13 +27,13 @@ func TestSessionDeletionOfficialClient(t *testing.T) { if err != nil { t.Fatal(err) } - h, err := api.NewHandler(s, auth, "codex", api.WithExecution(s)) + h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) if err != nil { t.Fatal(err) } server := httptest.NewServer(h) defer server.Close() - h, err = api.NewHandler(store.New(pool), auth, "codex", api.WithExecution(store.New(pool))) + h, err = publicHandler(t, store.New(pool), auth, "codex", storeExecution(t, store.New(pool))) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/session_initial_public_test.go b/services/core/internal/store/session_initial_public_test.go index a431ba8e3..7bd4a6ba1 100644 --- a/services/core/internal/store/session_initial_public_test.go +++ b/services/core/internal/store/session_initial_public_test.go @@ -7,7 +7,6 @@ import ( "os/exec" "testing" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" @@ -37,13 +36,13 @@ func TestInitialSessionInputOfficialClient(t *testing.T) { t.Error(err) } }) - handler, err := api.NewHandler(s, auth, "codex", api.WithExecution(worker)) + handler, err := publicHandler(t, s, auth, "codex", workerExecution(worker)) if err != nil { t.Fatal(err) } server := httptest.NewServer(handler) defer server.Close() - unsupported, err := api.NewHandler(s, auth, "fake_alpha", api.WithExecution(worker)) + unsupported, err := publicHandler(t, s, auth, "fake_alpha", workerExecution(worker)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/session_model_execution_http_test.go b/services/core/internal/store/session_model_execution_http_test.go index 1de03a926..833f95043 100644 --- a/services/core/internal/store/session_model_execution_http_test.go +++ b/services/core/internal/store/session_model_execution_http_test.go @@ -7,7 +7,6 @@ import ( "strings" "testing" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" @@ -23,7 +22,7 @@ func TestModelExecutionHTTPWriteOnlyAndStrictAdmission(t *testing.T) { if err != nil { t.Fatal(err) } - handler, err := api.NewHandler(st, auth, "codex", api.WithHostedEnvironments(), api.WithExecution(st)) + handler, err := publicHandler(t, st, auth, "codex", storeExecution(t, st), managedSandboxes(t, st)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/session_reference_retry_public_test.go b/services/core/internal/store/session_reference_retry_public_test.go index 76a05efe9..2b509a688 100644 --- a/services/core/internal/store/session_reference_retry_public_test.go +++ b/services/core/internal/store/session_reference_retry_public_test.go @@ -10,7 +10,6 @@ import ( "testing" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" @@ -39,13 +38,13 @@ func TestSavedReferenceRetryOfficialClient(t *testing.T) { t.Error(err) } }) - handler, err := api.NewHandler(s, auth, "codex", api.WithExecution(worker)) + handler, err := publicHandler(t, s, auth, "codex", workerExecution(worker)) if err != nil { t.Fatal(err) } server := httptest.NewServer(handler) defer server.Close() - recovered, err := api.NewHandler(store.New(pool), auth, "codex") + recovered, err := publicHandler(t, store.New(pool), auth, "codex") if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/skill_selectors_public_test.go b/services/core/internal/store/skill_selectors_public_test.go index 1d71d21c0..2aee8c5ee 100644 --- a/services/core/internal/store/skill_selectors_public_test.go +++ b/services/core/internal/store/skill_selectors_public_test.go @@ -10,7 +10,6 @@ import ( "testing" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" @@ -36,14 +35,14 @@ func TestSkillSelectorsOfficialClientPostgres(t *testing.T) { if err != nil { t.Fatal(err) } - h, err := api.NewHandler(s, auth, "codex", api.WithSkills(s)) + h, err := publicHandler(t, s, auth, "codex") if err != nil { t.Fatal(err) } server := httptest.NewServer(h) defer server.Close() recoveredStore := store.NewWithCredentialCipher(pool, cipher) - h, err = api.NewHandler(recoveredStore, auth, "codex", api.WithSkills(recoveredStore)) + h, err = publicHandler(t, recoveredStore, auth, "codex") if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/skill_version_deletion_public_test.go b/services/core/internal/store/skill_version_deletion_public_test.go index 45b6250f9..87246a6c4 100644 --- a/services/core/internal/store/skill_version_deletion_public_test.go +++ b/services/core/internal/store/skill_version_deletion_public_test.go @@ -9,7 +9,6 @@ import ( "strings" "testing" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" @@ -35,7 +34,7 @@ func TestSkillVersionDeletionHTTPPostgres(t *testing.T) { if err != nil { t.Fatal(err) } - h, err := api.NewHandler(s, auth, "codex", api.WithSkills(s)) + h, err := publicHandler(t, s, auth, "codex") if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/skills_public_test.go b/services/core/internal/store/skills_public_test.go index 17de4ad95..099ff3887 100644 --- a/services/core/internal/store/skills_public_test.go +++ b/services/core/internal/store/skills_public_test.go @@ -10,7 +10,6 @@ import ( "testing" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" @@ -36,14 +35,14 @@ func TestSkillsOfficialClientPostgres(t *testing.T) { if err != nil { t.Fatal(err) } - h, err := api.NewHandler(s, auth, "codex", api.WithSkills(s)) + h, err := publicHandler(t, s, auth, "codex") if err != nil { t.Fatal(err) } server := httptest.NewServer(h) defer server.Close() recoveredStore := store.NewWithCredentialCipher(pool, cipher) - h, err = api.NewHandler(recoveredStore, auth, "codex", api.WithSkills(recoveredStore)) + h, err = publicHandler(t, recoveredStore, auth, "codex") if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/source_files_errors_public_test.go b/services/core/internal/store/source_files_errors_public_test.go index b79ec755d..a73c7e5df 100644 --- a/services/core/internal/store/source_files_errors_public_test.go +++ b/services/core/internal/store/source_files_errors_public_test.go @@ -8,7 +8,6 @@ import ( "testing" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" @@ -28,7 +27,7 @@ func TestSourceFileErrorsOfficialClientPostgres(t *testing.T) { if err != nil { t.Fatal(err) } - h, err := api.NewHandler(s, auth, "codex", api.WithSourceFiles(s)) + h, err := publicHandler(t, s, auth, "codex") if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/stream_authority_http_test.go b/services/core/internal/store/stream_authority_http_test.go index 75e7aa64b..981611775 100644 --- a/services/core/internal/store/stream_authority_http_test.go +++ b/services/core/internal/store/stream_authority_http_test.go @@ -10,7 +10,6 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/adminaudit" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" ) @@ -41,11 +40,7 @@ func TestLiveStreamClosesAfterKeyRevocationOrProjectArchive(t *testing.T) { if err != nil { t.Fatal(err) } - auth, err := api.NewDatabaseAuthenticator(s) - if err != nil { - t.Fatal(err) - } - h, err := api.NewHandler(s, auth, "codex", api.WithEnvironmentRemoteURL("wss://core.example/api/v1/agent-daemon/ws")) + h, err := publicHandler(t, s, nil, "codex", storeKeys(s), storeExecution(t, s)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/structured_output_native_test.go b/services/core/internal/store/structured_output_native_test.go index 7a5c19d03..2fe9f85bf 100644 --- a/services/core/internal/store/structured_output_native_test.go +++ b/services/core/internal/store/structured_output_native_test.go @@ -10,7 +10,6 @@ import ( "testing" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/google/uuid" @@ -51,7 +50,7 @@ func TestNativeStructuredOutputPublicExecution(t *testing.T) { if err != nil { t.Fatal(err) } - handler, err := api.NewHandler(h.s, auth, "claude_sdk", api.WithExecution(worker), nativeDeploymentDefaults(model, provider)) + handler, err := publicHandler(t, h.s, auth, "claude_sdk", workerExecution(worker), nativeDeploymentDefaults(h.s, model, provider)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/subagent_visibility_public_test.go b/services/core/internal/store/subagent_visibility_public_test.go index e1e70d522..8db73bbba 100644 --- a/services/core/internal/store/subagent_visibility_public_test.go +++ b/services/core/internal/store/subagent_visibility_public_test.go @@ -9,7 +9,6 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" "github.com/google/uuid" @@ -79,7 +78,7 @@ func TestSubagentVisibilityPublic(t *testing.T) { if err != nil { t.Fatal(err) } - handler, err := api.NewHandler(s, auth, "codex", api.WithExecution(storeAdmission{s}), api.WithSubagents(s)) + handler, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/template_composition_public_test.go b/services/core/internal/store/template_composition_public_test.go index c9f5de1ed..a6a7f1ec4 100644 --- a/services/core/internal/store/template_composition_public_test.go +++ b/services/core/internal/store/template_composition_public_test.go @@ -13,7 +13,6 @@ import ( "time" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" @@ -43,7 +42,7 @@ func TestTemplateCompositionOfficialClientPostgres(t *testing.T) { serve := func(current *store.Store) *httptest.Server { t.Helper() // Hosted admission and freezing use the real Store; no Runtime or model runs. - h, err := api.NewHandler(current, auth, "codex", api.WithHostedEnvironments(), api.WithExecution(current), api.WithSourceFiles(current), fixtureDeploymentProvider()) + h, err := publicHandler(t, current, auth, "codex", storeExecution(t, current), managedSandboxes(t, current), fixtureDeploymentProvider(current)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/template_null_selection_public_test.go b/services/core/internal/store/template_null_selection_public_test.go index a78579baa..fc9edca32 100644 --- a/services/core/internal/store/template_null_selection_public_test.go +++ b/services/core/internal/store/template_null_selection_public_test.go @@ -15,7 +15,6 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/internal/agentplugin" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" @@ -44,7 +43,7 @@ func TestTemplateNullSelectionOfficialClientPostgres(t *testing.T) { } serve := func(current *store.Store) *httptest.Server { t.Helper() - h, err := api.NewHandler(current, auth, "codex", api.WithHostedEnvironments(), api.WithExecution(current), api.WithSourceFiles(current), api.WithSkills(current), fixtureDeploymentProvider()) + h, err := publicHandler(t, current, auth, "codex", storeExecution(t, current), managedSandboxes(t, current), fixtureDeploymentProvider(current)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/tool_policy_native_test.go b/services/core/internal/store/tool_policy_native_test.go index 22982c981..f47dcabd0 100644 --- a/services/core/internal/store/tool_policy_native_test.go +++ b/services/core/internal/store/tool_policy_native_test.go @@ -10,7 +10,6 @@ import ( "testing" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/google/uuid" @@ -57,7 +56,7 @@ func TestNativeToolPolicyPublicExecution(t *testing.T) { if err != nil { t.Fatal(err) } - handler, err := api.NewHandler(h.s, auth, kind, api.WithExecution(worker), api.WithExecutionPolicy(h.d.Policy), nativeDeploymentDefaults(model, provider)) + handler, err := publicHandler(t, h.s, auth, kind, workerExecution(worker), withPolicy(h.d.Policy), nativeDeploymentDefaults(h.s, model, provider)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/tool_search_native_test.go b/services/core/internal/store/tool_search_native_test.go index 4e8e48a7b..1c7928c03 100644 --- a/services/core/internal/store/tool_search_native_test.go +++ b/services/core/internal/store/tool_search_native_test.go @@ -10,7 +10,6 @@ import ( "testing" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/google/uuid" @@ -51,7 +50,7 @@ func TestNativeToolSearchPublicExecution(t *testing.T) { if err != nil { t.Fatal(err) } - handler, err := api.NewHandler(h.s, auth, "claude_sdk", api.WithExecution(worker), nativeDeploymentDefaults(model, provider)) + handler, err := publicHandler(t, h.s, auth, "claude_sdk", workerExecution(worker), nativeDeploymentDefaults(h.s, model, provider)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/unified_model_configuration_http_test.go b/services/core/internal/store/unified_model_configuration_http_test.go index 2213c7d12..a168a0b6d 100644 --- a/services/core/internal/store/unified_model_configuration_http_test.go +++ b/services/core/internal/store/unified_model_configuration_http_test.go @@ -25,7 +25,7 @@ func TestUnifiedModelConfigurationHTTP(t *testing.T) { if err != nil { t.Fatal(err) } - handler, err := api.NewHandler(st, auth, "codex", api.WithProjectAPIKeys(st, admin), api.WithHarnesses([]string{"codex", "claude_sdk"}), api.WithHostedEnvironments(), api.WithExecution(st), api.WithEnvironmentRemoteURL("wss://core.example/api/v1/agent-daemon/ws"), api.WithModelProviderDefaults(st.DeploymentModelProvider)) + handler, err := publicHandler(t, st, auth, "codex", storeExecution(t, st), managedSandboxes(t, st), withCoreKeys(admin), withHarnesses([]string{"codex", "claude_sdk"})) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/unstorable_text_public_test.go b/services/core/internal/store/unstorable_text_public_test.go index f01d923cf..81bfb50e0 100644 --- a/services/core/internal/store/unstorable_text_public_test.go +++ b/services/core/internal/store/unstorable_text_public_test.go @@ -10,7 +10,6 @@ import ( "strings" "testing" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" @@ -32,7 +31,7 @@ func TestUnstorableTextRejectsWithoutWritesPostgres(t *testing.T) { if err != nil { t.Fatal(err) } - h, err := api.NewHandler(s, auth, "codex", api.WithExecution(s), api.WithSkills(s), api.WithSourceFiles(s)) + h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/whitespace_input_public_test.go b/services/core/internal/store/whitespace_input_public_test.go index c46e26561..0036fcbfc 100644 --- a/services/core/internal/store/whitespace_input_public_test.go +++ b/services/core/internal/store/whitespace_input_public_test.go @@ -8,7 +8,6 @@ import ( "reflect" "testing" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" @@ -27,7 +26,7 @@ func TestWhitespaceInputStoredVerbatimPostgres(t *testing.T) { if err != nil { t.Fatal(err) } - h, err := api.NewHandler(s, auth, "codex", api.WithExecution(s)) + h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) if err != nil { t.Fatal(err) } @@ -117,7 +116,7 @@ func TestWhitespaceOnlyTextHarnessAdmissionPostgres(t *testing.T) { } }) serve := func(engine string) pathIDClient { - handler, err := api.NewHandler(s, auth, engine, api.WithExecution(worker), api.WithEnvironmentRemoteURL("https://offline-executor.example")) + handler, err := publicHandler(t, s, auth, engine, workerExecution(worker), executorURL("https://offline-executor.example")) if err != nil { t.Fatal(err) } From 0c1d7544a6a19b5090d19e89462df37cc3c50192 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 30 Sep 2026 15:32:12 +0000 Subject: [PATCH 2/3] Remove what the explicit Dependencies left unused Core no longer emits skill_storage_unavailable, file_storage_unavailable or diagnostics_unavailable, so Web and the agents-client tests stop handling them. cmd/server drops a managed-node check that is always true and the unreachable type-asserted observation source behind it, plus a repeated installation ID check. CoreErrorBoolean had no caller, so Core error details no longer list booleans. --- .../features/files/file-operations.test.ts | 1 - .../web/src/features/files/file-operations.ts | 4 +--- .../src/features/skills/skill-operations.ts | 5 +---- apps/web/src/i18n/locales/en/core-errors.ts | 1 - apps/web/src/i18n/locales/en/files.ts | 5 ----- apps/web/src/i18n/locales/en/skills.ts | 5 ----- .../web/src/i18n/locales/zh-CN/core-errors.ts | 1 - apps/web/src/i18n/locales/zh-CN/files.ts | 5 ----- apps/web/src/i18n/locales/zh-CN/skills.ts | 5 ----- apps/web/src/lib/core-error.ts | 2 +- contracts/agents-api/core-errors.md | 2 +- .../src/fixtures/parsar-d3f55046/skills.json | 19 ------------------ .../parsar-d3f55046/source-files-list.json | 20 ------------------- packages/agents-client/src/skills.test.ts | 2 -- .../src/source-files-list.test.ts | 5 ----- services/core/IMPLEMENTATION.md | 2 +- services/core/cmd/server/main.go | 11 +--------- services/core/cmd/server/managed_nodes.go | 2 +- services/core/internal/api/core_errors.go | 3 +-- .../core/internal/api/core_errors_test.go | 6 +++--- 20 files changed, 11 insertions(+), 95 deletions(-) diff --git a/apps/web/src/features/files/file-operations.test.ts b/apps/web/src/features/files/file-operations.test.ts index 73b0a842c..b3a28b58a 100644 --- a/apps/web/src/features/files/file-operations.test.ts +++ b/apps/web/src/features/files/file-operations.test.ts @@ -56,7 +56,6 @@ describe("error mapping", () => { it("classifies a failed first list read", () => { expect(classifyFilesListError(new AgentCoreError("missing", 404))).toBe("unsupported"); expect(classifyFilesListError(new AgentCoreError("method", 405))).toBe("unsupported"); - expect(classifyFilesListError(new AgentCoreError("storage", 503, "file_storage_unavailable"))).toBe("storage-unavailable"); expect(classifyFilesListError(new AgentCoreError("busy", 503))).toBe("failed"); expect(classifyFilesListError(new TypeError("Failed to fetch"))).toBe("failed"); }); diff --git a/apps/web/src/features/files/file-operations.ts b/apps/web/src/features/files/file-operations.ts index 7e39a2d23..a130db446 100644 --- a/apps/web/src/features/files/file-operations.ts +++ b/apps/web/src/features/files/file-operations.ts @@ -35,13 +35,12 @@ export function isAbortError(error: unknown): boolean { return error instanceof DOMException && error.name === "AbortError"; } -export type FilesListFailure = "unsupported" | "storage-unavailable" | "failed"; +export type FilesListFailure = "unsupported" | "failed"; /** How a failed first list read is presented. */ export function classifyFilesListError(error: unknown): FilesListFailure { if (error instanceof AgentCoreError) { if (error.status === 404 || error.status === 405) return "unsupported"; - if (error.status === 503 && error.code === "file_storage_unavailable") return "storage-unavailable"; } return "failed"; } @@ -53,7 +52,6 @@ export function filesErrorReason(error: unknown, t: Translate): string { if (error instanceof AgentCoreError) { if (error.status === 401 || error.status === 403) return t("errors.unauthorized"); if (error.status === 413) return t("errors.tooLarge"); - if (error.status === 503 && error.code === "file_storage_unavailable") return t("errors.storage"); if (error.status === 502 && typeof error.code === "string" && error.code.startsWith("invalid_source_file")) { return t("errors.invalidResponse"); } diff --git a/apps/web/src/features/skills/skill-operations.ts b/apps/web/src/features/skills/skill-operations.ts index 72c5d3297..5d1c00cf6 100644 --- a/apps/web/src/features/skills/skill-operations.ts +++ b/apps/web/src/features/skills/skill-operations.ts @@ -6,10 +6,9 @@ import { appendCollectionPage } from "../../lib/collection-pagination"; * Skill navigation support, from the first Skill list request: * - `supported`: the list succeeded; * - `unsupported`: 404 or 405, an older Core without Skills (hide the entry); - * - `storage-unavailable`: 503 `skill_storage_unavailable` (show the entry with an explanation); * - `error`: anything else (show the entry with a retry). */ -export type SkillsSupport = "supported" | "unsupported" | "storage-unavailable" | "error"; +export type SkillsSupport = "supported" | "unsupported" | "error"; export const SKILLS_PAGE_SIZE = 20; @@ -20,7 +19,6 @@ export function isAbortError(error: unknown): boolean { export function classifySkillsError(error: unknown): Exclude { if (error instanceof AgentCoreError) { if (error.status === 404 || error.status === 405) return "unsupported"; - if (error.status === 503 && error.code === "skill_storage_unavailable") return "storage-unavailable"; } return "error"; } @@ -34,7 +32,6 @@ export function coreErrorMessage(error: unknown): string { export type SkillUploadFailure = | { kind: "invalid"; message: string } | { kind: "too-large" } - | { kind: "storage-unavailable" } | { kind: "interrupted"; cancelled: boolean } | { kind: "other"; message: string }; diff --git a/apps/web/src/i18n/locales/en/core-errors.ts b/apps/web/src/i18n/locales/en/core-errors.ts index 2aabbc23a..f35e539f2 100644 --- a/apps/web/src/i18n/locales/en/core-errors.ts +++ b/apps/web/src/i18n/locales/en/core-errors.ts @@ -27,7 +27,6 @@ export const coreErrors = { "project_api_key_exists": "An active API key with this name already exists.", "executor_credential_exists": "An executor credential with this name already exists.", "credential_storage_unavailable": "Core credential storage is unavailable. Check its credential encryption configuration.", - "diagnostics_unavailable": "Core diagnostics are unavailable.", "internal_error": "Core could not complete the request.", "sandbox_generation_stale": "Core has a newer sandbox configuration. Refresh and review it before submitting again.", "sandbox_reset_required": "Reset the sandbox deployment before changing this configuration.", diff --git a/apps/web/src/i18n/locales/en/files.ts b/apps/web/src/i18n/locales/en/files.ts index c6794fd91..392f901cb 100644 --- a/apps/web/src/i18n/locales/en/files.ts +++ b/apps/web/src/i18n/locales/en/files.ts @@ -50,10 +50,6 @@ export const files = { title: "This Core does not list files", description: "The Files list returned HTTP {{status}}.", }, - storage: { - title: "File storage is not configured", - description: "Core has no file storage configured, so files cannot be listed or uploaded.", - }, upload: { uploading: "Uploading {{name}}…", uploaded: "Uploaded {{name}}", @@ -74,7 +70,6 @@ export const files = { transport: "The request did not reach Core or its response was lost.", unauthorized: "Core rejected the connection credentials.", tooLarge: "The file is larger than Core accepts.", - storage: "Core has no file storage configured.", invalidResponse: "Core returned a response this console does not recognize.", }, } as const; diff --git a/apps/web/src/i18n/locales/en/skills.ts b/apps/web/src/i18n/locales/en/skills.ts index f86bdeae7..d4f212342 100644 --- a/apps/web/src/i18n/locales/en/skills.ts +++ b/apps/web/src/i18n/locales/en/skills.ts @@ -51,10 +51,6 @@ export const skills = { description: "Upload a folder, or a ZIP of one, whose SKILL.md starts like this:", exampleLabel: "Minimal report/SKILL.md", }, - storage: { - title: "Core has no Skill storage configured", - description: "Skills can be listed and uploaded once the Core operator configures Skill storage.", - }, unsupported: { title: "This Core does not offer Skills", description: "The connected Core answered the Skill list with {{status}}.", @@ -178,7 +174,6 @@ export const skills = { errors: { invalid: "Core rejected the Skill: {{message}}", tooLarge: "The files exceed the size limit.", - storage: "Core has no Skill storage configured.", interrupted: "The upload did not finish. Your selection is kept; Core may have stored it anyway, so check the list before trying again.", cancelled: "Upload cancelled. Your selection is kept; Core may have stored it anyway, so check the list before trying again.", other: "The upload failed: {{message}}", diff --git a/apps/web/src/i18n/locales/zh-CN/core-errors.ts b/apps/web/src/i18n/locales/zh-CN/core-errors.ts index 64db7d096..3af8049d1 100644 --- a/apps/web/src/i18n/locales/zh-CN/core-errors.ts +++ b/apps/web/src/i18n/locales/zh-CN/core-errors.ts @@ -27,7 +27,6 @@ export const coreErrors = { "project_api_key_exists": "此名称已被使用中的 API Key 占用。", "executor_credential_exists": "此名称的执行器凭证已存在。", "credential_storage_unavailable": "Core 凭证存储不可用,请检查凭证加密配置。", - "diagnostics_unavailable": "Core 诊断暂不可用。", "internal_error": "Core 未能完成请求。", "sandbox_generation_stale": "Core 的沙箱配置已更新。请刷新并检查后再提交。", "sandbox_reset_required": "请先重置沙箱部署,再更改此配置。", diff --git a/apps/web/src/i18n/locales/zh-CN/files.ts b/apps/web/src/i18n/locales/zh-CN/files.ts index 1ee7e05dd..872d5da1e 100644 --- a/apps/web/src/i18n/locales/zh-CN/files.ts +++ b/apps/web/src/i18n/locales/zh-CN/files.ts @@ -54,10 +54,6 @@ export const files: TranslationShape = { title: "此 Core 不支持列出文件", description: "文件列表请求返回 HTTP {{status}}。", }, - storage: { - title: "未配置文件存储", - description: "Core 未配置文件存储,因此无法列出或上传文件。", - }, upload: { uploading: "正在上传 {{name}}…", uploaded: "已上传 {{name}}", @@ -78,7 +74,6 @@ export const files: TranslationShape = { transport: "请求未到达 Core,或响应已丢失。", unauthorized: "Core 拒绝了当前连接凭据。", tooLarge: "文件超过 Core 的大小上限。", - storage: "Core 未配置文件存储。", invalidResponse: "Core 返回了控制台无法识别的响应。", }, }; diff --git a/apps/web/src/i18n/locales/zh-CN/skills.ts b/apps/web/src/i18n/locales/zh-CN/skills.ts index d26958e35..7f309eafa 100644 --- a/apps/web/src/i18n/locales/zh-CN/skills.ts +++ b/apps/web/src/i18n/locales/zh-CN/skills.ts @@ -51,10 +51,6 @@ export const skills = { description: "上传一个文件夹或它的 ZIP,其中的 SKILL.md 以这样的内容开头:", exampleLabel: "最小的 report/SKILL.md", }, - storage: { - title: "Core 未配置 Skill 存储", - description: "Core 运维方配置 Skill 存储后,才能查看和上传 Skill。", - }, unsupported: { title: "当前 Core 不提供 Skill", description: "所连接的 Core 对 Skill 列表请求返回了 {{status}}。", @@ -178,7 +174,6 @@ export const skills = { errors: { invalid: "Core 拒绝了这个 Skill:{{message}}", tooLarge: "文件超出大小限制。", - storage: "Core 未配置 Skill 存储。", interrupted: "上传没有完成。已选的文件仍然保留;Core 可能已经保存了它,重试前请先查看列表。", cancelled: "已取消上传。已选的文件仍然保留;Core 可能已经保存了它,重试前请先查看列表。", other: "上传失败:{{message}}", diff --git a/apps/web/src/lib/core-error.ts b/apps/web/src/lib/core-error.ts index 28acf1303..2e0405cf1 100644 --- a/apps/web/src/lib/core-error.ts +++ b/apps/web/src/lib/core-error.ts @@ -9,7 +9,7 @@ const codes = new Set([ "invalid_name", "invalid_node_capacity", "invalid_model_provider", "model_configuration_model_invalid", "harness_config_invalid", "model_provider_base_url_invalid", "model_provider_protocol_unsupported", "model_provider_api_key_invalid", "model_provider_token_limits_invalid", "invalid_sandbox_configuration", "project_archived", "project_exists", "project_api_key_exists", - "executor_credential_exists", "credential_storage_unavailable", "diagnostics_unavailable", "internal_error", + "executor_credential_exists", "credential_storage_unavailable", "internal_error", ]); /** Only catalogued, correctly typed detail keys can enter localized text. */ diff --git a/contracts/agents-api/core-errors.md b/contracts/agents-api/core-errors.md index be9440ac4..a2518ace5 100644 --- a/contracts/agents-api/core-errors.md +++ b/contracts/agents-api/core-errors.md @@ -10,7 +10,7 @@ Errors on `/v1` and `/api/v1` keep their own envelopes and never carry `details` ## Optional details -`error.details`, when present, is a nonempty flat object. Its values are strings, finite numbers, booleans, null or arrays of strings (possibly empty). It holds only Core-owned facts: never submitted names, URLs or keys, echoed request values, native error text or provider response bodies. Each code that has details lists its exact keys below. +`error.details`, when present, is a nonempty flat object. Its values are strings, finite numbers, null or arrays of strings (possibly empty). It holds only Core-owned facts: never submitted names, URLs or keys, echoed request values, native error text or provider response bodies. Each code that has details lists its exact keys below. | Code | Details | | --- | --- | diff --git a/packages/agents-client/src/fixtures/parsar-d3f55046/skills.json b/packages/agents-client/src/fixtures/parsar-d3f55046/skills.json index c9f7bd17d..a247e7029 100644 --- a/packages/agents-client/src/fixtures/parsar-d3f55046/skills.json +++ b/packages/agents-client/src/fixtures/parsar-d3f55046/skills.json @@ -307,25 +307,6 @@ "param": null } } - }, - "error_storage_unavailable": { - "request": { - "method": "GET", - "path": "/v1/skills" - }, - "status": 503, - "headers": { - "content-type": "application/json", - "cache-control": "no-store" - }, - "body": { - "error": { - "message": "Skill storage is unavailable.", - "type": "server_error", - "code": "skill_storage_unavailable", - "param": null - } - } } } } diff --git a/packages/agents-client/src/fixtures/parsar-d3f55046/source-files-list.json b/packages/agents-client/src/fixtures/parsar-d3f55046/source-files-list.json index bbd77ffba..0d783bbd2 100644 --- a/packages/agents-client/src/fixtures/parsar-d3f55046/source-files-list.json +++ b/packages/agents-client/src/fixtures/parsar-d3f55046/source-files-list.json @@ -227,26 +227,6 @@ "param": "purpose" } } - }, - "storage_unavailable": { - "request": { - "method": "GET", - "path": "/v1/files", - "query": "limit=100" - }, - "status": 503, - "headers": { - "content-type": "application/json", - "cache-control": "no-store" - }, - "body": { - "error": { - "message": "Source file storage is unavailable.", - "type": "server_error", - "code": "file_storage_unavailable", - "param": null - } - } } } } diff --git a/packages/agents-client/src/skills.test.ts b/packages/agents-client/src/skills.test.ts index c98bf0b60..0d70902e9 100644 --- a/packages/agents-client/src/skills.test.ts +++ b/packages/agents-client/src/skills.test.ts @@ -108,7 +108,6 @@ describe("Skill fixtures at Core d3f55046", () => { it("surfaces Core error envelopes as typed errors", async () => { const { client } = clientFor( fixtures.error_not_found, - fixtures.error_storage_unavailable, fixtures.error_request_too_large, fixtures.error_invalid_upload, fixtures.error_default_version_delete, @@ -116,7 +115,6 @@ describe("Skill fixtures at Core d3f55046", () => { const upload: SkillUploadInput = { kind: "zip", file: new Blob(["zip"]), filename: "report.zip" }; await expect(client.retrieveSkill("skill_missing")).rejects.toMatchObject({ status: 404, code: null }); - await expect(client.listSkills()).rejects.toMatchObject({ status: 503, code: "skill_storage_unavailable" }); await expect(client.uploadSkill(upload)).rejects.toMatchObject({ status: 413, code: "request_too_large" }); await expect(client.uploadSkill(upload)).rejects.toMatchObject({ status: 400, message: "Invalid resource identifier or request limits." }); await expect(client.deleteSkillVersion(skillId, "2")).rejects.toMatchObject({ status: 400, code: "invalid_value", param: "version" }); diff --git a/packages/agents-client/src/source-files-list.test.ts b/packages/agents-client/src/source-files-list.test.ts index 1ad184cad..691d0c25f 100644 --- a/packages/agents-client/src/source-files-list.test.ts +++ b/packages/agents-client/src/source-files-list.test.ts @@ -111,11 +111,6 @@ describe("Files list", () => { }); it("maps Core errors", async () => { - const unavailable = recordingClient(responses.storage_unavailable); - await expect(unavailable.client.listSourceFiles({ limit: 100 })).rejects.toMatchObject({ - status: 503, code: "file_storage_unavailable", errorType: "server_error", - }); - const invalid = recordingClient(responses.list_invalid_purpose); await expect(invalid.client.listSourceFiles()).rejects.toMatchObject({ status: 400, param: "purpose", code: null }); diff --git a/services/core/IMPLEMENTATION.md b/services/core/IMPLEMENTATION.md index 525bbe8c0..0c8ea6039 100644 --- a/services/core/IMPLEMENTATION.md +++ b/services/core/IMPLEMENTATION.md @@ -168,7 +168,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, boolean, 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. +- 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. - 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 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. diff --git a/services/core/cmd/server/main.go b/services/core/cmd/server/main.go index 7bb1c2929..79b0c20e2 100644 --- a/services/core/cmd/server/main.go +++ b/services/core/cmd/server/main.go @@ -36,7 +36,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/databaseurl" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/nativeinstaller" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/providercontract" "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" @@ -136,18 +135,10 @@ func run() error { } defer managedNodes.close() var managed *execution.RuntimeProvider + observationSources := map[string]runtimeobs.SourceResolver{} if managedNodes != nil { managed = managedNodes.runtime - } - observationSources := map[string]runtimeobs.SourceResolver{} - if managedNodes != nil && managedNodes.setup != nil { observationSources[managed.InstallationID] = managedNodes.setup - } else if managed != nil { - source, ok := managed.Provider.(runtimeobs.SourceResolver) - if !ok { - return providercontract.ErrContract - } - observationSources[managed.InstallationID] = source } observationResolver, err := observationstoreresolver.NewResolver(executionStore) if err != nil { diff --git a/services/core/cmd/server/managed_nodes.go b/services/core/cmd/server/managed_nodes.go index 5dfbe10c1..1e3e4fa2b 100644 --- a/services/core/cmd/server/managed_nodes.go +++ b/services/core/cmd/server/managed_nodes.go @@ -46,7 +46,7 @@ func configureManagedNodes(s *store.Store, publicURL string, owner func(context. if err != nil { return nil, err } - if setupID != "" && result.admin == nil { + if result.admin == nil { return nil, errors.New("Web sandbox setup requires OAC_CORE_KEY_DIGESTS_FILE with the Core key digest") } result.hub = node.NewHub(node.HubOptions{ diff --git a/services/core/internal/api/core_errors.go b/services/core/internal/api/core_errors.go index 1994389e9..50f118da4 100644 --- a/services/core/internal/api/core_errors.go +++ b/services/core/internal/api/core_errors.go @@ -33,7 +33,6 @@ type CoreErrorDetail struct{ value any } func CoreErrorString(value string) CoreErrorDetail { return CoreErrorDetail{value} } func CoreErrorNumber(value float64) CoreErrorDetail { return CoreErrorDetail{value} } -func CoreErrorBoolean(value bool) CoreErrorDetail { return CoreErrorDetail{value} } func CoreErrorNull() CoreErrorDetail { return CoreErrorDetail{} } func CoreErrorStrings(values ...string) CoreErrorDetail { return CoreErrorDetail{append([]string{}, values...)} @@ -50,7 +49,7 @@ func validCoreDetails(details CoreErrorDetails) CoreErrorDetails { return nil } switch value := detail.value.(type) { - case nil, string, bool, []string: + case nil, string, []string: case float64: if math.IsNaN(value) || math.IsInf(value, 0) { return nil diff --git a/services/core/internal/api/core_errors_test.go b/services/core/internal/api/core_errors_test.go index 84daa24c9..2cd2f555c 100644 --- a/services/core/internal/api/core_errors_test.go +++ b/services/core/internal/api/core_errors_test.go @@ -77,7 +77,7 @@ func TestCoreDetailsTypesOmissionAndSnapshot(t *testing.T) { values := []string{"docker"} stringsDetail := CoreErrorStrings(values...) values[0] = "private-request-value" - valid := CoreErrorDetails{"label": CoreErrorString("ready"), "number": CoreErrorNumber(1.5), "enabled": CoreErrorBoolean(true), "unset": CoreErrorNull(), "values": stringsDetail, "empty": CoreErrorStrings()} + valid := CoreErrorDetails{"label": CoreErrorString("ready"), "number": CoreErrorNumber(1.5), "unset": CoreErrorNull(), "values": stringsDetail, "empty": CoreErrorStrings()} for _, details := range []CoreErrorDetails{nil, {}, {"bad": CoreErrorNumber(math.NaN())}, {"bad": CoreErrorNumber(math.Inf(1))}, {"bad": CoreErrorDetail{map[string]string{"nested": "private"}}}, {"": CoreErrorString("private")}, valid} { out := httptest.NewRecorder() coreErrorResponses(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { @@ -113,7 +113,7 @@ func TestCoreMarkerPreservesObservationAndStreamingWrappers(t *testing.T) { if err := http.NewResponseController(w).SetWriteDeadline(deadline); err != nil { t.Fatal(err) } - writeCoreError(w, 503, "execution_unavailable", "Unavailable.", CoreErrorDetails{"retryable": CoreErrorBoolean(false)}) + writeCoreError(w, 503, "execution_unavailable", "Unavailable.", CoreErrorDetails{"reason": CoreErrorString("draining")}) if f, ok := w.(http.Flusher); !ok { t.Fatal("Flusher was lost") } else { @@ -128,7 +128,7 @@ func TestCoreMarkerPreservesObservationAndStreamingWrappers(t *testing.T) { handler = responseHeadersWithErrors(coreErrorResponses(handler), observer) } handler.ServeHTTP(out, httptest.NewRequest("GET", "/core/v1/test", nil)) - if !out.Flushed || !out.deadline.Equal(time.Unix(1234, 0)) || out.Header().Get("Openai-Processing-Ms") == "" || !reflect.DeepEqual(observed, []string{"execution_unavailable"}) || !strings.Contains(out.Body.String(), `"details":{"retryable":false}`) { + if !out.Flushed || !out.deadline.Equal(time.Unix(1234, 0)) || out.Header().Get("Openai-Processing-Ms") == "" || !reflect.DeepEqual(observed, []string{"execution_unavailable"}) || !strings.Contains(out.Body.String(), `"details":{"reason":"draining"}`) { t.Fatal(markerOutside, out, observed) } } From dd50d9479237af7766b7961b148ccff58c63eb92 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 30 Sep 2026 15:43:37 +0000 Subject: [PATCH 3/3] Fail strict fakes without Fatalf and simplify the fixture authenticator unexpectedCall now records the failure with t.Errorf and panics. net/http recovers the panic on an httptest server goroutine, where t.Fatalf cannot stop the test, and a direct ServeHTTP call fails loudly. The routing fixture's trapTB overrides Errorf, so a trapped call still reports "handler reached" without failing the test. newTestAuthenticator takes the test and fails it on an invalid fixture key instead of returning an error every caller only passed to t.Fatal. --- services/core/cmd/server/auth_fixture_test.go | 16 ++++++++-------- services/core/cmd/server/http_routes_test.go | 5 +---- services/core/internal/api/fakes_test.go | 6 +++++- services/core/internal/api/routing_test.go | 6 +++--- .../store/agent_execution_defaults_http_test.go | 5 +---- .../internal/store/agents_delete_public_test.go | 5 +---- .../internal/store/agents_update_public_test.go | 5 +---- .../core/internal/store/auth_fixture_test.go | 16 ++++++++-------- .../configuration_validation_public_test.go | 5 +---- .../creation_stream_settlement_public_test.go | 5 +---- .../deployment_model_providers_http_test.go | 17 ++++------------- ...ironment_file_write_semantics_public_test.go | 5 +---- .../store/environment_initial_public_test.go | 5 +---- .../store/environment_mcp_public_test.go | 5 +---- .../store/environment_retrieve_public_test.go | 5 +---- .../file_resource_semantics_public_test.go | 5 +---- .../store/function_images_native_test.go | 5 +---- .../store/function_inputs_public_test.go | 5 +---- .../store/function_public_native_test.go | 5 +---- .../store/function_state_public_test.go | 5 +---- .../internal/store/harness_onboarding_test.go | 5 +---- ...hosted_initialization_failure_public_test.go | 5 +---- .../internal/store/initial_files_http_test.go | 5 +---- .../store/input_conflicts_public_test.go | 5 +---- .../internal/store/list_cursor_public_test.go | 5 +---- .../internal/store/list_query_public_test.go | 5 +---- .../internal/store/mcode_public_native_test.go | 5 +---- .../mcp_credential_selection_public_test.go | 5 +---- .../store/message_images_native_test.go | 5 +---- .../store/model_protocol_native_test.go | 5 +---- .../store/native_public_execution_test.go | 5 +---- .../store/path_id_semantics_public_test.go | 5 +---- services/core/internal/store/remote_mcp_test.go | 5 +---- .../internal/store/request_body_public_test.go | 10 ++-------- .../store/saved_web_search_public_test.go | 5 +---- .../store/self_hosted_cancel_public_test.go | 5 +---- .../store/self_hosted_initial_public_test.go | 7 ++----- .../store/session_agent_filter_public_test.go | 5 +---- .../store/session_artifacts_public_test.go | 5 +---- .../session_deletion_lifecycle_public_test.go | 5 +---- .../store/session_deletion_public_test.go | 5 +---- .../store/session_initial_public_test.go | 5 +---- .../store/session_model_execution_http_test.go | 5 +---- .../session_reference_retry_public_test.go | 5 +---- .../store/skill_selectors_public_test.go | 5 +---- .../store/skill_version_deletion_public_test.go | 5 +---- .../core/internal/store/skills_public_test.go | 5 +---- .../store/source_files_errors_public_test.go | 5 +---- .../store/structured_output_native_test.go | 5 +---- .../store/subagent_visibility_public_test.go | 5 +---- .../store/template_composition_public_test.go | 5 +---- .../template_null_selection_public_test.go | 5 +---- .../internal/store/tool_policy_native_test.go | 5 +---- .../internal/store/tool_search_native_test.go | 5 +---- .../unified_model_configuration_http_test.go | 5 +---- .../store/unstorable_text_public_test.go | 5 +---- .../store/whitespace_input_public_test.go | 10 ++-------- 57 files changed, 83 insertions(+), 250 deletions(-) diff --git a/services/core/cmd/server/auth_fixture_test.go b/services/core/cmd/server/auth_fixture_test.go index 7cf61c11f..ab4deda6c 100644 --- a/services/core/cmd/server/auth_fixture_test.go +++ b/services/core/cmd/server/auth_fixture_test.go @@ -3,7 +3,7 @@ package main import ( "context" "encoding/hex" - "errors" + "testing" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" @@ -22,21 +22,21 @@ func (f fixtureKeyResolver) ResolveProjectAPIKey(_ context.Context, digest strin // newTestAuthenticator binds each key's digest to its Principal, as the Project // store does. -func newTestAuthenticator(keys []testAPIKey) (fixtureKeyResolver, error) { +func newTestAuthenticator(t testing.TB, keys []testAPIKey) fixtureKeyResolver { + t.Helper() resolver := fixtureKeyResolver{} for _, k := range keys { p := identity.Principal{ProjectScope: identity.ProjectScope{TenantID: k.TenantID, OrganizationID: k.OrganizationID, ProjectID: k.ProjectID}, SubjectKind: k.SubjectKind, SubjectID: k.SubjectID} if err := p.Validate(); err != nil { - return nil, err + t.Fatalf("invalid fixture principal: %v", err) } - digest, err := hex.DecodeString(k.TokenSHA256) - if err != nil || len(digest) != 32 { - return nil, errors.New("invalid fixture digest") + if digest, err := hex.DecodeString(k.TokenSHA256); err != nil || len(digest) != 32 { + t.Fatalf("invalid fixture digest %q", k.TokenSHA256) } if _, exists := resolver[k.TokenSHA256]; exists { - return nil, errors.New("duplicate fixture digest") + 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} } - return resolver, nil + return resolver } diff --git a/services/core/cmd/server/http_routes_test.go b/services/core/cmd/server/http_routes_test.go index eb3c9c066..2b61b2a05 100644 --- a/services/core/cmd/server/http_routes_test.go +++ b/services/core/cmd/server/http_routes_test.go @@ -79,11 +79,8 @@ func (p trapProjects) ResolveProjectAPIKey(ctx context.Context, digest string) ( // Every dependency call panics, marking a request that reached a handler. func daemonComposition(t testing.TB) http.Handler { t.Helper() - keys, err := newTestAuthenticator([]testAPIKey{{OrganizationID: "org", ProjectID: "project", SubjectKind: "service_account", + keys := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "org", ProjectID: "project", SubjectKind: "service_account", SubjectID: "runner", TokenSHA256: runtimedevice.HashCredential("project-key"), TenantID: uuid.NewString()}}) - if err != nil { - t.Fatal(err) - } admin, err := api.NewDeploymentAuthenticator([]string{runtimedevice.HashCredential("admin-key")}) if err != nil { t.Fatal(err) diff --git a/services/core/internal/api/fakes_test.go b/services/core/internal/api/fakes_test.go index 55e2bcaa7..cbc8368b1 100644 --- a/services/core/internal/api/fakes_test.go +++ b/services/core/internal/api/fakes_test.go @@ -19,9 +19,13 @@ import ( // Strict fakes: one per Dependencies area, with a func field per method. A // test sets only the funcs it expects; calling any other method fails the test. +// unexpectedCall fails the test and panics. net/http recovers the panic on an +// httptest server goroutine, where t.Fatalf cannot stop the test, and a direct +// ServeHTTP call fails loudly. func unexpectedCall(t testing.TB, method string) { t.Helper() - t.Fatalf("unexpected call to %s", method) + t.Errorf("unexpected call to %s", method) + panic("unexpected call to " + method) } type fakeAdmin struct { diff --git a/services/core/internal/api/routing_test.go b/services/core/internal/api/routing_test.go index bd0d6e9c2..b97b97a62 100644 --- a/services/core/internal/api/routing_test.go +++ b/services/core/internal/api/routing_test.go @@ -69,11 +69,11 @@ func (s *routingStore) UpdateAgent(_ context.Context, tenant, id string, input s return s.agent, nil } -// trapTB turns an unexpected call to a strict fake into a panic, which -// outcome reports as "handler reached". +// trapTB turns an unexpected call to a strict fake into a panic without failing +// the test, which outcome reports as "handler reached". type trapTB struct{ testing.TB } -func (trapTB) Fatalf(format string, args ...any) { panic(fmt.Sprintf(format, args...)) } +func (trapTB) Errorf(format string, args ...any) { panic(fmt.Sprintf(format, args...)) } // routingFixture returns the served handler and, for route enumeration, a // router built from the same Dependencies. They answer only Agent reads and diff --git a/services/core/internal/store/agent_execution_defaults_http_test.go b/services/core/internal/store/agent_execution_defaults_http_test.go index 6e01276d2..089dc8b70 100644 --- a/services/core/internal/store/agent_execution_defaults_http_test.go +++ b/services/core/internal/store/agent_execution_defaults_http_test.go @@ -20,10 +20,7 @@ func TestAgentExecutionDefaultsPublicSnapshotAndPrecedence(t *testing.T) { cipher, _ := credentialcrypto.New(bytes.Repeat([]byte{19}, 32)) st := store.NewWithCredentialCipher(pool, cipher) tenant, token := uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "defaults-test", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}}) - if err != nil { - t.Fatal(err) - } + auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "defaults-test", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}}) deployment := &v1.ModelProviderInput{Protocol: "responses", BaseURL: "https://deployment.example/v1", APIKey: "deployment-canary"} defaultsCalls := 0 handler, err := publicHandler(t, st, auth, "codex", storeExecution(t, st), managedSandboxes(t, st), withHarnesses([]string{"codex", "claude_sdk", "mcode"}), modelProviderDefaults(st, func(context.Context, string) (*store.DeploymentModelProviderSnapshot, error) { diff --git a/services/core/internal/store/agents_delete_public_test.go b/services/core/internal/store/agents_delete_public_test.go index ef1cb96fc..dbfc1cce0 100644 --- a/services/core/internal/store/agents_delete_public_test.go +++ b/services/core/internal/store/agents_delete_public_test.go @@ -20,13 +20,10 @@ func TestAgentDeletionOfficialClient(t *testing.T) { } s, pool := store.NewTestStore(t) token, foreign := uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: uuid.NewString()}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}, }) - if err != nil { - t.Fatal(err) - } h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/agents_update_public_test.go b/services/core/internal/store/agents_update_public_test.go index cea01b4b7..42b790908 100644 --- a/services/core/internal/store/agents_update_public_test.go +++ b/services/core/internal/store/agents_update_public_test.go @@ -20,13 +20,10 @@ func TestAgentUpdateOfficialClient(t *testing.T) { } s, pool := store.NewTestStore(t) token, foreign := uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: uuid.NewString()}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}, }) - if err != nil { - t.Fatal(err) - } h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/auth_fixture_test.go b/services/core/internal/store/auth_fixture_test.go index 37acb66ed..749e1b466 100644 --- a/services/core/internal/store/auth_fixture_test.go +++ b/services/core/internal/store/auth_fixture_test.go @@ -3,7 +3,7 @@ package store_test import ( "context" "encoding/hex" - "errors" + "testing" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/store" @@ -22,21 +22,21 @@ func (f fixtureKeyResolver) ResolveProjectAPIKey(_ context.Context, digest strin // newTestAuthenticator binds each key's digest to its Principal, for // publicHandler. -func newTestAuthenticator(keys []testAPIKey) (fixtureKeyResolver, error) { +func newTestAuthenticator(t testing.TB, keys []testAPIKey) fixtureKeyResolver { + t.Helper() resolver := fixtureKeyResolver{} for _, k := range keys { p := identity.Principal{ProjectScope: identity.ProjectScope{TenantID: k.TenantID, OrganizationID: k.OrganizationID, ProjectID: k.ProjectID}, SubjectKind: k.SubjectKind, SubjectID: k.SubjectID} if err := p.Validate(); err != nil { - return nil, err + t.Fatalf("invalid fixture principal: %v", err) } - digest, err := hex.DecodeString(k.TokenSHA256) - if err != nil || len(digest) != 32 { - return nil, errors.New("invalid fixture digest") + if digest, err := hex.DecodeString(k.TokenSHA256); err != nil || len(digest) != 32 { + t.Fatalf("invalid fixture digest %q", k.TokenSHA256) } if _, exists := resolver[k.TokenSHA256]; exists { - return nil, errors.New("duplicate fixture digest") + 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} } - return resolver, nil + return resolver } diff --git a/services/core/internal/store/configuration_validation_public_test.go b/services/core/internal/store/configuration_validation_public_test.go index 98bf37587..d91df9343 100644 --- a/services/core/internal/store/configuration_validation_public_test.go +++ b/services/core/internal/store/configuration_validation_public_test.go @@ -26,13 +26,10 @@ func TestAgentConfigurationValidationRejectsWithoutWritesPostgres(t *testing.T) } s := store.NewWithCredentialCipher(pool, cipher) owner, foreign, ownerTenant := uuid.NewString(), uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "config-owner", TokenSHA256: runtimedevice.HashCredential(owner), TenantID: ownerTenant}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "config-foreign", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}, }) - if err != nil { - t.Fatal(err) - } h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/creation_stream_settlement_public_test.go b/services/core/internal/store/creation_stream_settlement_public_test.go index 14d1f817b..c77c7afb9 100644 --- a/services/core/internal/store/creation_stream_settlement_public_test.go +++ b/services/core/internal/store/creation_stream_settlement_public_test.go @@ -115,10 +115,7 @@ func (s sseLines) open(t *testing.T) { func TestCreationStreamPublicLifetimes(t *testing.T) { s, pool := store.NewModelTestStore(t) tenant, token := uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{{OrganizationID: "test-org", ProjectID: tenant, SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}}) - if err != nil { - t.Fatal(err) - } + auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: tenant, SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}}) handler, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), executorURL("https://offline-executor.example")) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/deployment_model_providers_http_test.go b/services/core/internal/store/deployment_model_providers_http_test.go index 266a5c38f..0332f795b 100644 --- a/services/core/internal/store/deployment_model_providers_http_test.go +++ b/services/core/internal/store/deployment_model_providers_http_test.go @@ -31,10 +31,7 @@ func TestDeploymentModelProvidersHTTP(t *testing.T) { cipher, _ := credentialcrypto.New(bytes.Repeat([]byte{53}, 32)) st := store.NewWithCredentialCipher(pool, cipher) tenant, projectKey, coreKey := uuid.NewString(), uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "defaults-http", TokenSHA256: runtimedevice.HashCredential(projectKey), TenantID: tenant}}) - if err != nil { - t.Fatal(err) - } + auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "defaults-http", TokenSHA256: runtimedevice.HashCredential(projectKey), TenantID: tenant}}) admin, err := api.NewDeploymentAuthenticator([]string{runtimedevice.HashCredential(coreKey)}) if err != nil { t.Fatal(err) @@ -291,10 +288,7 @@ func TestNoneSessionRetryAfterDeploymentDefaultChanges(t *testing.T) { t.Fatal(err) } tenant, token := uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "none-retry", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}}) - if err != nil { - t.Fatal(err) - } + auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "none-retry", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}}) handler, err := publicHandler(t, st, auth, "codex", storeExecution(t, st)) if err != nil { t.Fatal(err) @@ -357,13 +351,10 @@ func TestNoneSessionRetryAfterDeploymentDefaultChanges(t *testing.T) { func TestDeploymentProviderResolutionPairsRevisionDuringReplacement(t *testing.T) { st, pool := store.NewManagedTestStore(t) tenant, token := uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "tuple-test", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}}) - if err != nil { - t.Fatal(err) - } + auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "tuple-test", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}}) admin := adminaudit.WithSource(t.Context(), adminaudit.Source{CredentialID: "fixture-admin", RequestID: uuid.NewString(), TraceID: uuid.NewString()}) provider := v1.ModelProviderInput{Protocol: "responses", BaseURL: "https://original.example/v1", APIKey: "original-fixture-key"} - if _, err = st.SetDeploymentModelProvider(admin, "codex", v1.ModelConfigurationInput{ModelProvider: provider, Model: "fixture"}); err != nil { + if _, err := st.SetDeploymentModelProvider(admin, "codex", v1.ModelConfigurationInput{ModelProvider: provider, Model: "fixture"}); err != nil { t.Fatal(err) } original, err := st.DeploymentModelProvider(t.Context(), "codex") diff --git a/services/core/internal/store/environment_file_write_semantics_public_test.go b/services/core/internal/store/environment_file_write_semantics_public_test.go index 78697d6e4..aa111057a 100644 --- a/services/core/internal/store/environment_file_write_semantics_public_test.go +++ b/services/core/internal/store/environment_file_write_semantics_public_test.go @@ -55,13 +55,10 @@ func TestEnvironmentFileCreateRejectionsLeaveNoReceiptOrConsumption(t *testing.T t.Fatal(err) } token, other := uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: h.tenant}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "tenant-b", TokenSHA256: runtimedevice.HashCredential(other), TenantID: uuid.NewString()}, }) - if err != nil { - t.Fatal(err) - } handler, err := publicHandler(t, h.s, auth, "codex", workerExecution(w)) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/environment_initial_public_test.go b/services/core/internal/store/environment_initial_public_test.go index a9aa49c4c..249af4af6 100644 --- a/services/core/internal/store/environment_initial_public_test.go +++ b/services/core/internal/store/environment_initial_public_test.go @@ -23,13 +23,10 @@ func TestEnvironmentInitialFailureOfficialClient(t *testing.T) { } s, pool := store.NewTestStore(t) tenant, token, foreign := uuid.NewString(), uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test-org", ProjectID: tenant, SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}, {OrganizationID: "test-org", ProjectID: "other-project", SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}, }) - if err != nil { - t.Fatal(err) - } // Private setup isolates the persistence prerequisite from public creation admission. configuration := json.RawMessage(`{"agent":{"id":"agent_initial_failure","model":"fixture","tools":[],"multi_agent":{"enabled":false,"max_concurrent_subagents":null},"reasoning":{},"service_tier":"auto","text":{"format":{"type":"text"},"verbosity":"medium"}},"environment":{"type":"self_hosted","workspace_directory":"/workspace"}}`) session, err := s.CreateSession(t.Context(), tenant, store.CreateSessionInput{ diff --git a/services/core/internal/store/environment_mcp_public_test.go b/services/core/internal/store/environment_mcp_public_test.go index 2353ad1b7..ba21a2aaa 100644 --- a/services/core/internal/store/environment_mcp_public_test.go +++ b/services/core/internal/store/environment_mcp_public_test.go @@ -16,10 +16,7 @@ func TestPublicEnvironmentMCPUsesAttachedVaultSelection(t *testing.T) { for _, kind := range []string{"codex", "claude_sdk", "mcode"} { t.Run(kind, func(t *testing.T) { s, pool, tenant, vault, credential := selfHostedMCPAdmissionFixture(t) - auth, err := newTestAuthenticator([]testAPIKey{{OrganizationID: "test-org", ProjectID: tenant, SubjectKind: "service_account", SubjectID: "test", TenantID: tenant, TokenSHA256: runtimedevice.HashCredential("test-token")}}) - if err != nil { - t.Fatal(err) - } + auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: tenant, SubjectKind: "service_account", SubjectID: "test", TenantID: tenant, TokenSHA256: runtimedevice.HashCredential("test-token")}}) handler, err := publicHandler(t, s, auth, kind, workerExecution(&execution.Worker{}), executorURL("https://executor.example")) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/environment_retrieve_public_test.go b/services/core/internal/store/environment_retrieve_public_test.go index fd55c908f..892a2038f 100644 --- a/services/core/internal/store/environment_retrieve_public_test.go +++ b/services/core/internal/store/environment_retrieve_public_test.go @@ -26,14 +26,11 @@ func TestEnvironmentRetrievalOfficialClient(t *testing.T) { tenant, foreignTenant := uuid.NewString(), uuid.NewString() principal := store.FixtureExecutorPrincipal(t, s, tenant) token, peer, foreign := uuid.NewString(), uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: principal.OrganizationID, ProjectID: tenant, SubjectKind: principal.SubjectKind, SubjectID: principal.SubjectID, TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}, {OrganizationID: principal.OrganizationID, ProjectID: tenant, SubjectKind: principal.SubjectKind, SubjectID: principal.SubjectID, TokenSHA256: runtimedevice.HashCredential(peer), TenantID: tenant}, {OrganizationID: principal.OrganizationID, ProjectID: foreignTenant, SubjectKind: principal.SubjectKind, SubjectID: principal.SubjectID, TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: foreignTenant}, }) - if err != nil { - t.Fatal(err) - } if err := s.EnsureProjectScopes(t.Context(), []identity.ProjectScope{{TenantID: tenant, OrganizationID: principal.OrganizationID, ProjectID: tenant}, {TenantID: foreignTenant, OrganizationID: principal.OrganizationID, ProjectID: foreignTenant}}); err != nil { t.Fatal(err) } diff --git a/services/core/internal/store/file_resource_semantics_public_test.go b/services/core/internal/store/file_resource_semantics_public_test.go index a30509091..9a7c4bf75 100644 --- a/services/core/internal/store/file_resource_semantics_public_test.go +++ b/services/core/internal/store/file_resource_semantics_public_test.go @@ -27,13 +27,10 @@ func TestFileResourceSemanticsOfficialClientPostgres(t *testing.T) { t.Fatal(err) } token, foreign := uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "resources-owner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: uuid.NewString()}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "resources-foreign", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}, }) - if err != nil { - t.Fatal(err) - } newServer := func() *httptest.Server { t.Helper() s := store.NewWithCredentialCipher(pool, cipher) diff --git a/services/core/internal/store/function_images_native_test.go b/services/core/internal/store/function_images_native_test.go index a1cbc18cd..a9f9b32d7 100644 --- a/services/core/internal/store/function_images_native_test.go +++ b/services/core/internal/store/function_images_native_test.go @@ -47,13 +47,10 @@ func TestNativeFunctionImagePublicExecution(t *testing.T) { } }() token, foreign := uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test", ProjectID: h.tenant, SubjectKind: "service_account", SubjectID: "owner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: h.tenant}, {OrganizationID: "test", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "other", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}, }) - if err != nil { - t.Fatal(err) - } handler, err := publicHandler(t, h.s, auth, kind, workerExecution(worker), withPolicy(h.d.Policy), nativeDeploymentDefaults(h.s, model, provider)) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/function_inputs_public_test.go b/services/core/internal/store/function_inputs_public_test.go index abfb2685f..df973ba0a 100644 --- a/services/core/internal/store/function_inputs_public_test.go +++ b/services/core/internal/store/function_inputs_public_test.go @@ -45,10 +45,7 @@ func TestFunctionInputsOfficialClientAtomicAdmission(t *testing.T) { t.Fatal(err) } } - auth, err := newTestAuthenticator([]testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}}) - if err != nil { - t.Fatal(err) - } + auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}}) handler, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/function_public_native_test.go b/services/core/internal/store/function_public_native_test.go index 4d29aa55b..41b7523fc 100644 --- a/services/core/internal/store/function_public_native_test.go +++ b/services/core/internal/store/function_public_native_test.go @@ -78,10 +78,7 @@ func nativePublicFunctionServer(t *testing.T, h *dispatchHarness, ctx context.Co } }) token := uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: h.tenant}}) - if err != nil { - t.Fatal(err) - } + auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: h.tenant}}) handler, err := publicHandler(t, h.s, auth, "codex", workerExecution(worker), nativeDeploymentDefaults(h.s, "gpt-5.5", provider)) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/function_state_public_test.go b/services/core/internal/store/function_state_public_test.go index d98748895..5e63aa8ba 100644 --- a/services/core/internal/store/function_state_public_test.go +++ b/services/core/internal/store/function_state_public_test.go @@ -44,10 +44,7 @@ func TestFunctionStateOfficialClientReadsAndLiveEvents(t *testing.T) { } } record("first") - auth, err := newTestAuthenticator([]testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}}) - if err != nil { - t.Fatal(err) - } + auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}}) handler, err := publicHandler(t, s, auth, "codex") if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/harness_onboarding_test.go b/services/core/internal/store/harness_onboarding_test.go index 96e39cd37..209944f64 100644 --- a/services/core/internal/store/harness_onboarding_test.go +++ b/services/core/internal/store/harness_onboarding_test.go @@ -64,10 +64,7 @@ func TestThirdHarnessPublicOnboarding(t *testing.T) { } }() token := uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{{OrganizationID: "test-org", ProjectID: h.tenant, SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: h.tenant}}) - if err != nil { - t.Fatal(err) - } + auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: h.tenant, SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: h.tenant}}) handler, err := publicHandler(t, h.s, auth, "fixture_harness", workerExecution(worker), withPolicy(policy)) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/hosted_initialization_failure_public_test.go b/services/core/internal/store/hosted_initialization_failure_public_test.go index 259b9b0bb..bad7d2821 100644 --- a/services/core/internal/store/hosted_initialization_failure_public_test.go +++ b/services/core/internal/store/hosted_initialization_failure_public_test.go @@ -292,13 +292,10 @@ func TestHostedInitializationFailurePublicHTTP(t *testing.T) { slog.SetDefault(slog.New(slog.NewTextHandler(logs, &slog.HandlerOptions{Level: slog.LevelDebug}))) t.Cleanup(func() { slog.SetDefault(previous) }) w, _ := managedWorkerMode(t, s, key, p, false, true) - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test-org", ProjectID: tenant, SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "tenant-b", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}, }) - if err != nil { - t.Fatal(err) - } handler, err := publicHandler(t, s, auth, "codex", workerExecution(w)) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/initial_files_http_test.go b/services/core/internal/store/initial_files_http_test.go index 7976009a8..d70d54ace 100644 --- a/services/core/internal/store/initial_files_http_test.go +++ b/services/core/internal/store/initial_files_http_test.go @@ -22,10 +22,7 @@ func TestInitialFilesHTTPInlineLimitsAndRetry(t *testing.T) { } s := store.NewWithCredentialCipher(pool, cipher) tenant, token := uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}}) - if err != nil { - t.Fatal(err) - } + auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}}) // Exercise HTTP parsing and durable storage without starting a Runtime. handler, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), managedSandboxes(t, s), fixtureDeploymentProvider(s)) if err != nil { diff --git a/services/core/internal/store/input_conflicts_public_test.go b/services/core/internal/store/input_conflicts_public_test.go index 243dfc817..de54583e8 100644 --- a/services/core/internal/store/input_conflicts_public_test.go +++ b/services/core/internal/store/input_conflicts_public_test.go @@ -40,13 +40,10 @@ func TestSessionInputConflictsAndResultTargetsPostgres(t *testing.T) { s, pool := store.NewManagedTestStore(t) ctx := t.Context() tenant, owner, foreign := uuid.NewString(), uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test-org", ProjectID: tenant, SubjectKind: "service_account", SubjectID: "conflict-owner", TokenSHA256: runtimedevice.HashCredential(owner), TenantID: tenant}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "conflict-foreign", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}, }) - if err != nil { - t.Fatal(err) - } h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), executorURL("https://executor.example")) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/list_cursor_public_test.go b/services/core/internal/store/list_cursor_public_test.go index fb5a38027..6a005701e 100644 --- a/services/core/internal/store/list_cursor_public_test.go +++ b/services/core/internal/store/list_cursor_public_test.go @@ -224,13 +224,10 @@ func TestListCursorErrorsPostgres(t *testing.T) { s := store.NewWithCredentialCipher(pool, cipher) owner, foreign := uuid.NewString(), uuid.NewString() ownerTenant, foreignTenant := uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "cursor-owner", TokenSHA256: runtimedevice.HashCredential(owner), TenantID: ownerTenant}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "cursor-foreign", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: foreignTenant}, }) - if err != nil { - t.Fatal(err) - } h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/list_query_public_test.go b/services/core/internal/store/list_query_public_test.go index 515a32005..e8faa2df6 100644 --- a/services/core/internal/store/list_query_public_test.go +++ b/services/core/internal/store/list_query_public_test.go @@ -28,13 +28,10 @@ func TestListQueryOfficialClientPostgres(t *testing.T) { } s := store.NewWithCredentialCipher(pool, cipher) token, foreign := uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "query-owner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: uuid.NewString()}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "query-foreign", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}, }) - if err != nil { - t.Fatal(err) - } // Use real admission while leaving dispatch paused. Public cancellation retains // the queued history; this fixture does not perform native or model execution. worker, err := execution.StartWorker(t.Context(), &execution.Dispatcher{Store: s}) diff --git a/services/core/internal/store/mcode_public_native_test.go b/services/core/internal/store/mcode_public_native_test.go index c160088cd..51e1c9c74 100644 --- a/services/core/internal/store/mcode_public_native_test.go +++ b/services/core/internal/store/mcode_public_native_test.go @@ -46,13 +46,10 @@ func TestNativeMCodePublicExecution(t *testing.T) { } }() token, foreign := uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test", ProjectID: h.tenant, SubjectKind: "service_account", SubjectID: "owner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: h.tenant}, {OrganizationID: "test", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "other", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}, }) - if err != nil { - t.Fatal(err) - } handler, err := publicHandler(t, h.s, auth, "mcode", workerExecution(worker), acceptUnavailable(t), nativeDeploymentDefaults(h.s, model, provider)) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/mcp_credential_selection_public_test.go b/services/core/internal/store/mcp_credential_selection_public_test.go index cb4351a67..62629755f 100644 --- a/services/core/internal/store/mcp_credential_selection_public_test.go +++ b/services/core/internal/store/mcp_credential_selection_public_test.go @@ -32,13 +32,10 @@ func TestMCPCredentialSelectionPublicPostgres(t *testing.T) { } s := store.NewWithCredentialCipher(pool, cipher) tenantA, tokenA, tokenB := uuid.NewString(), uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "selection-a", TokenSHA256: runtimedevice.HashCredential(tokenA), TenantID: tenantA}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "selection-b", TokenSHA256: runtimedevice.HashCredential(tokenB), TenantID: uuid.NewString()}, }) - if err != nil { - t.Fatal(err) - } h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/message_images_native_test.go b/services/core/internal/store/message_images_native_test.go index 0b8082c76..31a0862d3 100644 --- a/services/core/internal/store/message_images_native_test.go +++ b/services/core/internal/store/message_images_native_test.go @@ -47,13 +47,10 @@ func TestNativeMessageImagePublicExecution(t *testing.T) { } }() token, foreign := uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test", ProjectID: h.tenant, SubjectKind: "service_account", SubjectID: "owner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: h.tenant}, {OrganizationID: "test", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "other", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}, }) - if err != nil { - t.Fatal(err) - } handler, err := publicHandler(t, h.s, auth, kind, workerExecution(worker), withPolicy(h.d.Policy), nativeDeploymentDefaults(h.s, model, provider)) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/model_protocol_native_test.go b/services/core/internal/store/model_protocol_native_test.go index f820b2cfb..4172e0779 100644 --- a/services/core/internal/store/model_protocol_native_test.go +++ b/services/core/internal/store/model_protocol_native_test.go @@ -77,10 +77,7 @@ func TestNativeModelProtocolPublicExecution(t *testing.T) { } }() token := uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{{OrganizationID: "test", ProjectID: h.tenant, SubjectKind: "service_account", SubjectID: "owner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: h.tenant}}) - if err != nil { - t.Fatal("cannot create fixture authenticator") - } + auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test", ProjectID: h.tenant, SubjectKind: "service_account", SubjectID: "owner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: h.tenant}}) providerRevision := uuid.New() handler, err := publicHandler(t, h.s, auth, options.Engine, workerExecution(worker), withPolicy(h.d.Policy), modelProviderDefaults(h.s, func(context.Context, string) (*store.DeploymentModelProviderSnapshot, error) { return &store.DeploymentModelProviderSnapshot{Model: options.Model, HarnessConfig: options.HarnessConfig, Provider: &options.Provider, Revision: providerRevision}, nil diff --git a/services/core/internal/store/native_public_execution_test.go b/services/core/internal/store/native_public_execution_test.go index 43420f2be..de2aaf958 100644 --- a/services/core/internal/store/native_public_execution_test.go +++ b/services/core/internal/store/native_public_execution_test.go @@ -39,10 +39,7 @@ func verifyNativePublicExecution(t *testing.T, h *dispatchHarness, parent contex } }() token, foreign := uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: h.tenant}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}}) - if err != nil { - t.Fatal(err) - } + auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: h.tenant}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}}) handler, err := publicHandler(t, h.s, auth, "codex", workerExecution(worker), nativeDeploymentDefaults(h.s, "gpt-5.5", provider)) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/path_id_semantics_public_test.go b/services/core/internal/store/path_id_semantics_public_test.go index a57a25391..bb6712156 100644 --- a/services/core/internal/store/path_id_semantics_public_test.go +++ b/services/core/internal/store/path_id_semantics_public_test.go @@ -93,13 +93,10 @@ func TestMalformedPathIDsMatchMissingPostgres(t *testing.T) { s := store.NewWithCredentialCipher(pool, cipher) owner, foreign := uuid.NewString(), uuid.NewString() ownerTenant := uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "path-owner", TokenSHA256: runtimedevice.HashCredential(owner), TenantID: ownerTenant}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "path-foreign", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}, }) - if err != nil { - t.Fatal(err) - } h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/remote_mcp_test.go b/services/core/internal/store/remote_mcp_test.go index 1dcf2287a..0f543ffd8 100644 --- a/services/core/internal/store/remote_mcp_test.go +++ b/services/core/internal/store/remote_mcp_test.go @@ -94,10 +94,7 @@ func selfHostedMCPAdmissionFixture(t *testing.T) (*store.Store, *pgxpool.Pool, s func selfHostedMCPAdmissionHandler(t *testing.T, s *store.Store, tenant string) http.Handler { t.Helper() - auth, err := newTestAuthenticator([]testAPIKey{{OrganizationID: "test-org", ProjectID: tenant, SubjectKind: "service_account", SubjectID: "test", TenantID: tenant, TokenSHA256: runtimedevice.HashCredential("test-token")}}) - if err != nil { - t.Fatal(err) - } + auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: tenant, SubjectKind: "service_account", SubjectID: "test", TenantID: tenant, TokenSHA256: runtimedevice.HashCredential("test-token")}}) handler, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), executorURL("https://executor.example")) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/request_body_public_test.go b/services/core/internal/store/request_body_public_test.go index 478ef5005..6ce2663f6 100644 --- a/services/core/internal/store/request_body_public_test.go +++ b/services/core/internal/store/request_body_public_test.go @@ -31,13 +31,10 @@ func TestRequestBodyGateRejectsWithoutWritesPostgres(t *testing.T) { } s := store.NewWithCredentialCipher(pool, cipher) owner, foreign, ownerTenant := uuid.NewString(), uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "body-owner", TokenSHA256: runtimedevice.HashCredential(owner), TenantID: ownerTenant}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "body-foreign", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}, }) - if err != nil { - t.Fatal(err) - } // No Runtime is connected, so a file write that passes the gate is unavailable. unavailable := func(d *api.Dependencies) { d.Execution.Workspaces = unavailableWorkspaces{strictStandIn{t}} } h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), unavailable, acceptUnavailable(t)) @@ -175,10 +172,7 @@ func TestRequestBodyGateExcludedRoutesPostgres(t *testing.T) { } s := store.NewWithCredentialCipher(pool, cipher) token, tenant := uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "excluded-owner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}}) - if err != nil { - t.Fatal(err) - } + auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "excluded-owner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}}) h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/saved_web_search_public_test.go b/services/core/internal/store/saved_web_search_public_test.go index 0e5798492..368cae540 100644 --- a/services/core/internal/store/saved_web_search_public_test.go +++ b/services/core/internal/store/saved_web_search_public_test.go @@ -21,13 +21,10 @@ func TestSavedWebSearchPostgres(t *testing.T) { // An isolated database keeps the no-write digest independent of other tests. s, pool := store.NewManagedTestStore(t) owner, foreign, ownerTenant := uuid.NewString(), uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "search-owner", TokenSHA256: runtimedevice.HashCredential(owner), TenantID: ownerTenant}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "search-foreign", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}, }) - if err != nil { - t.Fatal(err) - } h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/self_hosted_cancel_public_test.go b/services/core/internal/store/self_hosted_cancel_public_test.go index 359cfebf2..46882ad5d 100644 --- a/services/core/internal/store/self_hosted_cancel_public_test.go +++ b/services/core/internal/store/self_hosted_cancel_public_test.go @@ -24,13 +24,10 @@ func TestSelfHostedCancellationOfficialClient(t *testing.T) { s, pool := store.NewModelTestStore(t) tenant, foreignTenant := uuid.NewString(), uuid.NewString() token, foreign := uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test-org", ProjectID: tenant, SubjectKind: "service_account", SubjectID: "cancel-caller", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}, {OrganizationID: "test-org", ProjectID: foreignTenant, SubjectKind: "service_account", SubjectID: "cancel-caller", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: foreignTenant}, }) - if err != nil { - t.Fatal(err) - } serve := func() (*httptest.Server, func(bool)) { t.Helper() worker, stop := publicInitialWorker(t, s) diff --git a/services/core/internal/store/self_hosted_initial_public_test.go b/services/core/internal/store/self_hosted_initial_public_test.go index 6f5fc030a..896549d70 100644 --- a/services/core/internal/store/self_hosted_initial_public_test.go +++ b/services/core/internal/store/self_hosted_initial_public_test.go @@ -32,14 +32,11 @@ func TestSelfHostedInitialCreationOfficialClient(t *testing.T) { s, pool := store.NewModelTestStore(t) tenant, foreignTenant := uuid.NewString(), uuid.NewString() token, peer, foreign := uuid.NewString(), uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test-org", ProjectID: tenant, SubjectKind: "service_account", SubjectID: "initial-creator", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}, {OrganizationID: "test-org", ProjectID: tenant, SubjectKind: "user", SubjectID: "different-creator", TokenSHA256: runtimedevice.HashCredential(peer), TenantID: tenant}, {OrganizationID: "test-org", ProjectID: foreignTenant, SubjectKind: "service_account", SubjectID: "initial-creator", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: foreignTenant}, }) - if err != nil { - t.Fatal(err) - } const origin = "https://offline-executor.example" serve := func(s *store.Store, worker *execution.Worker) *httptest.Server { t.Helper() @@ -186,7 +183,7 @@ func TestSelfHostedInitialCreationOfficialClient(t *testing.T) { } } var pid uint32 - err = reopenedPool.QueryRow(t.Context(), `SELECT pid FROM pg_locks WHERE locktype='advisory' + err := reopenedPool.QueryRow(t.Context(), `SELECT pid FROM pg_locks WHERE locktype='advisory' AND database=(SELECT oid FROM pg_database WHERE datname=current_database()) AND classid=(706172736172::bigint >> 32)::oid AND objid=(706172736172::bigint & 4294967295)::oid AND objsubid=1 AND granted`).Scan(&pid) diff --git a/services/core/internal/store/session_agent_filter_public_test.go b/services/core/internal/store/session_agent_filter_public_test.go index 704e65ba0..3e51233f5 100644 --- a/services/core/internal/store/session_agent_filter_public_test.go +++ b/services/core/internal/store/session_agent_filter_public_test.go @@ -20,13 +20,10 @@ func TestSessionAgentFilterOfficialClient(t *testing.T) { } s, pool := store.NewTestStore(t) token, foreign := uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: uuid.NewString()}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}, }) - if err != nil { - t.Fatal(err) - } h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/session_artifacts_public_test.go b/services/core/internal/store/session_artifacts_public_test.go index 44a840615..7a7cbf9cb 100644 --- a/services/core/internal/store/session_artifacts_public_test.go +++ b/services/core/internal/store/session_artifacts_public_test.go @@ -72,13 +72,10 @@ func artifactHTTPServer(t *testing.T, s *store.Store) (server *httptest.Server, t.Helper() owner, foreign = uuid.NewString(), uuid.NewString() ownerTenant, foreignTenant = uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "artifact-owner", TokenSHA256: runtimedevice.HashCredential(owner), TenantID: ownerTenant}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "artifact-foreign", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: foreignTenant}, }) - if err != nil { - t.Fatal(err) - } h, err := publicHandler(t, s, auth, "codex") if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/session_deletion_lifecycle_public_test.go b/services/core/internal/store/session_deletion_lifecycle_public_test.go index 2ba0263e0..75e15b00c 100644 --- a/services/core/internal/store/session_deletion_lifecycle_public_test.go +++ b/services/core/internal/store/session_deletion_lifecycle_public_test.go @@ -25,13 +25,10 @@ func TestSessionDeletionLifecyclePostgres(t *testing.T) { s, pool := store.NewManagedTestStore(t) ctx := t.Context() tenant, owner, foreign := uuid.NewString(), uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test-org", ProjectID: tenant, SubjectKind: "service_account", SubjectID: "deletion-owner", TokenSHA256: runtimedevice.HashCredential(owner), TenantID: tenant}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "deletion-foreign", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}, }) - if err != nil { - t.Fatal(err) - } h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), executorURL("https://executor.example")) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/session_deletion_public_test.go b/services/core/internal/store/session_deletion_public_test.go index ee55b5750..fcae8aead 100644 --- a/services/core/internal/store/session_deletion_public_test.go +++ b/services/core/internal/store/session_deletion_public_test.go @@ -20,13 +20,10 @@ func TestSessionDeletionOfficialClient(t *testing.T) { } s, pool := store.NewTestStore(t) token, foreign := uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: uuid.NewString()}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}, }) - if err != nil { - t.Fatal(err) - } h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/session_initial_public_test.go b/services/core/internal/store/session_initial_public_test.go index 7bd4a6ba1..85f4a973a 100644 --- a/services/core/internal/store/session_initial_public_test.go +++ b/services/core/internal/store/session_initial_public_test.go @@ -20,10 +20,7 @@ func TestInitialSessionInputOfficialClient(t *testing.T) { } s, _ := store.NewTestStore(t) token, foreign := uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: uuid.NewString()}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}}) - if err != nil { - t.Fatal(err) - } + auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: uuid.NewString()}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}}) // Exercise real worker admission with dispatch paused for deterministic reads. worker, err := execution.StartWorker(t.Context(), &execution.Dispatcher{Store: s}) if err != nil { diff --git a/services/core/internal/store/session_model_execution_http_test.go b/services/core/internal/store/session_model_execution_http_test.go index 833f95043..c5342f3ed 100644 --- a/services/core/internal/store/session_model_execution_http_test.go +++ b/services/core/internal/store/session_model_execution_http_test.go @@ -18,10 +18,7 @@ func TestModelExecutionHTTPWriteOnlyAndStrictAdmission(t *testing.T) { cipher, _ := credentialcrypto.New(bytes.Repeat([]byte{6}, 32)) st := store.NewWithCredentialCipher(pool, cipher) tenant, token := uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "catalog-test", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}}) - if err != nil { - t.Fatal(err) - } + auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "catalog-test", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}}) handler, err := publicHandler(t, st, auth, "codex", storeExecution(t, st), managedSandboxes(t, st)) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/session_reference_retry_public_test.go b/services/core/internal/store/session_reference_retry_public_test.go index 2b509a688..0e245ced6 100644 --- a/services/core/internal/store/session_reference_retry_public_test.go +++ b/services/core/internal/store/session_reference_retry_public_test.go @@ -23,10 +23,7 @@ func TestSavedReferenceRetryOfficialClient(t *testing.T) { } s, pool := store.NewTestStore(t) tenant, token, foreign := uuid.NewString(), uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}}) - if err != nil { - t.Fatal(err) - } + auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}}) worker, err := execution.StartWorker(t.Context(), &execution.Dispatcher{Store: s}) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/skill_selectors_public_test.go b/services/core/internal/store/skill_selectors_public_test.go index 2aee8c5ee..2a8c1ad35 100644 --- a/services/core/internal/store/skill_selectors_public_test.go +++ b/services/core/internal/store/skill_selectors_public_test.go @@ -28,13 +28,10 @@ func TestSkillSelectorsOfficialClientPostgres(t *testing.T) { } s := store.NewWithCredentialCipher(pool, cipher) token, foreign := uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: uuid.NewString()}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}, }) - if err != nil { - t.Fatal(err) - } h, err := publicHandler(t, s, auth, "codex") if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/skill_version_deletion_public_test.go b/services/core/internal/store/skill_version_deletion_public_test.go index 87246a6c4..3d05c650c 100644 --- a/services/core/internal/store/skill_version_deletion_public_test.go +++ b/services/core/internal/store/skill_version_deletion_public_test.go @@ -27,13 +27,10 @@ func TestSkillVersionDeletionHTTPPostgres(t *testing.T) { } s := store.NewWithCredentialCipher(pool, cipher) owner, foreign, ownerTenant := uuid.NewString(), uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "skill-owner", TokenSHA256: runtimedevice.HashCredential(owner), TenantID: ownerTenant}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "skill-foreign", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}, }) - if err != nil { - t.Fatal(err) - } h, err := publicHandler(t, s, auth, "codex") if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/skills_public_test.go b/services/core/internal/store/skills_public_test.go index 099ff3887..3b42c7a01 100644 --- a/services/core/internal/store/skills_public_test.go +++ b/services/core/internal/store/skills_public_test.go @@ -28,13 +28,10 @@ func TestSkillsOfficialClientPostgres(t *testing.T) { } s := store.NewWithCredentialCipher(pool, cipher) token, foreign := uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: uuid.NewString()}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}, }) - if err != nil { - t.Fatal(err) - } h, err := publicHandler(t, s, auth, "codex") if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/source_files_errors_public_test.go b/services/core/internal/store/source_files_errors_public_test.go index a73c7e5df..51ae30054 100644 --- a/services/core/internal/store/source_files_errors_public_test.go +++ b/services/core/internal/store/source_files_errors_public_test.go @@ -20,13 +20,10 @@ func TestSourceFileErrorsOfficialClientPostgres(t *testing.T) { } s, _ := store.NewTestStore(t) token, foreign := uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "files-owner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: uuid.NewString()}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "files-foreign", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}, }) - if err != nil { - t.Fatal(err) - } h, err := publicHandler(t, s, auth, "codex") if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/structured_output_native_test.go b/services/core/internal/store/structured_output_native_test.go index 2fe9f85bf..c9a313fae 100644 --- a/services/core/internal/store/structured_output_native_test.go +++ b/services/core/internal/store/structured_output_native_test.go @@ -43,13 +43,10 @@ func TestNativeStructuredOutputPublicExecution(t *testing.T) { } }() token, foreign := uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test", ProjectID: h.tenant, SubjectKind: "service_account", SubjectID: "owner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: h.tenant}, {OrganizationID: "test", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "other", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}, }) - if err != nil { - t.Fatal(err) - } handler, err := publicHandler(t, h.s, auth, "claude_sdk", workerExecution(worker), nativeDeploymentDefaults(h.s, model, provider)) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/subagent_visibility_public_test.go b/services/core/internal/store/subagent_visibility_public_test.go index 8db73bbba..ae2d33ae2 100644 --- a/services/core/internal/store/subagent_visibility_public_test.go +++ b/services/core/internal/store/subagent_visibility_public_test.go @@ -71,13 +71,10 @@ func subagentFixture(kind string, value any) store.ExecutionEvent { func TestSubagentVisibilityPublic(t *testing.T) { s, _ := store.NewTestStore(t) tenant, token, foreign := uuid.NewString(), uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test-org", ProjectID: tenant, SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "foreign", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}, }) - if err != nil { - t.Fatal(err) - } handler, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/template_composition_public_test.go b/services/core/internal/store/template_composition_public_test.go index a6a7f1ec4..1d57d2e86 100644 --- a/services/core/internal/store/template_composition_public_test.go +++ b/services/core/internal/store/template_composition_public_test.go @@ -32,13 +32,10 @@ func TestTemplateCompositionOfficialClientPostgres(t *testing.T) { s := store.NewWithCredentialCipher(pool, cipher) reopenedStore := store.NewWithCredentialCipher(pool, cipher) tenant, foreignTenant, token, foreign := uuid.NewString(), uuid.NewString(), uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "composition-owner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "composition-foreign", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: foreignTenant}, }) - if err != nil { - t.Fatal(err) - } serve := func(current *store.Store) *httptest.Server { t.Helper() // Hosted admission and freezing use the real Store; no Runtime or model runs. diff --git a/services/core/internal/store/template_null_selection_public_test.go b/services/core/internal/store/template_null_selection_public_test.go index fc9edca32..f53c7f928 100644 --- a/services/core/internal/store/template_null_selection_public_test.go +++ b/services/core/internal/store/template_null_selection_public_test.go @@ -34,13 +34,10 @@ func TestTemplateNullSelectionOfficialClientPostgres(t *testing.T) { s := store.NewWithCredentialCipher(pool, cipher) reopenedStore := store.NewWithCredentialCipher(pool, cipher) tenant, foreignTenant, token, foreign := uuid.NewString(), uuid.NewString(), uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "selection-owner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "selection-foreign", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: foreignTenant}, }) - if err != nil { - t.Fatal(err) - } serve := func(current *store.Store) *httptest.Server { t.Helper() h, err := publicHandler(t, current, auth, "codex", storeExecution(t, current), managedSandboxes(t, current), fixtureDeploymentProvider(current)) diff --git a/services/core/internal/store/tool_policy_native_test.go b/services/core/internal/store/tool_policy_native_test.go index f47dcabd0..141612ac7 100644 --- a/services/core/internal/store/tool_policy_native_test.go +++ b/services/core/internal/store/tool_policy_native_test.go @@ -49,13 +49,10 @@ func TestNativeToolPolicyPublicExecution(t *testing.T) { } }() token, foreign, foreignTenant := uuid.NewString(), uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test", ProjectID: h.tenant, SubjectKind: "service_account", SubjectID: "owner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: h.tenant}, {OrganizationID: "test", ProjectID: foreignTenant, SubjectKind: "service_account", SubjectID: "other", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: foreignTenant}, }) - if err != nil { - t.Fatal(err) - } handler, err := publicHandler(t, h.s, auth, kind, workerExecution(worker), withPolicy(h.d.Policy), nativeDeploymentDefaults(h.s, model, provider)) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/tool_search_native_test.go b/services/core/internal/store/tool_search_native_test.go index 1c7928c03..33ec2eeee 100644 --- a/services/core/internal/store/tool_search_native_test.go +++ b/services/core/internal/store/tool_search_native_test.go @@ -43,13 +43,10 @@ func TestNativeToolSearchPublicExecution(t *testing.T) { } }() token, foreign := uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{ + auth := newTestAuthenticator(t, []testAPIKey{ {OrganizationID: "test", ProjectID: h.tenant, SubjectKind: "service_account", SubjectID: "owner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: h.tenant}, {OrganizationID: "test", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "other", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}, }) - if err != nil { - t.Fatal(err) - } handler, err := publicHandler(t, h.s, auth, "claude_sdk", workerExecution(worker), nativeDeploymentDefaults(h.s, model, provider)) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/unified_model_configuration_http_test.go b/services/core/internal/store/unified_model_configuration_http_test.go index a168a0b6d..f1e6787f8 100644 --- a/services/core/internal/store/unified_model_configuration_http_test.go +++ b/services/core/internal/store/unified_model_configuration_http_test.go @@ -17,10 +17,7 @@ import ( func TestUnifiedModelConfigurationHTTP(t *testing.T) { st, _ := store.NewManagedTestStore(t) tenant, token, coreKey := uuid.NewString(), uuid.NewString(), uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "model-configuration", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}}) - if err != nil { - t.Fatal(err) - } + auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "model-configuration", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}}) admin, err := api.NewDeploymentAuthenticator([]string{runtimedevice.HashCredential(coreKey)}) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/unstorable_text_public_test.go b/services/core/internal/store/unstorable_text_public_test.go index 81bfb50e0..f389bc084 100644 --- a/services/core/internal/store/unstorable_text_public_test.go +++ b/services/core/internal/store/unstorable_text_public_test.go @@ -27,10 +27,7 @@ func TestUnstorableTextRejectsWithoutWritesPostgres(t *testing.T) { } s := store.NewWithCredentialCipher(pool, cipher) token := uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "nul-owner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: uuid.NewString()}}) - if err != nil { - t.Fatal(err) - } + auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "nul-owner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: uuid.NewString()}}) h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) if err != nil { t.Fatal(err) diff --git a/services/core/internal/store/whitespace_input_public_test.go b/services/core/internal/store/whitespace_input_public_test.go index 0036fcbfc..1fb64838f 100644 --- a/services/core/internal/store/whitespace_input_public_test.go +++ b/services/core/internal/store/whitespace_input_public_test.go @@ -22,10 +22,7 @@ func TestWhitespaceInputStoredVerbatimPostgres(t *testing.T) { // An isolated database keeps the no-write digest independent of other tests. s, pool := store.NewManagedTestStore(t) token := uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "whitespace-owner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: uuid.NewString()}}) - if err != nil { - t.Fatal(err) - } + auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "whitespace-owner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: uuid.NewString()}}) h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s)) if err != nil { t.Fatal(err) @@ -99,10 +96,7 @@ func TestWhitespaceInputStoredVerbatimPostgres(t *testing.T) { func TestWhitespaceOnlyTextHarnessAdmissionPostgres(t *testing.T) { s, pool := store.NewManagedTestStore(t) token := uuid.NewString() - auth, err := newTestAuthenticator([]testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "whitespace-harness", TokenSHA256: runtimedevice.HashCredential(token), TenantID: uuid.NewString()}}) - if err != nil { - t.Fatal(err) - } + auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "whitespace-harness", TokenSHA256: runtimedevice.HashCredential(token), TenantID: uuid.NewString()}}) // Real Worker admission with dispatch paused keeps admitted Turns queued. worker, err := execution.StartWorker(t.Context(), &execution.Dispatcher{Store: s, Registry: runtimegateway.NewRegistry()}) if err != nil {