diff --git a/Makefile b/Makefile index f6b4b3836..5a93f513b 100644 --- a/Makefile +++ b/Makefile @@ -43,7 +43,7 @@ openapi: output=$$(mktemp -d "$$root/core-openapi.XXXXXX"); trap 'rm -rf "$$output"' EXIT; \ python3 scripts/generate-public-api.py $(OPENAPI_FLAGS) --swag-roots "$$output/roots.go"; \ $(SWAG) init \ - -g cmd/server/main.go --dir "./services/core,./contracts/agents-api/v1,$$output" \ + -g cmd/server/main.go --dir "./services/core,./contracts/agents-api/v1,./internal/modelprovider,$$output" \ --output "$$output" \ --outputTypes yaml --parseInternal; \ python3 scripts/patch-agents-openapi.py "$$output/swagger.yaml"; \ diff --git a/apps/web/src/features/sandbox/SandboxResetControls.tsx b/apps/web/src/features/sandbox/SandboxResetControls.tsx index d269eff98..c384c9299 100644 --- a/apps/web/src/features/sandbox/SandboxResetControls.tsx +++ b/apps/web/src/features/sandbox/SandboxResetControls.tsx @@ -21,7 +21,7 @@ export function SandboxResetControls({ deployment, disabled, stale, onStart, onC const id = useId(); const reset = deployment.reset; const [dialog, setDialog] = useState<{ action: Action; generation: number; requestedAt: string | null } | null>(null); - const [clear, setClear] = useState<"auto" | "force">("auto"); + const [clear, setClear] = useState("auto"); const [deadline, setDeadline] = useState("3600"); const [submitting, setSubmitting] = useState(false); const changed = dialog !== null && (dialog.generation !== deployment.generation || dialog.requestedAt !== (reset?.requested_at ?? null)); diff --git a/contracts/agents-api/core.openapi.yaml b/contracts/agents-api/core.openapi.yaml index 3d52d068b..0950ed4e1 100644 --- a/contracts/agents-api/core.openapi.yaml +++ b/contracts/agents-api/core.openapi.yaml @@ -388,6 +388,16 @@ definitions: - failed_at - params type: object + api.DiagnosticSource: + enum: + - turn + - environment + - environment_input + type: string + x-enum-varnames: + - DiagnosticTurn + - DiagnosticEnvironment + - DiagnosticEnvironmentInput api.EnvironmentExecutorCredentialRequest: properties: key_id: @@ -413,17 +423,23 @@ definitions: type: string x-nullable: true status: - enum: - - never_enrolled - - connected - - disconnected - type: string + $ref: '#/definitions/api.ExecutorConnectionStatus' required: - bound_key_id - enrolled_at - last_seen_at - status type: object + api.ExecutorConnectionStatus: + enum: + - never_enrolled + - connected + - disconnected + type: string + x-enum-varnames: + - ExecutorNeverEnrolled + - ExecutorConnected + - ExecutorDisconnected api.ExecutorCredentialList: properties: connection: @@ -451,17 +467,8 @@ definitions: type: string x-nullable: true last_error_code: - enum: - - authentication_error - - connection_failed - - rate_limit_exceeded - - usage_limit_exceeded - - server_overloaded - - server_error - - resource_not_found - - request_timeout - - invalid_request - type: string + allOf: + - $ref: '#/definitions/modelconfiguration.ProviderErrorCode' x-nullable: true last_used_at: format: date-time @@ -535,6 +542,14 @@ definitions: required: - settings type: object + api.InstallationService: + enum: + - core + - web + type: string + x-enum-varnames: + - InstallationCore + - InstallationWeb api.InstallationSetting: properties: changeable: @@ -551,10 +566,7 @@ definitions: restarts: description: Services that restart when the setting changes. items: - enum: - - core - - web - type: string + $ref: '#/definitions/api.InstallationService' type: array sensitive: type: boolean @@ -753,11 +765,7 @@ definitions: params: type: object source: - enum: - - turn - - environment - - environment_input - type: string + $ref: '#/definitions/api.DiagnosticSource' turn_id: type: string required: @@ -934,12 +942,7 @@ definitions: type: integer x-nullable: true status: - enum: - - ok - - failing - - stopped - - unknown - type: string + $ref: '#/definitions/coremetrics.JobStatus' required: - failed - id @@ -947,6 +950,18 @@ definitions: - processed - status type: object + coremetrics.JobStatus: + enum: + - ok + - failing + - stopped + - unknown + type: string + x-enum-varnames: + - JobOk + - JobFailing + - JobStopped + - JobUnknown coremetrics.Latency: properties: p50: @@ -1048,16 +1063,21 @@ definitions: type: string x-nullable: true status: - enum: - - running - - degraded - type: string + $ref: '#/definitions/coremetrics.ServiceStatus' required: - execution_owner - revision - started_at - status type: object + coremetrics.ServiceStatus: + enum: + - running + - degraded + type: string + x-enum-varnames: + - ServiceRunning + - ServiceDegraded coremetrics.View: properties: database: @@ -1157,15 +1177,9 @@ definitions: created_at: type: string diagnostic: + allOf: + - $ref: '#/definitions/sandbox.NodeDiagnosticCode' description: Fixed reason for the last reported unreadiness; absent while the provider is ready. Clients treat an unknown value as provider_unavailable. - enum: - - provider_unavailable - - host_unsupported - - artifacts_unavailable - - runtime_download_failed - - runtime_image_unavailable - - capacity_insufficient - type: string enrollment_id: description: The enrollment_id of the command that registered this node (POST /core/v1/sandbox/enrollment-tokens); null for nodes enrolled before Core recorded it. type: string @@ -1292,15 +1306,9 @@ definitions: created_at: type: string diagnostic: + allOf: + - $ref: '#/definitions/sandbox.NodeDiagnosticCode' description: Fixed reason for the last reported unreadiness; absent while the provider is ready. Clients treat an unknown value as provider_unavailable. - enum: - - provider_unavailable - - host_unsupported - - artifacts_unavailable - - runtime_download_failed - - runtime_image_unavailable - - capacity_insufficient - type: string enrollment_id: description: The enrollment_id of the command that registered this node (POST /core/v1/sandbox/enrollment-tokens); null for nodes enrolled before Core recorded it. type: string @@ -1392,31 +1400,33 @@ definitions: deployment.NodeRollout: properties: diagnostic: - enum: - - provider_unavailable - - host_unsupported - - artifacts_unavailable - - runtime_download_failed - - runtime_image_unavailable - - capacity_insufficient - type: string + $ref: '#/definitions/sandbox.NodeDiagnosticCode' ready_generation: description: Durable serving-generation pin; online and provider_ready still gate placement. type: integer x-nullable: true state: + allOf: + - $ref: '#/definitions/deployment.NodeRolloutState' description: Target preparation, independent of an old pin's serving readiness. - enum: - - ready - - preparing - - failed - - update_required - - unknown - type: string required: - ready_generation - state type: object + deployment.NodeRolloutState: + enum: + - ready + - preparing + - failed + - update_required + - unknown + type: string + x-enum-varnames: + - NodeRolloutReady + - NodeRolloutPreparing + - NodeRolloutFailed + - NodeRolloutUpdateRequired + - NodeRolloutUnknown deployment.NodeUpdate: properties: max_active: @@ -1433,10 +1443,7 @@ definitions: deployment.Reset: properties: clear: - enum: - - auto - - force - type: string + $ref: '#/definitions/deployment.ResetMode' deadline_at: type: string x-nullable: true @@ -1454,6 +1461,14 @@ definitions: - remaining - requested_at type: object + deployment.ResetMode: + enum: + - auto + - force + type: string + x-enum-varnames: + - ResetAuto + - ResetForce deployment.ResetOfflineNode: properties: name: @@ -1491,10 +1506,7 @@ definitions: deployment.ResetRequest: properties: clear: - enum: - - auto - - force - type: string + $ref: '#/definitions/deployment.ResetMode' deadline_seconds: maximum: 86400 minimum: 300 @@ -1525,10 +1537,7 @@ definitions: previous_generation_sandboxes: type: integer state: - enum: - - settled - - preparing - type: string + $ref: '#/definitions/deployment.RolloutState' required: - nodes - previous_generation_sandboxes @@ -1553,6 +1562,14 @@ definitions: - unknown - update_required type: object + deployment.RolloutState: + enum: + - settled + - preparing + type: string + x-enum-varnames: + - RolloutSettled + - RolloutPreparing deployment.Suspension: properties: idle_seconds: @@ -1579,11 +1596,7 @@ definitions: metadata: type: object mode: - enum: - - "" - - nodes - - direct - type: string + $ref: '#/definitions/sandbox.DeploymentMode' owner_epoch: type: integer provider: @@ -1618,6 +1631,38 @@ definitions: - rollout - suspension type: object + modelconfiguration.ProviderErrorCode: + enum: + - authentication_error + - connection_failed + - rate_limit_exceeded + - usage_limit_exceeded + - server_overloaded + - server_error + - resource_not_found + - request_timeout + - invalid_request + type: string + x-enum-varnames: + - ProviderAuthenticationError + - ProviderConnectionFailed + - ProviderRateLimitExceeded + - ProviderUsageLimitExceeded + - ProviderServerOverloaded + - ProviderServerError + - ProviderResourceNotFound + - ProviderRequestTimeout + - ProviderInvalidRequest + modelprovider.Protocol: + enum: + - anthropic + - responses + - chat_completions + type: string + x-enum-varnames: + - Anthropic + - Responses + - ChatCompletions projects.APIKey: properties: created_at: @@ -1720,6 +1765,16 @@ definitions: query: type: object type: object + sandbox.DeploymentMode: + enum: + - "" + - nodes + - direct + type: string + x-enum-varnames: + - DeploymentUnconfigured + - DeploymentNodes + - DeploymentDirect sandbox.DeploymentSpec: properties: resources: @@ -1729,6 +1784,22 @@ definitions: required: - resources type: object + sandbox.NodeDiagnosticCode: + enum: + - provider_unavailable + - host_unsupported + - artifacts_unavailable + - runtime_download_failed + - runtime_image_unavailable + - capacity_insufficient + type: string + x-enum-varnames: + - NodeProviderUnavailable + - NodeHostUnsupported + - NodeArtifactsUnavailable + - NodeRuntimeDownloadFailed + - NodeRuntimeImageUnavailable + - NodeCapacityInsufficient sandbox.Resources: properties: cpus: @@ -1821,16 +1892,22 @@ definitions: session_id: type: string state: - enum: - - active - - cleanup_pending - - released - type: string + $ref: '#/definitions/sessions.ManagedArchiveState' required: - environment_id - session_id - state type: object + sessions.ManagedArchiveState: + enum: + - active + - cleanup_pending + - released + type: string + x-enum-varnames: + - ManagedArchiveActive + - ManagedArchiveCleanupPending + - ManagedArchiveReleased v1.Agent: properties: id: @@ -1998,10 +2075,7 @@ definitions: message: type: string status: - enum: - - available - - unavailable - type: string + $ref: '#/definitions/v1.InstallationStatus' version: type: string type: object @@ -2131,12 +2205,7 @@ definitions: v1.ExecutionHarnessConfigSelection: properties: source: - enum: - - session - - agent - - deployment - - unknown - type: string + $ref: '#/definitions/v1.ExecutionSource' value: type: object required: @@ -2150,32 +2219,28 @@ definitions: - $ref: '#/definitions/v1.ModelProviderView' x-nullable: true source: - enum: - - session - - agent - - deployment - - unknown - type: string + $ref: '#/definitions/v1.ExecutionSource' status: - enum: - - available - - redacted - - unavailable - type: string + $ref: '#/definitions/v1.ExecutionProviderStatus' required: - configuration - source - status type: object + v1.ExecutionProviderStatus: + enum: + - available + - redacted + - unavailable + type: string + x-enum-varnames: + - ExecutionProviderAvailable + - ExecutionProviderRedacted + - ExecutionProviderUnavailable v1.ExecutionSelection: properties: source: - enum: - - session - - agent - - deployment - - unknown - type: string + $ref: '#/definitions/v1.ExecutionSource' value: type: string x-nullable: true @@ -2183,6 +2248,18 @@ definitions: - source - value type: object + v1.ExecutionSource: + enum: + - session + - agent + - deployment + - unknown + type: string + x-enum-varnames: + - ExecutionSourceSession + - ExecutionSourceAgent + - ExecutionSourceDeployment + - ExecutionSourceUnknown v1.InputTokenDetails: properties: cached_tokens: @@ -2190,6 +2267,14 @@ definitions: required: - cached_tokens type: object + v1.InstallationStatus: + enum: + - available + - unavailable + type: string + x-enum-varnames: + - InstallationAvailable + - InstallationUnavailable v1.Item: properties: action: @@ -2369,11 +2454,7 @@ definitions: max_output_tokens: type: integer protocol: - enum: - - anthropic - - responses - - chat_completions - type: string + $ref: '#/definitions/modelprovider.Protocol' required: - api_key - base_url @@ -2390,11 +2471,7 @@ definitions: max_output_tokens: type: integer protocol: - enum: - - anthropic - - responses - - chat_completions - type: string + $ref: '#/definitions/modelprovider.Protocol' required: - api_key_configured - base_url @@ -3770,18 +3847,28 @@ definitions: - prefix - revoked_at type: object + writeaudit.Action: + enum: + - create + - update + - delete + - send_events + - upload_file + - upload_version + - update_default_version + type: string + x-enum-varnames: + - ActionCreate + - ActionUpdate + - ActionDelete + - ActionSendEvents + - ActionUploadFile + - ActionUploadVersion + - ActionUpdateDefaultVersion writeaudit.Operation: properties: action: - enum: - - create - - update - - delete - - send_events - - upload_file - - upload_version - - update_default_version - type: string + $ref: '#/definitions/writeaudit.Action' api_key: $ref: '#/definitions/writeaudit.APIKey' created_at: @@ -3795,18 +3882,7 @@ definitions: resource_id: type: string resource_type: - enum: - - agent - - session - - environment - - environment_template - - skill - - skill_version - - file - - vault - - credential - - artifact - type: string + $ref: '#/definitions/writeaudit.ResourceType' trace_id: type: string required: @@ -3847,6 +3923,30 @@ definitions: - api_key - resource_id type: object + writeaudit.ResourceType: + enum: + - agent + - session + - environment + - environment_template + - skill + - skill_version + - file + - vault + - credential + - artifact + type: string + x-enum-varnames: + - ResourceAgent + - ResourceSession + - ResourceEnvironment + - ResourceEnvironmentTemplate + - ResourceSkill + - ResourceSkillVersion + - ResourceFile + - ResourceVault + - ResourceCredential + - ResourceArtifact info: contact: {} description: Deployment and operations routes under /core/v1 for Core Web's server and operator scripts. Every operation requires the Core key; Project API keys and machine credentials are not accepted. diff --git a/contracts/agents-api/index.md b/contracts/agents-api/index.md index 280696bbb..9e9f6bb7f 100644 --- a/contracts/agents-api/index.md +++ b/contracts/agents-api/index.md @@ -14,7 +14,7 @@ Core targets the complete OpenAI Agents API as pinned below ([public API rule](h | [openapi.yaml](./openapi.yaml) | The official public contract with Core's `x_agents_core` extension on Agent and Session request/response objects | | [go-bindings.json](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/contracts/agents-api/go-bindings.json) | Go names, field representations, encoding order and stored projections; it does not define official field membership, enums or constraints | -Run `make openapi` to regenerate the public Go types, the Agent request shapes, the route inventory and all three OpenAPI documents. `scripts/generate-public-api.py` reads the checked-in, checksum-verified official source without network access. It selects Agents, Vaults, Files and Skills and follows their schema references, preserving union types, nullability, required fields and constraints. The request shapes in `services/core/internal/api/official_shapes.gen.go` project `CreateAgentParams`, `UpdateAgentParams` and `SessionAgentConfigParam`; Core checks request bodies against them before it reads an Agent configuration. Core's extension types in `v1/` remain authored in Go and are added to the public schema during generation. The TypeScript client's types, enum values and field names in `packages/agents-client/src/generated/public-api.ts` are generated from the resulting public schema. The internal `/core/v1` and `/api/v1` documents come from handler annotations; the same generator projects the `/core/v1` document into `packages/agents-client/src/generated/core-api.ts`, which imports the public types it references from `public-api.ts`. In those annotations a response field Core always sends carries `binding:"required"`, a field that can be null carries `extensions:"x-nullable"`, and a closed set carries `enums:`; the tags only shape the documents, and the client rejects a response that breaks them. `make check-openapi` checks freshness and the generator; it also runs through `make check-go`. +Run `make openapi` to regenerate the public Go types, the Agent request shapes, the route inventory and all three OpenAPI documents. `scripts/generate-public-api.py` reads the checked-in, checksum-verified official source without network access. It selects Agents, Vaults, Files and Skills and follows their schema references, preserving union types, nullability, required fields and constraints. The request shapes in `services/core/internal/api/official_shapes.gen.go` project `CreateAgentParams`, `UpdateAgentParams` and `SessionAgentConfigParam`; Core checks request bodies against them before it reads an Agent configuration. Core's extension types in `v1/` remain authored in Go and are added to the public schema during generation. The TypeScript client's types, enum values and field names in `packages/agents-client/src/generated/public-api.ts` are generated from the resulting public schema. The internal `/core/v1` and `/api/v1` documents come from handler annotations; the same generator projects the `/core/v1` document into `packages/agents-client/src/generated/core-api.ts`, which imports the public types it references from `public-api.ts`. In those annotations a response field Core always sends carries `binding:"required"`, a field that can be null carries `extensions:"x-nullable"`, and a Core-owned closed set uses a named Go type with typed constants in its producing package. Swag derives one enum definition from that type, shared by every wire field. Single-value discriminators keep `enums:` tags; fields mirroring official or Runtime-owned sets retain tags checked against their owners by contract tests. These annotations only shape the documents, and the client rejects a response that breaks them. `make check-openapi` checks freshness and the generator; it also runs through `make check-go`. The public contract is the official API plus Core extensions. Standard fields are generated into `v1/official.gen.go`; `go-bindings.json` lists types consumed by Core and overrides only the Go representation or field order that existing storage or custom JSON encoding requires. Unspecified fields follow the official schema; shared shapes use one Go type. Selected discriminated unions also generate JSON serializers to retain required nullable fields for each variant. Other union serializers, Core's local limits, execution admission and state transitions remain implementation code. Contract tests verify that the public schema preserves the official definitions, extensions remain in `x_agents_core`, and all documents match registered routes. Official-client and raw HTTP tests verify behavior. Schema generation does not qualify an unimplemented feature; the gaps below still apply. Upstream upgrades update the OpenAPI and SDK pins together after comparison and compatibility tests. diff --git a/contracts/agents-api/openapi.yaml b/contracts/agents-api/openapi.yaml index 1885da9ed..43e25a570 100644 --- a/contracts/agents-api/openapi.yaml +++ b/contracts/agents-api/openapi.yaml @@ -17663,6 +17663,19 @@ "additionalProperties": false, "description": "Confirmation that a vault credential was deleted." }, + "modelprovider.Protocol": { + "enum": [ + "anthropic", + "responses", + "chat_completions" + ], + "type": "string", + "x-enum-varnames": [ + "Anthropic", + "Responses", + "ChatCompletions" + ] + }, "v1.AgentsCore": { "properties": { "harness": { @@ -17694,11 +17707,7 @@ "type": "string" }, "status": { - "enum": [ - "available", - "unavailable" - ], - "type": "string" + "$ref": "#/components/schemas/v1.InstallationStatus" }, "version": { "type": "string" @@ -17706,6 +17715,17 @@ }, "type": "object" }, + "v1.InstallationStatus": { + "enum": [ + "available", + "unavailable" + ], + "type": "string", + "x-enum-varnames": [ + "InstallationAvailable", + "InstallationUnavailable" + ] + }, "v1.ModelProviderInput": { "properties": { "api_key": { @@ -17721,12 +17741,7 @@ "type": "integer" }, "protocol": { - "enum": [ - "anthropic", - "responses", - "chat_completions" - ], - "type": "string" + "$ref": "#/components/schemas/modelprovider.Protocol" } }, "required": [ @@ -17751,12 +17766,7 @@ "type": "integer" }, "protocol": { - "enum": [ - "anthropic", - "responses", - "chat_completions" - ], - "type": "string" + "$ref": "#/components/schemas/modelprovider.Protocol" } }, "required": [ diff --git a/contracts/agents-api/v1/installation.go b/contracts/agents-api/v1/installation.go index 9ae3a993e..a1f77808a 100644 --- a/contracts/agents-api/v1/installation.go +++ b/contracts/agents-api/v1/installation.go @@ -1,5 +1,12 @@ package v1 +type InstallationStatus string + +const ( + InstallationAvailable InstallationStatus = "available" + InstallationUnavailable InstallationStatus = "unavailable" +) + // SessionCore exposes optional Core additions without changing official fields. type SessionCore struct { Installation *EnvironmentInstallation `json:"installation,omitempty"` @@ -8,11 +15,11 @@ type SessionCore struct { // EnvironmentInstallation contains short-lived, Environment-scoped commands. // Only authenticated creation and detail responses include this authorization. type EnvironmentInstallation struct { - Status string `json:"status" enums:"available,unavailable"` - Version string `json:"version"` - ExpiresAt int64 `json:"expires_at,omitempty"` - Commands map[string]string `json:"commands,omitempty"` - Message string `json:"message,omitempty"` + Status InstallationStatus `json:"status"` + Version string `json:"version"` + ExpiresAt int64 `json:"expires_at,omitempty"` + Commands map[string]string `json:"commands,omitempty"` + Message string `json:"message,omitempty"` } // NativeInstallationContext is bootstrap metadata, not an execution protocol. diff --git a/contracts/agents-api/v1/model_execution.go b/contracts/agents-api/v1/model_execution.go index 903255c44..66b75e64b 100644 --- a/contracts/agents-api/v1/model_execution.go +++ b/contracts/agents-api/v1/model_execution.go @@ -6,6 +6,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/builtin" + "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" ) // ModelProviderError preserves the shared validation message while allowing Core @@ -26,11 +27,11 @@ type SessionExecutionInput struct { } type ModelProviderInput struct { - Protocol string `json:"protocol" enums:"anthropic,responses,chat_completions" binding:"required"` - BaseURL string `json:"base_url" binding:"required"` - APIKey string `json:"api_key" binding:"required"` - ContextWindow int32 `json:"context_window,omitempty"` - MaxOutputTokens int32 `json:"max_output_tokens,omitempty"` + Protocol modelprovider.Protocol `json:"protocol" binding:"required"` + BaseURL string `json:"base_url" binding:"required"` + APIKey string `json:"api_key" binding:"required"` + ContextWindow int32 `json:"context_window,omitempty"` + MaxOutputTokens int32 `json:"max_output_tokens,omitempty"` } func (p *ModelProviderInput) Validate() error { @@ -44,7 +45,7 @@ func (p *ModelProviderInput) validate(registry harnessconfig.Registry) error { if !validModelProviderBaseURL(p.BaseURL) { return &ModelProviderError{Code: "model_provider_base_url_invalid", Param: "base_url", message: "model provider requires an HTTPS base_url without credentials, query or fragment"} } - if !registry.SupportsProtocol(p.Protocol) { + if !registry.SupportsProtocol(string(p.Protocol)) { return &ModelProviderError{Code: "model_provider_protocol_unsupported", Param: "protocol", message: "unsupported model provider protocol"} } if strings.TrimSpace(p.APIKey) == "" || len(p.APIKey) > 16384 || strings.ContainsAny(p.APIKey, "\x00\r\n") { @@ -71,10 +72,10 @@ func (p *ModelProviderInput) ValidateHarnessWithRegistry(harness string, registr return err } configuration, _ := registry.Lookup(harness) - if err := configuration.ValidateProtocol(p.Protocol); err != nil { + if err := configuration.ValidateProtocol(string(p.Protocol)); err != nil { return &ModelProviderError{Code: "model_provider_protocol_unsupported", Param: "protocol", message: err.Error()} } - if err := configuration.Validate(p.Protocol, p.ContextWindow, p.MaxOutputTokens); err != nil { + if err := configuration.Validate(string(p.Protocol), p.ContextWindow, p.MaxOutputTokens); err != nil { param := "max_output_tokens" if p.ContextWindow <= 0 { param = "context_window" diff --git a/contracts/agents-api/v1/model_execution_test.go b/contracts/agents-api/v1/model_execution_test.go index 8db6938ea..8e7bce603 100644 --- a/contracts/agents-api/v1/model_execution_test.go +++ b/contracts/agents-api/v1/model_execution_test.go @@ -1,12 +1,16 @@ package v1 -import "testing" +import ( + "testing" + + "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" +) func TestModelExecutionValidation(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) { - p := ModelProviderInput{Protocol: protocol, BaseURL: "https://example.com/v1", APIKey: "secret", ContextWindow: 200000, MaxOutputTokens: 8000} + p := ModelProviderInput{Protocol: modelprovider.Protocol(protocol), BaseURL: "https://example.com/v1", APIKey: "secret", ContextWindow: 200000, MaxOutputTokens: 8000} native := harness == "mcode" || (harness == "codex" && protocol == "responses") || (harness == "claude_sdk" && protocol == "anthropic") if err := p.ValidateHarness(harness); (err == nil) != native { t.Fatalf("wrong native protocol admission: %v", err) @@ -22,7 +26,7 @@ func TestModelExecutionValidation(t *testing.T) { {"responses", ""}, {"responses", "unknown"}, {"unknown", "codex"}, {"openai", "codex"}, {"chat", "claude_sdk"}, {"chat-completions", "mcode"}, } { - p := ModelProviderInput{Protocol: tc.protocol, BaseURL: "https://example.com/v1", APIKey: "secret", ContextWindow: 200000, MaxOutputTokens: 8000} + p := ModelProviderInput{Protocol: modelprovider.Protocol(tc.protocol), BaseURL: "https://example.com/v1", APIKey: "secret", ContextWindow: 200000, MaxOutputTokens: 8000} if p.ValidateHarness(tc.harness) == nil { t.Fatalf("unsupported protocol or harness accepted: %s/%s", tc.protocol, tc.harness) } diff --git a/contracts/agents-api/v1/model_provider_admission.go b/contracts/agents-api/v1/model_provider_admission.go index fa91a1cc2..d1b0d3f1e 100644 --- a/contracts/agents-api/v1/model_provider_admission.go +++ b/contracts/agents-api/v1/model_provider_admission.go @@ -15,22 +15,15 @@ const providerScheme = "https" // hyphen or STD3 restrictions), rejecting invalid labels such as bad punycode. var providerHost = idna.New(idna.MapForLookup(), idna.BidiRule(), idna.StrictDomainName(false), idna.CheckHyphens(false)) -// Model provider sources, as recorded in a Session's execution configuration. -const ( - ModelProviderSourceSession = "session" - ModelProviderSourceAgent = "agent" - ModelProviderSourceDeployment = "deployment" -) - // ModelProviderAllowed reports whether a provider bundle from source may be // frozen into a Session placed in environment. Caller bundles (Session or saved // Agent) run on Core-managed or caller-owned compute. The deployment default // holds the operator's key, so it stays on operator compute: openai_hosted and // operator-registered none devices. An unknown source is allowed only where // every source is. -func ModelProviderAllowed(environment, source string) bool { - caller := source == ModelProviderSourceSession || source == ModelProviderSourceAgent - deployment := source == ModelProviderSourceDeployment +func ModelProviderAllowed(environment string, source ExecutionSource) bool { + caller := source == ExecutionSourceSession || source == ExecutionSourceAgent + deployment := source == ExecutionSourceDeployment switch environment { case "openai_hosted": return true diff --git a/contracts/agents-api/v1/saved_core_extension.go b/contracts/agents-api/v1/saved_core_extension.go index 1b1353a3d..6f2fc2733 100644 --- a/contracts/agents-api/v1/saved_core_extension.go +++ b/contracts/agents-api/v1/saved_core_extension.go @@ -6,6 +6,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/builtin" + "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" ) // SavedAgentCoreInput carries defaults for future Sessions. The provider bundle @@ -24,11 +25,11 @@ type SavedAgentCore struct { } type ModelProviderView struct { - Protocol string `json:"protocol" enums:"anthropic,responses,chat_completions" binding:"required"` - BaseURL string `json:"base_url" binding:"required"` - ContextWindow int32 `json:"context_window,omitempty"` - MaxOutputTokens int32 `json:"max_output_tokens,omitempty"` - APIKeyConfigured bool `json:"api_key_configured" binding:"required"` + Protocol modelprovider.Protocol `json:"protocol" binding:"required"` + BaseURL string `json:"base_url" binding:"required"` + ContextWindow int32 `json:"context_window,omitempty"` + MaxOutputTokens int32 `json:"max_output_tokens,omitempty"` + APIKeyConfigured bool `json:"api_key_configured" binding:"required"` } func (x *SavedAgentCoreInput) Validate() error { @@ -78,5 +79,5 @@ func (p *ModelProviderView) ValidateHarnessWithRegistry(harness string, registry if p == nil { return errors.New("model_provider is required") } - return registry.Validate(harness, p.Protocol, p.ContextWindow, p.MaxOutputTokens) + return registry.Validate(harness, string(p.Protocol), p.ContextWindow, p.MaxOutputTokens) } diff --git a/contracts/agents-api/v1/saved_core_extension_test.go b/contracts/agents-api/v1/saved_core_extension_test.go index 54a36b611..e253998e1 100644 --- a/contracts/agents-api/v1/saved_core_extension_test.go +++ b/contracts/agents-api/v1/saved_core_extension_test.go @@ -4,6 +4,8 @@ import ( "encoding/json" "strings" "testing" + + "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" ) func TestSavedProviderSafeView(t *testing.T) { @@ -32,7 +34,7 @@ func TestSavedProviderExplicitHarnessCompatibility(t *testing.T) { for _, protocol := range []string{"anthropic", "responses", "chat_completions"} { t.Run(harness+"/"+protocol, func(t *testing.T) { x := &SavedAgentCoreInput{Harness: harness, ModelProvider: &ModelProviderInput{ - Protocol: protocol, BaseURL: "https://example.test", APIKey: "fixture", ContextWindow: 100, MaxOutputTokens: 20, + Protocol: modelprovider.Protocol(protocol), BaseURL: "https://example.test", APIKey: "fixture", ContextWindow: 100, MaxOutputTokens: 20, }} native := harness == "" || harness == "mcode" || (harness == "codex" && protocol == "responses") || (harness == "claude_sdk" && protocol == "anthropic") if err := x.Validate(); (err == nil) != native { @@ -49,7 +51,7 @@ func TestSavedProviderExplicitHarnessCompatibility(t *testing.T) { for _, protocol := range []string{"anthropic", "responses", "chat_completions"} { for _, limits := range [][2]int32{{0, 0}, {100, 0}, {0, 20}} { x := &SavedAgentCoreInput{Harness: "mcode", ModelProvider: &ModelProviderInput{ - Protocol: protocol, BaseURL: "https://example.test", APIKey: "fixture", ContextWindow: limits[0], MaxOutputTokens: limits[1], + Protocol: modelprovider.Protocol(protocol), BaseURL: "https://example.test", APIKey: "fixture", ContextWindow: limits[0], MaxOutputTokens: limits[1], }} if x.Validate() == nil || x.SafeView().ModelProvider.ValidateHarness("mcode") == nil { t.Fatal("MiniMax limits must be complete for every upstream protocol") diff --git a/contracts/agents-api/v1/session_execution_configuration.go b/contracts/agents-api/v1/session_execution_configuration.go index 1a07b0e6c..a03fa238a 100644 --- a/contracts/agents-api/v1/session_execution_configuration.go +++ b/contracts/agents-api/v1/session_execution_configuration.go @@ -2,6 +2,23 @@ package v1 import "encoding/json" +type ExecutionProviderStatus string + +const ( + ExecutionProviderAvailable ExecutionProviderStatus = "available" + ExecutionProviderRedacted ExecutionProviderStatus = "redacted" + ExecutionProviderUnavailable ExecutionProviderStatus = "unavailable" +) + +type ExecutionSource string + +const ( + ExecutionSourceSession ExecutionSource = "session" + ExecutionSourceAgent ExecutionSource = "agent" + ExecutionSourceDeployment ExecutionSource = "deployment" + ExecutionSourceUnknown ExecutionSource = "unknown" +) + // SessionExecutionConfiguration describes committed configuration, not live // execution health. Unknown provenance is never reconstructed from defaults. type SessionExecutionConfiguration struct { @@ -15,20 +32,20 @@ type SessionExecutionConfiguration struct { } type ExecutionSelection struct { - Value *string `json:"value" binding:"required" extensions:"x-nullable"` - Source string `json:"source" binding:"required" enums:"session,agent,deployment,unknown"` + Value *string `json:"value" binding:"required" extensions:"x-nullable"` + Source ExecutionSource `json:"source" binding:"required"` } // Deployment credentials have no public endpoint projection. Unavailable means // no trustworthy safe provider snapshot was recorded, not failed readiness. type ExecutionProviderSelection struct { - Source string `json:"source" binding:"required" enums:"session,agent,deployment,unknown"` - Status string `json:"status" binding:"required" enums:"available,redacted,unavailable"` - Configuration *ModelProviderView `json:"configuration" binding:"required" extensions:"x-nullable"` + Source ExecutionSource `json:"source" binding:"required"` + Status ExecutionProviderStatus `json:"status" binding:"required"` + Configuration *ModelProviderView `json:"configuration" binding:"required" extensions:"x-nullable"` } // ExecutionHarnessConfigSelection records the immutable adapter parameters. type ExecutionHarnessConfigSelection struct { Value json.RawMessage `json:"value" swaggertype:"object" binding:"required"` - Source string `json:"source" binding:"required" enums:"session,agent,deployment,unknown"` + Source ExecutionSource `json:"source" binding:"required"` } diff --git a/contracts/agents-api/v1/upstream_contract_test.go b/contracts/agents-api/v1/upstream_contract_test.go index 9be78a569..140dd2c75 100644 --- a/contracts/agents-api/v1/upstream_contract_test.go +++ b/contracts/agents-api/v1/upstream_contract_test.go @@ -44,8 +44,9 @@ func TestPublicSchemaPreservesOfficialDefinitions(t *testing.T) { upstream := source["components"].(map[string]any)["schemas"].(map[string]any) schemas := public["components"].(map[string]any)["schemas"].(map[string]any) owners := map[string]bool{"AgentResource": true, "CreateAgentParams": true, "UpdateAgentParams": true, "SessionAgentConfigParam": true, "SessionAgentResource": true, "CreateAgentSessionParams": true, "SessionResource": true} + // Extension dependencies retain their producing Go package names. for name, value := range schemas { - if strings.HasPrefix(name, "v1.") { + if strings.HasPrefix(name, "v1.") || name == "modelprovider.Protocol" { continue } schema := value.(map[string]any) diff --git a/contracts/agents-api/zh/index.md b/contracts/agents-api/zh/index.md index 9f98d96c2..85acfdf9f 100644 --- a/contracts/agents-api/zh/index.md +++ b/contracts/agents-api/zh/index.md @@ -1,7 +1,7 @@ --- title: "Agents API 覆盖台账" source: contracts/agents-api/index.md -source_hash: dc1388f616997b89e298113b7626fac569a9650f9a52710500f96a86c2b1af80 +source_hash: f4a4bf113b88eb25ebccea95cc3125a7413e40493066e60bf2215da86471a5c8 --- Core 旨在以下方固定版本为准支持完整的 OpenAI Agents API([public API rule](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/AGENTS.md#public-api))。本台账记录 Core 对各项资源实现了哪些内容、哪些契约保存其详细信息,并列出相对于 OpenAI 服务的所有已知差异和所有未解决缺口。[API namespaces and credentials](../../../docs/zh/api/index.md) 说明谁调用哪些 API;[Agents API guide](../../../docs/zh/api/public-agent-api.md) 介绍使用方法。 @@ -16,7 +16,7 @@ Core 旨在以下方固定版本为准支持完整的 OpenAI Agents API([publi | [openapi.yaml](../openapi.yaml) | 官方公共契约,并在 Agent 和 Session 请求及响应对象上加入 Core 的 `x_agents_core` 扩展 | | [go-bindings.json](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/contracts/agents-api/go-bindings.json) | Go 名称、字段表示、编码顺序和存储投影;不定义官方字段集合、枚举或约束 | -运行 `make openapi` 重新生成公共 Go 类型、Agent 请求结构、路由清单和三个 OpenAPI 文档。`scripts/generate-public-api.py` 读取仓库内经过校验和验证的官方源文件,无需网络。它选择 Agents、Vaults、Files 和 Skills,并跟随 schema 引用,保留联合类型、可空性、必填字段和约束。`services/core/internal/api/official_shapes.gen.go` 中的请求结构投影 `CreateAgentParams`、`UpdateAgentParams` 和 `SessionAgentConfigParam`;Core 在读取 Agent 配置前先按它们检查请求体。Core 在 `v1/` 中的扩展类型继续由 Go 定义,在生成时加入公共 schema。TypeScript 客户端在 `packages/agents-client/src/generated/public-api.ts` 中的类型、枚举值和字段名由生成后的公共 schema 生成。内部 `/core/v1` 和 `/api/v1` 文档由处理函数注解生成;同一生成器把 `/core/v1` 文档投影为 `packages/agents-client/src/generated/core-api.ts`,其中引用的公共类型从 `public-api.ts` 导入。在这些注解里,Core 总会发送的响应字段带 `binding:"required"`,可以为 null 的字段带 `extensions:"x-nullable"`,封闭集合带 `enums:`;这些标签只影响文档,客户端会拒绝不符合它们的响应。`make check-openapi` 检查生成结果是否最新并测试生成器;`make check-go` 也会运行此检查。 +运行 `make openapi` 重新生成公共 Go 类型、Agent 请求结构、路由清单和三个 OpenAPI 文档。`scripts/generate-public-api.py` 读取仓库内经过校验和验证的官方源文件,无需网络。它选择 Agents、Vaults、Files 和 Skills,并跟随 schema 引用,保留联合类型、可空性、必填字段和约束。`services/core/internal/api/official_shapes.gen.go` 中的请求结构投影 `CreateAgentParams`、`UpdateAgentParams` 和 `SessionAgentConfigParam`;Core 在读取 Agent 配置前先按它们检查请求体。Core 在 `v1/` 中的扩展类型继续由 Go 定义,在生成时加入公共 schema。TypeScript 客户端在 `packages/agents-client/src/generated/public-api.ts` 中的类型、枚举值和字段名由生成后的公共 schema 生成。内部 `/core/v1` 和 `/api/v1` 文档由处理函数注解生成;同一生成器把 `/core/v1` 文档投影为 `packages/agents-client/src/generated/core-api.ts`,其中引用的公共类型从 `public-api.ts` 导入。在这些注解里,Core 总会发送的响应字段带 `binding:"required"`,可以为 null 的字段带 `extensions:"x-nullable"`,Core 拥有的封闭集合使用生产方包中的具名 Go 类型和带类型的常量。Swag 从该类型生成一个枚举定义,供所有线上字段共享。单值判别字段保留 `enums:` 标签;映射官方或 Runtime 所有集合的字段保留标签,并通过契约测试与其所有者核对。这些注解只影响文档,客户端会拒绝不符合它们的响应。`make check-openapi` 检查生成结果是否最新并测试生成器;`make check-go` 也会运行此检查。 公共契约是官方 API 加上 Core 扩展。标准字段生成到 `v1/official.gen.go`;`go-bindings.json` 只列出 Core 使用的类型,仅在已有存储或自定义 JSON 编码需要时覆盖 Go 表示或字段顺序。未覆盖的字段遵循官方 schema,相同结构复用同一个 Go 类型。部分带判别字段的联合类型也从 schema 生成 JSON 序列化代码,保留每个分支必需的可空字段。其他联合类型序列化、Core 的本地限制、执行准入和状态转换仍由实现代码负责。契约测试验证公共 schema 保留官方定义、扩展位于 `x_agents_core` 中,且所有文档与注册路由一致。官方客户端和原始 HTTP 测试验证行为。生成 schema 不代表某个尚未实现的功能已经得到验证;下方缺口仍然适用。升级上游时,在比对和兼容性测试后一起更新 OpenAPI 和 SDK 固定版本。 diff --git a/packages/agents-client/src/admin-projection.ts b/packages/agents-client/src/admin-projection.ts index de765a7df..15877ecc5 100644 --- a/packages/agents-client/src/admin-projection.ts +++ b/packages/agents-client/src/admin-projection.ts @@ -8,11 +8,11 @@ import { addressBindingsFields, adminAssetCountsFields, adminauditOperationFields, adminauditPageFields, adminRuntimeObservationFields, adminRuntimeObservationListFields, adminSessionCountsFields, adminSummaryResponseFields, adminSummaryRowFields, adminUsageCoverageFields, coreHarnessFields, coreHarnessListFields, executorConnectionFields, executorConnectionStatusValues, executorCredentialFields, - executorCredentialListFields, harnessModelConfigurationFields, harnessModelConfigurationLastErrorCodeValues, installationConfigurationFields, - installationFields, installationSettingFields, installationSettingRequired, installationSettingRestartsValues, issuedExecutorCredentialFields, + executorCredentialListFields, harnessModelConfigurationFields, providerErrorCodeValues, installationConfigurationFields, + installationFields, installationSettingFields, installationSettingRequired, installationServiceValues, issuedExecutorCredentialFields, managedArchiveFields, managedArchiveStateValues, modelConfigurationSupportFields, projectFields, projectsAPIKeyFields, resourceOwnerFields, - resourceOwnerListFields, runtimeDiskObservationFields, writeauditAPIKeyFields, writeauditOperationActionValues, writeauditOperationFields, - writeauditOperationResourceTypeValues, writeauditPageFields, + resourceOwnerListFields, runtimeDiskObservationFields, writeauditAPIKeyFields, actionValues, writeauditOperationFields, + resourceTypeValues, writeauditPageFields, } from "./generated/core-api"; import type { CoreHarness, CoreHarnessKind, HarnessModelConfiguration, ListPage, SavedAgent, SessionArtifact } from "./types"; import type { @@ -119,7 +119,7 @@ export function projectWriteOperations(value: unknown): AdminWriteOperationPage const data = page.data.map((entry) => { const operation = record(entry, writeauditOperationFields); strings(operation, ["id", "created_at", "action", "resource_id", "parent_id", "request_id", "trace_id"]); - if (!date(operation.created_at) || !isOneOf(writeauditOperationActionValues, operation.action) || !isOneOf(writeauditOperationResourceTypeValues, operation.resource_type)) return invalidAdminResponse(); + if (!date(operation.created_at) || !isOneOf(actionValues, operation.action) || !isOneOf(resourceTypeValues, operation.resource_type)) return invalidAdminResponse(); return { ...operation, api_key: projectProvenance(operation.api_key) }; }); return { data, has_more: page.has_more, next_cursor: page.next_cursor } as AdminWriteOperationPage; @@ -212,7 +212,7 @@ export function projectHarnessModelConfiguration(value: unknown, harness?: CoreH const { object, harness: kind, updated_at, last_used_at, last_error_code, last_error_at, model_provider, model, harness_config } = value; if ((last_used_at !== null && (typeof last_used_at !== "string" || !date(last_used_at))) || (last_error_at !== null && (typeof last_error_at !== "string" || !date(last_error_at))) || - (last_error_code !== null && !isOneOf(harnessModelConfigurationLastErrorCodeValues, last_error_code)) || + (last_error_code !== null && !isOneOf(providerErrorCodeValues, last_error_code)) || ((last_error_code === null) !== (last_error_at === null))) return invalidAdminResponse(); if (object !== "core.model_configuration" || !isOneOf(coreHarnessKinds, kind) || (harness !== undefined && kind !== harness) || typeof updated_at !== "string" || !date(updated_at)) return invalidAdminResponse(); @@ -255,7 +255,7 @@ function projectInstallationSetting(value: unknown): CoreInstallationSetting { // `configured` is present exactly for a sensitive setting. const setting = record(value, value.sensitive ? installationSettingFields : installationSettingRequired); if (typeof setting.key !== "string" || !settingKey.test(setting.key) || typeof setting.changeable !== "boolean" || - !Array.isArray(setting.restarts) || !setting.restarts.every((service) => isOneOf(installationSettingRestartsValues, service)) || + !Array.isArray(setting.restarts) || !setting.restarts.every((service) => isOneOf(installationServiceValues, service)) || (setting.sensitive && (setting.value !== null || setting.default !== null || typeof setting.configured !== "boolean"))) return invalidAdminResponse(); return { ...setting } as unknown as CoreInstallationSetting; } diff --git a/packages/agents-client/src/admin-types.ts b/packages/agents-client/src/admin-types.ts index 266296360..2b5b5b939 100644 --- a/packages/agents-client/src/admin-types.ts +++ b/packages/agents-client/src/admin-types.ts @@ -2,12 +2,12 @@ import type { AddressBindings, AdminauditOperation, AdminauditPage, AdminRuntimeObservationDetail, AdminSessionArchiveRequest, AdminSummaryResponse, AdminSummaryRow, EnvironmentExecutorCredentialRequest, Installation, InstallationConfiguration, InstallationSetting, IssuedAPIKey, IssuedExecutorCredential as IssuedExecutorCredentialResource, ManagedArchive, Project, ProjectAPIKeyRequest, ProjectRequest, ProjectsAPIKey, - ResourceOwner, WriteauditAPIKey, WriteauditOperation, WriteauditOperationResourceType, WriteauditPage, + ResourceOwner, WriteauditAPIKey, WriteauditOperation, ResourceType, WriteauditPage, } from "./generated/core-api"; import type { PageOptions, RuntimeObservation } from "./types"; export type { ExecutorConnection, ExecutorCredential, ExecutorCredentialList, RuntimeDiskObservation } from "./generated/core-api"; -export { writeauditOperationResourceTypeValues as adminResourceTypes } from "./generated/core-api"; +export { resourceTypeValues as adminResourceTypes } from "./generated/core-api"; // Generated types keep their schema names in ./generated/core-api; these are the client's names for them. export type AdminProject = Project; @@ -19,7 +19,7 @@ export type IssueAdminAPIKeyInput = ProjectAPIKeyRequest; export type ArchiveAdminSessionInput = AdminSessionArchiveRequest; /** Current resource disposition; released does not imply that the active Turn has finalized. */ export type AdminSessionArchive = ManagedArchive; -export type AdminResourceType = WriteauditOperationResourceType; +export type AdminResourceType = ResourceType; export type AdminKeyProvenance = WriteauditAPIKey; /** `api_key` is null when Core has no creation record. */ export type AdminResourceOwner = ResourceOwner; diff --git a/packages/agents-client/src/core-metrics.ts b/packages/agents-client/src/core-metrics.ts index 43cd21e9e..f6859cfd1 100644 --- a/packages/agents-client/src/core-metrics.ts +++ b/packages/agents-client/src/core-metrics.ts @@ -2,7 +2,7 @@ import { AgentCoreError } from "./client"; import { CoreRequester, type CoreClientOptions } from "./core-request"; import { isOneOf } from "./response-projection"; import type { ReadOptions } from "./types"; -import { jobStatusValues, serviceStateStatusValues, type CoremetricsView, type JobStatus, type Latency, type ServiceState, type ServiceStateStatus } from "./generated/core-api"; +import { jobStatusValues, serviceStatusValues, type CoremetricsView, type JobStatus, type Latency, type ServiceState, type ServiceStatus } from "./generated/core-api"; function invalidCoreMetrics(): never { throw new AgentCoreError("Core metrics: the response is not JSON.", 0, "invalid_response"); @@ -19,7 +19,7 @@ export type CoreJobStatus = JobStatus; * null, never zero. The client reads a service status it does not recognise * as `unknown`, which is never shown as running. */ -export type CoreMetrics = Omit & { service: Omit & { status: ServiceStateStatus | "unknown" } }; +export type CoreMetrics = Omit & { service: Omit & { status: ServiceStatus | "unknown" } }; type Json = Record; @@ -67,7 +67,7 @@ export function projectCoreMetrics(value: unknown): CoreMetrics { const database = optional(body.database); const pool = optional(database.pool); const process = optional(body.process); - const status = isOneOf(serviceStateStatusValues, service.status) ? service.status : "unknown"; + const status = isOneOf(serviceStatusValues, service.status) ? service.status : "unknown"; const resolution = number(range.resolution_seconds); if (resolution === null || resolution <= 0) throw new AgentCoreError("Core metrics: range.resolution_seconds is missing.", 0, "invalid_response"); return { diff --git a/packages/agents-client/src/execution-configuration-projection.ts b/packages/agents-client/src/execution-configuration-projection.ts index efaca3016..e0a1506cc 100644 --- a/packages/agents-client/src/execution-configuration-projection.ts +++ b/packages/agents-client/src/execution-configuration-projection.ts @@ -1,8 +1,8 @@ import { modelProviderProtocols } from "./harness-catalog"; import { modelProviderViewFields } from "./generated/public-api"; import { - executionHarnessConfigSelectionFields, executionProviderSelectionFields, executionSelectionFields, executionSelectionSourceValues, - sessionExecutionConfigurationFields, type ExecutionSelectionSource, + executionHarnessConfigSelectionFields, executionProviderSelectionFields, executionSelectionFields, executionSourceValues, + sessionExecutionConfigurationFields, type ExecutionSource, } from "./generated/core-api"; import { canonicalUuid, exactFields, isNonnegativeInteger, isOneOf, isRecord, onlyFields, sameResourceId } from "./response-projection"; import type { ModelProviderView, SessionExecutionConfiguration } from "./types"; @@ -11,10 +11,10 @@ type Invalid = () => never; const protocols: ReadonlySet = new Set(modelProviderProtocols); function selection(value: unknown, invalid: Invalid): SessionExecutionConfiguration["model"] { - if (!isRecord(value) || !exactFields(value, executionSelectionFields) || !isOneOf(executionSelectionSourceValues, value.source) || + if (!isRecord(value) || !exactFields(value, executionSelectionFields) || !isOneOf(executionSourceValues, value.source) || (value.value !== null && (typeof value.value !== "string" || value.value.length === 0)) || (value.value === null && value.source !== "unknown")) return invalid(); - return { value: value.value as string | null, source: value.source as ExecutionSelectionSource }; + return { value: value.value as string | null, source: value.source as ExecutionSource }; } // Splits an absolute URL as RFC 3986 appendix B does, without parsing its host. @@ -56,7 +56,7 @@ export function projectExecutionConfiguration(value: unknown, sessionId: string, !isRecord(value.model_provider) || !exactFields(value.model_provider, executionProviderSelectionFields)) return invalid(); const native = value.harness_config; if (!isRecord(native) || !exactFields(native, executionHarnessConfigSelectionFields) || !isRecord(native.value) || - !isOneOf(executionSelectionSourceValues, native.source) || (native.source === "unknown" && Object.keys(native.value).length !== 0)) return invalid(); + !isOneOf(executionSourceValues, native.source) || (native.source === "unknown" && Object.keys(native.value).length !== 0)) return invalid(); const provider = value.model_provider; let configuration: ModelProviderView | null = null; if (provider.status === "available" && (provider.source === "session" || provider.source === "agent" || provider.source === "deployment")) { @@ -66,7 +66,7 @@ export function projectExecutionConfiguration(value: unknown, sessionId: string, return { object: "agent.session.execution_configuration", schema_version: 1, session_id: value.session_id, model: selection(value.model, invalid), harness: selection(value.harness, invalid), - harness_config: { value: { ...native.value }, source: native.source as ExecutionSelectionSource }, - model_provider: { source: provider.source as ExecutionSelectionSource, status: provider.status as SessionExecutionConfiguration["model_provider"]["status"], configuration }, + harness_config: { value: { ...native.value }, source: native.source as ExecutionSource }, + model_provider: { source: provider.source as ExecutionSource, status: provider.status as SessionExecutionConfiguration["model_provider"]["status"], configuration }, }; } diff --git a/packages/agents-client/src/generated/core-api.ts b/packages/agents-client/src/generated/core-api.ts index 34497e5bc..ef70621b7 100644 --- a/packages/agents-client/src/generated/core-api.ts +++ b/packages/agents-client/src/generated/core-api.ts @@ -1,6 +1,8 @@ // Code generated by scripts/generate-public-api.py from contracts/agents-api/core.openapi.yaml; DO NOT EDIT. import type { ModelProviderInput, ModelProviderView, TokenUsageResource } from "./public-api"; +export const actionValues = ["create", "update", "delete", "send_events", "upload_file", "upload_version", "update_default_version"] as const; +export type Action = (typeof actionValues)[number]; export interface AddressBindings { hosted_sandboxes: number; nodes: number; @@ -171,6 +173,8 @@ export interface DatabaseBucket { start: string; } export const databaseBucketFields = ["ping_p95_ms", "pool_in_use", "start"] as const; +export const deploymentModeValues = ["", "nodes", "direct"] as const; +export type DeploymentMode = (typeof deploymentModeValues)[number]; export interface DeploymentResources { allocations: number; pending: number; @@ -189,7 +193,7 @@ export interface DeploymentView { generation: number; installation_id: string; metadata?: Record; - mode: DeploymentViewMode; + mode: DeploymentMode; owner_epoch: number; provider: string; reset: Reset | null; @@ -201,8 +205,6 @@ export interface DeploymentView { } export const deploymentViewFields = ["configuration", "core_url", "credential_configured", "generation", "installation_id", "metadata", "mode", "owner_epoch", "provider", "reset", "resources", "rollout", "specification", "specification_digest", "suspension"] as const; export const deploymentViewRequired = ["core_url", "credential_configured", "generation", "installation_id", "mode", "owner_epoch", "provider", "reset", "resources", "rollout", "suspension"] as const; -export const deploymentViewModeValues = ["", "nodes", "direct"] as const; -export type DeploymentViewMode = (typeof deploymentViewModeValues)[number]; export interface DiagnosticFailure { code: DiagnosticFailureCode; failed_at: string | null; @@ -211,6 +213,8 @@ export interface DiagnosticFailure { export const diagnosticFailureFields = ["code", "failed_at", "params"] as const; export const diagnosticFailureCodeValues = ["harness_error", "model_provider_required", "runtime_unavailable", "runtime_disconnected", "runtime_preparation_failed", "execution_interrupted", "delivery_unconfirmed", "input_rejected", "executor_protocol_error", "core_storage_failed", "internal_error", "environment_connection_timeout", "environment_unavailable", "environment_provisioning_failed", "authentication_error", "rate_limit_exceeded", "usage_limit_exceeded", "server_overloaded", "server_error", "invalid_request", "resource_not_found", "request_timeout", "context_length_exceeded", "cyber_policy", "connection_failed"] as const; export type DiagnosticFailureCode = (typeof diagnosticFailureCodeValues)[number]; +export const diagnosticSourceValues = ["turn", "environment", "environment_input"] as const; +export type DiagnosticSource = (typeof diagnosticSourceValues)[number]; export interface EnvironmentExecutorCredentialRequest { key_id: string; rotate?: boolean; @@ -239,29 +243,25 @@ export interface ExecutionBucket { } export const executionBucketFields = ["in_progress", "queue_wait_p95_ms", "queued", "start"] as const; export interface ExecutionHarnessConfigSelection { - source: ExecutionHarnessConfigSelectionSource; + source: ExecutionSource; value: Record; } export const executionHarnessConfigSelectionFields = ["source", "value"] as const; -export const executionHarnessConfigSelectionSourceValues = ["session", "agent", "deployment", "unknown"] as const; -export type ExecutionHarnessConfigSelectionSource = (typeof executionHarnessConfigSelectionSourceValues)[number]; export interface ExecutionProviderSelection { configuration: ModelProviderView | null; - source: ExecutionProviderSelectionSource; - status: ExecutionProviderSelectionStatus; + source: ExecutionSource; + status: ExecutionProviderStatus; } export const executionProviderSelectionFields = ["configuration", "source", "status"] as const; -export const executionProviderSelectionSourceValues = ["session", "agent", "deployment", "unknown"] as const; -export type ExecutionProviderSelectionSource = (typeof executionProviderSelectionSourceValues)[number]; -export const executionProviderSelectionStatusValues = ["available", "redacted", "unavailable"] as const; -export type ExecutionProviderSelectionStatus = (typeof executionProviderSelectionStatusValues)[number]; +export const executionProviderStatusValues = ["available", "redacted", "unavailable"] as const; +export type ExecutionProviderStatus = (typeof executionProviderStatusValues)[number]; export interface ExecutionSelection { - source: ExecutionSelectionSource; + source: ExecutionSource; value: string | null; } export const executionSelectionFields = ["source", "value"] as const; -export const executionSelectionSourceValues = ["session", "agent", "deployment", "unknown"] as const; -export type ExecutionSelectionSource = (typeof executionSelectionSourceValues)[number]; +export const executionSourceValues = ["session", "agent", "deployment", "unknown"] as const; +export type ExecutionSource = (typeof executionSourceValues)[number]; export interface ExecutorConnection { bound_key_id: string | null; enrolled_at: string | null; @@ -286,7 +286,7 @@ export interface HarnessModelConfiguration { harness: HarnessModelConfigurationHarness; harness_config: Record; last_error_at: string | null; - last_error_code: HarnessModelConfigurationLastErrorCode | null; + last_error_code: ProviderErrorCode | null; last_used_at: string | null; model: string; model_provider: ModelProviderView; @@ -296,8 +296,6 @@ export interface HarnessModelConfiguration { export const harnessModelConfigurationFields = ["harness", "harness_config", "last_error_at", "last_error_code", "last_used_at", "model", "model_provider", "object", "updated_at"] as const; export const harnessModelConfigurationHarnessValues = ["claude_sdk", "codex", "mcode"] as const; export type HarnessModelConfigurationHarness = (typeof harnessModelConfigurationHarnessValues)[number]; -export const harnessModelConfigurationLastErrorCodeValues = ["authentication_error", "connection_failed", "rate_limit_exceeded", "usage_limit_exceeded", "server_overloaded", "server_error", "resource_not_found", "request_timeout", "invalid_request"] as const; -export type HarnessModelConfigurationLastErrorCode = (typeof harnessModelConfigurationLastErrorCodeValues)[number]; export interface HostHistory { points: HostHistoryPoint[]; resolution_seconds: number; @@ -325,19 +323,19 @@ export interface InstallationConfiguration { settings: InstallationSetting[]; } export const installationConfigurationFields = ["settings"] as const; +export const installationServiceValues = ["core", "web"] as const; +export type InstallationService = (typeof installationServiceValues)[number]; export interface InstallationSetting { changeable: boolean; configured?: boolean; default: unknown | null; key: string; - restarts: InstallationSettingRestarts[]; + restarts: InstallationService[]; sensitive: boolean; value: unknown | null; } export const installationSettingFields = ["changeable", "configured", "default", "key", "restarts", "sensitive", "value"] as const; export const installationSettingRequired = ["changeable", "default", "key", "restarts", "sensitive", "value"] as const; -export const installationSettingRestartsValues = ["core", "web"] as const; -export type InstallationSettingRestarts = (typeof installationSettingRestartsValues)[number]; export interface IssuedAPIKey { created_at: string; id: string; @@ -411,7 +409,7 @@ export interface Node { core_url: string; cpu_count: number | null; created_at: string; - diagnostic?: NodeDiagnostic; + diagnostic?: NodeDiagnosticCode; enrollment_id: string | null; id: string; last_seen_at: string | null; @@ -454,7 +452,7 @@ export interface NodeDetail { core_url: string; cpu_count: number | null; created_at: string; - diagnostic?: NodeDetailDiagnostic; + diagnostic?: NodeDiagnosticCode; enrollment_id: string | null; history: HostHistory; host: NodeHost; @@ -474,10 +472,8 @@ export interface NodeDetail { } export const nodeDetailFields = ["active", "available_disk_bytes", "available_memory_bytes", "cleanup_pending", "core_url", "cpu_count", "created_at", "diagnostic", "enrollment_id", "history", "host", "id", "last_seen_at", "max_active", "max_retained", "name", "online", "provider", "provider_ready", "reserved", "retained", "rollout", "running", "snapshots"] as const; export const nodeDetailRequired = ["active", "available_disk_bytes", "available_memory_bytes", "cleanup_pending", "core_url", "cpu_count", "created_at", "enrollment_id", "history", "host", "id", "last_seen_at", "max_active", "max_retained", "name", "online", "provider", "provider_ready", "reserved", "retained", "rollout", "running", "snapshots"] as const; -export const nodeDetailDiagnosticValues = ["provider_unavailable", "host_unsupported", "artifacts_unavailable", "runtime_download_failed", "runtime_image_unavailable", "capacity_insufficient"] as const; -export type NodeDetailDiagnostic = (typeof nodeDetailDiagnosticValues)[number]; -export const nodeDiagnosticValues = ["provider_unavailable", "host_unsupported", "artifacts_unavailable", "runtime_download_failed", "runtime_image_unavailable", "capacity_insufficient"] as const; -export type NodeDiagnostic = (typeof nodeDiagnosticValues)[number]; +export const nodeDiagnosticCodeValues = ["provider_unavailable", "host_unsupported", "artifacts_unavailable", "runtime_download_failed", "runtime_image_unavailable", "capacity_insufficient"] as const; +export type NodeDiagnosticCode = (typeof nodeDiagnosticCodeValues)[number]; export interface NodeHost { available_disk_bytes: number | null; available_memory_bytes: number | null; @@ -488,14 +484,12 @@ export interface NodeHost { } export const nodeHostFields = ["available_disk_bytes", "available_memory_bytes", "cpu_utilization", "effective_cpu_cores", "observed_at", "total_memory_bytes"] as const; export interface NodeRollout { - diagnostic?: NodeRolloutDiagnostic; + diagnostic?: NodeDiagnosticCode; ready_generation: number | null; state: NodeRolloutState; } export const nodeRolloutFields = ["diagnostic", "ready_generation", "state"] as const; export const nodeRolloutRequired = ["ready_generation", "state"] as const; -export const nodeRolloutDiagnosticValues = ["provider_unavailable", "host_unsupported", "artifacts_unavailable", "runtime_download_failed", "runtime_image_unavailable", "capacity_insufficient"] as const; -export type NodeRolloutDiagnostic = (typeof nodeRolloutDiagnosticValues)[number]; export const nodeRolloutStateValues = ["ready", "preparing", "failed", "update_required", "unknown"] as const; export type NodeRolloutState = (typeof nodeRolloutStateValues)[number]; export interface NodeUpdate { @@ -557,6 +551,8 @@ export interface ProjectsPage { has_more: boolean; } export const projectsPageFields = ["data", "has_more"] as const; +export const providerErrorCodeValues = ["authentication_error", "connection_failed", "rate_limit_exceeded", "usage_limit_exceeded", "server_overloaded", "server_error", "resource_not_found", "request_timeout", "invalid_request"] as const; +export type ProviderErrorCode = (typeof providerErrorCodeValues)[number]; export interface Range { end: string; resolution_seconds: number; @@ -564,15 +560,15 @@ export interface Range { } export const rangeFields = ["end", "resolution_seconds", "start"] as const; export interface Reset { - clear: ResetClear; + clear: ResetMode; deadline_at: string | null; forced_at: string | null; remaining: ResetRemaining; requested_at: string; } export const resetFields = ["clear", "deadline_at", "forced_at", "remaining", "requested_at"] as const; -export const resetClearValues = ["auto", "force"] as const; -export type ResetClear = (typeof resetClearValues)[number]; +export const resetModeValues = ["auto", "force"] as const; +export type ResetMode = (typeof resetModeValues)[number]; export interface ResetOfflineNode { name: string; node_id: string; @@ -588,14 +584,12 @@ export interface ResetRemaining { } export const resetRemainingFields = ["busy", "cleanup", "idle", "offline_nodes", "on_offline_nodes"] as const; export interface ResetRequest { - clear: ResetRequestClear; + clear: ResetMode; deadline_seconds?: number; expected_generation: number; } export const resetRequestFields = ["clear", "deadline_seconds", "expected_generation"] as const; export const resetRequestRequired = ["clear", "expected_generation"] as const; -export const resetRequestClearValues = ["auto", "force"] as const; -export type ResetRequestClear = (typeof resetRequestClearValues)[number]; export interface ResourceOwner { api_key: WriteauditAPIKey | null; resource_id: string; @@ -605,6 +599,8 @@ export interface ResourceOwnerList { data: ResourceOwner[]; } export const resourceOwnerListFields = ["data"] as const; +export const resourceTypeValues = ["agent", "session", "environment", "environment_template", "skill", "skill_version", "file", "vault", "credential", "artifact"] as const; +export type ResourceType = (typeof resourceTypeValues)[number]; export interface Rollout { nodes: RolloutNodes | null; previous_generation_sandboxes: number; @@ -823,24 +819,22 @@ export interface ServiceState { execution_owner: boolean | null; revision: string | null; started_at: string | null; - status: ServiceStateStatus; + status: ServiceStatus; } export const serviceStateFields = ["execution_owner", "revision", "started_at", "status"] as const; -export const serviceStateStatusValues = ["running", "degraded"] as const; -export type ServiceStateStatus = (typeof serviceStateStatusValues)[number]; +export const serviceStatusValues = ["running", "degraded"] as const; +export type ServiceStatus = (typeof serviceStatusValues)[number]; export interface SessionDiagnosticFailure { code: SessionDiagnosticFailureCode; failed_at: string | null; params: Record; - source: SessionDiagnosticFailureSource; + source: DiagnosticSource; turn_id?: string; } export const sessionDiagnosticFailureFields = ["code", "failed_at", "params", "source", "turn_id"] as const; export const sessionDiagnosticFailureRequired = ["code", "failed_at", "params", "source"] as const; export const sessionDiagnosticFailureCodeValues = ["harness_error", "model_provider_required", "runtime_unavailable", "runtime_disconnected", "runtime_preparation_failed", "execution_interrupted", "delivery_unconfirmed", "input_rejected", "executor_protocol_error", "core_storage_failed", "internal_error", "environment_connection_timeout", "environment_unavailable", "environment_provisioning_failed", "authentication_error", "rate_limit_exceeded", "usage_limit_exceeded", "server_overloaded", "server_error", "invalid_request", "resource_not_found", "request_timeout", "context_length_exceeded", "cyber_policy", "connection_failed"] as const; export type SessionDiagnosticFailureCode = (typeof sessionDiagnosticFailureCodeValues)[number]; -export const sessionDiagnosticFailureSourceValues = ["turn", "environment", "environment_input"] as const; -export type SessionDiagnosticFailureSource = (typeof sessionDiagnosticFailureSourceValues)[number]; export interface SessionDiagnostics { failure: SessionDiagnosticFailure | null; object: "core.session_diagnostics"; @@ -884,21 +878,17 @@ export interface WriteauditAPIKey { } export const writeauditAPIKeyFields = ["id", "kind", "name", "prefix", "revoked_at"] as const; export interface WriteauditOperation { - action: WriteauditOperationAction; + action: Action; api_key: WriteauditAPIKey; created_at: string; id: string; parent_id: string; request_id: string; resource_id: string; - resource_type: WriteauditOperationResourceType; + resource_type: ResourceType; trace_id: string; } export const writeauditOperationFields = ["action", "api_key", "created_at", "id", "parent_id", "request_id", "resource_id", "resource_type", "trace_id"] as const; -export const writeauditOperationActionValues = ["create", "update", "delete", "send_events", "upload_file", "upload_version", "update_default_version"] as const; -export type WriteauditOperationAction = (typeof writeauditOperationActionValues)[number]; -export const writeauditOperationResourceTypeValues = ["agent", "session", "environment", "environment_template", "skill", "skill_version", "file", "vault", "credential", "artifact"] as const; -export type WriteauditOperationResourceType = (typeof writeauditOperationResourceTypeValues)[number]; export interface WriteauditPage { data: WriteauditOperation[]; has_more: boolean; diff --git a/packages/agents-client/src/generated/public-api.ts b/packages/agents-client/src/generated/public-api.ts index 4edc43fd2..80d6fa6c0 100644 --- a/packages/agents-client/src/generated/public-api.ts +++ b/packages/agents-client/src/generated/public-api.ts @@ -352,7 +352,7 @@ export interface EnvironmentInstallation { commands?: Record; expires_at?: number; message?: string; - status?: "available" | "unavailable"; + status?: InstallationStatus; version?: string; } export const environmentInstallationFields = ["commands", "expires_at", "message", "status", "version"] as const; @@ -639,6 +639,8 @@ export interface InputTokensDetailsResource { cached_tokens: number; } export const inputTokensDetailsResourceFields = ["cached_tokens"] as const; +export const installationStatusValues = ["available", "unavailable"] as const; +export type InstallationStatus = (typeof installationStatusValues)[number]; export interface InterruptSubagentCallItemResource { type: "interrupt_subagent_call"; id: string; @@ -768,7 +770,7 @@ export interface ModelProviderInput { base_url: string; context_window?: number; max_output_tokens?: number; - protocol: "anthropic" | "responses" | "chat_completions"; + protocol: Protocol; } export const modelProviderInputFields = ["api_key", "base_url", "context_window", "max_output_tokens", "protocol"] as const; export const modelProviderInputRequired = ["api_key", "base_url", "protocol"] as const; @@ -777,7 +779,7 @@ export interface ModelProviderView { base_url: string; context_window?: number; max_output_tokens?: number; - protocol: "anthropic" | "responses" | "chat_completions"; + protocol: Protocol; } export const modelProviderViewFields = ["api_key_configured", "base_url", "context_window", "max_output_tokens", "protocol"] as const; export const modelProviderViewRequired = ["api_key_configured", "base_url", "protocol"] as const; @@ -943,6 +945,8 @@ export interface PersistedMcpTransportResourceStdio { env_vars: string[]; } export const persistedMcpTransportResourceStdioFields = ["type", "command", "args", "cwd", "env_vars"] as const; +export const protocolValues = ["anthropic", "responses", "chat_completions"] as const; +export type Protocol = (typeof protocolValues)[number]; export interface PublicEnvironmentResource { id: string; object: "agent.environment"; diff --git a/packages/agents-client/src/sandbox-client.ts b/packages/agents-client/src/sandbox-client.ts index 1cdc84d2d..853bc4eeb 100644 --- a/packages/agents-client/src/sandbox-client.ts +++ b/packages/agents-client/src/sandbox-client.ts @@ -3,25 +3,25 @@ import { CoreRequester, type CoreClientOptions } from "./core-request"; import { deploymentContract } from "./deployment-contract"; import { hasOwn, isNonnegativeInteger, isOneOf, isRecord, onlyFields, sameResourceId, schemaFields } from "./response-projection"; import { - deploymentResourcesFields, deploymentSpecFields, deploymentSpecRequired, deploymentViewFields, deploymentViewModeValues, deploymentViewRequired, + deploymentResourcesFields, deploymentSpecFields, deploymentSpecRequired, deploymentViewFields, deploymentModeValues, deploymentViewRequired, hostHistoryFields, hostHistoryPointFields, nodeAllocationDiagnosticValues, nodeAllocationFields, nodeDetailFields, nodeDetailRequired, - nodeDiagnosticValues, nodeFields, nodeHostFields, nodeRequired, nodeRolloutFields, nodeRolloutRequired, nodeRolloutStateValues, resetClearValues, + nodeDiagnosticCodeValues, nodeFields, nodeHostFields, nodeRequired, nodeRolloutFields, nodeRolloutRequired, nodeRolloutStateValues, resetModeValues, resetFields, resetOfflineNodeFields, resetRemainingFields, rolloutFields, rolloutNodesFields, rolloutStateValues, runtimeReleaseFields, sandboxAllocationListFields, sandboxNodeListFields, sandboxResourcesFields, sandboxResourcesRequired, suspensionFields, - type DeploymentSpec, type DeploymentView, type HostHistoryPoint, type Node, type NodeAllocation, type NodeDetail, type NodeDiagnostic, + type DeploymentSpec, type DeploymentView, type HostHistoryPoint, type Node, type NodeAllocation, type NodeDetail, type NodeDiagnosticCode, type NodeHost, type NodeRollout, type NodeUpdate, type Reset, type ResetOfflineNode, type ResetRequest, type Rollout, type RuntimeRelease, type SandboxDeploymentInput, type SandboxEnrollmentToken, type SandboxResources as SandboxResourcesResource, } from "./generated/core-api"; import type { ReadOptions } from "./types"; /** Checked against Core's shared node-diagnostics.json fixture. */ -export const sandboxNodeDiagnostics = nodeDiagnosticValues; +export const sandboxNodeDiagnostics = nodeDiagnosticCodeValues; /** Fixed reason a node's provider is not ready. Core omits the field while the provider is ready, so read it as falsy (undefined) then. The client reads an unknown future value as provider_unavailable. */ -export type SandboxNodeDiagnostic = NodeDiagnostic; +export type SandboxNodeDiagnostic = NodeDiagnosticCode; /** Keep a known readiness cause; never expose unclassified node-supplied text. */ export function normalizeSandboxNodeDiagnostic(value: string): SandboxNodeDiagnostic { - return isOneOf(nodeDiagnosticValues, value) ? value : "provider_unavailable"; + return isOneOf(nodeDiagnosticCodeValues, value) ? value : "provider_unavailable"; } /** A registered Provider kind; deploymentContract.providers holds each one's declaration. */ @@ -121,7 +121,7 @@ function projectReset(value: unknown, held: number): SandboxReset | null { if (value === null) return null; const reset = members(value, resetFields); const remaining = members(reset.remaining, resetRemainingFields); - valid(isOneOf(resetClearValues, reset.clear) && timestamp(reset.requested_at) && + valid(isOneOf(resetModeValues, reset.clear) && timestamp(reset.requested_at) && nullable(timestamp)(reset.deadline_at) && nullable(timestamp)(reset.forced_at) && (reset.clear === "auto" ? reset.deadline_at !== null && reset.forced_at === null : reset.forced_at !== null) && [remaining.busy, remaining.idle, remaining.cleanup, remaining.on_offline_nodes].every(isNonnegativeInteger) && Array.isArray(remaining.offline_nodes)); @@ -161,7 +161,7 @@ function projectDeployment(value: unknown): SandboxDeployment { const resources = members(deployment.resources, deploymentResourcesFields); const suspension = deployment.suspension === null ? null : members(deployment.suspension, suspensionFields); const configured = hasOwn(deployment, "specification"); - valid(strings(deployment, ["installation_id", "core_url"]) && (deployment.provider === "" || (typeof deployment.provider === "string" && hasOwn(deploymentContract.providers, deployment.provider))) && isOneOf(deploymentViewModeValues, deployment.mode) && + valid(strings(deployment, ["installation_id", "core_url"]) && (deployment.provider === "" || (typeof deployment.provider === "string" && hasOwn(deploymentContract.providers, deployment.provider))) && isOneOf(deploymentModeValues, deployment.mode) && [deployment.owner_epoch, deployment.generation, resources.allocations, resources.pending].every(isNonnegativeInteger) && (suspension === null || [suspension.idle_seconds, suspension.retention_seconds].every(isNonnegativeInteger)) && configured === hasOwn(deployment, "specification_digest") && (!configured || (typeof deployment.specification_digest === "string" && deployment.specification_digest !== ""))); diff --git a/packages/agents-client/src/session-diagnostics.ts b/packages/agents-client/src/session-diagnostics.ts index f2793ca82..27ed6925d 100644 --- a/packages/agents-client/src/session-diagnostics.ts +++ b/packages/agents-client/src/session-diagnostics.ts @@ -3,7 +3,7 @@ import { invalidAdminResponse } from "./admin-projection"; import { turnStatusResourceValues } from "./generated/public-api"; import { diagnosticFailureFields, itemDiagnosticTimingFields, sessionDiagnosticFailureFields, sessionDiagnosticFailureRequired, - sessionDiagnosticFailureSourceValues, sessionDiagnosticsFields, sessionDiagnosticsStatusValues, turnDiagnosticsFields, + diagnosticSourceValues, sessionDiagnosticsFields, sessionDiagnosticsStatusValues, turnDiagnosticsFields, type DiagnosticFailure as DiagnosticFailureResource, type DiagnosticFailureCode, type ItemDiagnosticTiming, type SessionDiagnosticFailure as SessionDiagnosticFailureResource, type SessionDiagnostics as SessionDiagnosticsResource, type TurnDiagnostics as TurnDiagnosticsResource, @@ -50,7 +50,7 @@ export function projectSessionDiagnostics(value: unknown, sessionId: string): Se canonicalUuid(value.session_id) === null || !sameResourceId(value.session_id as string, sessionId) || !isOneOf(sessionDiagnosticsStatusValues, value.status)) return invalidAdminResponse(); let projected: SessionDiagnosticFailure | null = null; if (value.status === "failed") { - if (!isRecord(value.failure) || !isOneOf(sessionDiagnosticFailureSourceValues, value.failure.source)) return invalidAdminResponse(); + if (!isRecord(value.failure) || !isOneOf(diagnosticSourceValues, value.failure.source)) return invalidAdminResponse(); const f = value.failure; const source = value.failure.source; projected = { ...failure(f, source === "turn" ? turnCodes : source === "environment_input" ? inputCodes : environmentCodes, true), source }; diff --git a/scripts/generate-public-api.test.py b/scripts/generate-public-api.test.py index dd039c572..48ebbed83 100644 --- a/scripts/generate-public-api.test.py +++ b/scripts/generate-public-api.test.py @@ -23,7 +23,7 @@ def test_generated_types_are_current(self): def test_public_projection_is_current_and_keeps_source_immutable(self): source = copy.deepcopy(self.source) - extensions = {k: v for k, v in self.public['components']['schemas'].items() if k.startswith('v1.')} + extensions = {k: v for k, v in self.public['components']['schemas'].items() if k not in source['components']['schemas']} actual = generator.public_document(source, extensions, generator.extension_owners(self.bindings), self.pin["beta_header"]) self.assertEqual(actual, self.public) self.assertEqual(source, self.source) diff --git a/services/core/cmd/server/managed_generations.go b/services/core/cmd/server/managed_generations.go index e509ec494..b2a87357d 100644 --- a/services/core/cmd/server/managed_generations.go +++ b/services/core/cmd/server/managed_generations.go @@ -7,9 +7,8 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/providercontract" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/providercontract" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimeobs" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" ) @@ -98,7 +97,7 @@ func (p *generationRouter) Observe(ctx context.Context, t runtimeobs.Target) (ru // routeGenerations routes a direct provider's allocations through their own // generations. Node providers route through the node transport instead. func (s *managedSetup) routeGenerations(candidate execution.PreparedRuntimeDeployment, setup deployment.Setup) (execution.PreparedRuntimeDeployment, error) { - if setup.Mode == "nodes" { + if setup.Mode == string(sandbox.DeploymentNodes) { return candidate, nil } candidate.Config.Provider = &generationRouter{setup: s, operations: candidate.Config.Provider.ProviderOperations()} diff --git a/services/core/cmd/server/managed_nodes.go b/services/core/cmd/server/managed_nodes.go index 518dcd37b..b908bbab6 100644 --- a/services/core/cmd/server/managed_nodes.go +++ b/services/core/cmd/server/managed_nodes.go @@ -77,5 +77,5 @@ func configureManagedNodes(nodes *deployment.Service, reader deployment.Reader, } func nodeHealthRecord(health node.Health) deployment.NodeHealth { - return deployment.NodeHealth{Host: &deployment.NodeHost{EffectiveCPUCores: health.EffectiveCPUCores, CPUUtilization: health.CPUUtilization, TotalMemoryBytes: health.TotalMemoryBytes, AvailableMemoryBytes: health.AvailableMemoryBytes, AvailableDiskBytes: health.AvailableDiskBytes, ObservedAt: &health.ObservedAt}, ProviderReady: health.ProviderReady, Diagnostic: health.Diagnostic, CPUCount: health.CPUCount, AvailableMemoryBytes: health.AvailableMemoryBytes, AvailableDiskBytes: health.AvailableDiskBytes} + return deployment.NodeHealth{Host: &deployment.NodeHost{EffectiveCPUCores: health.EffectiveCPUCores, CPUUtilization: health.CPUUtilization, TotalMemoryBytes: health.TotalMemoryBytes, AvailableMemoryBytes: health.AvailableMemoryBytes, AvailableDiskBytes: health.AvailableDiskBytes, ObservedAt: &health.ObservedAt}, ProviderReady: health.ProviderReady, Diagnostic: sandbox.NodeDiagnosticCode(health.Diagnostic), CPUCount: health.CPUCount, AvailableMemoryBytes: health.AvailableMemoryBytes, AvailableDiskBytes: health.AvailableDiskBytes} } diff --git a/services/core/cmd/server/managed_setup.go b/services/core/cmd/server/managed_setup.go index 81a9ffdef..570717417 100644 --- a/services/core/cmd/server/managed_setup.go +++ b/services/core/cmd/server/managed_setup.go @@ -175,7 +175,7 @@ func (s *managedSetup) observationSource(ctx context.Context) (runtimeobs.Source // provider builds the setup's provider. The setup carries the mode and // declared operations that deployment read from the provider's registration. func (s *managedSetup) provider(setup deployment.Setup) (sandbox.SandboxProvider, error) { - if setup.Mode == "nodes" { + if setup.Mode == string(sandbox.DeploymentNodes) { if s.hub == nil { return nil, errors.New("sandbox node transport is unavailable") } diff --git a/services/core/internal/api/environment_executor_management.go b/services/core/internal/api/environment_executor_management.go index 8e20745e8..eb7650945 100644 --- a/services/core/internal/api/environment_executor_management.go +++ b/services/core/internal/api/environment_executor_management.go @@ -13,6 +13,14 @@ import ( "github.com/google/uuid" ) +type ExecutorConnectionStatus string + +const ( + ExecutorNeverEnrolled ExecutorConnectionStatus = "never_enrolled" + ExecutorConnected ExecutorConnectionStatus = "connected" + ExecutorDisconnected ExecutorConnectionStatus = "disconnected" +) + // 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 @@ -35,10 +43,10 @@ type ExecutorCredentialList struct { // ExecutorConnection reports binding history and current Core-observed connectivity. // Heartbeat times are observations, not execution or native readiness. type ExecutorConnection struct { - Status string `json:"status" binding:"required" enums:"never_enrolled,connected,disconnected"` - BoundKeyID *string `json:"bound_key_id" binding:"required" extensions:"x-nullable" format:"uuid"` - EnrolledAt *time.Time `json:"enrolled_at" binding:"required" format:"date-time" extensions:"x-nullable"` - LastSeenAt *time.Time `json:"last_seen_at" binding:"required" format:"date-time" extensions:"x-nullable"` + Status ExecutorConnectionStatus `json:"status" binding:"required"` + BoundKeyID *string `json:"bound_key_id" binding:"required" extensions:"x-nullable" format:"uuid"` + EnrolledAt *time.Time `json:"enrolled_at" binding:"required" format:"date-time" extensions:"x-nullable"` + LastSeenAt *time.Time `json:"last_seen_at" binding:"required" format:"date-time" extensions:"x-nullable"` } // registerExecutorCredentialRoutes adds executor credential issuance to the @@ -71,10 +79,10 @@ func (h *Handler) listExecutorCredentials(w http.ResponseWriter, r *http.Request writeSessionsError(w, r, err) return } - connection := ExecutorConnection{Status: "never_enrolled"} + connection := ExecutorConnection{Status: ExecutorNeverEnrolled} observed := state.Connection if observed.DeviceID != "" { - connection = ExecutorConnection{Status: "disconnected", BoundKeyID: observed.BoundKeyID, EnrolledAt: observed.EnrolledAt, LastSeenAt: observed.LastSeenAt} + connection = ExecutorConnection{Status: ExecutorDisconnected, BoundKeyID: observed.BoundKeyID, EnrolledAt: observed.EnrolledAt, LastSeenAt: observed.LastSeenAt} if observed.EnvironmentStatus == "connected" && observed.CredentialHash != "" { connected, err := h.ExecutorConnections.ExecutorConnected(r.Context(), state.EnvironmentID, observed.CredentialHash) if err != nil && !errors.Is(err, sessions.ErrNotFound) && !errors.Is(err, sessions.ErrDeviceBindingConflict) { @@ -82,7 +90,7 @@ func (h *Handler) listExecutorCredentials(w http.ResponseWriter, r *http.Request return } if err == nil && connected { - connection.Status = "connected" + connection.Status = ExecutorConnected } } } diff --git a/services/core/internal/api/environment_executor_management_test.go b/services/core/internal/api/environment_executor_management_test.go index 252a5bc83..3024a5e30 100644 --- a/services/core/internal/api/environment_executor_management_test.go +++ b/services/core/internal/api/environment_executor_management_test.go @@ -172,7 +172,7 @@ func TestExecutorConnectionListObservation(t *testing.T) { } if tc.status == 200 { var got ExecutorCredentialList - if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil || got.Connection.Status != tc.want || got.Connection.BoundKeyID == nil || *got.Connection.BoundKeyID != bound { + if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil || string(got.Connection.Status) != tc.want || got.Connection.BoundKeyID == nil || *got.Connection.BoundKeyID != bound { t.Fatal("projection", err, w.Body) } } diff --git a/services/core/internal/api/environment_installation.go b/services/core/internal/api/environment_installation.go index 3899bbf4d..4cdbab4d6 100644 --- a/services/core/internal/api/environment_installation.go +++ b/services/core/internal/api/environment_installation.go @@ -27,7 +27,7 @@ type NativeInstaller struct { func (h *Handler) installationFor(ctx context.Context, principal identity.Principal, environment string) (*v1.EnvironmentInstallation, error) { installer := h.Execution.NativeInstaller - result := &v1.EnvironmentInstallation{Status: "unavailable", Message: "This Core has no matching native installation distribution. Ask its operator to install the qualified release artifacts."} + result := &v1.EnvironmentInstallation{Status: v1.InstallationUnavailable, Message: "This Core has no matching native installation distribution. Ask its operator to install the qualified release artifacts."} if installer == nil { return result, nil } @@ -39,7 +39,7 @@ func (h *Handler) installationFor(ctx context.Context, principal identity.Princi if err != nil { return nil, err } - return &v1.EnvironmentInstallation{Status: "available", Version: installer.Version, ExpiresAt: expires, Commands: installer.Catalog.Commands(installer.Base, token)}, nil + return &v1.EnvironmentInstallation{Status: v1.InstallationAvailable, Version: installer.Version, ExpiresAt: expires, Commands: installer.Catalog.Commands(installer.Base, token)}, nil } func (h *Handler) addSessionInstallation(w http.ResponseWriter, r *http.Request, response *v1.Session) error { diff --git a/services/core/internal/api/handler.go b/services/core/internal/api/handler.go index 7d848e47e..dc4af3f1c 100644 --- a/services/core/internal/api/handler.go +++ b/services/core/internal/api/handler.go @@ -214,7 +214,7 @@ func (h *Handler) createSession(w http.ResponseWriter, r *http.Request) { } selectedEngine := h.Engine var provider *v1.ModelProviderInput - var providerSource string + var providerSource v1.ExecutionSource var deploymentRevision uuid.UUID if err == nil { selectedEngine, provider, providerSource, deploymentRevision, err = h.resolveSessionExecution(r.Context(), input, inheritedProvider, configuration) diff --git a/services/core/internal/api/harness_model_providers.go b/services/core/internal/api/harness_model_providers.go index 958d11485..57614a5e7 100644 --- a/services/core/internal/api/harness_model_providers.go +++ b/services/core/internal/api/harness_model_providers.go @@ -34,10 +34,10 @@ type HarnessModelConfiguration struct { Object string `json:"object" enums:"core.model_configuration" binding:"required"` Harness string `json:"harness" binding:"required"` v1.ModelConfigurationView - LastUsedAt *time.Time `json:"last_used_at" format:"date-time" extensions:"x-nullable" binding:"required"` - LastErrorCode *string `json:"last_error_code" extensions:"x-nullable" binding:"required" enums:"authentication_error,connection_failed,rate_limit_exceeded,usage_limit_exceeded,server_overloaded,server_error,resource_not_found,request_timeout,invalid_request"` - LastErrorAt *time.Time `json:"last_error_at" format:"date-time" extensions:"x-nullable" binding:"required"` - UpdatedAt time.Time `json:"updated_at" binding:"required"` + LastUsedAt *time.Time `json:"last_used_at" format:"date-time" extensions:"x-nullable" binding:"required"` + LastErrorCode *modelconfiguration.ProviderErrorCode `json:"last_error_code" extensions:"x-nullable" binding:"required"` + LastErrorAt *time.Time `json:"last_error_at" format:"date-time" extensions:"x-nullable" binding:"required"` + UpdatedAt time.Time `json:"updated_at" binding:"required"` } // CoreHarness describes one harness this build supports. Enabled and default diff --git a/services/core/internal/api/inputs.go b/services/core/internal/api/inputs.go index 5bc15f86e..8d300e5ed 100644 --- a/services/core/internal/api/inputs.go +++ b/services/core/internal/api/inputs.go @@ -9,6 +9,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/writeaudit" "github.com/go-chi/chi/v5" "github.com/google/uuid" ) @@ -50,7 +51,7 @@ func (h *Handler) createEvents(w http.ResponseWriter, r *http.Request) { writeSessionsError(w, r, err) return } - if !h.auditSessionOperation(w, r, chi.URLParam(r, "session_id"), "send_events") { + if !h.auditSessionOperation(w, r, chi.URLParam(r, "session_id"), string(writeaudit.ActionSendEvents)) { return } w.Header().Set("Cache-Control", "no-store") diff --git a/services/core/internal/api/installation.go b/services/core/internal/api/installation.go index e99e574ed..aae2c35d9 100644 --- a/services/core/internal/api/installation.go +++ b/services/core/internal/api/installation.go @@ -7,6 +7,13 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" ) +type InstallationService string + +const ( + InstallationCore InstallationService = "core" + InstallationWeb InstallationService = "web" +) + // Installation reports Core's installation facts. Core reads them from its // environment and build; configuration is the process settings it loaded. type Installation struct { @@ -43,7 +50,7 @@ type InstallationSetting struct { Changeable bool `json:"changeable" binding:"required"` Sensitive bool `json:"sensitive" binding:"required"` // Services that restart when the setting changes. - Restarts []string `json:"restarts" enums:"core,web" binding:"required"` + Restarts []InstallationService `json:"restarts" binding:"required"` } // InstallationBindings counts what is bound to the current public URL. diff --git a/services/core/internal/api/installation_test.go b/services/core/internal/api/installation_test.go index 8f65dcce9..b6e35df20 100644 --- a/services/core/internal/api/installation_test.go +++ b/services/core/internal/api/installation_test.go @@ -19,7 +19,7 @@ func TestInstallationReadNeedsOnlyTheCoreKey(t *testing.T) { fakes.projectsReader.resolveAPIKey = projectKeys(t, callerBinding()).ResolveAPIKey deps.CoreKeys = coreKeys(t, "administrator") public, id := "https://core.example", "5b7c0f3e-0000-4000-8000-000000000001" - settings := InstallationConfiguration{Settings: []InstallationSetting{{Key: "ports.core", Value: 8091, Default: 8091, Changeable: true, Restarts: []string{"core"}}}} + settings := InstallationConfiguration{Settings: []InstallationSetting{{Key: "ports.core", Value: 8091, Default: 8091, Changeable: true, Restarts: []InstallationService{"core"}}}} fakes.installationBindings.addressBindings = func(context.Context) (deployment.AddressBindings, error) { return deployment.AddressBindings{Nodes: 2, NodesOnOtherAddress: 1}, nil } diff --git a/services/core/internal/api/sandbox_deployment_setup.go b/services/core/internal/api/sandbox_deployment_setup.go index 5a26510b8..e24224db3 100644 --- a/services/core/internal/api/sandbox_deployment_setup.go +++ b/services/core/internal/api/sandbox_deployment_setup.go @@ -132,9 +132,9 @@ func (h *Handler) startSandboxReset(w http.ResponseWriter, r *http.Request) { return } var input struct { - ExpectedGeneration *uint64 `json:"expected_generation"` - Clear string `json:"clear"` - DeadlineSeconds *int32 `json:"deadline_seconds"` + ExpectedGeneration *uint64 `json:"expected_generation"` + Clear deployment.ResetMode `json:"clear"` + DeadlineSeconds *int32 `json:"deadline_seconds"` } if decodeInputObject(raw, &input, "expected_generation", "clear", "deadline_seconds") != nil || input.ExpectedGeneration == nil { writeError(w, http.StatusBadRequest, "invalid_request_error", "A current expected_generation is required.", "expected_generation") diff --git a/services/core/internal/api/saved_provider_test.go b/services/core/internal/api/saved_provider_test.go index 5624721c0..9c8a9bc0a 100644 --- a/services/core/internal/api/saved_provider_test.go +++ b/services/core/internal/api/saved_provider_test.go @@ -172,14 +172,14 @@ func TestSavedProviderProtocolHarnessMatrix(t *testing.T) { if path == "/v1/agents" { want = http.StatusCreated } - if response.Code != want || s.provider == nil || s.provider.Protocol != protocol || s.provider.APIKey != "saved-provider-secret" { + if response.Code != want || s.provider == nil || string(s.provider.Protocol) != protocol || s.provider.APIKey != "saved-provider-secret" { t.Fatalf("provider bundle rejected or changed: status=%d", response.Code) } assertSavedProviderRedacted(t, response.Body.String()) var result struct { Core v1.SavedAgentCore `json:"x_agents_core"` } - if json.Unmarshal(response.Body.Bytes(), &result) != nil || result.Core.Harness != harness || result.Core.ModelProvider == nil || result.Core.ModelProvider.Protocol != protocol { + if json.Unmarshal(response.Body.Bytes(), &result) != nil || result.Core.Harness != harness || result.Core.ModelProvider == nil || string(result.Core.ModelProvider.Protocol) != protocol { t.Fatal("safe view changed the selected harness or upstream protocol") } } diff --git a/services/core/internal/api/session_creation_identity.go b/services/core/internal/api/session_creation_identity.go index 8a3c96385..6225b74e1 100644 --- a/services/core/internal/api/session_creation_identity.go +++ b/services/core/internal/api/session_creation_identity.go @@ -6,8 +6,8 @@ import ( "net/http" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/writeaudit" ) // sessionCreationRequest records caller intent before mutable sources resolve: @@ -55,7 +55,7 @@ func (h *Handler) recoverSessionCreation(w http.ResponseWriter, r *http.Request, return true } if stream { - if !h.auditSessionOperation(w, r, result.Session.ID, "create") { + if !h.auditSessionOperation(w, r, result.Session.ID, string(writeaudit.ActionCreate)) { return true } // Recorded-intent lookup finds an existing creation, which sends no events. @@ -64,7 +64,7 @@ func (h *Handler) recoverSessionCreation(w http.ResponseWriter, r *http.Request, session, err := h.SessionsReader.GetSession(r.Context(), tenantID(r), result.Session.ID) if err != nil { writeSessionsError(w, r, err) - } else if h.auditSessionOperation(w, r, session.ID, "create") { + } else if h.auditSessionOperation(w, r, session.ID, string(writeaudit.ActionCreate)) { h.respondSessionStatus(w, r, session, http.StatusCreated) } } diff --git a/services/core/internal/api/session_diagnostics.go b/services/core/internal/api/session_diagnostics.go index 759e6aab4..dd5ad70d3 100644 --- a/services/core/internal/api/session_diagnostics.go +++ b/services/core/internal/api/session_diagnostics.go @@ -10,6 +10,14 @@ import ( "github.com/go-chi/chi/v5" ) +type DiagnosticSource string + +const ( + DiagnosticTurn DiagnosticSource = "turn" + DiagnosticEnvironment DiagnosticSource = "environment" + DiagnosticEnvironmentInput DiagnosticSource = "environment_input" +) + type DiagnosticFailure struct { Code string `json:"code" binding:"required" enums:"harness_error,model_provider_required,runtime_unavailable,runtime_disconnected,runtime_preparation_failed,execution_interrupted,delivery_unconfirmed,input_rejected,executor_protocol_error,core_storage_failed,internal_error,environment_connection_timeout,environment_unavailable,environment_provisioning_failed,authentication_error,rate_limit_exceeded,usage_limit_exceeded,server_overloaded,server_error,invalid_request,resource_not_found,request_timeout,context_length_exceeded,cyber_policy,connection_failed"` Params CoreErrorDetails `json:"params" swaggertype:"object" binding:"required"` @@ -18,8 +26,8 @@ type DiagnosticFailure struct { type SessionDiagnosticFailure struct { DiagnosticFailure - Source string `json:"source" binding:"required" enums:"turn,environment,environment_input"` - TurnID string `json:"turn_id,omitempty"` + Source DiagnosticSource `json:"source" binding:"required"` + TurnID string `json:"turn_id,omitempty"` } type SessionDiagnostics struct { @@ -82,12 +90,12 @@ func (h *Handler) getSessionDiagnostics(w http.ResponseWriter, r *http.Request) failure := SessionDiagnosticFailure{DiagnosticFailure: DiagnosticFailure{Code: "internal_error", Params: CoreErrorDetails{}}} switch { case session.EnvironmentFailure != nil: - failure.Source = "environment" + failure.Source = DiagnosticEnvironment failure.Code = "environment_provisioning_failed" failure.FailedAt = diagnosticTime(session.EnvironmentFailure.FailedAt) failure.Params = provisioningFailureParams(session.EnvironmentFailure.Detail) case session.EnvironmentInputActivity != nil: - failure.Source = "environment_input" + failure.Source = DiagnosticEnvironmentInput failure.FailedAt = diagnosticTime(session.EnvironmentInputActivity.LastActiveAt) switch session.EnvironmentInputActivity.Failure { case "": @@ -100,7 +108,7 @@ func (h *Handler) getSessionDiagnostics(w http.ResponseWriter, r *http.Request) failure.Code = "model_provider_required" } case session.LastTurn != nil: - failure.Source, failure.TurnID = "turn", session.LastTurn.ID + failure.Source, failure.TurnID = DiagnosticTurn, session.LastTurn.ID failure.DiagnosticFailure = *turnDiagnosticFailure(*session.LastTurn) } response.Failure = &failure diff --git a/services/core/internal/api/session_diagnostics_test.go b/services/core/internal/api/session_diagnostics_test.go index 3265feb8a..becdf229e 100644 --- a/services/core/internal/api/session_diagnostics_test.go +++ b/services/core/internal/api/session_diagnostics_test.go @@ -3,10 +3,12 @@ package api import ( "context" "encoding/json" + "reflect" "strings" "testing" "time" + 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/sessions" "github.com/google/uuid" @@ -122,3 +124,11 @@ func TestDiagnosticsHostedFailureOverridesInputWithoutParsingReason(t *testing.T } } } + +func TestDiagnosticStatusMatchesOfficialSession(t *testing.T) { + official, _ := reflect.TypeFor[v1.Session]().FieldByName("Status") + diagnostic, _ := reflect.TypeFor[SessionDiagnostics]().FieldByName("Status") + if diagnostic.Type != official.Type || diagnostic.Tag.Get("enums") != official.Tag.Get("enums") { + t.Fatal("diagnostics must preserve the pinned official Session status set") + } +} diff --git a/services/core/internal/api/session_execution_configuration.go b/services/core/internal/api/session_execution_configuration.go index 9acdc758c..6a14a7950 100644 --- a/services/core/internal/api/session_execution_configuration.go +++ b/services/core/internal/api/session_execution_configuration.go @@ -18,32 +18,32 @@ func sessionExecutionProjection(input sessionRequest, saved *v1.SavedAgent, inhe } `json:"agent"` } _ = json.Unmarshal(raw, &configuration) // The resolved configuration was already validated. - modelSource := "session" + modelSource := v1.ExecutionSourceSession if saved != nil && (input.Agent == nil || input.Agent.Model == nil) { - modelSource = "agent" + modelSource = v1.ExecutionSourceAgent } if input.modelSource != "" { modelSource = input.modelSource } - harnessSource := "deployment" + harnessSource := v1.ExecutionSourceDeployment if _, overridden := input.agentFields["x_agents_core"]; overridden { if input.Agent != nil && input.Agent.XAgentsCore != nil && input.Agent.XAgentsCore.Harness != "" { - harnessSource = "session" + harnessSource = v1.ExecutionSourceSession } else if input.Agent != nil && input.Agent.XAgentsCore != nil && saved != nil && saved.XAgentsCore != nil && saved.XAgentsCore.Harness != "" { - harnessSource = "agent" + harnessSource = v1.ExecutionSourceAgent } } else if saved != nil && saved.XAgentsCore != nil && saved.XAgentsCore.Harness != "" { - harnessSource = "agent" + harnessSource = v1.ExecutionSourceAgent } - selection := v1.ExecutionProviderSelection{Source: "unknown", Status: "unavailable"} + selection := v1.ExecutionProviderSelection{Source: v1.ExecutionSourceUnknown, Status: v1.ExecutionProviderUnavailable} if provider != nil { // Deployment defaults are readable with the same Core key, so new // Sessions record their safe view too; historical rows stay redacted. - selection.Source, selection.Status, selection.Configuration = "deployment", "available", provider.SafeView() + selection.Source, selection.Status, selection.Configuration = v1.ExecutionSourceDeployment, v1.ExecutionProviderAvailable, provider.SafeView() if input.XAgentsCore != nil && input.XAgentsCore.ModelProvider != nil { - selection.Source = "session" + selection.Source = v1.ExecutionSourceSession } else if inherited != nil { - selection.Source = "agent" + selection.Source = v1.ExecutionSourceAgent } } native := json.RawMessage(`{}`) @@ -52,7 +52,7 @@ func sessionExecutionProjection(input sessionRequest, saved *v1.SavedAgent, inhe } nativeSource := input.harnessConfigSource if nativeSource == "" { - nativeSource = "unknown" + nativeSource = v1.ExecutionSourceUnknown } return v1.SessionExecutionConfiguration{ HarnessConfig: v1.ExecutionHarnessConfigSelection{Value: native, Source: nativeSource}, diff --git a/services/core/internal/api/session_execution_configuration_test.go b/services/core/internal/api/session_execution_configuration_test.go index 6294695e4..c5cd4bcbd 100644 --- a/services/core/internal/api/session_execution_configuration_test.go +++ b/services/core/internal/api/session_execution_configuration_test.go @@ -40,7 +40,7 @@ func TestExecutionConfigurationSources(t *testing.T) { t.Fatal(err) } p := sessionExecutionProjection(input, tc.saved, tc.inherited, tc.provider, "codex", json.RawMessage(`{"agent":{"model":"resolved"}}`)) - if p.Model.Source != tc.modelSource || p.Harness.Source != tc.harnessSource || p.ModelProvider.Source != tc.providerSource || p.ModelProvider.Status != tc.status { + if string(p.Model.Source) != tc.modelSource || string(p.Harness.Source) != tc.harnessSource || string(p.ModelProvider.Source) != tc.providerSource || string(p.ModelProvider.Status) != tc.status { t.Fatalf("wrong sources: %#v", p) } raw, _ := json.Marshal(p) diff --git a/services/core/internal/api/session_model_configuration.go b/services/core/internal/api/session_model_configuration.go index 5134a4528..c40c82c18 100644 --- a/services/core/internal/api/session_model_configuration.go +++ b/services/core/internal/api/session_model_configuration.go @@ -34,7 +34,7 @@ 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 v1.ModelProviderAllowed(input.Environment.Type, v1.ModelProviderSourceDeployment) && ((inherited == nil && !explicitProvider) || needsModel) { + if v1.ModelProviderAllowed(input.Environment.Type, v1.ExecutionSourceDeployment) && ((inherited == nil && !explicitProvider) || needsModel) { input.deploymentDefaults, err = h.ModelProviders.Resolve(ctx, engine) if err != nil { var configurationError *v1.ModelProviderError @@ -44,9 +44,9 @@ func (h *Handler) prepareSessionModelConfiguration(ctx context.Context, input *s return &storedDataError{err} } } - input.modelSource = "session" + input.modelSource = v1.ExecutionSourceSession if !explicitModel && saved != nil { - input.modelSource = "agent" + input.modelSource = v1.ExecutionSourceAgent } if needsModel && input.deploymentDefaults != nil { if input.Agent == nil { @@ -57,10 +57,10 @@ func (h *Handler) prepareSessionModelConfiguration(ctx context.Context, input *s } model := input.deploymentDefaults.Model input.Agent.Model = &model - input.modelSource = "deployment" + input.modelSource = v1.ExecutionSourceDeployment } raw := json.RawMessage(`{}`) - source := "unknown" + source := v1.ExecutionSourceUnknown var supplied json.RawMessage if input.Agent != nil && input.Agent.XAgentsCore != nil { supplied = input.Agent.XAgentsCore.HarnessConfig @@ -69,17 +69,17 @@ func (h *Handler) prepareSessionModelConfiguration(ctx context.Context, input *s supplied = input.XAgentsCore.HarnessConfig } if len(supplied) > 0 { - raw, source = supplied, "session" + raw, source = supplied, v1.ExecutionSourceSession } else if explicitModel || explicitProvider { - source = "session" + source = v1.ExecutionSourceSession } else if saved != nil { - source = "agent" + source = v1.ExecutionSourceAgent // Changing the harness also discards the former adapter's parameters. if _, overridden := input.agentFields["x_agents_core"]; !overridden && saved.XAgentsCore != nil { raw = v1.ResolvedHarnessConfig(saved.XAgentsCore.HarnessConfig) } } else if input.deploymentDefaults != nil { - raw, source = v1.ResolvedHarnessConfig(input.deploymentDefaults.HarnessConfig), "deployment" + raw, source = v1.ResolvedHarnessConfig(input.deploymentDefaults.HarnessConfig), v1.ExecutionSourceDeployment } model := "" if input.Agent != nil && input.Agent.Model != nil { diff --git a/services/core/internal/api/session_model_configuration_test.go b/services/core/internal/api/session_model_configuration_test.go index bda85d060..d3e378cd4 100644 --- a/services/core/internal/api/session_model_configuration_test.go +++ b/services/core/internal/api/session_model_configuration_test.go @@ -64,7 +64,7 @@ func TestSessionNativeConfigurationSources(t *testing.T) { if input.Agent != nil && input.Agent.Model != nil { value = *input.Agent.Model } - if value != tc.model || string(input.resolvedHarnessConfig) != tc.native || input.harnessConfigSource != tc.source { + if value != tc.model || string(input.resolvedHarnessConfig) != tc.native || string(input.harnessConfigSource) != tc.source { t.Fatalf("model=%s native=%s source=%s", value, input.resolvedHarnessConfig, input.harnessConfigSource) } }) diff --git a/services/core/internal/api/session_model_defaults.go b/services/core/internal/api/session_model_defaults.go index 20409c8e1..9923067ca 100644 --- a/services/core/internal/api/session_model_defaults.go +++ b/services/core/internal/api/session_model_defaults.go @@ -54,28 +54,28 @@ func modelProviderRequired(environment, engine string) error { // resolveSessionExecution applies provider precedence: the Session bundle, the // saved Agent bundle, then the deployment default where the environment allows // it. Bundles are never merged. -func (h *Handler) resolveSessionExecution(ctx context.Context, input sessionRequest, inherited *v1.ModelProviderInput, raw json.RawMessage) (string, *v1.ModelProviderInput, string, uuid.UUID, error) { +func (h *Handler) resolveSessionExecution(ctx context.Context, input sessionRequest, inherited *v1.ModelProviderInput, raw json.RawMessage) (string, *v1.ModelProviderInput, v1.ExecutionSource, uuid.UUID, error) { engine, err := h.sessionHarness(raw) if err != nil { return "", nil, "", uuid.Nil, err } var revision uuid.UUID - provider, source := inherited, v1.ModelProviderSourceAgent + provider, source := inherited, v1.ExecutionSourceAgent if extension := input.XAgentsCore; extension != nil { if extension.ModelProvider == nil && !input.modelProviderNull && len(extension.HarnessConfig) == 0 && len(extension.Environment) == 0 { return "", nil, "", uuid.Nil, errors.New("x_agents_core requires an execution option") } if extension.ModelProvider != nil { - provider, source = extension.ModelProvider, v1.ModelProviderSourceSession + provider, source = extension.ModelProvider, v1.ExecutionSourceSession } } environment := input.Environment.Type - if provider == nil && v1.ModelProviderAllowed(environment, v1.ModelProviderSourceDeployment) { + if provider == nil && v1.ModelProviderAllowed(environment, v1.ExecutionSourceDeployment) { snapshot := input.deploymentDefaults if snapshot != nil { provider, revision = snapshot.Provider, snapshot.Revision } - source = v1.ModelProviderSourceDeployment + source = v1.ExecutionSourceDeployment } if provider == nil { if v1.ModelProviderRequired(environment) { diff --git a/services/core/internal/api/session_request.go b/services/core/internal/api/session_request.go index d8971677d..b4e091ee8 100644 --- a/services/core/internal/api/session_request.go +++ b/services/core/internal/api/session_request.go @@ -30,8 +30,8 @@ type sessionRequest struct { originalEnvironment json.RawMessage modelProviderNull bool deploymentDefaults *modelconfiguration.Snapshot - modelSource string - harnessConfigSource string + modelSource v1.ExecutionSource + harnessConfigSource v1.ExecutionSource resolvedHarnessConfig json.RawMessage v1.CreateSessionRequest Input json.RawMessage diff --git a/services/core/internal/api/turn_diagnostic_failure_test.go b/services/core/internal/api/turn_diagnostic_failure_test.go index 8fe650ca2..d5b3b20a7 100644 --- a/services/core/internal/api/turn_diagnostic_failure_test.go +++ b/services/core/internal/api/turn_diagnostic_failure_test.go @@ -3,9 +3,12 @@ package api import ( "encoding/json" "os" + "reflect" + "slices" "strings" "testing" + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) @@ -20,6 +23,7 @@ func TestDiagnosticFailureWhitelist(t *testing.T) { if err != nil { t.Fatal(err) } + produced := map[string]bool{} for want, inputs := range cases { if !strings.Contains(string(catalog), "`"+want+"`") { t.Fatal("uncatalogued diagnostics code", want) @@ -27,12 +31,59 @@ func TestDiagnosticFailureWhitelist(t *testing.T) { for _, input := range inputs { raw, _ := json.Marshal(map[string]string{"error_code": input, "error": "raw-secret-canary"}) got := turnDiagnosticFailure(sessions.Turn{Status: sessions.TurnFailed, Outcome: raw}) + produced[got.Code] = true encoded, _ := json.Marshal(got) if got.Code != want || strings.Contains(string(encoded), "canary") { t.Fatal(input, got) } } } + // Runtime classifications share the observation fixture; Core mappings above + // and Environment failures below exercise the actual diagnostic producers. + raw, err := os.ReadFile("../modelconfiguration/testdata/observation_cases.json") + if err != nil { + t.Fatal(err) + } + var observations []struct { + EngineErrorCode string `json:"engine_error_code"` + } + if err := json.Unmarshal(raw, &observations); err != nil { + t.Fatal(err) + } + for _, observation := range observations { + if code, _ := proto.NormalizeEngineFailure(observation.EngineErrorCode, nil); code != "" { + outcome, _ := json.Marshal(map[string]string{"error_code": "engine_failed", "engine_error_code": code}) + produced[turnDiagnosticFailure(sessions.Turn{Status: sessions.TurnFailed, Outcome: outcome}).Code] = true + } + } + for _, failure := range []string{"", "environment_unavailable", "runtime_preparation_failed", "model_provider_required", "unknown", "provisioning"} { + session := hostedFailureSession() + if failure != "provisioning" { + session.EnvironmentFailure = nil + session.EnvironmentInputActivity = &sessions.EnvironmentInputActivity{Status: "failed", Failure: failure} + } else { + session.EnvironmentFailure = &sessions.EnvironmentFailure{} + } + h, _, _ := adminTestHandler(t, serveDiagnostics(diagnosticSnapshotStore{session: session})) + w := diagnosticRequest(h, adminSessionsPath+session.ID+"/diagnostics", "Bearer admin") + var response SessionDiagnostics + if w.Code != 200 || json.Unmarshal(w.Body.Bytes(), &response) != nil || response.Failure == nil { + t.Fatal(w.Code, w.Body) + } + produced[response.Failure.Code] = true + } + field, _ := reflect.TypeFor[DiagnosticFailure]().FieldByName("Code") + declared := strings.Split(field.Tag.Get("enums"), ",") + for code := range produced { + if !slices.Contains(declared, code) { + t.Errorf("diagnostic code %q missing from schema", code) + } + } + for _, code := range declared { + if !produced[code] { + t.Errorf("schema diagnostic code %q has no exercised producer", code) + } + } if got := turnDiagnosticFailure(sessions.Turn{Status: sessions.TurnFailed, Outcome: json.RawMessage(`{"error_code":"engine_failed",`)}); got.Code != "internal_error" { t.Fatal("malformed outcome accepted", got) } diff --git a/services/core/internal/coremetrics/service.go b/services/core/internal/coremetrics/service.go index 84dfc5f91..8b2c497e0 100644 --- a/services/core/internal/coremetrics/service.go +++ b/services/core/internal/coremetrics/service.go @@ -55,14 +55,14 @@ var errPanicked = errors.New("periodic job pass panicked") // New reports the scheduler, which the Source reads live, and then jobs in // their order. Run runs the jobs. An enabled job needs a positive Every. func New(started time.Time, revision string, source Source, jobs ...Periodic) (*Service, error) { - s := &Service{source: source, started: started.UTC(), now: time.Now, jobIDs: []string{"scheduler"}, jobs: map[string]Job{"scheduler": {ID: "scheduler", Status: "unknown"}}, periodic: jobs} + s := &Service{source: source, started: started.UTC(), now: time.Now, jobIDs: []string{"scheduler"}, jobs: map[string]Job{"scheduler": {ID: "scheduler", Status: JobUnknown}}, periodic: jobs} if revisionPattern.MatchString(revision) { s.revision = &revision } for _, job := range jobs { - status := "unknown" + status := JobUnknown if job.Run == nil { - status = "stopped" + status = JobStopped } else if job.Every <= 0 { return nil, errors.New("coremetrics: periodic job " + job.ID + " needs a positive interval") } @@ -88,9 +88,9 @@ func (s *Service) RecordUnavailable() { s.refusals[i].count++ } func (s *Service) reportJob(id string, at time.Time, processed *int64, failed *int64, err error) { - status := "ok" + status := JobOk if err != nil || (failed != nil && *failed > 0) { - status = "failing" + status = JobFailing } s.mu.Lock() defer s.mu.Unlock() @@ -103,7 +103,7 @@ func (s *Service) stopJob(id string) { s.mu.Lock() defer s.mu.Unlock() if j, ok := s.jobs[id]; ok { - j.Status = "stopped" + j.Status = JobStopped s.jobs[id] = j } } @@ -196,12 +196,12 @@ func (s *Service) Read(ctx context.Context, name string) (View, error) { return View{}, err } live := s.source.Live() - view := View{Object: "core.metrics", Range: window, Service: ServiceState{Status: "running", Revision: s.revision, StartedAt: ptr(s.started), ExecutionOwner: live.ExecutionOwner}, + view := View{Object: "core.metrics", Range: window, Service: ServiceState{Status: ServiceRunning, Revision: s.revision, StartedAt: ptr(s.started), ExecutionOwner: live.ExecutionOwner}, Execution: Execution{SlotsInUse: live.SlotsInUse, SlotsTotal: live.SlotsTotal, ConnectedDaemons: live.ConnectedDaemons}, Database: Database{Pool: live.Pool}, Jobs: make([]Job, 0, len(s.jobIDs))} s.mu.Lock() latest := s.latest if latest.At.IsZero() || now.Sub(latest.At) > 2*SampleInterval || !latest.Healthy { - view.Service.Status = "degraded" + view.Service.Status = ServiceDegraded } if !latest.At.IsZero() && now.Sub(latest.At) <= 2*SampleInterval { view.Execution.QueuedTurns, view.Execution.WaitingForDaemon, view.Execution.InProgressTurns = latest.Queued, latest.WaitingForDaemon, latest.InProgress @@ -223,7 +223,7 @@ func (s *Service) Read(ctx context.Context, name string) (View, error) { defer cancel() history, err := s.source.History(query, window.Start, window.End, time.Duration(window.ResolutionSeconds)*time.Second) if err != nil { - view.Service.Status = "degraded" + view.Service.Status = ServiceDegraded } else { view.Execution.Interrupted = ptr(history.Interrupted) view.Execution.QueueWaitMS = history.QueueWaitMS @@ -232,11 +232,11 @@ func (s *Service) Read(ctx context.Context, name string) (View, error) { } } if live.ExecutionOwner == nil || !*live.ExecutionOwner { - view.Service.Status = "degraded" + view.Service.Status = ServiceDegraded } for _, job := range view.Jobs { - if job.Status == "failing" { - view.Service.Status = "degraded" + if job.Status == JobFailing { + view.Service.Status = ServiceDegraded } } var memory runtime.MemStats diff --git a/services/core/internal/coremetrics/types.go b/services/core/internal/coremetrics/types.go index 611efe657..ca97cce30 100644 --- a/services/core/internal/coremetrics/types.go +++ b/services/core/internal/coremetrics/types.go @@ -12,6 +12,22 @@ import ( // resolution. var ErrInvalidRange = errors.New("invalid Core metrics range") +type JobStatus string + +const ( + JobOk JobStatus = "ok" + JobFailing JobStatus = "failing" + JobStopped JobStatus = "stopped" + JobUnknown JobStatus = "unknown" +) + +type ServiceStatus string + +const ( + ServiceRunning ServiceStatus = "running" + ServiceDegraded ServiceStatus = "degraded" +) + type Latency struct { P50 *float64 `json:"p50" extensions:"x-nullable" binding:"required"` P95 *float64 `json:"p95" extensions:"x-nullable" binding:"required"` @@ -22,10 +38,10 @@ type Range struct { ResolutionSeconds int64 `json:"resolution_seconds" binding:"required"` } type ServiceState struct { - Status string `json:"status" enums:"running,degraded" binding:"required"` - Revision *string `json:"revision" extensions:"x-nullable" binding:"required"` - StartedAt *time.Time `json:"started_at" extensions:"x-nullable" binding:"required"` - ExecutionOwner *bool `json:"execution_owner" extensions:"x-nullable" binding:"required"` + Status ServiceStatus `json:"status" binding:"required"` + Revision *string `json:"revision" extensions:"x-nullable" binding:"required"` + StartedAt *time.Time `json:"started_at" extensions:"x-nullable" binding:"required"` + ExecutionOwner *bool `json:"execution_owner" extensions:"x-nullable" binding:"required"` } type ExecutionBucket struct { Start time.Time `json:"start" binding:"required"` @@ -64,7 +80,7 @@ type Database struct { } type Job struct { ID string `json:"id" binding:"required"` - Status string `json:"status" enums:"ok,failing,stopped,unknown" binding:"required"` + Status JobStatus `json:"status" binding:"required"` LastRunAt *time.Time `json:"last_run_at" extensions:"x-nullable" binding:"required"` Processed *int64 `json:"processed" extensions:"x-nullable" binding:"required"` Failed *int64 `json:"failed" extensions:"x-nullable" binding:"required"` diff --git a/services/core/internal/deployment/allocations.go b/services/core/internal/deployment/allocations.go index 916e2e364..5c6630cd8 100644 --- a/services/core/internal/deployment/allocations.go +++ b/services/core/internal/deployment/allocations.go @@ -6,11 +6,10 @@ import ( "fmt" "time" - "github.com/google/uuid" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment/placement" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" + "github.com/google/uuid" ) // ReserveAllocation commits the allocation of the tenant's hosted Environment @@ -68,7 +67,7 @@ func (e *ExecutionOperations) ReserveAllocation(ctx context.Context, key Allocat return ErrInvalidInput } allocation := NewAllocation{ID: uuid.NewString(), EnvironmentID: environment.ID, DeviceID: uuid.NewString(), ProviderKey: installation, Generation: d.Generation} - if d.Mode == "nodes" { + if d.Mode == string(sandbox.DeploymentNodes) { reserved, err := tx.LoadReserved() if err != nil { return err @@ -352,7 +351,7 @@ func (s *Service) LifecycleNode(ctx context.Context, tenant, environment string) if err != nil { return "", err } - if p.Provider == "" || p.Mode == "direct" { + if p.Provider == "" || p.Mode == string(sandbox.DeploymentDirect) { if p.PlacementNodeID != "" || p.AllocationNodeID != "" { return "", placement.ErrNodeUnavailable } diff --git a/services/core/internal/deployment/execution.go b/services/core/internal/deployment/execution.go index ee57e97be..6af321418 100644 --- a/services/core/internal/deployment/execution.go +++ b/services/core/internal/deployment/execution.go @@ -272,7 +272,7 @@ func (e *ExecutionOperations) saveSelection(tx DeploymentTx, d Record, input san if err != nil { return err } - return tx.SaveSelection(SelectionRecord{InstallationID: d.InstallationID, Provider: input.Provider, BackendFingerprint: description.BackendFingerprint, Mode: description.Mode, + return tx.SaveSelection(SelectionRecord{InstallationID: d.InstallationID, Provider: input.Provider, BackendFingerprint: description.BackendFingerprint, Mode: string(description.Mode), Generation: d.Generation + 1, Specification: specification, Configuration: sandbox.ConfigurationRecord{Public: configurationJSON(record.Public), Metadata: configurationJSON(record.Metadata), Secret: record.Secret}}) } diff --git a/services/core/internal/deployment/health.go b/services/core/internal/deployment/health.go index e8e37ffca..3b5c9492b 100644 --- a/services/core/internal/deployment/health.go +++ b/services/core/internal/deployment/health.go @@ -12,7 +12,7 @@ import ( // unreadiness only: unknown values, including arbitrary text, become // provider_unavailable, a ready provider has none and empty stays empty. func normalizeHealth(health NodeHealth) (NodeHealth, error) { - health.Diagnostic = sandbox.NormalizeNodeDiagnostic(health.Diagnostic) + health.Diagnostic = sandbox.NodeDiagnosticCode(sandbox.NormalizeNodeDiagnostic(string(health.Diagnostic))) if health.ProviderReady { health.Diagnostic = "" } @@ -75,20 +75,20 @@ func historyPoints(window coremetrics.Range, samples []HostHistoryPoint) []HostH // nodeRollout reports a node's preparation of the target generation. func nodeRollout(n NodeRecord) NodeRollout { - out := NodeRollout{State: "unknown", ReadyGeneration: n.ReadyGeneration} + out := NodeRollout{State: NodeRolloutUnknown, ReadyGeneration: n.ReadyGeneration} if !n.Online { return out } if n.ProtocolVersion == 1 && n.DeploymentGeneration != n.TargetGeneration { - out.State = "update_required" + out.State = NodeRolloutUpdateRequired return out } - switch n.TargetState { - case "ready", "preparing", "failed": - out.State = n.TargetState + switch NodeRolloutState(n.TargetState) { + case NodeRolloutReady, NodeRolloutPreparing, NodeRolloutFailed: + out.State = NodeRolloutState(n.TargetState) } - if out.State == "failed" && n.TargetDiagnostic != "" { - out.Diagnostic = sandbox.NormalizeNodeDiagnostic(n.TargetDiagnostic) + if out.State == NodeRolloutFailed && n.TargetDiagnostic != "" { + out.Diagnostic = sandbox.NodeDiagnosticCode(sandbox.NormalizeNodeDiagnostic(n.TargetDiagnostic)) } return out } diff --git a/services/core/internal/deployment/node.go b/services/core/internal/deployment/node.go index 164d54234..0bd2e3b31 100644 --- a/services/core/internal/deployment/node.go +++ b/services/core/internal/deployment/node.go @@ -46,11 +46,11 @@ type Enrollment struct { type NodeHealth struct { Host *NodeHost `json:"-"` // Fixed reason for the last reported unreadiness; absent while the provider is ready. Clients treat an unknown value as provider_unavailable. - Diagnostic string `json:"diagnostic,omitempty" enums:"provider_unavailable,host_unsupported,artifacts_unavailable,runtime_download_failed,runtime_image_unavailable,capacity_insufficient"` - ProviderReady bool `json:"provider_ready" binding:"required"` - CPUCount *int64 `json:"cpu_count" extensions:"x-nullable" binding:"required"` - AvailableMemoryBytes *int64 `json:"available_memory_bytes" extensions:"x-nullable" binding:"required"` - AvailableDiskBytes *int64 `json:"available_disk_bytes" extensions:"x-nullable" binding:"required"` + Diagnostic sandbox.NodeDiagnosticCode `json:"diagnostic,omitempty"` + ProviderReady bool `json:"provider_ready" binding:"required"` + CPUCount *int64 `json:"cpu_count" extensions:"x-nullable" binding:"required"` + AvailableMemoryBytes *int64 `json:"available_memory_bytes" extensions:"x-nullable" binding:"required"` + AvailableDiskBytes *int64 `json:"available_disk_bytes" extensions:"x-nullable" binding:"required"` } type Node struct { diff --git a/services/core/internal/deployment/nodes.go b/services/core/internal/deployment/nodes.go index 91867c15f..f166a12b8 100644 --- a/services/core/internal/deployment/nodes.go +++ b/services/core/internal/deployment/nodes.go @@ -162,7 +162,7 @@ func (s *Service) CreateEnrollment(ctx context.Context, capacity Capacity) (Enro if d.Reset != nil { return ErrResetInProgress } - if d.Mode != "nodes" { + if d.Mode != string(sandbox.DeploymentNodes) { return ErrConflict } if _, err := s.specification(d); err != nil { @@ -211,7 +211,7 @@ func (s *Service) Enroll(ctx context.Context, token string, input Enrollment) (N if d.Reset != nil { return ErrResetInProgress } - if d.Mode != "nodes" || input.Provider != d.Provider { + if d.Mode != string(sandbox.DeploymentNodes) || input.Provider != d.Provider { return ErrInvalidInput } spec, err := s.specification(d) @@ -278,7 +278,7 @@ func (s *Service) AuthenticateNode(ctx context.Context, nodeID, credential strin if err != nil { return placement.ErrNodeUnavailable } - if n.InstallationID != d.InstallationID || d.Mode != "nodes" { + if n.InstallationID != d.InstallationID || d.Mode != string(sandbox.DeploymentNodes) { return ErrNodeCredential } if err := s.checkEnrollmentIdentity(tx, d, n); err != nil { @@ -397,7 +397,7 @@ func (s *Service) NodeConfiguration(ctx context.Context, nodeID, token string, g if node == nil && d.Reset != nil { return ErrResetInProgress } - if d.Mode != "nodes" { + if d.Mode != string(sandbox.DeploymentNodes) { return ErrConflict } if node != nil { @@ -449,7 +449,7 @@ func (s *Service) connection(tx NodeReads, d Record, nodeID, connectionID string if err != nil { return StoredNode{}, err } - if n.ConnectionID != connectionID || n.ConnectedEpoch != epoch || epoch == 0 || epoch > math.MaxInt64 || d.OwnerEpoch != epoch || n.InstallationID != d.InstallationID || d.Mode != "nodes" { + if n.ConnectionID != connectionID || n.ConnectedEpoch != epoch || epoch == 0 || epoch > math.MaxInt64 || d.OwnerEpoch != epoch || n.InstallationID != d.InstallationID || d.Mode != string(sandbox.DeploymentNodes) { return StoredNode{}, ErrNodeCredential } return n, nil @@ -563,7 +563,7 @@ func (s *Service) heartbeat(ctx context.Context, nodeID, connectionID string, ep if health.ProviderReady { state = "ready" } - statuses = []sandbox.GenerationStatus{{Generation: n.DeploymentGeneration, SpecificationDigest: n.SpecificationDigest, State: state, Diagnostic: health.Diagnostic}} + statuses = []sandbox.GenerationStatus{{Generation: n.DeploymentGeneration, SpecificationDigest: n.SpecificationDigest, State: state, Diagnostic: string(health.Diagnostic)}} } return s.recordGenerations(tx, d, n, statuses, protocol) }) diff --git a/services/core/internal/deployment/placement/placement.go b/services/core/internal/deployment/placement/placement.go index 28371a52e..d68b8e153 100644 --- a/services/core/internal/deployment/placement/placement.go +++ b/services/core/internal/deployment/placement/placement.go @@ -168,7 +168,7 @@ func (r *Rules) DecidePlacement(d Deployment, nodes []Node) (*Placement, error) if err := r.CheckPublicOrigin(d.Provider); err != nil { return nil, err } - if d.Mode == "direct" { + if d.Mode == string(sandbox.DeploymentDirect) { return nil, nil } var chosen *Node diff --git a/services/core/internal/deployment/reset.go b/services/core/internal/deployment/reset.go index a404549b1..90da87734 100644 --- a/services/core/internal/deployment/reset.go +++ b/services/core/internal/deployment/reset.go @@ -8,11 +8,13 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/adminaudit" ) +type ResetMode string + // ResetRequest starts or escalates a reset of the sandbox deployment. type ResetRequest struct { - ExpectedGeneration uint64 `json:"expected_generation" binding:"required" minimum:"0"` - Clear string `json:"clear" binding:"required" enums:"auto,force"` - DeadlineSeconds *int32 `json:"deadline_seconds,omitempty" minimum:"300" maximum:"86400"` + ExpectedGeneration uint64 `json:"expected_generation" binding:"required" minimum:"0"` + Clear ResetMode `json:"clear" binding:"required"` + DeadlineSeconds *int32 `json:"deadline_seconds,omitempty" minimum:"300" maximum:"86400"` } // ResetSession is a hosted Session a reset still has to archive. @@ -21,9 +23,9 @@ type ResetSession struct{ SessionID, TenantID string } const ( // ResetAuto archives idle Sessions and escalates to ResetForce at the // deadline. - ResetAuto = "auto" + ResetAuto ResetMode = "auto" // ResetForce archives every hosted Session, busy ones included. - ResetForce = "force" + ResetForce ResetMode = "force" defaultResetDeadlineSeconds = 3600 ) @@ -72,7 +74,7 @@ func (e *ExecutionOperations) StartReset(ctx context.Context, installation strin return ErrNotConfigured } if d.Reset != nil { - if d.Reset.Clear == input.Clear { + if d.Reset.Clear == string(input.Clear) { return nil } if input.Clear != ResetForce { @@ -87,7 +89,7 @@ func (e *ExecutionOperations) StartReset(ctx context.Context, installation strin if !ok { return ErrInvalidInput } - if err := tx.StartReset(input.Clear, deadline, source); err != nil { + if err := tx.StartReset(string(input.Clear), deadline, source); err != nil { return err } return tx.RecordAudit("reset_start", installation) @@ -126,7 +128,7 @@ func (e *ExecutionOperations) AdvanceResetDeadline(ctx context.Context) error { if err != nil { return err } - if d.Reset == nil || d.Reset.Clear != ResetAuto || d.Reset.DeadlineAt == nil || time.Now().Before(*d.Reset.DeadlineAt) { + if d.Reset == nil || d.Reset.Clear != string(ResetAuto) || d.Reset.DeadlineAt == nil || time.Now().Before(*d.Reset.DeadlineAt) { return nil } source, err := tx.LoadResetSource() diff --git a/services/core/internal/deployment/reset_test.go b/services/core/internal/deployment/reset_test.go index 1a2c1187a..c7477b00c 100644 --- a/services/core/internal/deployment/reset_test.go +++ b/services/core/internal/deployment/reset_test.go @@ -9,9 +9,8 @@ import ( "testing" "time" - "github.com/google/uuid" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/adminaudit" + "github.com/google/uuid" ) // resetTx serves stored and records each reset write, with its arguments. @@ -37,9 +36,9 @@ func resetTx(t *testing.T, stored Record, resources Resources, source adminaudit } } -func resetting(t *testing.T, installation, clear string, requestedAt time.Time, deadline *time.Time) Record { +func resetting(t *testing.T, installation string, clear ResetMode, requestedAt time.Time, deadline *time.Time) Record { d := webDeployment(t, installation, "docker", 4) - d.Reset = &ResetState{Clear: clear, RequestedAt: requestedAt, DeadlineAt: deadline} + d.Reset = &ResetState{Clear: string(clear), RequestedAt: requestedAt, DeadlineAt: deadline} return d } diff --git a/services/core/internal/deployment/rules_test.go b/services/core/internal/deployment/rules_test.go index 84be8e066..53c875dcc 100644 --- a/services/core/internal/deployment/rules_test.go +++ b/services/core/internal/deployment/rules_test.go @@ -7,10 +7,9 @@ import ( "testing" "time" - "github.com/google/uuid" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/coremetrics" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" + "github.com/google/uuid" ) func TestPublicOrigin(t *testing.T) { @@ -170,7 +169,7 @@ func TestNormalizeHealth(t *testing.T) { for _, c := range []struct { name string in NodeHealth - diagnostic string + diagnostic sandbox.NodeDiagnosticCode invalid bool }{ {"ready clears the diagnostic", NodeHealth{ProviderReady: true, Diagnostic: sandbox.NodeProviderUnavailable}, "", false}, @@ -252,8 +251,8 @@ func TestNodeRollout(t *testing.T) { for _, c := range []struct { name string n NodeRecord - state string - diagnostic string + state NodeRolloutState + diagnostic sandbox.NodeDiagnosticCode }{ {"offline", NodeRecord{TargetState: "ready", ReadyGeneration: &ready}, "unknown", ""}, {"protocol 1 on another generation", NodeRecord{Online: true, ProtocolVersion: 1, DeploymentGeneration: 1, TargetGeneration: 2, TargetState: "ready", ReadyGeneration: &ready}, "update_required", ""}, diff --git a/services/core/internal/deployment/service.go b/services/core/internal/deployment/service.go index c9e06fd89..5a08e0fa6 100644 --- a/services/core/internal/deployment/service.go +++ b/services/core/internal/deployment/service.go @@ -44,7 +44,7 @@ func (s *Service) View(ctx context.Context) (View, error) { // view reports the public URL as the deployment's read-only core_url. func (s *Service) view(snapshot Snapshot) (View, error) { d := snapshot.Record - result := View{InstallationID: d.InstallationID, Provider: d.Provider, CoreURL: s.rules.PublicURL(), OwnerEpoch: d.OwnerEpoch, Generation: d.Generation, Mode: d.Mode, Rollout: snapshot.Rollout, Resources: snapshot.Resources} + result := View{InstallationID: d.InstallationID, Provider: d.Provider, CoreURL: s.rules.PublicURL(), OwnerEpoch: d.OwnerEpoch, Generation: d.Generation, Mode: sandbox.DeploymentMode(d.Mode), Rollout: snapshot.Rollout, Resources: snapshot.Resources} if len(d.Specification) > 0 && string(d.Specification) != "{}" { var spec sandbox.DeploymentSpec if json.Unmarshal(d.Specification, &spec) == nil { @@ -69,7 +69,7 @@ func (s *Service) view(snapshot Snapshot) (View, error) { } } if d.Reset != nil { - result.Reset = &Reset{Clear: d.Reset.Clear, RequestedAt: d.Reset.RequestedAt, DeadlineAt: d.Reset.DeadlineAt, ForcedAt: d.Reset.ForcedAt, Remaining: snapshot.Remaining} + result.Reset = &Reset{Clear: ResetMode(d.Reset.Clear), RequestedAt: d.Reset.RequestedAt, DeadlineAt: d.Reset.DeadlineAt, ForcedAt: d.Reset.ForcedAt, Remaining: snapshot.Remaining} } return result, nil } @@ -204,7 +204,7 @@ func (s *Service) GenerationPage(ctx context.Context, after int64) ([]Setup, err if err != nil { return nil, err } - v.Mode, v.Operations = adapter.Mode, adapter.Operations() + v.Mode, v.Operations = string(adapter.Mode), adapter.Operations() result = append(result, v) } return result, nil @@ -245,7 +245,7 @@ func (s *Service) SetupForSelection(installationID string, input sandbox.Selecti if err := s.rules.CheckPublicOrigin(input.Provider); err != nil { return Setup{}, err } - result := Setup{InstallationID: installationID, Provider: input.Provider, Mode: description.Mode, Specification: normalized.DeploymentSpec, Configuration: normalized.Configuration, BackendFingerprint: description.BackendFingerprint} + result := Setup{InstallationID: installationID, Provider: input.Provider, Mode: string(description.Mode), Specification: normalized.DeploymentSpec, Configuration: normalized.Configuration, BackendFingerprint: description.BackendFingerprint} return s.describe(result) } diff --git a/services/core/internal/deployment/session_archive.go b/services/core/internal/deployment/session_archive.go index eb33b012d..0f6c98ad4 100644 --- a/services/core/internal/deployment/session_archive.go +++ b/services/core/internal/deployment/session_archive.go @@ -57,7 +57,7 @@ func (e *ExecutionOperations) archiveSession(ctx context.Context, tenantID, sess if err := checkArchiveReset(d, *resetRequestedAt); err != nil { return err } - if d.Reset.Clear == ResetAuto { + if d.Reset.Clear == string(ResetAuto) { busy, err := tx.LoadResetBusy() if err != nil { return err diff --git a/services/core/internal/deployment/session_archive_test.go b/services/core/internal/deployment/session_archive_test.go index 2db8c361c..d325e6b83 100644 --- a/services/core/internal/deployment/session_archive_test.go +++ b/services/core/internal/deployment/session_archive_test.go @@ -51,9 +51,9 @@ func TestCheckArchiveReset(t *testing.T) { reset *ResetState want error }{ - "running reset": {&ResetState{Clear: ResetAuto, RequestedAt: requested}, nil}, + "running reset": {&ResetState{Clear: string(ResetAuto), RequestedAt: requested}, nil}, "no reset": {nil, ErrConflict}, - "another request": {&ResetState{Clear: ResetAuto, RequestedAt: requested.Add(time.Second)}, ErrConflict}, + "another request": {&ResetState{Clear: string(ResetAuto), RequestedAt: requested.Add(time.Second)}, ErrConflict}, } { if err := checkArchiveReset(Record{Reset: test.reset}, requested); !errors.Is(err, test.want) { t.Errorf("%s: got %v, want %v", name, err, test.want) @@ -279,8 +279,8 @@ func TestArchiveSession(t *testing.T) { func TestArchiveResetSession(t *testing.T) { requested := time.Unix(100, 0) - resetting := func(clear string) Record { - return Record{InstallationID: "installation", Provider: "docker", Generation: 1, Reset: &ResetState{Clear: clear, RequestedAt: requested}} + resetting := func(clear ResetMode) Record { + return Record{InstallationID: "installation", Provider: "docker", Generation: 1, Reset: &ResetState{Clear: string(clear), RequestedAt: requested}} } hosted := &sessions.Environment{ID: "environment", Status: "connected", Configuration: json.RawMessage(`{"type":"openai_hosted"}`)} failed := &sessions.Environment{ID: "environment", Status: "failed", Configuration: hosted.Configuration} diff --git a/services/core/internal/deployment/view.go b/services/core/internal/deployment/view.go index f45b90afe..23bef5045 100644 --- a/services/core/internal/deployment/view.go +++ b/services/core/internal/deployment/view.go @@ -7,13 +7,30 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" ) +type NodeRolloutState string + +const ( + NodeRolloutReady NodeRolloutState = "ready" + NodeRolloutPreparing NodeRolloutState = "preparing" + NodeRolloutFailed NodeRolloutState = "failed" + NodeRolloutUpdateRequired NodeRolloutState = "update_required" + NodeRolloutUnknown NodeRolloutState = "unknown" +) + +type RolloutState string + +const ( + RolloutSettled RolloutState = "settled" + RolloutPreparing RolloutState = "preparing" +) + // View is the sandbox deployment as administrators read it. type View struct { Rollout Rollout `json:"rollout" binding:"required"` Specification *sandbox.DeploymentSpec `json:"specification,omitempty"` SpecificationDigest string `json:"specification_digest,omitempty"` Generation uint64 `json:"generation" binding:"required"` - Mode string `json:"mode" enums:",nodes,direct" binding:"required"` + Mode sandbox.DeploymentMode `json:"mode" binding:"required"` Resources Resources `json:"resources" binding:"required"` Configuration json.RawMessage `json:"configuration,omitempty" swaggertype:"object"` Metadata json.RawMessage `json:"metadata,omitempty" swaggertype:"object"` @@ -43,10 +60,10 @@ type Suspension struct { type NodeRollout struct { // Target preparation, independent of an old pin's serving readiness. - State string `json:"state" enums:"ready,preparing,failed,update_required,unknown" binding:"required"` + State NodeRolloutState `json:"state" binding:"required"` // Durable serving-generation pin; online and provider_ready still gate placement. - ReadyGeneration *uint64 `json:"ready_generation" extensions:"x-nullable" binding:"required"` - Diagnostic string `json:"diagnostic,omitempty" enums:"provider_unavailable,host_unsupported,artifacts_unavailable,runtime_download_failed,runtime_image_unavailable,capacity_insufficient"` + ReadyGeneration *uint64 `json:"ready_generation" extensions:"x-nullable" binding:"required"` + Diagnostic sandbox.NodeDiagnosticCode `json:"diagnostic,omitempty"` } type RolloutNodes struct { @@ -58,14 +75,14 @@ type RolloutNodes struct { } type Rollout struct { - State string `json:"state" enums:"settled,preparing" binding:"required"` + State RolloutState `json:"state" binding:"required"` PreviousGenerationSandboxes int64 `json:"previous_generation_sandboxes" binding:"required"` Nodes *RolloutNodes `json:"nodes" extensions:"x-nullable" binding:"required"` } // Reset contains only durable state and a single-snapshot resource partition. type Reset struct { - Clear string `json:"clear" enums:"auto,force" binding:"required"` + Clear ResetMode `json:"clear" binding:"required"` RequestedAt time.Time `json:"requested_at" binding:"required"` DeadlineAt *time.Time `json:"deadline_at" extensions:"x-nullable" binding:"required"` ForcedAt *time.Time `json:"forced_at" extensions:"x-nullable" binding:"required"` diff --git a/services/core/internal/engine/configuration_test.go b/services/core/internal/engine/configuration_test.go index 74de79937..23f74d9e4 100644 --- a/services/core/internal/engine/configuration_test.go +++ b/services/core/internal/engine/configuration_test.go @@ -5,6 +5,7 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/builtin" + "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" ) func TestProviderDeclarationsAgreeWithAdmission(t *testing.T) { @@ -16,7 +17,7 @@ func TestProviderDeclarationsAgreeWithAdmission(t *testing.T) { } for _, protocol := range []string{"responses", "anthropic", "unknown"} { provider, supported := declared.Provider(protocol) - input := v1.ModelProviderInput{Protocol: protocol, BaseURL: "https://example.test", APIKey: "private-fixture", ContextWindow: 100, MaxOutputTokens: 20} + input := v1.ModelProviderInput{Protocol: modelprovider.Protocol(protocol), BaseURL: "https://example.test", APIKey: "private-fixture", ContextWindow: 100, MaxOutputTokens: 20} if (input.ValidateHarness(kind) == nil) != supported { t.Fatalf("protocol %q disagrees with validation", protocol) } diff --git a/services/core/internal/execution/archive_cancellation_cleanup_test.go b/services/core/internal/execution/archive_cancellation_cleanup_test.go index a8b899ae7..3ac684477 100644 --- a/services/core/internal/execution/archive_cancellation_cleanup_test.go +++ b/services/core/internal/execution/archive_cancellation_cleanup_test.go @@ -9,22 +9,21 @@ import ( "testing" "time" + 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/adminaudit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/projectpg" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/projects" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/providercontract" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/microsandbox" - - 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/adminaudit" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtime" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/microsandbox" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" "github.com/gorilla/websocket" @@ -79,7 +78,7 @@ func TestArchiveWaitingCleanupReceiptBarrier(t *testing.T) { t.Fatal(err) } _, service := testSessions(t, pool, pgtest.CredentialKey(t)) - created, err := service.CreateSession(t.Context(), project.TenantID, sessions.CreateSession{Creator: identity.Subject{Kind: "service_account", ID: "fixture"}, Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"model":"test-model"},"environment":{"type":"openai_hosted","network":{"access":"disabled"}}}`), ModelProvider: &v1.ModelProviderInput{Protocol: "responses", BaseURL: "https://model.fixture.example/v1", APIKey: "fixture-key"}, ModelProviderSource: v1.ModelProviderSourceSession}) + created, err := service.CreateSession(t.Context(), project.TenantID, sessions.CreateSession{Creator: identity.Subject{Kind: "service_account", ID: "fixture"}, Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"model":"test-model"},"environment":{"type":"openai_hosted","network":{"access":"disabled"}}}`), ModelProvider: &v1.ModelProviderInput{Protocol: "responses", BaseURL: "https://model.fixture.example/v1", APIKey: "fixture-key"}, ModelProviderSource: v1.ExecutionSourceSession}) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/execution/deployment_provider_observations_test.go b/services/core/internal/execution/deployment_provider_observations_test.go index 9d769cf31..b1240173f 100644 --- a/services/core/internal/execution/deployment_provider_observations_test.go +++ b/services/core/internal/execution/deployment_provider_observations_test.go @@ -88,7 +88,7 @@ func (f finishObservationFixture) start(t *testing.T) sessions.InputReceipt { } return receipt } -func (f finishObservationFixture) fields(t *testing.T) (*time.Time, *string) { +func (f finishObservationFixture) fields(t *testing.T) (*time.Time, *modelconfiguration.ProviderErrorCode) { t.Helper() rows, err := f.defaults.List(t.Context()) if err != nil || len(rows) != 1 { diff --git a/services/core/internal/execution/model_execution.go b/services/core/internal/execution/model_execution.go index 926aac7e5..674d3abcb 100644 --- a/services/core/internal/execution/model_execution.go +++ b/services/core/internal/execution/model_execution.go @@ -20,7 +20,7 @@ func resolvedSessionModelOptions(provider *v1.ModelProviderInput, engine string) return nil, err } return map[string]any{"model_provider": map[string]any{ - "protocol": provider.Protocol, "base_url": provider.BaseURL, "api_key": provider.APIKey, + "protocol": string(provider.Protocol), "base_url": provider.BaseURL, "api_key": provider.APIKey, "context_window": provider.ContextWindow, "max_output_tokens": provider.MaxOutputTokens, }}, nil } diff --git a/services/core/internal/execution/model_execution_test.go b/services/core/internal/execution/model_execution_test.go index d4964e10b..b7da9a67e 100644 --- a/services/core/internal/execution/model_execution_test.go +++ b/services/core/internal/execution/model_execution_test.go @@ -6,12 +6,12 @@ import ( "strings" "testing" - "github.com/google/uuid" - v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" + "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" + "github.com/google/uuid" ) func TestSessionModelExecutionNeverFallsBack(t *testing.T) { @@ -35,7 +35,7 @@ func TestSessionModelOptionsPreserveUpstreamBundleForEveryHarness(t *testing.T) for _, engine := range []string{"codex", "claude_sdk", "mcode"} { for _, protocol := range []string{"anthropic", "responses", "chat_completions"} { t.Run(engine+"/"+protocol, func(t *testing.T) { - provider := &v1.ModelProviderInput{Protocol: protocol, BaseURL: "https://example.com/v1", APIKey: "private-key", ContextWindow: 200000, MaxOutputTokens: 8000} + provider := &v1.ModelProviderInput{Protocol: modelprovider.Protocol(protocol), BaseURL: "https://example.com/v1", APIKey: "private-key", ContextWindow: 200000, MaxOutputTokens: 8000} got, err := resolvedSessionModelOptions(provider, engine) native := engine == "mcode" || engine == "codex" && protocol == "responses" || engine == "claude_sdk" && protocol == "anthropic" if !native { @@ -43,7 +43,7 @@ func TestSessionModelOptionsPreserveUpstreamBundleForEveryHarness(t *testing.T) if got != nil || !errors.As(err, &protocolError) || strings.Contains(err.Error(), provider.APIKey) { t.Fatal("non-native provider was not safely rejected") } - if provider.Protocol != protocol { + if string(provider.Protocol) != protocol { t.Fatal("rejection rewrote the frozen provider protocol") } return diff --git a/services/core/internal/execution/runtime_lifecycle.go b/services/core/internal/execution/runtime_lifecycle.go index b2f072098..28cc7510a 100644 --- a/services/core/internal/execution/runtime_lifecycle.go +++ b/services/core/internal/execution/runtime_lifecycle.go @@ -9,14 +9,13 @@ import ( "sync" "time" - "github.com/google/uuid" - "github.com/MiniMax-AI/OpenAgentCore/internal/obs/log" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" + "github.com/google/uuid" ) // RuntimeProvider binds one deployment to one sandbox installation. @@ -85,10 +84,10 @@ func validatedRuntimeProvider(config *RuntimeProvider, registry *runtimegateway. return RuntimeProvider{}, err } copied := *config - if copied.ProviderKind == "" || (copied.Mode != "nodes" && copied.Mode != "direct") { + if copied.ProviderKind == "" || (copied.Mode != string(sandbox.DeploymentNodes) && copied.Mode != string(sandbox.DeploymentDirect)) { return RuntimeProvider{}, sandbox.ErrInvalid } - if copied.Mode == "direct" && copied.Suspension != nil { + if copied.Mode == string(sandbox.DeploymentDirect) && copied.Suspension != nil { return RuntimeProvider{}, sandbox.ErrInvalid } if config.Suspension != nil { diff --git a/services/core/internal/execution/runtime_manager.go b/services/core/internal/execution/runtime_manager.go index d7d1af2b2..d6ae06a56 100644 --- a/services/core/internal/execution/runtime_manager.go +++ b/services/core/internal/execution/runtime_manager.go @@ -9,6 +9,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) @@ -79,7 +80,7 @@ func (m *runtimeManager) node(id string) (*runtimeNode, error) { m.mu.Unlock() return nil, errRuntimeTransition } - if m.closed || m.config.Provider == nil || (id == "") != (m.config.Mode == "direct") { + if m.closed || m.config.Provider == nil || (id == "") != (m.config.Mode == string(sandbox.DeploymentDirect)) { m.mu.Unlock() return nil, ErrExecutionUnavailable } diff --git a/services/core/internal/execution/sandbox_deployment_switch.go b/services/core/internal/execution/sandbox_deployment_switch.go index 0dddd2923..e6606e5a0 100644 --- a/services/core/internal/execution/sandbox_deployment_switch.go +++ b/services/core/internal/execution/sandbox_deployment_switch.go @@ -115,7 +115,7 @@ func (m *runtimeManager) activateDeployment(ctx context.Context, expected deploy m.publishEmptyDeployment(expected.InstallationID, expected.Generation) return nil } - if config == nil || config.InstallationID != expected.InstallationID || config.Generation != expected.Generation || config.Mode != expected.Mode || config.ProviderKind != expected.Provider || config.loadDeployment != nil { + if config == nil || config.InstallationID != expected.InstallationID || config.Generation != expected.Generation || config.Mode != string(expected.Mode) || config.ProviderKind != expected.Provider || config.loadDeployment != nil { return sandbox.ErrInvalid } copied, err := validatedRuntimeProvider(config, m.registry) diff --git a/services/core/internal/execution/worker_metrics.go b/services/core/internal/execution/worker_metrics.go index c5833dfa1..cd486579c 100644 --- a/services/core/internal/execution/worker_metrics.go +++ b/services/core/internal/execution/worker_metrics.go @@ -4,6 +4,8 @@ import ( "errors" "sync" "time" + + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/coremetrics" ) // WorkerMetrics contains only observations from this worker's existing @@ -18,7 +20,7 @@ type WorkerMetrics struct { // WorkerJobMetrics describes the last completed scheduling poll. Failed counts // failed polls, not failed Turns; Processed is unknown when a poll fails. type WorkerJobMetrics struct { - Status string + Status coremetrics.JobStatus LastRunAt *time.Time Processed *int64 Failed *int64 @@ -41,7 +43,7 @@ func (w *Worker) MetricsSnapshot() WorkerMetrics { value.Scheduler.Processed = copyMetric(value.Scheduler.Processed) value.Scheduler.Failed = copyMetric(value.Scheduler.Failed) if value.Scheduler.Status == "" { - value.Scheduler.Status = "unknown" + value.Scheduler.Status = coremetrics.JobUnknown } return value } @@ -75,10 +77,10 @@ func (w *Worker) observeOwnership(err error) { func (w *Worker) observeSchedulerPoll(processed int, err error) { now, handled, failed := time.Now().UTC(), int64(processed), int64(0) - job := WorkerJobMetrics{Status: "ok", LastRunAt: &now, Processed: &handled, Failed: &failed} + job := WorkerJobMetrics{Status: coremetrics.JobOk, LastRunAt: &now, Processed: &handled, Failed: &failed} if err != nil { failed = 1 - job.Status, job.Processed = "failing", nil + job.Status, job.Processed = coremetrics.JobFailing, nil } w.metrics.mu.Lock() defer w.metrics.mu.Unlock() @@ -88,9 +90,9 @@ func (w *Worker) observeSchedulerPoll(processed int, err error) { func (w *Worker) observeWorkerStop(runErr, contextErr error) { w.metrics.mu.Lock() defer w.metrics.mu.Unlock() - w.metrics.value.Scheduler.Status = "stopped" + w.metrics.value.Scheduler.Status = coremetrics.JobStopped if runErr != nil && (contextErr == nil || !errors.Is(runErr, contextErr)) { - w.metrics.value.Scheduler.Status = "failing" + w.metrics.value.Scheduler.Status = coremetrics.JobFailing } } diff --git a/services/core/internal/modelconfiguration/configuration.go b/services/core/internal/modelconfiguration/configuration.go index f4b9e776e..f8e03f044 100644 --- a/services/core/internal/modelconfiguration/configuration.go +++ b/services/core/internal/modelconfiguration/configuration.go @@ -19,7 +19,7 @@ type Configuration struct { HarnessConfig json.RawMessage UpdatedAt time.Time LastUsedAt *time.Time - LastErrorCode *string + LastErrorCode *ProviderErrorCode LastErrorAt *time.Time } diff --git a/services/core/internal/modelconfiguration/observation.go b/services/core/internal/modelconfiguration/observation.go index 8d200c38c..fc9e48afc 100644 --- a/services/core/internal/modelconfiguration/observation.go +++ b/services/core/internal/modelconfiguration/observation.go @@ -2,6 +2,20 @@ package modelconfiguration import "slices" +type ProviderErrorCode string + +const ( + ProviderAuthenticationError ProviderErrorCode = "authentication_error" + ProviderConnectionFailed ProviderErrorCode = "connection_failed" + ProviderRateLimitExceeded ProviderErrorCode = "rate_limit_exceeded" + ProviderUsageLimitExceeded ProviderErrorCode = "usage_limit_exceeded" + ProviderServerOverloaded ProviderErrorCode = "server_overloaded" + ProviderServerError ProviderErrorCode = "server_error" + ProviderResourceNotFound ProviderErrorCode = "resource_not_found" + ProviderRequestTimeout ProviderErrorCode = "request_timeout" + ProviderInvalidRequest ProviderErrorCode = "invalid_request" +) + // Observation names a root Turn whose committed outcome may update the // last-use observations of the deployment default its Session froze. type Observation struct { @@ -12,11 +26,7 @@ type Observation struct { // itself. Context-length and cyber-policy failures describe the request. The // observation statement's fence lists the same codes; testdata holds the cases // both are checked against. -var providerErrorCodes = []string{ - "authentication_error", "connection_failed", "rate_limit_exceeded", - "usage_limit_exceeded", "server_overloaded", "server_error", - "resource_not_found", "request_timeout", "invalid_request", -} +var providerErrorCodes = []ProviderErrorCode{ProviderAuthenticationError, ProviderConnectionFailed, ProviderRateLimitExceeded, ProviderUsageLimitExceeded, ProviderServerOverloaded, ProviderServerError, ProviderResourceNotFound, ProviderRequestTimeout, ProviderInvalidRequest} // ShouldObserveProvider reports whether a committed root Turn outcome says // something about its model provider: every completed Turn, and a failed Turn @@ -27,7 +37,7 @@ func ShouldObserveProvider(status, errorCode, engineErrorCode string) bool { case "completed": return true case "failed": - return errorCode == "engine_failed" && slices.Contains(providerErrorCodes, engineErrorCode) + return errorCode == "engine_failed" && slices.Contains(providerErrorCodes, ProviderErrorCode(engineErrorCode)) default: return false } diff --git a/services/core/internal/persistence/postgres/agentpg/store.go b/services/core/internal/persistence/postgres/agentpg/store.go index a2f519a27..a0a7dba24 100644 --- a/services/core/internal/persistence/postgres/agentpg/store.go +++ b/services/core/internal/persistence/postgres/agentpg/store.go @@ -61,7 +61,7 @@ func (s *Store) CreateAgent(ctx context.Context, input agents.NewAgent) (agents. if created, err = agentFromRow(row); err != nil { return err } - return auditpg.RecordWriteAudit(ctx, q, input.TenantID, "create", "agent", created.ID, "", writeaudit.Resource{Type: "agent", ID: created.ID}) + return auditpg.RecordWriteAudit(ctx, q, input.TenantID, writeaudit.ActionCreate, writeaudit.ResourceAgent, created.ID, "", writeaudit.Resource{Type: writeaudit.ResourceAgent, ID: created.ID}) }) if err != nil { return agents.Agent{}, translate(err) @@ -130,7 +130,7 @@ func (t *updateTx) apply(revision agents.Revision) (agents.Agent, error) { if err != nil { return agents.Agent{}, err } - return updated, auditpg.RecordWriteAudit(t.ctx, t.q, t.tenantID, "update", "agent", updated.ID, "") + return updated, auditpg.RecordWriteAudit(t.ctx, t.q, t.tenantID, writeaudit.ActionUpdate, writeaudit.ResourceAgent, updated.ID, "") } // DeleteAgent treats an agentID that cannot name an Agent as a missing one. @@ -151,7 +151,7 @@ func (s *Store) DeleteAgent(ctx context.Context, tenantID, agentID string) (stri return err } deleted = uuid.UUID(id.Bytes).String() - return auditpg.RecordWriteAudit(ctx, q, tenantID, "delete", "agent", deleted, "") + return auditpg.RecordWriteAudit(ctx, q, tenantID, writeaudit.ActionDelete, writeaudit.ResourceAgent, deleted, "") }) if err != nil { return "", err diff --git a/services/core/internal/persistence/postgres/auditpg/auditpg_test.go b/services/core/internal/persistence/postgres/auditpg/auditpg_test.go index 1c4e52a38..a0eb6a801 100644 --- a/services/core/internal/persistence/postgres/auditpg/auditpg_test.go +++ b/services/core/internal/persistence/postgres/auditpg/auditpg_test.go @@ -9,16 +9,15 @@ import ( "testing" "time" - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgtype" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/adminaudit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/auditpg" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/writeaudit" + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" ) func openAudit(t *testing.T) (*pgunit.Pool, *auditpg.Store) { @@ -46,7 +45,7 @@ func record(t *testing.T, pool *pgunit.Pool, ctx context.Context, write func(con func recordWrite(t *testing.T, pool *pgunit.Pool, source writeaudit.Source, action, kind, id string, created ...writeaudit.Resource) { t.Helper() if err := record(t, pool, writeaudit.WithSource(t.Context(), source), func(ctx context.Context, q *sqlc.Queries) error { - return auditpg.RecordWriteAudit(ctx, q, source.TenantID, action, kind, id, "", created...) + return auditpg.RecordWriteAudit(ctx, q, source.TenantID, writeaudit.Action(action), writeaudit.ResourceType(kind), id, "", created...) }); err != nil { t.Fatal(err) } @@ -204,7 +203,7 @@ func TestMalformedProvenanceFailsClosed(t *testing.T) { id = "" } ctx := writeaudit.WithSource(t.Context(), source) - if err := auditpg.RecordWriteAudit(ctx, nil, valid.TenantID, action, kind, id, "", created...); !errors.Is(err, writeaudit.ErrInvalidSource) { + if err := auditpg.RecordWriteAudit(ctx, nil, valid.TenantID, writeaudit.Action(action), writeaudit.ResourceType(kind), id, "", created...); !errors.Is(err, writeaudit.ErrInvalidSource) { t.Fatalf("%s accepted: %v", field, err) } } diff --git a/services/core/internal/persistence/postgres/auditpg/record.go b/services/core/internal/persistence/postgres/auditpg/record.go index 651afe523..5892ceef7 100644 --- a/services/core/internal/persistence/postgres/auditpg/record.go +++ b/services/core/internal/persistence/postgres/auditpg/record.go @@ -7,14 +7,13 @@ import ( "context" "errors" - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgtype" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/adminaudit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/writeaudit" + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" ) // The recorders run in the caller's business transaction: q must belong to it, @@ -26,9 +25,9 @@ import ( // provenance, such as internal lifecycle work, stays unattributed; malformed // provenance fails closed with writeaudit.ErrInvalidSource. A request that // already recorded its operation records nothing more. -func RecordWriteAudit(ctx context.Context, q *sqlc.Queries, tenant, action, resourceType, resourceID, parentID string, created ...writeaudit.Resource) error { +func RecordWriteAudit(ctx context.Context, q *sqlc.Queries, tenant string, action writeaudit.Action, resourceType writeaudit.ResourceType, resourceID, parentID string, created ...writeaudit.Resource) error { if _, ok := adminaudit.FromContext(ctx); ok { - return RecordAdminMutation(ctx, q, tenant, action, resourceType, resourceID) + return RecordAdminMutation(ctx, q, tenant, string(action), string(resourceType), resourceID) } source, ok := writeaudit.FromContext(ctx) if !ok { @@ -45,7 +44,7 @@ func RecordWriteAudit(ctx context.Context, q *sqlc.Queries, tenant, action, reso id, err := q.InsertWriteAuditOperation(ctx, sqlc.InsertWriteAuditOperationParams{ ID: pgtype.UUID{Bytes: uuid.New(), Valid: true}, TenantID: tenantID, KeyID: source.KeyID, KeyName: source.Name, KeyPrefix: source.Prefix, KeyKind: source.Kind, - Action: action, ResourceType: resourceType, ResourceID: resourceID, ParentID: parentID, RequestID: source.RequestID, TraceID: source.TraceID, + Action: string(action), ResourceType: string(resourceType), ResourceID: resourceID, ParentID: parentID, RequestID: source.RequestID, TraceID: source.TraceID, }) if errors.Is(err, pgx.ErrNoRows) { return nil @@ -54,7 +53,7 @@ func RecordWriteAudit(ctx context.Context, q *sqlc.Queries, tenant, action, reso return err } for _, resource := range created { - if err := q.InsertWriteAuditOwner(ctx, sqlc.InsertWriteAuditOwnerParams{TenantID: tenantID, ResourceType: resource.Type, ResourceID: resource.ID, ParentID: resource.ParentID, OperationID: id}); err != nil { + if err := q.InsertWriteAuditOwner(ctx, sqlc.InsertWriteAuditOwnerParams{TenantID: tenantID, ResourceType: string(resource.Type), ResourceID: resource.ID, ParentID: resource.ParentID, OperationID: id}); err != nil { return err } } diff --git a/services/core/internal/persistence/postgres/auditpg/write_operations.go b/services/core/internal/persistence/postgres/auditpg/write_operations.go index 30def5fac..37973e396 100644 --- a/services/core/internal/persistence/postgres/auditpg/write_operations.go +++ b/services/core/internal/persistence/postgres/auditpg/write_operations.go @@ -6,13 +6,12 @@ import ( "fmt" "time" - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgtype" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/writeaudit" + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" ) func apiKey(id, name, prefix, kind string, revoked pgtype.Timestamptz) writeaudit.APIKey { @@ -105,7 +104,7 @@ func (s *Store) ListWriteOperations(ctx context.Context, tenantID string, filter rows = rows[:filter.Limit] } for _, row := range rows { - page.Data = append(page.Data, writeaudit.Operation{ID: uuid.UUID(row.ID.Bytes).String(), Action: row.Action, ResourceType: row.ResourceType, ResourceID: row.ResourceID, ParentID: row.ParentID, RequestID: row.RequestID, TraceID: row.TraceID, APIKey: apiKey(row.KeyID, row.KeyName, row.KeyPrefix, row.KeyKind, row.RevokedAt), CreatedAt: row.CreatedAt.Time}) + page.Data = append(page.Data, writeaudit.Operation{ID: uuid.UUID(row.ID.Bytes).String(), Action: writeaudit.Action(row.Action), ResourceType: writeaudit.ResourceType(row.ResourceType), ResourceID: row.ResourceID, ParentID: row.ParentID, RequestID: row.RequestID, TraceID: row.TraceID, APIKey: apiKey(row.KeyID, row.KeyName, row.KeyPrefix, row.KeyKind, row.RevokedAt), CreatedAt: row.CreatedAt.Time}) } if page.HasMore { page.NextCursor = encodeCursor(page.Data[len(page.Data)-1].ID, scope) diff --git a/services/core/internal/persistence/postgres/deploymentpg/presence_test.go b/services/core/internal/persistence/postgres/deploymentpg/presence_test.go index 7c8fd3905..52b788997 100644 --- a/services/core/internal/persistence/postgres/deploymentpg/presence_test.go +++ b/services/core/internal/persistence/postgres/deploymentpg/presence_test.go @@ -8,15 +8,14 @@ import ( "testing" "time" - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgtype" - "github.com/jackc/pgx/v5/pgxpool" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/deploymentpg" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" + "github.com/jackc/pgx/v5/pgxpool" ) func presenceContext(t *testing.T) context.Context { @@ -278,15 +277,15 @@ func TestNodeDiagnosticReachesListAndDetail(t *testing.T) { {reported: "dial unix /var/run/docker.sock: permission denied", want: "provider_unavailable"}, {reported: "artifacts_unavailable", want: "", ready: true}, } { - if err := f.service.Heartbeat(t.Context(), node.NodeID, connection, epoch, deployment.NodeHealth{ProviderReady: tc.ready, Diagnostic: tc.reported}); err != nil { + if err := f.service.Heartbeat(t.Context(), node.NodeID, connection, epoch, deployment.NodeHealth{ProviderReady: tc.ready, Diagnostic: sandbox.NodeDiagnosticCode(tc.reported)}); err != nil { t.Fatal(tc.reported, err) } list, err := f.service.ListNodes(t.Context()) - if err != nil || len(list) != 1 || list[0].Diagnostic != tc.want { + if err != nil || len(list) != 1 || string(list[0].Diagnostic) != tc.want { t.Fatal(tc.reported, list, err) } detail, err := f.service.NodeDetail(t.Context(), node.NodeID, "1h") - if err != nil || detail.Diagnostic != tc.want { + if err != nil || string(detail.Diagnostic) != tc.want { t.Fatal(tc.reported, detail.Diagnostic, err) } var stored string diff --git a/services/core/internal/persistence/postgres/filepg/filepg.go b/services/core/internal/persistence/postgres/filepg/filepg.go index 6bac2306c..09a488b87 100644 --- a/services/core/internal/persistence/postgres/filepg/filepg.go +++ b/services/core/internal/persistence/postgres/filepg/filepg.go @@ -7,15 +7,14 @@ import ( "errors" "io" - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgtype" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/files" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/auditpg" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/writeaudit" + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" ) type Store struct{ pool *pgunit.Pool } @@ -61,7 +60,7 @@ func (s *Store) Create(ctx context.Context, tenantID string, write func(io.Write return err } created = fileFromRow(row) - return auditpg.RecordWriteAudit(ctx, q, tenantID, "create", "file", created.ID, "", writeaudit.Resource{Type: "file", ID: created.ID}) + return auditpg.RecordWriteAudit(ctx, q, tenantID, writeaudit.ActionCreate, writeaudit.ResourceFile, created.ID, "", writeaudit.Resource{Type: writeaudit.ResourceFile, ID: created.ID}) }) if err != nil { return files.File{}, err @@ -207,7 +206,7 @@ func (s *Store) Delete(ctx context.Context, tenantID, fileID string) error { if err := objects.Unlink(ctx, oid.Uint32); err != nil { return err } - return auditpg.RecordWriteAudit(ctx, q, tenantID, "delete", "file", fileID, "") + return auditpg.RecordWriteAudit(ctx, q, tenantID, writeaudit.ActionDelete, writeaudit.ResourceFile, fileID, "") }) } diff --git a/services/core/internal/persistence/postgres/modelconfigurationpg/observation_test.go b/services/core/internal/persistence/postgres/modelconfigurationpg/observation_test.go index 8e788926f..2c59b8c1b 100644 --- a/services/core/internal/persistence/postgres/modelconfigurationpg/observation_test.go +++ b/services/core/internal/persistence/postgres/modelconfigurationpg/observation_test.go @@ -12,13 +12,12 @@ import ( "time" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/modelconfiguration" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgxpool" - - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/modelconfiguration" ) // The statement and ShouldObserveProvider classify the shared cases the same @@ -79,7 +78,7 @@ func TestObservationExcludesOtherSourcesAndHistoricalSessions(t *testing.T) { input.IdempotencyKey = uuid.NewString() projection := *input.ExecutionConfiguration input.ExecutionConfiguration = &projection - input.ModelProviderSource = source + input.ModelProviderSource = v1.ExecutionSource(source) // Historical metadata may name deployment but has no frozen private UUID. if source == "deployment" { input.DeploymentProviderRevision = uuid.Nil @@ -89,7 +88,7 @@ func TestObservationExcludesOtherSourcesAndHistoricalSessions(t *testing.T) { input.ModelProviderSource = "session" } } - projection.ModelProvider = v1.ExecutionProviderSelection{Source: source, Status: "available", Configuration: input.ModelProvider.SafeView()} + projection.ModelProvider = v1.ExecutionProviderSelection{Source: v1.ExecutionSource(source), Status: "available", Configuration: input.ModelProvider.SafeView()} created, err := o.sessions.CreateSession(t.Context(), o.tenant, input) if err != nil { t.Fatal(source, err) diff --git a/services/core/internal/persistence/postgres/modelconfigurationpg/store.go b/services/core/internal/persistence/postgres/modelconfigurationpg/store.go index 02a0a7d0d..fd17b9724 100644 --- a/services/core/internal/persistence/postgres/modelconfigurationpg/store.go +++ b/services/core/internal/persistence/postgres/modelconfigurationpg/store.go @@ -10,16 +10,16 @@ import ( "time" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgtype" - + "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/modelconfiguration" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/auditpg" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/textvalue" + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" ) // auditResource is the administrator audit resource type of a deployment @@ -77,7 +77,7 @@ func (s *Store) Replace(ctx context.Context, record modelconfiguration.Record) ( err = s.pool.Transaction(ctx, func(ctx context.Context, tx pgx.Tx) error { q := sqlc.New(tx) row, err := q.UpsertDeploymentModelProvider(ctx, sqlc.UpsertDeploymentModelProviderParams{ - Harness: record.Harness, Protocol: record.Provider.Protocol, BaseUrl: record.Provider.BaseURL, + Harness: record.Harness, Protocol: string(record.Provider.Protocol), BaseUrl: record.Provider.BaseURL, ContextWindow: record.Provider.ContextWindow, MaxOutputTokens: record.Provider.MaxOutputTokens, Model: record.Model, HarnessConfig: record.HarnessConfig, EncryptedConfig: sealed, Revision: pgtype.UUID{Bytes: uuid.New(), Valid: true}, @@ -180,10 +180,11 @@ func configuration(row sqlc.ListDeploymentModelProvidersRow) modelconfiguration. result := modelconfiguration.Configuration{ Harness: row.Harness, Model: row.Model, HarnessConfig: json.RawMessage(row.HarnessConfig), UpdatedAt: row.UpdatedAt.Time, LastUsedAt: timestamp(row.LastUsedAt), LastErrorAt: timestamp(row.LastErrorAt), - Provider: v1.ModelProviderView{Protocol: row.Protocol, BaseURL: row.BaseUrl, ContextWindow: row.ContextWindow, MaxOutputTokens: row.MaxOutputTokens, APIKeyConfigured: true}, + Provider: v1.ModelProviderView{Protocol: modelprovider.Protocol(row.Protocol), BaseURL: row.BaseUrl, ContextWindow: row.ContextWindow, MaxOutputTokens: row.MaxOutputTokens, APIKeyConfigured: true}, } if row.LastErrorCode.Valid { - result.LastErrorCode = &row.LastErrorCode.String + code := modelconfiguration.ProviderErrorCode(row.LastErrorCode.String) + result.LastErrorCode = &code } return result } diff --git a/services/core/internal/persistence/postgres/sessionpg/artifacts.go b/services/core/internal/persistence/postgres/sessionpg/artifacts.go index 371e8a0ab..21401d11d 100644 --- a/services/core/internal/persistence/postgres/sessionpg/artifacts.go +++ b/services/core/internal/persistence/postgres/sessionpg/artifacts.go @@ -6,14 +6,14 @@ import ( "fmt" "io" - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgtype" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/auditpg" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/writeaudit" + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" ) var ( @@ -138,7 +138,7 @@ func (s *Store) DeleteSessionArtifact(ctx context.Context, tenantID, sessionID, if err := objects.Unlink(ctx, oid.Uint32); err != nil { return err } - return auditpg.RecordWriteAudit(ctx, q, tenantID, "delete", "artifact", uuid.UUID(lookup.ID.Bytes).String(), uuid.UUID(lookup.SessionID.Bytes).String()) + return auditpg.RecordWriteAudit(ctx, q, tenantID, writeaudit.ActionDelete, writeaudit.ResourceArtifact, uuid.UUID(lookup.ID.Bytes).String(), uuid.UUID(lookup.SessionID.Bytes).String()) }) } diff --git a/services/core/internal/persistence/postgres/sessionpg/creation.go b/services/core/internal/persistence/postgres/sessionpg/creation.go index be18c68d7..7861947d8 100644 --- a/services/core/internal/persistence/postgres/sessionpg/creation.go +++ b/services/core/internal/persistence/postgres/sessionpg/creation.go @@ -6,10 +6,6 @@ import ( "errors" "fmt" - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgtype" - v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" @@ -24,6 +20,9 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/skills" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/writeaudit" + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" ) // modelProviderKeyPurpose keys the provider-key fingerprint in creation @@ -232,7 +231,7 @@ func (t *creationTx) PruneChanges(ctx context.Context) error { } func (t *creationTx) AuditCreation(ctx context.Context, created ...writeaudit.Resource) error { - return auditpg.RecordWriteAudit(ctx, t.q, t.tenantID, "create", "session", optionalID(t.session), "", created...) + return auditpg.RecordWriteAudit(ctx, t.q, t.tenantID, writeaudit.ActionCreate, writeaudit.ResourceSession, optionalID(t.session), "", created...) } func (t *creationTx) LoadSession(ctx context.Context) (sessions.Session, error) { diff --git a/services/core/internal/persistence/postgres/sessionpg/creation_test.go b/services/core/internal/persistence/postgres/sessionpg/creation_test.go index fac2cb753..2165b2c07 100644 --- a/services/core/internal/persistence/postgres/sessionpg/creation_test.go +++ b/services/core/internal/persistence/postgres/sessionpg/creation_test.go @@ -14,9 +14,6 @@ import ( "testing" "time" - "github.com/google/uuid" - "github.com/jackc/pgx/v5/pgxpool" - v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment/placement" @@ -32,6 +29,8 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/skills" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/writeaudit" + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgxpool" ) var creator = identity.Subject{Kind: "service_account", ID: "test-runner"} @@ -382,7 +381,7 @@ func TestCreationFreezesResourcesOnce(t *testing.T) { input := sessions.CreateSession{ Creator: creator, Engine: "codex", IdempotencyKey: "frozen", Configuration: environmentConfiguration, ModelProvider: &v1.ModelProviderInput{Protocol: "responses", BaseURL: "https://example.com/v1", APIKey: canary}, - ModelProviderSource: v1.ModelProviderSourceSession, + ModelProviderSource: v1.ExecutionSourceSession, Initialization: environmentconfig.Setup{Skills: []environmentconfig.Skill{inline, reference}}, InitialFiles: []environmentconfig.InitialFile{{Type: "inline", Path: "/workspace/a", Data: []byte(canary)}, fileID}, } diff --git a/services/core/internal/persistence/postgres/sessionpg/execution_file_writes.go b/services/core/internal/persistence/postgres/sessionpg/execution_file_writes.go index 14cbad562..b6b8a252c 100644 --- a/services/core/internal/persistence/postgres/sessionpg/execution_file_writes.go +++ b/services/core/internal/persistence/postgres/sessionpg/execution_file_writes.go @@ -5,13 +5,12 @@ import ( "encoding/json" "errors" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgtype" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/auditpg" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/writeaudit" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" ) // WithFileWriteReservation reads the Environment in the transaction before it @@ -160,7 +159,7 @@ func (t *fileWriteTx) RecordFileWriteAudit(ctx context.Context, write string) er if err := json.Unmarshal(row.EnvironmentFileWrite.AuditSource, &source); err != nil { return err } - return auditpg.RecordWriteAudit(writeaudit.WithSource(ctx, source), t.q, optionalID(t.tenant), "upload_file", "environment", optionalID(t.environment), optionalID(t.session)) + return auditpg.RecordWriteAudit(writeaudit.WithSource(ctx, source), t.q, optionalID(t.tenant), writeaudit.ActionUploadFile, writeaudit.ResourceEnvironment, optionalID(t.environment), optionalID(t.session)) } func fileWriteFromRow(row sqlc.EnvironmentFileWrite, session pgtype.UUID) sessions.EnvironmentFileWrite { diff --git a/services/core/internal/persistence/postgres/sessionpg/inputs.go b/services/core/internal/persistence/postgres/sessionpg/inputs.go index a55e41aaa..f1f9e7d6c 100644 --- a/services/core/internal/persistence/postgres/sessionpg/inputs.go +++ b/services/core/internal/persistence/postgres/sessionpg/inputs.go @@ -7,14 +7,14 @@ import ( "fmt" "time" - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgtype" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/auditpg" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/writeaudit" + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" ) var ( @@ -184,7 +184,7 @@ func (t *SessionTx) FailInputReservation(ctx context.Context, reservation, code } func (t *SessionTx) RecordInputAudit(ctx context.Context) error { - return auditpg.RecordWriteAudit(ctx, t.q, optionalID(t.tenant), "send_events", "session", optionalID(t.session), "") + return auditpg.RecordWriteAudit(ctx, t.q, optionalID(t.tenant), writeaudit.ActionSendEvents, writeaudit.ResourceSession, optionalID(t.session), "") } func (s *Store) ListTurnInputs(ctx context.Context, tenant, session, turn string, after int64, limit int) ([]sessions.TurnInput, error) { diff --git a/services/core/internal/persistence/postgres/sessionpg/session_reads.go b/services/core/internal/persistence/postgres/sessionpg/session_reads.go index f22c82e04..c987a5fdd 100644 --- a/services/core/internal/persistence/postgres/sessionpg/session_reads.go +++ b/services/core/internal/persistence/postgres/sessionpg/session_reads.go @@ -7,14 +7,13 @@ import ( "errors" "fmt" - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgtype" - v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" ) var _ sessions.SessionReader = (*Store)(nil) @@ -214,9 +213,9 @@ func (s *Store) GetSessionExecutionConfiguration(ctx context.Context, tenantID, if row.Engine != "" { harness = &row.Engine } - projection.Model = v1.ExecutionSelection{Value: model, Source: "unknown"} - projection.Harness = v1.ExecutionSelection{Value: harness, Source: "unknown"} - projection.ModelProvider = v1.ExecutionProviderSelection{Source: "unknown", Status: "unavailable"} + projection.Model = v1.ExecutionSelection{Value: model, Source: v1.ExecutionSourceUnknown} + projection.Harness = v1.ExecutionSelection{Value: harness, Source: v1.ExecutionSourceUnknown} + projection.ModelProvider = v1.ExecutionProviderSelection{Source: v1.ExecutionSourceUnknown, Status: v1.ExecutionProviderUnavailable} } else if err := json.Unmarshal(row.ExecutionConfiguration, &projection); err != nil { return v1.SessionExecutionConfiguration{}, errors.New("invalid stored session execution configuration") } @@ -261,7 +260,7 @@ func loadManagedArchive(ctx context.Context, q *sqlc.Queries, tenant, session pg if row.EnvironmentType != "openai_hosted" { return sessions.ManagedArchive{}, sessions.ErrInvalidInput } - return sessions.ManagedArchive{SessionID: uuid.UUID(row.SessionID.Bytes).String(), EnvironmentID: uuid.UUID(row.EnvironmentID.Bytes).String(), State: row.State}, nil + return sessions.ManagedArchive{SessionID: uuid.UUID(row.SessionID.Bytes).String(), EnvironmentID: uuid.UUID(row.EnvironmentID.Bytes).String(), State: sessions.ManagedArchiveState(row.State)}, nil } // loadSession reads, on q, the tenant's visible Session with the projection diff --git a/services/core/internal/persistence/postgres/sessionpg/session_writes.go b/services/core/internal/persistence/postgres/sessionpg/session_writes.go index ac1ad07be..c8cfae51f 100644 --- a/services/core/internal/persistence/postgres/sessionpg/session_writes.go +++ b/services/core/internal/persistence/postgres/sessionpg/session_writes.go @@ -5,14 +5,14 @@ import ( "errors" "fmt" - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgtype" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/auditpg" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/writeaudit" + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" ) var _ sessions.SessionStorage = (*Store)(nil) @@ -46,7 +46,7 @@ func (t *deletionTx) ApplyDeletion(ctx context.Context) error { } func (t *deletionTx) RecordDeletionAudit(ctx context.Context) error { - return auditpg.RecordWriteAudit(ctx, t.q, t.tenantID, "delete", "session", uuid.UUID(t.session.Bytes).String(), "") + return auditpg.RecordWriteAudit(ctx, t.q, t.tenantID, writeaudit.ActionDelete, writeaudit.ResourceSession, uuid.UUID(t.session.Bytes).String(), "") } func (s *Store) UpdateSessionMetadata(ctx context.Context, tenantID, sessionID string, encoded []byte) (sessions.Session, error) { @@ -61,7 +61,7 @@ func (s *Store) UpdateSessionMetadata(ctx context.Context, tenantID, sessionID s if err != nil { return err } - if err := auditpg.RecordWriteAudit(ctx, q, tenantID, "update", "session", uuid.UUID(row.ID.Bytes).String(), ""); err != nil { + if err := auditpg.RecordWriteAudit(ctx, q, tenantID, writeaudit.ActionUpdate, writeaudit.ResourceSession, uuid.UUID(row.ID.Bytes).String(), ""); err != nil { return err } if session, err = sessionFromRow(row); err != nil { @@ -81,6 +81,6 @@ func (s *Store) UpdateSessionMetadata(ctx context.Context, tenantID, sessionID s func (s *Store) AuditSessionOperation(ctx context.Context, tenantID, sessionID, action string) error { return s.withPublicSession(ctx, tenantID, sessionID, func(ctx context.Context, q *sqlc.Queries, session pgtype.UUID) error { - return auditpg.RecordWriteAudit(ctx, q, tenantID, action, "session", uuid.UUID(session.Bytes).String(), "") + return auditpg.RecordWriteAudit(ctx, q, tenantID, writeaudit.Action(action), writeaudit.ResourceSession, uuid.UUID(session.Bytes).String(), "") }) } diff --git a/services/core/internal/persistence/postgres/skillpg/skillpg.go b/services/core/internal/persistence/postgres/skillpg/skillpg.go index b65dfd8eb..cc3089737 100644 --- a/services/core/internal/persistence/postgres/skillpg/skillpg.go +++ b/services/core/internal/persistence/postgres/skillpg/skillpg.go @@ -55,9 +55,9 @@ func (s *Store) CreateSkill(ctx context.Context, in skills.NewSkill) (skills.Ski return err } result = skillFromRow(row) - return auditpg.RecordWriteAudit(ctx, q, in.TenantID, "create", "skill", result.ID, "", - writeaudit.Resource{Type: "skill", ID: result.ID}, - writeaudit.Resource{Type: "skill_version", ID: initial.ID, ParentID: result.ID}) + return auditpg.RecordWriteAudit(ctx, q, in.TenantID, writeaudit.ActionCreate, writeaudit.ResourceSkill, result.ID, "", + writeaudit.Resource{Type: writeaudit.ResourceSkill, ID: result.ID}, + writeaudit.Resource{Type: writeaudit.ResourceSkillVersion, ID: initial.ID, ParentID: result.ID}) }) if err != nil { return skills.Skill{}, translate(err) @@ -87,8 +87,8 @@ func (s *Store) CreateVersion(ctx context.Context, in skills.NewVersion) (skills if err := q.AdvanceSkillVersion(ctx, sqlc.AdvanceSkillVersionParams{TenantID: owner.TenantID, ID: owner.ID, MakeDefault: in.MakeDefault, Name: in.Name, Description: in.Description}); err != nil { return err } - return auditpg.RecordWriteAudit(ctx, q, in.TenantID, "upload_version", "skill_version", result.ID, result.SkillID, - writeaudit.Resource{Type: "skill_version", ID: result.ID, ParentID: result.SkillID}) + return auditpg.RecordWriteAudit(ctx, q, in.TenantID, writeaudit.ActionUploadVersion, writeaudit.ResourceSkillVersion, result.ID, result.SkillID, + writeaudit.Resource{Type: writeaudit.ResourceSkillVersion, ID: result.ID, ParentID: result.SkillID}) }) if err != nil { return skills.Version{}, translate(err) @@ -116,7 +116,7 @@ func (s *Store) SetDefaultVersion(ctx context.Context, tenantID string, skillID return err } result = skillFromRow(row) - return auditpg.RecordWriteAudit(ctx, q, tenantID, "update_default_version", "skill", result.ID, "") + return auditpg.RecordWriteAudit(ctx, q, tenantID, writeaudit.ActionUpdateDefaultVersion, writeaudit.ResourceSkill, result.ID, "") }) if err != nil { return skills.Skill{}, translate(err) @@ -134,7 +134,7 @@ func (s *Store) DeleteSkill(ctx context.Context, tenantID string, skillID uuid.U if _, err := q.DeleteSkill(ctx, sqlc.DeleteSkillParams{TenantID: tenant, ID: pgID(skillID)}); err != nil { return err } - return auditpg.RecordWriteAudit(ctx, q, tenantID, "delete", "skill", skills.FormatID(skillID), "") + return auditpg.RecordWriteAudit(ctx, q, tenantID, writeaudit.ActionDelete, writeaudit.ResourceSkill, skills.FormatID(skillID), "") }) return translate(err) } @@ -194,7 +194,7 @@ func (d *versionDeletion) ApplyVersionDeletion(decision skills.VersionDeletion) } } } - return translate(auditpg.RecordWriteAudit(d.ctx, d.q, d.tenantID, "delete", "skill_version", decision.Target.ID, decision.Target.SkillID)) + return translate(auditpg.RecordWriteAudit(d.ctx, d.q, d.tenantID, writeaudit.ActionDelete, writeaudit.ResourceSkillVersion, decision.Target.ID, decision.Target.SkillID)) } func (s *Store) Skill(ctx context.Context, tenantID string, id uuid.UUID) (skills.Skill, error) { diff --git a/services/core/internal/persistence/postgres/templatepg/store.go b/services/core/internal/persistence/postgres/templatepg/store.go index b4afbb0dc..953324da9 100644 --- a/services/core/internal/persistence/postgres/templatepg/store.go +++ b/services/core/internal/persistence/postgres/templatepg/store.go @@ -23,7 +23,7 @@ import ( ) // resource names Templates in credential bindings and audit rows. -const resource = "environment_template" +const resource = string(writeaudit.ResourceEnvironmentTemplate) // Store implements environmenttemplates.Storage and environmenttemplates.Reader. type Store struct { @@ -62,7 +62,7 @@ func (s *Store) Create(ctx context.Context, tenantID string, in environmenttempl if result, err = template(metadataRow(row)); err != nil { return err } - return auditpg.RecordWriteAudit(ctx, q, tenantID, "create", resource, result.ID, "", writeaudit.Resource{Type: resource, ID: result.ID}) + return auditpg.RecordWriteAudit(ctx, q, tenantID, writeaudit.ActionCreate, writeaudit.ResourceEnvironmentTemplate, result.ID, "", writeaudit.Resource{Type: writeaudit.ResourceEnvironmentTemplate, ID: result.ID}) }) return result, storageError(err) } @@ -90,7 +90,7 @@ func (s *Store) Update(ctx context.Context, tenantID, templateID string, in envi if result, err = template(metadataRow(row)); err != nil { return err } - return auditpg.RecordWriteAudit(ctx, q, tenantID, "update", resource, result.ID, "") + return auditpg.RecordWriteAudit(ctx, q, tenantID, writeaudit.ActionUpdate, writeaudit.ResourceEnvironmentTemplate, result.ID, "") }) return result, storageError(err) } @@ -108,7 +108,7 @@ func (s *Store) Delete(ctx context.Context, tenantID, templateID string) (string return err } deleted = uuid.UUID(id.Bytes).String() - return auditpg.RecordWriteAudit(ctx, q, tenantID, "delete", resource, deleted, "") + return auditpg.RecordWriteAudit(ctx, q, tenantID, writeaudit.ActionDelete, writeaudit.ResourceEnvironmentTemplate, deleted, "") }) if err != nil { return "", storageError(err) diff --git a/services/core/internal/persistence/postgres/vaultpg/credentials.go b/services/core/internal/persistence/postgres/vaultpg/credentials.go index 478325ddd..852c0448e 100644 --- a/services/core/internal/persistence/postgres/vaultpg/credentials.go +++ b/services/core/internal/persistence/postgres/vaultpg/credentials.go @@ -5,16 +5,15 @@ import ( "encoding/json" "errors" - "github.com/google/uuid" - "github.com/jackc/pgx/v5/pgconn" - "github.com/jackc/pgx/v5/pgtype" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/auditpg" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/vaults" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/writeaudit" + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgtype" ) // CreateCredential admits the owning Vault in the insert itself, so a missing, @@ -46,8 +45,8 @@ func (s *Store) CreateCredential(ctx context.Context, credential vaults.NewCrede if created, err = credentialFromRow(row); err != nil { return err } - return auditpg.RecordWriteAudit(ctx, q, credential.TenantID, "create", "credential", created.ID, created.VaultID, - writeaudit.Resource{Type: "credential", ID: created.ID, ParentID: created.VaultID}) + return auditpg.RecordWriteAudit(ctx, q, credential.TenantID, writeaudit.ActionCreate, writeaudit.ResourceCredential, created.ID, created.VaultID, + writeaudit.Resource{Type: writeaudit.ResourceCredential, ID: created.ID, ParentID: created.VaultID}) }) if err != nil { return vaults.Credential{}, err @@ -180,7 +179,7 @@ func (s *Store) ReplaceStaticToken(ctx context.Context, replacement vaults.Stati if err != nil { return err } - return auditpg.RecordWriteAudit(ctx, q, replacement.TenantID, "update", "credential", updated.ID, updated.VaultID) + return auditpg.RecordWriteAudit(ctx, q, replacement.TenantID, writeaudit.ActionUpdate, writeaudit.ResourceCredential, updated.ID, updated.VaultID) }) if err != nil { return vaults.Credential{}, err @@ -198,7 +197,7 @@ func (s *Store) DeleteCredential(ctx context.Context, key vaults.CredentialKey) return err } deleted = uuid.UUID(id.Bytes).String() - return auditpg.RecordWriteAudit(ctx, q, key.TenantID, "delete", "credential", deleted, uuid.UUID(vault.Bytes).String()) + return auditpg.RecordWriteAudit(ctx, q, key.TenantID, writeaudit.ActionDelete, writeaudit.ResourceCredential, deleted, uuid.UUID(vault.Bytes).String()) }) if err != nil { return "", err diff --git a/services/core/internal/persistence/postgres/vaultpg/oauth.go b/services/core/internal/persistence/postgres/vaultpg/oauth.go index 07746fe9d..4131ed111 100644 --- a/services/core/internal/persistence/postgres/vaultpg/oauth.go +++ b/services/core/internal/persistence/postgres/vaultpg/oauth.go @@ -4,14 +4,14 @@ import ( "context" "errors" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgtype" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/auditpg" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/vaults" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/writeaudit" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" ) // WithOAuthCredential holds the Credential's row lock, once loaded, until @@ -64,7 +64,7 @@ func (t *oauthTx) ApplyOAuthReplacement(ctx context.Context, grant vaults.OAuthG if err != nil { return vaults.Credential{}, err } - if err := auditpg.RecordWriteAudit(ctx, t.q, t.tenantID, "update", "credential", updated.ID, updated.VaultID); err != nil { + if err := auditpg.RecordWriteAudit(ctx, t.q, t.tenantID, writeaudit.ActionUpdate, writeaudit.ResourceCredential, updated.ID, updated.VaultID); err != nil { return vaults.Credential{}, translate(err) } return updated, nil diff --git a/services/core/internal/persistence/postgres/vaultpg/vaultpg.go b/services/core/internal/persistence/postgres/vaultpg/vaultpg.go index 1adb7e834..51bf0e2b0 100644 --- a/services/core/internal/persistence/postgres/vaultpg/vaultpg.go +++ b/services/core/internal/persistence/postgres/vaultpg/vaultpg.go @@ -6,10 +6,6 @@ import ( "encoding/json" "errors" - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgtype" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/auditpg" @@ -17,6 +13,9 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/textvalue" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/vaults" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/writeaudit" + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" ) // Store runs on pooled connections and seals and opens the Credential @@ -81,7 +80,7 @@ func (s *Store) CreateVault(ctx context.Context, vault vaults.NewVault) (vaults. if err != nil { return err } - return auditpg.RecordWriteAudit(ctx, q, vault.TenantID, "create", "vault", created.ID, "", writeaudit.Resource{Type: "vault", ID: created.ID}) + return auditpg.RecordWriteAudit(ctx, q, vault.TenantID, writeaudit.ActionCreate, writeaudit.ResourceVault, created.ID, "", writeaudit.Resource{Type: writeaudit.ResourceVault, ID: created.ID}) }) if err != nil { return vaults.Vault{}, err @@ -164,7 +163,7 @@ func (s *Store) DeleteVault(ctx context.Context, tenantID, vaultID string) (stri return err } deleted = uuid.UUID(id.Bytes).String() - return auditpg.RecordWriteAudit(ctx, q, tenantID, "delete", "vault", deleted, "") + return auditpg.RecordWriteAudit(ctx, q, tenantID, writeaudit.ActionDelete, writeaudit.ResourceVault, deleted, "") }) if err != nil { return "", err diff --git a/services/core/internal/processconfig/config.go b/services/core/internal/processconfig/config.go index 23ab40c92..6a5b75edf 100644 --- a/services/core/internal/processconfig/config.go +++ b/services/core/internal/processconfig/config.go @@ -18,9 +18,6 @@ import ( "strings" "time" - "github.com/google/uuid" - "golang.org/x/net/http/httpguts" - "github.com/MiniMax-AI/OpenAgentCore/internal/obs/log" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" @@ -30,6 +27,8 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/oauthrefresh" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" + "github.com/google/uuid" + "golang.org/x/net/http/httpguts" ) const ( @@ -167,16 +166,16 @@ func (c Config) Settings() []api.InstallationSetting { origins = []string{} } return []api.InstallationSetting{ - setting("public_url", c.PublicOrigin.String(), nil, []string{"core", "web"}), - setting("log.level", strings.ToLower(c.Log.Level.String()), "info", []string{"core", "web"}), - setting("log.format", format, "auto", []string{"core", "web"}), - setting("log.add_source", c.Log.AddSource, false, []string{"core", "web"}), - setting("core.execution_concurrency", c.ExecutionConcurrency, execution.DefaultExecutionConcurrency, []string{"core"}), - setting("core.harnesses", c.Harnesses, (engine.Catalog{}).Kinds(), []string{"core"}), - setting("core.default_harness", c.DefaultHarness, defaultHarness, []string{"core"}), - setting("core.write_audit_retention", duration(c.WriteAuditRetention), duration(defaultWriteAuditRetention), []string{"core"}), - setting("core.oauth_trusted_origins", origins, []string{}, []string{"core"}), - sensitive("core.runtime_history", c.RuntimeHistory.File != "", []string{"core"}), + setting("public_url", c.PublicOrigin.String(), nil, []api.InstallationService{api.InstallationCore, api.InstallationWeb}), + setting("log.level", strings.ToLower(c.Log.Level.String()), "info", []api.InstallationService{api.InstallationCore, api.InstallationWeb}), + setting("log.format", format, "auto", []api.InstallationService{api.InstallationCore, api.InstallationWeb}), + setting("log.add_source", c.Log.AddSource, false, []api.InstallationService{api.InstallationCore, api.InstallationWeb}), + setting("core.execution_concurrency", c.ExecutionConcurrency, execution.DefaultExecutionConcurrency, []api.InstallationService{api.InstallationCore}), + setting("core.harnesses", c.Harnesses, (engine.Catalog{}).Kinds(), []api.InstallationService{api.InstallationCore}), + setting("core.default_harness", c.DefaultHarness, defaultHarness, []api.InstallationService{api.InstallationCore}), + setting("core.write_audit_retention", duration(c.WriteAuditRetention), duration(defaultWriteAuditRetention), []api.InstallationService{api.InstallationCore}), + setting("core.oauth_trusted_origins", origins, []string{}, []api.InstallationService{api.InstallationCore}), + sensitive("core.runtime_history", c.RuntimeHistory.File != "", []api.InstallationService{api.InstallationCore}), } } @@ -193,11 +192,11 @@ func duration(d time.Duration) string { return s } -func setting(key string, value, fallback any, restarts []string) api.InstallationSetting { +func setting(key string, value, fallback any, restarts []api.InstallationService) api.InstallationSetting { return api.InstallationSetting{Key: key, Value: value, Default: fallback, Changeable: true, Sensitive: false, Restarts: restarts} } -func sensitive(key string, configured bool, restarts []string) api.InstallationSetting { +func sensitive(key string, configured bool, restarts []api.InstallationService) api.InstallationSetting { return api.InstallationSetting{Key: key, Configured: &configured, Changeable: true, Sensitive: true, Restarts: restarts} } diff --git a/services/core/internal/sandbox/deployment.go b/services/core/internal/sandbox/deployment.go index 4b96acb13..42f9d38ca 100644 --- a/services/core/internal/sandbox/deployment.go +++ b/services/core/internal/sandbox/deployment.go @@ -9,6 +9,14 @@ import ( "regexp" ) +type DeploymentMode string + +const ( + DeploymentUnconfigured DeploymentMode = "" + DeploymentNodes DeploymentMode = "nodes" + DeploymentDirect DeploymentMode = "direct" +) + // ValidationError preserves the sandbox error text and identity while identifying // a fixed configuration field and, for numeric limits, fixed inclusive bounds. type ValidationError struct { @@ -111,7 +119,8 @@ func (s DeploymentSpec) Digest(provider string) string { // Description is what a provider registration says about a deployment of it: // its mode and its backend namespace fingerprint. type Description struct { - Mode, BackendFingerprint string + Mode DeploymentMode + BackendFingerprint string } func BackendFingerprint(kind, namespace string) string { diff --git a/services/core/internal/sandbox/deployment_contract.go b/services/core/internal/sandbox/deployment_contract.go index da40c7ca3..ed929f458 100644 --- a/services/core/internal/sandbox/deployment_contract.go +++ b/services/core/internal/sandbox/deployment_contract.go @@ -41,7 +41,7 @@ var runtimeContract = []runtimeRule{ // ProviderProjection is one registered Provider in the generated projections. type ProviderProjection struct { - Mode string `json:"mode"` + Mode DeploymentMode `json:"mode"` DeploymentPolicy } diff --git a/services/core/internal/sandbox/node/agent.go b/services/core/internal/sandbox/node/agent.go index cb86b6eac..4a5c4abbf 100644 --- a/services/core/internal/sandbox/node/agent.go +++ b/services/core/internal/sandbox/node/agent.go @@ -164,7 +164,7 @@ func (a *agent) health(ctx context.Context, host *hostHealthSampler) (Health, er h.Diagnostic = sandbox.NodeDiagnostic(e) if e != nil && ctx.Err() != nil { // A closing connection cancelled the probe; that says nothing about the provider. - h.Diagnostic = sandbox.NodeProviderUnavailable + h.Diagnostic = string(sandbox.NodeProviderUnavailable) } else { wasReady := a.ready.Swap(h.ProviderReady) seen := a.healthSeen.Swap(true) diff --git a/services/core/internal/sandbox/node/generations.go b/services/core/internal/sandbox/node/generations.go index f538f0643..83354a61a 100644 --- a/services/core/internal/sandbox/node/generations.go +++ b/services/core/internal/sandbox/node/generations.go @@ -75,7 +75,7 @@ func NewGenerationManager(ctx context.Context, options GenerationManagerOptions) cancel() return nil, sandbox.ErrInvalid } - m.values[ref.Generation] = &localGeneration{value: GenerationProvider{Generation: ref.Generation, SpecificationDigest: ref.SpecificationDigest}, state: "failed", diagnostic: sandbox.NodeProviderUnavailable, repairing: true} + m.values[ref.Generation] = &localGeneration{value: GenerationProvider{Generation: ref.Generation, SpecificationDigest: ref.SpecificationDigest}, state: "failed", diagnostic: string(sandbox.NodeProviderUnavailable), repairing: true} } for _, ref := range options.Collect { if !validGeneration(ref.Generation) || !validSpecificationDigest(ref.SpecificationDigest) || m.values[ref.Generation] != nil { diff --git a/services/core/internal/sandbox/node_diagnostic.go b/services/core/internal/sandbox/node_diagnostic.go index efcdedc58..15527a4d2 100644 --- a/services/core/internal/sandbox/node_diagnostic.go +++ b/services/core/internal/sandbox/node_diagnostic.go @@ -32,19 +32,27 @@ func CheckCapacity(r Resources, cpus int, memory uint64) error { return nil } -// NodeProviderUnavailable also reports every readiness failure without a class. -const NodeProviderUnavailable = "provider_unavailable" +type NodeDiagnosticCode string + +const ( + NodeProviderUnavailable NodeDiagnosticCode = "provider_unavailable" + NodeHostUnsupported NodeDiagnosticCode = "host_unsupported" + NodeArtifactsUnavailable NodeDiagnosticCode = "artifacts_unavailable" + NodeRuntimeDownloadFailed NodeDiagnosticCode = "runtime_download_failed" + NodeRuntimeImageUnavailable NodeDiagnosticCode = "runtime_image_unavailable" + NodeCapacityInsufficient NodeDiagnosticCode = "capacity_insufficient" +) var nodeDiagnostics = []struct { err error - code string + code NodeDiagnosticCode }{ {ErrProviderUnavailable, NodeProviderUnavailable}, - {ErrHostUnsupported, "host_unsupported"}, - {ErrArtifactsUnavailable, "artifacts_unavailable"}, - {ErrRuntimeDownloadFailed, "runtime_download_failed"}, - {ErrRuntimeImageUnavailable, "runtime_image_unavailable"}, - {ErrCapacityInsufficient, "capacity_insufficient"}, + {ErrHostUnsupported, NodeHostUnsupported}, + {ErrArtifactsUnavailable, NodeArtifactsUnavailable}, + {ErrRuntimeDownloadFailed, NodeRuntimeDownloadFailed}, + {ErrRuntimeImageUnavailable, NodeRuntimeImageUnavailable}, + {ErrCapacityInsufficient, NodeCapacityInsufficient}, } // NodeDiagnostic maps a readiness probe result to its class code: empty when @@ -55,10 +63,10 @@ func NodeDiagnostic(err error) string { } for _, d := range nodeDiagnostics { if errors.Is(err, d.err) { - return d.code + return string(d.code) } } - return NodeProviderUnavailable + return string(NodeProviderUnavailable) } // NormalizeNodeDiagnostic keeps an empty or known code. Any other reported value @@ -68,9 +76,9 @@ func NormalizeNodeDiagnostic(code string) string { return code } for _, d := range nodeDiagnostics { - if code == d.code { + if code == string(d.code) { return code } } - return NodeProviderUnavailable + return string(NodeProviderUnavailable) } diff --git a/services/core/internal/sandbox/node_diagnostic_test.go b/services/core/internal/sandbox/node_diagnostic_test.go index 84b199cd6..5da89625a 100644 --- a/services/core/internal/sandbox/node_diagnostic_test.go +++ b/services/core/internal/sandbox/node_diagnostic_test.go @@ -20,12 +20,12 @@ func TestNodeDiagnosticContract(t *testing.T) { } var codes []string for _, diagnostic := range nodeDiagnostics { - codes = append(codes, diagnostic.code) - if got := NodeDiagnostic(fmt.Errorf("private probe detail: %w", diagnostic.err)); got != diagnostic.code { - t.Errorf("wrapped readiness cause = %q, want %q", got, diagnostic.code) + codes = append(codes, string(diagnostic.code)) + if got := NodeDiagnostic(fmt.Errorf("private probe detail: %w", diagnostic.err)); got != string(diagnostic.code) { + t.Errorf("wrapped readiness cause = %q, want %q", got, string(diagnostic.code)) } - if got := NormalizeNodeDiagnostic(diagnostic.code); got != diagnostic.code { - t.Errorf("normalized readiness cause = %q, want %q", got, diagnostic.code) + if got := NormalizeNodeDiagnostic(string(diagnostic.code)); got != string(diagnostic.code) { + t.Errorf("normalized readiness cause = %q, want %q", got, string(diagnostic.code)) } } slices.Sort(codes) @@ -36,7 +36,7 @@ func TestNodeDiagnosticContract(t *testing.T) { if NodeDiagnostic(nil) != "" || NormalizeNodeDiagnostic("") != "" { t.Fatal("ready state must have no diagnostic") } - if NodeDiagnostic(errors.New("private probe detail")) != NodeProviderUnavailable || NormalizeNodeDiagnostic("future_code") != NodeProviderUnavailable { + if NodeDiagnostic(errors.New("private probe detail")) != string(NodeProviderUnavailable) || NormalizeNodeDiagnostic("future_code") != string(NodeProviderUnavailable) { t.Fatal("unknown causes must remain provider_unavailable") } } diff --git a/services/core/internal/sandbox/providers/artifacts.go b/services/core/internal/sandbox/providers/artifacts.go index 8db3baa16..d92cc90a0 100644 --- a/services/core/internal/sandbox/providers/artifacts.go +++ b/services/core/internal/sandbox/providers/artifacts.go @@ -7,6 +7,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/providerassets" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/providercontract" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" ) var nodeProgram = providerassets.Artifact{Path: "native/bin/oac-node", Suffix: "sandbox-node", Role: "node"} @@ -23,7 +24,7 @@ func (r *Registry) ArtifactCatalog() (map[string][]providerassets.Artifact, erro if err != nil { return nil, err } - if a.Mode == "nodes" { + if a.Mode == sandbox.DeploymentNodes { for _, item := range a.NodeArtifacts { previous, exists := paths[item.Path] if exists && previous != item || suffixes[item.Suffix] != "" && suffixes[item.Suffix] != item.Path { diff --git a/services/core/internal/sandbox/providers/config.go b/services/core/internal/sandbox/providers/config.go index 07b0359b1..f530a71e1 100644 --- a/services/core/internal/sandbox/providers/config.go +++ b/services/core/internal/sandbox/providers/config.go @@ -39,7 +39,7 @@ func (r *Registry) Build(config sandbox.NodeConfig, options sandbox.LocalOptions if err != nil { return nil, closeProvider, err } - if adapter.Mode != "nodes" { + if adapter.Mode != sandbox.DeploymentNodes { return nil, closeProvider, fmt.Errorf("%w: selected provider does not support node hosting", sandbox.ErrInvalid) } if config.Generation == 0 { diff --git a/services/core/internal/sandbox/providers/registration.go b/services/core/internal/sandbox/providers/registration.go index 2cf638f56..a4d68dea2 100644 --- a/services/core/internal/sandbox/providers/registration.go +++ b/services/core/internal/sandbox/providers/registration.go @@ -15,14 +15,14 @@ func ValidateRegistration(a Adapter) error { return fmt.Errorf("%w: invalid registration %s", providercontract.ErrContract, field) } switch a.Mode { - case "nodes": + case sandbox.DeploymentNodes: if err := validateNodeArtifacts(a.NodeArtifacts); err != nil { return err } if a.BuildLocal == nil || a.BuildDirect != nil { return invalid("node constructor") } - case "direct": + case sandbox.DeploymentDirect: if len(a.NodeArtifacts) != 0 { return invalid("direct node artifacts") } @@ -53,7 +53,7 @@ func ValidateRegistration(a Adapter) error { } // The common lifecycle suspends only node allocations, so only a nodes // registration may declare checkpoint support. - if operations["Initial"].State == providercontract.Supported && a.Mode != "nodes" { + if operations["Initial"].State == providercontract.Supported && a.Mode != sandbox.DeploymentNodes { return invalid("checkpoint support outside nodes mode") } if a.Policy.DefaultResources != nil && a.ValidateResources(*a.Policy.DefaultResources) != nil { diff --git a/services/core/internal/sandbox/providers/registry.go b/services/core/internal/sandbox/providers/registry.go index fe936c38a..a39c0429f 100644 --- a/services/core/internal/sandbox/providers/registry.go +++ b/services/core/internal/sandbox/providers/registry.go @@ -7,7 +7,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/providerassets" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/providercontract" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/docker" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/e2b" @@ -22,7 +21,7 @@ type Adapter struct { Configuration sandbox.ConfigurationAdapter BuildLocal func(sandbox.NodeConfig, sandbox.LocalOptions, *sandbox.Built) (func(), error) BuildDirect func(sandbox.DirectConfig) (sandbox.SandboxProvider, error) - Mode string + Mode sandbox.DeploymentMode Operations func() providercontract.Operations ValidateSpecification func(sandbox.DeploymentSpec) error ValidateResources func(sandbox.Resources) error @@ -40,18 +39,18 @@ func Builtin() *Registry { return &Registry{adapters: map[string]Adapter{ "docker": { NodeArtifacts: []providerassets.Artifact{nodeProgram, runtimeImage, runtimePolicy}, - Policy: docker.Policy(), Operations: docker.Operations, Mode: "nodes", BuildLocal: docker.BuildNode, + Policy: docker.Policy(), Operations: docker.Operations, Mode: sandbox.DeploymentNodes, BuildLocal: docker.BuildNode, ValidateSpecification: docker.ValidateSpecification, ValidateResources: docker.ValidateResources, Configuration: nodeConfigurationAdapter{docker.ValidateSpecification}, }, "microsandbox": { NodeArtifacts: append([]providerassets.Artifact{nodeProgram, runtimeImage, runtimePolicy}, microsandbox.NodeArtifacts...), - Policy: microsandbox.Policy(), Operations: microsandbox.Operations, Mode: "nodes", BuildLocal: microsandbox.BuildNode, + Policy: microsandbox.Policy(), Operations: microsandbox.Operations, Mode: sandbox.DeploymentNodes, BuildLocal: microsandbox.BuildNode, ValidateSpecification: microsandbox.ValidateSpecification, ValidateResources: microsandbox.ValidateResources, Configuration: nodeConfigurationAdapter{microsandbox.ValidateSpecification}, }, "e2b": { - Policy: e2b.Policy(), Operations: e2b.Operations, Mode: "direct", BuildDirect: e2b.BuildDirect, + Policy: e2b.Policy(), Operations: e2b.Operations, Mode: sandbox.DeploymentDirect, BuildDirect: e2b.BuildDirect, Configuration: e2b.ConfigurationAdapter{}, ValidateSpecification: e2b.ValidateSpecification, ValidateResources: e2b.ValidateResources, }, @@ -104,7 +103,7 @@ func (r *Registry) RetainedLimit(kind string, active, retained int) (int, error) if err != nil { return 0, err } - if a.Mode == "nodes" && a.Operations()["Initial"].State != providercontract.Supported { + if a.Mode == sandbox.DeploymentNodes && a.Operations()["Initial"].State != providercontract.Supported { return active, nil } return retained, nil @@ -132,8 +131,8 @@ func (r *Registry) Describe(kind, installation string) (sandbox.Description, err if e != nil { return sandbox.Description{}, e } - namespace := a.Mode - if a.Mode == "direct" { + namespace := string(a.Mode) + if a.Mode == sandbox.DeploymentDirect { namespace = kind } return sandbox.Description{Mode: a.Mode, BackendFingerprint: sandbox.BackendFingerprint(kind, namespace+":"+installation)}, nil diff --git a/services/core/internal/sandbox/providers/registry_test.go b/services/core/internal/sandbox/providers/registry_test.go index bd6d1ac4f..47a0629bb 100644 --- a/services/core/internal/sandbox/providers/registry_test.go +++ b/services/core/internal/sandbox/providers/registry_test.go @@ -2,11 +2,12 @@ package providers import ( "errors" + "testing" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/providercontract" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/e2b" "github.com/google/uuid" - "testing" ) func TestRegistrationOwnsDeploymentPolicy(t *testing.T) { @@ -22,7 +23,7 @@ func TestRegistrationOwnsDeploymentPolicy(t *testing.T) { } { t.Run(tc.kind, func(t *testing.T) { d, err := registry.Describe(tc.kind, installation) - if err != nil || d.Mode != tc.mode || d.BackendFingerprint != sandbox.BackendFingerprint(tc.kind, tc.namespace+":"+installation) { + if err != nil || string(d.Mode) != tc.mode || d.BackendFingerprint != sandbox.BackendFingerprint(tc.kind, tc.namespace+":"+installation) { t.Fatalf("wrong mode or namespace: %+v %v", d, err) } a, err := registry.Lookup(tc.kind) diff --git a/services/core/internal/sessions/creation.go b/services/core/internal/sessions/creation.go index f53c6fca9..effe001a4 100644 --- a/services/core/internal/sessions/creation.go +++ b/services/core/internal/sessions/creation.go @@ -12,8 +12,6 @@ import ( "strconv" "strings" - "github.com/google/uuid" - v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment/placement" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" @@ -22,6 +20,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/metadata" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/skills" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/writeaudit" + "github.com/google/uuid" ) // CreationStorage persists Session creation and finds an earlier one. @@ -204,7 +203,7 @@ func (s *Service) createResources(ctx context.Context, tx CreationTx, session Se return nil, err } } - created := []writeaudit.Resource{{Type: "session", ID: session.ID}} + created := []writeaudit.Resource{{Type: writeaudit.ResourceSession, ID: session.ID}} creates, err := createsEnvironment(session.Configuration) if err != nil { return nil, err @@ -214,7 +213,7 @@ func (s *Service) createResources(ctx context.Context, tx CreationTx, session Se if environment, err = tx.CreateEnvironment(ctx); err != nil { return nil, err } - created = append(created, writeaudit.Resource{Type: "environment", ID: environment, ParentID: session.ID}) + created = append(created, writeaudit.Resource{Type: writeaudit.ResourceEnvironment, ID: environment, ParentID: session.ID}) } if hosted { nodes, err := tx.LoadNodes(ctx) @@ -299,7 +298,7 @@ func freezeSkills(ctx context.Context, tx CreationTx, setup environmentconfig.Se // and its provider bundle and completes it as stored. It also returns the // deployment provider revision to record, uuid.Nil for none. The projection is // creation metadata, never retry identity. -func freezeProjection(session Session, frozen v1.SessionExecutionConfiguration, provider *v1.ModelProviderInput, providerSource string, revision uuid.UUID) (v1.SessionExecutionConfiguration, uuid.UUID, error) { +func freezeProjection(session Session, frozen v1.SessionExecutionConfiguration, provider *v1.ModelProviderInput, providerSource v1.ExecutionSource, revision uuid.UUID) (v1.SessionExecutionConfiguration, uuid.UUID, error) { model, err := ExecutionModel(session.Configuration) if err != nil { return v1.SessionExecutionConfiguration{}, uuid.Nil, err @@ -310,24 +309,24 @@ func freezeProjection(session Session, frozen v1.SessionExecutionConfiguration, } recorded := uuid.Nil switch frozen.ModelProvider.Source { - case "deployment": + case v1.ExecutionSourceDeployment: if provider == nil { return v1.SessionExecutionConfiguration{}, uuid.Nil, fmt.Errorf("%w: execution projection has no model provider", ErrInvalidInput) } // The deployment default is readable with the same Core key, so the // safe view is recorded from the frozen bundle itself. Native options // are never part of it. - frozen.ModelProvider.Status = "available" + frozen.ModelProvider.Status = v1.ExecutionProviderAvailable frozen.ModelProvider.Configuration = provider.SafeView() - if providerSource == v1.ModelProviderSourceDeployment { + if providerSource == v1.ExecutionSourceDeployment { recorded = revision } - case "session", "agent": - if provider == nil || frozen.ModelProvider.Status != "available" || frozen.ModelProvider.Configuration == nil || *frozen.ModelProvider.Configuration != *provider.SafeView() { + case v1.ExecutionSourceSession, v1.ExecutionSourceAgent: + if provider == nil || frozen.ModelProvider.Status != v1.ExecutionProviderAvailable || frozen.ModelProvider.Configuration == nil || *frozen.ModelProvider.Configuration != *provider.SafeView() { return v1.SessionExecutionConfiguration{}, uuid.Nil, fmt.Errorf("%w: execution projection does not match model provider", ErrInvalidInput) } - case "unknown": - frozen.ModelProvider.Status = "unavailable" + case v1.ExecutionSourceUnknown: + frozen.ModelProvider.Status = v1.ExecutionProviderUnavailable frozen.ModelProvider.Configuration = nil default: return v1.SessionExecutionConfiguration{}, uuid.Nil, fmt.Errorf("%w: invalid execution projection source", ErrInvalidInput) @@ -340,9 +339,9 @@ func sameExecutionValue(a, b *string) bool { return a == nil && b == nil || a != nil && b != nil && *a == *b } -func validExecutionSource(source string) bool { +func validExecutionSource(source v1.ExecutionSource) bool { switch source { - case "session", "agent", "deployment", "unknown": + case v1.ExecutionSourceSession, v1.ExecutionSourceAgent, v1.ExecutionSourceDeployment, v1.ExecutionSourceUnknown: return true } return false @@ -411,7 +410,7 @@ func prepareCreation(input CreateSession, fingerprint func(string) (string, erro // is set, replaced or removed. var fingerprinted *v1.ModelProviderInput hashed := configuration - if input.ModelProviderSource == v1.ModelProviderSourceDeployment { + if input.ModelProviderSource == v1.ExecutionSourceDeployment { hashed = requested } else if fingerprinted, err = fingerprintedProvider(input.ModelProvider, fingerprint); err != nil { return NewSession{}, nil, nil, err diff --git a/services/core/internal/sessions/creation_test.go b/services/core/internal/sessions/creation_test.go index 7d9cc4524..16f7359ba 100644 --- a/services/core/internal/sessions/creation_test.go +++ b/services/core/internal/sessions/creation_test.go @@ -11,8 +11,6 @@ import ( "strings" "testing" - "github.com/google/uuid" - v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment/placement" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" @@ -20,6 +18,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/skills" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/writeaudit" + "github.com/google/uuid" ) // fakeCreationTx is fakeInputTx with the creation methods, equally strict. @@ -80,7 +79,7 @@ func (f *fakeCreationTx) SaveModelExecution(_ context.Context, provider v1.Model } func (f *fakeCreationTx) SaveExecutionConfiguration(_ context.Context, projection v1.SessionExecutionConfiguration, revision uuid.UUID) error { - f.record("SaveExecutionConfiguration", f.saveExecutionConfiguration != nil, projection.ModelProvider.Status, revision.String()) + f.record("SaveExecutionConfiguration", f.saveExecutionConfiguration != nil, string(projection.ModelProvider.Status), revision.String()) return f.saveExecutionConfiguration() } @@ -111,7 +110,7 @@ func (f *fakeCreationTx) PruneChanges(context.Context) error { func (f *fakeCreationTx) AuditCreation(_ context.Context, created ...writeaudit.Resource) error { var resources []string for _, resource := range created { - resources = append(resources, strings.TrimSuffix(resource.Type+":"+resource.ID+":"+resource.ParentID, ":")) + resources = append(resources, strings.TrimSuffix(string(resource.Type)+":"+resource.ID+":"+resource.ParentID, ":")) } f.record("AuditCreation", f.auditCreation != nil, resources...) return f.auditCreation() @@ -176,7 +175,7 @@ func TestPrepareCreation(t *testing.T) { } return session } - withProvider := func(environment, key, source string) func(*CreateSession) { + withProvider := func(environment, key string, source v1.ExecutionSource) func(*CreateSession) { return func(input *CreateSession) { copy := *provider copy.APIKey = key @@ -204,7 +203,7 @@ func TestPrepareCreation(t *testing.T) { if prepare(t, withProvider("self_hosted", "one", "session"), fingerprints).RequestHash == prepare(t, withProvider("self_hosted", "two", "session"), fingerprints).RequestHash { t.Fatal("caller keys share an identity") } - deployed := prepare(t, withProvider("none", "deployment-key", v1.ModelProviderSourceDeployment), failing) + deployed := prepare(t, withProvider("none", "deployment-key", v1.ExecutionSourceDeployment), failing) plain := prepare(t, func(input *CreateSession) { input.Configuration = creationInput("none").Configuration }, failing) if !strings.Contains(string(deployed.Configuration), `"model_provider_configured":true`) || deployed.RequestHash != plain.RequestHash { t.Fatalf("the deployment default joined the identity: %s", deployed.Configuration) @@ -227,7 +226,7 @@ func TestPrepareCreation(t *testing.T) { }, fingerprints, ErrInvalidInput}, "configuration array": {func(input *CreateSession) { input.Configuration = json.RawMessage(`[]`) }, fingerprints, ErrInvalidInput}, "cancel initial input": {func(input *CreateSession) { input.InitialInputs = []Input{cancelInput} }, fingerprints, ErrInvalidInput}, - "deployment self_hosted": {withProvider("self_hosted", "key", v1.ModelProviderSourceDeployment), fingerprints, ErrInvalidInput}, + "deployment self_hosted": {withProvider("self_hosted", "key", v1.ExecutionSourceDeployment), fingerprints, ErrInvalidInput}, "fingerprint failure": {withProvider("self_hosted", "key", "session"), failing, errFingerprint}, "unreadable intent": {func(input *CreateSession) { input.CreationRequest = json.RawMessage(`[]`) }, fingerprints, ErrInvalidInput}, } { @@ -281,7 +280,7 @@ func TestFreezeProjection(t *testing.T) { model, harness := "gpt", "codex" session := Session{ID: "session", Engine: harness, Configuration: json.RawMessage(`{"agent":{"model":"gpt"}}`)} revision := uuid.New() - projection := func(source, status string, view *v1.ModelProviderView) v1.SessionExecutionConfiguration { + projection := func(source v1.ExecutionSource, status v1.ExecutionProviderStatus, view *v1.ModelProviderView) v1.SessionExecutionConfiguration { return v1.SessionExecutionConfiguration{ Model: v1.ExecutionSelection{Value: &model, Source: "session"}, Harness: v1.ExecutionSelection{Value: &harness, Source: "agent"}, ModelProvider: v1.ExecutionProviderSelection{Source: source, Status: status, Configuration: view}, @@ -293,13 +292,13 @@ func TestFreezeProjection(t *testing.T) { for name, test := range map[string]struct { projection v1.SessionExecutionConfiguration provider *v1.ModelProviderInput - source string - status string + source v1.ExecutionSource + status v1.ExecutionProviderStatus revision uuid.UUID want error hasProjected bool }{ - "deployment records its revision": {projection("deployment", "", nil), provider, v1.ModelProviderSourceDeployment, "available", revision, nil, true}, + "deployment records its revision": {projection("deployment", "", nil), provider, v1.ExecutionSourceDeployment, "available", revision, nil, true}, "deployment from another source": {projection("deployment", "", nil), provider, "session", "available", uuid.Nil, nil, true}, "deployment without a provider": {projection("deployment", "", nil), nil, "", "", uuid.Nil, ErrInvalidInput, false}, "caller provider matches": {projection("session", "available", provider.SafeView()), provider, "session", "available", uuid.Nil, nil, true}, diff --git a/services/core/internal/sessions/execution_configuration.go b/services/core/internal/sessions/execution_configuration.go index 6065dbe97..9349ada3a 100644 --- a/services/core/internal/sessions/execution_configuration.go +++ b/services/core/internal/sessions/execution_configuration.go @@ -16,15 +16,15 @@ func NormalizeExecutionProjection(projection *v1.SessionExecutionConfiguration, projection.Object = "agent.session.execution_configuration" projection.SchemaVersion = 1 if projection.HarnessConfig.Source == "" { - projection.HarnessConfig.Source = "unknown" + projection.HarnessConfig.Source = v1.ExecutionSourceUnknown } projection.HarnessConfig.Value = v1.ResolvedHarnessConfig(projection.HarnessConfig.Value) projection.SessionID = sessionID - if projection.ModelProvider.Source == "deployment" && (projection.ModelProvider.Status != "available" || projection.ModelProvider.Configuration == nil) { + if projection.ModelProvider.Source == v1.ExecutionSourceDeployment && (projection.ModelProvider.Status != v1.ExecutionProviderAvailable || projection.ModelProvider.Configuration == nil) { // Sessions created before deployment defaults moved into Core stay redacted. - projection.ModelProvider.Status = "redacted" + projection.ModelProvider.Status = v1.ExecutionProviderRedacted projection.ModelProvider.Configuration = nil - } else if projection.ModelProvider.Status != "available" { + } else if projection.ModelProvider.Status != v1.ExecutionProviderAvailable { projection.ModelProvider.Configuration = nil } } diff --git a/services/core/internal/sessions/reads.go b/services/core/internal/sessions/reads.go index 9f4a167bd..016220a83 100644 --- a/services/core/internal/sessions/reads.go +++ b/services/core/internal/sessions/reads.go @@ -8,6 +8,14 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" ) +type ManagedArchiveState string + +const ( + ManagedArchiveActive ManagedArchiveState = "active" + ManagedArchiveCleanupPending ManagedArchiveState = "cleanup_pending" + ManagedArchiveReleased ManagedArchiveState = "released" +) + // SessionReader reads Sessions, their public change journal and their // diagnostics. type SessionReader interface { @@ -67,7 +75,7 @@ type TurnDiagnosticsSnapshot struct { // ManagedArchive reports resource disposal, not archive request provenance // or Turn settlement. Existing expiry and failed provisioning use the same states. type ManagedArchive struct { - SessionID string `json:"session_id" binding:"required"` - EnvironmentID string `json:"environment_id" binding:"required"` - State string `json:"state" enums:"active,cleanup_pending,released" binding:"required"` + SessionID string `json:"session_id" binding:"required"` + EnvironmentID string `json:"environment_id" binding:"required"` + State ManagedArchiveState `json:"state" binding:"required"` } diff --git a/services/core/internal/sessions/session.go b/services/core/internal/sessions/session.go index eb43abedc..9ce349aa4 100644 --- a/services/core/internal/sessions/session.go +++ b/services/core/internal/sessions/session.go @@ -7,12 +7,12 @@ import ( "regexp" "time" - "github.com/google/uuid" - v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/metadata" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/writeaudit" + "github.com/google/uuid" ) var enginePattern = regexp.MustCompile(`^[a-z][a-z0-9_-]{0,63}$`) @@ -46,7 +46,7 @@ type CreateSession struct { DeploymentProviderRevision uuid.UUID `json:"-"` ExecutionConfiguration *v1.SessionExecutionConfiguration ModelProvider *v1.ModelProviderInput - ModelProviderSource string // session, agent or deployment; empty allows only openai_hosted + ModelProviderSource v1.ExecutionSource // session, agent or deployment; empty allows only openai_hosted Initialization environmentconfig.Setup InitialFiles []environmentconfig.InitialFile Creator identity.Subject @@ -185,7 +185,7 @@ type AuditSessionOperationCommand struct { // replay on the visible Session. It cannot create ownership or admit execution // work. Another action is ErrInvalidInput. func (s *Service) AuditSessionOperation(ctx context.Context, command AuditSessionOperationCommand) error { - if command.Action != "create" && command.Action != "send_events" { + if command.Action != string(writeaudit.ActionCreate) && command.Action != string(writeaudit.ActionSendEvents) { return ErrInvalidInput } return s.storage.AuditSessionOperation(ctx, command.TenantID, command.SessionID, command.Action) diff --git a/services/core/internal/writeaudit/reader.go b/services/core/internal/writeaudit/reader.go index 367f752df..0b8629e8b 100644 --- a/services/core/internal/writeaudit/reader.go +++ b/services/core/internal/writeaudit/reader.go @@ -36,15 +36,15 @@ type ResourceOwner struct { // Operation is one committed write. type Operation struct { - ID string `json:"id" binding:"required"` - Action string `json:"action" enums:"create,update,delete,send_events,upload_file,upload_version,update_default_version" binding:"required"` - ResourceType string `json:"resource_type" enums:"agent,session,environment,environment_template,skill,skill_version,file,vault,credential,artifact" binding:"required"` - ResourceID string `json:"resource_id" binding:"required"` - ParentID string `json:"parent_id" binding:"required"` - RequestID string `json:"request_id" binding:"required"` - TraceID string `json:"trace_id" binding:"required"` - APIKey APIKey `json:"api_key" binding:"required"` - CreatedAt time.Time `json:"created_at" binding:"required"` + ID string `json:"id" binding:"required"` + Action Action `json:"action" binding:"required"` + ResourceType ResourceType `json:"resource_type" binding:"required"` + ResourceID string `json:"resource_id" binding:"required"` + ParentID string `json:"parent_id" binding:"required"` + RequestID string `json:"request_id" binding:"required"` + TraceID string `json:"trace_id" binding:"required"` + APIKey APIKey `json:"api_key" binding:"required"` + CreatedAt time.Time `json:"created_at" binding:"required"` } // Filter selects committed writes, newest first. A zero Limit is the default diff --git a/services/core/internal/writeaudit/record.go b/services/core/internal/writeaudit/record.go index e7a256a9e..155d0baf1 100644 --- a/services/core/internal/writeaudit/record.go +++ b/services/core/internal/writeaudit/record.go @@ -15,14 +15,44 @@ import ( // vocabulary. The write it belongs to fails closed. var ErrInvalidSource = errors.New("invalid write audit source") +type ResourceType string + +const ( + ResourceAgent ResourceType = "agent" + ResourceSession ResourceType = "session" + ResourceEnvironment ResourceType = "environment" + ResourceEnvironmentTemplate ResourceType = "environment_template" + ResourceSkill ResourceType = "skill" + ResourceSkillVersion ResourceType = "skill_version" + ResourceFile ResourceType = "file" + ResourceVault ResourceType = "vault" + ResourceCredential ResourceType = "credential" + ResourceArtifact ResourceType = "artifact" +) + +type Action string + +const ( + ActionCreate Action = "create" + ActionUpdate Action = "update" + ActionDelete Action = "delete" + ActionSendEvents Action = "send_events" + ActionUploadFile Action = "upload_file" + ActionUploadVersion Action = "upload_version" + ActionUpdateDefaultVersion Action = "update_default_version" +) + // Resource identifies a resource genuinely created by the current transaction. -type Resource struct{ Type, ID, ParentID string } +type Resource struct { + Type ResourceType + ID, ParentID string +} // ValidResourceType reports whether value is an audited resource type. The list // is closed; recording and owner queries share it. func ValidResourceType(value string) bool { - switch value { - case "agent", "session", "environment", "environment_template", "skill", "skill_version", "file", "vault", "credential", "artifact": + switch ResourceType(value) { + case ResourceAgent, ResourceSession, ResourceEnvironment, ResourceEnvironmentTemplate, ResourceSkill, ResourceSkillVersion, ResourceFile, ResourceVault, ResourceCredential, ResourceArtifact: return true } return false @@ -46,17 +76,17 @@ func (s Source) Validate(tenant string) error { // ValidateRecord checks a write record in tenant: source must be well-formed // provenance from tenant, action an audited write action, and every resource // an audited resource. -func ValidateRecord(source Source, tenant, action string, resources []Resource) error { +func ValidateRecord(source Source, tenant string, action Action, resources []Resource) error { if err := source.Validate(tenant); err != nil { return err } switch action { - case "create", "update", "delete", "send_events", "upload_file", "upload_version", "update_default_version": + case ActionCreate, ActionUpdate, ActionDelete, ActionSendEvents, ActionUploadFile, ActionUploadVersion, ActionUpdateDefaultVersion: default: return fmt.Errorf("%w: invalid action", ErrInvalidSource) } for _, resource := range resources { - if !ValidResourceType(resource.Type) || !ValidText(resource.ID, 256, true) || !ValidText(resource.ParentID, 256, false) { + if !ValidResourceType(string(resource.Type)) || !ValidText(resource.ID, 256, true) || !ValidText(resource.ParentID, 256, false) { return fmt.Errorf("%w: invalid resource", ErrInvalidSource) } } diff --git a/services/core/internal/writeaudit/record_test.go b/services/core/internal/writeaudit/record_test.go index 12b721844..ce8d4a5c3 100644 --- a/services/core/internal/writeaudit/record_test.go +++ b/services/core/internal/writeaudit/record_test.go @@ -41,7 +41,7 @@ func TestValidateRecord(t *testing.T) { } { source, action, resources := issuedSource(tenant), "create", agent change(&source, &action, &resources) - if err := ValidateRecord(source, tenant, action, resources); !errors.Is(err, ErrInvalidSource) { + if err := ValidateRecord(source, tenant, Action(action), resources); !errors.Is(err, ErrInvalidSource) { t.Errorf("%s accepted: %v", name, err) } } diff --git a/services/core/tests/integration/admin_session_archive_test.go b/services/core/tests/integration/admin_session_archive_test.go index a36c7290c..810a5ee48 100644 --- a/services/core/tests/integration/admin_session_archive_test.go +++ b/services/core/tests/integration/admin_session_archive_test.go @@ -83,7 +83,7 @@ func TestManagedSessionArchiveUnallocatedAndGuards(t *testing.T) { tenant, session := managedArchiveSession(t, s, input) ctx := adminDeleteContext(t.Context(), tenant, uuid.NewString()) active, err := sessionAdapter(s).GetManagedSessionArchive(t.Context(), tenant, session.ID) - if err != nil || active.State != "active" || active.SessionID != session.ID || active.EnvironmentID != session.Environment.ID { + if err != nil || active.State != sessions.ManagedArchiveActive || active.SessionID != session.ID || active.EnvironmentID != session.Environment.ID { t.Fatal("unallocated Session status", active, err) } for _, generation := range []uint64{0, 2, ^uint64(0)} { @@ -107,7 +107,7 @@ func TestManagedSessionArchiveUnallocatedAndGuards(t *testing.T) { t.Fatal("foreign status", err) } result, err := deploymentExecution(t, w).ArchiveSession(ctx, tenant, session.ID, 1) - if err != nil || result.State != "released" { + if err != nil || result.State != sessions.ManagedArchiveReleased { t.Fatal("unallocated archive", result, err) } row, err := sessionAdapter(s).GetSession(t.Context(), tenant, session.ID) @@ -160,7 +160,7 @@ func TestManagedSessionArchiveRetainsHistoryAndSettledResources(t *testing.T) { history := adminMutationSnapshot(t, s, "sessions", "turns", "session_items", "session_artifacts", "source_files", "pg_largeobject", "pg_largeobject_metadata") request := uuid.NewString() result, err := deploymentExecution(t, w).ArchiveSession(adminDeleteContext(t.Context(), tenant, request), tenant, session.ID, 1) - if err != nil || result.State != "cleanup_pending" { + if err != nil || result.State != sessions.ManagedArchiveCleanupPending { t.Fatal(result, err) } assertAdminMutationAudit(t, s, tenant, request, "archive", "session", session.ID) @@ -190,7 +190,7 @@ func TestManagedSessionArchiveRetainsHistoryAndSettledResources(t *testing.T) { if _, err := deploymentExecution(t, w).ReleaseAllocation(t.Context(), owner); err != nil { t.Fatal(err) } - if result, err := sessionAdapter(s).GetManagedSessionArchive(t.Context(), tenant, session.ID); err != nil || result.State != "released" { + if result, err := sessionAdapter(s).GetManagedSessionArchive(t.Context(), tenant, session.ID); err != nil || result.State != sessions.ManagedArchiveReleased { t.Fatal("release not reflected", result, err) } page, err := sessionAdapter(s).ListSessionArtifacts(t.Context(), tenant, session.ID, "", "", 100, true) diff --git a/services/core/tests/integration/fixtures_test.go b/services/core/tests/integration/fixtures_test.go index bf530806d..c3bb5605b 100644 --- a/services/core/tests/integration/fixtures_test.go +++ b/services/core/tests/integration/fixtures_test.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "errors" - "testing" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" @@ -40,7 +39,7 @@ func WithFixtureModelProvider(input sessions.CreateSession) sessions.CreateSessi if input.ModelProvider != nil || json.Unmarshal(input.Configuration, &configuration) != nil || !v1.ModelProviderRequired(configuration.Environment.Type) { return input } - input.ModelProvider, input.ModelProviderSource = FixtureModelProvider(input.Engine), v1.ModelProviderSourceSession + input.ModelProvider, input.ModelProviderSource = FixtureModelProvider(input.Engine), v1.ExecutionSourceSession if input.ExecutionConfiguration != nil { projection := *input.ExecutionConfiguration projection.ModelProvider = v1.ExecutionProviderSelection{Source: "session", Status: "available", Configuration: input.ModelProvider.SafeView()} diff --git a/services/core/tests/integration/function_execution_native_test.go b/services/core/tests/integration/function_execution_native_test.go index 96844b3d4..529694868 100644 --- a/services/core/tests/integration/function_execution_native_test.go +++ b/services/core/tests/integration/function_execution_native_test.go @@ -14,7 +14,7 @@ func TestNativeFunctionExecutionPersistsCallsResultsAndContinuity(t *testing.T) model, output, requests := nativeFunctionModel(t, home) defer model.Close() var err error - h.session, err = h.s.CreateSession(ctx, h.tenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "native-functions", Configuration: json.RawMessage(functionConfiguration), ModelProvider: nativeModelProvider(model), ModelProviderSource: v1.ModelProviderSourceDeployment}) + h.session, err = h.s.CreateSession(ctx, h.tenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "native-functions", Configuration: json.RawMessage(functionConfiguration), ModelProvider: nativeModelProvider(model), ModelProviderSource: v1.ExecutionSourceDeployment}) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/native_environment_test.go b/services/core/tests/integration/native_environment_test.go index dc02c61a2..3b7dd9ac7 100644 --- a/services/core/tests/integration/native_environment_test.go +++ b/services/core/tests/integration/native_environment_test.go @@ -98,7 +98,7 @@ func TestNativeNoExecutionEnvironment(t *testing.T) { provider := nativeModelProvider(model) config, _ := json.Marshal(map[string]any{"agent": map[string]string{"model": "gpt-5.5", "instructions": "Keep this instruction."}, "environment": map[string]string{"type": "none"}}) var err error - h.session, err = h.s.CreateSession(ctx, h.tenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "native-session", Configuration: config, ModelProvider: provider, ModelProviderSource: v1.ModelProviderSourceDeployment}) + h.session, err = h.s.CreateSession(ctx, h.tenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "native-session", Configuration: config, ModelProvider: provider, ModelProviderSource: v1.ExecutionSourceDeployment}) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/node_diagnostic_contract_test.go b/services/core/tests/integration/node_diagnostic_contract_test.go index 6d742c8ed..88cb7ce96 100644 --- a/services/core/tests/integration/node_diagnostic_contract_test.go +++ b/services/core/tests/integration/node_diagnostic_contract_test.go @@ -5,12 +5,11 @@ import ( "os" "reflect" "slices" - "strings" "testing" - "gopkg.in/yaml.v3" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" + "gopkg.in/yaml.v3" ) func TestNodeDiagnosticSchemaContract(t *testing.T) { @@ -35,7 +34,9 @@ func TestNodeDiagnosticSchemaContract(t *testing.T) { if !ok { t.Fatalf("%s has no Diagnostic field", model.Name()) } - check(model.Name(), strings.Split(field.Tag.Get("enums"), ",")) + if field.Type != reflect.TypeFor[sandbox.NodeDiagnosticCode]() { + t.Errorf("%s diagnostic must use the canonical code type", model.Name()) + } } raw, err = os.ReadFile("../../../../contracts/agents-api/core.openapi.yaml") if err != nil { @@ -43,15 +44,27 @@ func TestNodeDiagnosticSchemaContract(t *testing.T) { } var document struct { Definitions map[string]struct { + Enum []string `yaml:"enum"` Properties map[string]struct { - Enum []string `yaml:"enum"` + Ref string `yaml:"$ref"` + AllOf []struct { + Ref string `yaml:"$ref"` + } `yaml:"allOf"` } `yaml:"properties"` } `yaml:"definitions"` } if err := yaml.Unmarshal(raw, &document); err != nil { t.Fatal(err) } + check("sandbox.NodeDiagnosticCode", document.Definitions["sandbox.NodeDiagnosticCode"].Enum) for _, name := range []string{"deployment.Node", "deployment.NodeDetail", "deployment.NodeRollout"} { - check(name, document.Definitions[name].Properties["diagnostic"].Enum) + field := document.Definitions[name].Properties["diagnostic"] + ref := field.Ref + if len(field.AllOf) == 1 { + ref = field.AllOf[0].Ref + } + if ref != "#/definitions/sandbox.NodeDiagnosticCode" { + t.Errorf("%s diagnostic does not reference the canonical enum", name) + } } } diff --git a/services/core/tests/integration/sandbox_generations_test.go b/services/core/tests/integration/sandbox_generations_test.go index af3147a55..580459b2a 100644 --- a/services/core/tests/integration/sandbox_generations_test.go +++ b/services/core/tests/integration/sandbox_generations_test.go @@ -6,13 +6,11 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/e2b" - + "github.com/google/uuid" "github.com/jackc/pgx/v5/stdlib" "github.com/pressly/goose/v3" - - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" - "github.com/google/uuid" ) func TestE2BGenerationsRetainOwnershipAndUseCurrentCredential(t *testing.T) { @@ -291,14 +289,14 @@ func TestSandboxSnapshotRolloutEquivalence(t *testing.T) { t.Run(state, func(t *testing.T) { connection := onlineManagerNode(t, s, node.NodeID) want := deployment.RolloutNodes{} - wantState := "settled" + wantState := deployment.RolloutSettled switch state { case "ready": want.Ready = 1 case "preparing": heartbeat(connection, "preparing") want.Preparing = 1 - wantState = "preparing" + wantState = deployment.RolloutPreparing case "failed": heartbeat(connection, "failed") want.Failed = 1 diff --git a/services/core/tests/integration/session_execution_configuration_test.go b/services/core/tests/integration/session_execution_configuration_test.go index 4816546f2..54dea62c3 100644 --- a/services/core/tests/integration/session_execution_configuration_test.go +++ b/services/core/tests/integration/session_execution_configuration_test.go @@ -23,8 +23,8 @@ func executionProjectionInput(source string) sessions.CreateSession { Creator: FixtureCreator(), Engine: harness, IdempotencyKey: uuid.NewString(), Configuration: []byte(`{"agent":{"model":"frozen-model"},"environment":{"type":"openai_hosted"}}`), ExecutionConfiguration: &v1.SessionExecutionConfiguration{ - Model: v1.ExecutionSelection{Value: &model, Source: source}, - Harness: v1.ExecutionSelection{Value: &harness, Source: source}, + Model: v1.ExecutionSelection{Value: &model, Source: v1.ExecutionSource(source)}, + Harness: v1.ExecutionSelection{Value: &harness, Source: v1.ExecutionSource(source)}, ModelProvider: v1.ExecutionProviderSelection{Source: "unknown", Status: "unavailable"}, }, } @@ -39,7 +39,7 @@ func TestSessionExecutionConfigurationFrozenAcrossCreationPathsAndRetry(t *testi tenant := uuid.NewString() input := executionProjectionInput(source) input.ModelProvider = &v1.ModelProviderInput{Protocol: "responses", BaseURL: "https://private-deployment.example/v1", APIKey: "private-projection-key-canary"} - input.ExecutionConfiguration.ModelProvider = v1.ExecutionProviderSelection{Source: source, Status: "available", Configuration: input.ModelProvider.SafeView()} + input.ExecutionConfiguration.ModelProvider = v1.ExecutionProviderSelection{Source: v1.ExecutionSource(source), Status: "available", Configuration: input.ModelProvider.SafeView()} input.ExecutionConfiguration.Object = "untrusted-object" input.ExecutionConfiguration.SchemaVersion = 99 input.ExecutionConfiguration.SessionID = "untrusted-session" @@ -60,7 +60,7 @@ func TestSessionExecutionConfigurationFrozenAcrossCreationPathsAndRetry(t *testi if err != nil { t.Fatal(err) } - if frozen.Object != "agent.session.execution_configuration" || frozen.SchemaVersion != 1 || frozen.SessionID != session.ID || frozen.Model.Source != source || frozen.Harness.Source != source { + if frozen.Object != "agent.session.execution_configuration" || frozen.SchemaVersion != 1 || frozen.SessionID != session.ID || frozen.Model.Source != v1.ExecutionSource(source) || frozen.Harness.Source != v1.ExecutionSource(source) { t.Fatal("incorrect frozen projection identity or provenance") } // Deployment defaults are readable with the same Core key, so every diff --git a/services/core/tests/integration/unified_model_configuration_http_test.go b/services/core/tests/integration/unified_model_configuration_http_test.go index 70d5dd009..3fa13586f 100644 --- a/services/core/tests/integration/unified_model_configuration_http_test.go +++ b/services/core/tests/integration/unified_model_configuration_http_test.go @@ -109,7 +109,7 @@ func TestUnifiedModelConfigurationHTTP(t *testing.T) { if err != nil { t.Fatal(err) } - if snapshot.Model.Value == nil || *snapshot.Model.Value != model || snapshot.Model.Source != modelSource || snapshot.HarnessConfig.Source != nativeSource || snapshot.ModelProvider.Source != providerSource { + if snapshot.Model.Value == nil || *snapshot.Model.Value != model || snapshot.Model.Source != v1.ExecutionSource(modelSource) || snapshot.HarnessConfig.Source != v1.ExecutionSource(nativeSource) || snapshot.ModelProvider.Source != v1.ExecutionSource(providerSource) { t.Fatalf("wrong frozen selections: %#v", snapshot) } equalJSON(snapshot.HarnessConfig.Value, native)