From 40004b5af5ac5191746f44b54afc7164145e848d Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Tue, 20 Oct 2026 06:35:59 +0000 Subject: [PATCH 1/2] Remove the upgrade contract --- .github/workflows/api-acceptance.yml | 10 +- .github/workflows/check.yml | 10 +- AGENTS.md | 2 +- CONTRIBUTING.md | 21 +- Makefile | 7 +- contracts/agents-api/source-files.md | 2 - contracts/agents-api/zh/source-files.md | 4 +- deploy/README.md | 4 +- deploy/compose/compose.yaml | 8 +- deploy/compose/smoke-pins.json | 4 - deploy/compose/test_compose.py | 1 - deploy/install-source.py | 39 +- deploy/test_install.py | 2 +- deploy/test_install_source.py | 46 +- docs/configuration.md | 4 +- docs/development.md | 2 +- docs/getting-started/install-options.md | 8 +- docs/getting-started/install.md | 6 +- docs/getting-started/nodes.md | 2 +- docs/getting-started/operations.md | 55 +- docs/getting-started/troubleshooting.md | 8 +- docs/maintainers.md | 8 +- docs/zh/configuration.md | 6 +- docs/zh/development.md | 4 +- docs/zh/getting-started/install-options.md | 10 +- docs/zh/getting-started/install.md | 8 +- docs/zh/getting-started/nodes.md | 4 +- docs/zh/getting-started/operations.md | 57 +- docs/zh/getting-started/troubleshooting.md | 10 +- docs/zh/maintainers.md | 10 +- scripts/build-core-distribution.sh | 2 +- scripts/check-names.py | 107 -- scripts/check-names.test.py | 148 -- scripts/ci_plan.py | 2 +- scripts/compose-smoke.py | 52 +- scripts/core-distribution-manifest.py | 4 +- scripts/core-distribution-manifest.test.py | 6 +- scripts/name-allowlist.json | 272 ---- services/core/IMPLEMENTATION.md | 2 +- services/core/README.md | 2 +- services/core/cmd/oac/init.go | 42 +- services/core/cmd/oac/init_test.go | 69 +- services/core/cmd/oac/install.go | 118 +- services/core/cmd/oac/install_test.go | 80 +- services/core/cmd/oac/main.go | 2 +- services/core/cmd/oac/uninstall_test.go | 11 - services/core/cmd/server/main.go | 6 +- .../credentialcrypto/agent_model_execution.go | 2 +- .../core/internal/credentialcrypto/cipher.go | 4 +- .../deployment_model_provider.go | 2 +- .../credentialcrypto/environment_file.go | 2 +- .../credentialcrypto/environment_setup.go | 2 +- .../credentialcrypto/model_execution.go | 2 +- .../credentialcrypto/sandbox_deployment.go | 2 +- .../core/internal/credentialcrypto/skill.go | 2 +- .../internal/databaseurl/databaseurl_test.go | 12 +- .../postgres/deploymentpg/nodes_test.go | 110 -- .../modelconfigurationpg/observation_test.go | 36 - .../postgres/pgtest/pgtest_test.go | 6 +- .../postgres/sessionpg/creation.go | 2 +- services/core/migrations/000001_baseline.sql | 1344 +++++++++++++++++ services/core/migrations/000001_sessions.sql | 15 - .../000002_session_configuration.sql | 6 - services/core/migrations/000003_turns.sql | 35 - .../core/migrations/000004_input_batches.sql | 12 - services/core/migrations/000005_devices.sql | 20 - .../migrations/000006_native_sessions.sql | 5 - .../core/migrations/000007_turn_events.sql | 19 - .../core/migrations/000008_session_items.sql | 26 - .../migrations/000009_execution_queue.sql | 6 - .../core/migrations/000010_token_usage.sql | 5 - .../core/migrations/000011_item_order.sql | 23 - .../core/migrations/000012_session_events.sql | 13 - .../core/migrations/000013_function_calls.sql | 21 - .../migrations/000014_function_inputs.sql | 13 - .../000015_retire_item_backfill.sql | 16 - services/core/migrations/000016_agents.sql | 13 - .../000017_session_creation_identity.sql | 6 - .../000018_session_agent_filter.sql | 6 - .../migrations/000019_session_deletion.sql | 13 - .../core/migrations/000020_environments.sql | 20 - ...00021_environment_executor_credentials.sql | 10 - .../000022_environment_input_reservations.sql | 29 - .../000023_environment_input_expiry_index.sql | 6 - .../000024_execution_project_scopes.sql | 10 - .../migrations/000025_session_creators.sql | 15 - .../migrations/000026_executor_principals.sql | 43 - .../000027_environment_connections.sql | 18 - .../000028_environment_input_activity.sql | 6 - .../000029_environment_initial_input.sql | 15 - services/core/migrations/000030_vaults.sql | 11 - .../migrations/000031_vault_credentials.sql | 14 - .../core/migrations/000032_vault_status.sql | 16 - .../migrations/000033_credential_status.sql | 17 - .../migrations/000034_subagent_identities.sql | 19 - .../000035_local_environment_devices.sql | 14 - .../000036_environment_file_writes.sql | 25 - .../core/migrations/000037_source_files.sql | 21 - .../migrations/000038_runtime_allocations.sql | 27 - .../000039_environment_input_failure.sql | 10 - .../migrations/000040_session_artifacts.sql | 28 - .../migrations/000041_source_files_list.sql | 5 - .../000042_environment_templates.sql | 13 - .../000043_environment_initial_files.sql | 20 - .../migrations/000044_environment_setup.sql | 13 - .../migrations/000045_environment_skills.sql | 7 - .../000046_session_model_execution.sql | 8 - .../000047_environment_network_domains.sql | 21 - services/core/migrations/000048_skills.sql | 37 - .../migrations/000049_environment_plugins.sql | 11 - .../migrations/000050_runtime_enrollment.sql | 40 - .../migrations/000051_subagent_resources.sql | 66 - .../migrations/000052_runtime_suspension.sql | 27 - .../migrations/000053_runtime_deployment.sql | 22 - .../migrations/000054_oauth_credentials.sql | 17 - .../000055_skill_default_metadata.sql | 12 - .../migrations/000056_runtime_history.sql | 36 - .../core/migrations/000057_runtime_nodes.sql | 55 - .../000058_sandbox_deployment_setup.sql | 38 - .../000059_agent_model_execution.sql | 8 - ...000060_session_execution_configuration.sql | 8 - .../migrations/000061_project_api_keys.sql | 20 - .../migrations/000062_environment_failure.sql | 27 - .../000063_hosted_provider_selection.sql | 46 - .../core/migrations/000064_write_audit.sql | 37 - .../000065_environment_file_write_audit.sql | 8 - .../core/migrations/000066_admin_projects.sql | 48 - .../migrations/000067_admin_asset_copies.sql | 23 - .../000068_core_metrics_indexes.sql | 9 - .../000069_hosted_specification.sql | 18 - .../000070_node_enrollment_capacity.sql | 8 - .../migrations/000071_node_host_history.sql | 13 - ...073_e2b_observation_and_template_build.sql | 26 - .../000074_allocation_phase_time.sql | 7 - .../migrations/000075_public_url_binding.sql | 42 - .../000076_deployment_model_providers.sql | 39 - .../migrations/000077_node_enrollment_id.sql | 13 - .../migrations/000078_oac_runtime_names.sql | 29 - .../core/migrations/000079_sandbox_reset.sql | 63 - .../000080_archived_cancellation_receipts.sql | 17 - .../migrations/000081_sandbox_generations.sql | 67 - .../migrations/000082_node_generations.sql | 30 - .../migrations/000083_session_diagnostics.sql | 8 - ...00084_deployment_provider_observations.sql | 27 - .../000085_deployment_model_configuration.sql | 7 - .../migrations/000086_e2b_custom_endpoint.sql | 26 - .../000087_e2b_generation_endpoints.sql | 21 - ..._environment_input_preparation_failure.sql | 11 - .../000089_provider_registration.sql | 82 - .../000090_environment_initialization.sql | 9 - .../000091_provider_configuration.sql | 117 -- .../migrations/000092_delete_admin_copies.sql | 24 - .../migrations/000093_web_deployment_only.sql | 78 - .../000094_suspension_from_declaration.sql | 26 - .../000095_node_readiness_classes.sql | 19 - .../000096_configured_admission.sql | 20 - .../migrations/000097_workspace_storage.sql | 56 - .../000098_compute_incarnations.sql | 40 - .../000099_checkpoint_compatibility.sql | 6 - .../migrations/000100_workspace_capacity.sql | 68 - .../migrations/000101_sandbox_idle_policy.sql | 5 - .../000102_reasoning_effort_single_home.sql | 32 - .../000103_function_call_withdrawn.sql | 11 - services/core/migrations/migrations.go | 26 +- services/core/migrations/migrations_test.go | 24 - .../archived_cancellation_migration_test.go | 57 - .../credential_oauth_migration_test.go | 79 - .../credential_status_migration_test.go | 86 -- .../environment_connection_migration_test.go | 89 -- .../environment_failure_migration_test.go | 85 -- .../environment_file_write_migration_test.go | 90 -- .../environment_initial_migration_test.go | 86 -- .../environment_input_migration_test.go | 92 -- .../environments_migration_test.go | 133 -- .../executor_principals_migration_test.go | 131 -- .../integration/item_order_migration_test.go | 163 -- .../oac_runtime_names_migration_test.go | 142 -- .../provider_configuration_migration_test.go | 83 - .../provider_registration_migration_test.go | 57 - .../integration/public_url_migration_test.go | 102 -- .../integration/sandbox_generations_test.go | 101 -- .../sandbox_reset_migration_test.go | 52 - .../skill_default_metadata_migration_test.go | 118 -- .../vault_status_migration_test.go | 83 - .../web_deployment_only_migration_test.go | 39 - website/tests/install.test.mjs | 2 +- 186 files changed, 1509 insertions(+), 5620 deletions(-) delete mode 100644 deploy/compose/smoke-pins.json delete mode 100644 scripts/check-names.py delete mode 100644 scripts/check-names.test.py delete mode 100644 scripts/name-allowlist.json create mode 100644 services/core/migrations/000001_baseline.sql delete mode 100644 services/core/migrations/000001_sessions.sql delete mode 100644 services/core/migrations/000002_session_configuration.sql delete mode 100644 services/core/migrations/000003_turns.sql delete mode 100644 services/core/migrations/000004_input_batches.sql delete mode 100644 services/core/migrations/000005_devices.sql delete mode 100644 services/core/migrations/000006_native_sessions.sql delete mode 100644 services/core/migrations/000007_turn_events.sql delete mode 100644 services/core/migrations/000008_session_items.sql delete mode 100644 services/core/migrations/000009_execution_queue.sql delete mode 100644 services/core/migrations/000010_token_usage.sql delete mode 100644 services/core/migrations/000011_item_order.sql delete mode 100644 services/core/migrations/000012_session_events.sql delete mode 100644 services/core/migrations/000013_function_calls.sql delete mode 100644 services/core/migrations/000014_function_inputs.sql delete mode 100644 services/core/migrations/000015_retire_item_backfill.sql delete mode 100644 services/core/migrations/000016_agents.sql delete mode 100644 services/core/migrations/000017_session_creation_identity.sql delete mode 100644 services/core/migrations/000018_session_agent_filter.sql delete mode 100644 services/core/migrations/000019_session_deletion.sql delete mode 100644 services/core/migrations/000020_environments.sql delete mode 100644 services/core/migrations/000021_environment_executor_credentials.sql delete mode 100644 services/core/migrations/000022_environment_input_reservations.sql delete mode 100644 services/core/migrations/000023_environment_input_expiry_index.sql delete mode 100644 services/core/migrations/000024_execution_project_scopes.sql delete mode 100644 services/core/migrations/000025_session_creators.sql delete mode 100644 services/core/migrations/000026_executor_principals.sql delete mode 100644 services/core/migrations/000027_environment_connections.sql delete mode 100644 services/core/migrations/000028_environment_input_activity.sql delete mode 100644 services/core/migrations/000029_environment_initial_input.sql delete mode 100644 services/core/migrations/000030_vaults.sql delete mode 100644 services/core/migrations/000031_vault_credentials.sql delete mode 100644 services/core/migrations/000032_vault_status.sql delete mode 100644 services/core/migrations/000033_credential_status.sql delete mode 100644 services/core/migrations/000034_subagent_identities.sql delete mode 100644 services/core/migrations/000035_local_environment_devices.sql delete mode 100644 services/core/migrations/000036_environment_file_writes.sql delete mode 100644 services/core/migrations/000037_source_files.sql delete mode 100644 services/core/migrations/000038_runtime_allocations.sql delete mode 100644 services/core/migrations/000039_environment_input_failure.sql delete mode 100644 services/core/migrations/000040_session_artifacts.sql delete mode 100644 services/core/migrations/000041_source_files_list.sql delete mode 100644 services/core/migrations/000042_environment_templates.sql delete mode 100644 services/core/migrations/000043_environment_initial_files.sql delete mode 100644 services/core/migrations/000044_environment_setup.sql delete mode 100644 services/core/migrations/000045_environment_skills.sql delete mode 100644 services/core/migrations/000046_session_model_execution.sql delete mode 100644 services/core/migrations/000047_environment_network_domains.sql delete mode 100644 services/core/migrations/000048_skills.sql delete mode 100644 services/core/migrations/000049_environment_plugins.sql delete mode 100644 services/core/migrations/000050_runtime_enrollment.sql delete mode 100644 services/core/migrations/000051_subagent_resources.sql delete mode 100644 services/core/migrations/000052_runtime_suspension.sql delete mode 100644 services/core/migrations/000053_runtime_deployment.sql delete mode 100644 services/core/migrations/000054_oauth_credentials.sql delete mode 100644 services/core/migrations/000055_skill_default_metadata.sql delete mode 100644 services/core/migrations/000056_runtime_history.sql delete mode 100644 services/core/migrations/000057_runtime_nodes.sql delete mode 100644 services/core/migrations/000058_sandbox_deployment_setup.sql delete mode 100644 services/core/migrations/000059_agent_model_execution.sql delete mode 100644 services/core/migrations/000060_session_execution_configuration.sql delete mode 100644 services/core/migrations/000061_project_api_keys.sql delete mode 100644 services/core/migrations/000062_environment_failure.sql delete mode 100644 services/core/migrations/000063_hosted_provider_selection.sql delete mode 100644 services/core/migrations/000064_write_audit.sql delete mode 100644 services/core/migrations/000065_environment_file_write_audit.sql delete mode 100644 services/core/migrations/000066_admin_projects.sql delete mode 100644 services/core/migrations/000067_admin_asset_copies.sql delete mode 100644 services/core/migrations/000068_core_metrics_indexes.sql delete mode 100644 services/core/migrations/000069_hosted_specification.sql delete mode 100644 services/core/migrations/000070_node_enrollment_capacity.sql delete mode 100644 services/core/migrations/000071_node_host_history.sql delete mode 100644 services/core/migrations/000073_e2b_observation_and_template_build.sql delete mode 100644 services/core/migrations/000074_allocation_phase_time.sql delete mode 100644 services/core/migrations/000075_public_url_binding.sql delete mode 100644 services/core/migrations/000076_deployment_model_providers.sql delete mode 100644 services/core/migrations/000077_node_enrollment_id.sql delete mode 100644 services/core/migrations/000078_oac_runtime_names.sql delete mode 100644 services/core/migrations/000079_sandbox_reset.sql delete mode 100644 services/core/migrations/000080_archived_cancellation_receipts.sql delete mode 100644 services/core/migrations/000081_sandbox_generations.sql delete mode 100644 services/core/migrations/000082_node_generations.sql delete mode 100644 services/core/migrations/000083_session_diagnostics.sql delete mode 100644 services/core/migrations/000084_deployment_provider_observations.sql delete mode 100644 services/core/migrations/000085_deployment_model_configuration.sql delete mode 100644 services/core/migrations/000086_e2b_custom_endpoint.sql delete mode 100644 services/core/migrations/000087_e2b_generation_endpoints.sql delete mode 100644 services/core/migrations/000088_environment_input_preparation_failure.sql delete mode 100644 services/core/migrations/000089_provider_registration.sql delete mode 100644 services/core/migrations/000090_environment_initialization.sql delete mode 100644 services/core/migrations/000091_provider_configuration.sql delete mode 100644 services/core/migrations/000092_delete_admin_copies.sql delete mode 100644 services/core/migrations/000093_web_deployment_only.sql delete mode 100644 services/core/migrations/000094_suspension_from_declaration.sql delete mode 100644 services/core/migrations/000095_node_readiness_classes.sql delete mode 100644 services/core/migrations/000096_configured_admission.sql delete mode 100644 services/core/migrations/000097_workspace_storage.sql delete mode 100644 services/core/migrations/000098_compute_incarnations.sql delete mode 100644 services/core/migrations/000099_checkpoint_compatibility.sql delete mode 100644 services/core/migrations/000100_workspace_capacity.sql delete mode 100644 services/core/migrations/000101_sandbox_idle_policy.sql delete mode 100644 services/core/migrations/000102_reasoning_effort_single_home.sql delete mode 100644 services/core/migrations/000103_function_call_withdrawn.sql delete mode 100644 services/core/migrations/migrations_test.go delete mode 100644 services/core/tests/integration/archived_cancellation_migration_test.go delete mode 100644 services/core/tests/integration/credential_oauth_migration_test.go delete mode 100644 services/core/tests/integration/credential_status_migration_test.go delete mode 100644 services/core/tests/integration/environment_connection_migration_test.go delete mode 100644 services/core/tests/integration/environment_failure_migration_test.go delete mode 100644 services/core/tests/integration/environment_file_write_migration_test.go delete mode 100644 services/core/tests/integration/environment_initial_migration_test.go delete mode 100644 services/core/tests/integration/environment_input_migration_test.go delete mode 100644 services/core/tests/integration/environments_migration_test.go delete mode 100644 services/core/tests/integration/executor_principals_migration_test.go delete mode 100644 services/core/tests/integration/item_order_migration_test.go delete mode 100644 services/core/tests/integration/oac_runtime_names_migration_test.go delete mode 100644 services/core/tests/integration/provider_configuration_migration_test.go delete mode 100644 services/core/tests/integration/provider_registration_migration_test.go delete mode 100644 services/core/tests/integration/public_url_migration_test.go delete mode 100644 services/core/tests/integration/sandbox_reset_migration_test.go delete mode 100644 services/core/tests/integration/skill_default_metadata_migration_test.go delete mode 100644 services/core/tests/integration/vault_status_migration_test.go delete mode 100644 services/core/tests/integration/web_deployment_only_migration_test.go diff --git a/.github/workflows/api-acceptance.yml b/.github/workflows/api-acceptance.yml index ee010ed18..1db726e16 100644 --- a/.github/workflows/api-acceptance.yml +++ b/.github/workflows/api-acceptance.yml @@ -23,12 +23,12 @@ jobs: image: postgres:16-alpine env: POSTGRES_DB: oac_ci_tests - POSTGRES_USER: agents_api - POSTGRES_PASSWORD: agents_api_test_only + POSTGRES_USER: oac + POSTGRES_PASSWORD: oac_test_only ports: - 5432/tcp options: >- - --health-cmd "pg_isready -U agents_api -d oac_ci_tests" + --health-cmd "pg_isready -U oac -d oac_ci_tests" --health-interval 5s --health-timeout 5s --health-retries 10 @@ -58,7 +58,7 @@ jobs: PY - name: Verify official-client HTTP compatibility env: - OAC_TEST_DATABASE_URL: postgres://agents_api:agents_api_test_only@127.0.0.1:${{ job.services.postgres.ports['5432'] }}/oac_ci_tests?sslmode=disable + OAC_TEST_DATABASE_URL: postgres://oac:oac_test_only@127.0.0.1:${{ job.services.postgres.ports['5432'] }}/oac_ci_tests?sslmode=disable OAC_TEST_SERVER_BIN: ${{ runner.temp }}/oac-core-build/oac-core OAC_TEST_OFFICIAL_SDK_PYTHON: python run: | @@ -70,7 +70,7 @@ jobs: - name: Verify the distribution's Core image if: inputs.container env: - OAC_TEST_DATABASE_URL: postgres://agents_api:agents_api_test_only@127.0.0.1:${{ job.services.postgres.ports['5432'] }}/oac_ci_tests?sslmode=disable + OAC_TEST_DATABASE_URL: postgres://oac:oac_test_only@127.0.0.1:${{ job.services.postgres.ports['5432'] }}/oac_ci_tests?sslmode=disable OAC_DEV_CORE_IMAGE: oac-core:ci OAC_TEST_OFFICIAL_SDK_PYTHON: python run: make check-core-container diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 99f556c93..9ee59a3ab 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -49,8 +49,8 @@ jobs: - uses: actions/checkout@v7 with: ref: ${{ inputs.ref || github.sha }} - - name: Verify names, documentation and CI selection invariants - run: make check-names check-docs check-ci + - name: Verify documentation and CI selection invariants + run: make check-docs check-ci # Parts run in parallel; the serial integration package is split into deterministic shards. backend: @@ -76,12 +76,12 @@ jobs: image: postgres:16-alpine env: POSTGRES_DB: oac_core_ci_tests - POSTGRES_USER: agents_api + POSTGRES_USER: oac POSTGRES_PASSWORD: core_test_only ports: - 5432/tcp options: >- - --health-cmd "pg_isready -U agents_api -d oac_core_ci_tests" + --health-cmd "pg_isready -U oac -d oac_core_ci_tests" --health-interval 5s --health-timeout 5s --health-retries 10 @@ -119,7 +119,7 @@ jobs: sudo apt-get install -y build-essential pkg-config libssl-dev - name: Verify dedicated test database env: - OAC_TEST_DATABASE_URL: postgres://agents_api:core_test_only@127.0.0.1:${{ job.services.postgres.ports['5432'] }}/oac_core_ci_tests?sslmode=disable + OAC_TEST_DATABASE_URL: postgres://oac:core_test_only@127.0.0.1:${{ job.services.postgres.ports['5432'] }}/oac_core_ci_tests?sslmode=disable run: | echo "OAC_TEST_DATABASE_URL=$OAC_TEST_DATABASE_URL" >> "$GITHUB_ENV" make check-database diff --git a/AGENTS.md b/AGENTS.md index 7bac569df..0294c0e47 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -69,7 +69,7 @@ The categories are [process settings](docs/configuration.md#process-settings), [ ### Pre-release: no compatibility layers -OpenAgentCore is pre-release. Replace superseded interfaces, execution paths and files outright. Keep no version fallback, compatibility shim or migration for superseded behavior unless an explicit upgrade contract requires it. The [in-place upgrade](docs/getting-started/operations.md#upgrade) from v0.0.10 or later is that contract: persisted changes ship as forward migrations (goose for PostgreSQL, `oac init` for the data volume), and a landed migration is never edited. Protocols may change; the release notes list each change with the operator actions it requires. Keep the pinned official public protocol, valid data and still-used, verified infrastructure. Do not rewrite working infrastructure only to rename it. +OpenAgentCore has had no public release. Replace superseded interfaces, execution paths and files outright. Keep no version fallback, compatibility shim or migration for superseded behavior. Before the first public release, a schema change edits the baseline migration in place, and an installation created from an earlier baseline is reinstalled. Keep the pinned official public protocol, valid data and still-used, verified infrastructure. Do not rewrite working infrastructure only to rename it. ## Documentation diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3a51d0bff..b3cc650b5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -74,7 +74,7 @@ Toolchain setup and focused commands are in [Develop OpenAgentCore](docs/develop ### Checks for a change -Validate the current diff and the behavior it directly affects, with the smallest checks that establish correctness. Include migration or cross-component tests only when those behaviors change. A review, documentation edit or CI configuration change does not need a full repository test run. After a follow-up edit, rerun only the checks that edit affects, and record what passed and any limits. +Validate the current diff and the behavior it directly affects, with the smallest checks that establish correctness. Include cross-component tests only when those behaviors change. A review, documentation edit or CI configuration change does not need a full repository test run. After a follow-up edit, rerun only the checks that edit affects, and record what passed and any limits. The [CI selection policy](docs/maintainers.md#continuous-integration) names affected groups. A narrower check is enough when it covers the change. `make check` is the full gate for an explicitly requested full validation and for releases. [Live acceptance](#live-acceptance) applies when native execution behavior changes. @@ -95,7 +95,7 @@ The role needs `CREATE DATABASE`: tests of database-wide state, such as the exec - The standard fields are generated into `v1/official.gen.go`. `go-bindings.json` lists the types Core consumes and overrides only the Go representation or field order that existing storage or custom JSON encoding requires; shared shapes use one Go type. Selected discriminated unions also generate JSON serializers that keep required nullable fields for each variant. Go input projections exclude `packages.system` to preserve its explicit rejection. Other union serializers, Core's local limits, execution admission and state transitions remain implementation code. - `packages/agents-client/src/generated/public-api.ts` generates the TypeScript client's types, enum values and field names from the resulting public schema. The same generator projects the `/core/v1` document into `core-api.ts`, which imports the public types it references from `public-api.ts`. The internal `/core/v1` and `/api/v1` documents come from handler annotations: a response field Core always sends carries `binding:"required"`, a nullable field carries `extensions:"x-nullable"`, and a Core-owned closed set uses a named Go type with typed constants in its producing package, from which Swag derives one shared enum definition. Single-value discriminators keep `enums:` tags; fields mirroring official or Runtime-owned sets keep tags that contract tests check against their owners. The annotations only shape the documents, and the client rejects a response that breaks them. - Contract tests verify that the public schema preserves the official definitions, extensions stay 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 known gaps in [OpenAI compatibility](contracts/agents-api/index.md#known-gaps) still apply. Upstream upgrades update the OpenAPI and SDK pins together after comparison and compatibility tests. -- `make sqlc-generate` owns only `services/core/internal/db/sqlc` (sqlc v1.29.0). Do not rewrite landed migrations. +- `make sqlc-generate` owns only `services/core/internal/db/sqlc` (sqlc v1.29.0). Before the first public release, schema changes edit the baseline migration in place, and an installation created from an earlier baseline is reinstalled. - `make check-runtime-contract` is the focused Core–Runtime contract entry point; see [Contract verification](docs/runtime-protocol.md#contract-verification). It also runs through `check-go` and `check-core`. ### Compatibility evidence @@ -123,23 +123,6 @@ Native adapter changes require their build/check targets and live provider accep Provider bootstrap, Runtime images and Harness adapters must agree on these names. -[Upgrade](docs/getting-started/operations.md#upgrade) owns release changes and preservation of installed data and resources. - ## Branding Use OpenAgentCore for public project branding. The canonical mark is `docs/assets/openagentcore-logo.svg`; Web uses its outline with cropped transparent margins, theme-aware favicon colors and dark-surface inversion. The README wordmark is `docs/assets/openagentcore-wordmark-light.svg` with a dark variant, and `docs/assets/og-image.png` is the social preview. The architecture diagram that README and the website show, `docs/assets/architecture.svg` and `architecture-dark.svg`, is generated by `scripts/generate-architecture-diagram.py`; change the script and run it again. The `example/parsar/` workbench keeps its own name, logo and favicon. Preserve external repository URLs and data identifiers when changing display copy. - -## OpenAgentCore name guard - -`make check-names` scans tracked text for retired branding, GitHub organization, settings and installed command names. Each exception in `scripts/name-allowlist.json` names a path glob, a regular expression and a reason. - -- An exception covers only its matched text: an allowed repository import cannot hide a retired setting elsewhere on the line. -- Keep exceptions narrow and explain the preserved contract or detection input. -- The guard fails on an exception that excuses no retired identifier. Remove an exception together with the last text it covers. - -These identities stay unchanged: - -- public `AgentCoreError`, upstream contract fields and the separate Parsar product; -- persisted credential encryption domains and native-session resume keys, so existing data can be decrypted and Sessions can resume. - -Detection inputs name the identifiers they reject. Landed migrations keep their original identifiers; application and operator examples use the current names. diff --git a/Makefile b/Makefile index cc2c6b4bf..2e73eade1 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ SWAG_VERSION ?= v1.16.4 help: @printf '%s\n' 'make build-core Build standalone Core commands' 'make build-daemon Build the execution daemon' 'make check Run Core, persistence and runtime checks' 'See README.md for runtime prerequisites and deployment.' -check: check-ci check-harness-catalog check-names check-distribution check-database check-sqlc check-go check-microsandbox-provider check-core check-claude-sdk check-web check-example check-mcode-harness +check: check-ci check-harness-catalog check-distribution check-database check-sqlc check-go check-microsandbox-provider check-core check-claude-sdk check-web check-example check-mcode-harness @printf 'OpenAgentCore checks passed.\n' .PHONY: generate-harness-catalog check-harness-catalog @@ -19,11 +19,6 @@ check-harness-catalog: python3 scripts/generate-harness-catalog.py --check python3 scripts/generate-harness-catalog.test.py -.PHONY: check-names -check-names: - python3 scripts/check-names.test.py - python3 scripts/check-names.py - check-database: @test -n "$${OAC_TEST_DATABASE_URL:-}" || { echo 'Set OAC_TEST_DATABASE_URL to a dedicated test PostgreSQL database' >&2; exit 1; } diff --git a/contracts/agents-api/source-files.md b/contracts/agents-api/source-files.md index 6a3b563f3..4ce6d05e3 100644 --- a/contracts/agents-api/source-files.md +++ b/contracts/agents-api/source-files.md @@ -55,8 +55,6 @@ A missing or foreign File returns 404 with type `invalid_request_error`, a null Core stores File bytes as PostgreSQL large objects in its own database. An upload and a deletion each commit in one transaction, so a failure leaves neither partial bytes nor metadata. [Back up](../../docs/getting-started/operations.md#back-up) the database with its large objects; deleting a File does not remove it from write-ahead logs or earlier backups. -The source Files schema refuses a downgrade while File rows remain. Delete Files through the API first so their large objects are removed. - A copy into a workspace reads a consistent snapshot of the File and can finish after the File is deleted; later lookups fail. Deleting a File never changes a workspace copy. ## Skills diff --git a/contracts/agents-api/zh/source-files.md b/contracts/agents-api/zh/source-files.md index 6f8e1f59c..3eae82672 100644 --- a/contracts/agents-api/zh/source-files.md +++ b/contracts/agents-api/zh/source-files.md @@ -1,7 +1,7 @@ --- title: "文件与 Skill" source: contracts/agents-api/source-files.md -source_hash: 320216a4e7ac0d1e3e2455dfe622e0f91576ea1d0aa00d11911683b0e98387f1 +source_hash: 86d63f3ea780394ed99c0f5ec9d566117140a8650cb780e4b038e4ab45155ba9 --- Files(`/v1/files`)和 Skills(`/v1/skills`)是具有独立生命周期的项目资源,不依赖 Session。File 保存上传字节,Environment 通过 ID 复制它们。Skill 保存不可变、版本化的包,由模板和 Session 引用。项目的所有 API 密钥共享这些资源。 @@ -57,8 +57,6 @@ Files(`/v1/files`)和 Skills(`/v1/skills`)是具有独立生命周期的 Core 在自身数据库中以 PostgreSQL 大对象存储 File 字节。上传和删除分别在单一事务中提交,失败不会留下部分字节或元数据。[备份](../../../docs/zh/getting-started/operations.md#back-up)数据库时包含大对象;删除 File 不会将其从预写日志或旧备份移除。 -存在 File 行时,源文件 schema 拒绝降级。先通过 API 删除 File,以清理大对象。 - 复制到工作区时读取 File 一致快照,可在 File 删除后完成;后续查询失败。删除 File 不改变工作区副本。 ## Skill {#skills} diff --git a/deploy/README.md b/deploy/README.md index 53ce99450..642beaee9 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -14,9 +14,9 @@ Preparation happens in `.staging`. On entry, the installer removes an empty stage or one with the empty ownership directory `.oac-installer-`; it preserves unrecognized directories. Ordinary failures remove owned staging immediately. The ownership marker is created atomically before downloading files. A killed process leaves staging for the next attempt. The release's `compose.yaml` is verified against its published checksum in the stage and must name the installer's own release in `OAC_REVISION`. Publication happens only after downloads, checks and pulls succeed. Later failures preserve saved configuration, containers and the Docker data volume. `.lock` is a persistent lock directory shared by installation and mutating operator commands; the existing `runtimefs` platform adapter owns locking. -An existing directory runs the release named by `OAC_REVISION` in its `compose.yaml`. When that is the installer's release, the installer resumes: it keeps the saved settings, pulls only missing images and starts the services the way `oac apply` does. The port check applies only to new directories. Another release is refused unless `--upgrade` is given. An upgrade stages the new `compose.yaml` and `oac`, pulls the new images, then runs `oac-core check-config` and init against the staged `compose.yaml` together with the directory's `.env` and `compose.override.yaml`. Init upgrades the data volume or refuses, for example a downgrade; until it succeeds, the running installation is unchanged. The installer then renames `oac` and `compose.yaml` into place and starts the services with `--pull never --remove-orphans`, recreating every changed container. Rerunning the same command retries an interrupted upgrade. The upgrade leaves the deployment's Runtime unchanged; the final message names the console step that applies the new one. +An existing directory runs the release named by `OAC_REVISION` in its `compose.yaml`. When that is the installer's release, the installer resumes: it keeps the saved settings, pulls only missing images and starts the services the way `oac apply` does. The port check applies only to new directories. Another release is refused. -`install.py --upgrade` upgrades a source-bundle installation the same way, under the same lock. It publishes the bundle's node payload beside earlier releases, renders the new `compose.yaml` and rewrites only its own `compose.override.yaml` entries, the four service images and Web's `/node-payload` bind, keeping every other entry. After the same two checks it moves the payload's `active.json`, renames `oac`, `compose.override.yaml` and `compose.yaml` into place and starts the services. On its own release it resumes like `oac install`; another release is refused without `--upgrade`, and `--public-url` is required only for a new directory. +`install.py` resumes a source-bundle installation of its own release under the same lock; another release is refused, and `--public-url` is required only for a new directory. `install.py` prepares a new installation in a `.staging-*` directory inside the installation directory: the node payload, `.env`, `oac` and both Compose files. After `docker compose config` accepts them, it moves them into place, `compose.yaml` last. An earlier failure removes the stage, and the directory too when the run created it, so the same command can be rerun; the next run also removes a stage left by a killed run. A directory with other contents but no `compose.yaml` is refused. diff --git a/deploy/compose/compose.yaml b/deploy/compose/compose.yaml index 9e46b7930..9286caf86 100644 --- a/deploy/compose/compose.yaml +++ b/deploy/compose/compose.yaml @@ -22,8 +22,8 @@ services: depends_on: init: {condition: service_completed_successfully} environment: - POSTGRES_USER: agents_api - POSTGRES_DB: agents_api + POSTGRES_USER: oac + POSTGRES_DB: oac POSTGRES_PASSWORD_FILE: /run/database/password volumes: - type: volume @@ -36,7 +36,7 @@ services: volume: {nocopy: true, subpath: secrets/database} read_only: true healthcheck: - test: [CMD-SHELL, "pg_isready -h 127.0.0.1 -U agents_api -d agents_api"] + test: [CMD-SHELL, "pg_isready -h 127.0.0.1 -U oac -d oac"] interval: 2s timeout: 5s retries: 30 @@ -54,7 +54,7 @@ services: environment: OAC_PUBLIC_URL: &public-url ${OAC_PUBLIC_URL:-http://localhost:${OAC_WEB_PORT:-8080}} OAC_INSTALLATION_ID_FILE: /run/oac/installation.id - OAC_DATABASE_URL: postgres://agents_api@database:5432/agents_api?sslmode=disable + OAC_DATABASE_URL: postgres://oac@database:5432/oac?sslmode=disable OAC_DATABASE_PASSWORD_FILE: /run/database/password OAC_CREDENTIAL_KEY_FILE: /run/oac/credential.key OAC_CORE_KEY_DIGESTS_FILE: /run/oac/core-key-digests.json diff --git a/deploy/compose/smoke-pins.json b/deploy/compose/smoke-pins.json deleted file mode 100644 index 5d7cd61e8..000000000 --- a/deploy/compose/smoke-pins.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "tag": "v0.0.11", - "compose_sha256": "0368e256f8f1830017e5440820cbdb0f148a613fa5e50abdf3a459e20fd72a5e" -} diff --git a/deploy/compose/test_compose.py b/deploy/compose/test_compose.py index a973b9552..e15c7d011 100644 --- a/deploy/compose/test_compose.py +++ b/deploy/compose/test_compose.py @@ -82,7 +82,6 @@ def test_compose_uses_private_services_and_ordered_initialization(self): self.assertEqual(services['core']['environment'][name], '', name) def test_database_stays_on_postgresql_16_pinned_by_digest(self): - # An upgrade keeps the data volume only within one PostgreSQL major version. self.assertRegex(render_compose.DATABASE_PIN, r'^postgres:16-alpine@sha256:[0-9a-f]{64}$') def test_public_url_can_be_configured_after_initial_startup(self): diff --git a/deploy/install-source.py b/deploy/install-source.py index 8c8ee6652..c3dd0dcc1 100644 --- a/deploy/install-source.py +++ b/deploy/install-source.py @@ -70,7 +70,7 @@ def port_holder_hint(port): if not directory: return f'\nThe Compose project {name} holds it.' if Path(directory).exists(): - return f'\nThe installation in {directory} holds it; upgrade or manage it with --install-dir {directory}.' + return f'\nThe installation in {directory} holds it; manage it with --install-dir {directory}.' return f'\nThe Compose project {name} holds it, and its installation directory {directory} no longer exists. Remove it with: docker compose -p {name} down --volumes --remove-orphans (this deletes its data)' return '' @@ -94,7 +94,7 @@ def compose_revision(text): return next((line.split(':', 1)[1].strip() for line in text.splitlines() if line.strip().startswith('OAC_REVISION:')), '') -def install(bundle, destination, host, port, public_url, upgrade): +def install(bundle, destination, host, port, public_url): manifest = load_manifest(offline_root=bundle) revision = manifest['source_commit'] for name in manifest['artifacts'].values(): @@ -120,8 +120,8 @@ def install(bundle, destination, host, port, public_url, upgrade): previous = compose_revision((destination / 'compose.yaml').read_text()) if existing else '' if existing and not previous: raise ValueError(f'Incomplete installation: preserve {destination} and choose another directory') - if previous and previous != revision and not upgrade: - raise ValueError(f'This installation runs release {previous[:12]}; this bundle is {revision[:12]}. Back up the installation (Upgrade in the operations guide), then rerun with --upgrade.') + if previous and previous != revision: + raise ValueError(f'This installation runs release {previous[:12]}; this bundle is {revision[:12]}. Reinstall to use this release.') if not existing and public_url is None: raise ValueError('--public-url is required for a new installation') check_docker() @@ -160,13 +160,10 @@ def install(bundle, destination, host, port, public_url, upgrade): except ValueError: raise ValueError('compose.override.yaml must stay JSON so the installer can replace its own entries') from None services = override.setdefault('services', {}) - # compose.yaml is the one home of the image references; earlier installers also wrote them here. - for name in ('core', 'web', 'init', 'database'): - services.setdefault(name, {}).pop('image', None) - web = services['web'] + web = services.setdefault('web', {}) web['volumes'] = [volume for volume in web.get('volumes', []) if not (isinstance(volume, dict) and volume.get('target') == '/node-payload')] web['volumes'].append({'type': 'bind', 'source': str(destination / 'node-payload'), 'target': '/node-payload', 'read_only': True}) - core = services['core'] + core = services.setdefault('core', {}) core['volumes'] = [volume for volume in core.get('volumes', []) if not (isinstance(volume, dict) and volume.get('target') == '/opt/oac/native-installers')] if native.is_dir(): core['volumes'].append({'type': 'bind', 'source': str(destination / 'native-installers' / revision), 'target': '/opt/oac/native-installers', 'read_only': True}) @@ -176,17 +173,12 @@ def install(bundle, destination, host, port, public_url, upgrade): os.chmod(stage / 'oac', 0o700) compose = ('docker', 'compose', '-f', str(stage / 'compose.yaml'), '-f', str(stage / 'compose.override.yaml'), '--project-directory', str(root)) if existing: - # Init upgrades the data volume or refuses; until then the running release is untouched. - # On the same release init comes first: it finishes a first start that stopped before it, + # Init comes first: it finishes a first start that stopped before it, # so check-config finds the secrets. - checks = [('Configuration check', ('run', '--rm', '--no-deps', '--entrypoint', '/usr/local/bin/oac-core', 'core', 'check-config')), - ('Data volume check', ('run', '--rm', '--no-deps', 'init'))] - if previous == revision: - checks.reverse() + checks = [('Data volume check', ('run', '--rm', '--no-deps', 'init')), + ('Configuration check', ('run', '--rm', '--no-deps', '--entrypoint', '/usr/local/bin/oac-core', 'core', 'check-config'))] for step, check in checks: - failure = (f'{step} failed; no service was changed. See the output above.' if previous == revision - else f'Upgrade stopped; the running services are unchanged. {step} failed; see the output above.') - run(failure, *compose, *check, cwd=destination) + run(f'{step} failed; no service was changed. See the output above.', *compose, *check, cwd=destination) else: run('Compose configuration check failed; see the output above. Nothing was installed.', *compose, 'config', '--quiet', cwd=destination) node_payload.activate(payload, revision) @@ -196,8 +188,6 @@ def install(bundle, destination, host, port, public_url, upgrade): for name in ('oac', 'compose.override.yaml', 'compose.yaml') if existing else ('node-payload', '.env', 'oac', 'compose.override.yaml', 'compose.yaml'): os.replace(stage / name, destination / name) committed = True - # Installations from earlier releases kept a checksum that no longer describes compose.yaml. - (destination / 'compose-sha256sums.txt').unlink(missing_ok=True) print(f'Configuration ready in {destination}. Starting services…', flush=True) run('Starting services failed; see the output above. Settings and data are kept: fix the cause and rerun the same command.', 'docker', 'compose', 'up', '-d', '--wait', '--wait-timeout', '180', '--pull', 'never', '--remove-orphans', cwd=destination) @@ -208,13 +198,7 @@ def install(bundle, destination, host, port, public_url, upgrade): published = docker('docker', 'compose', 'port', 'web', '8080', cwd=destination) if published.returncode or not published.stdout.strip(): raise ValueError(f"Web is running without its published port {web_port}; something else holds it.{port_holder_hint(web_port)}\nFree the port, then rerun the same command.") - # Only the running release's native installers are served. - for other in (destination / 'native-installers').glob('*'): - if other.name != revision: - shutil.rmtree(other) print(f"Console: {settings.get('OAC_PUBLIC_URL') or 'http://localhost:' + web_port}") - if previous and previous != revision: - print(f"Upgraded from {previous[:12]} to {revision[:12]}. Settings → Sandbox and scheduling offers this release's Runtime; sandboxes keep their current Runtime until you apply it.") if existing: print(f'Core key: {destination / "oac"} core-key --show') else: @@ -234,7 +218,6 @@ def main(): parser.add_argument('--host', default='127.0.0.1') parser.add_argument('--web-port', type=int, default=8080) parser.add_argument('--public-url', help='HTTP(S) origin reachable by browsers and nodes; required for a new installation') - parser.add_argument('--upgrade', action='store_true', help='upgrade an existing installation to this release') args = parser.parse_args() if not args.install_dir.is_absolute() or args.install_dir == Path('/'): parser.error('installation directory must be absolute and cannot be /') @@ -249,7 +232,7 @@ def main(): parser.error('public URL must be a canonical http or https origin without a path, such as https://core.example') destination = args.install_dir.resolve() try: - install(BUNDLE, destination, args.host, args.web_port, args.public_url, args.upgrade) + install(BUNDLE, destination, args.host, args.web_port, args.public_url) except KeyboardInterrupt: sys.exit('Interrupted; rerun the same command to continue.') except PermissionError as error: diff --git a/deploy/test_install.py b/deploy/test_install.py index 7523fe949..18d8c9407 100644 --- a/deploy/test_install.py +++ b/deploy/test_install.py @@ -70,7 +70,7 @@ def test_no_arguments_run_the_installer(self): def test_version_forms_and_dangling_version(self): for arguments in [('--version=v1.2.3',), ('--version', 'v1.2.3')]: self.assertEqual(self.run_launcher(arguments=arguments).returncode, 0) - result = self.run_launcher(arguments=('--upgrade', '--version')) + result = self.run_launcher(arguments=('--install-dir', '--version')) self.assertEqual(result.returncode, 1) self.assertIn('Missing release tag', result.stderr) diff --git a/deploy/test_install_source.py b/deploy/test_install_source.py index 0f494eebf..f88fe34f0 100644 --- a/deploy/test_install_source.py +++ b/deploy/test_install_source.py @@ -47,14 +47,14 @@ def docker(self, *args, **kwargs): reply = self.replies.get(key, (0, '127.0.0.1:19873' if key == 'port' else '', '')) return subprocess.CompletedProcess(args, reply[0], reply[1], reply[2]) - def install(self, upgrade=False, image='sha256:' + '1' * 64, failure=None, port=19873): + def install(self, image='sha256:' + '1' * 64, failure=None, port=19873): def run(message, *args, **kwargs): self.calls.append((args, kwargs)) if failure and failure in args: raise ValueError(message) with patch.object(installer, 'ensure_docker_image', return_value=image) as load, patch.object(installer, 'run', side_effect=run), patch.object(installer, 'docker', side_effect=self.docker): self.load = load - installer.install(self.bundle, self.destination, '127.0.0.1', port, None if upgrade else f'http://127.0.0.1:{port}', upgrade) + installer.install(self.bundle, self.destination, '127.0.0.1', port, f'http://127.0.0.1:{port}') def main(self): """Run the command line and return its exit message.""" @@ -144,43 +144,23 @@ def test_same_release_reruns_init_before_the_configuration_check(self): with self.assertRaisesRegex(ValueError, '^Data volume check failed; no service was changed. See the output above.$'): self.install(failure='init') - def test_upgrade_checks_the_new_release_before_replacing_it_and_keeps_operator_entries(self): + def test_another_release_is_refused_and_a_rerun_keeps_operator_entries(self): self.install() override = json.loads((self.destination / 'compose.override.yaml').read_text()) nfs = {'type': 'bind', 'source': '/srv/workspaces', 'target': '/srv/workspaces'} override['services']['core']['volumes'] = [nfs] - override['services']['core']['image'] = 'old' (self.destination / 'compose.override.yaml').write_text(json.dumps(override)) + self.install() + services = json.loads((self.destination / 'compose.override.yaml').read_text())['services'] + self.assertEqual(services['core']['volumes'], [nfs]) + self.assertEqual(os.stat(self.destination / 'compose.override.yaml').st_mode & 0o777, 0o600) before = self.snapshot() - upgraded = 'b' * 40 - self.manifest['source_commit'] = upgraded + self.manifest['source_commit'] = 'b' * 40 (self.bundle / 'manifest.json').write_text(json.dumps(self.manifest)) self.write_sums() - (self.bundle / 'oac').write_bytes(b'upgraded operator') - - self.calls = [] - with self.assertRaisesRegex(ValueError, 'rerun with --upgrade'): + with self.assertRaisesRegex(ValueError, 'Reinstall to use this release'): self.install(image='sha256:' + '2' * 64) self.assertEqual(self.snapshot(), before) - with self.assertRaisesRegex(ValueError, '^Upgrade stopped; the running services are unchanged. Data volume check failed; see the output above.$'): - self.install(upgrade=True, image='sha256:' + '2' * 64, failure='init') - self.assertEqual(self.snapshot(), before) - - self.calls = [] - self.install(upgrade=True, image='sha256:' + '2' * 64) - self.assertEqual([command[-1] for command in self.commands()], ['check-config', 'init', '--remove-orphans']) - services = json.loads((self.destination / 'compose.override.yaml').read_text())['services'] - self.assertEqual(services['core']['volumes'], [nfs]) - self.assertEqual(os.stat(self.destination / 'compose.override.yaml').st_mode & 0o777, 0o600) - self.assertFalse([name for name, service in services.items() if 'image' in service]) - self.assertEqual((self.destination / 'compose.yaml').read_text().count('sha256:' + '2' * 64), 4) - self.assertEqual([volume['target'] for volume in services['web']['volumes']], ['/node-payload']) - self.assertIn('OAC_REVISION: ' + upgraded, (self.destination / 'compose.yaml').read_text()) - self.assertEqual((self.destination / 'oac').read_bytes(), b'upgraded operator') - self.assertEqual((self.destination / '.env').read_bytes(), before['.env']) - payload = self.destination / 'node-payload' - self.assertEqual(json.loads((payload / 'active.json').read_text()), {'source_commit': upgraded}) - self.assertEqual(sorted(path.name for path in (payload / 'releases').iterdir()), [self.revision, upgraded]) def test_core_serves_the_bundled_native_installers_of_the_running_release(self): native = self.bundle / 'native-installers' @@ -194,14 +174,6 @@ def test_core_serves_the_bundled_native_installers_of_the_running_release(self): self.assertEqual((mount['type'], mount['source'], mount['read_only']), ('bind', str(served), True)) self.assertEqual((served / 'linux-amd64.tar.gz').read_bytes(), b'native') self.assertEqual(os.stat(served / 'linux-amd64.tar.gz').st_mode & 0o777, 0o644) - upgraded = 'b' * 40 - self.manifest['source_commit'] = upgraded - (self.bundle / 'manifest.json').write_text(json.dumps(self.manifest)) - self.write_sums() - self.install(upgrade=True) - self.assertEqual([path.name for path in (self.destination / 'native-installers').iterdir()], [upgraded]) - core = json.loads((self.destination / 'compose.override.yaml').read_text())['services']['core'] - self.assertEqual([volume['source'] for volume in core['volumes']], [str(self.destination / 'native-installers' / upgraded)]) def test_thin_bundle_rejected_before_creating_installation(self): self.manifest['artifacts'] = {'native/bin/node': {'filename': 'missing'}} diff --git a/docs/configuration.md b/docs/configuration.md index 08018548d..897068315 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -126,8 +126,8 @@ The installer creates `~/.oac/core` by default (`$HOME/.oac/core` on Windows). I | Path | Content | Changed by | | --- | --- | --- | | `.env` | Process settings and the stable Compose project name | You, then `oac apply` | -| `compose.yaml` | Verified release service definition | The installer, when it installs or [upgrades](./getting-started/operations.md#upgrade) | -| `compose.override.yaml` | Optional local Compose entries. For a source bundle it is JSON and also holds the four service images and Web's `/node-payload` bind | You. `install.py` replaces only its own entries and requires JSON. `oac install --upgrade` removes the `/node-payload` bind and the host `node-payload/` directory, because a release serves its payload from the data volume | +| `compose.yaml` | Verified release service definition | The installer, when it installs | +| `compose.override.yaml` | Optional local Compose entries. For a source bundle it is JSON and also holds the four service images and Web's `/node-payload` bind | You. `install.py` replaces only its own entries and requires JSON | | `oac` (`oac.exe` on Windows) | Native management command | The installer | The sibling `.lock` directory remains for synchronization; `.staging` holds unpublished installation files. Neither contains service data. On Unix the installer creates private directories with mode `0700` and configuration files with mode `0600`. diff --git a/docs/development.md b/docs/development.md index 8e136b683..e4bad1ea7 100644 --- a/docs/development.md +++ b/docs/development.md @@ -128,7 +128,7 @@ Choose focused checks for the current diff and its directly affected behavior us | Claude Code adapter and artifact | `make check-claude-sdk` | | Web UI and clients | `make check-web` | | Distribution or installer | `make check-distribution` | -| Documentation | `make check-docs` (Markdown links and anchors), `make check-names`, and `pnpm --dir website check-translations` for Chinese mirrors | +| Documentation | `make check-docs` (Markdown links and anchors) and `pnpm --dir website check-translations` for Chinese mirrors | Fixture browser acceptance uses loopback ports 18092 and 4174. Select unused ports with `AGENTS_FIXTURE_PORT` and `AGENTS_WEB_PORT` when running parallel validation. Keep databases, ports and containers separate between validation workers. Compilation, fixture success and live model/provider acceptance establish different facts; report skipped or unavailable checks explicitly. Follow the [review workflow](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/CONTRIBUTING.md#review) after validation. diff --git a/docs/getting-started/install-options.md b/docs/getting-started/install-options.md index c8ca80bd4..28ccc0201 100644 --- a/docs/getting-started/install-options.md +++ b/docs/getting-started/install-options.md @@ -27,9 +27,8 @@ On Windows, `irm … | iex` cannot pass flags and the default execution policy b | `--public-url` | Derived from the host's private address, or `http://localhost:`; see [Choose the public URL](./public-address.md#choose-the-public-url) | `OAC_PUBLIC_URL` | The origin applications, nodes and executors use | | `--host` | `0.0.0.0` | `OAC_HOST` | The address Web binds; see [Listeners and access](./public-address.md#listeners-and-access) | | `--web-port` | `8080` | `OAC_WEB_PORT` | The host port of Web; see [Ports](./public-address.md#ports) | -| `--install-dir` | `~/.oac/core` | | Absolute installation directory. Use an empty or missing directory for a new installation, or an existing one to [retry](./install.md#install) or upgrade | +| `--install-dir` | `~/.oac/core` | | Absolute installation directory. Use an empty or missing directory for a new installation, or an existing one to [retry](./install.md#install) | | `--version` | `latest` | | A published release tag | -| `--upgrade` | | | Upgrade the existing installation in `--install-dir` to the installer's release; see [Upgrade](./operations.md#upgrade) | The flags that map to `.env` are written once. After installation, edit that file and run `oac apply`. Rerunning the installer on an existing installation ignores them, names those that differ from the saved settings, and starts it with the saved settings. @@ -77,7 +76,7 @@ On either platform, open its server terminal and run `docker compose ls` to find After signing in, [choose a sandbox backend](./sandbox-backends.md) and add nodes using [Add and manage nodes](./nodes.md). The Compose stack deploys the control plane; execution machines remain separate. -Stop with `docker compose stop` using the same files and environment. Back up the data volume and configuration together while the services are stopped. To upgrade, back up first, then deploy the new release's `compose.yaml` with the same project, environment and data volume: initialization upgrades the volume and Core migrates the database. [Upgrade](./operations.md#upgrade) describes what is kept and why an older release is refused. +Stop with `docker compose stop` using the same files and environment. Back up the data volume and configuration together while the services are stopped. To move to another release, [reinstall](./install.md#install). ## Offline hosts @@ -97,11 +96,10 @@ python3 oac--linux-amd64/install.py --install-dir "$HOME/.oac/local" --h | `--host` | `127.0.0.1` | The address Web binds. Unlike `oac install`, `install.py` defaults to loopback: pass `0.0.0.0` for access from other machines | | `--web-port` | `8080` | The host port of Web | | `--public-url` | | The origin browsers and nodes use. Required for a new installation | -| `--upgrade` | | Upgrade an existing installation to this bundle's release; see [Upgrade](./operations.md#upgrade) | Replace `YOUR_CORE_HOST` with an address your browser and sandbox nodes can reach, or use your HTTPS origin behind a reverse proxy. A new installation prints the Core key; a rerun prints the command that shows it. The installation is the standard Compose installation plus a read-only `node-payload` directory holding the bundle's public node artifacts. Allow disk space for both the extracted bundle and the installed copy. -Rerunning the same bundle's `install.py` on its installation directory resumes the installation with the saved settings. A bundle from another commit refuses an existing installation unless you pass `--upgrade`. If a new installation fails before it starts the services, it leaves the installation directory as it found it; fix the cause and rerun the same command. +Rerunning the same bundle's `install.py` on its installation directory resumes the installation with the saved settings. A bundle from another commit refuses an existing installation. If a new installation fails before it starts the services, it leaves the installation directory as it found it; fix the cause and rerun the same command. For a startup failure or a stopped installation, use the saved configuration instead of reinstalling: diff --git a/docs/getting-started/install.md b/docs/getting-started/install.md index e035062d3..ca7401a54 100644 --- a/docs/getting-started/install.md +++ b/docs/getting-started/install.md @@ -42,7 +42,7 @@ irm https://github.com/MiniMax-AI/OpenAgentCore/releases/latest/download/install ::: -The installer checks Docker and the port, writes the [installation directory](../configuration.md#installation-directory) `~/.oac/core` with its `.env` and the `oac` command (`oac.exe` on Windows), and starts the services. Web serves the console on port 8080 and [forwards API requests to Core](./public-address.md#listeners-and-access). Downloads and configuration checks finish before the installation directory is created, so a failed new installation leaves nothing behind. It prints the console address and, on a fresh installation, the Core key; a rerun or upgrade prints `oac core-key --show` instead. +The installer checks Docker and the port, writes the [installation directory](../configuration.md#installation-directory) `~/.oac/core` with its `.env` and the `oac` command (`oac.exe` on Windows), and starts the services. Web serves the console on port 8080 and [forwards API requests to Core](./public-address.md#listeners-and-access). Downloads and configuration checks finish before the installation directory is created, so a failed new installation leaves nothing behind. It prints the console address and, on a fresh installation, the Core key; a rerun prints `oac core-key --show` instead. The console address defaults to this host's private IP on the default route, or `http://localhost:8080` when there is none. Browsers must open exactly that address. To choose it yourself, for example the HTTPS address of an existing reverse proxy, pass `--public-url`. To pass flags on Windows, see [Pass flags](./install-options.md#pass-flags). @@ -61,7 +61,7 @@ curl -fsSL https://openagentcore.dev/install.sh | bash -s -- --public-url https: > [!TIP] > Rerunning is safe. If the installer stops on a failed check, on a service that is not healthy within three minutes, or on Ctrl-C, fix what the message names and run the same command again. A rerun keeps the configuration and data and reuses existing images. `--public-url`, `--host` and `--web-port` apply only to a new installation; to change them later, edit `.env` and run `oac apply`. On Linux and macOS the launcher also resumes an unfinished download. [Troubleshooting](./troubleshooting.md#installation) lists the messages. -To move to a newer release, follow [Upgrade](./operations.md#upgrade). +To move to a newer release, reinstall: [uninstall](./operations.md#uninstall) the installation, then run the installer again. ## Sign in to Web @@ -101,5 +101,5 @@ This step is optional for a trial on one machine. Applications, nodes and sandbo | --- | --- | | Make an API call | [Run your first Session](./quickstart.md) | | Add Linux nodes | [Add and manage nodes](./nodes.md) | -| Back up, upgrade and uninstall | [Operations](./operations.md) | +| Back up and uninstall | [Operations](./operations.md) | | Flags, Compose and offline hosts | [Installation options](./install-options.md) | diff --git a/docs/getting-started/nodes.md b/docs/getting-started/nodes.md index 5580872f5..b513200d1 100644 --- a/docs/getting-started/nodes.md +++ b/docs/getting-started/nodes.md @@ -80,7 +80,7 @@ Root only prepares the account, the group and the unit; everything else, the Doc **One Core per host.** Nodes share the `oac-node` account, so a host serves one Core; a command from a second Core is refused. -**Node software.** A node keeps the host program of the console that added it; an upgrade of Core doesn't replace it. A release that changes the node protocol lists the actions it requires; see [protocol changes](./operations.md#protocol-changes). +**Node software.** A node keeps the host program of the console that added it. To move a node to another release, [remove it](#remove-a-node) and add it again. ## Rerun, expiry and slow links diff --git a/docs/getting-started/operations.md b/docs/getting-started/operations.md index 038c765c8..713843fc6 100644 --- a/docs/getting-started/operations.md +++ b/docs/getting-started/operations.md @@ -2,7 +2,7 @@ title: "Operations" --- -Day-to-day tasks for the operator of a Core installation: the `oac` command, service health, the Core key, backups, upgrades and uninstalling. Nodes have [their own guide](./nodes.md), and [Troubleshooting](./troubleshooting.md) lists installer and access messages. Settings are in the [Configuration reference](../configuration.md). +Day-to-day tasks for the operator of a Core installation: the `oac` command, service health, the Core key, backups and uninstalling. Nodes have [their own guide](./nodes.md), and [Troubleshooting](./troubleshooting.md) lists installer and access messages. Settings are in the [Configuration reference](../configuration.md). ## The oac command @@ -10,7 +10,7 @@ Each installation has its own `oac` command in its directory (`oac.exe` on Windo | Command | What it does | | --- | --- | -| `oac install` | Installs, resumes or [upgrades](#upgrade) an installation; the installer scripts run it | +| `oac install` | Installs an installation or resumes one that stopped before it started; the installer scripts run it | | `oac apply` | Checks the data volume and `.env`, then starts the services and recreates those whose configuration changed; see [how oac apply works](../configuration.md#how-oac-apply-works). A failed check changes no service. On success it prints `Services are running with the current settings.` | | `oac core-key [--show]` | Identifies the key location in the data volume, or prints the key with `--show` | | `oac rotate-core-key` | Replaces the Core key and restarts Core and Web, then says how to read the new key | @@ -125,7 +125,7 @@ Core records which key made each public resource write; the retention of that hi ## Back up -A recoverable backup contains one consistent, stopped-write set. A database dump or Core volume export alone does not include independent workspace storage or node compute state. Restore the data volume together with the installation directory from the same backup, which holds the release that wrote it; a newer release can [upgrade](#upgrade) the restored installation afterward. +A recoverable backup contains one consistent, stopped-write set. A database dump or Core volume export alone does not include independent workspace storage or node compute state. Restore the data volume together with the installation directory from the same backup, which holds the release that wrote it. ### Single-host backup and restore {#single-host-backup} @@ -181,7 +181,7 @@ For a logical database copy during the stopped-write window, leave only the data ```sh docker compose -f "$HOME/.oac/core/compose.yaml" exec -T database \ - pg_dump -U agents_api agents_api > oac-backup.sql + pg_dump -U oac oac > oac-backup.sql ``` A logical dump still needs the matching secrets, state, independent filesystems and required node resources. Docker Desktop's **Volumes** export is a way to copy a stopped data volume; it does not coordinate these other components. @@ -196,53 +196,6 @@ The cold released baseline resumes only qualified native history after new compu Start the database, Core and node services only after their restored dependencies are complete; keep ingress restricted while checking them. In an isolated rehearsal, verify a retained Session's files and native continuation on fresh compute where qualified, confirm that deleted objects cannot be reopened, and check that no old writer can access the restored namespace. Record the results before relying on the backup for recovery. -## Upgrade {#upgrade} - -An installation of v0.0.10 or later upgrades in place to a newer release. It keeps its database, data volume, `.env`, secrets, Core key, installation ID and the entries you added to `compose.override.yaml`. An older installation can't upgrade: install the new release into a new directory. Downgrades are refused. - -> [!IMPORTANT] -> Before you upgrade, read the target release's notes for [protocol changes](#protocol-changes) and the actions they require, and [back up](#single-host-backup) the installation. The upgrade takes no backup, and after it the backup is the only way back to the previous release. - -1. Run the target release's installer with `--upgrade` and the installation directory: - - ```sh - curl -fsSL https://openagentcore.dev/install.sh | bash -s -- --upgrade --install-dir "$HOME/.oac/core" - ``` - - `--version TAG` selects a release other than the latest. On Windows, run `& ([scriptblock]::Create((irm https://github.com/MiniMax-AI/OpenAgentCore/releases/latest/download/install.ps1))) --upgrade --install-dir "$HOME\.oac\core"`. For an installation from a [source bundle](../development.md#install-a-checkout-without-publishing-a-release), run the new bundle's installer: `python3 /install.py --install-dir --upgrade`. -2. When you are ready, [apply the release's Runtime](#runtime-after-an-upgrade). - -Without `--upgrade`, another release's installer changes nothing and stops with `This installation runs release ; this installer is . Back up the installation (Upgrade in the operations guide), then rerun with --upgrade.` (`install.py` says `this bundle is`). The installed release's installer resumes the installation, with or without the flag. Installation and mutating `oac` commands share the [installation lock](../configuration.md#installation-directory); if another command is running, wait for it to finish before retrying. - -### What the upgrade does - -The installer downloads the release's `compose.yaml` and pulls missing images. Before it replaces any file, it runs the new release's configuration check and initialization with the installation's `.env` and Compose overrides. Initialization upgrades the data volume, or refuses a downgrade. The installer then puts the new `compose.yaml` and `oac` in place and starts the services, recreating those that changed; Core migrates the database as it starts. [Deployment](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/README.md#installation) describes each step. The restart interrupts work as any [Core restart](#stop-and-restart) does; nodes and sandboxes keep running and reconnect. - -On success the installer prints the console address, `oac core-key --show` in place of the key, and the line `Upgraded from to . Settings → Sandbox and scheduling offers this release's Runtime; sandboxes keep their current Runtime until you apply it.` - -If a check fails, the installer prints `Upgrade stopped; the running services are unchanged` with Docker's output, and the previous release keeps serving. Fix the cause and rerun the command. Once initialization has upgraded the data volume, the previous release can no longer start on it: if a later step fails, for example with `startup failed; data retained, rerun the installer`, rerun the same command, which repeats every step safely, or [roll back](#roll-back). - -### Runtime after an upgrade - -The upgrade leaves the sandbox configuration unchanged: nodes keep serving the saved Runtime, and new Sessions use it. To move to the release's Runtime, open **Settings** → **Sandbox and scheduling**, choose **Use this installation's Runtime** in the **Runtime** row, review the change and choose **Save configuration**. Nodes then prepare the new generation, and existing sandboxes keep theirs until they are released; see [Change the sandbox configuration](./nodes.md#change-the-sandbox-configuration). An E2B deployment has no **Runtime** row; its template keeps working unless the release changes the Core–Runtime protocol. - -### Protocol changes - -Three connections require the same protocol version on both sides: Core and its nodes, a node and its microsandbox helper, and Core and the Runtime. The upgrade changes no node host and no running sandbox. A release that changes one of these protocols says so in its release notes and names which of these actions it requires: - -- reinstall each node: [remove it](./nodes.md#remove-a-node), clean up the host, then add the host again with **Add node**. Core refuses to remove a node that still holds sandboxes, so archive those Sessions before you upgrade; -- apply the release's Runtime right after the upgrade; sandboxes on earlier generations stop working; -- [build a new E2B template](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/deploy/e2b/README.md#build-a-template) and select it in [the sandbox configuration](./nodes.md#change-the-sandbox-configuration); -- reconnect [self-hosted machines](./self-hosted.md#reconnect-an-existing-session). - -### Downgrades - -Initialization refuses a release that is not newer than the installed one, with `This installation runs release (); this release, (), is not newer. Downgrades are not supported: run that release or a newer one, or restore the backup taken before the upgrade.` The installer reports it as a stopped upgrade, and the installation keeps running. Core refuses a database that a newer release migrated, with `Agents API database schema version N is newer than this Core supports (M). Downgrades are not supported: run the release that upgraded this database or a newer one, or restore the backup taken before the upgrade.` To return to an earlier release, [roll back](#roll-back). - -### Roll back - -A rollback restores the backup taken before the upgrade: stop the services, [restore](#restore-and-rehearse) the data volume and the installation directory, then start them. Restore before you apply the release's Runtime if you can: once nodes have prepared it, the restored database doesn't know their new generation, so also [remove every node](./nodes.md#remove-a-node) and add it again. Sessions and data created after the backup are lost. - ## Uninstall Remove nodes first; see [Remove a node](./nodes.md#remove-a-node). diff --git a/docs/getting-started/troubleshooting.md b/docs/getting-started/troubleshooting.md index 21effffdd..9432f9067 100644 --- a/docs/getting-started/troubleshooting.md +++ b/docs/getting-started/troubleshooting.md @@ -13,8 +13,8 @@ For a failing request or Turn, start with Web's **Monitor → Request log** and | `Web port unavailable; choose --web-port` | Another program holds that port. Stop it, or choose another `--web-port`. The installer does not move to a different port. When the message names a Compose project `oac-…`, an earlier installation whose directory is gone still runs: remove it as in [Uninstall](./operations.md#uninstall) | | `incomplete installation: preserve … and choose another directory` | Preserve the directory and choose another `--install-dir` | | `configuration check failed; no service was changed` | `.env` has a value Core rejects. The message names the variable and not the value. Fix `.env` and run `oac apply` again | -| `data volume check failed; no service was changed` | Initialization refused the data volume; the message names the cause, such as a volume a newer release upgraded. See [downgrades](./operations.md#downgrades) | -| `This installation runs release …` | The installer or initialization belongs to another release. To move to a newer one, follow [Upgrade](./operations.md#upgrade); downgrades are refused | +| `data volume check failed; no service was changed` | Initialization refused the data volume; the message names the cause, such as a volume that belongs to another release (`existing data belongs to release …`). Reinstall to use this release | +| `This installation runs release …` | The installer or initialization belongs to another release. Reinstall to use this release | | `This account cannot use Docker` | Add the account to the `docker` group and log in again, or use Docker Desktop or rootless Docker. Do not run the installer with `sudo` | | `This account cannot access …` | An earlier run with `sudo` left files owned by root. Run the `sudo chown` command the message prints, then rerun as your own account | | `Docker Compose 2.26 or newer is required` | Install or update the Docker Compose plugin | @@ -54,7 +54,7 @@ A registered node that can't connect to Core says why in its [log](./nodes.md#lo | Another status, such as `HTTP 404 on the WebSocket upgrade` or `HTTP 502 …` | The reverse proxy doesn't route `/api/v1` to Core or doesn't preserve Host, or Core is not running | | `dial tcp: …`, such as `connection refused`, `i/o timeout` or `no such host` | The host can't reach Core at the public URL. Check DNS and firewalls; the node service uses the [proxy](./nodes.md#download-through-a-proxy) the installer ran with, so rerun the command with the right settings if the proxy changed | | `tls: …`, or `TLS certificate expired or not yet valid` | The host doesn't trust Core's certificate, or the public URL uses `https` on a port that doesn't serve it | -| `Core did not accept the node: …` | Core closed the connection after the node introduced itself. After a Core upgrade, apply the release's [protocol changes](./operations.md#protocol-changes); otherwise read Core's log | +| `Core did not accept the node: …` | Core closed the connection after the node introduced itself. Read Core's log | ### Installer messages @@ -63,7 +63,7 @@ A registered node that can't connect to Core says why in its [log](./nodes.md#lo | `The enrollment command expired or was already used` | Generate a new command in Web and run it within an hour | | `Core rejected the node configuration read (HTTP …)` | Check the Core URL and the reverse proxy in front of it, then rerun | | `Not enough disk space under …` or `The disk is full` | Free the space it names, then rerun; the download resumes | -| This host's node program differs from the one this console serves | Core was upgraded after the node was added. Choose **Remove node**, uninstall it on the host, then add it again with **Add node** | +| This host's node program differs from the one this console serves | Core was reinstalled after the node was added. Choose **Remove node**, uninstall it on the host, then add it again with **Add node** | | Another node installation is running, or this node is preparing a new sandbox configuration | Retry once **Nodes** no longer shows **Preparing target** | | A sandbox provider check failed before registering, such as `This host lacks a capability …` | Fix what it names (see [Readiness codes](./nodes.md#readiness-codes)) and rerun; the token was not used | | Core's public URL changed after this command was generated | Generate a new command in Web and run it | diff --git a/docs/maintainers.md b/docs/maintainers.md index ad53ac0b2..9f1a3fe33 100644 --- a/docs/maintainers.md +++ b/docs/maintainers.md @@ -37,7 +37,7 @@ make build-core-distribution | `CORE_DISTRIBUTION_MICROSANDBOX_ARCHIVE` | Cached microsandbox release archive. Default: `~/.oac/cache/microsandbox-v0.7.8-linux-x86_64.tar.gz`, downloaded when missing | | `CORE_DISTRIBUTION_DATABASE_IMAGE` | PostgreSQL 16 image; the default is the linux/amd64 manifest of the index in `deploy/compose/database-image.txt` | -The build reuses the Core, Web, Runtime, SDK and helper builders. The manifest records the commit, its commit time (`source_time`, which orders releases for upgrades) and source tree, image config and OCI manifest digests, the Runtime OCI manifest digest, the microsandbox runtime and firmware hashes, and the size and SHA-256 of every Runtime and node artifact; native installers carry only their SHA-256 in the [catalog](#native-installers). Output is the control archive and its `.sha256`, the optional offline archive, and the versioned Runtime, node and native installer assets. Nothing is published. Rebuilding into a directory that already holds this commit's distribution is refused. +The build reuses the Core, Web, Runtime, SDK and helper builders. The manifest records the commit and its source tree, image config and OCI manifest digests, the Runtime OCI manifest digest, the microsandbox runtime and firmware hashes, and the size and SHA-256 of every Runtime and node artifact; native installers carry only their SHA-256 in the [catalog](#native-installers). Output is the control archive and its `.sha256`, the optional offline archive, and the versioned Runtime, node and native installer assets. Nothing is published. Rebuilding into a directory that already holds this commit's distribution is refused. The ingress image carries only `oac` and four node metadata files: `manifest.json`, `SHA256SUMS`, `node-install.pyz` and `runtime/seccomp.json`. The build prepares their manifest from the same Runtime and artifact identities as the distribution, then verifies initialization with networking disabled. The node manifest omits control-plane image identities so it can be packaged before the ingress image itself exists. @@ -137,8 +137,6 @@ flowchart LR Tags use `vMAJOR.MINOR.PATCH`, optionally with a prerelease suffix such as `-rc.1` and build metadata such as `+build.1`. A prerelease suffix creates a GitHub prerelease. Pushing the tag is the release decision. Automated checks establish build and test results, not real-model qualification: assess live execution evidence before you push the tag. Model credentials and private certificate authorities never enter CI or release inputs, including acceptance images that contain them. -The release notes list every protocol version change (the node wire protocol, the microsandbox helper or the Core–Runtime protocol) with the actions it requires of operators. Core must never require a secret or file that only a newer initialization creates: an upgrade runs the new Core's `check-config` before the new `init`. After the Release is published, set `deploy/compose/smoke-pins.json` to its tag and the SHA-256 of its `compose.yaml`, so the Compose smoke upgrades from the latest published release. - The workflow runs `check` on the tagged commit, including the full local gate, official-client and image acceptance, and the native matrix with its packaging artifacts enabled. After checks succeed, one `build` job on `blacksmith-4vcpu-ubuntu-2204` prepares the pinned Runtime inputs, reuses the native installers, builds the distribution and publishes directly from its local files. This combined job has `contents: write` and `packages: write`; checkout does not persist credentials. It retains the assets as an uncompressed Actions artifact only when publication fails, for recovery, or when a manual build does not publish. Distribution and Runtime archives use `pigz` level 6 with at most four compression workers and no filename or timestamp in the gzip header. The publisher verifies archive and native installer checksums, resolves the repository identity, refuses an existing Release or draft for the tag and creates one draft with a fixed ID. Up to four assets upload concurrently, largest first, without retries. After confirming the complete remote inventory, the publisher validates all image archives and existing registry tags before pushing up to four images concurrently. Each image config and registry manifest is verified; any error leaves the Release unpublished. In-flight transfers finish before a failed operation returns. The publisher rechecks the version tag before publishing the draft by its ID. @@ -179,7 +177,7 @@ The planner compares the PR event's tested merge commit with its verified first | --- | --- | | `hygiene` | Names, repository links, bundled documentation integrity, and CI planner/gate tests; runs for every change | | `distribution` | Harness catalog and installer schema, install/apply/recovery/cleanup tests, Compose parsing and initialization fixtures, release/download and bundle contracts, Go console tests and build; needs Docker Compose for template parsing, no pnpm install or browser | -| `compose` | Real startup from empty volumes using images built from the checkout, sign-in and API access, file upload and node installer download, then URL reconfiguration and container recreation with preserved credentials and data, and an in-place upgrade from the pinned published release; needs Docker, Go and network access, no model credentials | +| `compose` | Real startup from empty volumes using images built from the checkout, sign-in and API access, file upload and node installer download, then URL reconfiguration and container recreation with preserved credentials and data; needs Docker, Go and network access, no model credentials | | `backend` | Parallel parts, each with a dedicated PostgreSQL guard: `runtime` (sqlc freshness, Runtime/shared Go tests, Linux microsandbox helper, daemon build), `core` (standalone Core build, Core service and client tests) and three `integration` shards of the serial Core integration test package | | `harness` | Claude Code adapter tests and packaging, MiniMax companion scripts | | `example` | Optional application typecheck, tests, build and isolated browser acceptance | @@ -192,7 +190,7 @@ The planner compares the PR event's tested merge commit with its verified first `.github/actionlint.yaml` selects hygiene and lint. Known workflow changes select their consumers: the CI review and actionlint workflows run hygiene and lint; native workflow changes add native checks; API acceptance workflow changes add API checks with container acceptance enabled; website workflow changes add website checks. The shared Node action selects every job that uses it plus lint. A new or unclassified workflow/action selects the full gate until its consumers are declared in the planner. Planner tests and CI measurement scripts run hygiene; changing the planner itself runs the full gate. -Compose template and Compose test changes select both `distribution` fixtures and the `compose` smoke job; Core, Web, shared Go packages and the image Dockerfiles also select the smoke job. Run `python3 scripts/compose-smoke.py` locally with Docker available to repeat it. The script uses a unique project, an automatically assigned loopback port and artifacts under `~/.oac/tests/`; it removes its containers and volumes on exit. CI also performs cleanup after a failed or interrupted smoke step. Diagnostics show container status without printing HTTP response bodies or sign-in keys. Core, Web and the ingress image are built from the checkout, with node metadata for the checkout's commit; Web serves a placeholder page instead of the console build, and the initialization container runs with networking disabled. The upgrade phase starts a new installation from the published `compose.yaml` of the release pinned by tag and SHA-256 in `deploy/compose/smoke-pins.json`, creates a Project, a Project key and a file, then runs the installer's preflight with the checkout's images (`core check-config`, then `init`) and `up --wait`. It checks that the installation ID, Core key, Project, Project key and file survive, that Core and the node payload report the checkout's commit and that the schema is at the latest migration. The smoke matrix runs on native Linux amd64 and arm64 runners; the native matrix builds and tests the shared Core installer on Linux, macOS and Windows. +Compose template and Compose test changes select both `distribution` fixtures and the `compose` smoke job; Core, Web, shared Go packages and the image Dockerfiles also select the smoke job. Run `python3 scripts/compose-smoke.py` locally with Docker available to repeat it. The script uses a unique project, an automatically assigned loopback port and artifacts under `~/.oac/tests/`; it removes its containers and volumes on exit. CI also performs cleanup after a failed or interrupted smoke step. Diagnostics show container status without printing HTTP response bodies or sign-in keys. Core, Web and the ingress image are built from the checkout, with node metadata for the checkout's commit; Web serves a placeholder page instead of the console build, and the initialization container runs with networking disabled. The smoke matrix runs on native Linux amd64 and arm64 runners; the native matrix builds and tests the shared Core installer on Linux, macOS and Windows. `scripts/ci_plan.py` selects groups from the changed paths. Each selected set includes `hygiene`, mixed changes accumulate their consumers, and every job reads the same plan instead of keeping its own path list. A notification-only PR skips database, browser and native jobs; a notification plus Core change adds backend and API checks. diff --git a/docs/zh/configuration.md b/docs/zh/configuration.md index 785f048aa..8a6409851 100644 --- a/docs/zh/configuration.md +++ b/docs/zh/configuration.md @@ -1,7 +1,7 @@ --- title: "配置参考" source: docs/configuration.md -source_hash: 676250762ae38625aef9f3aeda80427424d97342b3aeaf52dcc61661142d7b36 +source_hash: c1675b5ebd7f5cdda3d7acc5cea2663714a634220e9a59d0fc5cc6b6b0891e58 --- Core 安装的每项设置、其归属位置以及修改方式。安装程序写入初始值;之后请使用本参考。每项设置都恰好只有一个归属位置,分属以下三类: @@ -130,8 +130,8 @@ Session 创建可以在异步工作区准入之前成功。容量耗尽时,Cor | 路径 | 内容 | 修改者 | | --- | --- | --- | | `.env` | 进程设置和固定的 Compose 项目名 | 用户修改后运行 `oac apply` | -| `compose.yaml` | 已校验的发行版服务定义 | 安装程序,在安装或[升级](getting-started/operations.md#upgrade)时 | -| `compose.override.yaml` | 可选的本地 Compose 条目。源码包安装中它是 JSON,还保存四个服务镜像和 Web 的 `/node-payload` 绑定 | 用户。`install.py` 只替换自己的条目,并要求 JSON 格式。`oac install --upgrade` 会删除 `/node-payload` 绑定和主机上的 `node-payload/` 目录,因为发布版本从数据卷提供该载荷 | +| `compose.yaml` | 已校验的发行版服务定义 | 安装程序,在安装时 | +| `compose.override.yaml` | 可选的本地 Compose 条目。源码包安装中它是 JSON,还保存四个服务镜像和 Web 的 `/node-payload` 绑定 | 用户。`install.py` 只替换自己的条目,并要求 JSON 格式 | | `oac`(Windows 为 `oac.exe`) | 原生管理命令 | 安装程序 | 同级 `.lock` 目录用于同步操作并一直保留;`.staging` 保存尚未就位的安装文件。两者都不保存服务数据。Unix 上安装程序以 `0700` 创建私有目录,以 `0600` 创建配置文件。 diff --git a/docs/zh/development.md b/docs/zh/development.md index 42dcb22f6..102c1183b 100644 --- a/docs/zh/development.md +++ b/docs/zh/development.md @@ -1,7 +1,7 @@ --- title: "开发 OpenAgentCore" source: docs/development.md -source_hash: 1e525c548fb44a07bcd8331b3b3768fd144c1bf029bcdc1ac9d38f9c48435b9d +source_hash: f0e015b4f13aeafa5ec486c42a5d4210f6dd9a9ce40b34112cdef1ed3aad28ba --- 准备工作副本,构建组件并验证修改。如需使用已安装的实例,从[概览](getting-started/index.md)开始。修改代码前阅读[贡献者规则](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/CONTRIBUTING.md)。 @@ -130,7 +130,7 @@ Core 构建产物和输出目录设置见[独立 Core 构建](maintainers.md#sta | Claude Code adapter 与产物 | `make check-claude-sdk` | | Web UI 与 client | `make check-web` | | 发行或安装程序 | `make check-distribution` | -| 文档 | `make check-docs`(Markdown 链接与锚点)、`make check-names`,以及用于中文镜像的 `pnpm --dir website check-translations` | +| 文档 | `make check-docs`(Markdown 链接与锚点),以及用于中文镜像的 `pnpm --dir website check-translations` | Fixture 浏览器验收使用 loopback 端口 18092 和 4174。并行验证时通过 `AGENTS_FIXTURE_PORT` 和 `AGENTS_WEB_PORT` 选择空闲端口。验证 worker 之间使用独立数据库、端口和容器。编译、fixture 成功和真实模型/提供商验收分别证明不同事实;应明确报告跳过或不可用的检查。验证后遵循[审查流程](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/CONTRIBUTING.md#review)。 diff --git a/docs/zh/getting-started/install-options.md b/docs/zh/getting-started/install-options.md index d81fa1be2..e489dd72f 100644 --- a/docs/zh/getting-started/install-options.md +++ b/docs/zh/getting-started/install-options.md @@ -1,7 +1,7 @@ --- title: "安装选项" source: docs/getting-started/install-options.md -source_hash: de599bae1dc932f9f1c25f06c2b67bee8b90d2287ebd9e45157141128ac9fec1 +source_hash: 8cfae2d39cc95a588bc231fe9f3661f3c22db7884b2278c6a1483c114a5185fd --- 本页是安装程序参数的参考,并说明如何用默认方式以外的方式安装:在已有反向代理之后、在托管平台上使用 Docker Compose,或在无法访问互联网的主机上。[默认安装](./install.md)无需任何选项。 @@ -29,9 +29,8 @@ Windows 上 `irm … | iex` 无法传参,默认执行策略也会拦截下载 | `--public-url` | 由主机的私网地址推导,或 `http://localhost:`;参阅[选择公开 URL](./public-address.md#choose-the-public-url) | `OAC_PUBLIC_URL` | 应用、节点和执行器使用的源地址 | | `--host` | `0.0.0.0` | `OAC_HOST` | Web 绑定的地址;参阅[监听器与访问](./public-address.md#listeners-and-access) | | `--web-port` | `8080` | `OAC_WEB_PORT` | Web 的主机端口;参阅[端口](./public-address.md#ports) | -| `--install-dir` | `~/.oac/core` | | 绝对安装目录。新安装使用空目录或不存在的目录;[重试](./install.md#install)或升级时使用已有安装目录 | +| `--install-dir` | `~/.oac/core` | | 绝对安装目录。新安装使用空目录或不存在的目录;[重试](./install.md#install)时使用已有安装目录 | | `--version` | `latest` | | 已发布的版本标签 | -| `--upgrade` | | | 将 `--install-dir` 中的已有安装升级到安装程序所属的版本;参阅[升级](./operations.md#upgrade) | 映射到 `.env` 的参数只会写入一次。安装完成后,编辑该文件并运行 `oac apply`。对已有安装重新运行安装程序时,这些参数会被忽略,安装程序会指出与已保存设置不同的参数,并以已保存的设置启动。 @@ -79,7 +78,7 @@ docker compose exec web oac-web core-key 登录后,[选择沙箱提供商](./sandbox-backends.md),并按[添加和管理节点](./nodes.md)添加节点。Compose 堆栈部署控制平面;执行机器仍需单独部署。 -使用相同的文件和环境运行 `docker compose stop` 以停止服务。停止服务后,一并备份数据卷和配置。升级时先备份,再使用相同的项目、环境和数据卷部署新版本的 `compose.yaml`:初始化会升级数据卷,Core 会迁移数据库。[升级](./operations.md#upgrade)说明会保留哪些内容,以及为何拒绝较旧的版本。 +使用相同的文件和环境运行 `docker compose stop` 以停止服务。停止服务后,一并备份数据卷和配置。要换到另一个版本,请[重新安装](./install.md#install)。 ## 离线主机 {#offline-hosts} @@ -99,11 +98,10 @@ python3 oac--linux-amd64/install.py --install-dir "$HOME/.oac/local" --h | `--host` | `127.0.0.1` | Web 绑定的地址。与 `oac install` 不同,`install.py` 默认使用回环地址:需要从其他机器访问时传入 `0.0.0.0` | | `--web-port` | `8080` | Web 的主机端口 | | `--public-url` | | 浏览器和节点使用的源地址。新安装必须提供 | -| `--upgrade` | | 将已有安装升级到此安装包所属的版本;参阅[升级](./operations.md#upgrade) | 将 `YOUR_CORE_HOST` 替换为浏览器和沙箱节点都能访问的地址,或在反向代理之后使用你的 HTTPS 源地址。新安装会打印 Core 密钥;重新运行则打印显示它的命令。该安装是标准的 Compose 安装,外加一个只读的 `node-payload` 目录,其中保存安装包的公开节点制品。请为解压后的安装包和已安装的副本都预留磁盘空间。 -在其安装目录上重新运行同一安装包的 `install.py`,会以已保存的设置继续安装。来自其他提交的安装包会拒绝已有安装,除非传入 `--upgrade`。如果新安装在启动服务之前失败,它会保持安装目录原样;修复原因后重新运行同一命令。 +在其安装目录上重新运行同一安装包的 `install.py`,会以已保存的设置继续安装。来自其他提交的安装包会拒绝已有安装。如果新安装在启动服务之前失败,它会保持安装目录原样;修复原因后重新运行同一命令。 启动失败或安装中断时,使用已保存的配置,而不是重新安装: diff --git a/docs/zh/getting-started/install.md b/docs/zh/getting-started/install.md index 1ee216f5d..ec3f614c2 100644 --- a/docs/zh/getting-started/install.md +++ b/docs/zh/getting-started/install.md @@ -1,7 +1,7 @@ --- title: "安装 Core 和 Web" source: docs/getting-started/install.md -source_hash: e638457e86e53dac6f049c96e1a1dbd91646a26b1b6179f90c5b39f8750425b5 +source_hash: ec59f9762d21e486cf84108e4dda087c9640095196527bb8059c6833279df8ec --- 本指南带运维人员从一台空主机到可用的安装:在 Docker 下运行的 Core、Web 控制台和 PostgreSQL,一个沙箱提供商,一个默认模型和第一个项目 API 密钥。Web 的 **概览** 通过 **新手引导** 清单跟踪同样的步骤。 @@ -44,7 +44,7 @@ irm https://github.com/MiniMax-AI/OpenAgentCore/releases/latest/download/install ::: -安装程序检查 Docker 和端口,写入[安装目录](../configuration.md#installation-directory) `~/.oac/core`,其中包含 `.env` 和 `oac` 命令(Windows 为 `oac.exe`),然后启动服务。Web 在 8080 端口提供控制台,并[把 API 请求转发到 Core](./public-address.md#listeners-and-access)。下载和配置检查在创建安装目录之前完成,因此全新安装失败时不会留下任何文件。安装程序会打印控制台地址;仅在全新安装时打印 Core 密钥,重新运行或升级则打印 `oac core-key --show`。 +安装程序检查 Docker 和端口,写入[安装目录](../configuration.md#installation-directory) `~/.oac/core`,其中包含 `.env` 和 `oac` 命令(Windows 为 `oac.exe`),然后启动服务。Web 在 8080 端口提供控制台,并[把 API 请求转发到 Core](./public-address.md#listeners-and-access)。下载和配置检查在创建安装目录之前完成,因此全新安装失败时不会留下任何文件。安装程序会打印控制台地址;仅在全新安装时打印 Core 密钥,重新运行则打印 `oac core-key --show`。 控制台地址默认是本机默认路由上的私网 IP;没有时为 `http://localhost:8080`。浏览器必须打开这个确切的地址。要自行指定,例如已有反向代理的 HTTPS 地址,请传入 `--public-url`。Windows 上传参的方式见[传入参数](./install-options.md#pass-flags)。 @@ -63,7 +63,7 @@ curl -fsSL https://openagentcore.dev/install.sh | bash -s -- --public-url https: > [!TIP] > 可以放心重新运行。如果安装程序因检查失败、服务三分钟内未变为健康或按 Ctrl-C 而停止,修复提示信息所指的问题后重新执行同一命令。重新运行会保留配置和数据,并复用已有镜像。`--public-url`、`--host` 和 `--web-port` 只对全新安装生效;之后要修改它们,请编辑 `.env` 并运行 `oac apply`。在 Linux 和 macOS 上,启动脚本还会继续未完成的下载。[故障排查](./troubleshooting.md#installation)列出了这些提示信息。 -要换到更新的发布版本,请按[升级](./operations.md#upgrade)操作。 +要换到更新的发布版本,请重新安装:先[卸载](./operations.md#uninstall)现有安装,再运行安装程序。 ## 登录 Web {#sign-in-to-web} @@ -103,5 +103,5 @@ curl -fsSL https://openagentcore.dev/install.sh | bash -s -- --public-url https: | --- | --- | | 发起 API 调用 | [运行第一个 Session](./quickstart.md) | | 添加 Linux 节点 | [添加和管理节点](./nodes.md) | -| 备份、升级和卸载 | [运维](./operations.md) | +| 备份和卸载 | [运维](./operations.md) | | 参数、Compose 和离线主机 | [安装选项](./install-options.md) | diff --git a/docs/zh/getting-started/nodes.md b/docs/zh/getting-started/nodes.md index 443e76f6b..d2ca38fe8 100644 --- a/docs/zh/getting-started/nodes.md +++ b/docs/zh/getting-started/nodes.md @@ -1,7 +1,7 @@ --- title: "添加和管理节点" source: docs/getting-started/nodes.md -source_hash: 86f0ce968eff1d82b31620be4c216e44741ad6a0536ef9dfde3d5a96c6bd0675 +source_hash: 6581acc433887f6006933cbacdf4eee0810d1d1e96871b31c968612ced3b74de --- 节点是一台 Linux 主机,为 Core 托管 Session 运行沙箱。本指南面向在沙箱提供商(参阅[选择沙箱提供商](./sandbox-backends.md))为 microsandbox 或 Docker 时添加、修改和移除节点的运维人员;E2B 不需要节点。 @@ -82,7 +82,7 @@ root 只准备账号、组和服务单元;其他操作(包括 Docker 网络 **每台主机只服务一个 Core。** 节点共享 `oac-node` 账号,因此主机只服务一个 Core;第二个 Core 的命令会被拒绝。 -**节点程序。** 节点保留添加它的控制台所提供的主机程序;升级 Core 不会替换它。更改节点协议的版本会列出所需操作;参阅[协议变更](operations.md#protocol-changes)。 +**节点程序。** 节点保留添加它的控制台所提供的主机程序。要让节点使用另一个版本,请[移除节点](#remove-a-node)后重新添加。 ## 重新运行、过期与慢速网络 {#rerun-expiry-and-slow-links} diff --git a/docs/zh/getting-started/operations.md b/docs/zh/getting-started/operations.md index e8ebd043e..d3d8b8234 100644 --- a/docs/zh/getting-started/operations.md +++ b/docs/zh/getting-started/operations.md @@ -1,10 +1,10 @@ --- title: "运维" source: docs/getting-started/operations.md -source_hash: 54c6d2de28e483497c00a3f2bfc310b4d8b7ff22519607778e7340ec851f252e +source_hash: 2f6435641f47d00db1ae4556594d99367223270013dfdf282c16648f8a6cd275 --- -Core 安装运维人员的日常任务:`oac` 命令、服务健康状态、Core 密钥、备份、升级和卸载。节点有[专门的指南](./nodes.md),[故障排查](./troubleshooting.md)列出了安装程序和访问方面的提示信息。设置见[配置参考](../configuration.md)。 +Core 安装运维人员的日常任务:`oac` 命令、服务健康状态、Core 密钥、备份和卸载。节点有[专门的指南](./nodes.md),[故障排查](./troubleshooting.md)列出了安装程序和访问方面的提示信息。设置见[配置参考](../configuration.md)。 ## oac 命令 {#the-oac-command} @@ -12,7 +12,7 @@ Core 安装运维人员的日常任务:`oac` 命令、服务健康状态、Cor | 命令 | 功能 | | --- | --- | -| `oac install` | 安装、继续或[升级](#upgrade)安装;安装脚本会运行它 | +| `oac install` | 安装,或继续一个在启动前停止的安装;安装脚本会运行它 | | `oac apply` | 检查数据卷和 `.env`,然后启动服务,并重新创建配置有变化的服务;参见 [oac apply 的工作方式](../configuration.md#how-oac-apply-works)。校验失败时不改动任何服务。成功时打印 `Services are running with the current settings.` | | `oac core-key [--show]` | 指出 Core 密钥在数据卷中的位置;加上 `--show` 时打印密钥本身 | | `oac rotate-core-key` | 替换 Core 密钥并重启 Core 和 Web,然后说明如何读取新密钥 | @@ -127,7 +127,7 @@ Core 记录每次公开资源写入所使用的密钥;历史保留策略为 [` ## 备份 {#back-up} -可恢复的备份必须是一组在停止写入期间取得的一致数据。仅数据库转储或 Core 卷导出不包含独立工作区存储及节点计算状态。恢复数据卷时,同时恢复同一份备份中的安装目录,其中保存着写入该数据的版本;之后可以用更新的版本[升级](#upgrade)恢复后的安装。 +可恢复的备份必须是一组在停止写入期间取得的一致数据。仅数据库转储或 Core 卷导出不包含独立工作区存储及节点计算状态。恢复数据卷时,同时恢复同一份备份中的安装目录,其中保存着写入该数据的版本。 ### 单机备份与恢复 {#single-host-backup} @@ -183,7 +183,7 @@ docker run --rm -v "${project}_data:/data" -v ~/oac-backup:/backup:ro --entrypoi ```sh docker compose -f "$HOME/.oac/core/compose.yaml" exec -T database \ - pg_dump -U agents_api agents_api > oac-backup.sql + pg_dump -U oac oac > oac-backup.sql ``` 逻辑转储仍然需要匹配的 secrets、state、独立文件系统及所需节点资源。Docker Desktop 的 **Volumes** 导出可用于复制已停止的数据卷,但不会协调这些其他部分。 @@ -198,53 +198,6 @@ docker compose -f "$HOME/.oac/core/compose.yaml" exec -T database \ 恢复依赖全部完整后才启动 database、Core 和节点服务;检查期间保持入口受限。在隔离演练中,验证保留 Session 的文件和具备资格时在新计算资源上的原生续接,确认已删除对象无法重新打开,并检查旧写入方均无法访问恢复后的命名空间。在依赖备份进行恢复前记录结果。 -## 升级 {#upgrade} - -v0.0.10 及更高版本的安装可以原地升级到更新的发布版本,并保留数据库、数据卷、`.env`、密钥、Core 密钥、安装 ID 以及你在 `compose.override.yaml` 中添加的条目。更早的安装无法升级:请将新版本安装到新目录。降级会被拒绝。 - -> [!IMPORTANT] -> 升级之前,阅读目标版本的发布说明,了解[协议变更](#protocol-changes)及其要求的操作,并[备份](#single-host-backup)安装。升级不会备份;升级之后,备份是回到先前版本的唯一途径。 - -1. 使用目标版本的安装程序,带上 `--upgrade` 和安装目录运行: - - ```sh - curl -fsSL https://openagentcore.dev/install.sh | bash -s -- --upgrade --install-dir "$HOME/.oac/core" - ``` - - `--version TAG` 选择最新版本以外的发布版本。Windows 上运行 `& ([scriptblock]::Create((irm https://github.com/MiniMax-AI/OpenAgentCore/releases/latest/download/install.ps1))) --upgrade --install-dir "$HOME\.oac\core"`。对于从[源码包](../development.md#install-a-checkout-without-publishing-a-release)安装的实例,运行新源码包的安装程序:`python3 /install.py --install-dir --upgrade`。 -2. 准备好后,[应用该版本的 Runtime](#runtime-after-an-upgrade)。 - -不带 `--upgrade` 时,其他版本的安装程序不做任何更改,并以 `This installation runs release ; this installer is . Back up the installation (Upgrade in the operations guide), then rerun with --upgrade.` 停止(`install.py` 的提示为 `this bundle is`)。无论是否带该参数,已安装版本的安装程序都会继续该安装。安装程序和修改状态的 `oac` 命令共用[安装锁](../configuration.md#installation-directory);其他命令正在运行时,等待其结束后重试。 - -### 升级会做什么 {#what-the-upgrade-does} - -安装程序下载该版本的 `compose.yaml` 并拉取缺失的镜像。替换任何文件之前,它使用安装的 `.env` 和 Compose 覆盖文件运行新版本的配置检查和初始化。初始化会升级数据卷,或拒绝降级。随后安装程序将新的 `compose.yaml` 和 `oac` 放到位并启动服务,重新创建有变化的服务;Core 启动时迁移数据库。[部署](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/README.md#installation)说明了每个步骤。这次重启对工作的影响与任何一次 [Core 重启](#stop-and-restart)相同;节点和沙箱继续运行并重新连接。 - -成功时,安装程序打印控制台地址、用 `oac core-key --show` 代替密钥,以及一行 `Upgraded from to . Settings → Sandbox and scheduling offers this release's Runtime; sandboxes keep their current Runtime until you apply it.` - -如果某项检查失败,安装程序会打印 `Upgrade stopped; the running services are unchanged` 及 Docker 的输出,先前版本继续提供服务。修复原因后重新运行命令。初始化升级数据卷后,先前版本便无法再在该数据卷上启动:如果后续步骤失败(例如出现 `startup failed; data retained, rerun the installer`),重新运行同一命令,它会安全地重复每个步骤;也可以[回滚](#roll-back)。 - -### 升级后的 Runtime {#runtime-after-an-upgrade} - -升级不会改变沙箱配置:节点继续服务已保存的 Runtime,新 Session 也使用它。要切换到该版本的 Runtime,打开 **设置** → **沙箱与调度**,在 **Runtime** 行选择 **使用本安装的 Runtime**,确认更改后选择 **保存配置**。节点随后准备新代次,现有沙箱保留原代次,直到被释放;参阅[修改沙箱配置](nodes.md#change-the-sandbox-configuration)。E2B 部署没有 **Runtime** 行;除非该版本更改了 Core–Runtime 协议,其模板继续可用。 - -### 协议变更 {#protocol-changes} - -以下三种连接要求两端的协议版本相同:Core 与节点、节点与 microsandbox 辅助程序、Core 与 Runtime。升级不会改动任何节点主机或正在运行的沙箱。更改其中某个协议的版本会在发布说明中注明,并指出需要以下哪些操作: - -- 重新安装每个节点:[移除节点](nodes.md#remove-a-node),清理主机,再通过 **添加节点** 重新添加该主机。Core 拒绝移除仍持有沙箱的节点,所以升级前先归档这些 Session; -- 升级后立即应用该版本的 Runtime;仍在较早代次上的沙箱将停止工作; -- [构建新的 E2B 模板](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/deploy/e2b/README.md#build-a-template),并在[沙箱配置](nodes.md#change-the-sandbox-configuration)中选择它; -- 重新连接[自托管机器](self-hosted.md#reconnect-an-existing-session)。 - -### 降级 {#downgrades} - -初始化会拒绝不比已安装版本更新的版本,并提示 `This installation runs release (); this release, (), is not newer. Downgrades are not supported: run that release or a newer one, or restore the backup taken before the upgrade.` 安装程序将其报告为已停止的升级,安装继续运行。Core 会拒绝由更新版本迁移过的数据库,并提示 `Agents API database schema version N is newer than this Core supports (M). Downgrades are not supported: run the release that upgraded this database or a newer one, or restore the backup taken before the upgrade.` 要回到较早的版本,请[回滚](#roll-back)。 - -### 回滚 {#roll-back} - -回滚即恢复升级前的备份:停止服务,[恢复](#restore-and-rehearse)数据卷和安装目录,然后启动服务。尽量在应用该版本的 Runtime 之前恢复:节点准备好它之后,恢复的数据库并不知道节点的新代次,因此还需[移除每个节点](nodes.md#remove-a-node)并重新添加。备份之后创建的 Session 和数据会丢失。 - ## 卸载 {#uninstall} 先移除节点,参见[移除节点](./nodes.md#remove-a-node)。 diff --git a/docs/zh/getting-started/troubleshooting.md b/docs/zh/getting-started/troubleshooting.md index 3821bc359..39fd40754 100644 --- a/docs/zh/getting-started/troubleshooting.md +++ b/docs/zh/getting-started/troubleshooting.md @@ -1,7 +1,7 @@ --- title: "故障排查" source: docs/getting-started/troubleshooting.md -source_hash: c562ed30e22adc12c0be5dbfbcbf740aaa977c467d38bf6c2f1f5bc961d90ccc +source_hash: 8cbeb8f1ecce90c1b386e05fdca5239f39a153c51c1720567bc19980bfeb5c0d --- 按症状查找安装、访问、节点和 Session 问题的原因与解决方法。本页面向 Core 安装的运维人员。 @@ -15,8 +15,8 @@ source_hash: c562ed30e22adc12c0be5dbfbcbf740aaa977c467d38bf6c2f1f5bc961d90ccc | `Web port unavailable; choose --web-port` | 其他程序占用该端口。停止它,或另选 `--web-port`。安装程序不会改用其他端口。如果消息点名了 Compose 项目 `oac-…`,说明目录已不存在的早期安装仍在运行:按[卸载](./operations.md#uninstall)移除它 | | `incomplete installation: preserve … and choose another directory` | 保留目录内容,选择另一个 `--install-dir` | | `configuration check failed; no service was changed` | `.env` 中有 Core 拒绝的值。消息只包含变量名。修正 `.env` 后再次运行 `oac apply` | -| `data volume check failed; no service was changed` | 初始化拒绝了数据卷;消息会说明原因,例如数据卷已被更新的版本升级。参阅[降级](./operations.md#downgrades) | -| `This installation runs release …` | 安装程序或初始化属于另一个版本。要换到更新的版本,请按[升级](./operations.md#upgrade)操作;降级会被拒绝 | +| `data volume check failed; no service was changed` | 初始化拒绝了数据卷;消息会说明原因,例如数据卷属于另一个版本(`existing data belongs to release …`)。请重新安装以使用此版本 | +| `This installation runs release …` | 安装程序或初始化属于另一个版本。请重新安装以使用此版本 | | `This account cannot use Docker` | 将该账号加入 `docker` 组后重新登录,或使用 Docker Desktop 或 rootless Docker。不要用 `sudo` 运行安装程序 | | `This account cannot access …` | 之前用 `sudo` 运行留下了属于 root 的文件。运行消息给出的 `sudo chown` 命令,再以自己的账号重新运行 | | `Docker Compose 2.26 or newer is required` | 安装或更新 Docker Compose 插件 | @@ -56,7 +56,7 @@ microsandbox 沙箱无法访问私有地址上的模型提供商或 MCP 服务 | 其他状态码,例如 `HTTP 404 on the WebSocket upgrade` 或 `HTTP 502 …` | 反向代理没有将 `/api/v1` 转发到 Core 或没有保留 Host,或者 Core 未运行 | | `dial tcp: …`,例如 `connection refused`、`i/o timeout` 或 `no such host` | 主机无法通过公开 URL 访问 Core。检查 DNS 和防火墙;节点服务使用安装程序运行时的[代理](./nodes.md#download-through-a-proxy),代理变化后请用正确的设置重新执行命令 | | `tls: …`,或 `TLS certificate expired or not yet valid` | 主机不信任 Core 的证书,或公开 URL 对不提供 HTTPS 的端口使用了 `https` | -| `Core did not accept the node: …` | 节点自我介绍后 Core 关闭了连接。Core 升级后,执行该发行版的[协议变更](./operations.md#protocol-changes);否则查看 Core 日志 | +| `Core did not accept the node: …` | 节点自我介绍后 Core 关闭了连接。查看 Core 日志 | ### 安装程序消息 {#installer-messages} @@ -65,7 +65,7 @@ microsandbox 沙箱无法访问私有地址上的模型提供商或 MCP 服务 | `The enrollment command expired or was already used` | 在 Web 生成新命令并在 1 小时内执行 | | `Core rejected the node configuration read (HTTP …)` | 检查 Core URL 及其前面的反向代理后重试 | | `Not enough disk space under …` 或 `The disk is full` | 按提示释放空间后重试,下载会继续 | -| This host's node program differs from the one this console serves | 节点添加后 Core 已升级。选择 **移除节点**,在主机上卸载,再通过 **添加节点** 重新添加 | +| This host's node program differs from the one this console serves | 节点添加后 Core 已重新安装。选择 **移除节点**,在主机上卸载,再通过 **添加节点** 重新添加 | | Another node installation is running, or this node is preparing a new sandbox configuration | 待 **节点** 不再显示 **正在准备目标配置** 后重试 | | 注册前的沙箱提供商检查失败,例如 `This host lacks a capability …` | 修复提示的问题(参阅[就绪状态码](./nodes.md#readiness-codes))后重试;令牌未被使用 | | Core's public URL changed after this command was generated | 在 Web 生成并执行新命令 | diff --git a/docs/zh/maintainers.md b/docs/zh/maintainers.md index 8c25a491e..ca5f226e2 100644 --- a/docs/zh/maintainers.md +++ b/docs/zh/maintainers.md @@ -1,7 +1,7 @@ --- title: "构建并发布 OpenAgentCore" source: docs/maintainers.md -source_hash: 12472ab16b7a282f88c4a53d0f204ed27694665f15962f1c93a20a6bde96023b +source_hash: bf140052262bf0255190a87767308af560c10fe4fd9eed93bd86836aefeb524a --- 本指南面向负责构建和发布 OpenAgentCore 的维护者。要安装 Core 和 Web,请按 [安装 Core 和 Web](getting-started/install.md) 操作。安装程序代码遵循的规则见 [部署](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/README.md) 和 [节点安装程序](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/node/README.md);必需检查见 [CONTRIBUTING](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/CONTRIBUTING.md#required-checks)。 @@ -39,7 +39,7 @@ make build-core-distribution | `CORE_DISTRIBUTION_MICROSANDBOX_ARCHIVE` | 已缓存的 microsandbox 发布归档。默认值:`~/.oac/cache/microsandbox-v0.7.8-linux-x86_64.tar.gz`,缺失时下载 | | `CORE_DISTRIBUTION_DATABASE_IMAGE` | PostgreSQL 16 镜像;默认值为 `deploy/compose/database-image.txt` 中索引的 linux/amd64 清单 | -构建过程会复用 Core、Web、Runtime、SDK 和辅助程序构建器。清单会记录提交、其提交时间(`source_time`,用于在升级时为发布版本排序)和源代码树、镜像配置及 OCI 清单摘要、Runtime OCI 清单摘要、microsandbox 运行时和固件哈希,以及每个 Runtime 和节点构件的大小与 SHA-256;原生安装程序在[目录](#native-installers)中仅记录其 SHA-256。输出包括控制归档及其 `.sha256`、可选的离线归档,以及带版本号的 Runtime、节点和原生安装程序资源。此过程不会发布任何内容。如果目标目录中已包含此提交的分发包,重建会拒绝执行。 +构建过程会复用 Core、Web、Runtime、SDK 和辅助程序构建器。清单会记录提交及其源代码树、镜像配置及 OCI 清单摘要、Runtime OCI 清单摘要、microsandbox 运行时和固件哈希,以及每个 Runtime 和节点构件的大小与 SHA-256;原生安装程序在[目录](#native-installers)中仅记录其 SHA-256。输出包括控制归档及其 `.sha256`、可选的离线归档,以及带版本号的 Runtime、节点和原生安装程序资源。此过程不会发布任何内容。如果目标目录中已包含此提交的分发包,重建会拒绝执行。 ingress 镜像仅包含 `oac` 和四个节点元数据文件:`manifest.json`、`SHA256SUMS`、`node-install.pyz` 与 `runtime/seccomp.json`。构建流程根据与分发包相同的 Runtime 和构件身份生成其清单,再禁用网络验证初始化。节点清单不包含控制平面镜像身份,因此可在 ingress 镜像生成前打包。 @@ -139,8 +139,6 @@ flowchart LR 标签使用 `vMAJOR.MINOR.PATCH` 格式,可选用 `-rc.1` 等预发布后缀以及 `+build.1` 等构建元数据。预发布后缀会创建 GitHub 预发布版。推送标签即表示发布决定。自动检查用于确定构建和测试结果,而不是真实模型资格:推送标签前应评估实际执行证据。模型凭据和私有证书颁发机构绝不能进入 CI 或发布输入,包含它们的验收镜像也不例外。 -发布说明会列出每一项协议版本变更(节点线协议、microsandbox 辅助程序或 Core–Runtime 协议),以及它要求运维人员执行的操作。Core 绝不能依赖只有更新版本的初始化才会创建的密钥或文件:升级会先运行新 Core 的 `check-config`,再运行新的 `init`。Release 发布后,将 `deploy/compose/smoke-pins.json` 设为它的标签及其 `compose.yaml` 的 SHA-256,使 Compose 冒烟测试从最新的已发布版本升级。 - 工作流会在带标签的提交上运行 `check`,包括完整的本地门禁、官方客户端和镜像验收,以及启用打包构件的原生平台矩阵。检查成功后,`blacksmith-4vcpu-ubuntu-2204` 上的一个 `build` 作业会准备固定的 Runtime 输入、复用原生安装程序、构建分发包,并直接从本地文件发布。此合并作业具有 `contents: write` 和 `packages: write` 权限;检出过程不会保留凭据。只有发布失败或手动构建不发布时,才会把这些文件保留为未压缩的 Actions 构建产物,供恢复使用。 分发归档和 Runtime 归档使用 `pigz` 级别 6,最多使用四个压缩工作线程,并且 gzip 头部中不包含文件名或时间戳。发布器会验证归档和原生安装程序校验和、解析仓库身份、拒绝使用该标签已有的 Release 或草稿,并创建一个具有固定 ID 的草稿。最多四个资源可并发上传,按从大到小的顺序进行,且不会重试。确认完整的远程资源清单后,发布器会验证所有镜像归档和现有注册表标签,再并发推送最多四个镜像。每个镜像配置和注册表清单都会接受验证;任何错误都会使 Release 保持未发布状态。失败操作返回前,正在进行的传输会完成。发布器会按 ID 发布草稿,并在发布前重新检查版本标签。 @@ -181,7 +179,7 @@ gh workflow run core-release --repo MiniMax-AI/OpenAgentCore --ref main \ | --- | --- | | `hygiene` | 名称、仓库链接、随包文档完整性以及 CI 计划器/门禁测试;每次变更都会运行 | | `distribution` | Harness 目录和安装程序模式、安装/应用/恢复/清理测试、Compose 解析和初始化固定数据、发布/下载和捆绑包契约、Go 控制台测试与构建;模板解析需要 Docker Compose,不需要 pnpm install 或浏览器 | -| `compose` | 使用从当前检出构建的镜像从空数据卷实际启动、登录和 API 访问、上传文件和下载节点安装程序,然后在保留凭据和数据的同时重新配置 URL 并重新创建容器,并从固定的已发布版本原地升级;需要 Docker、Go 和网络访问,不需要模型凭据 | +| `compose` | 使用从当前检出构建的镜像从空数据卷实际启动、登录和 API 访问、上传文件和下载节点安装程序,然后在保留凭据和数据的同时重新配置 URL 并重新创建容器;需要 Docker、Go 和网络访问,不需要模型凭据 | | `backend` | 并行部分,每部分都有专用 PostgreSQL 保护检查:`runtime`(sqlc 新鲜度、Runtime/共享 Go 测试、Linux microsandbox 辅助程序、守护进程构建)、`core`(独立 Core 构建、Core 服务和客户端测试),以及串行 Core 集成测试包的三个 `integration` 分片 | | `harness` | Claude Code adapter 测试和打包、MiniMax 配套脚本 | | `example` | 可选的应用程序类型检查、测试、构建和隔离的浏览器验收 | @@ -194,7 +192,7 @@ gh workflow run core-release --repo MiniMax-AI/OpenAgentCore --ref main \ `.github/actionlint.yaml` 会选择 hygiene 和 lint。已知工作流变更会选择其使用方:CI review 和 actionlint 工作流运行 hygiene 和 lint;原生工作流变更会添加原生检查;API 验收工作流变更会添加启用容器验收的 API 检查;网站工作流变更会添加网站检查。共享 Node 操作会选择使用它的每个作业以及 lint。新工作流或未分类的工作流/操作会选择完整门禁,直至在计划器中声明其使用方。计划器测试和 CI 测量脚本运行 hygiene;更改计划器本身会运行完整门禁。 -Compose 模板和 Compose 测试发生变更时,会同时选择 `distribution` 固定数据和 `compose` 冒烟作业;Core、Web、共享 Go 软件包和镜像 Dockerfile 的变更也会选择冒烟作业。安装 Docker 后,可在本地运行 `python3 scripts/compose-smoke.py` 重复该测试。该脚本使用唯一的项目、自动分配的回环端口,并将在 `~/.oac/tests/` 下生成构件;退出时移除其容器和数据卷。CI 还会在冒烟步骤失败或中断后执行清理。诊断信息会显示容器状态,但不会打印 HTTP 响应正文或登录密钥。Core、Web 和 ingress 镜像都从当前检出构建,并带有当前检出提交的节点元数据;Web 提供占位页面而不是控制台构建,初始化容器禁用网络运行。升级阶段使用 `deploy/compose/smoke-pins.json` 中按标签和 SHA-256 固定的发布版本的已发布 `compose.yaml` 启动一个新安装,创建一个 Project、一个 Project 密钥和一个文件,然后使用当前检出的镜像运行安装程序的预检(先 `core check-config`,再 `init`)和 `up --wait`。它会检查安装 ID、Core 密钥、Project、Project 密钥和文件均被保留,Core 和节点载荷报告当前检出的提交,并且数据库架构处于最新迁移。冒烟矩阵使用 Linux amd64 和 arm64 原生 runner;原生矩阵在 Linux、macOS 和 Windows 上构建并测试共享的 Core 安装程序。 +Compose 模板和 Compose 测试发生变更时,会同时选择 `distribution` 固定数据和 `compose` 冒烟作业;Core、Web、共享 Go 软件包和镜像 Dockerfile 的变更也会选择冒烟作业。安装 Docker 后,可在本地运行 `python3 scripts/compose-smoke.py` 重复该测试。该脚本使用唯一的项目、自动分配的回环端口,并将在 `~/.oac/tests/` 下生成构件;退出时移除其容器和数据卷。CI 还会在冒烟步骤失败或中断后执行清理。诊断信息会显示容器状态,但不会打印 HTTP 响应正文或登录密钥。Core、Web 和 ingress 镜像都从当前检出构建,并带有当前检出提交的节点元数据;Web 提供占位页面而不是控制台构建,初始化容器禁用网络运行。冒烟矩阵使用 Linux amd64 和 arm64 原生 runner;原生矩阵在 Linux、macOS 和 Windows 上构建并测试共享的 Core 安装程序。 `scripts/ci_plan.py` 根据变更路径选择检查组。每个所选集合都包含 `hygiene`,混合变更会累加其使用方,并且每个作业都读取同一计划,而不是维护各自的路径列表。仅修改通知的 PR 会跳过数据库、浏览器和原生作业;同时修改通知和 Core 的 PR 会添加后端和 API 检查。 diff --git a/scripts/build-core-distribution.sh b/scripts/build-core-distribution.sh index cb403f83f..a2a3c5249 100755 --- a/scripts/build-core-distribution.sh +++ b/scripts/build-core-distribution.sh @@ -231,7 +231,7 @@ msb=(docker run --rm --network none --user "$(id -u):$(id -g)" \ --entrypoint /opt/microsandbox/msb "$core_image") "${msb[@]}" image load --input /runtime.tar --tag oac-runtime:distribution --quiet "${msb[@]}" image inspect oac-runtime:distribution --format json > "$stage/runtime-inspect.json" -python3 scripts/core-distribution-manifest.py node-payload "$bundle" "$stage" "$revision" "$source_tree" "$source_epoch" "$release_base_url" "$offline" +python3 scripts/core-distribution-manifest.py node-payload "$bundle" "$stage" "$revision" "$source_tree" "$release_base_url" "$offline" mkdir -p "$stage/ingress" cp deploy/distribution/Ingress.Dockerfile "$stage/ingress/Dockerfile" cp "$stage/core/bin/oac" "$stage/ingress/oac" diff --git a/scripts/check-names.py b/scripts/check-names.py deleted file mode 100644 index d1fba0a4f..000000000 --- a/scripts/check-names.py +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/env python3 -"""Reject retired identifiers in tracked text, with reasoned span-level exceptions.""" -from __future__ import annotations - -import argparse -from dataclasses import dataclass -import fnmatch -import json -from pathlib import Path -import re -import subprocess -import sys - -# These spellings are detection inputs, not supported compatibility aliases. -FORBIDDEN = re.compile( - r"(?i:parsar)|\bAGENTS_CORE_WEB_[A-Z][A-Z0-9_]*|\bAGENTS_API_[A-Z][A-Z0-9_]*|\bCORE_CONSOLE_[A-Z][A-Z0-9_]*" - r"|\bagents-api(?:-(?:migrate|device|environment-key|e2b-provider|microsandbox-provider" - r"|tool-root|codex-directory|codex-write|workspace-export|runtime-initialize|claude-shell-prefix))?\b" - r"|\bcore-console\b|\bagents-runtime\b|(?i:\bAgents? Core(?: Web)?\b)|\bagents-core-web\b" - r"|(?i:\bminimax-ai-dev\b)", -) - - -@dataclass(frozen=True) -class ExceptionRule: - path: str - regex: re.Pattern[str] - reason: str - - -def load_rules(path: Path) -> list[ExceptionRule]: - entries = json.loads(path.read_text(encoding="utf-8")) - if not isinstance(entries, list): - raise ValueError("allowlist must be a list") - rules = [] - for index, entry in enumerate(entries): - if not isinstance(entry, dict) or set(entry) != {"path", "regex", "reason"}: - raise ValueError(f"allowlist entry {index} needs path, regex and reason") - if any(not isinstance(entry[key], str) or not entry[key].strip() for key in entry): - raise ValueError(f"allowlist entry {index} has an empty path, regex or reason") - regex = re.compile(entry["regex"]) - if regex.search("") is not None: - raise ValueError(f"allowlist entry {index} matches empty text") - rules.append(ExceptionRule(entry["path"], regex, entry["reason"])) - return rules - - -def violations(path: str, content: str, rules: list[ExceptionRule], - used: set[int] | None = None) -> list[tuple[int, int, str]]: - """Return unexcused identifiers; add the index of each rule that excuses one to used.""" - allowed = [(index, match.span()) for index, rule in enumerate(rules) if fnmatch.fnmatchcase(path, rule.path) - for match in rule.regex.finditer(content)] - result = [] - for match in FORBIDDEN.finditer(content): - excusing = {index for index, (start, end) in allowed if start <= match.start() and match.end() <= end} - if excusing: - if used is not None: - used.update(excusing) - continue - line = content.count("\n", 0, match.start()) + 1 - column = match.start() - content.rfind("\n", 0, match.start()) - result.append((line, column, match.group())) - return result - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parent.parent) - parser.add_argument("--allowlist", type=Path, default=Path(__file__).with_name("name-allowlist.json")) - args = parser.parse_args() - try: - rules = load_rules(args.allowlist) - names = subprocess.check_output(["git", "-C", str(args.root), "ls-files", "-z"]).split(b"\0") - failures = [] - used: set[int] = set() - for raw in names: - if not raw: - continue - name = raw.decode("utf-8") - path = args.root / name - # Never follow a tracked symlink into files outside this repository. - if path.is_symlink() or not path.is_file(): - continue - data = path.read_bytes() - if b"\0" in data: - continue - content = data.decode("utf-8") - failures.extend((name, *item) for item in violations(name, content, rules, used)) - except (OSError, ValueError, re.error, subprocess.CalledProcessError) as error: - print(f"Name guard failed: {error}", file=sys.stderr) - return 2 - for name, line, column, token in failures: - print(f"{name}:{line}:{column}: retired identifier {token!r}") - # An exception that excuses nothing hides nothing today but would silently allow the name later. - unused = [rule for index, rule in enumerate(rules) if index not in used] - for rule in unused: - print(f"{args.allowlist.name}: exception {rule.path} {rule.regex.pattern!r} excuses no retired identifier") - if failures or unused: - print(f"Name guard found {len(failures)} unapproved identifiers and {len(unused)} unused exceptions.", - file=sys.stderr) - return 1 - print("OpenAgentCore name guard passed.") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/check-names.test.py b/scripts/check-names.test.py deleted file mode 100644 index f72aca40c..000000000 --- a/scripts/check-names.test.py +++ /dev/null @@ -1,148 +0,0 @@ -#!/usr/bin/env python3 -"""Regression tests for scoped name exceptions and repository scanning.""" -import importlib.util -import json -from pathlib import Path -import re -import subprocess -import sys -import tempfile -import unittest - -SPEC = importlib.util.spec_from_file_location("check_names", Path(__file__).with_name("check-names.py")) -names = importlib.util.module_from_spec(SPEC) -sys.modules[SPEC.name] = names -SPEC.loader.exec_module(names) - - -class NameGuardTests(unittest.TestCase): - def rule(self, pattern, path="*"): - return names.ExceptionRule(path, re.compile(pattern), "Deferred source identity") - - def test_allowed_import_does_not_hide_setting_on_same_line(self): - content = 'import "github.com/MiniMax-AI-Dev/parsar/internal/foo"; setting = "PARSAR_HOME"' - rules = [self.rule(r"github\.com/MiniMax-AI-Dev/parsar/[a-z/]+")] - self.assertEqual(names.violations("main.go", content, rules), [(1, 68, "PARSAR")]) - - def test_partial_exception_cannot_hide_longer_retired_setting(self): - self.assertEqual(names.violations("main.go", "AGENTS_API_PORT", [self.rule("AGENTS_API")]), - [(1, 1, "AGENTS_API_PORT")]) - - def test_exception_is_path_scoped_and_case_sensitive(self): - rule = self.rule("parsar", "history/*") - self.assertFalse(names.violations("history/source.json", "parsar", [rule])) - self.assertTrue(names.violations("README.md", "parsar", [rule])) - self.assertTrue(names.violations("history/source.json", "PARSAR", [rule])) - - def test_detections_include_commands_settings_labels_and_display(self): - for value in ("PaRsAr", "io.parsar.installation", "AGENTS_API_PORT", "CORE_CONSOLE_BIND", "AGENTS_CORE_WEB_ADDR", - "agents-api", "agents-api-codex-write", "core-console", "agents-runtime-dev", - "Agent Core", "Agents Core Web"): - with self.subTest(value=value): - self.assertTrue(names.violations("x", value, [])) - - def test_new_names_and_pinned_public_contract_are_not_retired(self): - content = ("OpenAgentCore oac-core OAC_CORE_PORT agents_api AgentCoreError x_agents_core core_console_session" - " github.com/MiniMax-AI/OpenAgentCore") - self.assertFalse(names.violations("x", content, [])) - - def test_coordinates_and_multiple_matches_do_not_disclose_line_content(self): - content = "first\n parsar secret=value CORE_CONSOLE_BIND" - self.assertEqual(names.violations("x", content, []), - [(2, 3, "parsar"), (2, 23, "CORE_CONSOLE_BIND")]) - - def test_multiline_exception_covers_only_its_span(self): - content = "Parsar\nproduct\nPARSAR_HOME" - rules = [self.rule("Parsar\\nproduct")] - self.assertEqual(names.violations("x", content, rules), [(3, 1, "PARSAR")]) - - def test_invalid_allowlist_fails_closed(self): - with tempfile.TemporaryDirectory() as directory: - path = Path(directory) / "rules.json" - for value in ({}, [{"path": "*", "regex": "parsar", "reason": " "}], - [{"path": "*", "regex": ".*", "reason": "too broad"}], - [{"path": "*", "regex": "[", "reason": "invalid"}], - [{"path": "*", "regex": "parsar", "reason": "valid", "typo": True}]): - path.write_text(json.dumps(value)) - with self.subTest(value=value), self.assertRaises((ValueError, re.error)): - names.load_rules(path) - - def test_repository_scan_uses_tracked_text_and_skips_binary_without_following_links(self): - with tempfile.TemporaryDirectory() as directory: - root = Path(directory) - subprocess.run(["git", "init", "-q", directory], check=True) - (root / "tracked.txt").write_text("PARSAR_HOME\n") - (root / "untracked.txt").write_text("PARSAR_OTHER\n") - (root / "binary").write_bytes(b"\0parsar") - (root / "link").symlink_to("untracked.txt") - subprocess.run(["git", "-C", directory, "add", "tracked.txt", "binary", "link"], check=True) - rules = root / "rules.json" - rules.write_text("[]") - command = [sys.executable, str(Path(names.__file__)), "--root", directory, "--allowlist", str(rules)] - result = subprocess.run(command, text=True, capture_output=True) - self.assertEqual(result.returncode, 1) - self.assertIn("tracked.txt:1:1:", result.stdout) - self.assertNotIn("untracked.txt", result.stdout) - self.assertNotIn("binary:", result.stdout) - (root / "tracked.txt").write_text("OAC_RUNTIME_HOME\n") - self.assertEqual(subprocess.run(command, capture_output=True).returncode, 0) - - def test_exception_that_excuses_no_identifier_fails_the_scan(self): - with tempfile.TemporaryDirectory() as directory: - root = Path(directory) - subprocess.run(["git", "init", "-q", directory], check=True) - (root / "tracked.txt").write_text("PARSAR_HOME X-Core-Console-Actor\n") - subprocess.run(["git", "-C", directory, "add", "tracked.txt"], check=True) - rules = root / "rules.json" - used = {"path": "tracked.txt", "regex": "PARSAR_HOME", "reason": "Excuses the setting"} - command = [sys.executable, str(Path(names.__file__)), "--root", directory, "--allowlist", str(rules)] - for unused in ({"path": "missing/*", "regex": "parsar", "reason": "Matches no file"}, - {"path": "*", "regex": "X-Core-Console-Actor", "reason": "Matches only allowed text"}): - with self.subTest(unused=unused["regex"]): - rules.write_text(json.dumps([used, unused])) - result = subprocess.run(command, text=True, capture_output=True) - self.assertEqual(result.returncode, 1) - self.assertIn(repr(unused["regex"]), result.stdout) - self.assertNotIn("PARSAR_HOME", result.stdout) - rules.write_text(json.dumps([used])) - self.assertEqual(subprocess.run(command, capture_output=True).returncode, 0) - - def test_persisted_domain_exception_does_not_allow_other_settings_or_paths(self): - rules = names.load_rules(Path(__file__).with_name("name-allowlist.json")) - content = '"parsar.agents-api.credential"; "PARSAR_HOME"' - found = names.violations("services/core/internal/credentialcrypto/cipher.go", content, rules) - self.assertEqual([item[2] for item in found], ["PARSAR"]) - self.assertTrue(names.violations("README.md", content, rules)) - - def test_scoped_exception_does_not_hide_runtime_setting(self): - rules = [self.rule(r"AGENTS_API_ADDR", path="services/core/cmd/server/process_configuration.go")] - content = '{"AGENTS_API_ADDR", "OAC_ADDR"}; os.Getenv("PARSAR_HOME")' - found = names.violations("services/core/cmd/server/process_configuration.go", content, rules) - self.assertEqual([item[2] for item in found], ["PARSAR"]) - - def test_checked_in_exceptions_do_not_hide_unrelated_retired_setting(self): - rules = names.load_rules(Path(__file__).with_name("name-allowlist.json")) - for content in ('"contracts/agents-api/index.md" PARSAR_HOME', - '"Parsar product" PARSAR_HOME'): - with self.subTest(content=content): - self.assertEqual(len(names.violations("README.md", content, rules)), 1) - - def test_retired_organization_is_rejected(self): - for content in ("Copyright (c) 2026 MiniMax-AI-Dev", "https://github.com/minimax-ai-dev/OpenAgentCore"): - with self.subTest(content=content): - self.assertEqual([item[2].lower() for item in names.violations("LICENSE", content, [])], - ["minimax-ai-dev"]) - - def test_former_repository_identities_are_rejected(self): - rules = names.load_rules(Path(__file__).with_name("name-allowlist.json")) - for content in ("https://github.com/MiniMax-AI/parsar-core", '"github.com/MiniMax-AI-Dev/parsar/internal/foo"', - "services/agents-api/cmd/server", "services/core-console", "apps/parsar-daemon/cmd/parsar-daemon", - "scripts/build-agents-api-image.sh", "scripts/build-core-console.sh", - "scripts/agents-api-subagents-acceptance.py", "make check-agents-api", '"name": "parsar-core"', - "@agents-core-web/web", "@parsar/claude-sdk-adapter", "parsar-mcode-harness"): - with self.subTest(content=content): - self.assertTrue(names.violations("README.md", content, rules)) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/ci_plan.py b/scripts/ci_plan.py index 905f45a7f..76ec014cd 100644 --- a/scripts/ci_plan.py +++ b/scripts/ci_plan.py @@ -86,7 +86,7 @@ (("scripts/generate-harness-catalog", "scripts/harness-catalog/", "scripts/openapi-split/", "scripts/patch-agents-openapi.py", "scripts/generate-public-api"), SCRIPTS, JOBS), (("scripts/check-sqlc.py", "scripts/go-test-shard.py"), (".py",), ("backend",)), - (("scripts/check-names", "scripts/name-allowlist.json", "scripts/ci_"), (".py", ".json"), ("hygiene",)), + (("scripts/ci_",), (".py",), ("hygiene",)), ((".github/workflows/", ".github/actions/"), (".yml", ".yaml"), JOBS), ) # Embedded files and test fixtures are executable inputs regardless of suffix. diff --git a/scripts/compose-smoke.py b/scripts/compose-smoke.py index 163afafbc..671b08e54 100644 --- a/scripts/compose-smoke.py +++ b/scripts/compose-smoke.py @@ -3,8 +3,7 @@ Core, Web and the ingress image are built from this checkout, with node metadata for this commit. Web serves a placeholder page instead of the console -build. A second phase starts the published release pinned in -deploy/compose/smoke-pins.json and upgrades it in place to this checkout. +build. """ import hashlib @@ -24,7 +23,6 @@ import uuid ROOT = Path(__file__).resolve().parents[1] -RELEASES = 'https://github.com/MiniMax-AI/OpenAgentCore/releases/download/' def load(name, path): @@ -49,7 +47,7 @@ def prepare_payload(destination, revision, source_time): shutil.copyfile(sources / 'node-install.pyz', destination / 'node-install.pyz') shutil.copyfile(ROOT / 'services/core/deploy/codex/seccomp.json', destination / 'runtime/seccomp.json') (destination / 'manifest.json').write_text(json.dumps( - {'source_commit': revision, 'source_time': int(source_time), 'platform': 'linux/amd64'})) + {'source_commit': revision, 'platform': 'linux/amd64'})) distribution.checksums(destination) @@ -240,52 +238,6 @@ def terminate(_signum, _frame): assert compose('exec', '-T', 'web', '/usr/local/bin/oac-web', 'core-key').decode().strip() == rotated, 'Rotated key was not retained' private_logs(key, rotated, project_key) print('PASS: startup, origin validation, sign-in, API, upload, node installer, key rotation and persistent installation', flush=True) - - pins = json.loads((ROOT / 'deploy/compose/smoke-pins.json').read_text()) - print(f'Starting the published {pins["tag"]} release in a new installation and upgrading it to this checkout', flush=True) - compose('down', '--volumes') - published = directory / 'published-compose.yaml' - urllib.request.urlretrieve(RELEASES + pins['tag'] + '/compose.yaml', published) - if hashlib.sha256(published.read_bytes()).hexdigest() != pins['compose_sha256']: - raise ValueError('Pinned release compose.yaml checksum mismatch') - env.pop('OAC_PUBLIC_URL') - released = runner([published], {name: value for name, value in env.items() if not name.startswith('OAC_IMAGE_')}) - released('up', '-d', '--wait', '--wait-timeout', '600', timeout=900) - address = 'http://' + released('port', 'web', '8080').decode().strip() - origin = 'http://localhost:8080' - browser = client() - key = released('exec', '-T', 'web', '/usr/local/bin/oac-web', 'core-key').decode().strip() - request('/console/auth/login', {'core_key': key}) - facts = get('/core/v1/installation') - project_data = get('/core/v1/projects', body={'name': 'Upgrade smoke'}, status=201) - project_key = get('/core/v1/projects/' + project_data['id'] + '/keys', body={'name': 'smoke'}, status=201)['key'] - api = {'Authorization': 'Bearer ' + project_key} - content = b'upgrade smoke\n' - uploaded = upload(api, content) - - # The installer's preflight with the new release's images, then its start. - compose('run', '--rm', '--no-deps', '--entrypoint', '/usr/local/bin/oac-core', 'core', 'check-config') - compose('run', '--rm', '--no-deps', 'init') - compose('up', '-d', '--wait', '--wait-timeout', '180', '--pull', 'never', '--remove-orphans', timeout=240) - address = 'http://' + compose('port', 'web', '8080').decode().strip() - # The release's default origin ignored the Web port; this checkout's follows it. - origin = 'http://localhost:' + port - browser = client() - assert compose('exec', '-T', 'web', '/usr/local/bin/oac-web', 'core-key').decode().strip() == key, 'Upgrade changed the Core key' - request('/console/auth/login', {'core_key': key}) - upgraded = get('/core/v1/installation') - assert upgraded['installation_id'] == facts['installation_id'], 'Upgrade changed the installation identity' - assert upgraded['public_url'] == origin, 'The upgraded default origin does not follow the Web port' - assert upgraded['source_commit'] == revision, 'Core does not run this checkout' - assert json.loads(request('/node-install/manifest.json'))['source_commit'] == revision, 'Web serves another release' - assert any(p['id'] == project_data['id'] for p in get('/core/v1/projects')['data']), 'Upgrade lost the Project' - assert get('/v1/files/' + uploaded['id'], headers=api)['bytes'] == len(content), 'Upgrade lost the Project key or file' - schema = compose('exec', '-T', 'database', 'psql', '-U', 'agents_api', '-d', 'agents_api', '-Atc', - 'SELECT max(version_id) FROM agents_api_schema_version').decode().strip() - latest = max(int(path.name.split('_', 1)[0]) for path in (ROOT / 'services/core/migrations').glob('*.sql')) - assert int(schema) == latest, 'Upgrade did not migrate the schema' - private_logs(key, project_key) - print(f'PASS: in-place upgrade from {pins["tag"]} keeps the installation, Core key, Project, Project key and file', flush=True) except BaseException: # Service status identifies failed containers without dumping secret-bearing logs. status = subprocess.run(['docker', 'compose', '-p', project, 'ps', '--all'], env=env, capture_output=True, timeout=30) diff --git a/scripts/core-distribution-manifest.py b/scripts/core-distribution-manifest.py index 3c2822d5c..18d0ec015 100644 --- a/scripts/core-distribution-manifest.py +++ b/scripts/core-distribution-manifest.py @@ -327,7 +327,7 @@ def bootstraps(bundle, epoch, revision): zipapp.create_archive(directory, bundle / "node-install.pyz", compressed=True) -def node_payload(bundle, stage, revision, source_tree, source_time, artifact_base_url="", offline="0"): +def node_payload(bundle, stage, revision, source_tree, artifact_base_url="", offline="0"): bundle, stage = pathlib.Path(bundle), pathlib.Path(stage) artifact_base_url = release_base(artifact_base_url) if not artifact_base_url and offline != "1": @@ -346,8 +346,6 @@ def node_payload(bundle, stage, revision, source_tree, source_time, artifact_bas metadata = { "source_commit": revision, "source_tree": source_tree, - # The commit time orders releases: initialization refuses an older one. - "source_time": int(source_time), "platform": "linux/amd64", "artifact_base_url": artifact_base_url, "artifacts": package_artifacts(bundle, stage, revision), diff --git a/scripts/core-distribution-manifest.test.py b/scripts/core-distribution-manifest.test.py index 5d3730f8f..cb4b78ce8 100644 --- a/scripts/core-distribution-manifest.test.py +++ b/scripts/core-distribution-manifest.test.py @@ -21,7 +21,6 @@ REVISION = "a" * 40 TREE = "b" * 40 -SOURCE_TIME = "1760000000" RELEASE_BASE = "https://example.com/releases/" + REVISION @@ -113,7 +112,7 @@ def setUp(self): self.runtime_bytes = (self.bundle / "images/runtime.tar").read_bytes() def manifest(self, base=RELEASE_BASE, offline="0"): - distribution.node_payload(self.bundle, self.stage, REVISION, TREE, SOURCE_TIME, base, offline) + distribution.node_payload(self.bundle, self.stage, REVISION, TREE, base, offline) distribution.manifest(self.bundle, self.stage) def write_inspection(self): @@ -129,9 +128,8 @@ def test_init_payload_contains_only_verified_node_metadata(self): self.assertEqual(distribution.sha256(payload / name), checksum) bundled = json.loads((payload / "manifest.json").read_text()) full = json.loads((self.bundle / "manifest.json").read_text()) - for name in ("source_commit", "source_time", "platform", "artifacts", "runtime_ref", "microsandbox"): + for name in ("source_commit", "platform", "artifacts", "runtime_ref", "microsandbox"): self.assertEqual(bundled[name], full[name]) - self.assertEqual(bundled["source_time"], 1760000000) self.assertEqual(list(bundled["images"]), ["runtime"]) def test_oci_manifest_identity_is_distinct_from_docker_config_identity(self): diff --git a/scripts/name-allowlist.json b/scripts/name-allowlist.json deleted file mode 100644 index 69b3837f1..000000000 --- a/scripts/name-allowlist.json +++ /dev/null @@ -1,272 +0,0 @@ -[ - { - "path": "*", - "regex": "contracts/agents-api(?:/[A-Za-z0-9_.*-]+)*", - "reason": "The Agents API contract directory is an existing source path, not an installed executable." - }, - { - "path": "packages/agents-client/src/*", - "regex": "(?:\\./)?fixtures/parsar-[0-9a-f]+/[A-Za-z0-9_./-]+", - "reason": "Fixture directory paths identify historical product protocol snapshots." - }, - { - "path": "packages/agents-client/src/protocol-types.test.ts", - "regex": "Parsar [0-9a-f]{8}\\b", - "reason": "The test profile labels cite the historical source revision, not the current Core brand." - }, - { - "path": "*", - "regex": "Parsar product\\b", - "reason": "This phrase explicitly refers to the separate Parsar product." - }, - { - "path": "packages/agents-client/v1/client.go", - "regex": "Parsar's execution path", - "reason": "The Go SDK does not switch the separate product execution path." - }, - { - "path": "scripts/check-names.py", - "regex": "(?m)^ r\"[^\\n]+\"[,\\n]?", - "reason": "These exact regex literals are retired-name detection inputs, not supported settings." - }, - { - "path": "scripts/name-allowlist.json", - "regex": "(?i)parsar|\\bAGENTS_CORE_WEB_[A-Z][A-Z0-9_]*|\\bAGENTS_API_[A-Z][A-Z0-9_]*|\\bCORE_CONSOLE_[A-Z][A-Z0-9_]*|\\bagents-api(?:-(?:migrate|device|environment-key|e2b-provider|microsandbox-provider|tool-root|codex-directory|codex-write|workspace-export|runtime-initialize|claude-shell-prefix))?\\b|\\bcore-console\\b|\\bagents-runtime-|\\bAgents? Core(?: Web)?\\b|@agents-core-web/|\\bminimax-ai-dev\\b", - "reason": "The reviewed guard policy must spell the names it explains; entries are checked for nonempty reasons." - }, - { - "path": "scripts/check-names.test.py", - "regex": "(?i)parsar|\\bAGENTS_CORE_WEB_[A-Z][A-Z0-9_]*|\\bAGENTS_API_[A-Z][A-Z0-9_]*|\\bCORE_CONSOLE_[A-Z][A-Z0-9_]*|\\bagents-api(?:-(?:migrate|device|environment-key|e2b-provider|microsandbox-provider|tool-root|codex-directory|codex-write|workspace-export|runtime-initialize|claude-shell-prefix))?\\b|\\bcore-console\\b|\\bagents-runtime-|\\bAgents? Core(?: Web)?\\b|@agents-core-web/|\\bminimax-ai-dev\\b", - "reason": "Guard regression fixtures intentionally contain retired identifiers, including rejected examples." - }, - { - "path": "services/core/migrations/000078_oac_runtime_names.sql", - "regex": "parsar-core-runtime@", - "reason": "The guarded up/down data migration must recognize and restore the historical stored Runtime reference prefix." - }, - { - "path": "*", - "regex": "https://developers\\.openai\\.com/api/docs/guides/agents-api(?:/[A-Za-z0-9_./-]*)?", - "reason": "Official upstream documentation URLs are external protocol references." - }, - { - "path": "services/core/internal/credentialcrypto/*.go", - "regex": "parsar\\.agents-api\\.(?:agent-model-execution\\.v1|credential(?:-fingerprint\\.v1)?|deployment-model-provider\\.v1|environment-file|environment-setup|session-model-execution\\.v1|sandbox-deployment\\.v1|skill)", - "reason": "These exact authenticated encryption domain/AAD strings must remain stable to decrypt existing stored credentials and assets." - }, - { - "path": "services/core/internal/persistence/postgres/sessionpg/creation.go", - "regex": "parsar\\.agents-api\\.model-provider-api-key\\.v1", - "reason": "This persisted model-provider key fingerprint purpose remains stable across upgrade and request replay." - }, - { - "path": "apps/daemon/internal/agent/codex/preparation_router_test.go", - "regex": "\"agents-api-(?:session)?\"", - "reason": "AgentStateKey is a persisted daemon/native-session resume identity carried on the existing machine wire contract; changing it would select another session." - }, - { - "path": "apps/daemon/internal/dispatch/local_directory_test.go", - "regex": "\"agents-api-(?:session)?\"", - "reason": "AgentStateKey is a persisted daemon/native-session resume identity carried on the existing machine wire contract; changing it would select another session." - }, - { - "path": "apps/daemon/internal/dispatch/preparation_test.go", - "regex": "\"agents-api-(?:session)?\"", - "reason": "AgentStateKey is a persisted daemon/native-session resume identity carried on the existing machine wire contract; changing it would select another session." - }, - { - "path": "apps/daemon/internal/localworkspace/binding.go", - "regex": "\"agents-api-(?:session)?\"", - "reason": "AgentStateKey is a persisted daemon/native-session resume identity carried on the existing machine wire contract; changing it would select another session." - }, - { - "path": "apps/daemon/internal/localworkspace/native_binding.go", - "regex": "\"agents-api-(?:session)?\"", - "reason": "AgentStateKey is a persisted daemon/native-session resume identity carried on the existing machine wire contract; changing it would select another session." - }, - { - "path": "apps/daemon/internal/localworkspace/binding_test.go", - "regex": "\"agents-api-(?:session)?\"", - "reason": "AgentStateKey is a persisted daemon/native-session resume identity carried on the existing machine wire contract; changing it would select another session." - }, - { - "path": "apps/daemon/internal/localworkspace/write.go", - "regex": "\"agents-api-(?:session)?\"", - "reason": "AgentStateKey is a persisted daemon/native-session resume identity carried on the existing machine wire contract; changing it would select another session." - }, - { - "path": "services/core/internal/execution/directory_preparation.go", - "regex": "\"agents-api-(?:session)?\"", - "reason": "AgentStateKey is a persisted daemon/native-session resume identity carried on the existing machine wire contract; changing it would select another session." - }, - { - "path": "services/core/internal/execution/request.go", - "regex": "\"agents-api-(?:session)?\"", - "reason": "AgentStateKey is a persisted daemon/native-session resume identity carried on the existing machine wire contract; changing it would select another session." - }, - { - "path": "services/core/internal/execution/recovery_test.go", - "regex": "\"agents-api-(?:session)?\"", - "reason": "AgentStateKey is a persisted daemon/native-session resume identity carried on the existing machine wire contract; changing it would select another session." - }, - { - "path": "services/core/tests/integration/dispatch_test.go", - "regex": "\"agents-api-(?:session)?\"", - "reason": "AgentStateKey is a persisted daemon/native-session resume identity carried on the existing machine wire contract; changing it would select another session." - }, - { - "path": "services/core/tests/integration/environment_directory_test.go", - "regex": "\"agents-api-(?:session)?\"", - "reason": "AgentStateKey is a persisted daemon/native-session resume identity carried on the existing machine wire contract; changing it would select another session." - }, - { - "path": "services/core/tests/integration/environment_worker_helpers_test.go", - "regex": "\"agents-api-(?:session)?\"", - "reason": "AgentStateKey is a persisted daemon/native-session resume identity carried on the existing machine wire contract; changing it would select another session." - }, - { - "path": "services/core/tests/integration/environment_worker_test.go", - "regex": "\"agents-api-(?:session)?\"", - "reason": "AgentStateKey is a persisted daemon/native-session resume identity carried on the existing machine wire contract; changing it would select another session." - }, - { - "path": "services/core/tests/integration/oac_runtime_names_migration_test.go", - "regex": "parsar-core-runtime@sha256:|parsar_worker /home/runtime/\\.parsar", - "reason": "Migration input and historical Session content assertions prove only the Runtime reference changes, never stored conversation data." - }, - { - "path": "services/core/migrations/000075_public_url_binding.sql", - "regex": "AGENTS_API_PUBLIC_URL", - "reason": "Historical migration commentary records the environment name used when this migration was written." - }, - { - "path": "CONTRIBUTING.md", - "regex": "apps/parsar/|Parsar owns", - "reason": "These exact phrases refer to the separate Parsar product, its ownership or historical source, not the OpenAgentCore brand." - }, - { - "path": "apps/web/PRODUCT.md", - "regex": "and Parsar itself", - "reason": "These exact phrases refer to the separate Parsar product, its ownership or historical source, not the OpenAgentCore brand." - }, - { - "path": "services/core/tests/official_environment_plugin_mcp.py", - "regex": "'PARSAR_'", - "reason": "The external-client credential-leak test rejects product-secret prefixes in observed model tool output; this is a leak detector, not a runtime setting." - }, - { - "path": "apps/web/.impeccable/surfaces/src-app-tsx.md", - "regex": "Parsar indigo", - "reason": "The design records explicitly attribute the copied indigo palette to its original product; it is not a rendered Core product label." - }, - { - "path": "example/*", - "regex": "(?i)parsar", - "reason": "The explicitly named Parsar application example retains product branding; it does not rename the Core runtime." - }, - { - "path": "pnpm-workspace.yaml", - "regex": "example/parsar", - "reason": "The explicitly named Parsar application example retains product branding; it does not rename the Core runtime." - }, - { - "path": "docs/examples.md", - "regex": "(?i)parsar", - "reason": "The examples guide describes the explicitly named Parsar application example; it does not rename the Core runtime." - }, - { - "path": "docs/zh/examples.md", - "regex": "(?i)parsar", - "reason": "The Chinese examples guide describes the explicitly named Parsar application example and its preserved source paths." - }, - { - "path": "Makefile", - "regex": "@oac/parsar-example", - "reason": "The explicitly named Parsar application example retains product branding; it does not rename the Core runtime." - }, - { - "path": "CONTRIBUTING.md", - "regex": "example/parsar/", - "reason": "The explicitly named Parsar application example retains product branding; it does not rename the Core runtime." - }, - { - "path": "apps/daemon/internal/cli/connect_cleanup_test.go", - "regex": "\"agents-api-cleanup\"", - "reason": "Executor ownership validates the current Core and local-workspace AgentStateKey namespace; this change does not introduce an alternate identity or compatibility path." - }, - { - "path": "apps/daemon/internal/dispatch/executor.go", - "regex": "\"agents-api-(?:session)?\"", - "reason": "Executor ownership validates the current Core and local-workspace AgentStateKey namespace; this change does not introduce an alternate identity or compatibility path." - }, - { - "path": "apps/daemon/internal/dispatch/executor_handoff_test.go", - "regex": "\"agents-api-(?:session)?\"", - "reason": "Executor ownership validates the current Core and local-workspace AgentStateKey namespace; this change does not introduce an alternate identity or compatibility path." - }, - { - "path": "apps/daemon/internal/dispatch/executor_test.go", - "regex": "\"agents-api-(?:session)?\"", - "reason": "Executor ownership validates the current Core and local-workspace AgentStateKey namespace; this change does not introduce an alternate identity or compatibility path." - }, - { - "path": "apps/daemon/internal/cli/claude_sdk_live_linux_test.go", - "regex": "\"agents-api-registered-acceptance\"", - "reason": "Executor ownership validates the current Core and local-workspace AgentStateKey namespace; this change does not introduce an alternate identity or compatibility path." - }, - { - "path": "services/core/internal/runtimegateway/mcp_bearer_live_linux_test.go", - "regex": "\"agents-api-mcp-bearer-acceptance\"", - "reason": "Executor ownership validates the current Core and local-workspace AgentStateKey namespace; this change does not introduce an alternate identity or compatibility path." - }, - { - "path": "apps/daemon/internal/localworkspace/capabilities.go", - "regex": "\"agents-api-\"", - "reason": "AgentStateKey retains the existing daemon/native-session resume identity; capability snapshots bind to the Session UUID carried in that identity." - }, - { - "path": "apps/daemon/internal/localworkspace/capability_preparation_test.go", - "regex": "\"agents-api-\"", - "reason": "AgentStateKey retains the existing daemon/native-session resume identity; capability snapshots bind to the Session UUID carried in that identity." - }, - { - "path": "internal/agentdaemon/proto/prototest/wire.go", - "regex": "agents-api-session", - "reason": "The shared wire scenarios use the existing Core Session state-key prefix required by execution admission." - }, - { - "path": "apps/daemon/internal/agent/mcode/tool_environment_test.go", - "regex": "agents-api-", - "reason": "The regression fixture uses the existing persisted Session state-key namespace required by Runtime binding validation." - }, - { - "path": "services/core/migrations/000015_retire_item_backfill.sql", - "regex": "services/agents-api/README\\.md", - "reason": "Landed migrations keep their original text." - }, - { - "path": "packages/agents-client/src/fixtures/parsar-*", - "regex": "services/agents-api/internal/api/", - "reason": "Historical Parsar protocol snapshots record the upstream source path they were captured from." - }, - { - "path": ".github/workflows/check.yml", - "regex": "(?:example/parsar|@oac/parsar-example)", - "reason": "The named Parsar example owns its dependency lock and component checks." - }, - { - "path": "scripts/ci_plan.py", - "regex": "(?:example/parsar|@oac/parsar-example)", - "reason": "The named Parsar example owns its dependency lock and component checks." - }, - { - "path": "scripts/ci_plan_test.py", - "regex": "(?:example/parsar|@oac/parsar-example)", - "reason": "The named Parsar example owns its dependency lock and component checks." - }, - { - "path": "apps/daemon/internal/cli/connect_suspend_test.go", - "regex": "\"agents-api-\"", - "reason": "The cleanup regression uses the persisted Session state-key prefix required by Runtime executor ownership validation." - } -] diff --git a/services/core/IMPLEMENTATION.md b/services/core/IMPLEMENTATION.md index b4672c756..b78fbc348 100644 --- a/services/core/IMPLEMENTATION.md +++ b/services/core/IMPLEMENTATION.md @@ -106,7 +106,7 @@ The sandbox node Hub's global mutex protects only in-memory connection state. Au Turn writes serialize on the tenant-scoped Session row. Input requests are ordered batches committed under the same lock: a retry key identifies the complete batch, a changed length, order or content conflicts, and a failed transaction leaves no partial input or cancellation. A single-event request keeps its identity at batch position zero. Internal admission limits are 64 events and 512 KiB of payload per request; the API still validates the upstream event schema. Cancellation keeps its first target, including an idle no-op, so a retry never stops later work. Terminal states and outcomes are never overwritten. -Session deletion takes its decision and commits the `deleted_at` marker under the tenant Session lock that orders Turn and input admission, so either an admission commits first and deletion conflicts, or admission observes the deletion. Admission checks visibility under that lock before its retry lookup. Internal Turn, receipt, finalization and restart reconciliation keep access to deleted Sessions so that work settles under the execution lease; queued work of a deleted Session is never claimed, and Runtime cleanup still cancels pending work. Deletion never revokes a shared device or removes a saved Agent. Deleted Session records are retained, and a migration rollback refuses to drop the column while any exist. +Session deletion takes its decision and commits the `deleted_at` marker under the tenant Session lock that orders Turn and input admission, so either an admission commits first and deletion conflicts, or admission observes the deletion. Admission checks visibility under that lock before its retry lookup. Internal Turn, receipt, finalization and restart reconciliation keep access to deleted Sessions so that work settles under the execution lease; queued work of a deleted Session is never claimed, and Runtime cleanup still cancels pending work. Deletion never revokes a shared device or removes a saved Agent. Deleted Session records are retained. Every new Session needs an explicit typed creator at the `sessions` boundary, internal callers included; public creation derives it from the authenticated principal only. Creator kind and ID persist in the creation transaction and never change on retry, update or source mutation. Both the early saved-reference recovery and the authoritative creation upsert require a matching creator before returning a Session or event cursor. Tenant scope always comes from the authenticated identity before any storage call; metadata grants nothing. Tests supply explicit synthetic creators. diff --git a/services/core/README.md b/services/core/README.md index ea7c23957..250095356 100644 --- a/services/core/README.md +++ b/services/core/README.md @@ -16,7 +16,7 @@ ## Database -Core uses its own PostgreSQL database and account and shares no tables with an application. Its migrations are embedded goose migrations in [`migrations/`](migrations), tracked in `agents_api_schema_version`; a migration is never edited after it lands. Core applies pending migrations when it starts and refuses a database whose schema version is newer than its own, which a [downgrade](../../docs/getting-started/operations.md#downgrades) would produce. Queries live in `internal/db/queries` and generate typed code with sqlc: run `make sqlc-generate` after changing a query and commit the generated files, which `make check-sqlc` compares. +Core uses its own PostgreSQL database and account and shares no tables with an application. Its schema is one embedded goose baseline migration in [`migrations/`](migrations), tracked in `oac_schema_version`. Before the first public release, schema changes edit the baseline in place, and an installation created from an earlier baseline is reinstalled. Core applies the baseline when it starts. Queries live in `internal/db/queries` and generate typed code with sqlc: run `make sqlc-generate` after changing a query and commit the generated files, which `make check-sqlc` compares. ## Run from source diff --git a/services/core/cmd/oac/init.go b/services/core/cmd/oac/init.go index e2772e2d4..17d2201d2 100644 --- a/services/core/cmd/oac/init.go +++ b/services/core/cmd/oac/init.go @@ -34,10 +34,8 @@ type releaseIdentity struct { } // releaseManifest holds the node manifest fields initialization reads. -// source_time, the commit time in Unix seconds, orders releases. type releaseManifest struct { SourceCommit string `json:"source_commit"` - SourceTime int64 `json:"source_time"` Platform string `json:"platform"` } @@ -221,6 +219,7 @@ func initialize(root string, release releaseIdentity, fetch func() (map[string][ logger.InfoContext(ctx, "Existing installation verified", "file_count", len(previous.Files)) return nil } + return fmt.Errorf("existing data belongs to release %.12s; reinstall to use release %.12s", previous.SourceCommit, release.revision) } nextStep("verify_bundled_metadata") files, err := fetch() @@ -228,22 +227,8 @@ func initialize(root string, release releaseIdentity, fetch func() (map[string][ return err } logger.InfoContext(ctx, "Bundled node installation metadata verified", "file_count", len(files)) - if previous.SourceCommit != "" { - nextStep("verify_release_order") - if err := requireNewer(root, previous.SourceCommit, release, files["manifest.json"]); err != nil { - return err - } - logger.InfoContext(ctx, "Upgrading installation", "from_revision", previous.SourceCommit) - } nextStep("publish_node_metadata") - // The receipt keeps every retained release authenticated; old releases stay - // for the nodes that still run them. names := []string{} - for name := range previous.Files { - if strings.HasPrefix(name, "node-payload/releases/") { - names = append(names, name) - } - } prefix := "node-payload/releases/" + release.revision + "/" for _, name := range releaseMembers { if err := writeOwned(filepath.Join(root, prefix+name), files[name]); err != nil { @@ -327,31 +312,6 @@ func writePointer(root string, release releaseIdentity) error { return writeOwned(filepath.Join(root, "node-payload", "active.json"), active) } -// requireNewer refuses a release that is not newer than the installed one by -// commit time. A manifest without source_time reads as 0, the oldest. -func requireNewer(root, installed string, release releaseIdentity, manifest []byte) error { - raw, err := os.ReadFile(filepath.Join(root, "node-payload", "releases", installed, "manifest.json")) - if errors.Is(err, fs.ErrNotExist) { - return fmt.Errorf("installation receipt names release %.12s, whose node metadata is missing; restore the matching data directory", installed) - } - if err != nil { - return err - } - var current, next releaseManifest - if err := json.Unmarshal(raw, ¤t); err != nil { - return err - } - if err := json.Unmarshal(manifest, &next); err != nil { - return err - } - if next.SourceTime > current.SourceTime { - return nil - } - date := func(seconds int64) string { return time.Unix(seconds, 0).UTC().Format("2006-01-02 15:04 UTC") } - return fmt.Errorf("This installation runs release %.12s (%s); this release, %.12s (%s), is not newer. Downgrades are not supported: run that release or a newer one, or restore the backup taken before the upgrade.", - installed, date(current.SourceTime), release.revision, date(next.SourceTime)) -} - func randomBytes(n int) []byte { value := make([]byte, n) if _, err := rand.Read(value); err != nil { diff --git a/services/core/cmd/oac/init_test.go b/services/core/cmd/oac/init_test.go index 5f0864978..2f1318ac3 100644 --- a/services/core/cmd/oac/init_test.go +++ b/services/core/cmd/oac/init_test.go @@ -25,22 +25,17 @@ func initFixture(t *testing.T) (string, releaseIdentity, map[string][]byte) { previous := chown chown = func(string, int, int) error { return nil } t.Cleanup(func() { chown = previous }) - release, files := releaseFixture(strings.Repeat("d", 40), 1760000000) + release, files := releaseFixture(strings.Repeat("d", 40)) return t.TempDir(), release, files } -// releaseFixture returns one release's metadata. A zero sourceTime omits -// source_time, as v0.0.10 and v0.0.11 manifests do. -func releaseFixture(revision string, sourceTime int64) (releaseIdentity, map[string][]byte) { +// releaseFixture returns one release's metadata. +func releaseFixture(revision string) (releaseIdentity, map[string][]byte) { files := map[string][]byte{} for _, name := range releaseMembers { files[name] = []byte("fixture " + revision) } - manifest := map[string]any{"source_commit": revision, "platform": "linux/amd64"} - if sourceTime != 0 { - manifest["source_time"] = sourceTime - } - files["manifest.json"], _ = json.Marshal(manifest) + files["manifest.json"], _ = json.Marshal(map[string]any{"source_commit": revision, "platform": "linux/amd64"}) return releaseIdentity{revision}, files } @@ -154,75 +149,37 @@ func readReceipt(t *testing.T, root string) installReceipt { return receipt } -func TestInitializeUpgradesAnOlderInstallationInPlace(t *testing.T) { +func TestInitializeHealsTheReleasePointerAfterACrash(t *testing.T) { root, release, files := initFixture(t) - old, oldFiles := releaseFixture(strings.Repeat("c", 40), 0) - if err := initialize(root, old, fixed(oldFiles)); err != nil { - t.Fatal(err) - } - // Receipts written by v0.0.10 and v0.0.11 also pin the release pointer. - receipt := readReceipt(t, root) - pointer := filepath.Join(root, "node-payload", "active.json") - receipt.Files["node-payload/active.json"], _ = fileDigest(pointer) - raw, _ := json.Marshal(receipt) - if err := os.WriteFile(filepath.Join(root, "installation.json"), raw, 0o600); err != nil { - t.Fatal(err) - } - before := snapshot(t, root) - if err := initialize(root, release, fixed(files)); err != nil { t.Fatal(err) } - after := snapshot(t, root) - for name, data := range before { - if (strings.HasPrefix(name, "secrets/") || strings.HasPrefix(name, "node-payload/releases/")) && after[name] != data { - t.Fatalf("upgrade changed %s", name) - } - } - receipt = readReceipt(t, root) - if receipt.SourceCommit != release.revision { - t.Fatalf("receipt release = %s", receipt.SourceCommit) - } - for _, revision := range []string{old.revision, release.revision} { - for _, name := range releaseMembers { - if receipt.Files["node-payload/releases/"+revision+"/"+name] == "" { - t.Fatalf("receipt lacks %s of %s", name, revision) - } - } - } - if _, pinned := receipt.Files["node-payload/active.json"]; pinned { - t.Fatal("receipt still pins the release pointer") - } - want := `{"source_commit":"` + release.revision + `"}` - if after["node-payload/active.json"] != want { - t.Fatalf("pointer = %s", after["node-payload/active.json"]) - } - - // A crash between the receipt and the pointer heals on the next start. - if err := os.WriteFile(pointer, []byte(before["node-payload/active.json"]), 0o600); err != nil { + pointer := filepath.Join(root, "node-payload", "active.json") + if err := os.WriteFile(pointer, []byte("{}"), 0o600); err != nil { t.Fatal(err) } if err := initialize(root, release, refuseDownload(t)); err != nil { t.Fatal(err) } + want := `{"source_commit":"` + release.revision + `"}` if healed, _ := os.ReadFile(pointer); string(healed) != want { t.Fatalf("pointer = %s", healed) } } -func TestInitializeRefusesAnOlderRelease(t *testing.T) { +func TestInitializeRefusesDataOfAnotherRelease(t *testing.T) { root, release, files := initFixture(t) if err := initialize(root, release, fixed(files)); err != nil { t.Fatal(err) } before := snapshot(t, root) - older, olderFiles := releaseFixture(strings.Repeat("e", 40), 1700000000) - want := "This installation runs release dddddddddddd (2025-10-09 08:53 UTC); this release, eeeeeeeeeeee (2023-11-14 22:13 UTC), is not newer. Downgrades are not supported: run that release or a newer one, or restore the backup taken before the upgrade." - if err := initialize(root, older, fixed(olderFiles)); err == nil || err.Error() != want { + other, otherFiles := releaseFixture(strings.Repeat("e", 40)) + want := "existing data belongs to release dddddddddddd; reinstall to use release eeeeeeeeeeee" + if err := initialize(root, other, fixed(otherFiles)); err == nil || err.Error() != want { t.Fatalf("err = %v", err) } if !maps.Equal(before, snapshot(t, root)) { - t.Fatal("a refused downgrade changed installation files") + t.Fatal("a refused release changed installation files") } } diff --git a/services/core/cmd/oac/install.go b/services/core/cmd/oac/install.go index 13c5a7e20..4e26d813d 100644 --- a/services/core/cmd/oac/install.go +++ b/services/core/cmd/oac/install.go @@ -30,7 +30,6 @@ import ( type installOptions struct { dir, version, publicURL, host string port int - upgrade bool // given names the settings flags the caller passed, which an existing installation ignores. given []string } @@ -52,7 +51,6 @@ func installCommand(ctx context.Context, args []string) error { flags.StringVar(&o.publicURL, "public-url", "", "URL reachable by browsers and nodes") flags.StringVar(&o.host, "host", "0.0.0.0", "Web bind address") flags.IntVar(&o.port, "web-port", 8080, "Web port") - flags.BoolVar(&o.upgrade, "upgrade", false, "upgrade an existing installation to this release") // The flag package's own report would repeat the error main prints. flags.SetOutput(io.Discard) if err := flags.Parse(args); err != nil { @@ -252,10 +250,6 @@ func (i installer) installLocked(ctx context.Context, o installOptions, detected return err } fresh := len(entries) == 0 - if fresh && o.upgrade { - return fmt.Errorf("No installation in %s; pass the installation's --install-dir.", o.dir) - } - installed := buildRevision if !fresh { for _, name := range []string{"compose.yaml", ".env", cliName()} { info, err := os.Lstat(filepath.Join(o.dir, name)) @@ -267,24 +261,21 @@ func (i installer) installLocked(ctx context.Context, o installOptions, detected if err != nil { return err } - if installed = composeRevision(raw); installed == "" { + installed := composeRevision(raw) + if installed == "" { return fmt.Errorf("incomplete installation: preserve %s and choose another directory", o.dir) } - } - upgrade := installed != buildRevision - if upgrade && !o.upgrade { - return fmt.Errorf("This installation runs release %s; this installer is %s. Back up the installation (Upgrade in the operations guide), then rerun with --upgrade.", short(installed), short(buildRevision)) + if installed != buildRevision { + return fmt.Errorf("This installation runs release %s; this installer is %s. Reinstall to use this release.", short(installed), short(buildRevision)) + } } compose := installCompose{docker: i.docker, dir: o.dir} - cleanedOverride := false - if fresh || upgrade { - if fresh { - listener, err := net.Listen("tcp", net.JoinHostPort(o.host, strconv.Itoa(o.port))) - if err != nil { - return fmt.Errorf("Web port unavailable; choose --web-port: %w%s", err, i.portHolderHint(ctx, o.port)) - } - listener.Close() + if fresh { + listener, err := net.Listen("tcp", net.JoinHostPort(o.host, strconv.Itoa(o.port))) + if err != nil { + return fmt.Errorf("Web port unavailable; choose --web-port: %w%s", err, i.portHolderHint(ctx, o.port)) } + listener.Close() if err := os.Mkdir(stage, 0o700); err != nil { return err } @@ -300,29 +291,11 @@ func (i installer) installLocked(ctx context.Context, o installOptions, detected if err := os.WriteFile(filepath.Join(stage, "compose.yaml"), release, 0o600); err != nil { return err } - if fresh { - contents := fmt.Sprintf("COMPOSE_PROJECT_NAME=oac-%s\nOAC_HOST='%s'\nOAC_WEB_PORT='%d'\nOAC_PUBLIC_URL='%s'\n", randomHex(5), o.host, o.port, o.publicURL) - if err := os.WriteFile(filepath.Join(stage, ".env"), []byte(contents), 0o600); err != nil { - return err - } - compose.dir = stage - } else { - // The staged release runs with the installation's settings, project name and override. - compose.files = []string{"-f", filepath.Join(stage, "compose.yaml")} - override := filepath.Join(o.dir, "compose.override.yaml") - if raw, err := os.ReadFile(override); err == nil { - if cleaned, changed := dropPayloadBinds(raw); changed { - // The release serves the node payload from the data volume and its native installers from the Core image, so the offline installer's host binds go. - if err := os.WriteFile(filepath.Join(stage, "compose.override.yaml"), cleaned, 0o600); err != nil { - return err - } - override = filepath.Join(stage, "compose.override.yaml") - cleanedOverride = true - } - compose.files = append(compose.files, "-f", override) - } - compose.files = append(compose.files, "--project-directory", o.dir) + contents := fmt.Sprintf("COMPOSE_PROJECT_NAME=oac-%s\nOAC_HOST='%s'\nOAC_WEB_PORT='%d'\nOAC_PUBLIC_URL='%s'\n", randomHex(5), o.host, o.port, o.publicURL) + if err := os.WriteFile(filepath.Join(stage, ".env"), []byte(contents), 0o600); err != nil { + return err } + compose.dir = stage if err := copyExecutable(i.executable, filepath.Join(stage, cliName())); err != nil { return err } @@ -346,33 +319,6 @@ func (i installer) installLocked(ctx context.Context, o installOptions, detected return fmt.Errorf("Docker could not pull %s: %w\nImage pulls go through the Docker daemon, which does not use this shell's proxy settings: if the registry is unreachable, configure the daemon's proxy or a registry mirror, or install from the offline archive (Offline hosts in the installation options).", image, err) } } - if upgrade { - // Init upgrades the data volume or refuses; until then the running release is untouched. - fmt.Println("Checking the upgrade...") - for _, check := range [][]string{checkConfig, initVolume} { - if err := compose.Run(ctx, check...); err != nil { - return fmt.Errorf("Upgrade stopped; the running services are unchanged: %w", err) - } - } - names := []string{cliName(), "compose.yaml"} - if cleanedOverride { - names = append(names, "compose.override.yaml") - } - for _, name := range names { - if err := os.Rename(filepath.Join(stage, name), filepath.Join(o.dir, name)); err != nil { - return err - } - } - if cleanedOverride { - // Nothing serves the offline installer's host copies any more. - _ = os.RemoveAll(filepath.Join(o.dir, "node-payload")) - _ = os.RemoveAll(filepath.Join(o.dir, "native-installers")) - } - } - if !fresh { - // Installations from earlier releases kept a checksum that no longer describes compose.yaml. - _ = os.Remove(filepath.Join(o.dir, "compose-sha256sums.txt")) - } if fresh { if _, err := os.Stat(o.dir); err == nil { if err := os.Remove(o.dir); err != nil { @@ -410,43 +356,12 @@ func (i installer) installLocked(ctx context.Context, o installOptions, detected fmt.Printf("Core key: %s core-key --show\n", command) } fmt.Printf("Command: %s\n", command) - if upgrade { - fmt.Printf("Upgraded from %s to %s. Settings → Sandbox and scheduling offers this release's Runtime; sandboxes keep their current Runtime until you apply it.\n", short(installed), short(buildRevision)) - } if fresh && detected { fmt.Printf("Browsers must open exactly %s. If they reach this host at another address, set OAC_PUBLIC_URL in .env and run %s apply.\n", origin, command) } return nil } -// dropPayloadBinds removes the offline installer's binds of Web's /node-payload and Core's /opt/oac/native-installers from a JSON override and keeps every other entry. Other formats are the operator's and stay as they are. -func dropPayloadBinds(raw []byte) ([]byte, bool) { - var override map[string]any - if json.Unmarshal(raw, &override) != nil { - return nil, false - } - services, _ := override["services"].(map[string]any) - changed := false - for name, target := range map[string]string{"web": "/node-payload", "core": "/opt/oac/native-installers"} { - service, _ := services[name].(map[string]any) - volumes, _ := service["volumes"].([]any) - kept := make([]any, 0, len(volumes)) - for _, volume := range volumes { - if entry, ok := volume.(map[string]any); !ok || entry["target"] != target { - kept = append(kept, volume) - } - } - if len(kept) != len(volumes) { - service["volumes"], changed = kept, true - } - } - if !changed { - return nil, false - } - cleaned, err := json.MarshalIndent(override, "", " ") - return append(cleaned, '\n'), err == nil -} - // portHolderHint names an OpenAgentCore Compose project that publishes the port. A project whose directory is gone keeps running; one whose directory exists is a live installation, so the hint names it instead of offering to delete it. func (i installer) portHolderHint(ctx context.Context, port int) string { rows, err := i.docker(ctx, "", "ps", "--filter", "publish="+strconv.Itoa(port), "--format", `{{.Label "com.docker.compose.project"}} {{.Label "com.docker.compose.project.working_dir"}}`) @@ -462,7 +377,7 @@ func (i installer) portHolderHint(ctx context.Context, port int) string { return fmt.Sprintf("\nThe Compose project %s holds it.", name) } if _, err := os.Stat(dir); err == nil { - return fmt.Sprintf("\nThe installation in %s holds it; upgrade or manage it with --install-dir %s.", dir, dir) + return fmt.Sprintf("\nThe installation in %s holds it; manage it with --install-dir %s.", dir, dir) } return fmt.Sprintf("\nThe Compose project %s holds it, and its installation directory %s no longer exists. Remove it with: docker compose -p %s down --volumes --remove-orphans (this deletes its data)", name, dir, name) } @@ -571,12 +486,11 @@ func copyExecutable(from, to string) error { type installCompose struct { docker func(context.Context, string, ...string) ([]byte, error) dir string - files []string } // Output reports a failure as the last line of Compose's output, which holds the cause; the progress lines before it only repeat what happened. func (c installCompose) Output(ctx context.Context, args ...string) ([]byte, error) { - output, err := c.docker(ctx, c.dir, append(append([]string{"compose"}, c.files...), args...)...) + output, err := c.docker(ctx, c.dir, append([]string{"compose"}, args...)...) if err != nil { if lines := strings.Split(strings.TrimSpace(err.Error()), "\n"); len(lines) > 1 { err = errors.New(strings.TrimSpace(lines[len(lines)-1])) diff --git a/services/core/cmd/oac/install_test.go b/services/core/cmd/oac/install_test.go index fae32ccd5..b76106c30 100644 --- a/services/core/cmd/oac/install_test.go +++ b/services/core/cmd/oac/install_test.go @@ -3,7 +3,6 @@ package main import ( "context" "crypto/sha256" - "encoding/json" "errors" "fmt" "io" @@ -212,7 +211,6 @@ func TestDownloadRetriesTransientFailures(t *testing.T) { } func TestInstallResumePreservesConfigAndUsesCachedImages(t *testing.T) { - // The same release resumes with or without --upgrade. f := newInstallFixture(t) f.failure = "compose up" if err := f.run(); err == nil { @@ -230,7 +228,6 @@ func TestInstallResumePreservesConfigAndUsesCachedImages(t *testing.T) { return nil, nil } f.options.publicURL = "https://replacement.invalid" - f.options.upgrade = true if err := f.run(); err != nil { t.Fatal(err) } @@ -247,94 +244,24 @@ func TestInstallResumePreservesConfigAndUsesCachedImages(t *testing.T) { assertCallsInOrder(t, f.calls, "compose "+strings.Join(initVolume, " "), "compose "+strings.Join(checkConfig, " "), "compose "+strings.Join(startServices, " ")) } -func TestInstallUpgradesAnotherReleaseOnlyWhenRequested(t *testing.T) { +func TestInstallRefusesAnotherRelease(t *testing.T) { f := newInstallFixture(t) setBuildRevision(t, "a1b2c3d4e5f6a7b8c9d0a1b2c3d4e5f6a7b8c9d0") if err := f.run(); err != nil { t.Fatal(err) } - os.WriteFile(filepath.Join(f.options.dir, "compose-sha256sums.txt"), []byte("previous release"), 0o600) before := installationFiles(t, f.options.dir) setBuildRevision(t, "development") - os.WriteFile(f.executable, []byte("upgraded binary"), 0o700) - f.calls = nil err := f.run() - if err == nil || err.Error() != "This installation runs release a1b2c3d4e5f6; this installer is development. Back up the installation (Upgrade in the operations guide), then rerun with --upgrade." { + if err == nil || err.Error() != "This installation runs release a1b2c3d4e5f6; this installer is development. Reinstall to use this release." { t.Fatal(err) } if len(f.calls) != 3 { t.Fatalf("refusal ran Docker beyond its checks: %v", f.calls) } - - f.options.upgrade = true - f.failure = "staged compose run --rm --no-deps init" - if err := f.run(); err == nil || !strings.HasPrefix(err.Error(), "Upgrade stopped; the running services are unchanged") { - t.Fatal(err) - } if after := installationFiles(t, f.options.dir); !maps.Equal(before, after) { - t.Fatal("refused upgrade changed the installation directory") - } - if _, err := os.Stat(f.options.dir + ".staging"); !os.IsNotExist(err) { - t.Fatal("refused upgrade left its stage") - } - - f.failure = "" - f.calls = nil - if err := f.run(); err != nil { - t.Fatal(err) - } - assertCallsInOrder(t, f.calls, - "pull example/core:latest", - "staged compose "+strings.Join(checkConfig, " "), - "staged compose run --rm --no-deps init", - "compose up -d --wait --wait-timeout 180 --pull never --remove-orphans") - after := installationFiles(t, f.options.dir) - if after[cliName()] != "upgraded binary" || after[".env"] != before[".env"] || f.installedRevision() != "development" { - t.Fatal("upgrade did not publish the release with the saved settings") - } - if _, ok := after["compose-sha256sums.txt"]; ok { - t.Fatal("upgrade kept the previous release checksum") - } -} - -func TestUpgradeDropsTheOfflinePayloadBinds(t *testing.T) { - f := newInstallFixture(t) - setBuildRevision(t, "a1b2c3d4e5f6a7b8c9d0a1b2c3d4e5f6a7b8c9d0") - if err := f.run(); err != nil { - t.Fatal(err) - } - payload, native := filepath.Join(f.options.dir, "node-payload"), filepath.Join(f.options.dir, "native-installers") - os.Mkdir(payload, 0o755) - os.Mkdir(native, 0o755) - source, _ := json.Marshal(payload) - override := `{"services":{"web":{"volumes":[{"type":"bind","source":` + string(source) + `,"target":"/node-payload","read_only":true},{"type":"bind","source":"/srv/ca","target":"/ca"}]},"core":{"volumes":[{"type":"bind","source":"/srv/native","target":"/opt/oac/native-installers","read_only":true}]}}}` - path := filepath.Join(f.options.dir, "compose.override.yaml") - os.WriteFile(path, []byte(override), 0o600) - setBuildRevision(t, "development") - f.options.upgrade = true - f.failure = "staged compose run --rm --no-deps init" - if err := f.run(); err == nil { - t.Fatal("upgrade should have stopped") - } - if raw, _ := os.ReadFile(path); string(raw) != override { - t.Fatal("a stopped upgrade changed the override") - } - if _, err := os.Stat(payload); err != nil { - t.Fatal("a stopped upgrade removed the node payload") - } - f.failure = "" - if err := f.run(); err != nil { - t.Fatal(err) - } - raw, _ := os.ReadFile(path) - if strings.Contains(string(raw), "/node-payload") || strings.Contains(string(raw), "/opt/oac/native-installers") || !strings.Contains(string(raw), "/srv/ca") { - t.Fatalf("override after upgrade: %s", raw) - } - for _, directory := range []string{payload, native} { - if _, err := os.Stat(directory); !os.IsNotExist(err) { - t.Fatalf("upgrade kept %s", directory) - } + t.Fatal("refusal changed the installation directory") } } @@ -483,7 +410,6 @@ func captureOutput(t *testing.T, fn func()) string { func TestFreshInstallStartsLikeApplyAndPrintsTheKeyOnce(t *testing.T) { f := newInstallFixture(t) - f.options.upgrade = false var err error out := captureOutput(t, func() { err = f.run() }) if err != nil { diff --git a/services/core/cmd/oac/main.go b/services/core/cmd/oac/main.go index caa970331..fb8975bce 100644 --- a/services/core/cmd/oac/main.go +++ b/services/core/cmd/oac/main.go @@ -184,7 +184,7 @@ func accessError(root string, err error) error { var ( initVolume = []string{"run", "--rm", "--no-deps", "init"} checkConfig = []string{"run", "--rm", "--no-deps", "--entrypoint", "/usr/local/bin/oac-core", "core", "check-config"} - // Starting recreates changed services, so a resumed upgrade leaves no container on the previous release. + // Starting recreates changed services. startServices = []string{"up", "-d", "--wait", "--wait-timeout", "180", "--pull", "never", "--remove-orphans"} ) diff --git a/services/core/cmd/oac/uninstall_test.go b/services/core/cmd/oac/uninstall_test.go index f6c7fbc8c..e6b50d5de 100644 --- a/services/core/cmd/oac/uninstall_test.go +++ b/services/core/cmd/oac/uninstall_test.go @@ -171,14 +171,3 @@ func TestInstallPortHintOffersDeletionOnlyForAMissingDirectory(t *testing.T) { } } } - -func TestUpgradeWithoutAnInstallationRefuses(t *testing.T) { - f := newInstallFixture(t) - f.options.upgrade = true - if err := f.run(); err == nil || !strings.Contains(err.Error(), "No installation in "+f.options.dir) { - t.Fatal(err) - } - if exists(f.options.dir) || exists(f.options.dir+".staging") { - t.Fatal("a refused upgrade left files") - } -} diff --git a/services/core/cmd/server/main.go b/services/core/cmd/server/main.go index 2c4c45ace..799a2a834 100644 --- a/services/core/cmd/server/main.go +++ b/services/core/cmd/server/main.go @@ -137,17 +137,17 @@ func run(config processconfig.Config) error { err := migrations.Apply(migrating, config.DatabaseURL) cancelMigration() if err != nil { - return fmt.Errorf("Agents API database migration failed: %w", err) + return fmt.Errorf("OpenAgentCore database migration failed: %w", err) } pool, err := pgxpool.New(ctx, config.DatabaseURL) if err != nil { - return errors.New("invalid Agents API database configuration") + return errors.New("invalid OpenAgentCore database configuration") } defer pool.Close() ready, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() if err := pool.Ping(ready); err != nil { - return errors.New("Agents API database connection failed") + return errors.New("OpenAgentCore database connection failed") } oauthClient, err := oauthrefresh.NewClient(config.OAuthTrustedOrigins) if err != nil { diff --git a/services/core/internal/credentialcrypto/agent_model_execution.go b/services/core/internal/credentialcrypto/agent_model_execution.go index 05ab2ac7f..2f8e2fad7 100644 --- a/services/core/internal/credentialcrypto/agent_model_execution.go +++ b/services/core/internal/credentialcrypto/agent_model_execution.go @@ -9,7 +9,7 @@ func agentModelExecutionData(tenantID, agentID string) ([]byte, error) { if tenantID == "" || agentID == "" || !utf8.ValidString(tenantID) || !utf8.ValidString(agentID) { return nil, errInvalidBinding } - return json.Marshal([]string{"parsar.agents-api.agent-model-execution.v1", tenantID, agentID}) + return json.Marshal([]string{"oac.agent-model-execution.v1", tenantID, agentID}) } func (c *Cipher) SealAgentModelExecution(plaintext []byte, tenantID, agentID string) ([]byte, error) { diff --git a/services/core/internal/credentialcrypto/cipher.go b/services/core/internal/credentialcrypto/cipher.go index 8a9136545..e1cbc40ce 100644 --- a/services/core/internal/credentialcrypto/cipher.go +++ b/services/core/internal/credentialcrypto/cipher.go @@ -15,7 +15,7 @@ import ( const ( formatVersion byte = 1 - bindingDomain = "parsar.agents-api.credential" + bindingDomain = "oac.credential" ) var ( @@ -55,7 +55,7 @@ func New(key []byte) (*Cipher, error) { return nil, errUnavailable } derive := hmac.New(sha256.New, key) - derive.Write([]byte("parsar.agents-api.credential-fingerprint.v1")) + derive.Write([]byte("oac.credential-fingerprint.v1")) return &Cipher{aead: aead, fingerprint: derive.Sum(nil)}, nil } diff --git a/services/core/internal/credentialcrypto/deployment_model_provider.go b/services/core/internal/credentialcrypto/deployment_model_provider.go index 07db34fc9..8c5dd8e49 100644 --- a/services/core/internal/credentialcrypto/deployment_model_provider.go +++ b/services/core/internal/credentialcrypto/deployment_model_provider.go @@ -12,7 +12,7 @@ func deploymentModelProviderData(harness string) ([]byte, error) { if harness == "" || !utf8.ValidString(harness) { return nil, errInvalidBinding } - return json.Marshal([]string{"parsar.agents-api.deployment-model-provider.v1", harness}) + return json.Marshal([]string{"oac.deployment-model-provider.v1", harness}) } func (c *Cipher) SealDeploymentModelProvider(plaintext []byte, harness string) ([]byte, error) { diff --git a/services/core/internal/credentialcrypto/environment_file.go b/services/core/internal/credentialcrypto/environment_file.go index cb079b959..779ed5e87 100644 --- a/services/core/internal/credentialcrypto/environment_file.go +++ b/services/core/internal/credentialcrypto/environment_file.go @@ -42,5 +42,5 @@ func environmentFileData(binding EnvironmentFileBinding) ([]byte, error) { Domain string `json:"domain"` Version byte `json:"version"` Binding EnvironmentFileBinding `json:"binding"` - }{"parsar.agents-api.environment-file", formatVersion, binding}) + }{"oac.environment-file", formatVersion, binding}) } diff --git a/services/core/internal/credentialcrypto/environment_setup.go b/services/core/internal/credentialcrypto/environment_setup.go index 8955d3394..7a452bc4b 100644 --- a/services/core/internal/credentialcrypto/environment_setup.go +++ b/services/core/internal/credentialcrypto/environment_setup.go @@ -42,5 +42,5 @@ func environmentSetupData(binding EnvironmentSetupBinding) ([]byte, error) { Domain string `json:"domain"` Version byte `json:"version"` Binding EnvironmentSetupBinding `json:"binding"` - }{"parsar.agents-api.environment-setup", formatVersion, binding}) + }{"oac.environment-setup", formatVersion, binding}) } diff --git a/services/core/internal/credentialcrypto/model_execution.go b/services/core/internal/credentialcrypto/model_execution.go index 61dc204c3..3a681d3a4 100644 --- a/services/core/internal/credentialcrypto/model_execution.go +++ b/services/core/internal/credentialcrypto/model_execution.go @@ -9,7 +9,7 @@ func modelExecutionData(tenantID, sessionID string) ([]byte, error) { if tenantID == "" || sessionID == "" || !utf8.ValidString(tenantID) || !utf8.ValidString(sessionID) { return nil, errInvalidBinding } - return json.Marshal([]string{"parsar.agents-api.session-model-execution.v1", tenantID, sessionID}) + return json.Marshal([]string{"oac.session-model-execution.v1", tenantID, sessionID}) } func (c *Cipher) SealModelExecution(plaintext []byte, tenantID, sessionID string) ([]byte, error) { diff --git a/services/core/internal/credentialcrypto/sandbox_deployment.go b/services/core/internal/credentialcrypto/sandbox_deployment.go index 0346f14ff..4066aed81 100644 --- a/services/core/internal/credentialcrypto/sandbox_deployment.go +++ b/services/core/internal/credentialcrypto/sandbox_deployment.go @@ -10,7 +10,7 @@ func sandboxDeploymentData(installation string, generation uint64) ([]byte, erro if installation == "" || !utf8.ValidString(installation) || generation == 0 { return nil, errInvalidBinding } - return json.Marshal([]string{"parsar.agents-api.sandbox-deployment.v1", installation, strconv.FormatUint(generation, 10)}) + return json.Marshal([]string{"oac.sandbox-deployment.v1", installation, strconv.FormatUint(generation, 10)}) } func (c *Cipher) SealSandboxDeployment(plaintext []byte, installation string, generation uint64) ([]byte, error) { diff --git a/services/core/internal/credentialcrypto/skill.go b/services/core/internal/credentialcrypto/skill.go index e827e24c9..29d84830f 100644 --- a/services/core/internal/credentialcrypto/skill.go +++ b/services/core/internal/credentialcrypto/skill.go @@ -48,5 +48,5 @@ func skillData(binding SkillBinding) ([]byte, error) { Domain string `json:"domain"` Version byte `json:"version"` Binding SkillBinding `json:"binding"` - }{"parsar.agents-api.skill", formatVersion, binding}) + }{"oac.skill", formatVersion, binding}) } diff --git a/services/core/internal/databaseurl/databaseurl_test.go b/services/core/internal/databaseurl/databaseurl_test.go index 716067da9..942cef8b6 100644 --- a/services/core/internal/databaseurl/databaseurl_test.go +++ b/services/core/internal/databaseurl/databaseurl_test.go @@ -13,22 +13,22 @@ func TestPasswordComesOnlyFromTheFile(t *testing.T) { t.Fatal(err) } t.Setenv("OAC_DATABASE_PASSWORD_FILE", file) - t.Setenv("OAC_DATABASE_URL", "postgres://agents_api@database:5432/agents_api?sslmode=disable") - if got, err := FromEnvironment(); err != nil || got != "postgres://agents_api:s3cret%2Fvalue@database:5432/agents_api?sslmode=disable" { + t.Setenv("OAC_DATABASE_URL", "postgres://oac@database:5432/oac?sslmode=disable") + if got, err := FromEnvironment(); err != nil || got != "postgres://oac:s3cret%2Fvalue@database:5432/oac?sslmode=disable" { t.Fatal(got, err) } - t.Setenv("OAC_DATABASE_URL", "postgres://agents_api:inline@database:5432/agents_api") + t.Setenv("OAC_DATABASE_URL", "postgres://oac:inline@database:5432/oac") if _, err := FromEnvironment(); err == nil || strings.Contains(err.Error(), "inline") { t.Fatal("accepted a password in two places", err) } t.Setenv("OAC_DATABASE_PASSWORD_FILE", filepath.Join(t.TempDir(), "missing")) - t.Setenv("OAC_DATABASE_URL", "postgres://agents_api@database:5432/agents_api") + t.Setenv("OAC_DATABASE_URL", "postgres://oac@database:5432/oac") if _, err := FromEnvironment(); err == nil { t.Fatal("accepted a missing password file") } t.Setenv("OAC_DATABASE_PASSWORD_FILE", "") - t.Setenv("OAC_DATABASE_URL", "postgres://agents_api:inline@database:5432/agents_api") - if got, err := FromEnvironment(); err != nil || got != "postgres://agents_api:inline@database:5432/agents_api" { + t.Setenv("OAC_DATABASE_URL", "postgres://oac:inline@database:5432/oac") + if got, err := FromEnvironment(); err != nil || got != "postgres://oac:inline@database:5432/oac" { t.Fatal("standalone URL changed", got, err) } } diff --git a/services/core/internal/persistence/postgres/deploymentpg/nodes_test.go b/services/core/internal/persistence/postgres/deploymentpg/nodes_test.go index a3c75e268..5f58df30f 100644 --- a/services/core/internal/persistence/postgres/deploymentpg/nodes_test.go +++ b/services/core/internal/persistence/postgres/deploymentpg/nodes_test.go @@ -2,17 +2,13 @@ package deploymentpg_test import ( "crypto/sha256" - "database/sql" "encoding/hex" "errors" - "os" "strings" "testing" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" - "github.com/jackc/pgx/v5/stdlib" - "github.com/pressly/goose/v3" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" @@ -355,109 +351,3 @@ func TestNodeUpdateBounds(t *testing.T) { t.Fatal("unknown node removed", err) } } - -func TestNodeGenerationDowngradePreservesServingProtocol(t *testing.T) { - for _, mode := range []string{"v2", "old_v1", "current_v1"} { - t.Run(mode, func(t *testing.T) { - f := newFixture(t) - changes, _ := f.execution(t) - input := sandbox.Selection{Provider: "docker", DeploymentSpec: testSpecification("docker")} - installation, first := f.initialize(t, changes, input) - node := f.enroll(t, first, deployment.Capacity{MaxActive: 4, MaxRetained: 16}) - f.connect(t, node.NodeID) - switch mode { - case "v2": - connection := f.connect(t, node.NodeID) - if err := f.service.HeartbeatGenerations(t.Context(), node.NodeID, connection, first.OwnerEpoch, deployment.NodeHealth{}, []sandbox.GenerationStatus{{Generation: first.Generation, SpecificationDigest: first.SpecificationDigest, State: "ready"}}); err != nil { - t.Fatal(err) - } - case "old_v1": - input.Resources.CPUs++ - input.ExpectedGeneration = first.Generation - next, err := changes.Update(admin(t), installation, input) - if err != nil { - t.Fatal(err) - } - if next.Generation != first.Generation+1 || next.OwnerEpoch != first.OwnerEpoch { - t.Fatal("target change replaced execution ownership", next) - } - } - db := sql.OpenDB(stdlib.GetConnector(*f.pool.Config().ConnConfig)) - defer db.Close() - migration, err := goose.NewProvider(goose.DialectPostgres, db, os.DirFS("../../../../migrations"), goose.WithTableName("agents_api_schema_version")) - if err != nil { - t.Fatal(err) - } - _, err = migration.DownTo(t.Context(), 81) - if mode == "current_v1" { - if err != nil { - t.Fatal("safe v1 downgrade refused", err) - } - } else { - if err == nil { - t.Fatal("downgrade discarded required node protocol") - } - // DownTo may have removed later, reversible migrations before the - // node protocol migration refused the downgrade. Restore the current - // schema before using this version of the adapter to verify recovery. - if _, err = migration.Up(t.Context()); err != nil { - t.Fatal("refused downgrade could not restore current schema", err) - } - if _, err = f.service.NodeConfiguration(t.Context(), node.NodeID, node.Credential, 1); err != nil { - t.Fatal("refused downgrade damaged retained recovery", err) - } - if err = f.service.RemoveNode(t.Context(), node.NodeID); err != nil { - t.Fatal(err) - } - if _, err = migration.DownTo(t.Context(), 81); err != nil { - t.Fatal("removed node blocked downgrade", err) - } - } - if _, err = migration.Up(t.Context()); err != nil { - t.Fatal("node schema could not upgrade again", err) - } - }) - } -} - -// Stored vendor codes from before the readiness classes, including an offline -// node's last report, are rewritten to their class. -func TestNodeReadinessClassMigrationRewritesStoredCodes(t *testing.T) { - f := newFixture(t) - changes, _ := f.execution(t) - _, view := f.initialize(t, changes, sandbox.Selection{Provider: "docker", DeploymentSpec: testSpecification("docker")}) - node := f.enroll(t, view, deployment.Capacity{MaxActive: 1, MaxRetained: 1}) - connection := f.connect(t, node.NodeID) - failed := []sandbox.GenerationStatus{{Generation: view.Generation, SpecificationDigest: view.SpecificationDigest, State: "failed", Diagnostic: "provider_unavailable"}} - if err := f.service.HeartbeatGenerations(t.Context(), node.NodeID, connection, view.OwnerEpoch, deployment.NodeHealth{Diagnostic: "provider_unavailable"}, failed); err != nil { - t.Fatal(err) - } - db := sql.OpenDB(stdlib.GetConnector(*f.pool.Config().ConnConfig)) - defer db.Close() - migration, err := goose.NewProvider(goose.DialectPostgres, db, os.DirFS("../../../../migrations"), goose.WithTableName("agents_api_schema_version")) - if err != nil { - t.Fatal(err) - } - var version int64 - for _, source := range migration.ListSources() { - if strings.HasSuffix(source.Path, "_node_readiness_classes.sql") { - version = source.Version - } - } - if _, err := migration.DownTo(t.Context(), version-1); err != nil { - t.Fatal(err) - } - if _, err := f.pool.Exec(t.Context(), `UPDATE runtime_nodes SET health=jsonb_set(health,'{diagnostic}','"kvm_unavailable"') WHERE id=$1`, node.NodeID); err != nil { - t.Fatal(err) - } - if _, err := f.pool.Exec(t.Context(), "UPDATE runtime_node_generation_status SET diagnostic='microsandbox_artifacts_unavailable' WHERE node_id=$1", node.NodeID); err != nil { - t.Fatal(err) - } - if _, err := migration.Up(t.Context()); err != nil { - t.Fatal(err) - } - detail, err := f.service.NodeDetail(t.Context(), node.NodeID, "1h") - if err != nil || detail.Diagnostic != "host_unsupported" || detail.Rollout.State != "failed" || detail.Rollout.Diagnostic != "artifacts_unavailable" { - t.Fatal(detail.Diagnostic, detail.Rollout, err) - } -} diff --git a/services/core/internal/persistence/postgres/modelconfigurationpg/observation_test.go b/services/core/internal/persistence/postgres/modelconfigurationpg/observation_test.go index 2c59b8c1b..80c29ba11 100644 --- a/services/core/internal/persistence/postgres/modelconfigurationpg/observation_test.go +++ b/services/core/internal/persistence/postgres/modelconfigurationpg/observation_test.go @@ -354,39 +354,3 @@ func TestReplacementWaitsForAWinningObservationAndClearsIt(t *testing.T) { } o.observe(t, turn, 0) } - -func TestObservationMigrationRoundTrip(t *testing.T) { - o := newObserved(t) - old := o.resolve(t).Revision - var secret, sessionBundle []byte - if err := o.pool.QueryRow(t.Context(), "SELECT (SELECT encrypted_config FROM deployment_model_providers), (SELECT encrypted_config FROM session_model_execution WHERE session_id=$1)", o.session.ID).Scan(&secret, &sessionBundle); err != nil { - t.Fatal(err) - } - raw, err := os.ReadFile("../../../../migrations/000084_deployment_provider_observations.sql") - if err != nil { - t.Fatal(err) - } - parts := strings.Split(string(raw), "-- +goose Down") - if _, err = o.pool.Exec(t.Context(), parts[1]); err != nil { - t.Fatal(err) - } - if _, err = o.pool.Exec(t.Context(), parts[0]); err != nil { - t.Fatal(err) - } - var revision uuid.UUID - var frozen pgtype.UUID - var preserved, preservedSession []byte - if err = o.pool.QueryRow(t.Context(), "SELECT revision, encrypted_config FROM deployment_model_providers").Scan(&revision, &preserved); err != nil { - t.Fatal(err) - } - if err = o.pool.QueryRow(t.Context(), "SELECT c.deployment_provider_revision, e.encrypted_config FROM session_execution_configuration c JOIN session_model_execution e USING (session_id) WHERE c.session_id=$1", o.session.ID).Scan(&frozen, &preservedSession); err != nil { - t.Fatal(err) - } - if revision == old || frozen.Valid || string(secret) != string(preserved) { - t.Fatal("migration recreated historical identity or changed ciphertext") - } - if string(sessionBundle) != string(preservedSession) { - t.Fatal("migration changed the Session bundle") - } - o.observe(t, o.terminal(t, o.session.ID, "completed", "", ""), 0) -} diff --git a/services/core/internal/persistence/postgres/pgtest/pgtest_test.go b/services/core/internal/persistence/postgres/pgtest/pgtest_test.go index 0a9190930..01ebbc3c5 100644 --- a/services/core/internal/persistence/postgres/pgtest/pgtest_test.go +++ b/services/core/internal/persistence/postgres/pgtest/pgtest_test.go @@ -4,9 +4,9 @@ import "testing" func TestDatabaseGuardUsesEffectiveDatabase(t *testing.T) { for _, dsn := range []string{ - "postgres://localhost/oac_local_tests?dbname=agents_api", - "host=localhost dbname=agents_api", - "postgres://localhost/agents_api", + "postgres://localhost/oac_local_tests?dbname=oac", + "host=localhost dbname=oac", + "postgres://localhost/oac", } { if _, err := databaseConfig(dsn); err == nil { t.Fatalf("unsafe database accepted: %s", dsn) diff --git a/services/core/internal/persistence/postgres/sessionpg/creation.go b/services/core/internal/persistence/postgres/sessionpg/creation.go index 127fe6752..de740e39b 100644 --- a/services/core/internal/persistence/postgres/sessionpg/creation.go +++ b/services/core/internal/persistence/postgres/sessionpg/creation.go @@ -27,7 +27,7 @@ import ( // modelProviderKeyPurpose keys the provider-key fingerprint in creation // identities. Changing it changes every stored identity. -const modelProviderKeyPurpose = "parsar.agents-api.model-provider-api-key.v1" +const modelProviderKeyPurpose = "oac.model-provider-api-key.v1" var _ sessions.CreationTx = (*creationTx)(nil) diff --git a/services/core/migrations/000001_baseline.sql b/services/core/migrations/000001_baseline.sql new file mode 100644 index 000000000..c23491663 --- /dev/null +++ b/services/core/migrations/000001_baseline.sql @@ -0,0 +1,1344 @@ +-- +goose Up +-- +goose StatementBegin +CREATE FUNCTION immutable_runtime_deployment_generation() RETURNS trigger LANGUAGE plpgsql AS $$ +BEGIN + IF NEW.deployment_generation IS DISTINCT FROM OLD.deployment_generation THEN + RAISE EXCEPTION 'Sandbox ownership generation is immutable'; + END IF; + RETURN NEW; +END $$; +-- +goose StatementEnd + +-- +goose StatementBegin +CREATE FUNCTION immutable_sandbox_specification() RETURNS trigger LANGUAGE plpgsql AS $$ +BEGIN + RAISE EXCEPTION 'Retained sandbox specifications are immutable'; +END $$; +-- +goose StatementEnd + +-- +goose StatementBegin +CREATE FUNCTION immutable_workspace_configuration() RETURNS trigger LANGUAGE plpgsql AS $$ +BEGIN + IF (NEW.id, NEW.adapter, NEW.parameters, NEW.credential) IS DISTINCT FROM (OLD.id, OLD.adapter, OLD.parameters, OLD.credential) THEN + RAISE EXCEPTION 'Workspace filesystem configuration is immutable'; + END IF; + RETURN NEW; +END $$; +-- +goose StatementEnd + +-- +goose StatementBegin +CREATE FUNCTION immutable_workspace_identity() RETURNS trigger LANGUAGE plpgsql AS $$ +BEGIN + IF (NEW.object_id, NEW.environment_id, NEW.configuration_id, NEW.capacity_mib) IS DISTINCT FROM (OLD.object_id, OLD.environment_id, OLD.configuration_id, OLD.capacity_mib) THEN + RAISE EXCEPTION 'Environment workspace identity is immutable'; + END IF; + RETURN NEW; +END $$; +-- +goose StatementEnd + +CREATE TABLE admin_audit_log ( + id uuid NOT NULL, + tenant_id uuid, + project_id uuid, + admin_credential_id text NOT NULL, + actor_label text NOT NULL, + action text NOT NULL, + resource_type text NOT NULL, + resource_id text NOT NULL, + request_id text NOT NULL, + trace_id text NOT NULL, + created_at timestamp with time zone DEFAULT clock_timestamp() NOT NULL, + CONSTRAINT admin_audit_log_scope_check CHECK (((tenant_id IS NULL) = (project_id IS NULL))) +); + +CREATE TABLE agent_model_execution ( + agent_id uuid NOT NULL, + encrypted_config bytea NOT NULL +); + +CREATE TABLE agents ( + id uuid NOT NULL, + tenant_id uuid NOT NULL, + metadata jsonb NOT NULL, + configuration jsonb NOT NULL, + created_at timestamp with time zone DEFAULT statement_timestamp() NOT NULL, + updated_at timestamp with time zone DEFAULT statement_timestamp() NOT NULL, + CONSTRAINT agents_configuration_check CHECK ((jsonb_typeof(configuration) = 'object'::text)), + CONSTRAINT agents_metadata_check CHECK ((jsonb_typeof(metadata) = 'object'::text)) +); + +CREATE TABLE deployment_model_providers ( + harness text NOT NULL, + protocol text NOT NULL, + base_url text NOT NULL, + context_window integer DEFAULT 0 NOT NULL, + max_output_tokens integer DEFAULT 0 NOT NULL, + encrypted_config bytea NOT NULL, + updated_at timestamp with time zone DEFAULT clock_timestamp() NOT NULL, + revision uuid DEFAULT gen_random_uuid() NOT NULL, + last_used_at timestamp with time zone, + last_error_code text, + last_error_at timestamp with time zone, + recovery_pending boolean DEFAULT false NOT NULL, + model text DEFAULT ''::text NOT NULL, + harness_config jsonb DEFAULT '{}'::jsonb NOT NULL, + CONSTRAINT deployment_model_providers_context_window_check CHECK ((context_window >= 0)), + CONSTRAINT deployment_model_providers_max_output_tokens_check CHECK ((max_output_tokens >= 0)), + CONSTRAINT deployment_provider_error_code CHECK ((last_error_code = ANY (ARRAY['authentication_error'::text, 'connection_failed'::text, 'rate_limit_exceeded'::text, 'usage_limit_exceeded'::text, 'server_overloaded'::text, 'server_error'::text, 'resource_not_found'::text, 'request_timeout'::text, 'invalid_request'::text]))), + CONSTRAINT deployment_provider_error_pair CHECK (((last_error_code IS NULL) = (last_error_at IS NULL))), + CONSTRAINT deployment_provider_recovery CHECK (((NOT recovery_pending) OR (last_error_at IS NOT NULL))) +); + +CREATE TABLE devices ( + id uuid NOT NULL, + tenant_id uuid NOT NULL, + name text NOT NULL, + credential_hash text, + created_at timestamp with time zone DEFAULT clock_timestamp() NOT NULL, + last_seen_at timestamp with time zone, + revoked_at timestamp with time zone, + environment_id uuid, + executor_key_id uuid, + archive_cancel_turn_id uuid, + supported_agent_kinds jsonb DEFAULT '[]'::jsonb NOT NULL, + CONSTRAINT device_credential_source CHECK ((((executor_key_id IS NULL) AND (credential_hash IS NOT NULL)) OR ((executor_key_id IS NOT NULL) AND (credential_hash IS NULL) AND (environment_id IS NOT NULL)))), + CONSTRAINT devices_credential_hash_check CHECK ((credential_hash ~ '^[0-9a-f]{64}$'::text)), + CONSTRAINT devices_supported_agent_kinds_check CHECK ((jsonb_typeof(supported_agent_kinds) = 'array'::text)) +); + +CREATE TABLE environment_connections ( + environment_id uuid NOT NULL, + generation uuid NOT NULL, + revision bigint DEFAULT 0 NOT NULL, + CONSTRAINT environment_connections_revision_check CHECK ((revision >= 0)) +); + +CREATE TABLE environment_executor_credentials ( + environment_id uuid, + token_sha256 text NOT NULL, + issued_at timestamp with time zone DEFAULT clock_timestamp() NOT NULL, + revoked_at timestamp with time zone, + key_id uuid NOT NULL, + tenant_id uuid, + subject_kind text, + subject_id text, + created_at timestamp with time zone, + CONSTRAINT environment_executor_credentials_token_sha256_check CHECK ((token_sha256 ~ '^[0-9a-f]{64}$'::text)), + CONSTRAINT executor_principal_complete CHECK ((((tenant_id IS NULL) AND (subject_kind IS NULL) AND (subject_id IS NULL) AND (environment_id IS NOT NULL) AND (revoked_at IS NOT NULL) AND (created_at IS NULL)) OR ((tenant_id IS NOT NULL) AND (subject_kind IS NOT NULL) AND (subject_id IS NOT NULL) AND (subject_kind = ANY (ARRAY['user'::text, 'service_account'::text])) AND (subject_id <> ''::text) AND (created_at IS NOT NULL)))) +); + +CREATE TABLE environment_file_writes ( + id uuid NOT NULL, + environment_id uuid NOT NULL, + device_id uuid NOT NULL, + request_sha256 text NOT NULL, + state text DEFAULT 'pending'::text NOT NULL, + created_at timestamp with time zone DEFAULT clock_timestamp() NOT NULL, + settled_at timestamp with time zone, + audit_source jsonb, + CONSTRAINT environment_file_write_audit_source_object CHECK (((audit_source IS NULL) OR (jsonb_typeof(audit_source) = 'object'::text))), + CONSTRAINT environment_file_writes_check CHECK (((state = 'pending'::text) = (settled_at IS NULL))), + CONSTRAINT environment_file_writes_request_sha256_check CHECK ((request_sha256 ~ '^[0-9a-f]{64}$'::text)), + CONSTRAINT environment_file_writes_state_check CHECK ((state = ANY (ARRAY['pending'::text, 'committed'::text, 'rejected'::text]))) +); + +CREATE TABLE environment_input_reservations ( + id uuid NOT NULL, + session_id uuid NOT NULL, + idempotency_key text NOT NULL, + batch jsonb NOT NULL, + state text DEFAULT 'pending'::text NOT NULL, + created_at timestamp with time zone NOT NULL, + deadline timestamp with time zone NOT NULL, + settled_at timestamp with time zone, + is_initial boolean DEFAULT false NOT NULL, + failure_code text, + CONSTRAINT environment_input_reservations_batch_check CHECK ( +CASE + WHEN (jsonb_typeof(batch) = 'array'::text) THEN ((jsonb_array_length(batch) >= 1) AND (jsonb_array_length(batch) <= 64)) + ELSE false +END), + CONSTRAINT environment_input_reservations_check CHECK (((settled_at IS NOT NULL) = (state <> 'pending'::text))), + CONSTRAINT environment_input_reservations_check1 CHECK (((failure_code IS NULL) OR ((state = 'failed'::text) AND (failure_code = ANY (ARRAY['model_provider_required'::text, 'runtime_preparation_failed'::text]))))), + CONSTRAINT environment_input_reservations_state_check CHECK ((state = ANY (ARRAY['pending'::text, 'admitted'::text, 'expired'::text, 'cancelled'::text, 'failed'::text]))) +); + +CREATE TABLE environment_setups ( + session_id uuid NOT NULL, + contents bytea NOT NULL +); + +CREATE TABLE environment_templates ( + id uuid NOT NULL, + tenant_id uuid NOT NULL, + name text, + network_access text NOT NULL, + created_at timestamp with time zone DEFAULT statement_timestamp() NOT NULL, + updated_at timestamp with time zone DEFAULT statement_timestamp() NOT NULL, + files jsonb DEFAULT '[]'::jsonb NOT NULL, + file_contents bytea, + packages jsonb DEFAULT '{"npm": [], "python": [], "system": []}'::jsonb NOT NULL, + env_contents bytea, + setup_contents bytea, + skills jsonb DEFAULT '[]'::jsonb NOT NULL, + skill_contents bytea, + network_allowed_domains text[] DEFAULT '{}'::text[] NOT NULL, + plugins jsonb DEFAULT '[]'::jsonb NOT NULL, + plugin_contents bytea, + capability_directories text[] DEFAULT '{}'::text[] NOT NULL, + CONSTRAINT environment_templates_files_check CHECK ((jsonb_typeof(files) = 'array'::text)), + CONSTRAINT environment_templates_name_check CHECK (((name IS NULL) OR ((char_length(name) >= 1) AND (char_length(name) <= 256)))), + CONSTRAINT environment_templates_network_access_check CHECK ((network_access = ANY (ARRAY['enabled'::text, 'disabled'::text, 'restricted'::text]))), + CONSTRAINT environment_templates_network_domains_check CHECK ((((network_access = 'restricted'::text) AND ((cardinality(network_allowed_domains) >= 1) AND (cardinality(network_allowed_domains) <= 100))) OR ((network_access = ANY (ARRAY['enabled'::text, 'disabled'::text])) AND (cardinality(network_allowed_domains) = 0)))), + CONSTRAINT environment_templates_packages_check CHECK ((jsonb_typeof(packages) = 'object'::text)) +); + +CREATE TABLE environment_workspaces ( + object_id uuid NOT NULL, + environment_id uuid NOT NULL, + configuration_id uuid NOT NULL, + state text DEFAULT 'creating'::text NOT NULL, + attachment jsonb, + capacity_mib bigint DEFAULT 0 NOT NULL, + CONSTRAINT environment_workspaces_attachment_check CHECK (((attachment IS NULL) OR (jsonb_typeof(attachment) = 'object'::text))), + CONSTRAINT environment_workspaces_capacity_mib_check CHECK (((capacity_mib >= 0) AND (capacity_mib <= '4294967295'::bigint))), + CONSTRAINT environment_workspaces_check CHECK (((state <> 'ready'::text) OR (attachment IS NOT NULL))), + CONSTRAINT environment_workspaces_state_check CHECK ((state = ANY (ARRAY['creating'::text, 'ready'::text, 'deleting'::text, 'deleted'::text]))) +); + +CREATE TABLE environments ( + id uuid NOT NULL, + session_id uuid NOT NULL, + status text DEFAULT 'pending'::text NOT NULL, + created_at timestamp with time zone DEFAULT clock_timestamp() NOT NULL, + failure_reason text, + failed_at timestamp with time zone, + failure_detail jsonb, + initialization text DEFAULT 'complete'::text NOT NULL, + CONSTRAINT environment_failure_recorded CHECK ((((failure_reason IS NULL) = (failed_at IS NULL)) AND ((failed_at IS NULL) OR (status = 'failed'::text)) AND (char_length(failure_reason) <= 256))), + CONSTRAINT environments_failure_detail_check CHECK (((failure_detail IS NULL) OR (jsonb_typeof(failure_detail) = 'object'::text))), + CONSTRAINT environments_initialization_check CHECK ((initialization = ANY (ARRAY['pending'::text, 'running'::text, 'complete'::text, 'failed'::text]))), + CONSTRAINT environments_status_check CHECK ((status = ANY (ARRAY['pending'::text, 'connected'::text, 'disconnected'::text, 'expired'::text, 'failed'::text]))) +); + +CREATE TABLE execution_project_scopes ( + tenant_id uuid NOT NULL, + organization_id text NOT NULL, + project_id text NOT NULL, + CONSTRAINT execution_project_scopes_organization_id_check CHECK ((organization_id <> ''::text)), + CONSTRAINT execution_project_scopes_project_id_check CHECK ((project_id <> ''::text)), + CONSTRAINT execution_project_scopes_tenant_id_check CHECK ((tenant_id <> '00000000-0000-0000-0000-000000000000'::uuid)) +); + +CREATE TABLE function_calls ( + session_id uuid NOT NULL, + turn_id uuid NOT NULL, + call_id text NOT NULL, + executor_call_id text NOT NULL, + name text NOT NULL, + arguments jsonb NOT NULL, + result jsonb, + applied boolean DEFAULT false NOT NULL, + created_at timestamp with time zone DEFAULT clock_timestamp() NOT NULL, + withdrawn boolean DEFAULT false NOT NULL, + CONSTRAINT function_calls_check CHECK (((NOT applied) OR (result IS NOT NULL))), + CONSTRAINT function_calls_not_applied_and_withdrawn CHECK ((NOT (applied AND withdrawn))), + CONSTRAINT function_calls_result_check CHECK (((result IS NULL) OR (jsonb_typeof(result) = 'object'::text))) +); + +CREATE TABLE initial_environment_files ( + id uuid NOT NULL, + session_id uuid NOT NULL, + "position" integer NOT NULL, + path text NOT NULL, + size_bytes bigint NOT NULL, + contents bytea NOT NULL, + CONSTRAINT initial_environment_files_position_check CHECK ((("position" >= 0) AND ("position" <= 49))), + CONSTRAINT initial_environment_files_size_bytes_check CHECK (((size_bytes >= 0) AND (size_bytes <= 52428800))) +); + +CREATE TABLE node_host_history_samples ( + node_id uuid NOT NULL, + observed_at timestamp with time zone NOT NULL, + cpu_utilization double precision, + memory_used_bytes bigint, + available_disk_bytes bigint, + CONSTRAINT node_host_history_samples_available_disk_bytes_check CHECK ((available_disk_bytes >= 0)), + CONSTRAINT node_host_history_samples_cpu_utilization_check CHECK (((cpu_utilization >= (0)::double precision) AND (cpu_utilization <= (1)::double precision))), + CONSTRAINT node_host_history_samples_memory_used_bytes_check CHECK ((memory_used_bytes >= 0)) +); + +CREATE TABLE project_api_keys ( + id uuid NOT NULL, + name text NOT NULL, + prefix text NOT NULL, + token_sha256 text NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + revoked_at timestamp with time zone, + project_id uuid NOT NULL, + CONSTRAINT project_api_keys_name_check CHECK (((char_length(name) >= 1) AND (char_length(name) <= 80))), + CONSTRAINT project_api_keys_token_sha256_check CHECK ((token_sha256 ~ '^[0-9a-f]{64}$'::text)) +); + +CREATE TABLE projects ( + id uuid NOT NULL, + name text NOT NULL, + tenant_id uuid NOT NULL, + subject_kind text NOT NULL, + subject_id text NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + archived_at timestamp with time zone, + CONSTRAINT projects_name_check CHECK (((char_length(name) >= 1) AND (char_length(name) <= 128))), + CONSTRAINT projects_subject_kind_check CHECK ((subject_kind = 'service_account'::text)) +); + +CREATE TABLE subagent_turns ( + id uuid NOT NULL, + session_id uuid NOT NULL, + subagent_id uuid NOT NULL, + native_id text NOT NULL, + status text NOT NULL, + created_at timestamp with time zone NOT NULL, + started_at timestamp with time zone, + completed_at timestamp with time zone, + token_usage jsonb, + CONSTRAINT subagent_turns_check CHECK (((completed_at IS NOT NULL) = (status = ANY (ARRAY['completed'::text, 'failed'::text, 'cancelled'::text])))), + CONSTRAINT subagent_turns_native_id_check CHECK ((native_id <> ''::text)), + CONSTRAINT subagent_turns_status_check CHECK ((status = ANY (ARRAY['queued'::text, 'in_progress'::text, 'waiting'::text, 'completed'::text, 'failed'::text, 'cancelled'::text]))) +); + +CREATE TABLE turns ( + id uuid NOT NULL, + session_id uuid NOT NULL, + status text DEFAULT 'queued'::text NOT NULL, + created_at timestamp with time zone DEFAULT clock_timestamp() NOT NULL, + started_at timestamp with time zone, + completed_at timestamp with time zone, + cancel_requested_at timestamp with time zone, + outcome jsonb DEFAULT '{}'::jsonb NOT NULL, + event_count integer DEFAULT 0 NOT NULL, + event_bytes bigint DEFAULT 0 NOT NULL, + token_usage jsonb, + artifact_capture_started boolean DEFAULT false NOT NULL, + CONSTRAINT turns_check CHECK (((completed_at IS NOT NULL) = (status = ANY (ARRAY['completed'::text, 'failed'::text, 'cancelled'::text])))), + CONSTRAINT turns_outcome_check CHECK ((jsonb_typeof(outcome) = 'object'::text)), + CONSTRAINT turns_status_check CHECK ((status = ANY (ARRAY['queued'::text, 'in_progress'::text, 'waiting'::text, 'completed'::text, 'failed'::text, 'cancelled'::text]))) +); + +CREATE VIEW public_execution_turns AS + SELECT turns.id, + turns.session_id, + turns.status, + turns.created_at, + turns.started_at, + turns.completed_at, + turns.cancel_requested_at, + turns.outcome, + turns.token_usage, + turns.artifact_capture_started, + NULL::uuid AS subagent_id + FROM turns +UNION ALL + SELECT subagent_turns.id, + subagent_turns.session_id, + subagent_turns.status, + subagent_turns.created_at, + subagent_turns.started_at, + subagent_turns.completed_at, + NULL::timestamp with time zone AS cancel_requested_at, + '{}'::jsonb AS outcome, + subagent_turns.token_usage, + false AS artifact_capture_started, + subagent_turns.subagent_id + FROM subagent_turns; + +CREATE TABLE runtime_allocations ( + id uuid NOT NULL, + environment_id uuid NOT NULL, + device_id uuid NOT NULL, + provider_key uuid NOT NULL, + state text DEFAULT 'creating'::text NOT NULL, + create_settled boolean DEFAULT false NOT NULL, + created_at timestamp with time zone DEFAULT clock_timestamp() NOT NULL, + released_at timestamp with time zone, + compute_phase text DEFAULT 'disabled'::text NOT NULL, + compute_revision bigint DEFAULT 0 NOT NULL, + compute_state jsonb DEFAULT '{}'::jsonb NOT NULL, + compute_activity_at timestamp with time zone DEFAULT clock_timestamp() NOT NULL, + compute_wake_requested boolean DEFAULT false NOT NULL, + compute_retained_until timestamp with time zone, + node_id uuid, + observation_error text DEFAULT ''::text NOT NULL, + compute_phase_changed_at timestamp with time zone DEFAULT clock_timestamp(), + deployment_generation bigint, + CONSTRAINT runtime_allocations_check CHECK (((state = 'released'::text) = (released_at IS NOT NULL))), + CONSTRAINT runtime_allocations_check1 CHECK (((state <> 'released'::text) OR create_settled)), + CONSTRAINT runtime_allocations_check2 CHECK (((state = 'released'::text) OR (deployment_generation IS NOT NULL))), + CONSTRAINT runtime_allocations_compute_phase_check CHECK ((compute_phase = ANY (ARRAY['disabled'::text, 'running'::text, 'quiescing'::text, 'suspending'::text, 'suspended'::text, 'restoring'::text, 'waking'::text]))), + CONSTRAINT runtime_allocations_compute_revision_check CHECK ((compute_revision >= 0)), + CONSTRAINT runtime_allocations_compute_state_check CHECK ((jsonb_typeof(compute_state) = 'object'::text)), + CONSTRAINT runtime_allocations_deployment_generation_check CHECK ((deployment_generation >= 0)), + CONSTRAINT runtime_allocations_observation_error_check CHECK ((observation_error = ANY (ARRAY[''::text, 'node_unavailable'::text, 'resource_missing'::text, 'compute_unconfirmed'::text, 'ownership_mismatch'::text, 'provider_unavailable'::text]))), + CONSTRAINT runtime_allocations_state_check CHECK ((state = ANY (ARRAY['creating'::text, 'running'::text, 'cleanup_pending'::text, 'released'::text]))) +); + +CREATE TABLE runtime_deployment ( + singleton boolean DEFAULT true NOT NULL, + installation_id uuid, + backend_fingerprint text DEFAULT ''::text NOT NULL, + updated_at timestamp with time zone DEFAULT clock_timestamp() NOT NULL, + provider_kind text DEFAULT ''::text NOT NULL, + owner_epoch bigint DEFAULT 0 NOT NULL, + generation bigint DEFAULT 0 NOT NULL, + mode text DEFAULT ''::text NOT NULL, + provider_credential bytea, + specification jsonb DEFAULT '{}'::jsonb NOT NULL, + reset_clear text, + reset_requested_at timestamp with time zone, + reset_deadline_at timestamp with time zone, + reset_forced_at timestamp with time zone, + reset_audit jsonb, + provider_config jsonb DEFAULT '{}'::jsonb NOT NULL, + provider_metadata jsonb DEFAULT '{}'::jsonb NOT NULL, + idle_seconds bigint DEFAULT 300 NOT NULL, + CONSTRAINT runtime_deployment_generation_check CHECK ((generation >= 0)), + CONSTRAINT runtime_deployment_identity_check CHECK ((((installation_id IS NULL) AND (backend_fingerprint = ''::text)) OR ((installation_id IS NOT NULL) AND (backend_fingerprint ~ '^[0-9a-f]{64}$'::text)) OR ((installation_id IS NOT NULL) AND (provider_kind = ''::text) AND (backend_fingerprint = ''::text)))), + CONSTRAINT runtime_deployment_idle_seconds_check CHECK (((idle_seconds >= 30) AND (idle_seconds <= 86400))), + CONSTRAINT runtime_deployment_mode_check CHECK ((mode = ANY (ARRAY[''::text, 'nodes'::text, 'direct'::text]))), + CONSTRAINT runtime_deployment_owner_epoch_check CHECK ((owner_epoch >= 0)), + CONSTRAINT runtime_deployment_provider_config_check CHECK ((jsonb_typeof(provider_config) = 'object'::text)), + CONSTRAINT runtime_deployment_provider_kind_check CHECK (((provider_kind = ''::text) OR (provider_kind ~ '^[a-z][a-z0-9_-]{0,63}$'::text))), + CONSTRAINT runtime_deployment_provider_metadata_check CHECK ((jsonb_typeof(provider_metadata) = 'object'::text)), + CONSTRAINT runtime_deployment_reset_check CHECK ((((reset_clear IS NULL) AND (reset_requested_at IS NULL) AND (reset_deadline_at IS NULL) AND (reset_forced_at IS NULL) AND (reset_audit IS NULL)) OR ((reset_clear IS NOT NULL) AND (installation_id IS NOT NULL) AND (provider_kind <> ''::text) AND (reset_requested_at IS NOT NULL) AND (reset_audit IS NOT NULL) AND (jsonb_typeof(reset_audit) = 'object'::text) AND (((reset_clear = 'auto'::text) AND (reset_deadline_at IS NOT NULL) AND (reset_forced_at IS NULL)) OR ((reset_clear = 'force'::text) AND (reset_forced_at IS NOT NULL)))))), + CONSTRAINT runtime_deployment_setup_check CHECK (((installation_id IS NULL) OR ((provider_kind = ''::text) AND (mode = ''::text) AND (generation >= 0)) OR ((provider_kind <> ''::text) AND (mode = ANY (ARRAY['nodes'::text, 'direct'::text])) AND (generation > 0)))), + CONSTRAINT runtime_deployment_singleton_check CHECK (singleton) +); + +CREATE TABLE runtime_deployment_generations ( + generation bigint NOT NULL, + provider_kind text NOT NULL, + specification jsonb NOT NULL, + created_at timestamp with time zone DEFAULT clock_timestamp() NOT NULL, + provider_config jsonb DEFAULT '{}'::jsonb NOT NULL, + provider_metadata jsonb DEFAULT '{}'::jsonb NOT NULL, + CONSTRAINT runtime_deployment_generations_generation_check CHECK ((generation >= 0)), + CONSTRAINT runtime_deployment_generations_provider_config_check CHECK ((jsonb_typeof(provider_config) = 'object'::text)), + CONSTRAINT runtime_deployment_generations_provider_kind_check CHECK ((provider_kind ~ '^[a-z][a-z0-9_-]{0,63}$'::text)), + CONSTRAINT runtime_deployment_generations_provider_metadata_check CHECK ((jsonb_typeof(provider_metadata) = 'object'::text)) +); + +CREATE TABLE sessions ( + id uuid NOT NULL, + tenant_id uuid NOT NULL, + engine text NOT NULL, + metadata jsonb DEFAULT '{}'::jsonb NOT NULL, + idempotency_key text NOT NULL, + request_hash text NOT NULL, + created_at timestamp with time zone DEFAULT clock_timestamp() NOT NULL, + configuration jsonb DEFAULT '{}'::jsonb NOT NULL, + event_sequence bigint DEFAULT 0 NOT NULL, + creation_request_hash text, + deleted_at timestamp with time zone, + creator_kind text, + creator_id text, + CONSTRAINT sessions_configuration_check CHECK ((jsonb_typeof(configuration) = 'object'::text)), + CONSTRAINT sessions_creation_request_hash_check CHECK ((creation_request_hash ~ '^[0-9a-f]{64}$'::text)), + CONSTRAINT sessions_creator_complete CHECK ((((creator_kind IS NULL) AND (creator_id IS NULL)) OR ((creator_kind IS NOT NULL) AND (creator_id IS NOT NULL) AND (creator_kind = ANY (ARRAY['user'::text, 'service_account'::text])) AND (creator_id <> ''::text)))), + CONSTRAINT sessions_metadata_check CHECK ((jsonb_typeof(metadata) = 'object'::text)) +); + +CREATE VIEW runtime_device_authority AS + SELECT d.id, + d.tenant_id, + d.name, + d.environment_id, + COALESCE(c.token_sha256, d.credential_hash) AS credential_hash + FROM (((devices d + LEFT JOIN environments e ON ((e.id = d.environment_id))) + LEFT JOIN sessions s ON (((s.id = e.session_id) AND (s.tenant_id = d.tenant_id)))) + LEFT JOIN environment_executor_credentials c ON ((c.key_id = d.executor_key_id))) + WHERE ((d.revoked_at IS NULL) AND ((d.environment_id IS NULL) OR ((s.id IS NOT NULL) AND (s.deleted_at IS NULL))) AND ((d.executor_key_id IS NULL) OR ((c.revoked_at IS NULL) AND (c.tenant_id = s.tenant_id) AND (c.subject_kind = s.creator_kind) AND (c.subject_id = s.creator_id) AND ((c.environment_id IS NULL) OR (c.environment_id = e.id)) AND (((s.configuration -> 'environment'::text) ->> 'type'::text) = 'self_hosted'::text) AND (e.status <> ALL (ARRAY['failed'::text, 'expired'::text])) AND (EXISTS ( SELECT 1 + FROM execution_project_scopes p + WHERE (p.tenant_id = c.tenant_id)))))); + +CREATE TABLE runtime_history_samples ( + tenant_id uuid NOT NULL, + session_id uuid NOT NULL, + environment_id uuid NOT NULL, + resolved_at_ns bigint NOT NULL, + allocation_id uuid, + provider_type text NOT NULL, + status text NOT NULL, + observed_at_ns bigint, + started_at_ns bigint, + cpu_usage_seconds double precision, + cpu_capacity_cores double precision, + memory_usage_bytes bigint, + memory_limit_bytes bigint, + input_tokens bigint, + output_tokens bigint, + cpu_utilization_ratio double precision, + CONSTRAINT runtime_history_samples_check CHECK (((input_tokens IS NULL) = (output_tokens IS NULL))), + CONSTRAINT runtime_history_samples_check1 CHECK (((observed_at_ns >= 0) AND (observed_at_ns <= resolved_at_ns))), + CONSTRAINT runtime_history_samples_check2 CHECK (((started_at_ns >= 0) AND (started_at_ns <= observed_at_ns))), + CONSTRAINT runtime_history_samples_check3 CHECK (((status = 'observed'::text) = (observed_at_ns IS NOT NULL))), + CONSTRAINT runtime_history_samples_check4 CHECK (((started_at_ns IS NULL) OR (observed_at_ns IS NOT NULL))), + CONSTRAINT runtime_history_samples_check5 CHECK (((started_at_ns IS NOT NULL) OR ((cpu_usage_seconds IS NULL) AND (cpu_capacity_cores IS NULL) AND (memory_usage_bytes IS NULL) AND (memory_limit_bytes IS NULL)))), + CONSTRAINT runtime_history_samples_cpu_capacity_cores_check CHECK (((cpu_capacity_cores > (0)::double precision) AND (cpu_capacity_cores < 'Infinity'::double precision))), + CONSTRAINT runtime_history_samples_cpu_usage_seconds_check CHECK (((cpu_usage_seconds >= (0)::double precision) AND (cpu_usage_seconds < 'Infinity'::double precision))), + CONSTRAINT runtime_history_samples_cpu_utilization_incarnation CHECK (((started_at_ns IS NOT NULL) OR (cpu_utilization_ratio IS NULL))), + CONSTRAINT runtime_history_samples_cpu_utilization_ratio_check CHECK (((cpu_utilization_ratio >= (0)::double precision) AND (cpu_utilization_ratio < 'Infinity'::double precision))), + CONSTRAINT runtime_history_samples_input_tokens_check CHECK (((input_tokens >= 0) AND (input_tokens <= '9007199254740991'::bigint))), + CONSTRAINT runtime_history_samples_memory_limit_bytes_check CHECK (((memory_limit_bytes > 0) AND (memory_limit_bytes <= '9007199254740991'::bigint))), + CONSTRAINT runtime_history_samples_memory_usage_bytes_check CHECK (((memory_usage_bytes >= 0) AND (memory_usage_bytes <= '9007199254740991'::bigint))), + CONSTRAINT runtime_history_samples_output_tokens_check CHECK (((output_tokens >= 0) AND (output_tokens <= '9007199254740991'::bigint))), + CONSTRAINT runtime_history_samples_resolved_at_ns_check CHECK ((resolved_at_ns >= 0)), + CONSTRAINT runtime_history_samples_status_check CHECK ((status = ANY (ARRAY['observed'::text, 'unavailable'::text, 'unsupported'::text]))) +); + +CREATE TABLE runtime_node_enrollments ( + token_sha256 text NOT NULL, + installation_id uuid NOT NULL, + expires_at timestamp with time zone NOT NULL, + consumed_at timestamp with time zone, + node_id uuid, + max_active integer DEFAULT 2 NOT NULL, + max_retained integer DEFAULT 8 NOT NULL, + id uuid, + CONSTRAINT runtime_enrollment_capacity CHECK (((max_active >= 1) AND (max_retained >= max_active) AND (max_retained <= 1000000))), + CONSTRAINT runtime_node_enrollments_token_sha256_check CHECK ((token_sha256 ~ '^[0-9a-f]{64}$'::text)) +); + +CREATE TABLE runtime_node_generation_status ( + node_id uuid NOT NULL, + generation bigint NOT NULL, + specification_digest text NOT NULL, + connection_id uuid NOT NULL, + owner_epoch bigint NOT NULL, + state text NOT NULL, + diagnostic text DEFAULT ''::text NOT NULL, + observed_at timestamp with time zone DEFAULT clock_timestamp() NOT NULL, + checkpoint jsonb, + CONSTRAINT runtime_node_generation_status_checkpoint_check CHECK (((checkpoint IS NULL) OR (jsonb_typeof(checkpoint) = 'object'::text))), + CONSTRAINT runtime_node_generation_status_generation_check CHECK ((generation > 0)), + CONSTRAINT runtime_node_generation_status_owner_epoch_check CHECK ((owner_epoch > 0)), + CONSTRAINT runtime_node_generation_status_specification_digest_check CHECK ((specification_digest ~ '^[0-9a-f]{64}$'::text)), + CONSTRAINT runtime_node_generation_status_state_check CHECK ((state = ANY (ARRAY['ready'::text, 'preparing'::text, 'failed'::text]))) +); + +CREATE TABLE runtime_nodes ( + id uuid NOT NULL, + installation_id uuid NOT NULL, + name text NOT NULL, + backend_fingerprint text NOT NULL, + credential_sha256 text NOT NULL, + max_active integer NOT NULL, + max_retained integer NOT NULL, + connection_id uuid, + provider_ready boolean DEFAULT false NOT NULL, + health jsonb DEFAULT '{}'::jsonb NOT NULL, + connected_epoch bigint DEFAULT 0 NOT NULL, + last_seen_at timestamp with time zone, + created_at timestamp with time zone DEFAULT clock_timestamp() NOT NULL, + removed_at timestamp with time zone, + specification_digest text DEFAULT ''::text NOT NULL, + deployment_generation bigint DEFAULT 0 NOT NULL, + core_url text DEFAULT ''::text NOT NULL, + enrollment_id uuid, + ready_generation bigint, + protocol_version integer DEFAULT 1 NOT NULL, + CONSTRAINT runtime_node_deployment_generation CHECK ((deployment_generation >= 0)), + CONSTRAINT runtime_node_specification_digest CHECK (((specification_digest = ''::text) OR (specification_digest ~ '^[0-9a-f]{64}$'::text))), + CONSTRAINT runtime_nodes_backend_fingerprint_check CHECK ((backend_fingerprint ~ '^[0-9a-f]{64}$'::text)), + CONSTRAINT runtime_nodes_check CHECK ((max_retained >= max_active)), + CONSTRAINT runtime_nodes_credential_sha256_check CHECK ((credential_sha256 ~ '^[0-9a-f]{64}$'::text)), + CONSTRAINT runtime_nodes_max_active_check CHECK ((max_active > 0)), + CONSTRAINT runtime_nodes_name_check CHECK (((length(name) >= 1) AND (length(name) <= 128))), + CONSTRAINT runtime_nodes_protocol_version_check CHECK ((protocol_version = ANY (ARRAY[1, 2]))), + CONSTRAINT runtime_nodes_ready_generation_check CHECK ((ready_generation >= 0)) +); + +CREATE TABLE runtime_placements ( + environment_id uuid NOT NULL, + node_id uuid NOT NULL, + reserved_at timestamp with time zone DEFAULT clock_timestamp() NOT NULL, + released_at timestamp with time zone, + deployment_generation bigint, + CONSTRAINT runtime_placements_check CHECK (((released_at IS NOT NULL) OR (deployment_generation IS NOT NULL))), + CONSTRAINT runtime_placements_deployment_generation_check CHECK ((deployment_generation >= 0)) +); + +CREATE VIEW runtime_reset_retained_environments AS + SELECT e.id AS environment_id, + s.id AS session_id, + s.tenant_id, + a.deployment_generation + FROM ((((environments e + JOIN sessions s ON ((s.id = e.session_id))) + JOIN environment_workspaces w ON (((w.environment_id = e.id) AND (w.state = 'ready'::text)))) + JOIN LATERAL ( SELECT allocation.id, + allocation.environment_id, + allocation.device_id, + allocation.provider_key, + allocation.state, + allocation.create_settled, + allocation.created_at, + allocation.released_at, + allocation.compute_phase, + allocation.compute_revision, + allocation.compute_state, + allocation.compute_activity_at, + allocation.compute_wake_requested, + allocation.compute_retained_until, + allocation.node_id, + allocation.observation_error, + allocation.compute_phase_changed_at, + allocation.deployment_generation + FROM runtime_allocations allocation + WHERE (allocation.environment_id = e.id) + ORDER BY allocation.created_at DESC, allocation.id DESC + LIMIT 1) a ON (true)) + CROSS JOIN runtime_deployment d) + WHERE ((d.reset_clear IS NOT NULL) AND (d.reset_requested_at IS NOT NULL) AND (s.deleted_at IS NULL) AND (s.created_at <= d.reset_requested_at) AND (e.status <> ALL (ARRAY['failed'::text, 'expired'::text])) AND (e.initialization = 'complete'::text) AND (((s.configuration -> 'environment'::text) ->> 'type'::text) = 'openai_hosted'::text) AND (a.state = 'released'::text) AND (a.provider_key = d.installation_id) AND (a.deployment_generation <= d.generation) AND (a.created_at <= d.reset_requested_at) AND (NOT (EXISTS ( SELECT 1 + FROM runtime_placements p + WHERE ((p.environment_id = e.id) AND (p.released_at IS NULL)))))); + +CREATE TABLE session_artifacts ( + id uuid NOT NULL, + session_id uuid NOT NULL, + turn_id uuid NOT NULL, + environment_id uuid NOT NULL, + path text NOT NULL, + size_bytes bigint NOT NULL, + body_oid oid NOT NULL, + sha256 text NOT NULL, + created_at timestamp with time zone, + CONSTRAINT session_artifacts_path_check CHECK (((octet_length(path) >= 20) AND (octet_length(path) <= 4110))), + CONSTRAINT session_artifacts_sha256_check CHECK ((sha256 ~ '^[0-9a-f]{64}$'::text)), + CONSTRAINT session_artifacts_size_bytes_check CHECK (((size_bytes >= 0) AND (size_bytes <= 209715200))) +); + +CREATE TABLE session_devices ( + session_id uuid NOT NULL, + device_id uuid NOT NULL, + native_session_id text DEFAULT ''::text NOT NULL +); + +CREATE TABLE session_events ( + session_id uuid NOT NULL, + sequence bigint NOT NULL, + payload jsonb NOT NULL, + payload_bytes integer GENERATED ALWAYS AS (octet_length((payload)::text)) STORED +); + +CREATE TABLE session_execution_configuration ( + session_id uuid NOT NULL, + configuration jsonb NOT NULL, + deployment_provider_revision uuid, + CONSTRAINT session_execution_configuration_configuration_check CHECK ((jsonb_typeof(configuration) = 'object'::text)) +); + +CREATE TABLE session_items ( + id uuid NOT NULL, + session_id uuid NOT NULL, + turn_id uuid NOT NULL, + created_at timestamp with time zone NOT NULL, + "position" integer DEFAULT 0 NOT NULL, + payload jsonb NOT NULL, + output_index integer, + settled_at timestamp with time zone, + CONSTRAINT session_items_output_index_check CHECK ((output_index >= 0)), + CONSTRAINT session_items_payload_check CHECK ((jsonb_typeof(payload) = 'object'::text)) +); + +CREATE TABLE session_model_execution ( + session_id uuid NOT NULL, + encrypted_config bytea NOT NULL +); + +CREATE TABLE skill_versions ( + id uuid NOT NULL, + tenant_id uuid NOT NULL, + skill_id uuid NOT NULL, + version bigint NOT NULL, + name text NOT NULL, + description text NOT NULL, + created_at timestamp with time zone DEFAULT clock_timestamp() NOT NULL, + contents bytea NOT NULL, + CONSTRAINT skill_versions_version_check CHECK ((version > 0)) +); + +CREATE TABLE skills ( + id uuid NOT NULL, + tenant_id uuid NOT NULL, + name text NOT NULL, + description text NOT NULL, + created_at timestamp with time zone DEFAULT clock_timestamp() NOT NULL, + default_version bigint NOT NULL, + latest_version bigint NOT NULL, + next_version bigint NOT NULL, + CONSTRAINT skills_check CHECK ((next_version > latest_version)), + CONSTRAINT skills_default_version_check CHECK ((default_version > 0)), + CONSTRAINT skills_latest_version_check CHECK ((latest_version > 0)) +); + +CREATE TABLE source_files ( + id uuid NOT NULL, + tenant_id uuid NOT NULL, + filename text NOT NULL, + purpose text NOT NULL, + body_oid oid NOT NULL, + size_bytes bigint NOT NULL, + sha256 text NOT NULL, + created_at timestamp with time zone DEFAULT statement_timestamp() NOT NULL, + CONSTRAINT source_files_filename_check CHECK (((octet_length(filename) >= 1) AND (octet_length(filename) <= 1024))), + CONSTRAINT source_files_purpose_check CHECK ((purpose = 'user_data'::text)), + CONSTRAINT source_files_sha256_check CHECK ((sha256 ~ '^[0-9a-f]{64}$'::text)), + CONSTRAINT source_files_size_bytes_check CHECK (((size_bytes >= 0) AND (size_bytes <= 536870912))) +); + +CREATE TABLE subagent_effects ( + session_id uuid NOT NULL, + effect_id text NOT NULL, + payload jsonb NOT NULL, + CONSTRAINT subagent_effects_payload_check CHECK ((jsonb_typeof(payload) = 'object'::text)) +); + +CREATE TABLE subagent_identities ( + id uuid NOT NULL, + session_id uuid NOT NULL, + device_id uuid NOT NULL, + engine text NOT NULL, + native_id text NOT NULL, + parent_native_id text NOT NULL, + native_created_at bigint NOT NULL, + first_turn_id uuid NOT NULL, + first_event_ordinal integer NOT NULL, + name text, + instructions text, + public_visible boolean DEFAULT false NOT NULL, + status text DEFAULT 'active'::text NOT NULL, + closed_at_ms bigint, + lifecycle_at_ms bigint DEFAULT 0 NOT NULL, + CONSTRAINT subagent_closed_state CHECK (((status = 'closed'::text) = (closed_at_ms IS NOT NULL))), + CONSTRAINT subagent_identities_check CHECK (((parent_native_id <> ''::text) AND (parent_native_id <> native_id))), + CONSTRAINT subagent_identities_native_created_at_check CHECK ((native_created_at > 0)), + CONSTRAINT subagent_identities_native_id_check CHECK ((native_id <> ''::text)), + CONSTRAINT subagent_identities_status_check CHECK ((status = ANY (ARRAY['active'::text, 'closed'::text]))) +); + +CREATE TABLE subagent_items ( + id uuid NOT NULL, + session_id uuid NOT NULL, + subagent_id uuid NOT NULL, + turn_id uuid NOT NULL, + "position" integer NOT NULL, + output_index integer, + payload jsonb NOT NULL, + CONSTRAINT subagent_items_payload_check CHECK ((jsonb_typeof(payload) = 'object'::text)), + CONSTRAINT subagent_items_position_check CHECK (("position" >= 0)) +); + +CREATE TABLE turn_events ( + session_id uuid NOT NULL, + turn_id uuid NOT NULL, + ordinal integer NOT NULL, + kind text NOT NULL, + payload jsonb NOT NULL, + created_at timestamp with time zone DEFAULT clock_timestamp() NOT NULL, + CONSTRAINT turn_events_ordinal_check CHECK ((ordinal > 0)), + CONSTRAINT turn_events_payload_check CHECK ((jsonb_typeof(payload) = 'object'::text)) +); + +CREATE TABLE turn_inputs ( + sequence bigint GENERATED ALWAYS AS IDENTITY NOT NULL, + session_id uuid NOT NULL, + turn_id uuid, + idempotency_key text NOT NULL, + kind text NOT NULL, + payload jsonb NOT NULL, + created_at timestamp with time zone DEFAULT clock_timestamp() NOT NULL, + batch_position integer DEFAULT 0 NOT NULL, + CONSTRAINT turn_inputs_batch_position_check CHECK ((batch_position >= 0)), + CONSTRAINT turn_inputs_check CHECK (((kind <> 'message'::text) OR (turn_id IS NOT NULL))), + CONSTRAINT turn_inputs_function_turn_check CHECK (((kind <> 'tool_result'::text) OR (turn_id IS NOT NULL))), + CONSTRAINT turn_inputs_kind_check CHECK ((kind = ANY (ARRAY['message'::text, 'cancel'::text, 'tool_result'::text]))), + CONSTRAINT turn_inputs_payload_check CHECK ((jsonb_typeof(payload) = 'object'::text)) +); + +CREATE TABLE vault_credentials ( + id uuid NOT NULL, + vault_id uuid NOT NULL, + name text NOT NULL, + auth_type text NOT NULL, + mcp_server_url text NOT NULL, + token_ciphertext bytea NOT NULL, + created_at timestamp with time zone DEFAULT statement_timestamp() NOT NULL, + updated_at timestamp with time zone DEFAULT statement_timestamp() NOT NULL, + status text DEFAULT 'active'::text NOT NULL, + oauth_metadata jsonb, + CONSTRAINT vault_credentials_auth_type_check CHECK ((auth_type = ANY (ARRAY['static_bearer'::text, 'mcp_oauth'::text]))), + CONSTRAINT vault_credentials_name_check CHECK (((octet_length(name) >= 1) AND (octet_length(name) <= 256))), + CONSTRAINT vault_credentials_oauth_metadata_check CHECK ((((auth_type = 'static_bearer'::text) AND (oauth_metadata IS NULL)) OR ((auth_type = 'mcp_oauth'::text) AND (oauth_metadata IS NOT NULL) AND (jsonb_typeof(oauth_metadata) = 'object'::text)))), + CONSTRAINT vault_credentials_status_check CHECK ((status = ANY (ARRAY['active'::text, 'archived'::text]))), + CONSTRAINT vault_credentials_token_ciphertext_check CHECK ((octet_length(token_ciphertext) > 0)) +); + +CREATE TABLE vaults ( + id uuid NOT NULL, + tenant_id uuid NOT NULL, + name text, + metadata jsonb NOT NULL, + created_at timestamp with time zone DEFAULT statement_timestamp() NOT NULL, + status text DEFAULT 'active'::text NOT NULL, + CONSTRAINT vaults_metadata_check CHECK ((jsonb_typeof(metadata) = 'object'::text)), + CONSTRAINT vaults_name_check CHECK (((name IS NULL) OR ((octet_length(name) >= 1) AND (octet_length(name) <= 256)))), + CONSTRAINT vaults_status_check CHECK ((status = ANY (ARRAY['active'::text, 'archived'::text]))) +); + +CREATE TABLE workspace_fs_configurations ( + id uuid NOT NULL, + adapter text NOT NULL, + parameters jsonb NOT NULL, + active boolean DEFAULT false NOT NULL, + credential bytea, + CONSTRAINT workspace_fs_configurations_adapter_check CHECK ((adapter <> ''::text)), + CONSTRAINT workspace_fs_configurations_credential_check CHECK (((credential IS NULL) OR (length(credential) > 0))), + CONSTRAINT workspace_fs_configurations_parameters_check CHECK ((jsonb_typeof(parameters) = 'object'::text)) +); + +CREATE TABLE workspace_storage_admission ( + singleton boolean DEFAULT true NOT NULL, + tenant_capacity_mib bigint DEFAULT 0 NOT NULL, + CONSTRAINT workspace_storage_admission_singleton_check CHECK (singleton), + CONSTRAINT workspace_storage_admission_tenant_capacity_mib_check CHECK (((tenant_capacity_mib >= 0) AND (tenant_capacity_mib <= '4294967295'::bigint))) +); + +CREATE TABLE write_audit_operations ( + id uuid NOT NULL, + tenant_id uuid NOT NULL, + key_id text NOT NULL, + key_name text NOT NULL, + key_prefix text NOT NULL, + key_kind text NOT NULL, + action text NOT NULL, + resource_type text NOT NULL, + resource_id text NOT NULL, + parent_id text DEFAULT ''::text NOT NULL, + request_id text NOT NULL, + trace_id text NOT NULL, + created_at timestamp with time zone DEFAULT clock_timestamp() NOT NULL, + CONSTRAINT write_audit_operations_key_kind_check CHECK ((key_kind = ANY (ARRAY['static'::text, 'issued'::text, 'console'::text]))) +); + +CREATE TABLE write_audit_owners ( + tenant_id uuid NOT NULL, + resource_type text NOT NULL, + resource_id text NOT NULL, + parent_id text DEFAULT ''::text NOT NULL, + operation_id uuid NOT NULL +); + +ALTER TABLE admin_audit_log + ADD CONSTRAINT admin_audit_log_pkey PRIMARY KEY (id); + +ALTER TABLE admin_audit_log + ADD CONSTRAINT admin_audit_log_tenant_id_request_id_action_resource_type_r_key UNIQUE (tenant_id, request_id, action, resource_type, resource_id); + +ALTER TABLE agent_model_execution + ADD CONSTRAINT agent_model_execution_pkey PRIMARY KEY (agent_id); + +ALTER TABLE agents + ADD CONSTRAINT agents_pkey PRIMARY KEY (id); + +ALTER TABLE deployment_model_providers + ADD CONSTRAINT deployment_model_providers_pkey PRIMARY KEY (harness); + +ALTER TABLE devices + ADD CONSTRAINT devices_pkey PRIMARY KEY (id); + +ALTER TABLE environment_connections + ADD CONSTRAINT environment_connections_pkey PRIMARY KEY (environment_id); + +ALTER TABLE environment_executor_credentials + ADD CONSTRAINT environment_executor_credentials_pkey PRIMARY KEY (key_id); + +ALTER TABLE environment_executor_credentials + ADD CONSTRAINT environment_executor_credentials_token_sha256_key UNIQUE (token_sha256); + +ALTER TABLE environment_file_writes + ADD CONSTRAINT environment_file_writes_pkey PRIMARY KEY (id); + +ALTER TABLE environment_input_reservations + ADD CONSTRAINT environment_input_reservations_pkey PRIMARY KEY (id); + +ALTER TABLE environment_input_reservations + ADD CONSTRAINT environment_input_reservations_session_id_idempotency_key_key UNIQUE (session_id, idempotency_key); + +ALTER TABLE environment_setups + ADD CONSTRAINT environment_setups_pkey PRIMARY KEY (session_id); + +ALTER TABLE environment_templates + ADD CONSTRAINT environment_templates_pkey PRIMARY KEY (id); + +ALTER TABLE environment_workspaces + ADD CONSTRAINT environment_workspaces_environment_id_key UNIQUE (environment_id); + +ALTER TABLE environment_workspaces + ADD CONSTRAINT environment_workspaces_pkey PRIMARY KEY (object_id); + +ALTER TABLE environments + ADD CONSTRAINT environments_pkey PRIMARY KEY (id); + +ALTER TABLE environments + ADD CONSTRAINT environments_session_id_key UNIQUE (session_id); + +ALTER TABLE execution_project_scopes + ADD CONSTRAINT execution_project_scopes_organization_id_project_id_key UNIQUE (organization_id, project_id); + +ALTER TABLE execution_project_scopes + ADD CONSTRAINT execution_project_scopes_pkey PRIMARY KEY (tenant_id); + +ALTER TABLE function_calls + ADD CONSTRAINT function_calls_pkey PRIMARY KEY (session_id, turn_id, call_id); + +ALTER TABLE function_calls + ADD CONSTRAINT function_calls_session_id_turn_id_executor_call_id_key UNIQUE (session_id, turn_id, executor_call_id); + +ALTER TABLE initial_environment_files + ADD CONSTRAINT initial_environment_files_pkey PRIMARY KEY (id); + +ALTER TABLE initial_environment_files + ADD CONSTRAINT initial_environment_files_session_id_path_key UNIQUE (session_id, path); + +ALTER TABLE initial_environment_files + ADD CONSTRAINT initial_environment_files_session_id_position_key UNIQUE (session_id, "position"); + +ALTER TABLE node_host_history_samples + ADD CONSTRAINT node_host_history_samples_pkey PRIMARY KEY (node_id, observed_at); + +ALTER TABLE project_api_keys + ADD CONSTRAINT project_api_keys_pkey PRIMARY KEY (id); + +ALTER TABLE project_api_keys + ADD CONSTRAINT project_api_keys_token_sha256_key UNIQUE (token_sha256); + +ALTER TABLE projects + ADD CONSTRAINT projects_id_tenant_id_key UNIQUE (id, tenant_id); + +ALTER TABLE projects + ADD CONSTRAINT projects_pkey PRIMARY KEY (id); + +ALTER TABLE projects + ADD CONSTRAINT projects_tenant_id_key UNIQUE (tenant_id); + +ALTER TABLE runtime_allocations + ADD CONSTRAINT runtime_allocations_device_id_key UNIQUE (device_id); + +ALTER TABLE runtime_allocations + ADD CONSTRAINT runtime_allocations_pkey PRIMARY KEY (id); + +ALTER TABLE runtime_deployment_generations + ADD CONSTRAINT runtime_deployment_generations_pkey PRIMARY KEY (generation); + +ALTER TABLE runtime_deployment + ADD CONSTRAINT runtime_deployment_pkey PRIMARY KEY (singleton); + +ALTER TABLE runtime_history_samples + ADD CONSTRAINT runtime_history_samples_pkey PRIMARY KEY (tenant_id, session_id, environment_id, resolved_at_ns); + +ALTER TABLE runtime_node_enrollments + ADD CONSTRAINT runtime_node_enrollments_id_key UNIQUE (id); + +ALTER TABLE runtime_node_enrollments + ADD CONSTRAINT runtime_node_enrollments_pkey PRIMARY KEY (token_sha256); + +ALTER TABLE runtime_node_generation_status + ADD CONSTRAINT runtime_node_generation_status_pkey PRIMARY KEY (node_id, generation); + +ALTER TABLE runtime_nodes + ADD CONSTRAINT runtime_nodes_pkey PRIMARY KEY (id); + +ALTER TABLE runtime_placements + ADD CONSTRAINT runtime_placements_environment_id_node_id_key UNIQUE (environment_id, node_id); + +ALTER TABLE runtime_placements + ADD CONSTRAINT runtime_placements_pkey PRIMARY KEY (environment_id); + +ALTER TABLE session_artifacts + ADD CONSTRAINT session_artifacts_body_oid_key UNIQUE (body_oid); + +ALTER TABLE session_artifacts + ADD CONSTRAINT session_artifacts_pkey PRIMARY KEY (id); + +ALTER TABLE session_artifacts + ADD CONSTRAINT session_artifacts_turn_id_path_key UNIQUE (turn_id, path); + +ALTER TABLE session_devices + ADD CONSTRAINT session_devices_pkey PRIMARY KEY (session_id); + +ALTER TABLE session_events + ADD CONSTRAINT session_events_pkey PRIMARY KEY (session_id, sequence); + +ALTER TABLE session_execution_configuration + ADD CONSTRAINT session_execution_configuration_pkey PRIMARY KEY (session_id); + +ALTER TABLE session_items + ADD CONSTRAINT session_items_pkey PRIMARY KEY (id); + +ALTER TABLE session_model_execution + ADD CONSTRAINT session_model_execution_pkey PRIMARY KEY (session_id); + +ALTER TABLE sessions + ADD CONSTRAINT sessions_pkey PRIMARY KEY (id); + +ALTER TABLE sessions + ADD CONSTRAINT sessions_tenant_id_idempotency_key_key UNIQUE (tenant_id, idempotency_key); + +ALTER TABLE skill_versions + ADD CONSTRAINT skill_versions_pkey PRIMARY KEY (id); + +ALTER TABLE skill_versions + ADD CONSTRAINT skill_versions_skill_id_version_key UNIQUE (skill_id, version); + +ALTER TABLE skills + ADD CONSTRAINT skills_pkey PRIMARY KEY (id); + +ALTER TABLE skills + ADD CONSTRAINT skills_tenant_id_id_key UNIQUE (tenant_id, id); + +ALTER TABLE source_files + ADD CONSTRAINT source_files_body_oid_key UNIQUE (body_oid); + +ALTER TABLE source_files + ADD CONSTRAINT source_files_pkey PRIMARY KEY (id); + +ALTER TABLE subagent_effects + ADD CONSTRAINT subagent_effects_pkey PRIMARY KEY (session_id, effect_id); + +ALTER TABLE subagent_identities + ADD CONSTRAINT subagent_identities_device_id_engine_native_id_key UNIQUE (device_id, engine, native_id); + +ALTER TABLE subagent_identities + ADD CONSTRAINT subagent_identities_pkey PRIMARY KEY (id); + +ALTER TABLE subagent_identities + ADD CONSTRAINT subagent_identities_session_id_native_id_key UNIQUE (session_id, native_id); + +ALTER TABLE subagent_items + ADD CONSTRAINT subagent_items_pkey PRIMARY KEY (id); + +ALTER TABLE subagent_items + ADD CONSTRAINT subagent_items_turn_id_position_key UNIQUE (turn_id, "position"); + +ALTER TABLE subagent_identities + ADD CONSTRAINT subagent_session_identity UNIQUE (session_id, id); + +ALTER TABLE subagent_turns + ADD CONSTRAINT subagent_turns_pkey PRIMARY KEY (id); + +ALTER TABLE subagent_turns + ADD CONSTRAINT subagent_turns_session_id_subagent_id_id_key UNIQUE (session_id, subagent_id, id); + +ALTER TABLE subagent_turns + ADD CONSTRAINT subagent_turns_subagent_id_native_id_key UNIQUE (subagent_id, native_id); + +ALTER TABLE turn_events + ADD CONSTRAINT turn_events_pkey PRIMARY KEY (turn_id, ordinal); + +ALTER TABLE turn_inputs + ADD CONSTRAINT turn_inputs_pkey PRIMARY KEY (sequence); + +ALTER TABLE turn_inputs + ADD CONSTRAINT turn_inputs_session_id_idempotency_key_batch_position_key UNIQUE (session_id, idempotency_key, batch_position); + +ALTER TABLE turns + ADD CONSTRAINT turns_pkey PRIMARY KEY (id); + +ALTER TABLE turns + ADD CONSTRAINT turns_session_id_id_key UNIQUE (session_id, id); + +ALTER TABLE vault_credentials + ADD CONSTRAINT vault_credentials_pkey PRIMARY KEY (id); + +ALTER TABLE vaults + ADD CONSTRAINT vaults_pkey PRIMARY KEY (id); + +ALTER TABLE workspace_fs_configurations + ADD CONSTRAINT workspace_fs_configurations_pkey PRIMARY KEY (id); + +ALTER TABLE workspace_storage_admission + ADD CONSTRAINT workspace_storage_admission_pkey PRIMARY KEY (singleton); + +ALTER TABLE write_audit_operations + ADD CONSTRAINT write_audit_operations_pkey PRIMARY KEY (id); + +ALTER TABLE write_audit_operations + ADD CONSTRAINT write_audit_operations_tenant_id_id_key UNIQUE (tenant_id, id); + +ALTER TABLE write_audit_operations + ADD CONSTRAINT write_audit_operations_tenant_id_request_id_key UNIQUE (tenant_id, request_id); + +ALTER TABLE write_audit_owners + ADD CONSTRAINT write_audit_owners_pkey PRIMARY KEY (tenant_id, resource_type, resource_id); + +CREATE INDEX admin_audit_log_history ON admin_audit_log USING btree (project_id, created_at DESC, id DESC); + +CREATE INDEX agents_tenant_created_idx ON agents USING btree (tenant_id, created_at DESC, id DESC); + +CREATE UNIQUE INDEX devices_environment_authority ON devices USING btree (environment_id) WHERE ((revoked_at IS NULL) OR (executor_key_id IS NOT NULL)); + +CREATE INDEX devices_tenant_idx ON devices USING btree (tenant_id); + +CREATE UNIQUE INDEX environment_file_writes_pending ON environment_file_writes USING btree (environment_id) WHERE (state = 'pending'::text); + +CREATE INDEX environment_input_reservations_latest_idx ON environment_input_reservations USING btree (session_id, created_at DESC, id DESC); + +CREATE UNIQUE INDEX environment_input_reservations_one_pending_idx ON environment_input_reservations USING btree (session_id) WHERE (state = 'pending'::text); + +CREATE INDEX environment_input_reservations_pending_deadline_idx ON environment_input_reservations USING btree (deadline, id) WHERE (state = 'pending'::text); + +CREATE INDEX environment_templates_tenant_order ON environment_templates USING btree (tenant_id, created_at, id); + +CREATE INDEX function_calls_pending_idx ON function_calls USING btree (session_id, turn_id) WHERE ((NOT applied) AND (NOT withdrawn)); + +CREATE INDEX node_host_history_retention ON node_host_history_samples USING btree (observed_at); + +CREATE INDEX project_api_keys_project ON project_api_keys USING btree (project_id, id); + +CREATE UNIQUE INDEX runtime_allocations_current_environment ON runtime_allocations USING btree (environment_id) WHERE (state <> 'released'::text); + +CREATE INDEX runtime_allocations_environment_receipts ON runtime_allocations USING btree (environment_id, created_at DESC, id DESC); + +CREATE INDEX runtime_allocations_generation_live ON runtime_allocations USING btree (deployment_generation) WHERE (state <> 'released'::text); + +CREATE INDEX runtime_history_samples_retention_idx ON runtime_history_samples USING btree (resolved_at_ns); + +CREATE INDEX runtime_nodes_generation_live ON runtime_nodes USING btree (ready_generation) WHERE (removed_at IS NULL); + +CREATE INDEX runtime_placements_generation_live ON runtime_placements USING btree (deployment_generation) WHERE (released_at IS NULL); + +CREATE INDEX runtime_placements_node_idx ON runtime_placements USING btree (node_id) WHERE (released_at IS NULL); + +CREATE INDEX session_artifacts_page_idx ON session_artifacts USING btree (session_id, created_at DESC, id DESC) WHERE (created_at IS NOT NULL); + +CREATE UNIQUE INDEX session_items_output_idx ON session_items USING btree (turn_id, output_index); + +CREATE INDEX session_items_page_idx ON session_items USING btree (session_id, created_at, "position", id); + +CREATE UNIQUE INDEX session_items_position_idx ON session_items USING btree (session_id, "position"); + +CREATE INDEX session_items_turn_idx ON session_items USING btree (turn_id); + +CREATE INDEX sessions_tenant_agent_created_idx ON sessions USING btree (tenant_id, ((configuration #>> '{agent,id}'::text[])), created_at DESC, id DESC); + +CREATE INDEX sessions_tenant_created_idx ON sessions USING btree (tenant_id, created_at DESC, id DESC); + +CREATE INDEX skills_tenant_created ON skills USING btree (tenant_id, created_at, id); + +CREATE INDEX source_files_page_idx ON source_files USING btree (tenant_id, created_at DESC, id DESC); + +CREATE INDEX turn_events_item_history_idx ON turn_events USING btree (turn_id, created_at, ordinal); + +CREATE INDEX turn_inputs_item_history_idx ON turn_inputs USING btree (turn_id, created_at, sequence) WHERE (kind = 'message'::text); + +CREATE INDEX turn_inputs_turn_sequence_idx ON turn_inputs USING btree (turn_id, sequence); + +CREATE INDEX turns_execution_queue_idx ON turns USING btree (status, id) WHERE (status = ANY (ARRAY['queued'::text, 'in_progress'::text, 'waiting'::text])); + +CREATE INDEX turns_metrics_interrupted_idx ON turns USING btree (completed_at) WHERE ((status = 'failed'::text) AND ((outcome ->> 'error_code'::text) = 'execution_interrupted'::text)); + +CREATE INDEX turns_metrics_started_idx ON turns USING btree (started_at) INCLUDE (created_at) WHERE (started_at IS NOT NULL); + +CREATE UNIQUE INDEX turns_one_active_idx ON turns USING btree (session_id) WHERE (status = ANY (ARRAY['queued'::text, 'in_progress'::text, 'waiting'::text])); + +CREATE INDEX vault_credentials_vault_created_id_idx ON vault_credentials USING btree (vault_id, created_at, id); + +CREATE INDEX vaults_tenant_created_id_idx ON vaults USING btree (tenant_id, created_at, id); + +CREATE UNIQUE INDEX workspace_fs_active_idx ON workspace_fs_configurations USING btree (active) WHERE active; + +CREATE INDEX write_audit_operations_expiry ON write_audit_operations USING btree (created_at, id); + +CREATE INDEX write_audit_operations_history ON write_audit_operations USING btree (tenant_id, created_at DESC, id DESC); + +CREATE INDEX write_audit_operations_key_history ON write_audit_operations USING btree (tenant_id, key_id, created_at DESC, id DESC); + +CREATE INDEX write_audit_operations_resource_history ON write_audit_operations USING btree (tenant_id, resource_type, resource_id, created_at DESC, id DESC); + +CREATE INDEX write_audit_owners_operation ON write_audit_owners USING btree (operation_id); + +CREATE TRIGGER immutable_runtime_allocation_generation BEFORE UPDATE ON runtime_allocations FOR EACH ROW EXECUTE FUNCTION immutable_runtime_deployment_generation(); + +CREATE TRIGGER immutable_runtime_placement_generation BEFORE UPDATE ON runtime_placements FOR EACH ROW EXECUTE FUNCTION immutable_runtime_deployment_generation(); + +CREATE TRIGGER immutable_sandbox_specification BEFORE UPDATE ON runtime_deployment_generations FOR EACH ROW EXECUTE FUNCTION immutable_sandbox_specification(); + +CREATE TRIGGER immutable_workspace_configuration BEFORE UPDATE ON workspace_fs_configurations FOR EACH ROW EXECUTE FUNCTION immutable_workspace_configuration(); + +CREATE TRIGGER immutable_workspace_identity BEFORE UPDATE ON environment_workspaces FOR EACH ROW EXECUTE FUNCTION immutable_workspace_identity(); + +ALTER TABLE admin_audit_log + ADD CONSTRAINT admin_audit_log_project_id_tenant_id_fkey FOREIGN KEY (project_id, tenant_id) REFERENCES projects(id, tenant_id); + +ALTER TABLE agent_model_execution + ADD CONSTRAINT agent_model_execution_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE; + +ALTER TABLE devices + ADD CONSTRAINT devices_archive_cancel_turn_id_fkey FOREIGN KEY (archive_cancel_turn_id) REFERENCES turns(id) ON DELETE SET NULL; + +ALTER TABLE devices + ADD CONSTRAINT devices_environment_id_fkey FOREIGN KEY (environment_id) REFERENCES environments(id); + +ALTER TABLE devices + ADD CONSTRAINT devices_executor_key_id_fkey FOREIGN KEY (executor_key_id) REFERENCES environment_executor_credentials(key_id); + +ALTER TABLE environment_connections + ADD CONSTRAINT environment_connections_environment_id_fkey FOREIGN KEY (environment_id) REFERENCES environments(id); + +ALTER TABLE environment_executor_credentials + ADD CONSTRAINT environment_executor_credentials_environment_id_fkey FOREIGN KEY (environment_id) REFERENCES environments(id); + +ALTER TABLE environment_executor_credentials + ADD CONSTRAINT environment_executor_credentials_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES execution_project_scopes(tenant_id); + +ALTER TABLE environment_file_writes + ADD CONSTRAINT environment_file_writes_device_id_fkey FOREIGN KEY (device_id) REFERENCES devices(id); + +ALTER TABLE environment_file_writes + ADD CONSTRAINT environment_file_writes_environment_id_fkey FOREIGN KEY (environment_id) REFERENCES environments(id); + +ALTER TABLE environment_input_reservations + ADD CONSTRAINT environment_input_reservations_session_id_fkey FOREIGN KEY (session_id) REFERENCES environments(session_id); + +ALTER TABLE environment_setups + ADD CONSTRAINT environment_setups_session_id_fkey FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE; + +ALTER TABLE environment_workspaces + ADD CONSTRAINT environment_workspaces_configuration_id_fkey FOREIGN KEY (configuration_id) REFERENCES workspace_fs_configurations(id); + +ALTER TABLE environment_workspaces + ADD CONSTRAINT environment_workspaces_environment_id_fkey FOREIGN KEY (environment_id) REFERENCES environments(id); + +ALTER TABLE environments + ADD CONSTRAINT environments_session_id_fkey FOREIGN KEY (session_id) REFERENCES sessions(id); + +ALTER TABLE function_calls + ADD CONSTRAINT function_calls_session_id_turn_id_fkey FOREIGN KEY (session_id, turn_id) REFERENCES turns(session_id, id); + +ALTER TABLE initial_environment_files + ADD CONSTRAINT initial_environment_files_session_id_fkey FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE; + +ALTER TABLE node_host_history_samples + ADD CONSTRAINT node_host_history_samples_node_id_fkey FOREIGN KEY (node_id) REFERENCES runtime_nodes(id); + +ALTER TABLE project_api_keys + ADD CONSTRAINT project_api_keys_project_id_fkey FOREIGN KEY (project_id) REFERENCES projects(id); + +ALTER TABLE projects + ADD CONSTRAINT projects_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES execution_project_scopes(tenant_id); + +ALTER TABLE runtime_allocations + ADD CONSTRAINT runtime_allocation_node_fk FOREIGN KEY (node_id) REFERENCES runtime_nodes(id); + +ALTER TABLE runtime_allocations + ADD CONSTRAINT runtime_allocations_device_id_fkey FOREIGN KEY (device_id) REFERENCES devices(id); + +ALTER TABLE runtime_allocations + ADD CONSTRAINT runtime_allocations_environment_id_fkey FOREIGN KEY (environment_id) REFERENCES environments(id); + +ALTER TABLE runtime_history_samples + ADD CONSTRAINT runtime_history_samples_environment_id_fkey FOREIGN KEY (environment_id) REFERENCES environments(id) ON DELETE CASCADE; + +ALTER TABLE runtime_history_samples + ADD CONSTRAINT runtime_history_samples_session_id_fkey FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE; + +ALTER TABLE runtime_node_enrollments + ADD CONSTRAINT runtime_node_enrollments_node_id_fkey FOREIGN KEY (node_id) REFERENCES runtime_nodes(id); + +ALTER TABLE runtime_node_generation_status + ADD CONSTRAINT runtime_node_generation_status_node_id_fkey FOREIGN KEY (node_id) REFERENCES runtime_nodes(id); + +ALTER TABLE runtime_placements + ADD CONSTRAINT runtime_placements_environment_id_fkey FOREIGN KEY (environment_id) REFERENCES environments(id); + +ALTER TABLE runtime_placements + ADD CONSTRAINT runtime_placements_node_id_fkey FOREIGN KEY (node_id) REFERENCES runtime_nodes(id); + +ALTER TABLE session_artifacts + ADD CONSTRAINT session_artifacts_environment_id_fkey FOREIGN KEY (environment_id) REFERENCES environments(id); + +ALTER TABLE session_artifacts + ADD CONSTRAINT session_artifacts_session_id_fkey FOREIGN KEY (session_id) REFERENCES sessions(id); + +ALTER TABLE session_artifacts + ADD CONSTRAINT session_artifacts_session_id_turn_id_fkey FOREIGN KEY (session_id, turn_id) REFERENCES turns(session_id, id); + +ALTER TABLE session_devices + ADD CONSTRAINT session_devices_device_id_fkey FOREIGN KEY (device_id) REFERENCES devices(id); + +ALTER TABLE session_devices + ADD CONSTRAINT session_devices_session_id_fkey FOREIGN KEY (session_id) REFERENCES sessions(id); + +ALTER TABLE session_events + ADD CONSTRAINT session_events_session_id_fkey FOREIGN KEY (session_id) REFERENCES sessions(id); + +ALTER TABLE session_execution_configuration + ADD CONSTRAINT session_execution_configuration_session_id_fkey FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE; + +ALTER TABLE session_items + ADD CONSTRAINT session_items_session_id_turn_id_fkey FOREIGN KEY (session_id, turn_id) REFERENCES turns(session_id, id); + +ALTER TABLE session_model_execution + ADD CONSTRAINT session_model_execution_session_id_fkey FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE; + +ALTER TABLE skill_versions + ADD CONSTRAINT skill_versions_tenant_id_skill_id_fkey FOREIGN KEY (tenant_id, skill_id) REFERENCES skills(tenant_id, id) ON DELETE CASCADE; + +ALTER TABLE skills + ADD CONSTRAINT skills_default_version FOREIGN KEY (id, default_version) REFERENCES skill_versions(skill_id, version) DEFERRABLE INITIALLY DEFERRED; + +ALTER TABLE skills + ADD CONSTRAINT skills_latest_version FOREIGN KEY (id, latest_version) REFERENCES skill_versions(skill_id, version) DEFERRABLE INITIALLY DEFERRED; + +ALTER TABLE subagent_effects + ADD CONSTRAINT subagent_effects_session_id_fkey FOREIGN KEY (session_id) REFERENCES sessions(id); + +ALTER TABLE subagent_identities + ADD CONSTRAINT subagent_identities_device_id_fkey FOREIGN KEY (device_id) REFERENCES devices(id); + +ALTER TABLE subagent_identities + ADD CONSTRAINT subagent_identities_first_turn_id_first_event_ordinal_fkey FOREIGN KEY (first_turn_id, first_event_ordinal) REFERENCES turn_events(turn_id, ordinal); + +ALTER TABLE subagent_identities + ADD CONSTRAINT subagent_identities_session_id_first_turn_id_fkey FOREIGN KEY (session_id, first_turn_id) REFERENCES turns(session_id, id); + +ALTER TABLE subagent_identities + ADD CONSTRAINT subagent_identities_session_id_fkey FOREIGN KEY (session_id) REFERENCES sessions(id); + +ALTER TABLE subagent_items + ADD CONSTRAINT subagent_items_session_id_subagent_id_turn_id_fkey FOREIGN KEY (session_id, subagent_id, turn_id) REFERENCES subagent_turns(session_id, subagent_id, id); + +ALTER TABLE subagent_turns + ADD CONSTRAINT subagent_turns_session_id_subagent_id_fkey FOREIGN KEY (session_id, subagent_id) REFERENCES subagent_identities(session_id, id); + +ALTER TABLE turn_events + ADD CONSTRAINT turn_events_session_id_turn_id_fkey FOREIGN KEY (session_id, turn_id) REFERENCES turns(session_id, id); + +ALTER TABLE turn_inputs + ADD CONSTRAINT turn_inputs_session_id_fkey FOREIGN KEY (session_id) REFERENCES sessions(id); + +ALTER TABLE turn_inputs + ADD CONSTRAINT turn_inputs_session_id_turn_id_fkey FOREIGN KEY (session_id, turn_id) REFERENCES turns(session_id, id); + +ALTER TABLE turns + ADD CONSTRAINT turns_session_id_fkey FOREIGN KEY (session_id) REFERENCES sessions(id); + +ALTER TABLE vault_credentials + ADD CONSTRAINT vault_credentials_vault_id_fkey FOREIGN KEY (vault_id) REFERENCES vaults(id) ON DELETE CASCADE; + +ALTER TABLE write_audit_owners + ADD CONSTRAINT write_audit_owners_tenant_id_operation_id_fkey FOREIGN KEY (tenant_id, operation_id) REFERENCES write_audit_operations(tenant_id, id); + +INSERT INTO runtime_deployment (singleton) VALUES (true); +INSERT INTO workspace_storage_admission DEFAULT VALUES; diff --git a/services/core/migrations/000001_sessions.sql b/services/core/migrations/000001_sessions.sql deleted file mode 100644 index 1ceeb5e7c..000000000 --- a/services/core/migrations/000001_sessions.sql +++ /dev/null @@ -1,15 +0,0 @@ --- +goose Up -CREATE TABLE sessions ( - id uuid PRIMARY KEY, - tenant_id uuid NOT NULL, - engine text NOT NULL, - metadata jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(metadata) = 'object'), - idempotency_key text NOT NULL, - request_hash text NOT NULL, - created_at timestamptz NOT NULL DEFAULT clock_timestamp(), - UNIQUE (tenant_id, idempotency_key) -); -CREATE INDEX sessions_tenant_created_idx ON sessions (tenant_id, created_at DESC, id DESC); - --- +goose Down -DROP TABLE sessions; diff --git a/services/core/migrations/000002_session_configuration.sql b/services/core/migrations/000002_session_configuration.sql deleted file mode 100644 index f97c2d629..000000000 --- a/services/core/migrations/000002_session_configuration.sql +++ /dev/null @@ -1,6 +0,0 @@ --- +goose Up -ALTER TABLE sessions ADD COLUMN configuration jsonb NOT NULL DEFAULT '{}'::jsonb - CHECK (jsonb_typeof(configuration) = 'object'); - --- +goose Down -ALTER TABLE sessions DROP COLUMN configuration; diff --git a/services/core/migrations/000003_turns.sql b/services/core/migrations/000003_turns.sql deleted file mode 100644 index 879bd9213..000000000 --- a/services/core/migrations/000003_turns.sql +++ /dev/null @@ -1,35 +0,0 @@ --- +goose Up -CREATE TABLE turns ( - id uuid PRIMARY KEY, - session_id uuid NOT NULL REFERENCES sessions(id), - status text NOT NULL DEFAULT 'queued' - CHECK (status IN ('queued', 'in_progress', 'waiting', 'completed', 'failed', 'cancelled')), - created_at timestamptz NOT NULL DEFAULT clock_timestamp(), - started_at timestamptz, - completed_at timestamptz, - cancel_requested_at timestamptz, - outcome jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(outcome) = 'object'), - UNIQUE (session_id, id), - CHECK ((completed_at IS NOT NULL) = (status IN ('completed', 'failed', 'cancelled'))) -); -CREATE UNIQUE INDEX turns_one_active_idx ON turns(session_id) - WHERE status IN ('queued', 'in_progress', 'waiting'); - --- Inputs also retain retry identity after their target Turn has ended. -CREATE TABLE turn_inputs ( - sequence bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - session_id uuid NOT NULL REFERENCES sessions(id), - turn_id uuid, - idempotency_key text NOT NULL, - kind text NOT NULL CHECK (kind IN ('message', 'cancel')), - payload jsonb NOT NULL CHECK (jsonb_typeof(payload) = 'object'), - created_at timestamptz NOT NULL DEFAULT clock_timestamp(), - UNIQUE (session_id, idempotency_key), - FOREIGN KEY (session_id, turn_id) REFERENCES turns(session_id, id), - CHECK (kind <> 'message' OR turn_id IS NOT NULL) -); -CREATE INDEX turn_inputs_turn_sequence_idx ON turn_inputs(turn_id, sequence); - --- +goose Down -DROP TABLE turn_inputs; -DROP TABLE turns; diff --git a/services/core/migrations/000004_input_batches.sql b/services/core/migrations/000004_input_batches.sql deleted file mode 100644 index a6b2845ec..000000000 --- a/services/core/migrations/000004_input_batches.sql +++ /dev/null @@ -1,12 +0,0 @@ --- +goose Up --- Existing single-event requests retain their retry identity at position zero. -ALTER TABLE turn_inputs ADD COLUMN batch_position integer NOT NULL DEFAULT 0 - CHECK (batch_position >= 0); -ALTER TABLE turn_inputs DROP CONSTRAINT turn_inputs_session_id_idempotency_key_key; -ALTER TABLE turn_inputs ADD UNIQUE (session_id, idempotency_key, batch_position); - --- +goose Down --- A multi-event request cannot be represented by the previous schema without --- losing inputs or retry identities. Let the uniqueness check stop such a downgrade. -ALTER TABLE turn_inputs ADD UNIQUE (session_id, idempotency_key); -ALTER TABLE turn_inputs DROP COLUMN batch_position; diff --git a/services/core/migrations/000005_devices.sql b/services/core/migrations/000005_devices.sql deleted file mode 100644 index 514bfc0fe..000000000 --- a/services/core/migrations/000005_devices.sql +++ /dev/null @@ -1,20 +0,0 @@ --- +goose Up -CREATE TABLE devices ( - id uuid PRIMARY KEY, - tenant_id uuid NOT NULL, - name text NOT NULL, - credential_hash text NOT NULL CHECK (credential_hash ~ '^[0-9a-f]{64}$'), - created_at timestamptz NOT NULL DEFAULT clock_timestamp(), - last_seen_at timestamptz, - revoked_at timestamptz -); -CREATE INDEX devices_tenant_idx ON devices (tenant_id); - -CREATE TABLE session_devices ( - session_id uuid PRIMARY KEY REFERENCES sessions(id), - device_id uuid NOT NULL REFERENCES devices(id) -); - --- +goose Down -DROP TABLE session_devices; -DROP TABLE devices; diff --git a/services/core/migrations/000006_native_sessions.sql b/services/core/migrations/000006_native_sessions.sql deleted file mode 100644 index 05ca6bf5e..000000000 --- a/services/core/migrations/000006_native_sessions.sql +++ /dev/null @@ -1,5 +0,0 @@ --- +goose Up -ALTER TABLE session_devices ADD COLUMN native_session_id text NOT NULL DEFAULT ''; - --- +goose Down -ALTER TABLE session_devices DROP COLUMN native_session_id; diff --git a/services/core/migrations/000007_turn_events.sql b/services/core/migrations/000007_turn_events.sql deleted file mode 100644 index 2a73d02b9..000000000 --- a/services/core/migrations/000007_turn_events.sql +++ /dev/null @@ -1,19 +0,0 @@ --- +goose Up -ALTER TABLE turns ADD COLUMN event_count integer NOT NULL DEFAULT 0; -ALTER TABLE turns ADD COLUMN event_bytes bigint NOT NULL DEFAULT 0; - -CREATE TABLE turn_events ( - session_id uuid NOT NULL, - turn_id uuid NOT NULL, - ordinal integer NOT NULL CHECK (ordinal > 0), - kind text NOT NULL, - payload jsonb NOT NULL CHECK (jsonb_typeof(payload) = 'object'), - created_at timestamptz NOT NULL DEFAULT clock_timestamp(), - PRIMARY KEY (turn_id, ordinal), - FOREIGN KEY (session_id, turn_id) REFERENCES turns(session_id, id) -); - --- +goose Down -DROP TABLE turn_events; -ALTER TABLE turns DROP COLUMN event_bytes; -ALTER TABLE turns DROP COLUMN event_count; diff --git a/services/core/migrations/000008_session_items.sql b/services/core/migrations/000008_session_items.sql deleted file mode 100644 index c0e680bea..000000000 --- a/services/core/migrations/000008_session_items.sql +++ /dev/null @@ -1,26 +0,0 @@ --- +goose Up -ALTER TABLE turns ADD COLUMN items_indexed boolean NOT NULL DEFAULT false; -ALTER TABLE turns ALTER COLUMN items_indexed SET DEFAULT true; - -CREATE TABLE session_items ( - id uuid PRIMARY KEY, - session_id uuid NOT NULL, - turn_id uuid NOT NULL, - created_at timestamptz NOT NULL, - position integer NOT NULL DEFAULT 0, - payload jsonb NOT NULL CHECK (jsonb_typeof(payload) = 'object'), - FOREIGN KEY (session_id, turn_id) REFERENCES turns(session_id, id) -); -CREATE INDEX session_items_page_idx ON session_items(session_id, created_at, position, id); -CREATE INDEX session_items_turn_idx ON session_items(turn_id); - -CREATE INDEX turns_unindexed_items_idx ON turns(session_id, created_at, id) WHERE NOT items_indexed; -CREATE INDEX turn_inputs_item_history_idx ON turn_inputs(turn_id, created_at, sequence) WHERE kind = 'message'; -CREATE INDEX turn_events_item_history_idx ON turn_events(turn_id, created_at, ordinal); - --- +goose Down -DROP INDEX turn_events_item_history_idx; -DROP INDEX turn_inputs_item_history_idx; -DROP INDEX turns_unindexed_items_idx; -DROP TABLE session_items; -ALTER TABLE turns DROP COLUMN items_indexed; diff --git a/services/core/migrations/000009_execution_queue.sql b/services/core/migrations/000009_execution_queue.sql deleted file mode 100644 index 60bc6284e..000000000 --- a/services/core/migrations/000009_execution_queue.sql +++ /dev/null @@ -1,6 +0,0 @@ --- +goose Up -CREATE INDEX turns_execution_queue_idx ON turns (status, id) -WHERE status IN ('queued', 'in_progress', 'waiting'); - --- +goose Down -DROP INDEX turns_execution_queue_idx; diff --git a/services/core/migrations/000010_token_usage.sql b/services/core/migrations/000010_token_usage.sql deleted file mode 100644 index c9bad6629..000000000 --- a/services/core/migrations/000010_token_usage.sql +++ /dev/null @@ -1,5 +0,0 @@ --- +goose Up -ALTER TABLE turns ADD COLUMN token_usage jsonb; - --- +goose Down -ALTER TABLE turns DROP COLUMN token_usage; diff --git a/services/core/migrations/000011_item_order.sql b/services/core/migrations/000011_item_order.sql deleted file mode 100644 index 0b6ac41c2..000000000 --- a/services/core/migrations/000011_item_order.sql +++ /dev/null @@ -1,23 +0,0 @@ --- +goose Up -ALTER TABLE session_items ADD COLUMN output_index integer CHECK (output_index >= 0); - -WITH ordered AS ( - SELECT id, - (row_number() OVER (PARTITION BY session_id ORDER BY created_at, position, id) - 1)::integer AS item_position, - CASE WHEN payload->>'role' IS DISTINCT FROM 'user' THEN - (count(*) FILTER (WHERE payload->>'role' IS DISTINCT FROM 'user') OVER ( - PARTITION BY turn_id ORDER BY created_at, position, id ROWS UNBOUNDED PRECEDING - ) - 1)::integer - END AS output_index - FROM session_items -) -UPDATE session_items i SET position = ordered.item_position, output_index = ordered.output_index -FROM ordered WHERE i.id = ordered.id; - -CREATE UNIQUE INDEX session_items_position_idx ON session_items(session_id, position); -CREATE UNIQUE INDEX session_items_output_idx ON session_items(turn_id, output_index); - --- +goose Down -DROP INDEX session_items_output_idx; -DROP INDEX session_items_position_idx; -ALTER TABLE session_items DROP COLUMN output_index; diff --git a/services/core/migrations/000012_session_events.sql b/services/core/migrations/000012_session_events.sql deleted file mode 100644 index a23e6c664..000000000 --- a/services/core/migrations/000012_session_events.sql +++ /dev/null @@ -1,13 +0,0 @@ --- +goose Up -ALTER TABLE sessions ADD COLUMN event_sequence bigint NOT NULL DEFAULT 0; -CREATE TABLE session_events ( - session_id uuid NOT NULL REFERENCES sessions(id), - sequence bigint NOT NULL, - payload jsonb NOT NULL, - payload_bytes integer GENERATED ALWAYS AS (octet_length(payload::text)) STORED, - PRIMARY KEY (session_id, sequence) -); - --- +goose Down -DROP TABLE session_events; -ALTER TABLE sessions DROP COLUMN event_sequence; diff --git a/services/core/migrations/000013_function_calls.sql b/services/core/migrations/000013_function_calls.sql deleted file mode 100644 index f514e456b..000000000 --- a/services/core/migrations/000013_function_calls.sql +++ /dev/null @@ -1,21 +0,0 @@ --- +goose Up -CREATE TABLE function_calls ( - session_id uuid NOT NULL, - turn_id uuid NOT NULL, - call_id text NOT NULL, - executor_call_id text NOT NULL, - name text NOT NULL, - arguments jsonb NOT NULL, - result jsonb, - applied boolean NOT NULL DEFAULT false, - created_at timestamptz NOT NULL DEFAULT clock_timestamp(), - PRIMARY KEY (session_id, turn_id, call_id), - UNIQUE (session_id, turn_id, executor_call_id), - FOREIGN KEY (session_id, turn_id) REFERENCES turns(session_id, id), - CHECK (result IS NULL OR jsonb_typeof(result) = 'object'), - CHECK (NOT applied OR result IS NOT NULL) -); -CREATE INDEX function_calls_pending_idx ON function_calls(session_id, turn_id) WHERE NOT applied; - --- +goose Down -DROP TABLE function_calls; diff --git a/services/core/migrations/000014_function_inputs.sql b/services/core/migrations/000014_function_inputs.sql deleted file mode 100644 index e185df786..000000000 --- a/services/core/migrations/000014_function_inputs.sql +++ /dev/null @@ -1,13 +0,0 @@ --- +goose Up -ALTER TABLE turn_inputs DROP CONSTRAINT turn_inputs_kind_check; -ALTER TABLE turn_inputs ADD CONSTRAINT turn_inputs_kind_check - CHECK (kind IN ('message', 'cancel', 'tool_result')); -ALTER TABLE turn_inputs ADD CONSTRAINT turn_inputs_function_turn_check - CHECK (kind <> 'tool_result' OR turn_id IS NOT NULL); - --- +goose Down --- Refuse a downgrade with saved function inputs rather than delete retry history. -ALTER TABLE turn_inputs DROP CONSTRAINT turn_inputs_kind_check; -ALTER TABLE turn_inputs ADD CONSTRAINT turn_inputs_kind_check - CHECK (kind IN ('message', 'cancel')); -ALTER TABLE turn_inputs DROP CONSTRAINT turn_inputs_function_turn_check; diff --git a/services/core/migrations/000015_retire_item_backfill.sql b/services/core/migrations/000015_retire_item_backfill.sql deleted file mode 100644 index 16ad3e312..000000000 --- a/services/core/migrations/000015_retire_item_backfill.sql +++ /dev/null @@ -1,16 +0,0 @@ --- +goose Up -LOCK TABLE turns IN ACCESS EXCLUSIVE MODE; --- +goose StatementBegin -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM turns WHERE NOT items_indexed) THEN - RAISE EXCEPTION 'Unindexed Agents API history: prepare all Session Items with release 906069e before upgrading; see services/agents-api/README.md'; - END IF; -END -$$; --- +goose StatementEnd -ALTER TABLE turns DROP COLUMN items_indexed; - --- +goose Down -ALTER TABLE turns ADD COLUMN items_indexed boolean NOT NULL DEFAULT true; -CREATE INDEX turns_unindexed_items_idx ON turns(session_id, created_at, id) WHERE NOT items_indexed; diff --git a/services/core/migrations/000016_agents.sql b/services/core/migrations/000016_agents.sql deleted file mode 100644 index 0a72c5b91..000000000 --- a/services/core/migrations/000016_agents.sql +++ /dev/null @@ -1,13 +0,0 @@ --- +goose Up -CREATE TABLE agents ( - id uuid PRIMARY KEY, - tenant_id uuid NOT NULL, - metadata jsonb NOT NULL CHECK (jsonb_typeof(metadata) = 'object'), - configuration jsonb NOT NULL CHECK (jsonb_typeof(configuration) = 'object'), - created_at timestamptz NOT NULL DEFAULT statement_timestamp(), - updated_at timestamptz NOT NULL DEFAULT statement_timestamp() -); -CREATE INDEX agents_tenant_created_idx ON agents (tenant_id, created_at DESC, id DESC); - --- +goose Down -DROP TABLE agents; diff --git a/services/core/migrations/000017_session_creation_identity.sql b/services/core/migrations/000017_session_creation_identity.sql deleted file mode 100644 index d2a72e176..000000000 --- a/services/core/migrations/000017_session_creation_identity.sql +++ /dev/null @@ -1,6 +0,0 @@ --- +goose Up -ALTER TABLE sessions ADD COLUMN creation_request_hash text - CHECK (creation_request_hash ~ '^[0-9a-f]{64}$'); - --- +goose Down -ALTER TABLE sessions DROP COLUMN creation_request_hash; diff --git a/services/core/migrations/000018_session_agent_filter.sql b/services/core/migrations/000018_session_agent_filter.sql deleted file mode 100644 index acad6ec8c..000000000 --- a/services/core/migrations/000018_session_agent_filter.sql +++ /dev/null @@ -1,6 +0,0 @@ --- +goose Up -CREATE INDEX sessions_tenant_agent_created_idx -ON sessions (tenant_id, (configuration #>> '{agent,id}'), created_at DESC, id DESC); - --- +goose Down -DROP INDEX sessions_tenant_agent_created_idx; diff --git a/services/core/migrations/000019_session_deletion.sql b/services/core/migrations/000019_session_deletion.sql deleted file mode 100644 index ec0388c3b..000000000 --- a/services/core/migrations/000019_session_deletion.sql +++ /dev/null @@ -1,13 +0,0 @@ --- +goose Up -ALTER TABLE sessions ADD COLUMN deleted_at timestamptz; - --- +goose Down --- +goose StatementBegin -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM sessions WHERE deleted_at IS NOT NULL) THEN - RAISE EXCEPTION 'Cannot remove Session deletion markers while deleted Sessions exist'; - END IF; -END $$; --- +goose StatementEnd -ALTER TABLE sessions DROP COLUMN deleted_at; diff --git a/services/core/migrations/000020_environments.sql b/services/core/migrations/000020_environments.sql deleted file mode 100644 index 91a41dd7d..000000000 --- a/services/core/migrations/000020_environments.sql +++ /dev/null @@ -1,20 +0,0 @@ --- +goose Up -CREATE TABLE environments ( - id uuid PRIMARY KEY, - session_id uuid NOT NULL UNIQUE REFERENCES sessions(id), - status text NOT NULL DEFAULT 'pending' - CHECK (status IN ('pending', 'connected', 'disconnected', 'expired', 'failed')), - created_at timestamptz NOT NULL DEFAULT clock_timestamp() -); - --- +goose Down -LOCK TABLE environments IN ACCESS EXCLUSIVE MODE; --- +goose StatementBegin -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM environments) THEN - RAISE EXCEPTION 'Cannot remove durable Environment identities while Environments exist'; - END IF; -END $$; --- +goose StatementEnd -DROP TABLE environments; diff --git a/services/core/migrations/000021_environment_executor_credentials.sql b/services/core/migrations/000021_environment_executor_credentials.sql deleted file mode 100644 index aa2aa38c5..000000000 --- a/services/core/migrations/000021_environment_executor_credentials.sql +++ /dev/null @@ -1,10 +0,0 @@ --- +goose Up -CREATE TABLE environment_executor_credentials ( - environment_id uuid PRIMARY KEY REFERENCES environments(id), - token_sha256 text NOT NULL UNIQUE CHECK (token_sha256 ~ '^[0-9a-f]{64}$'), - issued_at timestamptz NOT NULL DEFAULT clock_timestamp(), - revoked_at timestamptz -); - --- +goose Down -DROP TABLE environment_executor_credentials; diff --git a/services/core/migrations/000022_environment_input_reservations.sql b/services/core/migrations/000022_environment_input_reservations.sql deleted file mode 100644 index ac7ae1dbf..000000000 --- a/services/core/migrations/000022_environment_input_reservations.sql +++ /dev/null @@ -1,29 +0,0 @@ --- +goose Up -CREATE TABLE environment_input_reservations ( - id uuid PRIMARY KEY, - session_id uuid NOT NULL REFERENCES environments(session_id), - idempotency_key text NOT NULL, - batch jsonb NOT NULL CHECK (CASE WHEN jsonb_typeof(batch) = 'array' - THEN jsonb_array_length(batch) BETWEEN 1 AND 64 ELSE false END), - state text NOT NULL DEFAULT 'pending' - CHECK (state IN ('pending', 'admitted', 'expired', 'cancelled')), - created_at timestamptz NOT NULL, - deadline timestamptz NOT NULL, - settled_at timestamptz, - UNIQUE (session_id, idempotency_key), - CHECK ((settled_at IS NOT NULL) = (state <> 'pending')) -); -CREATE UNIQUE INDEX environment_input_reservations_one_pending_idx - ON environment_input_reservations(session_id) WHERE state = 'pending'; - --- +goose Down -LOCK TABLE environment_input_reservations IN ACCESS EXCLUSIVE MODE; --- +goose StatementBegin -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM environment_input_reservations) THEN - RAISE EXCEPTION 'Cannot remove durable Environment input identities while reservations exist'; - END IF; -END $$; --- +goose StatementEnd -DROP TABLE environment_input_reservations; diff --git a/services/core/migrations/000023_environment_input_expiry_index.sql b/services/core/migrations/000023_environment_input_expiry_index.sql deleted file mode 100644 index 1054e6d32..000000000 --- a/services/core/migrations/000023_environment_input_expiry_index.sql +++ /dev/null @@ -1,6 +0,0 @@ --- +goose Up -CREATE INDEX environment_input_reservations_pending_deadline_idx - ON environment_input_reservations(deadline, id) WHERE state = 'pending'; - --- +goose Down -DROP INDEX environment_input_reservations_pending_deadline_idx; diff --git a/services/core/migrations/000024_execution_project_scopes.sql b/services/core/migrations/000024_execution_project_scopes.sql deleted file mode 100644 index 479fb3099..000000000 --- a/services/core/migrations/000024_execution_project_scopes.sql +++ /dev/null @@ -1,10 +0,0 @@ --- +goose Up -CREATE TABLE execution_project_scopes ( - tenant_id uuid PRIMARY KEY CHECK (tenant_id <> '00000000-0000-0000-0000-000000000000'), - organization_id text NOT NULL CHECK (organization_id <> ''), - project_id text NOT NULL CHECK (project_id <> ''), - UNIQUE (organization_id, project_id) -); - --- +goose Down -DROP TABLE execution_project_scopes; diff --git a/services/core/migrations/000025_session_creators.sql b/services/core/migrations/000025_session_creators.sql deleted file mode 100644 index f1b7b60ec..000000000 --- a/services/core/migrations/000025_session_creators.sql +++ /dev/null @@ -1,15 +0,0 @@ --- +goose Up -ALTER TABLE sessions - ADD COLUMN creator_kind text, - ADD COLUMN creator_id text, - ADD CONSTRAINT sessions_creator_complete CHECK ( - (creator_kind IS NULL AND creator_id IS NULL) - OR (creator_kind IS NOT NULL AND creator_id IS NOT NULL - AND creator_kind IN ('user', 'service_account') AND creator_id <> '') - ); - --- +goose Down -ALTER TABLE sessions - DROP CONSTRAINT sessions_creator_complete, - DROP COLUMN creator_kind, - DROP COLUMN creator_id; diff --git a/services/core/migrations/000026_executor_principals.sql b/services/core/migrations/000026_executor_principals.sql deleted file mode 100644 index 57699d6db..000000000 --- a/services/core/migrations/000026_executor_principals.sql +++ /dev/null @@ -1,43 +0,0 @@ --- +goose Up -ALTER TABLE environment_executor_credentials - ADD COLUMN key_id uuid, - ADD COLUMN tenant_id uuid REFERENCES execution_project_scopes(tenant_id), - ADD COLUMN subject_kind text, - ADD COLUMN subject_id text, - ADD COLUMN created_at timestamptz; - -UPDATE environment_executor_credentials -SET key_id = environment_id, revoked_at = COALESCE(revoked_at, clock_timestamp()); - -ALTER TABLE environment_executor_credentials - DROP CONSTRAINT environment_executor_credentials_pkey, - ALTER COLUMN environment_id DROP NOT NULL, - ALTER COLUMN key_id SET NOT NULL, - ADD PRIMARY KEY (key_id), - ADD CONSTRAINT executor_principal_complete CHECK ( - (tenant_id IS NULL AND subject_kind IS NULL AND subject_id IS NULL - AND environment_id IS NOT NULL AND revoked_at IS NOT NULL AND created_at IS NULL) - OR (tenant_id IS NOT NULL AND subject_kind IS NOT NULL AND subject_id IS NOT NULL - AND subject_kind IN ('user', 'service_account') AND subject_id <> '' AND created_at IS NOT NULL) - ); - --- +goose Down -LOCK TABLE environment_executor_credentials IN ACCESS EXCLUSIVE MODE; --- +goose StatementBegin -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM environment_executor_credentials WHERE tenant_id IS NOT NULL) THEN - RAISE EXCEPTION 'Cannot remove durable executor principal identities while principal keys exist'; - END IF; -END $$; --- +goose StatementEnd -ALTER TABLE environment_executor_credentials - DROP CONSTRAINT executor_principal_complete, - DROP CONSTRAINT environment_executor_credentials_pkey, - ALTER COLUMN environment_id SET NOT NULL, - ADD PRIMARY KEY (environment_id), - DROP COLUMN key_id, - DROP COLUMN tenant_id, - DROP COLUMN subject_kind, - DROP COLUMN subject_id, - DROP COLUMN created_at; diff --git a/services/core/migrations/000027_environment_connections.sql b/services/core/migrations/000027_environment_connections.sql deleted file mode 100644 index 80633e958..000000000 --- a/services/core/migrations/000027_environment_connections.sql +++ /dev/null @@ -1,18 +0,0 @@ --- +goose Up -CREATE TABLE environment_connections ( - environment_id uuid PRIMARY KEY REFERENCES environments(id), - generation uuid NOT NULL, - revision bigint NOT NULL DEFAULT 0 CHECK (revision >= 0) -); - --- +goose Down -LOCK TABLE environment_connections IN ACCESS EXCLUSIVE MODE; --- +goose StatementBegin -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM environment_connections) THEN - RAISE EXCEPTION 'Cannot remove active Environment observation fencing'; - END IF; -END $$; --- +goose StatementEnd -DROP TABLE environment_connections; diff --git a/services/core/migrations/000028_environment_input_activity.sql b/services/core/migrations/000028_environment_input_activity.sql deleted file mode 100644 index 9de50b52a..000000000 --- a/services/core/migrations/000028_environment_input_activity.sql +++ /dev/null @@ -1,6 +0,0 @@ --- +goose Up -CREATE INDEX environment_input_reservations_latest_idx -ON environment_input_reservations(session_id, created_at DESC, id DESC); - --- +goose Down -DROP INDEX environment_input_reservations_latest_idx; diff --git a/services/core/migrations/000029_environment_initial_input.sql b/services/core/migrations/000029_environment_initial_input.sql deleted file mode 100644 index d846250ba..000000000 --- a/services/core/migrations/000029_environment_initial_input.sql +++ /dev/null @@ -1,15 +0,0 @@ --- +goose Up -ALTER TABLE environment_input_reservations -ADD COLUMN is_initial boolean NOT NULL DEFAULT false; - --- +goose Down -LOCK TABLE environment_input_reservations IN ACCESS EXCLUSIVE MODE; --- +goose StatementBegin -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM environment_input_reservations WHERE is_initial) THEN - RAISE EXCEPTION 'Cannot remove initial Environment input origin while initial reservations exist'; - END IF; -END $$; --- +goose StatementEnd -ALTER TABLE environment_input_reservations DROP COLUMN is_initial; diff --git a/services/core/migrations/000030_vaults.sql b/services/core/migrations/000030_vaults.sql deleted file mode 100644 index 5f3f7c439..000000000 --- a/services/core/migrations/000030_vaults.sql +++ /dev/null @@ -1,11 +0,0 @@ --- +goose Up -CREATE TABLE vaults ( - id uuid PRIMARY KEY, - tenant_id uuid NOT NULL, - name text CHECK (name IS NULL OR octet_length(name) BETWEEN 1 AND 256), - metadata jsonb NOT NULL CHECK (jsonb_typeof(metadata) = 'object'), - created_at timestamptz NOT NULL DEFAULT statement_timestamp() -); - --- +goose Down -DROP TABLE vaults; diff --git a/services/core/migrations/000031_vault_credentials.sql b/services/core/migrations/000031_vault_credentials.sql deleted file mode 100644 index 65e87e3bb..000000000 --- a/services/core/migrations/000031_vault_credentials.sql +++ /dev/null @@ -1,14 +0,0 @@ --- +goose Up -CREATE TABLE vault_credentials ( - id uuid PRIMARY KEY, - vault_id uuid NOT NULL REFERENCES vaults(id) ON DELETE CASCADE, - name text NOT NULL CHECK (octet_length(name) BETWEEN 1 AND 256), - auth_type text NOT NULL CHECK (auth_type = 'static_bearer'), - mcp_server_url text NOT NULL, - token_ciphertext bytea NOT NULL CHECK (octet_length(token_ciphertext) > 0), - created_at timestamptz NOT NULL DEFAULT statement_timestamp(), - updated_at timestamptz NOT NULL DEFAULT statement_timestamp() -); - --- +goose Down -DROP TABLE vault_credentials; diff --git a/services/core/migrations/000032_vault_status.sql b/services/core/migrations/000032_vault_status.sql deleted file mode 100644 index 30a2de62b..000000000 --- a/services/core/migrations/000032_vault_status.sql +++ /dev/null @@ -1,16 +0,0 @@ --- +goose Up -ALTER TABLE vaults ADD COLUMN status text NOT NULL DEFAULT 'active' - CHECK (status IN ('active', 'archived')); -CREATE INDEX vaults_tenant_created_id_idx ON vaults (tenant_id, created_at, id); - --- +goose Down --- +goose StatementBegin -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM vaults WHERE status = 'archived') THEN - RAISE EXCEPTION 'Cannot remove archived Vault classification'; - END IF; -END $$; --- +goose StatementEnd -DROP INDEX vaults_tenant_created_id_idx; -ALTER TABLE vaults DROP COLUMN status; diff --git a/services/core/migrations/000033_credential_status.sql b/services/core/migrations/000033_credential_status.sql deleted file mode 100644 index d3a315c29..000000000 --- a/services/core/migrations/000033_credential_status.sql +++ /dev/null @@ -1,17 +0,0 @@ --- +goose Up -ALTER TABLE vault_credentials ADD COLUMN status text NOT NULL DEFAULT 'active' - CHECK (status IN ('active', 'archived')); -CREATE INDEX vault_credentials_vault_created_id_idx ON vault_credentials (vault_id, created_at, id); - --- +goose Down --- +goose StatementBegin -DO $$ -BEGIN - LOCK TABLE vault_credentials IN ACCESS EXCLUSIVE MODE; - IF EXISTS (SELECT 1 FROM vault_credentials WHERE status = 'archived') THEN - RAISE EXCEPTION 'Cannot remove archived Credential classification'; - END IF; -END $$; --- +goose StatementEnd -DROP INDEX vault_credentials_vault_created_id_idx; -ALTER TABLE vault_credentials DROP COLUMN status; diff --git a/services/core/migrations/000034_subagent_identities.sql b/services/core/migrations/000034_subagent_identities.sql deleted file mode 100644 index 300ca88d9..000000000 --- a/services/core/migrations/000034_subagent_identities.sql +++ /dev/null @@ -1,19 +0,0 @@ --- +goose Up -CREATE TABLE subagent_identities ( - id uuid PRIMARY KEY, - session_id uuid NOT NULL REFERENCES sessions(id), - device_id uuid NOT NULL REFERENCES devices(id), - engine text NOT NULL, - native_id text NOT NULL CHECK (native_id <> ''), - parent_native_id text NOT NULL CHECK (parent_native_id <> '' AND parent_native_id <> native_id), - native_created_at bigint NOT NULL CHECK (native_created_at > 0), - first_turn_id uuid NOT NULL, - first_event_ordinal integer NOT NULL, - UNIQUE (device_id, engine, native_id), - UNIQUE (session_id, native_id), - FOREIGN KEY (session_id, first_turn_id) REFERENCES turns(session_id, id), - FOREIGN KEY (first_turn_id, first_event_ordinal) REFERENCES turn_events(turn_id, ordinal) -); - --- +goose Down -DROP TABLE subagent_identities; diff --git a/services/core/migrations/000035_local_environment_devices.sql b/services/core/migrations/000035_local_environment_devices.sql deleted file mode 100644 index a473681bf..000000000 --- a/services/core/migrations/000035_local_environment_devices.sql +++ /dev/null @@ -1,14 +0,0 @@ --- +goose Up -ALTER TABLE devices ADD COLUMN environment_id uuid UNIQUE REFERENCES environments(id); - --- +goose Down -LOCK TABLE devices IN ACCESS EXCLUSIVE MODE; --- +goose StatementBegin -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM devices WHERE environment_id IS NOT NULL) THEN - RAISE EXCEPTION 'Cannot discard dedicated Environment device authority'; - END IF; -END $$; --- +goose StatementEnd -ALTER TABLE devices DROP COLUMN environment_id; diff --git a/services/core/migrations/000036_environment_file_writes.sql b/services/core/migrations/000036_environment_file_writes.sql deleted file mode 100644 index 4c8a04f19..000000000 --- a/services/core/migrations/000036_environment_file_writes.sql +++ /dev/null @@ -1,25 +0,0 @@ --- +goose Up -CREATE TABLE environment_file_writes ( - id uuid PRIMARY KEY, - environment_id uuid NOT NULL REFERENCES environments(id), - device_id uuid NOT NULL REFERENCES devices(id), - request_sha256 text NOT NULL CHECK (request_sha256 ~ '^[0-9a-f]{64}$'), - state text NOT NULL DEFAULT 'pending' CHECK (state IN ('pending', 'committed', 'rejected')), - created_at timestamptz NOT NULL DEFAULT clock_timestamp(), - settled_at timestamptz, - CHECK ((state = 'pending') = (settled_at IS NULL)) -); -CREATE UNIQUE INDEX environment_file_writes_pending ON environment_file_writes(environment_id) - WHERE state = 'pending'; - --- +goose Down -LOCK TABLE environment_file_writes IN ACCESS EXCLUSIVE MODE; --- +goose StatementBegin -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM environment_file_writes) THEN - RAISE EXCEPTION 'Cannot remove durable Environment file write identities while writes exist'; - END IF; -END $$; --- +goose StatementEnd -DROP TABLE environment_file_writes; diff --git a/services/core/migrations/000037_source_files.sql b/services/core/migrations/000037_source_files.sql deleted file mode 100644 index 68d1f6220..000000000 --- a/services/core/migrations/000037_source_files.sql +++ /dev/null @@ -1,21 +0,0 @@ --- +goose Up -CREATE TABLE source_files ( - id uuid PRIMARY KEY, - tenant_id uuid NOT NULL, - filename text NOT NULL CHECK (octet_length(filename) BETWEEN 1 AND 1024), - purpose text NOT NULL CHECK (purpose = 'user_data'), - body_oid oid NOT NULL UNIQUE, - size_bytes bigint NOT NULL CHECK (size_bytes BETWEEN 0 AND 536870912), - sha256 text NOT NULL CHECK (sha256 ~ '^[0-9a-f]{64}$'), - created_at timestamptz NOT NULL DEFAULT statement_timestamp() -); - --- +goose Down --- +goose StatementBegin -DO $$ BEGIN - IF EXISTS (SELECT 1 FROM source_files) THEN - RAISE EXCEPTION 'delete source files through the service before downgrade'; - END IF; -END $$; --- +goose StatementEnd -DROP TABLE source_files; diff --git a/services/core/migrations/000038_runtime_allocations.sql b/services/core/migrations/000038_runtime_allocations.sql deleted file mode 100644 index 3921fefc5..000000000 --- a/services/core/migrations/000038_runtime_allocations.sql +++ /dev/null @@ -1,27 +0,0 @@ --- +goose Up -CREATE TABLE runtime_allocations ( - id uuid PRIMARY KEY, - environment_id uuid NOT NULL UNIQUE REFERENCES environments(id), - device_id uuid NOT NULL UNIQUE REFERENCES devices(id), - provider_key uuid NOT NULL, - state text NOT NULL DEFAULT 'creating' - CHECK (state IN ('creating', 'running', 'cleanup_pending', 'released')), - create_settled boolean NOT NULL DEFAULT false, - created_at timestamptz NOT NULL DEFAULT clock_timestamp(), - kept_at timestamptz NOT NULL DEFAULT clock_timestamp(), - released_at timestamptz, - CHECK ((state = 'released') = (released_at IS NOT NULL)), - CHECK (state <> 'released' OR create_settled) -); - --- +goose Down -LOCK TABLE runtime_allocations IN ACCESS EXCLUSIVE MODE; --- +goose StatementBegin -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM runtime_allocations) THEN - RAISE EXCEPTION 'Cannot discard managed Runtime allocation and cleanup identities'; - END IF; -END $$; --- +goose StatementEnd -DROP TABLE runtime_allocations; diff --git a/services/core/migrations/000039_environment_input_failure.sql b/services/core/migrations/000039_environment_input_failure.sql deleted file mode 100644 index 10b6b24a8..000000000 --- a/services/core/migrations/000039_environment_input_failure.sql +++ /dev/null @@ -1,10 +0,0 @@ --- +goose Up -ALTER TABLE environment_input_reservations DROP CONSTRAINT environment_input_reservations_state_check; -ALTER TABLE environment_input_reservations ADD CONSTRAINT environment_input_reservations_state_check - CHECK (state IN ('pending', 'admitted', 'expired', 'cancelled', 'failed')); - --- +goose Down --- The constraint refuses rollback while failed outcomes still need to be retained. -ALTER TABLE environment_input_reservations DROP CONSTRAINT environment_input_reservations_state_check; -ALTER TABLE environment_input_reservations ADD CONSTRAINT environment_input_reservations_state_check - CHECK (state IN ('pending', 'admitted', 'expired', 'cancelled')); diff --git a/services/core/migrations/000040_session_artifacts.sql b/services/core/migrations/000040_session_artifacts.sql deleted file mode 100644 index ac739682c..000000000 --- a/services/core/migrations/000040_session_artifacts.sql +++ /dev/null @@ -1,28 +0,0 @@ --- +goose Up -ALTER TABLE turns ADD COLUMN artifact_capture_started boolean NOT NULL DEFAULT false; -CREATE TABLE session_artifacts ( - id uuid PRIMARY KEY, - session_id uuid NOT NULL REFERENCES sessions(id), - turn_id uuid NOT NULL, - environment_id uuid NOT NULL REFERENCES environments(id), - path text NOT NULL CHECK (octet_length(path) BETWEEN 20 AND 4110), - size_bytes bigint NOT NULL CHECK (size_bytes BETWEEN 0 AND 209715200), - body_oid oid NOT NULL UNIQUE, - sha256 text NOT NULL CHECK (sha256 ~ '^[0-9a-f]{64}$'), - created_at timestamptz, - UNIQUE (turn_id, path), - FOREIGN KEY (session_id, turn_id) REFERENCES turns(session_id, id) -); -CREATE INDEX session_artifacts_page_idx ON session_artifacts (session_id, created_at DESC, id DESC) -WHERE created_at IS NOT NULL; - --- +goose Down --- +goose StatementBegin -DO $$ BEGIN - IF EXISTS (SELECT 1 FROM session_artifacts) THEN - RAISE EXCEPTION 'delete session artifacts through the service before downgrade'; - END IF; -END $$; --- +goose StatementEnd -DROP TABLE session_artifacts; -ALTER TABLE turns DROP COLUMN artifact_capture_started; diff --git a/services/core/migrations/000041_source_files_list.sql b/services/core/migrations/000041_source_files_list.sql deleted file mode 100644 index 2a645f287..000000000 --- a/services/core/migrations/000041_source_files_list.sql +++ /dev/null @@ -1,5 +0,0 @@ --- +goose Up -CREATE INDEX source_files_page_idx ON source_files (tenant_id, created_at DESC, id DESC); - --- +goose Down -DROP INDEX source_files_page_idx; diff --git a/services/core/migrations/000042_environment_templates.sql b/services/core/migrations/000042_environment_templates.sql deleted file mode 100644 index fb772148d..000000000 --- a/services/core/migrations/000042_environment_templates.sql +++ /dev/null @@ -1,13 +0,0 @@ --- +goose Up -CREATE TABLE environment_templates ( - id uuid PRIMARY KEY, - tenant_id uuid NOT NULL, - name text CHECK (name IS NULL OR char_length(name) BETWEEN 1 AND 256), - network_access text NOT NULL CHECK (network_access IN ('enabled', 'disabled')), - created_at timestamptz NOT NULL DEFAULT statement_timestamp(), - updated_at timestamptz NOT NULL DEFAULT statement_timestamp() -); -CREATE INDEX environment_templates_tenant_order ON environment_templates (tenant_id, created_at, id); - --- +goose Down -DROP TABLE environment_templates; diff --git a/services/core/migrations/000043_environment_initial_files.sql b/services/core/migrations/000043_environment_initial_files.sql deleted file mode 100644 index 66d29483f..000000000 --- a/services/core/migrations/000043_environment_initial_files.sql +++ /dev/null @@ -1,20 +0,0 @@ --- +goose Up -ALTER TABLE environment_templates ADD COLUMN files jsonb NOT NULL DEFAULT '[]'::jsonb CHECK (jsonb_typeof(files) = 'array'); -ALTER TABLE environment_templates ADD COLUMN file_contents bytea; -CREATE TABLE initial_environment_files ( - id uuid PRIMARY KEY, - session_id uuid NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, - position integer NOT NULL CHECK (position BETWEEN 0 AND 49), - path text NOT NULL, - size_bytes bigint NOT NULL CHECK (size_bytes BETWEEN 0 AND 52428800), - contents bytea NOT NULL, - UNIQUE (session_id, position), - UNIQUE (session_id, path) -); -ALTER TABLE runtime_allocations ADD COLUMN initialization text NOT NULL DEFAULT 'complete' - CHECK (initialization IN ('pending', 'running', 'complete')); - --- +goose Down -ALTER TABLE runtime_allocations DROP COLUMN initialization; -DROP TABLE initial_environment_files; -ALTER TABLE environment_templates DROP COLUMN file_contents, DROP COLUMN files; diff --git a/services/core/migrations/000044_environment_setup.sql b/services/core/migrations/000044_environment_setup.sql deleted file mode 100644 index 859a7471d..000000000 --- a/services/core/migrations/000044_environment_setup.sql +++ /dev/null @@ -1,13 +0,0 @@ --- +goose Up -ALTER TABLE environment_templates - ADD COLUMN packages jsonb NOT NULL DEFAULT '{"npm":[],"python":[],"system":[]}'::jsonb CHECK (jsonb_typeof(packages) = 'object'), - ADD COLUMN env_contents bytea, - ADD COLUMN setup_contents bytea; -CREATE TABLE environment_setups ( - session_id uuid PRIMARY KEY REFERENCES sessions(id) ON DELETE CASCADE, - contents bytea NOT NULL -); - --- +goose Down -DROP TABLE environment_setups; -ALTER TABLE environment_templates DROP COLUMN packages, DROP COLUMN env_contents, DROP COLUMN setup_contents; diff --git a/services/core/migrations/000045_environment_skills.sql b/services/core/migrations/000045_environment_skills.sql deleted file mode 100644 index dae32c831..000000000 --- a/services/core/migrations/000045_environment_skills.sql +++ /dev/null @@ -1,7 +0,0 @@ --- +goose Up -ALTER TABLE environment_templates - ADD COLUMN skills jsonb NOT NULL DEFAULT '[]'::jsonb, - ADD COLUMN skill_contents bytea; - --- +goose Down -ALTER TABLE environment_templates DROP COLUMN skill_contents, DROP COLUMN skills; diff --git a/services/core/migrations/000046_session_model_execution.sql b/services/core/migrations/000046_session_model_execution.sql deleted file mode 100644 index d43707456..000000000 --- a/services/core/migrations/000046_session_model_execution.sql +++ /dev/null @@ -1,8 +0,0 @@ --- +goose Up -CREATE TABLE session_model_execution ( - session_id uuid PRIMARY KEY REFERENCES sessions(id) ON DELETE CASCADE, - encrypted_config bytea NOT NULL -); - --- +goose Down -DROP TABLE session_model_execution; diff --git a/services/core/migrations/000047_environment_network_domains.sql b/services/core/migrations/000047_environment_network_domains.sql deleted file mode 100644 index b9e953444..000000000 --- a/services/core/migrations/000047_environment_network_domains.sql +++ /dev/null @@ -1,21 +0,0 @@ --- +goose Up -ALTER TABLE environment_templates - DROP CONSTRAINT environment_templates_network_access_check, - ADD COLUMN network_allowed_domains text[] NOT NULL DEFAULT '{}', - ADD CONSTRAINT environment_templates_network_access_check - CHECK (network_access IN ('enabled', 'disabled', 'restricted')), - ADD CONSTRAINT environment_templates_network_domains_check - CHECK ((network_access = 'restricted' AND cardinality(network_allowed_domains) BETWEEN 1 AND 100) - OR (network_access IN ('enabled', 'disabled') AND cardinality(network_allowed_domains) = 0)); - --- +goose Down --- Refuse rollback while restricted templates exist instead of broadening authority. -ALTER TABLE environment_templates - ADD CONSTRAINT environment_templates_legacy_network_check - CHECK (network_access IN ('enabled', 'disabled')); -ALTER TABLE environment_templates - DROP CONSTRAINT environment_templates_network_domains_check, - DROP CONSTRAINT environment_templates_network_access_check, - DROP COLUMN network_allowed_domains; -ALTER TABLE environment_templates - RENAME CONSTRAINT environment_templates_legacy_network_check TO environment_templates_network_access_check; diff --git a/services/core/migrations/000048_skills.sql b/services/core/migrations/000048_skills.sql deleted file mode 100644 index e9adef44d..000000000 --- a/services/core/migrations/000048_skills.sql +++ /dev/null @@ -1,37 +0,0 @@ --- +goose Up -CREATE TABLE skills ( - id uuid PRIMARY KEY, - tenant_id uuid NOT NULL, - name text NOT NULL, - description text NOT NULL, - created_at timestamptz NOT NULL DEFAULT clock_timestamp(), - default_version bigint NOT NULL CHECK (default_version > 0), - latest_version bigint NOT NULL CHECK (latest_version > 0), - next_version bigint NOT NULL CHECK (next_version > latest_version), - UNIQUE (tenant_id, id) -); -CREATE INDEX skills_tenant_created ON skills (tenant_id, created_at, id); - -CREATE TABLE skill_versions ( - id uuid PRIMARY KEY, - tenant_id uuid NOT NULL, - skill_id uuid NOT NULL, - version bigint NOT NULL CHECK (version > 0), - name text NOT NULL, - description text NOT NULL, - created_at timestamptz NOT NULL DEFAULT clock_timestamp(), - contents bytea NOT NULL, - UNIQUE (skill_id, version), - FOREIGN KEY (tenant_id, skill_id) REFERENCES skills (tenant_id, id) ON DELETE CASCADE -); -ALTER TABLE skills ADD CONSTRAINT skills_default_version - FOREIGN KEY (id, default_version) REFERENCES skill_versions (skill_id, version) - DEFERRABLE INITIALLY DEFERRED; -ALTER TABLE skills ADD CONSTRAINT skills_latest_version - FOREIGN KEY (id, latest_version) REFERENCES skill_versions (skill_id, version) - DEFERRABLE INITIALLY DEFERRED; - --- +goose Down -ALTER TABLE skills DROP CONSTRAINT skills_default_version, DROP CONSTRAINT skills_latest_version; -DROP TABLE skill_versions; -DROP TABLE skills; diff --git a/services/core/migrations/000049_environment_plugins.sql b/services/core/migrations/000049_environment_plugins.sql deleted file mode 100644 index 36fb04d99..000000000 --- a/services/core/migrations/000049_environment_plugins.sql +++ /dev/null @@ -1,11 +0,0 @@ --- +goose Up -ALTER TABLE environment_templates - ADD COLUMN plugins jsonb NOT NULL DEFAULT '[]'::jsonb, - ADD COLUMN plugin_contents bytea, - ADD COLUMN capability_directories text[] NOT NULL DEFAULT '{}'; - --- +goose Down -ALTER TABLE environment_templates - DROP COLUMN capability_directories, - DROP COLUMN plugin_contents, - DROP COLUMN plugins; diff --git a/services/core/migrations/000050_runtime_enrollment.sql b/services/core/migrations/000050_runtime_enrollment.sql deleted file mode 100644 index b263e5d79..000000000 --- a/services/core/migrations/000050_runtime_enrollment.sql +++ /dev/null @@ -1,40 +0,0 @@ --- +goose Up -ALTER TABLE devices - ADD COLUMN executor_key_id uuid REFERENCES environment_executor_credentials(key_id), - ALTER COLUMN credential_hash DROP NOT NULL, - ADD CONSTRAINT device_credential_source CHECK ( - (executor_key_id IS NULL AND credential_hash IS NOT NULL) - OR (executor_key_id IS NOT NULL AND credential_hash IS NULL AND environment_id IS NOT NULL) - ); - --- Resolve the current credential once for bootstrap, reconnect and heartbeat. -CREATE VIEW runtime_device_authority AS -SELECT d.id, d.tenant_id, d.name, d.environment_id, - COALESCE(c.token_sha256, d.credential_hash) AS credential_hash -FROM devices d -LEFT JOIN environments e ON e.id = d.environment_id -LEFT JOIN sessions s ON s.id = e.session_id AND s.tenant_id = d.tenant_id -LEFT JOIN environment_executor_credentials c ON c.key_id = d.executor_key_id -WHERE d.revoked_at IS NULL - AND (d.environment_id IS NULL OR (s.id IS NOT NULL AND s.deleted_at IS NULL)) - AND (d.executor_key_id IS NULL OR ( - c.revoked_at IS NULL AND c.tenant_id = s.tenant_id - AND c.subject_kind = s.creator_kind AND c.subject_id = s.creator_id - AND (c.environment_id IS NULL OR c.environment_id = e.id) - AND s.configuration->'environment'->>'type' = 'self_hosted' - AND e.status NOT IN ('failed', 'expired') - AND EXISTS (SELECT 1 FROM execution_project_scopes p WHERE p.tenant_id = c.tenant_id) - )); - --- +goose Down --- +goose StatementBegin -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM devices WHERE executor_key_id IS NOT NULL) THEN - RAISE EXCEPTION 'Cannot remove Runtime enrollment while bound devices exist'; - END IF; -END $$; --- +goose StatementEnd -DROP VIEW runtime_device_authority; -ALTER TABLE devices DROP CONSTRAINT device_credential_source, - DROP COLUMN executor_key_id, ALTER COLUMN credential_hash SET NOT NULL; diff --git a/services/core/migrations/000051_subagent_resources.sql b/services/core/migrations/000051_subagent_resources.sql deleted file mode 100644 index da1e7c15e..000000000 --- a/services/core/migrations/000051_subagent_resources.sql +++ /dev/null @@ -1,66 +0,0 @@ --- +goose Up -ALTER TABLE subagent_identities - ADD COLUMN name text, - ADD COLUMN instructions text, - ADD COLUMN public_visible boolean NOT NULL DEFAULT false, - ADD COLUMN status text NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'closed')), - ADD COLUMN closed_at_ms bigint, - ADD COLUMN lifecycle_at_ms bigint NOT NULL DEFAULT 0, - ADD CONSTRAINT subagent_closed_state CHECK ((status = 'closed') = (closed_at_ms IS NOT NULL)), - ADD CONSTRAINT subagent_session_identity UNIQUE (session_id, id); - --- Child work is observed from the native owner; it must never enter the Core queue. -CREATE TABLE subagent_turns ( - id uuid PRIMARY KEY, - session_id uuid NOT NULL, - subagent_id uuid NOT NULL, - native_id text NOT NULL CHECK (native_id <> ''), - status text NOT NULL CHECK (status IN ('queued', 'in_progress', 'waiting', 'completed', 'failed', 'cancelled')), - created_at timestamptz NOT NULL, - started_at timestamptz, - completed_at timestamptz, - token_usage jsonb, - FOREIGN KEY (session_id, subagent_id) REFERENCES subagent_identities(session_id, id), - UNIQUE (subagent_id, native_id), - UNIQUE (session_id, subagent_id, id), - CHECK ((completed_at IS NOT NULL) = (status IN ('completed', 'failed', 'cancelled'))) -); -CREATE TABLE subagent_items ( - id uuid PRIMARY KEY, - session_id uuid NOT NULL, - subagent_id uuid NOT NULL, - turn_id uuid NOT NULL, - position integer NOT NULL CHECK (position >= 0), - output_index integer, - payload jsonb NOT NULL CHECK (jsonb_typeof(payload) = 'object'), - FOREIGN KEY (session_id, subagent_id, turn_id) REFERENCES subagent_turns(session_id, subagent_id, id), - UNIQUE (turn_id, position) -); -CREATE TABLE subagent_effects ( - session_id uuid NOT NULL REFERENCES sessions(id), - effect_id text NOT NULL, - payload jsonb NOT NULL CHECK (jsonb_typeof(payload) = 'object'), - PRIMARY KEY (session_id, effect_id) -); - -CREATE VIEW public_execution_turns AS - SELECT id, session_id, status, created_at, started_at, completed_at, cancel_requested_at, outcome, - token_usage, artifact_capture_started, NULL::uuid AS subagent_id FROM turns - UNION ALL - SELECT id, session_id, status, created_at, started_at, completed_at, NULL::timestamptz, - '{}'::jsonb, token_usage, false, subagent_id FROM subagent_turns; - --- +goose Down -DROP VIEW public_execution_turns; -DROP TABLE subagent_effects; -DROP TABLE subagent_items; -DROP TABLE subagent_turns; -ALTER TABLE subagent_identities - DROP CONSTRAINT subagent_session_identity, - DROP CONSTRAINT subagent_closed_state, - DROP COLUMN lifecycle_at_ms, - DROP COLUMN closed_at_ms, - DROP COLUMN status, - DROP COLUMN public_visible, - DROP COLUMN instructions, - DROP COLUMN name; diff --git a/services/core/migrations/000052_runtime_suspension.sql b/services/core/migrations/000052_runtime_suspension.sql deleted file mode 100644 index d99902ee0..000000000 --- a/services/core/migrations/000052_runtime_suspension.sql +++ /dev/null @@ -1,27 +0,0 @@ --- +goose Up -ALTER TABLE runtime_allocations - ADD COLUMN compute_phase text NOT NULL DEFAULT 'disabled' - CHECK (compute_phase IN ('disabled', 'running', 'quiescing', 'suspending', 'suspended', 'restoring', 'waking')), - ADD COLUMN compute_revision bigint NOT NULL DEFAULT 0 CHECK (compute_revision >= 0), - ADD COLUMN compute_state jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(compute_state) = 'object'), - ADD COLUMN compute_activity_at timestamptz NOT NULL DEFAULT clock_timestamp(), - ADD COLUMN compute_wake_requested boolean NOT NULL DEFAULT false, - ADD COLUMN compute_retained_until timestamptz; - --- +goose Down -LOCK TABLE runtime_allocations IN ACCESS EXCLUSIVE MODE; --- +goose StatementBegin -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM runtime_allocations WHERE compute_phase <> 'disabled') THEN - RAISE EXCEPTION 'Cannot discard retained compute and snapshot ownership'; - END IF; -END $$; --- +goose StatementEnd -ALTER TABLE runtime_allocations - DROP COLUMN compute_retained_until, - DROP COLUMN compute_wake_requested, - DROP COLUMN compute_activity_at, - DROP COLUMN compute_state, - DROP COLUMN compute_revision, - DROP COLUMN compute_phase; diff --git a/services/core/migrations/000053_runtime_deployment.sql b/services/core/migrations/000053_runtime_deployment.sql deleted file mode 100644 index 017ae351c..000000000 --- a/services/core/migrations/000053_runtime_deployment.sql +++ /dev/null @@ -1,22 +0,0 @@ --- +goose Up -CREATE TABLE runtime_deployment ( - singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton), - installation_id uuid, - backend_fingerprint text NOT NULL DEFAULT '', - maintenance boolean NOT NULL DEFAULT false, - updated_at timestamptz NOT NULL DEFAULT clock_timestamp(), - CHECK ((installation_id IS NULL AND backend_fingerprint = '') OR - (installation_id IS NOT NULL AND backend_fingerprint ~ '^[0-9a-f]{64}$')) -); -INSERT INTO runtime_deployment (singleton) VALUES (true); - --- +goose Down --- +goose StatementBegin -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM runtime_deployment WHERE installation_id IS NOT NULL) THEN - RAISE EXCEPTION 'Cannot discard persisted sandbox installation identity'; - END IF; -END $$; --- +goose StatementEnd -DROP TABLE runtime_deployment; diff --git a/services/core/migrations/000054_oauth_credentials.sql b/services/core/migrations/000054_oauth_credentials.sql deleted file mode 100644 index 37dd72330..000000000 --- a/services/core/migrations/000054_oauth_credentials.sql +++ /dev/null @@ -1,17 +0,0 @@ --- +goose Up -ALTER TABLE vault_credentials DROP CONSTRAINT vault_credentials_auth_type_check; -ALTER TABLE vault_credentials ADD COLUMN oauth_metadata jsonb; -ALTER TABLE vault_credentials ADD CONSTRAINT vault_credentials_auth_type_check - CHECK (auth_type IN ('static_bearer', 'mcp_oauth')); -ALTER TABLE vault_credentials ADD CONSTRAINT vault_credentials_oauth_metadata_check - CHECK ((auth_type = 'static_bearer' AND oauth_metadata IS NULL) - OR (auth_type = 'mcp_oauth' AND oauth_metadata IS NOT NULL - AND jsonb_typeof(oauth_metadata) = 'object')); - --- +goose Down --- Refuse to discard grants when rolling back to static-only storage. -ALTER TABLE vault_credentials DROP CONSTRAINT vault_credentials_auth_type_check; -ALTER TABLE vault_credentials ADD CONSTRAINT vault_credentials_auth_type_check - CHECK (auth_type = 'static_bearer'); -ALTER TABLE vault_credentials DROP CONSTRAINT vault_credentials_oauth_metadata_check; -ALTER TABLE vault_credentials DROP COLUMN oauth_metadata; diff --git a/services/core/migrations/000055_skill_default_metadata.sql b/services/core/migrations/000055_skill_default_metadata.sql deleted file mode 100644 index 97ab38cd3..000000000 --- a/services/core/migrations/000055_skill_default_metadata.sql +++ /dev/null @@ -1,12 +0,0 @@ --- +goose Up -UPDATE skills s -SET name = v.name, description = v.description -FROM skill_versions v -WHERE v.tenant_id = s.tenant_id - AND v.skill_id = s.id - AND v.version = s.default_version - AND (s.name, s.description) IS DISTINCT FROM (v.name, v.description); - --- +goose Down --- Keep repaired metadata; restoring stale names and descriptions would corrupt it. -SELECT 1; diff --git a/services/core/migrations/000056_runtime_history.sql b/services/core/migrations/000056_runtime_history.sql deleted file mode 100644 index b4737a233..000000000 --- a/services/core/migrations/000056_runtime_history.sql +++ /dev/null @@ -1,36 +0,0 @@ --- +goose Up --- Periodic telemetry is a bounded projection, never execution or Usage authority. -CREATE TABLE runtime_history_samples ( - tenant_id uuid NOT NULL, - session_id uuid NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, - environment_id uuid NOT NULL REFERENCES environments(id) ON DELETE CASCADE, - resolved_at_ns bigint NOT NULL CHECK (resolved_at_ns >= 0), - allocation_id uuid, - provider_type text NOT NULL, - status text NOT NULL CHECK (status IN ('observed', 'unavailable', 'unsupported')), - observed_at_ns bigint, - started_at_ns bigint, - cpu_usage_seconds double precision, - cpu_capacity_cores double precision, - memory_usage_bytes bigint, - memory_limit_bytes bigint, - input_tokens bigint, - output_tokens bigint, - PRIMARY KEY (tenant_id, session_id, environment_id, resolved_at_ns), - CHECK ((input_tokens IS NULL) = (output_tokens IS NULL)), - CHECK (input_tokens >= 0 AND input_tokens <= 9007199254740991), - CHECK (output_tokens >= 0 AND output_tokens <= 9007199254740991), - CHECK (observed_at_ns >= 0 AND observed_at_ns <= resolved_at_ns), - CHECK (started_at_ns >= 0 AND started_at_ns <= observed_at_ns), - CHECK ((status = 'observed') = (observed_at_ns IS NOT NULL)), - CHECK (started_at_ns IS NULL OR observed_at_ns IS NOT NULL), - CHECK (cpu_usage_seconds >= 0 AND cpu_usage_seconds < 'Infinity'::double precision), - CHECK (cpu_capacity_cores > 0 AND cpu_capacity_cores < 'Infinity'::double precision), - CHECK (memory_usage_bytes >= 0 AND memory_usage_bytes <= 9007199254740991), - CHECK (memory_limit_bytes > 0 AND memory_limit_bytes <= 9007199254740991), - CHECK (started_at_ns IS NOT NULL OR (cpu_usage_seconds IS NULL AND cpu_capacity_cores IS NULL AND memory_usage_bytes IS NULL AND memory_limit_bytes IS NULL)) -); -CREATE INDEX runtime_history_samples_retention_idx ON runtime_history_samples (resolved_at_ns); - --- +goose Down -DROP TABLE runtime_history_samples; diff --git a/services/core/migrations/000057_runtime_nodes.sql b/services/core/migrations/000057_runtime_nodes.sql deleted file mode 100644 index f27652ed1..000000000 --- a/services/core/migrations/000057_runtime_nodes.sql +++ /dev/null @@ -1,55 +0,0 @@ --- +goose Up -ALTER TABLE runtime_deployment ADD COLUMN provider_kind text NOT NULL DEFAULT '' CHECK (provider_kind IN ('', 'docker', 'microsandbox')); -ALTER TABLE runtime_deployment ADD COLUMN local_node_id uuid; -ALTER TABLE runtime_deployment ADD COLUMN owner_epoch bigint NOT NULL DEFAULT 0 CHECK (owner_epoch >= 0); -CREATE TABLE runtime_nodes ( - id uuid PRIMARY KEY, - installation_id uuid NOT NULL, - name text NOT NULL CHECK (length(name) BETWEEN 1 AND 128), - backend_fingerprint text NOT NULL CHECK (backend_fingerprint ~ '^[0-9a-f]{64}$'), - credential_sha256 text NOT NULL CHECK (credential_sha256 ~ '^[0-9a-f]{64}$'), - max_active integer NOT NULL CHECK (max_active > 0), - max_retained integer NOT NULL CHECK (max_retained >= max_active), - connection_id uuid, - provider_ready boolean NOT NULL DEFAULT false, - health jsonb NOT NULL DEFAULT '{}'::jsonb, - connected_epoch bigint NOT NULL DEFAULT 0, - last_seen_at timestamptz, - created_at timestamptz NOT NULL DEFAULT clock_timestamp(), - removed_at timestamptz -); -CREATE TABLE runtime_placements ( - environment_id uuid PRIMARY KEY REFERENCES environments(id), - node_id uuid NOT NULL REFERENCES runtime_nodes(id), - reserved_at timestamptz NOT NULL DEFAULT clock_timestamp(), - released_at timestamptz, - UNIQUE(environment_id, node_id) -); -ALTER TABLE runtime_allocations ADD COLUMN node_id uuid; -ALTER TABLE runtime_allocations ADD COLUMN observation_error text NOT NULL DEFAULT '' CHECK (observation_error IN ('','node_unavailable','resource_missing','compute_unconfirmed','ownership_mismatch','provider_unavailable')); -ALTER TABLE runtime_allocations ADD CONSTRAINT runtime_allocation_placement_fk FOREIGN KEY (environment_id,node_id) REFERENCES runtime_placements(environment_id,node_id); -CREATE INDEX runtime_placements_node_idx ON runtime_placements(node_id) WHERE released_at IS NULL; -CREATE TABLE runtime_node_enrollments ( - token_sha256 text PRIMARY KEY CHECK (token_sha256 ~ '^[0-9a-f]{64}$'), - installation_id uuid NOT NULL, - expires_at timestamptz NOT NULL, - consumed_at timestamptz, - node_id uuid REFERENCES runtime_nodes(id) -); --- +goose Down --- +goose StatementBegin -DO $$ BEGIN -IF EXISTS (SELECT 1 FROM runtime_nodes) THEN - RAISE EXCEPTION 'Cannot discard sandbox node and placement identities'; -END IF; -END $$; --- +goose StatementEnd -DROP TABLE runtime_node_enrollments; -ALTER TABLE runtime_allocations DROP CONSTRAINT runtime_allocation_placement_fk; -ALTER TABLE runtime_allocations DROP COLUMN node_id; -ALTER TABLE runtime_allocations DROP COLUMN observation_error; -DROP TABLE runtime_placements; -DROP TABLE runtime_nodes; -ALTER TABLE runtime_deployment DROP COLUMN owner_epoch; -ALTER TABLE runtime_deployment DROP COLUMN local_node_id; -ALTER TABLE runtime_deployment DROP COLUMN provider_kind; diff --git a/services/core/migrations/000058_sandbox_deployment_setup.sql b/services/core/migrations/000058_sandbox_deployment_setup.sql deleted file mode 100644 index 80aa81826..000000000 --- a/services/core/migrations/000058_sandbox_deployment_setup.sql +++ /dev/null @@ -1,38 +0,0 @@ --- +goose Up -ALTER TABLE runtime_deployment ADD COLUMN web_managed boolean NOT NULL DEFAULT false; -ALTER TABLE runtime_deployment ADD COLUMN core_url text NOT NULL DEFAULT ''; -ALTER TABLE runtime_deployment ADD COLUMN idle_seconds bigint NOT NULL DEFAULT 0 CHECK (idle_seconds >= 0); -ALTER TABLE runtime_deployment ADD COLUMN retention_seconds bigint NOT NULL DEFAULT 0 CHECK (retention_seconds >= 0); -ALTER TABLE runtime_deployment DROP CONSTRAINT runtime_deployment_check; -ALTER TABLE runtime_deployment ADD CONSTRAINT runtime_deployment_identity_check CHECK ( - (installation_id IS NULL AND backend_fingerprint = '') OR - (installation_id IS NOT NULL AND backend_fingerprint ~ '^[0-9a-f]{64}$') OR - (web_managed AND installation_id IS NOT NULL AND provider_kind = '' AND backend_fingerprint = '') -); -ALTER TABLE runtime_deployment ADD CONSTRAINT runtime_deployment_setup_check CHECK ( - NOT web_managed OR (local_node_id IS NULL AND ( - (provider_kind = '' AND core_url = '' AND idle_seconds = 0 AND retention_seconds = 0) OR - (provider_kind = 'docker' AND core_url <> '' AND idle_seconds = 0 AND retention_seconds = 0) OR - (provider_kind = 'microsandbox' AND core_url <> '' AND idle_seconds > 0 AND retention_seconds > 0) - )) -); - --- +goose Down --- +goose StatementBegin -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM runtime_deployment WHERE web_managed) THEN - RAISE EXCEPTION 'Cannot discard Web-managed sandbox deployment setup'; - END IF; -END $$; --- +goose StatementEnd -ALTER TABLE runtime_deployment DROP CONSTRAINT runtime_deployment_setup_check; -ALTER TABLE runtime_deployment DROP CONSTRAINT runtime_deployment_identity_check; -ALTER TABLE runtime_deployment DROP COLUMN retention_seconds; -ALTER TABLE runtime_deployment DROP COLUMN idle_seconds; -ALTER TABLE runtime_deployment DROP COLUMN core_url; -ALTER TABLE runtime_deployment DROP COLUMN web_managed; -ALTER TABLE runtime_deployment ADD CONSTRAINT runtime_deployment_check CHECK ( - (installation_id IS NULL AND backend_fingerprint = '') OR - (installation_id IS NOT NULL AND backend_fingerprint ~ '^[0-9a-f]{64}$') -); diff --git a/services/core/migrations/000059_agent_model_execution.sql b/services/core/migrations/000059_agent_model_execution.sql deleted file mode 100644 index 22e383591..000000000 --- a/services/core/migrations/000059_agent_model_execution.sql +++ /dev/null @@ -1,8 +0,0 @@ --- +goose Up -CREATE TABLE agent_model_execution ( - agent_id uuid PRIMARY KEY REFERENCES agents(id) ON DELETE CASCADE, - encrypted_config bytea NOT NULL -); - --- +goose Down -DROP TABLE agent_model_execution; diff --git a/services/core/migrations/000060_session_execution_configuration.sql b/services/core/migrations/000060_session_execution_configuration.sql deleted file mode 100644 index d8ded49b5..000000000 --- a/services/core/migrations/000060_session_execution_configuration.sql +++ /dev/null @@ -1,8 +0,0 @@ --- +goose Up -CREATE TABLE session_execution_configuration ( - session_id uuid PRIMARY KEY REFERENCES sessions(id) ON DELETE CASCADE, - configuration jsonb NOT NULL CHECK (jsonb_typeof(configuration) = 'object') -); - --- +goose Down -DROP TABLE session_execution_configuration; diff --git a/services/core/migrations/000061_project_api_keys.sql b/services/core/migrations/000061_project_api_keys.sql deleted file mode 100644 index fdb6ac73c..000000000 --- a/services/core/migrations/000061_project_api_keys.sql +++ /dev/null @@ -1,20 +0,0 @@ --- +goose Up -CREATE TABLE project_api_keys ( - id uuid PRIMARY KEY, - name text NOT NULL CHECK (char_length(name) BETWEEN 1 AND 80), - prefix text NOT NULL, - token_sha256 text NOT NULL UNIQUE CHECK (token_sha256 ~ '^[0-9a-f]{64}$'), - binding_digest text NOT NULL CHECK (binding_digest ~ '^[0-9a-f]{64}$'), - tenant_id uuid NOT NULL REFERENCES execution_project_scopes(tenant_id), - organization_id text NOT NULL, - project_id text NOT NULL, - subject_kind text NOT NULL CHECK (subject_kind IN ('user', 'service_account')), - subject_id text NOT NULL, - created_at timestamptz NOT NULL DEFAULT now(), - revoked_at timestamptz -); -CREATE INDEX project_api_keys_owner ON project_api_keys - (binding_digest, tenant_id, organization_id, project_id, subject_kind, subject_id, created_at DESC, id); - --- +goose Down -DROP TABLE project_api_keys; diff --git a/services/core/migrations/000062_environment_failure.sql b/services/core/migrations/000062_environment_failure.sql deleted file mode 100644 index 6a2f3861b..000000000 --- a/services/core/migrations/000062_environment_failure.sql +++ /dev/null @@ -1,27 +0,0 @@ --- +goose Up --- A failed hosted Environment keeps the public reason and time of its --- provisioning failure. Core composes the reason from a fixed step label and an --- exit status; Runtime output is never stored. Earlier failures keep NULL. -ALTER TABLE environments - ADD COLUMN failure_reason text, - ADD COLUMN failed_at timestamptz, - ADD CONSTRAINT environment_failure_recorded CHECK ( - (failure_reason IS NULL) = (failed_at IS NULL) - AND (failed_at IS NULL OR status = 'failed') - AND char_length(failure_reason) <= 256 - ); - --- +goose Down -LOCK TABLE environments IN ACCESS EXCLUSIVE MODE; --- +goose StatementBegin -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM environments WHERE failure_reason IS NOT NULL) THEN - RAISE EXCEPTION 'Cannot remove recorded hosted provisioning failures'; - END IF; -END $$; --- +goose StatementEnd -ALTER TABLE environments - DROP CONSTRAINT environment_failure_recorded, - DROP COLUMN failed_at, - DROP COLUMN failure_reason; diff --git a/services/core/migrations/000063_hosted_provider_selection.sql b/services/core/migrations/000063_hosted_provider_selection.sql deleted file mode 100644 index c7fe69d42..000000000 --- a/services/core/migrations/000063_hosted_provider_selection.sql +++ /dev/null @@ -1,46 +0,0 @@ --- +goose Up -ALTER TABLE runtime_deployment DROP CONSTRAINT runtime_deployment_provider_kind_check; -ALTER TABLE runtime_deployment ADD CONSTRAINT runtime_deployment_provider_kind_check CHECK (provider_kind IN ('','docker','microsandbox','e2b')); -ALTER TABLE runtime_deployment ADD COLUMN generation bigint NOT NULL DEFAULT 0 CHECK (generation >= 0); -ALTER TABLE runtime_deployment ADD COLUMN mode text NOT NULL DEFAULT '' CHECK (mode IN ('','nodes','direct')); -ALTER TABLE runtime_deployment ADD COLUMN e2b_template text NOT NULL DEFAULT ''; -ALTER TABLE runtime_deployment ADD COLUMN e2b_credential bytea; -UPDATE runtime_deployment SET generation=1,mode='nodes' WHERE provider_kind<>''; -ALTER TABLE runtime_deployment DROP CONSTRAINT runtime_deployment_setup_check; -ALTER TABLE runtime_deployment ADD CONSTRAINT runtime_deployment_setup_check CHECK ( - NOT web_managed OR (local_node_id IS NULL AND ( - (provider_kind='' AND core_url='' AND mode='' AND generation=0 AND idle_seconds=0 AND retention_seconds=0) OR - (provider_kind='docker' AND core_url<>'' AND mode='nodes' AND generation>0 AND idle_seconds=0 AND retention_seconds=0) OR - (provider_kind='microsandbox' AND core_url<>'' AND mode='nodes' AND generation>0 AND idle_seconds>0 AND retention_seconds>0) OR - (provider_kind='e2b' AND core_url<>'' AND mode='direct' AND generation>0 AND idle_seconds=0 AND retention_seconds=0) - )) -); -ALTER TABLE runtime_deployment ADD CONSTRAINT runtime_deployment_e2b_check CHECK ( - (provider_kind='e2b' AND e2b_template<>'' AND e2b_credential IS NOT NULL) OR - (provider_kind<>'e2b' AND e2b_template='' AND e2b_credential IS NULL) -); - --- +goose Down --- +goose StatementBegin -DO $$ BEGIN - IF EXISTS (SELECT 1 FROM runtime_deployment WHERE provider_kind='e2b' OR generation>1) THEN - RAISE EXCEPTION 'Cannot discard hosted provider configuration generations'; - END IF; -END $$; --- +goose StatementEnd - -ALTER TABLE runtime_deployment DROP CONSTRAINT runtime_deployment_e2b_check; -ALTER TABLE runtime_deployment DROP CONSTRAINT runtime_deployment_setup_check; -ALTER TABLE runtime_deployment DROP COLUMN e2b_credential; -ALTER TABLE runtime_deployment DROP COLUMN e2b_template; -ALTER TABLE runtime_deployment DROP COLUMN mode; -ALTER TABLE runtime_deployment DROP COLUMN generation; -ALTER TABLE runtime_deployment DROP CONSTRAINT runtime_deployment_provider_kind_check; -ALTER TABLE runtime_deployment ADD CONSTRAINT runtime_deployment_provider_kind_check CHECK (provider_kind IN ('','docker','microsandbox')); -ALTER TABLE runtime_deployment ADD CONSTRAINT runtime_deployment_setup_check CHECK ( - NOT web_managed OR (local_node_id IS NULL AND ( - (provider_kind='' AND core_url='' AND idle_seconds=0 AND retention_seconds=0) OR - (provider_kind='docker' AND core_url<>'' AND idle_seconds=0 AND retention_seconds=0) OR - (provider_kind='microsandbox' AND core_url<>'' AND idle_seconds>0 AND retention_seconds>0) - )) -); diff --git a/services/core/migrations/000064_write_audit.sql b/services/core/migrations/000064_write_audit.sql deleted file mode 100644 index 9456502f3..000000000 --- a/services/core/migrations/000064_write_audit.sql +++ /dev/null @@ -1,37 +0,0 @@ --- +goose Up -CREATE TABLE write_audit_operations ( - id uuid PRIMARY KEY, - tenant_id uuid NOT NULL, - key_id text NOT NULL, - key_name text NOT NULL, - key_prefix text NOT NULL, - key_kind text NOT NULL CHECK (key_kind IN ('static', 'issued', 'console')), - action text NOT NULL, - resource_type text NOT NULL, - resource_id text NOT NULL, - parent_id text NOT NULL DEFAULT '', - request_id text NOT NULL, - trace_id text NOT NULL, - created_at timestamptz NOT NULL DEFAULT clock_timestamp(), - UNIQUE (tenant_id, request_id), - UNIQUE (tenant_id, id) -); -CREATE INDEX write_audit_operations_history ON write_audit_operations (tenant_id, created_at DESC, id DESC); -CREATE INDEX write_audit_operations_key_history ON write_audit_operations (tenant_id, key_id, created_at DESC, id DESC); -CREATE INDEX write_audit_operations_resource_history ON write_audit_operations (tenant_id, resource_type, resource_id, created_at DESC, id DESC); -CREATE INDEX write_audit_operations_expiry ON write_audit_operations (created_at, id); - -CREATE TABLE write_audit_owners ( - tenant_id uuid NOT NULL, - resource_type text NOT NULL, - resource_id text NOT NULL, - parent_id text NOT NULL DEFAULT '', - operation_id uuid NOT NULL, - PRIMARY KEY (tenant_id, resource_type, resource_id), - FOREIGN KEY (tenant_id, operation_id) REFERENCES write_audit_operations (tenant_id, id) -); -CREATE INDEX write_audit_owners_operation ON write_audit_owners (operation_id); - --- +goose Down -DROP TABLE write_audit_owners; -DROP TABLE write_audit_operations; diff --git a/services/core/migrations/000065_environment_file_write_audit.sql b/services/core/migrations/000065_environment_file_write_audit.sql deleted file mode 100644 index 3f355099d..000000000 --- a/services/core/migrations/000065_environment_file_write_audit.sql +++ /dev/null @@ -1,8 +0,0 @@ --- +goose Up --- Safe request identity survives native execution and request detachment. -ALTER TABLE environment_file_writes ADD COLUMN audit_source jsonb; -ALTER TABLE environment_file_writes ADD CONSTRAINT environment_file_write_audit_source_object - CHECK (audit_source IS NULL OR jsonb_typeof(audit_source) = 'object'); - --- +goose Down -ALTER TABLE environment_file_writes DROP COLUMN audit_source; diff --git a/services/core/migrations/000066_admin_projects.sql b/services/core/migrations/000066_admin_projects.sql deleted file mode 100644 index 96eff7151..000000000 --- a/services/core/migrations/000066_admin_projects.sql +++ /dev/null @@ -1,48 +0,0 @@ --- +goose Up --- +goose StatementBegin -DO $$ BEGIN - IF EXISTS (SELECT 1 FROM project_api_keys) THEN - RAISE EXCEPTION 'Legacy inherited API keys prevent Project catalog installation; use a clean private installation. No existing data has been removed.'; - END IF; -END $$; --- +goose StatementEnd -CREATE TABLE projects ( - id uuid PRIMARY KEY, - name text NOT NULL CHECK (char_length(name) BETWEEN 1 AND 128), - tenant_id uuid NOT NULL UNIQUE REFERENCES execution_project_scopes(tenant_id), - subject_kind text NOT NULL CHECK (subject_kind = 'service_account'), - subject_id text NOT NULL, - created_at timestamptz NOT NULL DEFAULT now(), - archived_at timestamptz, - UNIQUE (id, tenant_id) -); -DROP INDEX project_api_keys_owner; -ALTER TABLE project_api_keys DROP COLUMN binding_digest, - DROP COLUMN tenant_id, DROP COLUMN organization_id, DROP COLUMN project_id, - DROP COLUMN subject_kind, DROP COLUMN subject_id; -ALTER TABLE project_api_keys ADD COLUMN project_id uuid NOT NULL REFERENCES projects(id); -CREATE INDEX project_api_keys_project ON project_api_keys(project_id,id); -CREATE TABLE admin_audit_log ( - id uuid PRIMARY KEY, - tenant_id uuid NOT NULL, - project_id uuid NOT NULL, - admin_credential_id text NOT NULL, - actor_label text NOT NULL, - action text NOT NULL, - resource_type text NOT NULL, - resource_id text NOT NULL, - result_ids jsonb NOT NULL DEFAULT '[]', - request_id text NOT NULL, - trace_id text NOT NULL, - created_at timestamptz NOT NULL DEFAULT clock_timestamp(), - FOREIGN KEY (project_id,tenant_id) REFERENCES projects(id,tenant_id), - UNIQUE (tenant_id, request_id, action, resource_type, resource_id) -); -CREATE INDEX admin_audit_log_history ON admin_audit_log (project_id, created_at DESC, id DESC); - --- +goose Down --- +goose StatementBegin -DO $$ BEGIN - RAISE EXCEPTION 'Project catalog downgrade is unsupported; restore a complete database backup instead.'; -END $$; --- +goose StatementEnd diff --git a/services/core/migrations/000067_admin_asset_copies.sql b/services/core/migrations/000067_admin_asset_copies.sql deleted file mode 100644 index 2debb9e77..000000000 --- a/services/core/migrations/000067_admin_asset_copies.sql +++ /dev/null @@ -1,23 +0,0 @@ --- +goose Up -CREATE TABLE admin_asset_copies ( - target_tenant_id uuid NOT NULL, - idempotency_key text NOT NULL, - request_hash bytea NOT NULL, - result jsonb NOT NULL, - audit_id uuid NOT NULL REFERENCES admin_audit_log(id), - PRIMARY KEY (target_tenant_id, idempotency_key) -); - -CREATE TABLE admin_resource_owners ( - tenant_id uuid NOT NULL, - resource_type text NOT NULL, - resource_id text NOT NULL, - parent_id text NOT NULL DEFAULT '', - audit_id uuid NOT NULL REFERENCES admin_audit_log(id), - PRIMARY KEY (tenant_id, resource_type, resource_id) -); -CREATE INDEX admin_resource_owners_audit ON admin_resource_owners(audit_id); - --- +goose Down -DROP TABLE admin_resource_owners; -DROP TABLE admin_asset_copies; diff --git a/services/core/migrations/000068_core_metrics_indexes.sql b/services/core/migrations/000068_core_metrics_indexes.sql deleted file mode 100644 index a45fa0f6a..000000000 --- a/services/core/migrations/000068_core_metrics_indexes.sql +++ /dev/null @@ -1,9 +0,0 @@ --- +goose Up -CREATE INDEX turns_metrics_started_idx ON turns(started_at) INCLUDE (created_at) - WHERE started_at IS NOT NULL; -CREATE INDEX turns_metrics_interrupted_idx ON turns(completed_at) - WHERE status = 'failed' AND outcome->>'error_code' = 'execution_interrupted'; - --- +goose Down -DROP INDEX turns_metrics_interrupted_idx; -DROP INDEX turns_metrics_started_idx; diff --git a/services/core/migrations/000069_hosted_specification.sql b/services/core/migrations/000069_hosted_specification.sql deleted file mode 100644 index f9a76d57d..000000000 --- a/services/core/migrations/000069_hosted_specification.sql +++ /dev/null @@ -1,18 +0,0 @@ --- +goose Up -ALTER TABLE runtime_deployment ADD COLUMN specification jsonb NOT NULL DEFAULT '{}'::jsonb; -ALTER TABLE runtime_nodes ADD COLUMN specification_digest text NOT NULL DEFAULT ''; -ALTER TABLE runtime_nodes ADD COLUMN deployment_generation bigint NOT NULL DEFAULT 0; -ALTER TABLE runtime_nodes ADD CONSTRAINT runtime_node_specification_digest CHECK (specification_digest = '' OR specification_digest ~ '^[0-9a-f]{64}$'); -ALTER TABLE runtime_nodes ADD CONSTRAINT runtime_node_deployment_generation CHECK (deployment_generation >= 0); - --- +goose Down --- +goose StatementBegin -DO $$ BEGIN - IF EXISTS (SELECT 1 FROM runtime_deployment WHERE specification <> '{}'::jsonb) THEN - RAISE EXCEPTION 'Cannot discard an active sandbox specification'; - END IF; -END $$; --- +goose StatementEnd -ALTER TABLE runtime_nodes DROP COLUMN deployment_generation; -ALTER TABLE runtime_nodes DROP COLUMN specification_digest; -ALTER TABLE runtime_deployment DROP COLUMN specification; diff --git a/services/core/migrations/000070_node_enrollment_capacity.sql b/services/core/migrations/000070_node_enrollment_capacity.sql deleted file mode 100644 index 59903de1b..000000000 --- a/services/core/migrations/000070_node_enrollment_capacity.sql +++ /dev/null @@ -1,8 +0,0 @@ --- +goose Up -ALTER TABLE runtime_node_enrollments ADD COLUMN max_active integer NOT NULL DEFAULT 2; -ALTER TABLE runtime_node_enrollments ADD COLUMN max_retained integer NOT NULL DEFAULT 8; -ALTER TABLE runtime_node_enrollments ADD CONSTRAINT runtime_enrollment_capacity CHECK (max_active >= 1 AND max_retained >= max_active AND max_retained <= 1000000); - --- +goose Down -ALTER TABLE runtime_node_enrollments DROP COLUMN max_retained; -ALTER TABLE runtime_node_enrollments DROP COLUMN max_active; diff --git a/services/core/migrations/000071_node_host_history.sql b/services/core/migrations/000071_node_host_history.sql deleted file mode 100644 index cbdfe96ca..000000000 --- a/services/core/migrations/000071_node_host_history.sql +++ /dev/null @@ -1,13 +0,0 @@ --- +goose Up -CREATE TABLE node_host_history_samples ( - node_id uuid NOT NULL REFERENCES runtime_nodes(id), - observed_at timestamptz NOT NULL, - cpu_utilization double precision CHECK (cpu_utilization >= 0 AND cpu_utilization <= 1), - memory_used_bytes bigint CHECK (memory_used_bytes >= 0), - available_disk_bytes bigint CHECK (available_disk_bytes >= 0), - PRIMARY KEY (node_id, observed_at) -); -CREATE INDEX node_host_history_retention ON node_host_history_samples(observed_at); - --- +goose Down -DROP TABLE node_host_history_samples; diff --git a/services/core/migrations/000073_e2b_observation_and_template_build.sql b/services/core/migrations/000073_e2b_observation_and_template_build.sql deleted file mode 100644 index ba8344e2c..000000000 --- a/services/core/migrations/000073_e2b_observation_and_template_build.sql +++ /dev/null @@ -1,26 +0,0 @@ --- +goose Up --- Providers such as E2B report a current CPU share instead of cumulative time. -ALTER TABLE runtime_history_samples ADD COLUMN cpu_utilization_ratio double precision - CHECK (cpu_utilization_ratio >= 0 AND cpu_utilization_ratio < 'Infinity'::double precision); -ALTER TABLE runtime_history_samples ADD CONSTRAINT runtime_history_samples_cpu_utilization_incarnation - CHECK (started_at_ns IS NOT NULL OR cpu_utilization_ratio IS NULL); - --- The fixed E2B template build as read by the validation that admitted the --- selection. Selections saved before these columns existed keep nulls. -ALTER TABLE runtime_deployment ADD COLUMN e2b_template_build_status text CHECK (e2b_template_build_status <> ''); -ALTER TABLE runtime_deployment ADD COLUMN e2b_template_cpus integer CHECK (e2b_template_cpus > 0); -ALTER TABLE runtime_deployment ADD COLUMN e2b_template_memory_mib integer CHECK (e2b_template_memory_mib > 0); -ALTER TABLE runtime_deployment ADD COLUMN e2b_template_root_disk_mib integer CHECK (e2b_template_root_disk_mib > 0); -ALTER TABLE runtime_deployment ADD CONSTRAINT runtime_deployment_e2b_template_build_check CHECK ( - provider_kind = 'e2b' OR (e2b_template_build_status IS NULL AND e2b_template_cpus IS NULL AND - e2b_template_memory_mib IS NULL AND e2b_template_root_disk_mib IS NULL) -); - --- +goose Down -ALTER TABLE runtime_deployment DROP CONSTRAINT runtime_deployment_e2b_template_build_check; -ALTER TABLE runtime_deployment DROP COLUMN e2b_template_root_disk_mib; -ALTER TABLE runtime_deployment DROP COLUMN e2b_template_memory_mib; -ALTER TABLE runtime_deployment DROP COLUMN e2b_template_cpus; -ALTER TABLE runtime_deployment DROP COLUMN e2b_template_build_status; -ALTER TABLE runtime_history_samples DROP CONSTRAINT runtime_history_samples_cpu_utilization_incarnation; -ALTER TABLE runtime_history_samples DROP COLUMN cpu_utilization_ratio; diff --git a/services/core/migrations/000074_allocation_phase_time.sql b/services/core/migrations/000074_allocation_phase_time.sql deleted file mode 100644 index caae36a2d..000000000 --- a/services/core/migrations/000074_allocation_phase_time.sql +++ /dev/null @@ -1,7 +0,0 @@ --- +goose Up --- Existing rows keep NULL: the time they entered their current phase is unknown. -ALTER TABLE runtime_allocations ADD COLUMN compute_phase_changed_at timestamptz; -ALTER TABLE runtime_allocations ALTER COLUMN compute_phase_changed_at SET DEFAULT clock_timestamp(); - --- +goose Down -ALTER TABLE runtime_allocations DROP COLUMN compute_phase_changed_at; diff --git a/services/core/migrations/000075_public_url_binding.sql b/services/core/migrations/000075_public_url_binding.sql deleted file mode 100644 index f4bd26275..000000000 --- a/services/core/migrations/000075_public_url_binding.sql +++ /dev/null @@ -1,42 +0,0 @@ --- +goose Up --- Core derives the address nodes and sandboxes use from AGENTS_API_PUBLIC_URL, --- so the deployment no longer stores one. Each node keeps the address it --- enrolled with, which tells an administrator which nodes a change affects. -ALTER TABLE runtime_nodes ADD COLUMN core_url text NOT NULL DEFAULT ''; -UPDATE runtime_nodes n SET core_url = d.core_url FROM runtime_deployment d WHERE d.singleton AND n.removed_at IS NULL; -ALTER TABLE runtime_deployment DROP CONSTRAINT runtime_deployment_setup_check; -ALTER TABLE runtime_deployment ADD CONSTRAINT runtime_deployment_setup_check CHECK ( - NOT web_managed OR (local_node_id IS NULL AND ( - (provider_kind='' AND mode='' AND generation=0 AND idle_seconds=0 AND retention_seconds=0) OR - (provider_kind='docker' AND mode='nodes' AND generation>0 AND idle_seconds=0 AND retention_seconds=0) OR - (provider_kind='microsandbox' AND mode='nodes' AND generation>0 AND idle_seconds>0 AND retention_seconds>0) OR - (provider_kind='e2b' AND mode='direct' AND generation>0 AND idle_seconds=0 AND retention_seconds=0) - )) -); -ALTER TABLE runtime_deployment DROP COLUMN core_url; - --- +goose Down -ALTER TABLE runtime_deployment ADD COLUMN core_url text NOT NULL DEFAULT ''; -UPDATE runtime_deployment d SET core_url = COALESCE((SELECT n.core_url FROM runtime_nodes n - WHERE n.removed_at IS NULL AND n.core_url <> '' ORDER BY n.created_at DESC LIMIT 1), '') -WHERE d.web_managed AND d.provider_kind <> ''; - --- +goose StatementBegin -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM runtime_deployment WHERE web_managed AND provider_kind <> '' AND core_url = '') THEN - RAISE EXCEPTION 'Cannot restore the sandbox deployment Core address: no enrolled node records it'; - END IF; -END $$; --- +goose StatementEnd - -ALTER TABLE runtime_deployment DROP CONSTRAINT runtime_deployment_setup_check; -ALTER TABLE runtime_deployment ADD CONSTRAINT runtime_deployment_setup_check CHECK ( - NOT web_managed OR (local_node_id IS NULL AND ( - (provider_kind='' AND core_url='' AND mode='' AND generation=0 AND idle_seconds=0 AND retention_seconds=0) OR - (provider_kind='docker' AND core_url<>'' AND mode='nodes' AND generation>0 AND idle_seconds=0 AND retention_seconds=0) OR - (provider_kind='microsandbox' AND core_url<>'' AND mode='nodes' AND generation>0 AND idle_seconds>0 AND retention_seconds>0) OR - (provider_kind='e2b' AND core_url<>'' AND mode='direct' AND generation>0 AND idle_seconds=0 AND retention_seconds=0) - )) -); -ALTER TABLE runtime_nodes DROP COLUMN core_url; diff --git a/services/core/migrations/000076_deployment_model_providers.sql b/services/core/migrations/000076_deployment_model_providers.sql deleted file mode 100644 index 8c6064619..000000000 --- a/services/core/migrations/000076_deployment_model_providers.sql +++ /dev/null @@ -1,39 +0,0 @@ --- +goose Up --- One deployment default model provider per harness. The safe columns serve --- reads without decryption; encrypted_config holds the complete bundle. -CREATE TABLE deployment_model_providers ( - harness text PRIMARY KEY, - protocol text NOT NULL, - base_url text NOT NULL, - context_window integer NOT NULL DEFAULT 0 CHECK (context_window >= 0), - max_output_tokens integer NOT NULL DEFAULT 0 CHECK (max_output_tokens >= 0), - encrypted_config bytea NOT NULL, - updated_at timestamptz NOT NULL DEFAULT clock_timestamp() -); - --- Deployment-wide administrator writes have no Project. -ALTER TABLE admin_audit_log ALTER COLUMN tenant_id DROP NOT NULL; -ALTER TABLE admin_audit_log ALTER COLUMN project_id DROP NOT NULL; -ALTER TABLE admin_audit_log ADD CONSTRAINT admin_audit_log_scope_check CHECK ((tenant_id IS NULL) = (project_id IS NULL)); - --- Input reserved before providers were required fails with this reason instead --- of waiting for its deadline. -ALTER TABLE environment_input_reservations ADD COLUMN failure_code text - CHECK (failure_code IS NULL OR (state = 'failed' AND failure_code = 'model_provider_required')); - --- +goose Down --- +goose StatementBegin -DO $$ BEGIN - IF EXISTS (SELECT 1 FROM admin_audit_log WHERE project_id IS NULL) THEN - RAISE EXCEPTION 'Deployment-wide audit entries prevent this downgrade; restore a complete database backup instead.'; - END IF; - IF EXISTS (SELECT 1 FROM deployment_model_providers) THEN - RAISE EXCEPTION 'Deployment model providers prevent this downgrade; remove them or restore a complete database backup instead.'; - END IF; -END $$; --- +goose StatementEnd -ALTER TABLE environment_input_reservations DROP COLUMN failure_code; -ALTER TABLE admin_audit_log DROP CONSTRAINT admin_audit_log_scope_check; -ALTER TABLE admin_audit_log ALTER COLUMN project_id SET NOT NULL; -ALTER TABLE admin_audit_log ALTER COLUMN tenant_id SET NOT NULL; -DROP TABLE deployment_model_providers; diff --git a/services/core/migrations/000077_node_enrollment_id.sql b/services/core/migrations/000077_node_enrollment_id.sql deleted file mode 100644 index 2caaf1b15..000000000 --- a/services/core/migrations/000077_node_enrollment_id.sql +++ /dev/null @@ -1,13 +0,0 @@ --- +goose Up --- A public, non-secret handle for each node enrollment command, so the console can --- tell which node a command registered. It never authenticates; the token does. --- Tokens issued before this migration have none. -ALTER TABLE runtime_node_enrollments ADD COLUMN id uuid UNIQUE; --- The enrollment that registered the node, copied at registration. There is no --- foreign key: runtime_node_enrollments.node_id already references the node, and a --- reverse key would only add a cycle. Nodes enrolled earlier keep null. -ALTER TABLE runtime_nodes ADD COLUMN enrollment_id uuid; - --- +goose Down -ALTER TABLE runtime_nodes DROP COLUMN enrollment_id; -ALTER TABLE runtime_node_enrollments DROP COLUMN id; diff --git a/services/core/migrations/000078_oac_runtime_names.sql b/services/core/migrations/000078_oac_runtime_names.sql deleted file mode 100644 index 62bb98455..000000000 --- a/services/core/migrations/000078_oac_runtime_names.sql +++ /dev/null @@ -1,29 +0,0 @@ --- +goose Up --- Admission also locks the deployment first. Keep the drain check and rewrite --- atomic with respect to allocation creation and cleanup. -LOCK TABLE runtime_deployment, runtime_allocations IN ACCESS EXCLUSIVE MODE; --- +goose StatementBegin -DO $$ BEGIN - IF EXISTS (SELECT 1 FROM runtime_allocations WHERE released_at IS NULL) THEN - RAISE EXCEPTION 'Drain the sandbox deployment with the previous release before upgrading to OpenAgentCore: sandboxes created before the rename cannot be managed by this release'; - END IF; -END $$; --- +goose StatementEnd -UPDATE runtime_deployment - SET specification = jsonb_set(specification, '{runtime,microsandbox_ref}', - to_jsonb('oac-runtime@' || split_part(specification #>> '{runtime,microsandbox_ref}', '@', 2)), false) - WHERE specification #>> '{runtime,microsandbox_ref}' LIKE 'parsar-core-runtime@%'; - --- +goose Down -LOCK TABLE runtime_deployment, runtime_allocations IN ACCESS EXCLUSIVE MODE; --- +goose StatementBegin -DO $$ BEGIN - IF EXISTS (SELECT 1 FROM runtime_allocations WHERE released_at IS NULL) THEN - RAISE EXCEPTION 'Drain the sandbox deployment before reverting OpenAgentCore Runtime names: sandboxes created after the rename cannot be managed by the previous release'; - END IF; -END $$; --- +goose StatementEnd -UPDATE runtime_deployment - SET specification = jsonb_set(specification, '{runtime,microsandbox_ref}', - to_jsonb('parsar-core-runtime@' || split_part(specification #>> '{runtime,microsandbox_ref}', '@', 2)), false) - WHERE specification #>> '{runtime,microsandbox_ref}' LIKE 'oac-runtime@%'; diff --git a/services/core/migrations/000079_sandbox_reset.sql b/services/core/migrations/000079_sandbox_reset.sql deleted file mode 100644 index 7c71dde23..000000000 --- a/services/core/migrations/000079_sandbox_reset.sql +++ /dev/null @@ -1,63 +0,0 @@ --- +goose Up -ALTER TABLE runtime_deployment RENAME COLUMN maintenance TO admission_paused; --- Old Web-managed maintenance is retired; ordinary upgrades resume admission. -UPDATE runtime_deployment SET admission_paused = false WHERE web_managed; -ALTER TABLE runtime_deployment - ADD COLUMN reset_clear text, - ADD COLUMN reset_requested_at timestamptz, - ADD COLUMN reset_deadline_at timestamptz, - ADD COLUMN reset_forced_at timestamptz, - ADD COLUMN reset_audit jsonb, - ADD CONSTRAINT runtime_deployment_reset_check CHECK ( - (reset_clear IS NULL AND reset_requested_at IS NULL AND reset_deadline_at IS NULL - AND reset_forced_at IS NULL AND reset_audit IS NULL) - OR (reset_clear IS NOT NULL AND web_managed AND provider_kind <> '' - AND reset_requested_at IS NOT NULL AND reset_audit IS NOT NULL - AND jsonb_typeof(reset_audit) = 'object' - AND ((reset_clear = 'auto' AND reset_deadline_at IS NOT NULL AND reset_forced_at IS NULL) - OR (reset_clear = 'force' AND reset_forced_at IS NOT NULL))) - ), - ADD CONSTRAINT runtime_deployment_reset_admission_check CHECK ( - NOT web_managed OR admission_paused = (reset_clear IS NOT NULL) - ); - -ALTER TABLE runtime_deployment DROP CONSTRAINT runtime_deployment_setup_check; -ALTER TABLE runtime_deployment ADD CONSTRAINT runtime_deployment_setup_check CHECK ( - NOT web_managed OR (local_node_id IS NULL AND ( - (provider_kind='' AND mode='' AND generation>=0 AND idle_seconds=0 AND retention_seconds=0) OR - (provider_kind='docker' AND mode='nodes' AND generation>0 AND idle_seconds=0 AND retention_seconds=0) OR - (provider_kind='microsandbox' AND mode='nodes' AND generation>0 AND idle_seconds>0 AND retention_seconds>0) OR - (provider_kind='e2b' AND mode='direct' AND generation>0 AND idle_seconds=0 AND retention_seconds=0) - )) -); - --- +goose Down --- +goose StatementBegin -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM runtime_deployment WHERE reset_clear IS NOT NULL) THEN - RAISE EXCEPTION 'Cannot downgrade while a sandbox reset is active'; - END IF; - IF EXISTS (SELECT 1 FROM runtime_deployment WHERE web_managed AND provider_kind = '' AND generation > 0) THEN - RAISE EXCEPTION 'Configure a sandbox provider before downgrading a completed reset'; - END IF; -END $$; --- +goose StatementEnd -ALTER TABLE runtime_deployment - DROP CONSTRAINT runtime_deployment_reset_admission_check, - DROP CONSTRAINT runtime_deployment_reset_check, - DROP COLUMN reset_clear, - DROP COLUMN reset_requested_at, - DROP COLUMN reset_deadline_at, - DROP COLUMN reset_forced_at, - DROP COLUMN reset_audit; -ALTER TABLE runtime_deployment RENAME COLUMN admission_paused TO maintenance; -ALTER TABLE runtime_deployment DROP CONSTRAINT runtime_deployment_setup_check; -ALTER TABLE runtime_deployment ADD CONSTRAINT runtime_deployment_setup_check CHECK ( - NOT web_managed OR (local_node_id IS NULL AND ( - (provider_kind='' AND mode='' AND generation=0 AND idle_seconds=0 AND retention_seconds=0) OR - (provider_kind='docker' AND mode='nodes' AND generation>0 AND idle_seconds=0 AND retention_seconds=0) OR - (provider_kind='microsandbox' AND mode='nodes' AND generation>0 AND idle_seconds>0 AND retention_seconds>0) OR - (provider_kind='e2b' AND mode='direct' AND generation>0 AND idle_seconds=0 AND retention_seconds=0) - )) -); diff --git a/services/core/migrations/000080_archived_cancellation_receipts.sql b/services/core/migrations/000080_archived_cancellation_receipts.sql deleted file mode 100644 index 97e0ff974..000000000 --- a/services/core/migrations/000080_archived_cancellation_receipts.sql +++ /dev/null @@ -1,17 +0,0 @@ --- +goose Up --- Only the archive transaction that first revokes the device may retain the --- existing delivery's cancellation receipt. Old revocations are not adopted. -ALTER TABLE devices - ADD COLUMN archive_cancel_turn_id uuid REFERENCES turns(id) ON DELETE SET NULL; - --- +goose Down --- +goose StatementBegin -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM devices d JOIN runtime_allocations a ON a.device_id = d.id - WHERE d.archive_cancel_turn_id IS NOT NULL AND a.state <> 'released') THEN - RAISE EXCEPTION 'Cannot remove archived cancellation receipts while cleanup is unsettled'; - END IF; -END $$; --- +goose StatementEnd -ALTER TABLE devices DROP COLUMN archive_cancel_turn_id; diff --git a/services/core/migrations/000081_sandbox_generations.sql b/services/core/migrations/000081_sandbox_generations.sql deleted file mode 100644 index e9655b3ff..000000000 --- a/services/core/migrations/000081_sandbox_generations.sql +++ /dev/null @@ -1,67 +0,0 @@ --- +goose Up -CREATE TABLE runtime_deployment_generations ( - generation bigint PRIMARY KEY CHECK (generation >= 0), - provider_kind text NOT NULL CHECK (provider_kind IN ('docker','microsandbox','e2b')), - specification jsonb NOT NULL, - e2b_template text NOT NULL DEFAULT '', - e2b_template_build_status text, - e2b_template_cpus integer, - e2b_template_memory_mib integer, - e2b_template_root_disk_mib integer, - created_at timestamptz NOT NULL DEFAULT clock_timestamp() -); -ALTER TABLE runtime_allocations ADD COLUMN deployment_generation bigint; -ALTER TABLE runtime_placements ADD COLUMN deployment_generation bigint; -UPDATE runtime_allocations SET deployment_generation=(SELECT generation FROM runtime_deployment) WHERE state <> 'released'; -UPDATE runtime_placements SET deployment_generation=(SELECT generation FROM runtime_deployment) WHERE released_at IS NULL; -ALTER TABLE runtime_allocations ADD CHECK (deployment_generation >= 0), - ADD CHECK (state = 'released' OR deployment_generation IS NOT NULL); -ALTER TABLE runtime_placements ADD CHECK (deployment_generation >= 0), - ADD CHECK (released_at IS NOT NULL OR deployment_generation IS NOT NULL); -ALTER TABLE runtime_nodes ADD COLUMN ready_generation bigint CHECK (ready_generation >= 0); --- The serving pin is durable, independent of current connection readiness. -UPDATE runtime_nodes SET ready_generation=deployment_generation WHERE removed_at IS NULL; -CREATE INDEX runtime_allocations_generation_live ON runtime_allocations(deployment_generation) WHERE state <> 'released'; -CREATE INDEX runtime_placements_generation_live ON runtime_placements(deployment_generation) WHERE released_at IS NULL; -CREATE INDEX runtime_nodes_generation_live ON runtime_nodes(ready_generation) WHERE removed_at IS NULL; --- +goose StatementBegin -CREATE FUNCTION immutable_runtime_deployment_generation() RETURNS trigger LANGUAGE plpgsql AS $$ -BEGIN - IF NEW.deployment_generation IS DISTINCT FROM OLD.deployment_generation THEN - RAISE EXCEPTION 'Sandbox ownership generation is immutable'; - END IF; - RETURN NEW; -END $$; --- +goose StatementEnd -CREATE TRIGGER immutable_runtime_allocation_generation BEFORE UPDATE ON runtime_allocations -FOR EACH ROW EXECUTE FUNCTION immutable_runtime_deployment_generation(); -CREATE TRIGGER immutable_runtime_placement_generation BEFORE UPDATE ON runtime_placements -FOR EACH ROW EXECUTE FUNCTION immutable_runtime_deployment_generation(); - --- +goose StatementBegin -CREATE FUNCTION immutable_sandbox_specification() RETURNS trigger LANGUAGE plpgsql AS $$ -BEGIN - RAISE EXCEPTION 'Retained sandbox specifications are immutable'; -END $$; --- +goose StatementEnd -CREATE TRIGGER immutable_sandbox_specification BEFORE UPDATE ON runtime_deployment_generations -FOR EACH ROW EXECUTE FUNCTION immutable_sandbox_specification(); - --- +goose Down --- +goose StatementBegin -DO $$ BEGIN - IF EXISTS (SELECT 1 FROM runtime_allocations a CROSS JOIN runtime_deployment d WHERE a.state <> 'released' AND a.deployment_generation <> d.generation) - OR EXISTS (SELECT 1 FROM runtime_placements p CROSS JOIN runtime_deployment d WHERE p.released_at IS NULL AND p.deployment_generation <> d.generation) - OR EXISTS (SELECT 1 FROM runtime_nodes n CROSS JOIN runtime_deployment d WHERE n.removed_at IS NULL AND n.ready_generation <> d.generation) THEN - RAISE EXCEPTION 'Cannot downgrade while retained ownership or node serving pins require generation routing'; - END IF; -END $$; --- +goose StatementEnd -DROP TRIGGER immutable_runtime_allocation_generation ON runtime_allocations; -DROP TRIGGER immutable_runtime_placement_generation ON runtime_placements; -DROP FUNCTION immutable_runtime_deployment_generation(); -ALTER TABLE runtime_nodes DROP COLUMN ready_generation; -ALTER TABLE runtime_placements DROP COLUMN deployment_generation; -ALTER TABLE runtime_allocations DROP COLUMN deployment_generation; -DROP TABLE runtime_deployment_generations; -DROP FUNCTION immutable_sandbox_specification(); diff --git a/services/core/migrations/000082_node_generations.sql b/services/core/migrations/000082_node_generations.sql deleted file mode 100644 index 1e1485518..000000000 --- a/services/core/migrations/000082_node_generations.sql +++ /dev/null @@ -1,30 +0,0 @@ --- +goose Up -ALTER TABLE runtime_nodes ADD COLUMN protocol_version integer NOT NULL DEFAULT 1 CHECK (protocol_version IN (1,2)); -CREATE TABLE runtime_node_generation_status ( - node_id uuid NOT NULL REFERENCES runtime_nodes(id), - generation bigint NOT NULL CHECK (generation > 0), - specification_digest text NOT NULL CHECK (specification_digest ~ '^[0-9a-f]{64}$'), - connection_id uuid NOT NULL, - owner_epoch bigint NOT NULL CHECK (owner_epoch > 0), - state text NOT NULL CHECK (state IN ('ready','preparing','failed')), - diagnostic text NOT NULL DEFAULT '', - observed_at timestamptz NOT NULL DEFAULT clock_timestamp(), - PRIMARY KEY(node_id,generation) -); -INSERT INTO runtime_node_generation_status(node_id,generation,specification_digest,connection_id,owner_epoch,state) -SELECT id,deployment_generation,specification_digest,connection_id,connected_epoch, - CASE WHEN provider_ready THEN 'ready' ELSE 'failed' END FROM runtime_nodes -WHERE removed_at IS NULL AND connection_id IS NOT NULL AND connected_epoch>0 -AND deployment_generation>0 AND specification_digest ~ '^[0-9a-f]{64}$'; - --- +goose Down --- +goose StatementBegin -DO $$ BEGIN - IF EXISTS(SELECT 1 FROM runtime_nodes n CROSS JOIN runtime_deployment d WHERE n.removed_at IS NULL - AND (n.protocol_version=2 OR n.deployment_generation<>d.generation OR n.ready_generation<>d.generation)) THEN - RAISE EXCEPTION 'Cannot downgrade while protocol v2 or retained-generation nodes remain; remove them after releasing their resources first'; - END IF; -END $$; --- +goose StatementEnd -DROP TABLE runtime_node_generation_status; -ALTER TABLE runtime_nodes DROP COLUMN protocol_version; diff --git a/services/core/migrations/000083_session_diagnostics.sql b/services/core/migrations/000083_session_diagnostics.sql deleted file mode 100644 index 80be75a10..000000000 --- a/services/core/migrations/000083_session_diagnostics.sql +++ /dev/null @@ -1,8 +0,0 @@ --- +goose Up --- Historical terminal Items retain unknown settlement times. -ALTER TABLE session_items ADD COLUMN settled_at timestamptz; -ALTER TABLE environments ADD COLUMN failure_detail jsonb CHECK (failure_detail IS NULL OR jsonb_typeof(failure_detail) = 'object'); - --- +goose Down -ALTER TABLE environments DROP COLUMN failure_detail; -ALTER TABLE session_items DROP COLUMN settled_at; diff --git a/services/core/migrations/000084_deployment_provider_observations.sql b/services/core/migrations/000084_deployment_provider_observations.sql deleted file mode 100644 index 301a8ef33..000000000 --- a/services/core/migrations/000084_deployment_provider_observations.sql +++ /dev/null @@ -1,27 +0,0 @@ --- +goose Up --- Revision identity is private and independent of content and wall-clock time. -ALTER TABLE deployment_model_providers - ADD COLUMN revision uuid NOT NULL DEFAULT gen_random_uuid(), - ADD COLUMN last_used_at timestamptz, - ADD COLUMN last_error_code text, - ADD COLUMN last_error_at timestamptz, - ADD COLUMN recovery_pending boolean NOT NULL DEFAULT false, - ADD CONSTRAINT deployment_provider_error_pair CHECK ((last_error_code IS NULL) = (last_error_at IS NULL)), - ADD CONSTRAINT deployment_provider_recovery CHECK (NOT recovery_pending OR last_error_at IS NOT NULL), - ADD CONSTRAINT deployment_provider_error_code CHECK (last_error_code IN ( - 'authentication_error', 'connection_failed', 'rate_limit_exceeded', - 'usage_limit_exceeded', 'server_overloaded', 'server_error', - 'resource_not_found', 'request_timeout', 'invalid_request')); -ALTER TABLE session_execution_configuration ADD COLUMN deployment_provider_revision uuid; - --- +goose Down -ALTER TABLE session_execution_configuration DROP COLUMN deployment_provider_revision; -ALTER TABLE deployment_model_providers - DROP CONSTRAINT deployment_provider_error_pair, - DROP CONSTRAINT deployment_provider_recovery, - DROP CONSTRAINT deployment_provider_error_code, - DROP COLUMN revision, - DROP COLUMN last_used_at, - DROP COLUMN last_error_code, - DROP COLUMN last_error_at, - DROP COLUMN recovery_pending; diff --git a/services/core/migrations/000085_deployment_model_configuration.sql b/services/core/migrations/000085_deployment_model_configuration.sql deleted file mode 100644 index 82a3e6140..000000000 --- a/services/core/migrations/000085_deployment_model_configuration.sql +++ /dev/null @@ -1,7 +0,0 @@ --- +goose Up -ALTER TABLE deployment_model_providers ADD COLUMN model text NOT NULL DEFAULT ''; -ALTER TABLE deployment_model_providers ADD COLUMN harness_config jsonb NOT NULL DEFAULT '{}'::jsonb; - --- +goose Down -ALTER TABLE deployment_model_providers DROP COLUMN harness_config; -ALTER TABLE deployment_model_providers DROP COLUMN model; diff --git a/services/core/migrations/000086_e2b_custom_endpoint.sql b/services/core/migrations/000086_e2b_custom_endpoint.sql deleted file mode 100644 index 9644ce087..000000000 --- a/services/core/migrations/000086_e2b_custom_endpoint.sql +++ /dev/null @@ -1,26 +0,0 @@ --- +goose Up -ALTER TABLE runtime_deployment ADD COLUMN IF NOT EXISTS e2b_api_url text NOT NULL DEFAULT ''; -ALTER TABLE runtime_deployment ADD COLUMN IF NOT EXISTS e2b_domain text NOT NULL DEFAULT ''; --- +goose StatementBegin -DO $$ BEGIN -IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'runtime_deployment_e2b_endpoint_check') THEN -ALTER TABLE runtime_deployment ADD CONSTRAINT runtime_deployment_e2b_endpoint_check CHECK ( - (provider_kind = 'e2b' AND ((e2b_api_url = '' AND e2b_domain = '') OR - (e2b_api_url <> '' AND e2b_domain <> ''))) OR - (provider_kind <> 'e2b' AND e2b_api_url = '' AND e2b_domain = '') -); -END IF; -END $$; --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -DO $$ BEGIN - IF EXISTS (SELECT 1 FROM runtime_deployment WHERE e2b_api_url <> '' OR e2b_domain <> '') THEN - RAISE EXCEPTION 'cannot remove E2B endpoint columns while a custom endpoint is configured'; - END IF; -END $$; --- +goose StatementEnd -ALTER TABLE runtime_deployment DROP CONSTRAINT runtime_deployment_e2b_endpoint_check; -ALTER TABLE runtime_deployment DROP COLUMN e2b_domain; -ALTER TABLE runtime_deployment DROP COLUMN e2b_api_url; diff --git a/services/core/migrations/000087_e2b_generation_endpoints.sql b/services/core/migrations/000087_e2b_generation_endpoints.sql deleted file mode 100644 index 4f0c5d888..000000000 --- a/services/core/migrations/000087_e2b_generation_endpoints.sql +++ /dev/null @@ -1,21 +0,0 @@ --- +goose Up --- Earlier generation rows predate custom endpoints and use the official defaults. -ALTER TABLE runtime_deployment_generations ADD COLUMN e2b_api_url text NOT NULL DEFAULT ''; -ALTER TABLE runtime_deployment_generations ADD COLUMN e2b_domain text NOT NULL DEFAULT ''; -ALTER TABLE runtime_deployment_generations ADD CONSTRAINT runtime_deployment_generation_e2b_endpoint_check CHECK ( - (provider_kind = 'e2b' AND ((e2b_api_url = '' AND e2b_domain = '') OR - (e2b_api_url <> '' AND e2b_domain <> ''))) OR - (provider_kind <> 'e2b' AND e2b_api_url = '' AND e2b_domain = '') -); - --- +goose Down --- +goose StatementBegin -DO $$ BEGIN - IF EXISTS (SELECT 1 FROM runtime_deployment_generations WHERE e2b_api_url <> '' OR e2b_domain <> '') THEN - RAISE EXCEPTION 'cannot remove E2B generation endpoints while a custom endpoint is retained'; - END IF; -END $$; --- +goose StatementEnd -ALTER TABLE runtime_deployment_generations DROP CONSTRAINT runtime_deployment_generation_e2b_endpoint_check; -ALTER TABLE runtime_deployment_generations DROP COLUMN e2b_domain; -ALTER TABLE runtime_deployment_generations DROP COLUMN e2b_api_url; diff --git a/services/core/migrations/000088_environment_input_preparation_failure.sql b/services/core/migrations/000088_environment_input_preparation_failure.sql deleted file mode 100644 index 06b57a41d..000000000 --- a/services/core/migrations/000088_environment_input_preparation_failure.sql +++ /dev/null @@ -1,11 +0,0 @@ --- +goose Up -ALTER TABLE environment_input_reservations - DROP CONSTRAINT environment_input_reservations_check1, - ADD CONSTRAINT environment_input_reservations_check1 - CHECK (failure_code IS NULL OR (state = 'failed' AND failure_code IN ('model_provider_required', 'runtime_preparation_failed'))); - --- +goose Down -ALTER TABLE environment_input_reservations - DROP CONSTRAINT environment_input_reservations_check1, - ADD CONSTRAINT environment_input_reservations_check1 - CHECK (failure_code IS NULL OR (state = 'failed' AND failure_code = 'model_provider_required')); diff --git a/services/core/migrations/000089_provider_registration.sql b/services/core/migrations/000089_provider_registration.sql deleted file mode 100644 index 4a4e743ed..000000000 --- a/services/core/migrations/000089_provider_registration.sql +++ /dev/null @@ -1,82 +0,0 @@ --- +goose Up --- Provider registration owns supported kinds, modes and native configuration. --- Persistence enforces shape and ownership without enumerating implementations. -ALTER TABLE runtime_deployment DROP CONSTRAINT runtime_deployment_provider_kind_check; -ALTER TABLE runtime_deployment ADD CONSTRAINT runtime_deployment_provider_kind_check - CHECK (provider_kind = '' OR provider_kind ~ '^[a-z][a-z0-9_-]{0,63}$'); -ALTER TABLE runtime_deployment_generations DROP CONSTRAINT runtime_deployment_generations_provider_kind_check; -ALTER TABLE runtime_deployment_generations ADD CONSTRAINT runtime_deployment_generations_provider_kind_check - CHECK (provider_kind ~ '^[a-z][a-z0-9_-]{0,63}$'); -ALTER TABLE runtime_deployment DROP CONSTRAINT runtime_deployment_setup_check; -ALTER TABLE runtime_deployment ADD CONSTRAINT runtime_deployment_setup_check CHECK ( - NOT web_managed OR (local_node_id IS NULL AND ( - (provider_kind = '' AND mode = '' AND generation >= 0 AND idle_seconds = 0 AND retention_seconds = 0) OR - (provider_kind <> '' AND mode IN ('nodes','direct') AND generation > 0 AND - ((idle_seconds = 0 AND retention_seconds = 0) OR (idle_seconds > 0 AND retention_seconds > 0))) - )) -); -ALTER TABLE runtime_deployment DROP CONSTRAINT runtime_deployment_e2b_check; -ALTER TABLE runtime_deployment ADD CONSTRAINT runtime_deployment_e2b_check CHECK ( - (provider_kind <> '' AND e2b_template <> '' AND e2b_credential IS NOT NULL) OR - (e2b_template = '' AND e2b_credential IS NULL) -); -ALTER TABLE runtime_deployment DROP CONSTRAINT runtime_deployment_e2b_template_build_check; -ALTER TABLE runtime_deployment ADD CONSTRAINT runtime_deployment_e2b_template_build_check CHECK ( - e2b_template <> '' OR (e2b_template_build_status IS NULL AND e2b_template_cpus IS NULL AND - e2b_template_memory_mib IS NULL AND e2b_template_root_disk_mib IS NULL) -); -ALTER TABLE runtime_deployment DROP CONSTRAINT IF EXISTS runtime_deployment_e2b_endpoint_check; -ALTER TABLE runtime_deployment ADD CONSTRAINT runtime_deployment_e2b_endpoint_check CHECK ( - (e2b_api_url = '' AND e2b_domain = '') OR - (e2b_template <> '' AND e2b_api_url <> '' AND e2b_domain <> '') -); -ALTER TABLE runtime_deployment_generations DROP CONSTRAINT runtime_deployment_generation_e2b_endpoint_check; -ALTER TABLE runtime_deployment_generations ADD CONSTRAINT runtime_deployment_generation_e2b_endpoint_check CHECK ( - (e2b_api_url = '' AND e2b_domain = '') OR - (e2b_template <> '' AND e2b_api_url <> '' AND e2b_domain <> '') -); - --- +goose Down --- Restore the previous representation of official endpoints. Custom endpoints --- remain explicit so older downgrade guards still protect their ownership. -UPDATE runtime_deployment SET e2b_api_url = '', e2b_domain = '' -WHERE e2b_api_url = 'https://api.e2b.app' AND e2b_domain = 'e2b.app'; --- This transaction changes representation only, not the endpoint or resource owner. -ALTER TABLE runtime_deployment_generations DISABLE TRIGGER immutable_sandbox_specification; -UPDATE runtime_deployment_generations SET e2b_api_url = '', e2b_domain = '' -WHERE e2b_api_url = 'https://api.e2b.app' AND e2b_domain = 'e2b.app'; -ALTER TABLE runtime_deployment_generations ENABLE TRIGGER immutable_sandbox_specification; --- Narrowing registration cannot silently invalidate retained resource ownership. -ALTER TABLE runtime_deployment DROP CONSTRAINT runtime_deployment_provider_kind_check; -ALTER TABLE runtime_deployment ADD CONSTRAINT runtime_deployment_provider_kind_check CHECK (provider_kind IN ('','docker','microsandbox','e2b')); -ALTER TABLE runtime_deployment_generations DROP CONSTRAINT runtime_deployment_generations_provider_kind_check; -ALTER TABLE runtime_deployment_generations ADD CONSTRAINT runtime_deployment_generations_provider_kind_check CHECK (provider_kind IN ('docker','microsandbox','e2b')); -ALTER TABLE runtime_deployment DROP CONSTRAINT runtime_deployment_setup_check; -ALTER TABLE runtime_deployment ADD CONSTRAINT runtime_deployment_setup_check CHECK ( - NOT web_managed OR (local_node_id IS NULL AND ( - (provider_kind='' AND mode='' AND generation>=0 AND idle_seconds=0 AND retention_seconds=0) OR - (provider_kind='docker' AND mode='nodes' AND generation>0 AND idle_seconds=0 AND retention_seconds=0) OR - (provider_kind='microsandbox' AND mode='nodes' AND generation>0 AND idle_seconds>0 AND retention_seconds>0) OR - (provider_kind='e2b' AND mode='direct' AND generation>0 AND idle_seconds=0 AND retention_seconds=0) - )) -); -ALTER TABLE runtime_deployment DROP CONSTRAINT runtime_deployment_e2b_check; -ALTER TABLE runtime_deployment ADD CONSTRAINT runtime_deployment_e2b_check CHECK ( - (provider_kind='e2b' AND e2b_template<>'' AND e2b_credential IS NOT NULL) OR - (provider_kind<>'e2b' AND e2b_template='' AND e2b_credential IS NULL) -); -ALTER TABLE runtime_deployment DROP CONSTRAINT runtime_deployment_e2b_template_build_check; -ALTER TABLE runtime_deployment ADD CONSTRAINT runtime_deployment_e2b_template_build_check CHECK ( - provider_kind = 'e2b' OR (e2b_template_build_status IS NULL AND e2b_template_cpus IS NULL AND - e2b_template_memory_mib IS NULL AND e2b_template_root_disk_mib IS NULL) -); -ALTER TABLE runtime_deployment DROP CONSTRAINT runtime_deployment_e2b_endpoint_check; -ALTER TABLE runtime_deployment ADD CONSTRAINT runtime_deployment_e2b_endpoint_check CHECK ( - (provider_kind = 'e2b' AND ((e2b_api_url = '' AND e2b_domain = '') OR (e2b_api_url <> '' AND e2b_domain <> ''))) OR - (provider_kind <> 'e2b' AND e2b_api_url = '' AND e2b_domain = '') -); -ALTER TABLE runtime_deployment_generations DROP CONSTRAINT runtime_deployment_generation_e2b_endpoint_check; -ALTER TABLE runtime_deployment_generations ADD CONSTRAINT runtime_deployment_generation_e2b_endpoint_check CHECK ( - (provider_kind = 'e2b' AND ((e2b_api_url = '' AND e2b_domain = '') OR (e2b_api_url <> '' AND e2b_domain <> ''))) OR - (provider_kind <> 'e2b' AND e2b_api_url = '' AND e2b_domain = '') -); diff --git a/services/core/migrations/000090_environment_initialization.sql b/services/core/migrations/000090_environment_initialization.sql deleted file mode 100644 index ac5354f8d..000000000 --- a/services/core/migrations/000090_environment_initialization.sql +++ /dev/null @@ -1,9 +0,0 @@ --- +goose Up -ALTER TABLE environments ADD COLUMN initialization text NOT NULL DEFAULT 'complete' - CHECK (initialization IN ('pending', 'running', 'complete', 'failed')); -ALTER TABLE runtime_allocations DROP COLUMN initialization; - --- +goose Down -ALTER TABLE runtime_allocations ADD COLUMN initialization text NOT NULL DEFAULT 'complete' - CHECK (initialization IN ('pending', 'running', 'complete')); -ALTER TABLE environments DROP COLUMN initialization; diff --git a/services/core/migrations/000091_provider_configuration.sql b/services/core/migrations/000091_provider_configuration.sql deleted file mode 100644 index 9241d3f39..000000000 --- a/services/core/migrations/000091_provider_configuration.sql +++ /dev/null @@ -1,117 +0,0 @@ --- +goose Up --- Copy only public selectors/observations. Ciphertext and its installation/generation --- associated data are unchanged; credentials are never decoded by SQL. -ALTER TABLE runtime_deployment RENAME COLUMN e2b_credential TO provider_credential; -ALTER TABLE runtime_deployment ADD COLUMN provider_config jsonb NOT NULL DEFAULT '{}' CHECK (jsonb_typeof(provider_config)='object'); -ALTER TABLE runtime_deployment ADD COLUMN provider_metadata jsonb NOT NULL DEFAULT '{}' CHECK (jsonb_typeof(provider_metadata)='object'); -UPDATE runtime_deployment SET -provider_config=jsonb_build_object('template',e2b_template,'api_url',e2b_api_url,'domain',e2b_domain), -provider_metadata=jsonb_build_object('template_build',jsonb_build_object('status',e2b_template_build_status,'resources', - jsonb_build_object('cpus',e2b_template_cpus,'memory_mib',e2b_template_memory_mib,'root_disk_mib',e2b_template_root_disk_mib))) -WHERE e2b_template<>''; -ALTER TABLE runtime_deployment DROP COLUMN e2b_template; -ALTER TABLE runtime_deployment DROP COLUMN e2b_api_url; -ALTER TABLE runtime_deployment DROP COLUMN e2b_domain; -ALTER TABLE runtime_deployment DROP COLUMN e2b_template_build_status; -ALTER TABLE runtime_deployment DROP COLUMN e2b_template_cpus; -ALTER TABLE runtime_deployment DROP COLUMN e2b_template_memory_mib; -ALTER TABLE runtime_deployment DROP COLUMN e2b_template_root_disk_mib; -ALTER TABLE runtime_deployment_generations ADD COLUMN provider_config jsonb NOT NULL DEFAULT '{}' CHECK (jsonb_typeof(provider_config)='object'); -ALTER TABLE runtime_deployment_generations ADD COLUMN provider_metadata jsonb NOT NULL DEFAULT '{}' CHECK (jsonb_typeof(provider_metadata)='object'); -ALTER TABLE runtime_deployment_generations DISABLE TRIGGER immutable_sandbox_specification; -UPDATE runtime_deployment_generations SET -provider_config=jsonb_build_object('template',e2b_template,'api_url',e2b_api_url,'domain',e2b_domain), -provider_metadata=jsonb_build_object('template_build',jsonb_build_object('status',e2b_template_build_status,'resources', - jsonb_build_object('cpus',e2b_template_cpus,'memory_mib',e2b_template_memory_mib,'root_disk_mib',e2b_template_root_disk_mib))) -WHERE e2b_template<>''; -ALTER TABLE runtime_deployment_generations ENABLE TRIGGER immutable_sandbox_specification; -ALTER TABLE runtime_deployment_generations DROP COLUMN e2b_template; -ALTER TABLE runtime_deployment_generations DROP COLUMN e2b_api_url; -ALTER TABLE runtime_deployment_generations DROP COLUMN e2b_domain; -ALTER TABLE runtime_deployment_generations DROP COLUMN e2b_template_build_status; -ALTER TABLE runtime_deployment_generations DROP COLUMN e2b_template_cpus; -ALTER TABLE runtime_deployment_generations DROP COLUMN e2b_template_memory_mib; -ALTER TABLE runtime_deployment_generations DROP COLUMN e2b_template_root_disk_mib; - --- +goose Down --- Reject a lossy downgrade; no unknown provider configuration may be discarded. --- +goose StatementBegin -DO $$ BEGIN -IF EXISTS (SELECT 1 FROM runtime_deployment WHERE provider_kind NOT IN ('','docker','microsandbox','e2b')) - OR EXISTS (SELECT 1 FROM runtime_deployment_generations WHERE provider_kind NOT IN ('docker','microsandbox','e2b')) THEN - RAISE EXCEPTION 'Cannot downgrade provider configurations unknown to the previous schema'; -END IF; -END $$; --- +goose StatementEnd --- +goose StatementBegin -DO $$ BEGIN -IF EXISTS (SELECT 1 FROM runtime_deployment WHERE - (provider_kind<>'e2b' AND (provider_config<>'{}'::jsonb OR provider_metadata<>'{}'::jsonb)) OR - (provider_kind='e2b' AND (provider_config - ARRAY['template','api_url','domain'] <> '{}'::jsonb OR - provider_metadata - 'template_build' <> '{}'::jsonb OR - COALESCE(provider_metadata->'template_build','{}'::jsonb) - ARRAY['status','resources'] <> '{}'::jsonb OR - COALESCE(provider_metadata#>'{template_build,resources}','{}'::jsonb) - ARRAY['cpus','memory_mib','root_disk_mib'] <> '{}'::jsonb))) THEN - RAISE EXCEPTION 'Cannot downgrade provider configuration without losing fields'; -END IF; -END $$; --- +goose StatementEnd -ALTER TABLE runtime_deployment ADD COLUMN e2b_template text NOT NULL DEFAULT ''; -ALTER TABLE runtime_deployment ADD COLUMN e2b_api_url text NOT NULL DEFAULT ''; -ALTER TABLE runtime_deployment ADD COLUMN e2b_domain text NOT NULL DEFAULT ''; -ALTER TABLE runtime_deployment ADD COLUMN e2b_template_build_status text; -ALTER TABLE runtime_deployment ADD COLUMN e2b_template_cpus integer; -ALTER TABLE runtime_deployment ADD COLUMN e2b_template_memory_mib integer; -ALTER TABLE runtime_deployment ADD COLUMN e2b_template_root_disk_mib integer; -UPDATE runtime_deployment SET - e2b_template=COALESCE(provider_config->>'template',''),e2b_api_url=COALESCE(provider_config->>'api_url',''),e2b_domain=COALESCE(provider_config->>'domain',''), - e2b_template_build_status=provider_metadata#>>'{template_build,status}', - e2b_template_cpus=(provider_metadata#>>'{template_build,resources,cpus}')::integer, - e2b_template_memory_mib=(provider_metadata#>>'{template_build,resources,memory_mib}')::integer, - e2b_template_root_disk_mib=(provider_metadata#>>'{template_build,resources,root_disk_mib}')::integer; -ALTER TABLE runtime_deployment DROP COLUMN provider_config, DROP COLUMN provider_metadata; --- +goose StatementBegin -DO $$ BEGIN -IF EXISTS (SELECT 1 FROM runtime_deployment_generations WHERE - (provider_kind<>'e2b' AND (provider_config<>'{}'::jsonb OR provider_metadata<>'{}'::jsonb)) OR - (provider_kind='e2b' AND (provider_config - ARRAY['template','api_url','domain'] <> '{}'::jsonb OR - provider_metadata - 'template_build' <> '{}'::jsonb OR - COALESCE(provider_metadata->'template_build','{}'::jsonb) - ARRAY['status','resources'] <> '{}'::jsonb OR - COALESCE(provider_metadata#>'{template_build,resources}','{}'::jsonb) - ARRAY['cpus','memory_mib','root_disk_mib'] <> '{}'::jsonb))) THEN - RAISE EXCEPTION 'Cannot downgrade provider configuration without losing fields'; -END IF; -END $$; --- +goose StatementEnd -ALTER TABLE runtime_deployment_generations ADD COLUMN e2b_template text NOT NULL DEFAULT ''; -ALTER TABLE runtime_deployment_generations ADD COLUMN e2b_api_url text NOT NULL DEFAULT ''; -ALTER TABLE runtime_deployment_generations ADD COLUMN e2b_domain text NOT NULL DEFAULT ''; -ALTER TABLE runtime_deployment_generations ADD COLUMN e2b_template_build_status text; -ALTER TABLE runtime_deployment_generations ADD COLUMN e2b_template_cpus integer; -ALTER TABLE runtime_deployment_generations ADD COLUMN e2b_template_memory_mib integer; -ALTER TABLE runtime_deployment_generations ADD COLUMN e2b_template_root_disk_mib integer; -ALTER TABLE runtime_deployment_generations DISABLE TRIGGER immutable_sandbox_specification; -UPDATE runtime_deployment_generations SET - e2b_template=COALESCE(provider_config->>'template',''),e2b_api_url=COALESCE(provider_config->>'api_url',''),e2b_domain=COALESCE(provider_config->>'domain',''), - e2b_template_build_status=provider_metadata#>>'{template_build,status}', - e2b_template_cpus=(provider_metadata#>>'{template_build,resources,cpus}')::integer, - e2b_template_memory_mib=(provider_metadata#>>'{template_build,resources,memory_mib}')::integer, - e2b_template_root_disk_mib=(provider_metadata#>>'{template_build,resources,root_disk_mib}')::integer; -ALTER TABLE runtime_deployment_generations ENABLE TRIGGER immutable_sandbox_specification; -ALTER TABLE runtime_deployment_generations DROP COLUMN provider_config, DROP COLUMN provider_metadata; -ALTER TABLE runtime_deployment RENAME COLUMN provider_credential TO e2b_credential; -ALTER TABLE runtime_deployment ADD CONSTRAINT runtime_deployment_e2b_check CHECK ( - (provider_kind <> '' AND e2b_template <> '' AND e2b_credential IS NOT NULL) OR - (e2b_template = '' AND e2b_credential IS NULL) -); -ALTER TABLE runtime_deployment ADD CONSTRAINT runtime_deployment_e2b_template_build_check CHECK ( - e2b_template <> '' OR (e2b_template_build_status IS NULL AND e2b_template_cpus IS NULL AND - e2b_template_memory_mib IS NULL AND e2b_template_root_disk_mib IS NULL) -); -ALTER TABLE runtime_deployment ADD CONSTRAINT runtime_deployment_e2b_endpoint_check CHECK ( - (e2b_api_url = '' AND e2b_domain = '') OR - (e2b_template <> '' AND e2b_api_url <> '' AND e2b_domain <> '') -); -ALTER TABLE runtime_deployment_generations ADD CONSTRAINT runtime_deployment_generation_e2b_endpoint_check CHECK ( - (e2b_api_url = '' AND e2b_domain = '') OR - (e2b_template <> '' AND e2b_api_url <> '' AND e2b_domain <> '') -); - diff --git a/services/core/migrations/000092_delete_admin_copies.sql b/services/core/migrations/000092_delete_admin_copies.sql deleted file mode 100644 index 046f74a72..000000000 --- a/services/core/migrations/000092_delete_admin_copies.sql +++ /dev/null @@ -1,24 +0,0 @@ --- +goose Up -DROP TABLE admin_resource_owners; -DROP TABLE admin_asset_copies; -ALTER TABLE admin_audit_log DROP COLUMN result_ids; - --- +goose Down -ALTER TABLE admin_audit_log ADD COLUMN result_ids jsonb NOT NULL DEFAULT '[]'; -CREATE TABLE admin_asset_copies ( - target_tenant_id uuid NOT NULL, - idempotency_key text NOT NULL, - request_hash bytea NOT NULL, - result jsonb NOT NULL, - audit_id uuid NOT NULL REFERENCES admin_audit_log(id), - PRIMARY KEY (target_tenant_id, idempotency_key) -); -CREATE TABLE admin_resource_owners ( - tenant_id uuid NOT NULL, - resource_type text NOT NULL, - resource_id text NOT NULL, - parent_id text NOT NULL DEFAULT '', - audit_id uuid NOT NULL REFERENCES admin_audit_log(id), - PRIMARY KEY (tenant_id, resource_type, resource_id) -); -CREATE INDEX admin_resource_owners_audit ON admin_resource_owners(audit_id); diff --git a/services/core/migrations/000093_web_deployment_only.sql b/services/core/migrations/000093_web_deployment_only.sql deleted file mode 100644 index 54886e099..000000000 --- a/services/core/migrations/000093_web_deployment_only.sql +++ /dev/null @@ -1,78 +0,0 @@ --- +goose Up --- Web setup is the only writer of the installation, so a recorded installation --- is Web-managed, no deployment names a process-configured local node, every --- node has the Core address it enrolled with, and only a sandbox reset pauses --- admission. --- +goose StatementBegin -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM runtime_deployment WHERE local_node_id IS NOT NULL OR (installation_id IS NOT NULL AND NOT web_managed) - OR admission_paused <> (reset_clear IS NOT NULL)) - OR EXISTS (SELECT 1 FROM runtime_nodes WHERE removed_at IS NULL AND core_url = '') THEN - RAISE EXCEPTION 'Cannot upgrade: the sandbox deployment was configured outside Web setup, or a live node has no Core address. Reset the sandbox in Web, then upgrade'; - END IF; -END $$; --- +goose StatementEnd -ALTER TABLE runtime_deployment - DROP CONSTRAINT runtime_deployment_identity_check, - DROP CONSTRAINT runtime_deployment_setup_check, - DROP CONSTRAINT runtime_deployment_reset_check, - DROP CONSTRAINT runtime_deployment_reset_admission_check, - DROP COLUMN web_managed, - DROP COLUMN local_node_id, - DROP COLUMN admission_paused, - ADD CONSTRAINT runtime_deployment_identity_check CHECK ( - (installation_id IS NULL AND backend_fingerprint = '') OR - (installation_id IS NOT NULL AND backend_fingerprint ~ '^[0-9a-f]{64}$') OR - (installation_id IS NOT NULL AND provider_kind = '' AND backend_fingerprint = '') - ), - ADD CONSTRAINT runtime_deployment_setup_check CHECK ( - installation_id IS NULL OR - (provider_kind = '' AND mode = '' AND generation >= 0 AND idle_seconds = 0 AND retention_seconds = 0) OR - (provider_kind <> '' AND mode IN ('nodes','direct') AND generation > 0 AND - ((idle_seconds = 0 AND retention_seconds = 0) OR (idle_seconds > 0 AND retention_seconds > 0))) - ), - ADD CONSTRAINT runtime_deployment_reset_check CHECK ( - (reset_clear IS NULL AND reset_requested_at IS NULL AND reset_deadline_at IS NULL - AND reset_forced_at IS NULL AND reset_audit IS NULL) - OR (reset_clear IS NOT NULL AND installation_id IS NOT NULL AND provider_kind <> '' - AND reset_requested_at IS NOT NULL AND reset_audit IS NOT NULL - AND jsonb_typeof(reset_audit) = 'object' - AND ((reset_clear = 'auto' AND reset_deadline_at IS NOT NULL AND reset_forced_at IS NULL) - OR (reset_clear = 'force' AND reset_forced_at IS NOT NULL))) - ); - --- +goose Down -ALTER TABLE runtime_deployment - DROP CONSTRAINT runtime_deployment_identity_check, - DROP CONSTRAINT runtime_deployment_setup_check, - DROP CONSTRAINT runtime_deployment_reset_check, - ADD COLUMN web_managed boolean NOT NULL DEFAULT false, - ADD COLUMN local_node_id uuid, - ADD COLUMN admission_paused boolean NOT NULL DEFAULT false; -UPDATE runtime_deployment SET web_managed = installation_id IS NOT NULL, admission_paused = reset_clear IS NOT NULL; -ALTER TABLE runtime_deployment - ADD CONSTRAINT runtime_deployment_identity_check CHECK ( - (installation_id IS NULL AND backend_fingerprint = '') OR - (installation_id IS NOT NULL AND backend_fingerprint ~ '^[0-9a-f]{64}$') OR - (web_managed AND installation_id IS NOT NULL AND provider_kind = '' AND backend_fingerprint = '') - ), - ADD CONSTRAINT runtime_deployment_setup_check CHECK ( - NOT web_managed OR (local_node_id IS NULL AND ( - (provider_kind = '' AND mode = '' AND generation >= 0 AND idle_seconds = 0 AND retention_seconds = 0) OR - (provider_kind <> '' AND mode IN ('nodes','direct') AND generation > 0 AND - ((idle_seconds = 0 AND retention_seconds = 0) OR (idle_seconds > 0 AND retention_seconds > 0))) - )) - ), - ADD CONSTRAINT runtime_deployment_reset_check CHECK ( - (reset_clear IS NULL AND reset_requested_at IS NULL AND reset_deadline_at IS NULL - AND reset_forced_at IS NULL AND reset_audit IS NULL) - OR (reset_clear IS NOT NULL AND web_managed AND provider_kind <> '' - AND reset_requested_at IS NOT NULL AND reset_audit IS NOT NULL - AND jsonb_typeof(reset_audit) = 'object' - AND ((reset_clear = 'auto' AND reset_deadline_at IS NOT NULL AND reset_forced_at IS NULL) - OR (reset_clear = 'force' AND reset_forced_at IS NOT NULL))) - ), - ADD CONSTRAINT runtime_deployment_reset_admission_check CHECK ( - NOT web_managed OR admission_paused = (reset_clear IS NOT NULL) - ); diff --git a/services/core/migrations/000094_suspension_from_declaration.sql b/services/core/migrations/000094_suspension_from_declaration.sql deleted file mode 100644 index 67bd968aa..000000000 --- a/services/core/migrations/000094_suspension_from_declaration.sql +++ /dev/null @@ -1,26 +0,0 @@ --- +goose Up --- Core derives the idle suspension policy from the Provider's checkpoint --- declaration, so the deployment no longer stores it. -ALTER TABLE runtime_deployment - DROP CONSTRAINT runtime_deployment_setup_check, - DROP COLUMN idle_seconds, - DROP COLUMN retention_seconds, - ADD CONSTRAINT runtime_deployment_setup_check CHECK ( - installation_id IS NULL OR - (provider_kind = '' AND mode = '' AND generation >= 0) OR - (provider_kind <> '' AND mode IN ('nodes','direct') AND generation > 0) - ); - --- +goose Down -ALTER TABLE runtime_deployment - DROP CONSTRAINT runtime_deployment_setup_check, - ADD COLUMN idle_seconds bigint NOT NULL DEFAULT 0 CHECK (idle_seconds >= 0), - ADD COLUMN retention_seconds bigint NOT NULL DEFAULT 0 CHECK (retention_seconds >= 0); -UPDATE runtime_deployment SET idle_seconds = 300, retention_seconds = 86400 WHERE provider_kind = 'microsandbox'; -ALTER TABLE runtime_deployment - ADD CONSTRAINT runtime_deployment_setup_check CHECK ( - installation_id IS NULL OR - (provider_kind = '' AND mode = '' AND generation >= 0 AND idle_seconds = 0 AND retention_seconds = 0) OR - (provider_kind <> '' AND mode IN ('nodes','direct') AND generation > 0 AND - ((idle_seconds = 0 AND retention_seconds = 0) OR (idle_seconds > 0 AND retention_seconds > 0))) - ); diff --git a/services/core/migrations/000095_node_readiness_classes.sql b/services/core/migrations/000095_node_readiness_classes.sql deleted file mode 100644 index 36cc27afb..000000000 --- a/services/core/migrations/000095_node_readiness_classes.sql +++ /dev/null @@ -1,19 +0,0 @@ --- +goose Up --- Node readiness diagnostics are Provider-neutral classes. An offline node keeps --- its last report, so rewrite every stored vendor code to its class. -UPDATE runtime_nodes SET health = jsonb_set(health, '{diagnostic}', to_jsonb(CASE health->>'diagnostic' - WHEN 'docker_unavailable' THEN 'provider_unavailable' - WHEN 'microsandbox_artifacts_unavailable' THEN 'artifacts_unavailable' - ELSE 'host_unsupported' END::text)) -WHERE health->>'diagnostic' IN ('docker_unavailable', 'docker_limits_unsupported', 'kvm_unavailable', 'microsandbox_artifacts_unavailable'); -UPDATE runtime_node_generation_status SET diagnostic = CASE diagnostic - WHEN 'docker_unavailable' THEN 'provider_unavailable' - WHEN 'microsandbox_artifacts_unavailable' THEN 'artifacts_unavailable' - ELSE 'host_unsupported' END -WHERE diagnostic IN ('docker_unavailable', 'docker_limits_unsupported', 'kvm_unavailable', 'microsandbox_artifacts_unavailable'); - --- +goose Down -UPDATE runtime_nodes SET health = jsonb_set(health, '{diagnostic}', '"provider_unavailable"') -WHERE health->>'diagnostic' IN ('host_unsupported', 'artifacts_unavailable'); -UPDATE runtime_node_generation_status SET diagnostic = 'provider_unavailable' -WHERE diagnostic IN ('host_unsupported', 'artifacts_unavailable'); diff --git a/services/core/migrations/000096_configured_admission.sql b/services/core/migrations/000096_configured_admission.sql deleted file mode 100644 index 0d2c98c4c..000000000 --- a/services/core/migrations/000096_configured_admission.sql +++ /dev/null @@ -1,20 +0,0 @@ --- +goose Up --- Only a claimed deployment with a provider admits hosted work, so every --- allocation runs on a node or belongs to a direct deployment, and none needs --- a keepalive lease. --- +goose StatementBegin -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM runtime_allocations a CROSS JOIN runtime_deployment d - WHERE a.state <> 'released' AND a.node_id IS NULL AND d.mode <> 'direct') - OR EXISTS (SELECT 1 FROM environments e JOIN sessions s ON s.id = e.session_id CROSS JOIN runtime_deployment d - WHERE d.installation_id IS NULL AND s.deleted_at IS NULL AND e.status = 'pending' - AND s.configuration->'environment'->>'type' = 'openai_hosted') THEN - RAISE EXCEPTION 'Cannot upgrade: hosted Sessions hold sandbox work that no configured sandbox deployment admitted. Delete those Sessions and release their runtime allocations, then upgrade'; - END IF; -END $$; --- +goose StatementEnd -ALTER TABLE runtime_allocations DROP COLUMN kept_at; - --- +goose Down -ALTER TABLE runtime_allocations ADD COLUMN kept_at timestamptz NOT NULL DEFAULT clock_timestamp(); diff --git a/services/core/migrations/000097_workspace_storage.sql b/services/core/migrations/000097_workspace_storage.sql deleted file mode 100644 index 99cc822a1..000000000 --- a/services/core/migrations/000097_workspace_storage.sql +++ /dev/null @@ -1,56 +0,0 @@ --- +goose Up -CREATE TABLE workspace_fs_configurations ( - id uuid PRIMARY KEY, - adapter text NOT NULL CHECK (adapter <> ''), - parameters jsonb NOT NULL CHECK (jsonb_typeof(parameters) = 'object'), - active boolean NOT NULL DEFAULT false -); -CREATE UNIQUE INDEX workspace_fs_active_idx ON workspace_fs_configurations (active) WHERE active; - --- +goose StatementBegin -CREATE FUNCTION immutable_workspace_configuration() RETURNS trigger LANGUAGE plpgsql AS $$ -BEGIN - IF (NEW.id, NEW.adapter, NEW.parameters) IS DISTINCT FROM (OLD.id, OLD.adapter, OLD.parameters) THEN - RAISE EXCEPTION 'Workspace filesystem configuration is immutable'; - END IF; - RETURN NEW; -END $$; --- +goose StatementEnd -CREATE TRIGGER immutable_workspace_configuration BEFORE UPDATE ON workspace_fs_configurations -FOR EACH ROW EXECUTE FUNCTION immutable_workspace_configuration(); - -CREATE TABLE environment_workspaces ( - object_id uuid PRIMARY KEY, - environment_id uuid NOT NULL UNIQUE REFERENCES environments(id), - configuration_id uuid NOT NULL REFERENCES workspace_fs_configurations(id), - state text NOT NULL DEFAULT 'creating' CHECK (state IN ('creating', 'ready', 'deleting', 'deleted')), - attachment jsonb CHECK (attachment IS NULL OR jsonb_typeof(attachment) = 'object'), - CHECK (state <> 'ready' OR attachment IS NOT NULL) -); - --- +goose StatementBegin -CREATE FUNCTION immutable_workspace_identity() RETURNS trigger LANGUAGE plpgsql AS $$ -BEGIN - IF (NEW.object_id, NEW.environment_id, NEW.configuration_id) IS DISTINCT FROM (OLD.object_id, OLD.environment_id, OLD.configuration_id) THEN - RAISE EXCEPTION 'Environment workspace identity is immutable'; - END IF; - RETURN NEW; -END $$; --- +goose StatementEnd -CREATE TRIGGER immutable_workspace_identity BEFORE UPDATE ON environment_workspaces -FOR EACH ROW EXECUTE FUNCTION immutable_workspace_identity(); - --- +goose Down -LOCK TABLE environment_workspaces, workspace_fs_configurations IN ACCESS EXCLUSIVE MODE; --- +goose StatementBegin -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM environment_workspaces) THEN - RAISE EXCEPTION 'Cannot discard durable workspace ownership, including deleted identities'; - END IF; -END $$; --- +goose StatementEnd -DROP TABLE environment_workspaces; -DROP TABLE workspace_fs_configurations; -DROP FUNCTION immutable_workspace_identity(); -DROP FUNCTION immutable_workspace_configuration(); diff --git a/services/core/migrations/000098_compute_incarnations.sql b/services/core/migrations/000098_compute_incarnations.sql deleted file mode 100644 index 7fb951331..000000000 --- a/services/core/migrations/000098_compute_incarnations.sql +++ /dev/null @@ -1,40 +0,0 @@ --- +goose Up -ALTER TABLE runtime_allocations DROP CONSTRAINT runtime_allocations_environment_id_key; -CREATE UNIQUE INDEX runtime_allocations_current_environment ON runtime_allocations(environment_id) WHERE state <> 'released'; -CREATE INDEX runtime_allocations_environment_receipts ON runtime_allocations(environment_id, created_at DESC, id DESC); -ALTER TABLE runtime_allocations DROP CONSTRAINT runtime_allocation_placement_fk; -ALTER TABLE runtime_allocations ADD CONSTRAINT runtime_allocation_node_fk FOREIGN KEY(node_id) REFERENCES runtime_nodes(id); -ALTER TABLE devices DROP CONSTRAINT devices_environment_id_key; -CREATE UNIQUE INDEX devices_environment_authority ON devices(environment_id) WHERE revoked_at IS NULL OR executor_key_id IS NOT NULL; - -ALTER TABLE devices ADD COLUMN supported_agent_kinds jsonb NOT NULL DEFAULT '[]' CHECK (jsonb_typeof(supported_agent_kinds) = 'array'); - --- Cold retained Sessions own no compute, but a reset must end their execution lifetime. -CREATE VIEW runtime_reset_retained_environments AS -SELECT e.id AS environment_id, s.id AS session_id, s.tenant_id, a.deployment_generation -FROM environments e JOIN sessions s ON s.id = e.session_id -JOIN environment_workspaces w ON w.environment_id = e.id AND w.state = 'ready' -JOIN LATERAL ( - SELECT allocation.* FROM runtime_allocations allocation - WHERE allocation.environment_id = e.id - ORDER BY allocation.created_at DESC, allocation.id DESC LIMIT 1 -) a ON true -CROSS JOIN runtime_deployment d -WHERE d.reset_clear IS NOT NULL AND d.reset_requested_at IS NOT NULL - AND s.deleted_at IS NULL AND s.created_at <= d.reset_requested_at - AND e.status NOT IN ('failed','expired') AND e.initialization = 'complete' - AND s.configuration->'environment'->>'type' = 'openai_hosted' - AND a.state = 'released' AND a.provider_key = d.installation_id - AND a.deployment_generation <= d.generation AND a.created_at <= d.reset_requested_at - AND NOT EXISTS (SELECT 1 FROM runtime_placements p WHERE p.environment_id = e.id AND p.released_at IS NULL); - --- +goose Down -DROP VIEW runtime_reset_retained_environments; -ALTER TABLE devices DROP COLUMN supported_agent_kinds; -DROP INDEX devices_environment_authority; -ALTER TABLE devices ADD CONSTRAINT devices_environment_id_key UNIQUE(environment_id); -ALTER TABLE runtime_allocations DROP CONSTRAINT runtime_allocation_node_fk; -ALTER TABLE runtime_allocations ADD CONSTRAINT runtime_allocation_placement_fk FOREIGN KEY(environment_id,node_id) REFERENCES runtime_placements(environment_id,node_id); -DROP INDEX runtime_allocations_environment_receipts; -DROP INDEX runtime_allocations_current_environment; -ALTER TABLE runtime_allocations ADD CONSTRAINT runtime_allocations_environment_id_key UNIQUE(environment_id); diff --git a/services/core/migrations/000099_checkpoint_compatibility.sql b/services/core/migrations/000099_checkpoint_compatibility.sql deleted file mode 100644 index 0e9b0b490..000000000 --- a/services/core/migrations/000099_checkpoint_compatibility.sql +++ /dev/null @@ -1,6 +0,0 @@ --- +goose Up -ALTER TABLE runtime_node_generation_status ADD COLUMN checkpoint jsonb - CHECK (checkpoint IS NULL OR jsonb_typeof(checkpoint) = 'object'); - --- +goose Down -ALTER TABLE runtime_node_generation_status DROP COLUMN checkpoint; diff --git a/services/core/migrations/000100_workspace_capacity.sql b/services/core/migrations/000100_workspace_capacity.sql deleted file mode 100644 index a8c6fdd7d..000000000 --- a/services/core/migrations/000100_workspace_capacity.sql +++ /dev/null @@ -1,68 +0,0 @@ --- +goose Up --- Adapter-encoded private credential, sealed with the installation and --- configuration ID. Existing configurations require none. -ALTER TABLE workspace_fs_configurations ADD COLUMN credential bytea - CHECK (credential IS NULL OR length(credential) > 0); --- The admitted per-workspace capacity. Existing workspaces were admitted without one. -ALTER TABLE environment_workspaces ADD COLUMN capacity_mib bigint NOT NULL DEFAULT 0 - CHECK (capacity_mib BETWEEN 0 AND 4294967295); --- The installation's single tenant capacity policy; zero disables admission. -CREATE TABLE workspace_storage_admission ( - singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton), - tenant_capacity_mib bigint NOT NULL DEFAULT 0 CHECK (tenant_capacity_mib BETWEEN 0 AND 4294967295) -); -INSERT INTO workspace_storage_admission DEFAULT VALUES; - --- +goose StatementBegin -CREATE OR REPLACE FUNCTION immutable_workspace_configuration() RETURNS trigger LANGUAGE plpgsql AS $$ -BEGIN - IF (NEW.id, NEW.adapter, NEW.parameters, NEW.credential) IS DISTINCT FROM (OLD.id, OLD.adapter, OLD.parameters, OLD.credential) THEN - RAISE EXCEPTION 'Workspace filesystem configuration is immutable'; - END IF; - RETURN NEW; -END $$; --- +goose StatementEnd - --- +goose StatementBegin -CREATE OR REPLACE FUNCTION immutable_workspace_identity() RETURNS trigger LANGUAGE plpgsql AS $$ -BEGIN - IF (NEW.object_id, NEW.environment_id, NEW.configuration_id, NEW.capacity_mib) IS DISTINCT FROM (OLD.object_id, OLD.environment_id, OLD.configuration_id, OLD.capacity_mib) THEN - RAISE EXCEPTION 'Environment workspace identity is immutable'; - END IF; - RETURN NEW; -END $$; --- +goose StatementEnd - --- +goose Down -LOCK TABLE environment_workspaces, workspace_fs_configurations, workspace_storage_admission IN ACCESS EXCLUSIVE MODE; --- +goose StatementBegin -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM workspace_fs_configurations WHERE credential IS NOT NULL) - OR EXISTS (SELECT 1 FROM environment_workspaces WHERE capacity_mib <> 0) - OR EXISTS (SELECT 1 FROM workspace_storage_admission WHERE tenant_capacity_mib <> 0) THEN - RAISE EXCEPTION 'Cannot discard workspace credentials, admitted capacities or tenant capacity policy'; - END IF; -END $$; --- +goose StatementEnd --- +goose StatementBegin -CREATE OR REPLACE FUNCTION immutable_workspace_configuration() RETURNS trigger LANGUAGE plpgsql AS $$ -BEGIN - IF (NEW.id, NEW.adapter, NEW.parameters) IS DISTINCT FROM (OLD.id, OLD.adapter, OLD.parameters) THEN - RAISE EXCEPTION 'Workspace filesystem configuration is immutable'; - END IF; - RETURN NEW; -END $$; --- +goose StatementEnd --- +goose StatementBegin -CREATE OR REPLACE FUNCTION immutable_workspace_identity() RETURNS trigger LANGUAGE plpgsql AS $$ -BEGIN - IF (NEW.object_id, NEW.environment_id, NEW.configuration_id) IS DISTINCT FROM (OLD.object_id, OLD.environment_id, OLD.configuration_id) THEN - RAISE EXCEPTION 'Environment workspace identity is immutable'; - END IF; - RETURN NEW; -END $$; --- +goose StatementEnd -DROP TABLE workspace_storage_admission; -ALTER TABLE environment_workspaces DROP COLUMN capacity_mib; -ALTER TABLE workspace_fs_configurations DROP COLUMN credential; diff --git a/services/core/migrations/000101_sandbox_idle_policy.sql b/services/core/migrations/000101_sandbox_idle_policy.sql deleted file mode 100644 index 65c64bc99..000000000 --- a/services/core/migrations/000101_sandbox_idle_policy.sql +++ /dev/null @@ -1,5 +0,0 @@ --- +goose Up -ALTER TABLE runtime_deployment ADD COLUMN idle_seconds bigint NOT NULL DEFAULT 300 CHECK (idle_seconds BETWEEN 30 AND 86400); - --- +goose Down -ALTER TABLE runtime_deployment DROP COLUMN idle_seconds; diff --git a/services/core/migrations/000102_reasoning_effort_single_home.sql b/services/core/migrations/000102_reasoning_effort_single_home.sql deleted file mode 100644 index 9113864f1..000000000 --- a/services/core/migrations/000102_reasoning_effort_single_home.sql +++ /dev/null @@ -1,32 +0,0 @@ --- +goose Up --- Reasoning effort has one home, reasoning.effort. Move the Harness-native keys there when it is unset, then drop them everywhere they were stored. -UPDATE agents SET configuration = jsonb_set( - configuration, - '{reasoning}', - CASE WHEN jsonb_typeof(configuration -> 'reasoning') = 'object' THEN configuration -> 'reasoning' ELSE '{}'::jsonb END - || jsonb_build_object('effort', COALESCE(configuration #> '{x_agents_core,harness_config,model_reasoning_effort}', configuration #> '{x_agents_core,harness_config,effort}'))) -WHERE COALESCE(configuration #> '{x_agents_core,harness_config,model_reasoning_effort}', configuration #> '{x_agents_core,harness_config,effort}') IS NOT NULL - AND COALESCE(configuration #> '{reasoning,effort}', 'null'::jsonb) = 'null'::jsonb; - -UPDATE agents SET configuration = configuration #- '{x_agents_core,harness_config,model_reasoning_effort}' #- '{x_agents_core,harness_config,effort}' -WHERE configuration #> '{x_agents_core,harness_config}' ?| ARRAY['model_reasoning_effort', 'effort']; - -UPDATE sessions SET configuration = jsonb_set( - configuration, - '{agent,reasoning}', - CASE WHEN jsonb_typeof(configuration #> '{agent,reasoning}') = 'object' THEN configuration #> '{agent,reasoning}' ELSE '{}'::jsonb END - || jsonb_build_object('effort', COALESCE(configuration #> '{agent,x_agents_core,harness_config,model_reasoning_effort}', configuration #> '{agent,x_agents_core,harness_config,effort}'))) -WHERE COALESCE(configuration #> '{agent,x_agents_core,harness_config,model_reasoning_effort}', configuration #> '{agent,x_agents_core,harness_config,effort}') IS NOT NULL - AND COALESCE(configuration #> '{agent,reasoning,effort}', 'null'::jsonb) = 'null'::jsonb; - -UPDATE sessions SET configuration = configuration #- '{agent,x_agents_core,harness_config,model_reasoning_effort}' #- '{agent,x_agents_core,harness_config,effort}' -WHERE configuration #> '{agent,x_agents_core,harness_config}' ?| ARRAY['model_reasoning_effort', 'effort']; - -UPDATE session_execution_configuration SET configuration = configuration #- '{harness_config,value,model_reasoning_effort}' #- '{harness_config,value,effort}' -WHERE configuration #> '{harness_config,value}' ?| ARRAY['model_reasoning_effort', 'effort']; - -UPDATE deployment_model_providers SET harness_config = harness_config - 'model_reasoning_effort' - 'effort' -WHERE harness_config ?| ARRAY['model_reasoning_effort', 'effort']; - --- +goose Down -SELECT 1; diff --git a/services/core/migrations/000103_function_call_withdrawn.sql b/services/core/migrations/000103_function_call_withdrawn.sql deleted file mode 100644 index 2c7f2863e..000000000 --- a/services/core/migrations/000103_function_call_withdrawn.sql +++ /dev/null @@ -1,11 +0,0 @@ --- +goose Up --- A native Harness can abandon a call before it applies a result. A withdrawn call is no longer a required action. -ALTER TABLE function_calls ADD COLUMN withdrawn boolean NOT NULL DEFAULT false; -ALTER TABLE function_calls ADD CONSTRAINT function_calls_not_applied_and_withdrawn CHECK (NOT (applied AND withdrawn)); -DROP INDEX function_calls_pending_idx; -CREATE INDEX function_calls_pending_idx ON function_calls(session_id, turn_id) WHERE NOT applied AND NOT withdrawn; - --- +goose Down -DROP INDEX function_calls_pending_idx; -CREATE INDEX function_calls_pending_idx ON function_calls(session_id, turn_id) WHERE NOT applied; -ALTER TABLE function_calls DROP COLUMN withdrawn; diff --git a/services/core/migrations/migrations.go b/services/core/migrations/migrations.go index 121ba1a84..33a59d9d3 100644 --- a/services/core/migrations/migrations.go +++ b/services/core/migrations/migrations.go @@ -1,11 +1,10 @@ -// Package migrations owns only the Agents API database schema. +// Package migrations owns only the OpenAgentCore database schema. package migrations import ( "context" "database/sql" "embed" - "fmt" _ "github.com/jackc/pgx/v5/stdlib" "github.com/pressly/goose/v3" @@ -15,9 +14,9 @@ import ( //go:embed *.sql var files embed.FS -// Apply upgrades the dedicated execution database without consulting product -// data and refuses a schema newer than its migrations. A session advisory lock -// serializes concurrent Core starts. +// Apply creates the schema in the dedicated execution database without +// consulting product data. A session advisory lock serializes concurrent Core +// starts. func Apply(ctx context.Context, databaseURL string) error { db, err := sql.Open("pgx", databaseURL) if err != nil { @@ -29,21 +28,10 @@ func Apply(ctx context.Context, databaseURL string) error { return err } provider, err := goose.NewProvider(goose.DialectPostgres, db, files, - goose.WithTableName("agents_api_schema_version"), goose.WithSessionLocker(locker)) + goose.WithTableName("oac_schema_version"), goose.WithSessionLocker(locker)) if err != nil { return err } - if _, err = provider.Up(ctx); err != nil { - return err - } - // Up ignores versions above the embedded ones, so an older Core would start - // on a schema it does not know. - current, target, err := provider.GetVersions(ctx) - if err != nil { - return err - } - if current > target { - return fmt.Errorf("Agents API database schema version %d is newer than this Core supports (%d). Downgrades are not supported: run the release that upgraded this database or a newer one, or restore the backup taken before the upgrade.", current, target) - } - return nil + _, err = provider.Up(ctx) + return err } diff --git a/services/core/migrations/migrations_test.go b/services/core/migrations/migrations_test.go deleted file mode 100644 index bf176591c..000000000 --- a/services/core/migrations/migrations_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package migrations_test - -import ( - "strings" - "testing" - - "github.com/jackc/pgx/v5/stdlib" - - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest" - "github.com/MiniMax-AI/OpenAgentCore/services/core/migrations" -) - -func TestApplyRefusesANewerSchema(t *testing.T) { - pool := pgtest.OpenIsolated(t, nil) - if _, err := pool.Exec(t.Context(), "INSERT INTO agents_api_schema_version (version_id, is_applied) VALUES (999999, true)"); err != nil { - t.Fatal(err) - } - connection := stdlib.RegisterConnConfig(pool.Config().ConnConfig) - defer stdlib.UnregisterConnConfig(connection) - err := migrations.Apply(t.Context(), connection) - if err == nil || !strings.HasPrefix(err.Error(), "Agents API database schema version 999999 is newer than this Core supports (") { - t.Fatalf("err = %v", err) - } -} diff --git a/services/core/tests/integration/archived_cancellation_migration_test.go b/services/core/tests/integration/archived_cancellation_migration_test.go deleted file mode 100644 index 705a695d6..000000000 --- a/services/core/tests/integration/archived_cancellation_migration_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package integration - -import ( - "database/sql" - "os" - "strings" - "testing" - - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/google/uuid" - "github.com/jackc/pgx/v5/stdlib" - "github.com/pressly/goose/v3" -) - -func TestArchivedCancellationMigrationDoesNotAdoptOldRevocations(t *testing.T) { - s, w, installation := managedArchiveFixture(t) - tenant, session := managedArchiveSession(t, s, managerSessionInput(uuid.NewString())) - owner := archiveAllocation(t, w, tenant, session, installation) - input := submitMessage(t, s, tenant, session.ID, "waiting") - transition(t, w, tenant, session.ID, input.TurnID, sessions.TurnQueued, sessions.TurnInProgress) - transition(t, w, tenant, session.ID, input.TurnID, sessions.TurnInProgress, sessions.TurnWaiting) - if _, err := deploymentExecution(t, w).ArchiveSession(adminDeleteContext(t.Context(), tenant, uuid.NewString()), tenant, session.ID, 1); err != nil { - t.Fatal(err) - } - db := sql.OpenDB(stdlib.GetConnector(*s.pool.Config().ConnConfig)) - defer db.Close() - provider, err := goose.NewProvider(goose.DialectPostgres, db, os.DirFS("../../migrations"), goose.WithTableName("agents_api_schema_version")) - if err != nil { - t.Fatal(err) - } - if _, err := provider.DownTo(t.Context(), 79); err == nil { - t.Fatal("downgrade discarded unsettled cancellation marker") - } else if !strings.Contains(err.Error(), "Cannot remove archived cancellation receipts while cleanup is unsettled") { - t.Fatal("downgrade failed before checking the cancellation marker", err) - } - // Later migrations can already have been reverted before migration 80 - // refuses. Restore the current schema before using current generated queries. - if _, err := provider.Up(t.Context()); err != nil { - t.Fatal(err) - } - if _, err := deploymentExecution(t, w).SettleCreation(t.Context(), owner); err != nil { - t.Fatal(err) - } - if _, err := deploymentExecution(t, w).ReleaseAllocation(t.Context(), owner); err != nil { - t.Fatal(err) - } - if _, err := provider.DownTo(t.Context(), 79); err != nil { - t.Fatal(err) - } - if _, err := provider.Up(t.Context()); err != nil { - t.Fatal(err) - } - var marker *string - if err := s.pool.QueryRow(t.Context(), "SELECT archive_cancel_turn_id::text FROM devices WHERE id=$1", owner.DeviceID).Scan(&marker); err != nil || marker != nil { - t.Fatal("upgrade adopted historical revoked device", marker, err) - } -} diff --git a/services/core/tests/integration/credential_oauth_migration_test.go b/services/core/tests/integration/credential_oauth_migration_test.go deleted file mode 100644 index 6c607c388..000000000 --- a/services/core/tests/integration/credential_oauth_migration_test.go +++ /dev/null @@ -1,79 +0,0 @@ -package integration - -import ( - "context" - "database/sql" - "os" - "testing" - - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/stdlib" - "github.com/pressly/goose/v3" -) - -func TestOAuthMigrationPreservesStaticGrantsAndRefusesLossyRollback(t *testing.T) { - _, pool := testStore(t) - schema := "oauth_migration_" + uuid.NewString()[:8] - quoted := pgx.Identifier{schema}.Sanitize() - if _, err := pool.Exec(t.Context(), "CREATE SCHEMA "+quoted); err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - if _, err := pool.Exec(context.Background(), "DROP SCHEMA "+quoted+" CASCADE"); err != nil { - t.Error(err) - } - }) - cfg := pool.Config().ConnConfig.Copy() - cfg.RuntimeParams["search_path"] = schema - db := sql.OpenDB(stdlib.GetConnector(*cfg)) - t.Cleanup(func() { _ = db.Close() }) - provider, err := goose.NewProvider(goose.DialectPostgres, db, os.DirFS("../../migrations"), goose.WithTableName("agents_api_schema_version")) - if err != nil { - t.Fatal(err) - } - if _, err := provider.UpTo(t.Context(), 53); err != nil { - t.Fatal(err) - } - vault, id := uuid.NewString(), uuid.NewString() - if _, err := db.ExecContext(t.Context(), "INSERT INTO vaults(id,tenant_id,metadata) VALUES ($1,$2,'{}')", vault, uuid.NewString()); err != nil { - t.Fatal(err) - } - if _, err := db.ExecContext(t.Context(), "INSERT INTO vault_credentials(id,vault_id,name,auth_type,mcp_server_url,token_ciphertext) VALUES ($1,$2,'Existing','static_bearer','https://mcp.example/tools',$3)", id, vault, []byte{1, 2, 3}); err != nil { - t.Fatal(err) - } - snapshot := func() string { - t.Helper() - var value string - if err := db.QueryRowContext(t.Context(), "SELECT (to_jsonb(c)-'oauth_metadata')::text FROM vault_credentials c WHERE id=$1", id).Scan(&value); err != nil { - t.Fatal(err) - } - return value - } - before := snapshot() - if _, err := provider.UpTo(t.Context(), 54); err != nil || snapshot() != before { - t.Fatal("OAuth migration changed static grant", err) - } - if _, err := provider.DownTo(t.Context(), 53); err != nil || snapshot() != before { - t.Fatal("static-only rollback failed", err) - } - if _, err := provider.UpTo(t.Context(), 54); err != nil { - t.Fatal(err) - } - oauthID := uuid.NewString() - if _, err := db.ExecContext(t.Context(), "INSERT INTO vault_credentials(id,vault_id,name,auth_type,mcp_server_url,token_ciphertext,oauth_metadata) VALUES ($1,$2,'OAuth','mcp_oauth','https://mcp.example/tools',$3,'{}')", oauthID, vault, []byte{4, 5, 6}); err != nil { - t.Fatal(err) - } - if _, err := provider.DownTo(t.Context(), 53); err == nil { - t.Fatal("rollback discarded OAuth support with existing grants") - } - var count int - if err := db.QueryRowContext(t.Context(), "SELECT count(*) FROM vault_credentials WHERE id=$1 AND auth_type='mcp_oauth' AND oauth_metadata='{}'::jsonb", oauthID).Scan(&count); err != nil || count != 1 || snapshot() != before { - t.Fatal("failed rollback altered grants", err) - } - for _, mutation := range []string{"oauth_metadata=NULL", "oauth_metadata='[]'::jsonb", "auth_type='static_bearer'"} { - if _, err := db.ExecContext(t.Context(), "UPDATE vault_credentials SET "+mutation+" WHERE id=$1", oauthID); err == nil { - t.Fatal("invalid OAuth metadata constraint admitted") - } - } -} diff --git a/services/core/tests/integration/credential_status_migration_test.go b/services/core/tests/integration/credential_status_migration_test.go deleted file mode 100644 index 1bbea5ef6..000000000 --- a/services/core/tests/integration/credential_status_migration_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package integration - -import ( - "context" - "database/sql" - "os" - "strings" - "testing" - - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/stdlib" - "github.com/pressly/goose/v3" -) - -func TestCredentialStatusMigrationPreservesMetadataAndCiphertext(t *testing.T) { - _, pool := testStore(t) - ctx := t.Context() - schema := "credential_status_" + uuid.NewString()[:8] - quoted := pgx.Identifier{schema}.Sanitize() - if _, err := pool.Exec(ctx, "CREATE SCHEMA "+quoted); err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - if _, err := pool.Exec(context.Background(), "DROP SCHEMA "+quoted+" CASCADE"); err != nil { - t.Error(err) - } - }) - cfg := pool.Config().ConnConfig.Copy() - cfg.RuntimeParams["search_path"] = schema - db := sql.OpenDB(stdlib.GetConnector(*cfg)) - t.Cleanup(func() { _ = db.Close() }) - provider, err := goose.NewProvider(goose.DialectPostgres, db, os.DirFS("../../migrations"), goose.WithTableName("agents_api_schema_version")) - if err != nil { - t.Fatal(err) - } - if _, err := provider.UpTo(ctx, 32); err != nil { - t.Fatal(err) - } - id, vault, tenant := uuid.NewString(), uuid.NewString(), uuid.NewString() - if _, err := db.ExecContext(ctx, "INSERT INTO vaults(id,tenant_id,metadata) VALUES ($1,$2,'{}')", vault, tenant); err != nil { - t.Fatal(err) - } - if _, err := db.ExecContext(ctx, "INSERT INTO vault_credentials(id,vault_id,name,auth_type,mcp_server_url,token_ciphertext) VALUES ($1,$2,'Existing Credential','static_bearer','https://example.invalid/mcp',$3)", id, vault, []byte{1, 2, 3}); err != nil { - t.Fatal(err) - } - snapshot := func() string { - t.Helper() - var value string - if err := db.QueryRowContext(ctx, "SELECT (to_jsonb(v)-'status')::text FROM vault_credentials v WHERE id=$1", id).Scan(&value); err != nil { - t.Fatal(err) - } - return value - } - before := snapshot() - if _, err := provider.UpTo(ctx, 33); err != nil { - t.Fatal(err) - } - var status string - if err := db.QueryRowContext(ctx, "SELECT status FROM vault_credentials WHERE id=$1", id).Scan(&status); err != nil || status != "active" || snapshot() != before { - t.Fatal("migration changed historical resource", status, err) - } - if err := db.QueryRowContext(ctx, "INSERT INTO vault_credentials(id,vault_id,name,auth_type,mcp_server_url,token_ciphertext) VALUES ($1,$2,'New Credential','static_bearer','https://example.invalid/mcp',$3) RETURNING status", uuid.NewString(), vault, []byte{4, 5, 6}).Scan(&status); err != nil || status != "active" { - t.Fatal("new Credential default", status, err) - } - for _, invalid := range []any{nil, "deleted"} { - if _, err := db.ExecContext(ctx, "UPDATE vault_credentials SET status=$1 WHERE id=$2", invalid, id); err == nil { - t.Fatal("invalid classification accepted") - } - } - if _, err := provider.DownTo(ctx, 32); err != nil || snapshot() != before { - t.Fatal("reversible active-only migration", err) - } - if _, err := provider.UpTo(ctx, 33); err != nil { - t.Fatal(err) - } - if _, err := db.ExecContext(ctx, "UPDATE vault_credentials SET status='archived' WHERE id=$1", id); err != nil { - t.Fatal(err) - } - if _, err := provider.DownTo(ctx, 32); err == nil || !strings.Contains(err.Error(), "Cannot remove archived Credential classification") { - t.Fatal("downgrade lost archived classification", err) - } - if err := db.QueryRowContext(ctx, "SELECT status FROM vault_credentials WHERE id=$1", id).Scan(&status); err != nil || status != "archived" || snapshot() != before { - t.Fatal("failed downgrade changed data", status, err) - } -} diff --git a/services/core/tests/integration/environment_connection_migration_test.go b/services/core/tests/integration/environment_connection_migration_test.go deleted file mode 100644 index f48ba3360..000000000 --- a/services/core/tests/integration/environment_connection_migration_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package integration - -import ( - "context" - "database/sql" - "os" - "strings" - "testing" - - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/stdlib" - "github.com/pressly/goose/v3" -) - -func TestEnvironmentConnectionMigrationPreservesStateAndGuardsFencing(t *testing.T) { - _, pool := testStore(t) - ctx := t.Context() - schema := "connection_" + uuid.NewString()[:8] - quoted := pgx.Identifier{schema}.Sanitize() - if _, err := pool.Exec(ctx, "CREATE SCHEMA "+quoted); err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - if _, err := pool.Exec(context.Background(), "DROP SCHEMA "+quoted+" CASCADE"); err != nil { - t.Error(err) - } - }) - cfg := pool.Config().ConnConfig.Copy() - cfg.RuntimeParams["search_path"] = schema - db := sql.OpenDB(stdlib.GetConnector(*cfg)) - t.Cleanup(func() { _ = db.Close() }) - provider, err := goose.NewProvider(goose.DialectPostgres, db, os.DirFS("../../migrations"), goose.WithTableName("agents_api_schema_version")) - if err != nil { - t.Fatal(err) - } - if _, err := provider.UpTo(ctx, 26); err != nil { - t.Fatal(err) - } - session, environment, tenant := uuid.NewString(), uuid.NewString(), uuid.NewString() - if _, err := db.ExecContext(ctx, `INSERT INTO sessions(id,tenant_id,engine,idempotency_key,request_hash,configuration) VALUES ($1,$2,'codex','historical','historical','{"environment":{"type":"self_hosted"}}')`, session, tenant); err != nil { - t.Fatal(err) - } - if _, err := db.ExecContext(ctx, "INSERT INTO environments(id,session_id) VALUES ($1,$2)", environment, session); err != nil { - t.Fatal(err) - } - snapshot := func() string { - t.Helper() - var value string - if err := db.QueryRowContext(ctx, "SELECT to_jsonb(e)::text FROM environments e WHERE id=$1", environment).Scan(&value); err != nil { - t.Fatal(err) - } - return value - } - before := snapshot() - if _, err := provider.UpTo(ctx, 27); err != nil { - t.Fatal(err) - } - if snapshot() != before { - t.Fatal("migration invented connection state") - } - var count int - if err := db.QueryRowContext(ctx, "SELECT count(*) FROM environment_connections").Scan(&count); err != nil || count != 0 { - t.Fatal(count, err) - } - if _, err := provider.DownTo(ctx, 26); err != nil { - t.Fatal(err) - } - if _, err := provider.UpTo(ctx, 27); err != nil { - t.Fatal(err) - } - generation := uuid.NewString() - if _, err := db.ExecContext(ctx, "INSERT INTO environment_connections(environment_id,generation) VALUES ($1,$2)", environment, generation); err != nil { - t.Fatal(err) - } - if _, err := db.ExecContext(ctx, "UPDATE environment_connections SET revision=-1 WHERE environment_id=$1", environment); err == nil { - t.Fatal("negative observation revision accepted") - } - if _, err := provider.DownTo(ctx, 26); err == nil || !strings.Contains(err.Error(), "Cannot remove active Environment observation fencing") { - t.Fatal("downgrade lost generation", err) - } - var retained string - if err := db.QueryRowContext(ctx, "SELECT generation::text FROM environment_connections WHERE environment_id=$1", environment).Scan(&retained); err != nil || retained != generation { - t.Fatal(retained, err) - } - if snapshot() != before { - t.Fatal("migration changed Environment ownership") - } -} diff --git a/services/core/tests/integration/environment_failure_migration_test.go b/services/core/tests/integration/environment_failure_migration_test.go deleted file mode 100644 index d6e094324..000000000 --- a/services/core/tests/integration/environment_failure_migration_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package integration - -import ( - "context" - "database/sql" - "os" - "strings" - "testing" - - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/stdlib" - "github.com/pressly/goose/v3" -) - -// The failure columns are additive for existing Environments, and a rollback -// refuses to discard a recorded hosted provisioning failure. -func TestEnvironmentFailureMigrationGuardsRecordedFailures(t *testing.T) { - _, pool := testStore(t) - ctx := t.Context() - schema := "environment_failure_" + uuid.NewString()[:8] - quoted := pgx.Identifier{schema}.Sanitize() - if _, err := pool.Exec(ctx, "CREATE SCHEMA "+quoted); err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - if _, err := pool.Exec(context.Background(), "DROP SCHEMA "+quoted+" CASCADE"); err != nil { - t.Error(err) - } - }) - cfg := pool.Config().ConnConfig.Copy() - cfg.RuntimeParams["search_path"] = schema - db := sql.OpenDB(stdlib.GetConnector(*cfg)) - t.Cleanup(func() { _ = db.Close() }) - provider, err := goose.NewProvider(goose.DialectPostgres, db, os.DirFS("../../migrations"), goose.WithTableName("agents_api_schema_version")) - if err != nil { - t.Fatal(err) - } - if _, err := provider.UpTo(ctx, 61); err != nil { - t.Fatal(err) - } - session, environment := uuid.NewString(), uuid.NewString() - if _, err := db.ExecContext(ctx, `INSERT INTO sessions(id,tenant_id,engine,idempotency_key,request_hash,configuration) - VALUES ($1,$2,'codex','historical','historical','{"environment":{"type":"openai_hosted"}}')`, session, uuid.NewString()); err != nil { - t.Fatal(err) - } - // A failure recorded before this migration keeps no reason. - if _, err := db.ExecContext(ctx, "INSERT INTO environments(id,session_id,status) VALUES ($1,$2,'failed')", environment, session); err != nil { - t.Fatal(err) - } - upgrade := func() { - t.Helper() - if _, err := provider.UpTo(ctx, 62); err != nil { - t.Fatal(err) - } - var reason, failedAt sql.NullString - if err := db.QueryRowContext(ctx, "SELECT failure_reason, failed_at::text FROM environments WHERE id=$1", environment).Scan(&reason, &failedAt); err != nil || reason.Valid || failedAt.Valid { - t.Fatal("migration invented a failure reason", reason, failedAt, err) - } - } - upgrade() - for _, statement := range []string{ - "UPDATE environments SET failure_reason='reason' WHERE id=$1", - "UPDATE environments SET status='connected', failure_reason='reason', failed_at=clock_timestamp() WHERE id=$1", - "UPDATE environments SET failure_reason=repeat('x', 257), failed_at=clock_timestamp() WHERE id=$1", - } { - if _, err := db.ExecContext(ctx, statement, environment); err == nil || !strings.Contains(err.Error(), "environment_failure_recorded") { - t.Fatal("inconsistent failure accepted", statement, err) - } - } - if _, err := provider.DownTo(ctx, 61); err != nil { - t.Fatal("rollback without recorded failures failed", err) - } - upgrade() - if _, err := db.ExecContext(ctx, "UPDATE environments SET failure_reason='Failed to provision environment', failed_at=clock_timestamp() WHERE id=$1", environment); err != nil { - t.Fatal(err) - } - if _, err := provider.DownTo(ctx, 61); err == nil || !strings.Contains(err.Error(), "Cannot remove recorded hosted provisioning failures") { - t.Fatal("rollback discarded a recorded failure", err) - } - var reason string - if err := db.QueryRowContext(ctx, "SELECT failure_reason FROM environments WHERE id=$1", environment).Scan(&reason); err != nil || reason != "Failed to provision environment" { - t.Fatal("refused rollback changed the failure", reason, err) - } -} diff --git a/services/core/tests/integration/environment_file_write_migration_test.go b/services/core/tests/integration/environment_file_write_migration_test.go deleted file mode 100644 index a1124e313..000000000 --- a/services/core/tests/integration/environment_file_write_migration_test.go +++ /dev/null @@ -1,90 +0,0 @@ -package integration - -import ( - "context" - "database/sql" - "os" - "strings" - "testing" - "time" - - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/stdlib" - "github.com/pressly/goose/v3" -) - -func TestEnvironmentFileWriteMigrationRetainsUnresolvedIdentity(t *testing.T) { - _, pool := testStore(t) - ctx := t.Context() - schema := "file_write_" + uuid.NewString()[:8] - quoted := pgx.Identifier{schema}.Sanitize() - if _, err := pool.Exec(ctx, "CREATE SCHEMA "+quoted); err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - cleanup, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - if _, err := pool.Exec(cleanup, "DROP SCHEMA "+quoted+" CASCADE"); err != nil { - t.Error(err) - } - }) - cfg := pool.Config().ConnConfig.Copy() - cfg.RuntimeParams["search_path"] = schema - db := sql.OpenDB(stdlib.GetConnector(*cfg)) - t.Cleanup(func() { _ = db.Close() }) - provider, err := goose.NewProvider(goose.DialectPostgres, db, os.DirFS("../../migrations"), goose.WithTableName("agents_api_schema_version")) - if err != nil { - t.Fatal(err) - } - if _, err := provider.UpTo(ctx, 35); err != nil { - t.Fatal(err) - } - session, tenant, environment, device, write := uuid.NewString(), uuid.NewString(), uuid.NewString(), uuid.NewString(), uuid.NewString() - if _, err := db.ExecContext(ctx, `INSERT INTO sessions(id,tenant_id,engine,idempotency_key,request_hash,configuration) - VALUES ($1,$2,'codex','old','old','{}')`, session, tenant); err != nil { - t.Fatal(err) - } - if _, err := db.ExecContext(ctx, `INSERT INTO environments(id,session_id) VALUES ($1,$2)`, environment, session); err != nil { - t.Fatal(err) - } - if _, err := db.ExecContext(ctx, `INSERT INTO devices(id,tenant_id,name,credential_hash) VALUES ($1,$2,'old',$3)`, device, tenant, strings.Repeat("a", 64)); err != nil { - t.Fatal(err) - } - var before, after string - if err := db.QueryRowContext(ctx, `SELECT to_jsonb(e)::text FROM environments e WHERE id=$1`, environment).Scan(&before); err != nil { - t.Fatal(err) - } - for range 2 { - if _, err := provider.UpTo(ctx, 36); err != nil { - t.Fatal(err) - } - var count int - if err := db.QueryRowContext(ctx, "SELECT count(*) FROM environment_file_writes").Scan(&count); err != nil || count != 0 { - t.Fatal("migration manufactured write authority", count, err) - } - if _, err := provider.DownTo(ctx, 35); err != nil { - t.Fatal("empty downgrade", err) - } - } - if _, err := provider.UpTo(ctx, 36); err != nil { - t.Fatal(err) - } - if _, err := db.ExecContext(ctx, `INSERT INTO environment_file_writes(id,environment_id,device_id,request_sha256) - VALUES ($1,$2,$3,$4)`, write, environment, device, strings.Repeat("b", 64)); err != nil { - t.Fatal(err) - } - var original, retained string - if err := db.QueryRowContext(ctx, `SELECT to_jsonb(w)::text FROM environment_file_writes w WHERE id=$1`, write).Scan(&original); err != nil { - t.Fatal(err) - } - if _, err := provider.DownTo(ctx, 35); err == nil || !strings.Contains(err.Error(), "Cannot remove durable Environment file write identities") { - t.Fatal("downgrade removed unresolved identity", err) - } - if err := db.QueryRowContext(ctx, `SELECT to_jsonb(w)::text FROM environment_file_writes w WHERE id=$1`, write).Scan(&retained); err != nil || retained != original { - t.Fatal("failed downgrade changed write", err) - } - if err := db.QueryRowContext(ctx, `SELECT to_jsonb(e)::text FROM environments e WHERE id=$1`, environment).Scan(&after); err != nil || after != before { - t.Fatal("migration changed Environment", err) - } -} diff --git a/services/core/tests/integration/environment_initial_migration_test.go b/services/core/tests/integration/environment_initial_migration_test.go deleted file mode 100644 index 21a7524de..000000000 --- a/services/core/tests/integration/environment_initial_migration_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package integration - -import ( - "context" - "database/sql" - "os" - "strings" - "testing" - - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/stdlib" - "github.com/pressly/goose/v3" -) - -func TestEnvironmentInitialMigrationPreservesLaterInputOrigin(t *testing.T) { - _, pool := testStore(t) - ctx := t.Context() - schema := "initial_origin_" + uuid.NewString()[:8] - quoted := pgx.Identifier{schema}.Sanitize() - if _, err := pool.Exec(ctx, "CREATE SCHEMA "+quoted); err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - if _, err := pool.Exec(context.Background(), "DROP SCHEMA "+quoted+" CASCADE"); err != nil { - t.Error(err) - } - }) - cfg := pool.Config().ConnConfig.Copy() - cfg.RuntimeParams["search_path"] = schema - db := sql.OpenDB(stdlib.GetConnector(*cfg)) - t.Cleanup(func() { _ = db.Close() }) - provider, err := goose.NewProvider(goose.DialectPostgres, db, os.DirFS("../../migrations"), goose.WithTableName("agents_api_schema_version")) - if err != nil { - t.Fatal(err) - } - if _, err := provider.UpTo(ctx, 28); err != nil { - t.Fatal(err) - } - session, later := uuid.NewString(), uuid.NewString() - if _, err := db.ExecContext(ctx, `INSERT INTO sessions(id,tenant_id,engine,idempotency_key,request_hash) VALUES ($1,$2,'codex','historical','historical')`, session, uuid.NewString()); err != nil { - t.Fatal(err) - } - if _, err := db.ExecContext(ctx, "INSERT INTO environments(id,session_id) VALUES ($1,$2)", uuid.NewString(), session); err != nil { - t.Fatal(err) - } - if _, err := db.ExecContext(ctx, `INSERT INTO environment_input_reservations(id,session_id,idempotency_key,batch,created_at,deadline) - VALUES ($1,$2,'later','[{"kind":"message","payload":{"text":"historical"}}]',clock_timestamp(),clock_timestamp()+interval '5 minutes')`, later, session); err != nil { - t.Fatal(err) - } - snapshot := func() string { - t.Helper() - var result string - if err := db.QueryRowContext(ctx, "SELECT (to_jsonb(r)-'is_initial')::text FROM environment_input_reservations r WHERE id=$1", later).Scan(&result); err != nil { - t.Fatal(err) - } - return result - } - before := snapshot() - for i := 0; i < 2; i++ { - if _, err := provider.UpTo(ctx, 29); err != nil { - t.Fatal(err) - } - var initial bool - if err := db.QueryRowContext(ctx, "SELECT is_initial FROM environment_input_reservations WHERE id=$1", later).Scan(&initial); err != nil || initial || snapshot() != before { - t.Fatal("migration inferred origin or changed historical data", initial, err) - } - if i == 0 { - if _, err := provider.DownTo(ctx, 28); err != nil { - t.Fatal("later-only downgrade failed", err) - } - } - } - initial := uuid.NewString() - if _, err := db.ExecContext(ctx, `INSERT INTO environment_input_reservations(id,session_id,idempotency_key,batch,is_initial,state,created_at,deadline,settled_at) - VALUES ($1,$2,'initial','[{"kind":"message","payload":{"text":"initial"}}]',true,'expired',clock_timestamp()-interval '6 minutes',clock_timestamp()-interval '1 minute',clock_timestamp())`, initial, session); err != nil { - t.Fatal(err) - } - if _, err := provider.DownTo(ctx, 28); err == nil || !strings.Contains(err.Error(), "Cannot remove initial Environment input origin") { - t.Fatal("downgrade discarded initial failure meaning", err) - } - var retained bool - if err := db.QueryRowContext(ctx, "SELECT is_initial FROM environment_input_reservations WHERE id=$1", initial).Scan(&retained); err != nil || !retained || snapshot() != before { - t.Fatal("rejected downgrade changed origin/history", retained, err) - } -} diff --git a/services/core/tests/integration/environment_input_migration_test.go b/services/core/tests/integration/environment_input_migration_test.go deleted file mode 100644 index 8e57ded17..000000000 --- a/services/core/tests/integration/environment_input_migration_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package integration - -import ( - "context" - "database/sql" - "os" - "strings" - "testing" - - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/stdlib" - "github.com/pressly/goose/v3" -) - -func TestEnvironmentInputMigrationRetainsHistoryAndRetryIdentity(t *testing.T) { - _, pool := testStore(t) - ctx := context.Background() - schema := "environment_input_" + uuid.NewString()[:8] - quoted := pgx.Identifier{schema}.Sanitize() - if _, err := pool.Exec(ctx, "CREATE SCHEMA "+quoted); err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - if _, err := pool.Exec(ctx, "DROP SCHEMA "+quoted+" CASCADE"); err != nil { - t.Error(err) - } - }) - cfg := pool.Config().ConnConfig.Copy() - cfg.RuntimeParams["search_path"] = schema - db := sql.OpenDB(stdlib.GetConnector(*cfg)) - t.Cleanup(func() { _ = db.Close() }) - provider, err := goose.NewProvider(goose.DialectPostgres, db, os.DirFS("../../migrations"), goose.WithTableName("agents_api_schema_version")) - if err != nil { - t.Fatal(err) - } - if _, err := provider.UpTo(ctx, 21); err != nil { - t.Fatal(err) - } - tenant, session := uuid.NewString(), uuid.NewString() - // Historical schemas must be seeded without the current Store's creation contract. - input := environmentInput("session", "self_hosted", "/workspace") - if _, err := db.ExecContext(ctx, `INSERT INTO sessions(id,tenant_id,engine,idempotency_key,request_hash,configuration) - VALUES ($1,$2,'codex','session','historical',$3)`, session, tenant, input.Configuration); err != nil { - t.Fatal(err) - } - if _, err := db.ExecContext(ctx, "INSERT INTO environments(id,session_id) VALUES ($1,$2)", uuid.NewString(), session); err != nil { - t.Fatal(err) - } - var before, after string - if err := db.QueryRowContext(ctx, "SELECT to_jsonb(s)::text FROM sessions s WHERE id=$1", session).Scan(&before); err != nil { - t.Fatal(err) - } - if _, err := provider.UpTo(ctx, 22); err != nil { - t.Fatal(err) - } - var count int - if err := db.QueryRowContext(ctx, "SELECT count(*) FROM environment_input_reservations").Scan(&count); err != nil || count != 0 { - t.Fatal("migration manufactured reservations", count, err) - } - if _, err := provider.DownTo(ctx, 21); err != nil { - t.Fatal("empty downgrade", err) - } - if _, err := provider.UpTo(ctx, 22); err != nil { - t.Fatal(err) - } - if err := db.QueryRowContext(ctx, "SELECT to_jsonb(s)::text FROM sessions s WHERE id=$1", session).Scan(&after); err != nil || before != after { - t.Fatal("migration changed Session", err) - } - // Seed the historical schema directly; current queries require later columns. - pending := uuid.NewString() - if _, err := db.ExecContext(ctx, `INSERT INTO environment_input_reservations(id,session_id,idempotency_key,batch,created_at,deadline) - VALUES ($1,$2,'pending','[{"kind":"message","payload":{"text":"pending"}}]',clock_timestamp(),clock_timestamp()+interval '5 minutes')`, pending, session); err != nil { - t.Fatal(err) - } - var original, retained string - if err := db.QueryRowContext(ctx, "SELECT to_jsonb(r)::text FROM environment_input_reservations r WHERE id=$1", pending).Scan(&original); err != nil { - t.Fatal(err) - } - if _, err := provider.DownTo(ctx, 21); err == nil || !strings.Contains(err.Error(), "Cannot remove durable Environment input identities") { - t.Fatal("downgrade discarded retry identity", err) - } - if err := db.QueryRowContext(ctx, "SELECT to_jsonb(r)::text FROM environment_input_reservations r WHERE id=$1", pending).Scan(&retained); err != nil || retained != original { - t.Fatal("downgrade lost reservation", retained, err) - } - if err := db.QueryRowContext(ctx, "SELECT to_jsonb(s)::text FROM sessions s WHERE id=$1", session).Scan(&after); err != nil || before != after { - t.Fatal("migration changed Session", err) - } - if err := db.QueryRowContext(ctx, "SELECT count(*) FROM turns WHERE session_id=$1", session).Scan(&count); err != nil || count != 0 { - t.Fatal("migration manufactured a Turn", count, err) - } -} diff --git a/services/core/tests/integration/environments_migration_test.go b/services/core/tests/integration/environments_migration_test.go deleted file mode 100644 index 2645ed971..000000000 --- a/services/core/tests/integration/environments_migration_test.go +++ /dev/null @@ -1,133 +0,0 @@ -package integration - -import ( - "context" - "database/sql" - "os" - "strings" - "testing" - "time" - - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgxpool" - "github.com/jackc/pgx/v5/stdlib" - "github.com/pressly/goose/v3" -) - -func TestEnvironmentMigrationPreservesHistoryAndGuardsIdentity(t *testing.T) { - _, pool := testStore(t) - ctx := context.Background() - schema := "environment_" + uuid.NewString()[:8] - quoted := pgx.Identifier{schema}.Sanitize() - if _, err := pool.Exec(ctx, "CREATE SCHEMA "+quoted); err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - if _, err := pool.Exec(ctx, "DROP SCHEMA "+quoted+" CASCADE"); err != nil { - t.Error(err) - } - }) - cfg := pool.Config().ConnConfig.Copy() - cfg.RuntimeParams["search_path"] = schema - db := sql.OpenDB(stdlib.GetConnector(*cfg)) - t.Cleanup(func() { _ = db.Close() }) - provider, err := goose.NewProvider(goose.DialectPostgres, db, os.DirFS("../../migrations"), goose.WithTableName("agents_api_schema_version")) - if err != nil { - t.Fatal(err) - } - if _, err := provider.UpTo(ctx, 19); err != nil { - t.Fatal(err) - } - tenant := uuid.NewString() - for _, kind := range []string{"none", "self_hosted"} { - if _, err := db.ExecContext(ctx, `INSERT INTO sessions (id,tenant_id,engine,idempotency_key,request_hash,configuration) - VALUES ($1,$2,'codex',$3,'historical',jsonb_build_object('environment',jsonb_build_object('type',$3::text)))`, uuid.NewString(), tenant, kind); err != nil { - t.Fatal(err) - } - } - history := func() string { - t.Helper() - var snapshot string - if err := db.QueryRowContext(ctx, "SELECT jsonb_agg(to_jsonb(s) ORDER BY id)::text FROM sessions s").Scan(&snapshot); err != nil { - t.Fatal(err) - } - return snapshot - } - before := history() - if _, err := provider.UpTo(ctx, 20); err != nil { - t.Fatal(err) - } - if after := history(); after != before { - t.Fatal("migration changed historical Sessions") - } - var count int - if err := db.QueryRowContext(ctx, "SELECT count(*) FROM environments").Scan(&count); err != nil || count != 0 { - t.Fatal("manufactured historical ownership", count, err) - } - if _, err := provider.DownTo(ctx, 19); err != nil { - t.Fatal("empty downgrade failed", err) - } - if _, err := provider.UpTo(ctx, 20); err != nil { - t.Fatal(err) - } - poolConfig := pool.Config() - poolConfig.ConnConfig = cfg - migrated, err := pgxpool.NewWithConfig(ctx, poolConfig) - if err != nil { - t.Fatal(err) - } - t.Cleanup(migrated.Close) - - sessionID, environmentID := uuid.NewString(), uuid.NewString() - tx, err := migrated.Begin(ctx) - if err != nil { - t.Fatal(err) - } - defer func() { _ = tx.Rollback(ctx) }() - input := environmentInput("new", "self_hosted", "/workspace") - if _, err := tx.Exec(ctx, `INSERT INTO sessions(id,tenant_id,engine,idempotency_key,request_hash,configuration) - VALUES ($1,$2,'codex','new','new',$3)`, sessionID, tenant, input.Configuration); err != nil { - t.Fatal(err) - } - if _, err := tx.Exec(ctx, "INSERT INTO environments(id,session_id) VALUES ($1,$2)", environmentID, sessionID); err != nil { - t.Fatal(err) - } - downgradeCtx, cancel := context.WithTimeout(ctx, 5*time.Second) - defer cancel() - outcome := make(chan error, 1) - go func() { _, err := provider.DownTo(downgradeCtx, 19); outcome <- err }() - // The uncommitted creator must block the downgrade before its emptiness decision. - for { - var blocked bool - err := pool.QueryRow(downgradeCtx, `SELECT EXISTS ( - SELECT 1 FROM pg_locks WHERE relation=$1::regclass - AND mode='AccessExclusiveLock' AND NOT granted)`, quoted+".environments").Scan(&blocked) - if err != nil { - t.Fatal(err) - } - if blocked { - break - } - select { - case err := <-outcome: - t.Fatal("downgrade did not wait for creator", err) - case <-downgradeCtx.Done(): - t.Fatal("downgrade lock was not observed") - case <-time.After(5 * time.Millisecond): - } - } - if err := tx.Commit(ctx); err != nil { - t.Fatal(err) - } - if err := <-outcome; err == nil || !strings.Contains(err.Error(), "Cannot remove durable Environment identities") { - t.Fatal("concurrent downgrade discarded identity", err) - } - // Read the version-20 row directly: current Store queries select columns - // that later migrations add to environments. - var retained string - if err := migrated.QueryRow(ctx, `SELECT e.session_id::text FROM environments e JOIN sessions s ON s.id = e.session_id - WHERE s.tenant_id = $1 AND e.id = $2 AND s.deleted_at IS NULL`, tenant, environmentID).Scan(&retained); err != nil || retained != sessionID { - t.Fatal("downgrade destroyed ownership", retained, err) - } -} diff --git a/services/core/tests/integration/executor_principals_migration_test.go b/services/core/tests/integration/executor_principals_migration_test.go deleted file mode 100644 index 4157d1422..000000000 --- a/services/core/tests/integration/executor_principals_migration_test.go +++ /dev/null @@ -1,131 +0,0 @@ -package integration - -import ( - "context" - "database/sql" - "errors" - "os" - "strings" - "testing" - - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgxpool" - "github.com/jackc/pgx/v5/stdlib" - "github.com/pressly/goose/v3" - - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" -) - -func TestExecutorPrincipalMigrationRetiresUnknownAuthority(t *testing.T) { - _, pool := testStore(t) - ctx := t.Context() - schema := "executor_principal_" + uuid.NewString()[:8] - quoted := pgx.Identifier{schema}.Sanitize() - if _, err := pool.Exec(ctx, "CREATE SCHEMA "+quoted); err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - if _, err := pool.Exec(context.Background(), "DROP SCHEMA "+quoted+" CASCADE"); err != nil { - t.Error(err) - } - }) - cfg := pool.Config().ConnConfig.Copy() - cfg.RuntimeParams["search_path"] = schema - db := sql.OpenDB(stdlib.GetConnector(*cfg)) - t.Cleanup(func() { _ = db.Close() }) - provider, err := goose.NewProvider(goose.DialectPostgres, db, os.DirFS("../../migrations"), goose.WithTableName("agents_api_schema_version")) - if err != nil { - t.Fatal(err) - } - if _, err := provider.UpTo(ctx, 25); err != nil { - t.Fatal(err) - } - tenant, session, environment := uuid.NewString(), uuid.NewString(), uuid.NewString() - input := environmentInput("legacy", "self_hosted", "/workspace") - if _, err := db.ExecContext(ctx, `INSERT INTO sessions(id,tenant_id,engine,idempotency_key,request_hash,configuration) - VALUES ($1,$2,'codex','legacy','historical',$3)`, session, tenant, input.Configuration); err != nil { - t.Fatal(err) - } - if _, err := db.ExecContext(ctx, "INSERT INTO environments(id,session_id) VALUES ($1,$2)", environment, session); err != nil { - t.Fatal(err) - } - _, digest, err := newExecutorSecret() - if err != nil { - t.Fatal(err) - } - if _, err := db.ExecContext(ctx, `INSERT INTO environment_executor_credentials(environment_id,token_sha256,issued_at) - VALUES ($1,$2,now()-interval '2 days')`, environment, digest); err != nil { - t.Fatal(err) - } - var oldSession, oldCredential, after string - if err := db.QueryRowContext(ctx, "SELECT to_jsonb(s)::text FROM sessions s WHERE id=$1", session).Scan(&oldSession); err != nil { - t.Fatal(err) - } - if err := db.QueryRowContext(ctx, "SELECT (to_jsonb(c)-'revoked_at')::text FROM environment_executor_credentials c WHERE environment_id=$1", environment).Scan(&oldCredential); err != nil { - t.Fatal(err) - } - if _, err := provider.UpTo(ctx, 26); err != nil { - t.Fatal(err) - } - if err := db.QueryRowContext(ctx, "SELECT to_jsonb(s)::text FROM sessions s WHERE id=$1", session).Scan(&after); err != nil || oldSession != after { - t.Fatal("migration changed Session", err) - } - if err := db.QueryRowContext(ctx, `SELECT (to_jsonb(c)-ARRAY['key_id','tenant_id','subject_kind','subject_id','created_at','revoked_at'])::text - FROM environment_executor_credentials c WHERE key_id=$1`, environment).Scan(&after); err != nil || oldCredential != after { - t.Fatal("migration changed legacy digest/restriction/issuance", err) - } - var retired bool - if err := db.QueryRowContext(ctx, `SELECT key_id=environment_id AND tenant_id IS NULL AND subject_kind IS NULL - AND subject_id IS NULL AND created_at IS NULL AND revoked_at IS NOT NULL FROM environment_executor_credentials WHERE key_id=$1`, environment).Scan(&retired); err != nil || !retired { - t.Fatal("migration inferred authority", err) - } - var mappings int - if err := db.QueryRowContext(ctx, "SELECT count(*) FROM execution_project_scopes").Scan(&mappings); err != nil || mappings != 0 { - t.Fatal("migration invented project", err) - } - if _, err := db.ExecContext(ctx, "UPDATE environment_executor_credentials SET revoked_at=NULL WHERE key_id=$1", environment); err == nil { - t.Fatal("unknown principal regained authority") - } - if _, err := provider.DownTo(ctx, 25); err != nil { - t.Fatal("legacy-only downgrade", err) - } - if err := db.QueryRowContext(ctx, "SELECT revoked_at IS NOT NULL FROM environment_executor_credentials WHERE environment_id=$1", environment).Scan(&retired); err != nil || !retired { - t.Fatal("downgrade undid revocation", err) - } - if _, err := provider.UpTo(ctx, 26); err != nil { - t.Fatal(err) - } - poolConfig := pool.Config() - poolConfig.ConnConfig = cfg - migrated, err := pgxpool.NewWithConfig(ctx, poolConfig) - if err != nil { - t.Fatal(err) - } - t.Cleanup(migrated.Close) - s := New(t, migrated) - p := FixtureExecutorPrincipal(t, s, tenant) - credentials := sessionService(t, s) - if _, err := sessionAdapter(s).AuthenticateEnvironmentExecutor(ctx, environment, digest); !errors.Is(err, sessions.ErrNotFound) { - t.Fatal("legacy credential accepted", err) - } - if _, err := credentials.IssueExecutorCredential(ctx, p, environment, ""); !errors.Is(err, sessions.ErrExecutorCredentialExists) { - t.Fatal("legacy key ID claimed", err) - } - if _, err := credentials.RotateExecutorCredential(ctx, p, environment); !errors.Is(err, sessions.ErrNotFound) { - t.Fatal("legacy principal claimed", err) - } - if err := credentials.RevokeExecutorCredential(ctx, p, environment); !errors.Is(err, sessions.ErrNotFound) { - t.Fatal("legacy principal manufactured", err) - } - key, err := credentials.IssueExecutorCredential(ctx, p, uuid.NewString(), "") - if err != nil { - t.Fatal(err) - } - if _, err := provider.DownTo(ctx, 25); err == nil || !strings.Contains(err.Error(), "Cannot remove durable executor principal identities") { - t.Fatal("downgrade lost principal keys", err) - } - if _, err := credentials.RotateExecutorCredential(ctx, p, key.KeyID); err != nil { - t.Fatal("failed downgrade damaged identity", err) - } -} diff --git a/services/core/tests/integration/item_order_migration_test.go b/services/core/tests/integration/item_order_migration_test.go deleted file mode 100644 index f47deb1d8..000000000 --- a/services/core/tests/integration/item_order_migration_test.go +++ /dev/null @@ -1,163 +0,0 @@ -package integration - -import ( - "context" - "database/sql" - "encoding/json" - "os" - "reflect" - "strings" - "testing" - - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/items" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgtype" - "github.com/jackc/pgx/v5/pgxpool" - "github.com/jackc/pgx/v5/stdlib" - "github.com/pressly/goose/v3" -) - -func TestItemOrderMigrationPreservesIndexedHistory(t *testing.T) { - ctx := context.Background() - _, pool := testStore(t) - schema := "item_order_" + uuid.New().String()[:8] - quoted := pgx.Identifier{schema}.Sanitize() - if _, err := pool.Exec(ctx, "CREATE SCHEMA "+quoted); err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - if _, err := pool.Exec(ctx, "DROP SCHEMA "+quoted+" CASCADE"); err != nil { - t.Error(err) - } - }) - cfg := pool.Config().ConnConfig.Copy() - cfg.RuntimeParams["search_path"] = schema - db := sql.OpenDB(stdlib.GetConnector(*cfg)) - t.Cleanup(func() { _ = db.Close() }) - provider, err := goose.NewProvider(goose.DialectPostgres, db, os.DirFS("../../migrations"), goose.WithTableName("agents_api_schema_version")) - if err != nil { - t.Fatal(err) - } - if _, err = provider.UpTo(ctx, 10); err != nil { - t.Fatal(err) - } - session, turn, tenant := uuid.NewString(), uuid.NewString(), uuid.NewString() - if _, err = db.ExecContext(ctx, "INSERT INTO sessions(id,tenant_id,engine,idempotency_key,request_hash) VALUES ($1,$2,'codex','legacy','legacy')", session, tenant); err != nil { - t.Fatal(err) - } - if _, err = db.ExecContext(ctx, "INSERT INTO turns(id,session_id) VALUES ($1,$2)", turn, session); err != nil { - t.Fatal(err) - } - for i, id := range []string{ - "00000000-0000-0000-0000-000000000003", - "00000000-0000-0000-0000-000000000001", - "00000000-0000-0000-0000-000000000002", - } { - payload := `{"type":"function_call_output"}` - if i == 1 { - payload = `{"type":"message","role":"user"}` - } - if _, err = db.ExecContext(ctx, "INSERT INTO session_items(id,session_id,turn_id,created_at,position,payload) VALUES ($1,$2,$3,'2026-01-01',0,$4)", id, session, turn, payload); err != nil { - t.Fatal(err) - } - } - readIDs := func() []string { - t.Helper() - rows, err := db.QueryContext(ctx, "SELECT id FROM session_items ORDER BY created_at,position,id") - if err != nil { - t.Fatal(err) - } - defer rows.Close() - var ids []string - for rows.Next() { - var id string - if err = rows.Scan(&id); err != nil { - t.Fatal(err) - } - ids = append(ids, id) - } - if err = rows.Err(); err != nil { - t.Fatal(err) - } - return ids - } - want := readIDs() - if _, err = provider.UpTo(ctx, 11); err != nil { - t.Fatal(err) - } - if got := readIDs(); !reflect.DeepEqual(got, want) { - t.Fatalf("migration reordered history: %v; want %v", got, want) - } - for i, id := range want { - var position int - var output pgtype.Int4 - if err = db.QueryRowContext(ctx, "SELECT position,output_index FROM session_items WHERE id=$1", id).Scan(&position, &output); err != nil { - t.Fatal(err) - } - if position != i || output.Valid != (i > 0) || (output.Valid && int(output.Int32) != i-1) { - t.Fatalf("migrated item %d: position=%d output=%+v", i, position, output) - } - } - if _, err = provider.UpTo(ctx, 14); err != nil { - t.Fatal(err) - } - var before string - if err = db.QueryRowContext(ctx, "SELECT jsonb_agg(to_jsonb(i) ORDER BY id)::text FROM session_items i").Scan(&before); err != nil { - t.Fatal(err) - } - if _, err = db.ExecContext(ctx, "UPDATE turns SET items_indexed=false WHERE id=$1", turn); err != nil { - t.Fatal(err) - } - if _, err = provider.Up(ctx); err == nil || !strings.Contains(err.Error(), "Unindexed Agents API history") { - t.Fatalf("unprepared upgrade: %v", err) - } - var indexed bool - if err = db.QueryRowContext(ctx, "SELECT items_indexed FROM turns WHERE id=$1", turn).Scan(&indexed); err != nil || indexed { - t.Fatal("failed migration changed preparation state", err) - } - // Simulate the previous release finishing its index before retrying the upgrade. - if _, err = db.ExecContext(ctx, "UPDATE turns SET items_indexed=true WHERE id=$1", turn); err != nil { - t.Fatal(err) - } - if _, err = provider.UpTo(ctx, 65); err != nil { - t.Fatal(err) - } - var after string - if err = db.QueryRowContext(ctx, "SELECT jsonb_agg(to_jsonb(i) ORDER BY id)::text FROM session_items i").Scan(&after); err != nil || after != before { - t.Fatal("upgrade changed indexed history", err) - } - - // Exercise the reversible history migrations before the Project catalog, - // whose schema intentionally cannot be downgraded. - if _, err = provider.DownTo(ctx, 10); err != nil { - t.Fatal(err) - } - if got := readIDs(); !reflect.DeepEqual(got, want) { - t.Fatalf("downgrade reordered history: %v", got) - } - if _, err = provider.Up(ctx); err != nil { - t.Fatal(err) - } - - poolConfig := pool.Config() - poolConfig.ConnConfig = cfg - migratedPool, err := pgxpool.NewWithConfig(ctx, poolConfig) - if err != nil { - t.Fatal(err) - } - t.Cleanup(migratedPool.Close) - s := New(t, migratedPool) - if _, err = transitionTurn(ctx, s, tenant, session, turn, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { - t.Fatal(err) - } - if err = executionOwner(t, New(t, migratedPool)).Sessions.AppendTurnEvents(ctx, tenant, session, turn, 1, []sessions.ExecutionEvent{{Kind: "delta", Payload: json.RawMessage(`{"item_id":"after-upgrade","delta":"continued"}`)}}); err != nil { - t.Fatal(err) - } - addedID := items.Identity(turn, "message:after-upgrade") - var position, output int - if err = db.QueryRowContext(ctx, "SELECT position,output_index FROM session_items WHERE id=$1", addedID).Scan(&position, &output); err != nil || position != 3 || output != 2 { - t.Fatalf("post-upgrade append: position=%d output=%d err=%v", position, output, err) - } -} diff --git a/services/core/tests/integration/oac_runtime_names_migration_test.go b/services/core/tests/integration/oac_runtime_names_migration_test.go deleted file mode 100644 index d4d7122c7..000000000 --- a/services/core/tests/integration/oac_runtime_names_migration_test.go +++ /dev/null @@ -1,142 +0,0 @@ -package integration - -import ( - "context" - "database/sql" - "os" - "strings" - "testing" - - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/stdlib" - "github.com/pressly/goose/v3" -) - -func runtimeNamesMigrationSchema(t *testing.T) (*sql.DB, *goose.Provider) { - t.Helper() - _, pool := testStore(t) - schema := "runtime_names_" + uuid.NewString()[:8] - quoted := pgx.Identifier{schema}.Sanitize() - if _, err := pool.Exec(t.Context(), "CREATE SCHEMA "+quoted); err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - if _, err := pool.Exec(context.Background(), "DROP SCHEMA "+quoted+" CASCADE"); err != nil { - t.Error(err) - } - }) - cfg := pool.Config().ConnConfig.Copy() - cfg.RuntimeParams["search_path"] = schema - db := sql.OpenDB(stdlib.GetConnector(*cfg)) - t.Cleanup(func() { _ = db.Close() }) - provider, err := goose.NewProvider(goose.DialectPostgres, db, os.DirFS("../../migrations"), goose.WithTableName("agents_api_schema_version")) - if err != nil { - t.Fatal(err) - } - if _, err = provider.UpTo(t.Context(), 77); err != nil { - t.Fatal(err) - } - return db, provider -} - -func TestRuntimeNamesMigrationRequiresReleasedAllocationsBothDirections(t *testing.T) { - db, provider := runtimeNamesMigrationSchema(t) - ctx := t.Context() - exec := func(query string, args ...any) { - t.Helper() - if _, err := db.ExecContext(ctx, query, args...); err != nil { - t.Fatal(err) - } - } - read := func(query string) string { - t.Helper() - var value string - if err := db.QueryRowContext(ctx, query).Scan(&value); err != nil { - t.Fatal(err) - } - return value - } - digest := strings.Repeat("a", 64) - oldRef, newRef := "parsar-core-runtime@sha256:"+digest, "oac-runtime@sha256:"+digest - specification := `{"runtime":{"microsandbox_ref":"` + oldRef + `","image_id":"sha256:` + digest + `"},"resources":{"cpus":2}}` - exec("UPDATE runtime_deployment SET specification=$1", specification) - session, tenant, environment, device, allocation := uuid.NewString(), uuid.NewString(), uuid.NewString(), uuid.NewString(), uuid.NewString() - exec(`INSERT INTO sessions(id,tenant_id,engine,idempotency_key,request_hash,configuration) VALUES ($1,$2,'codex','old','old','{"history":"parsar_worker /home/runtime/.parsar"}')`, session, tenant) - exec("INSERT INTO environments(id,session_id) VALUES ($1,$2)", environment, session) - exec("INSERT INTO devices(id,tenant_id,name,credential_hash) VALUES ($1,$2,'old',$3)", device, tenant, digest) - exec("INSERT INTO runtime_allocations(id,environment_id,device_id,provider_key) VALUES ($1,$2,$3,$4)", allocation, environment, device, uuid.NewString()) - history := read("SELECT to_jsonb(s)::text FROM sessions s") - for _, direction := range []string{"up", "down"} { - before := read("SELECT specification::text FROM runtime_deployment") - for _, state := range []string{"creating", "running", "cleanup_pending", "suspended"} { - phase, stored := "disabled", state - if state == "suspended" { - phase, stored = "suspended", "running" - } - exec("UPDATE runtime_allocations SET state=$1,released_at=NULL,create_settled=true,compute_phase=$2", stored, phase) - allocationBefore := read("SELECT to_jsonb(a)::text FROM runtime_allocations a") - var err error - if direction == "up" { - _, err = provider.UpTo(ctx, 78) - } else { - _, err = provider.DownTo(ctx, 77) - } - if err == nil || !strings.Contains(err.Error(), "Drain the sandbox deployment") { - t.Fatalf("%s with %s allocation did not refuse: %v", direction, state, err) - } - if got := read("SELECT specification::text FROM runtime_deployment"); got != before { - t.Fatal("refused migration changed specification") - } - if got := read("SELECT to_jsonb(a)::text FROM runtime_allocations a"); got != allocationBefore { - t.Fatal("refused migration changed allocation") - } - } - exec("UPDATE runtime_allocations SET state='released',released_at=clock_timestamp(),create_settled=true") - released := read("SELECT to_jsonb(a)::text FROM runtime_allocations a") - var err error - expected := strings.Replace(before, oldRef, newRef, 1) - if direction == "up" { - _, err = provider.UpTo(ctx, 78) - } else { - _, err = provider.DownTo(ctx, 77) - expected = strings.Replace(before, newRef, oldRef, 1) - } - if err != nil { - t.Fatal("released allocation blocked migration", err) - } - if got := read("SELECT specification::text FROM runtime_deployment"); got != expected { - t.Fatalf("migration changed fields beyond reference: %s", got) - } - if got := read("SELECT to_jsonb(a)::text FROM runtime_allocations a"); got != released { - t.Fatal("migration changed released allocation") - } - if got := read("SELECT to_jsonb(s)::text FROM sessions s"); got != history { - t.Fatal("migration rewrote historical Session content") - } - } -} - -func TestRuntimeNamesMigrationPreservesAbsentAndOtherReferences(t *testing.T) { - db, provider := runtimeNamesMigrationSchema(t) - for _, spec := range []string{`{}`, `{"runtime":null}`, `{"runtime":{"microsandbox_ref":"custom-runtime@sha256:` + strings.Repeat("b", 64) + `"}}`} { - if _, err := db.ExecContext(t.Context(), "UPDATE runtime_deployment SET specification=$1", spec); err != nil { - t.Fatal(err) - } - for _, up := range []bool{true, false} { - var err error - if up { - _, err = provider.UpTo(t.Context(), 78) - } else { - _, err = provider.DownTo(t.Context(), 77) - } - if err != nil { - t.Fatal(err) - } - var equal bool - if err = db.QueryRowContext(t.Context(), "SELECT specification=$1::jsonb FROM runtime_deployment", spec).Scan(&equal); err != nil || !equal { - t.Fatal("migration changed unrelated reference", err) - } - } - } -} diff --git a/services/core/tests/integration/provider_configuration_migration_test.go b/services/core/tests/integration/provider_configuration_migration_test.go deleted file mode 100644 index 1e9d06f5c..000000000 --- a/services/core/tests/integration/provider_configuration_migration_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package integration - -import ( - "bytes" - "database/sql" - "encoding/json" - "os" - "testing" - - "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" -) - -func TestProviderConfigurationMigrationPreservesCiphertextAndRetainedOwnership(t *testing.T) { - s, w, view, input := webSpecificationFixture(t, "e2b") - tenant, session := managedArchiveSession(t, s, managerSessionInput(uuid.NewString())) - archiveAllocation(t, w, tenant, session, view.InstallationID) - input.Configuration.(*e2b.DeploymentConfiguration).Template = "next:" + uuid.NewString() - input.ExpectedGeneration = 1 - if _, err := deploymentExecution(t, w).Update(SandboxResetTestContext(t.Context()), view.InstallationID, input); err != nil { - t.Fatal(err) - } - db := sql.OpenDB(stdlib.GetConnector(*s.pool.Config().ConnConfig)) - defer db.Close() - migration, err := goose.NewProvider(goose.DialectPostgres, db, os.DirFS("../../migrations"), goose.WithTableName("agents_api_schema_version")) - if err != nil { - t.Fatal(err) - } - if _, err = migration.DownTo(t.Context(), 90); err != nil { - t.Fatal(err) - } - // A valid old record can have only part of its observation populated. - if _, err = db.ExecContext(t.Context(), "UPDATE runtime_deployment SET e2b_template_cpus=2"); err != nil { - t.Fatal(err) - } - var before, after []byte - var generation int64 - if err = db.QueryRowContext(t.Context(), "SELECT e2b_credential,generation FROM runtime_deployment").Scan(&before, &generation); err != nil { - t.Fatal(err) - } - if _, err = migration.Up(t.Context()); err != nil { - t.Fatal(err) - } - if err = db.QueryRowContext(t.Context(), "SELECT provider_credential FROM runtime_deployment").Scan(&after); err != nil || !bytes.Equal(before, after) { - t.Fatal("ciphertext rewritten", err) - } - restored, err := deploymentService(t, s).Setup(t.Context()) - if err != nil || restored.Generation != uint64(generation) || restored.Configuration.(*e2b.DeploymentConfiguration).APIKey != input.Configuration.(*e2b.DeploymentConfiguration).APIKey { - t.Fatal("ownership or credential changed", err) - } - public, err := deploymentService(t, s).View(t.Context()) - if err != nil { - t.Fatal(err) - } - var metadata struct { - TemplateBuild struct { - Resources struct { - CPUs *int `json:"cpus"` - Memory *int `json:"memory_mib"` - } `json:"resources"` - } `json:"template_build"` - } - if err = json.Unmarshal(public.Metadata, &metadata); err != nil || metadata.TemplateBuild.Resources.CPUs == nil || *metadata.TemplateBuild.Resources.CPUs != 2 || metadata.TemplateBuild.Resources.Memory != nil { - t.Fatal("partial metadata lost", string(public.Metadata), err) - } - var retained int - if err = db.QueryRowContext(t.Context(), "SELECT count(*) FROM runtime_deployment_generations WHERE generation=1 AND provider_config->>'template'<>$1", input.Configuration.(*e2b.DeploymentConfiguration).Template).Scan(&retained); err != nil || retained != 1 { - t.Fatal("retained ownership lost", err) - } - if _, err = db.ExecContext(t.Context(), "UPDATE runtime_deployment_generations SET provider_config='{}'"); err == nil { - t.Fatal("migration removed immutability") - } - if _, err = migration.DownTo(t.Context(), 90); err != nil { - t.Fatal(err) - } - var cpu int - var memory *int - if err = db.QueryRowContext(t.Context(), "SELECT e2b_credential,e2b_template_cpus,e2b_template_memory_mib FROM runtime_deployment").Scan(&after, &cpu, &memory); err != nil || !bytes.Equal(before, after) || cpu != 2 || memory != nil { - t.Fatal("downgrade lost valid data", err) - } -} diff --git a/services/core/tests/integration/provider_registration_migration_test.go b/services/core/tests/integration/provider_registration_migration_test.go deleted file mode 100644 index bbc4cfb1e..000000000 --- a/services/core/tests/integration/provider_registration_migration_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package integration - -import ( - "database/sql" - "os" - "strings" - "testing" - - "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" -) - -func TestProviderRegistrationDowngradePreservesCustomEndpoints(t *testing.T) { - s, w, view, input := webSpecificationFixture(t, "e2b") - tenant, session := managedArchiveSession(t, s, managerSessionInput(uuid.NewString())) - archiveAllocation(t, w, tenant, session, view.InstallationID) - input.Configuration.(*e2b.DeploymentConfiguration).APIURL, input.Configuration.(*e2b.DeploymentConfiguration).Domain = "https://api.example.test", "example.test" - input.ExpectedGeneration = view.Generation - if _, err := deploymentExecution(t, w).Update(SandboxResetTestContext(t.Context()), view.InstallationID, input); err != nil { - t.Fatal(err) - } - tenant, session = managedArchiveSession(t, s, managerSessionInput(uuid.NewString())) - archiveAllocation(t, w, tenant, session, view.InstallationID) - input.Configuration.(*e2b.DeploymentConfiguration).Template = "next:" + uuid.NewString() - input.ExpectedGeneration = 2 - if _, err := deploymentExecution(t, w).Update(SandboxResetTestContext(t.Context()), view.InstallationID, input); err != nil { - t.Fatal(err) - } - db := sql.OpenDB(stdlib.GetConnector(*s.pool.Config().ConnConfig)) - defer db.Close() - migrations, err := goose.NewProvider(goose.DialectPostgres, db, os.DirFS("../../migrations"), goose.WithTableName("agents_api_schema_version")) - if err != nil { - t.Fatal(err) - } - if _, err := migrations.DownTo(t.Context(), 88); err != nil { - t.Fatal(err) - } - for generation, want := range map[int]string{1: "", 2: input.Configuration.(*e2b.DeploymentConfiguration).APIURL} { - var endpoint string - if err := db.QueryRowContext(t.Context(), "SELECT e2b_api_url FROM runtime_deployment_generations WHERE generation=$1", generation).Scan(&endpoint); err != nil || endpoint != want { - t.Fatal("downgrade changed endpoint identity", generation, endpoint, err) - } - } - var endpoint string - if err := db.QueryRowContext(t.Context(), "SELECT e2b_api_url FROM runtime_deployment").Scan(&endpoint); err != nil || endpoint != input.Configuration.(*e2b.DeploymentConfiguration).APIURL { - t.Fatal("downgrade changed current endpoint", endpoint, err) - } - if _, err := db.ExecContext(t.Context(), "UPDATE runtime_deployment_generations SET e2b_domain='changed.test' WHERE generation=1"); err == nil || !strings.Contains(err.Error(), "Retained sandbox specifications are immutable") { - t.Fatal("downgrade left specifications mutable", err) - } - if _, err := migrations.DownTo(t.Context(), 86); err == nil || !strings.Contains(err.Error(), "custom endpoint is retained") { - t.Fatal("downgrade discarded a custom endpoint", err) - } -} diff --git a/services/core/tests/integration/public_url_migration_test.go b/services/core/tests/integration/public_url_migration_test.go deleted file mode 100644 index d2e6e522d..000000000 --- a/services/core/tests/integration/public_url_migration_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package integration - -import ( - "context" - "database/sql" - "os" - "strings" - "testing" - - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/stdlib" - "github.com/pressly/goose/v3" -) - -// publicURLMigrationSchema migrates an isolated schema up to version 74. -func publicURLMigrationSchema(t *testing.T) (*sql.DB, *goose.Provider) { - t.Helper() - _, pool := testStore(t) - ctx := t.Context() - schema := "public_url_" + uuid.NewString()[:8] - quoted := pgx.Identifier{schema}.Sanitize() - if _, err := pool.Exec(ctx, "CREATE SCHEMA "+quoted); err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - if _, err := pool.Exec(context.Background(), "DROP SCHEMA "+quoted+" CASCADE"); err != nil { - t.Error(err) - } - }) - cfg := pool.Config().ConnConfig.Copy() - cfg.RuntimeParams["search_path"] = schema - db := sql.OpenDB(stdlib.GetConnector(*cfg)) - t.Cleanup(func() { _ = db.Close() }) - provider, err := goose.NewProvider(goose.DialectPostgres, db, os.DirFS("../../migrations"), goose.WithTableName("agents_api_schema_version")) - if err != nil { - t.Fatal(err) - } - if _, err := provider.UpTo(ctx, 74); err != nil { - t.Fatal(err) - } - return db, provider -} - -// The deployment's saved Core address moves to each node it enrolled, and a -// rollback takes it back from them. -func TestPublicURLMigrationMovesTheAddressToNodes(t *testing.T) { - ctx := t.Context() - db, provider := publicURLMigrationSchema(t) - installation, active, removed := uuid.NewString(), uuid.NewString(), uuid.NewString() - if _, err := db.ExecContext(ctx, `UPDATE runtime_deployment SET installation_id=$1, web_managed=true, provider_kind='docker', mode='nodes', generation=1, - core_url='https://core.example', backend_fingerprint=$2`, installation, strings.Repeat("a", 64)); err != nil { - t.Fatal(err) - } - for _, node := range []struct { - id string - removed bool - }{{active, false}, {removed, true}} { - if _, err := db.ExecContext(ctx, `INSERT INTO runtime_nodes(id,installation_id,name,backend_fingerprint,credential_sha256,max_active,max_retained,removed_at) - VALUES ($1,$2,'node',$3,$4,2,8,CASE WHEN $5 THEN clock_timestamp() END)`, node.id, installation, strings.Repeat("a", 64), strings.Repeat("c", 64), node.removed); err != nil { - t.Fatal(err) - } - } - if _, err := provider.UpTo(ctx, 75); err != nil { - t.Fatal(err) - } - address := func(id string) string { - t.Helper() - var value string - if err := db.QueryRowContext(ctx, "SELECT core_url FROM runtime_nodes WHERE id=$1", id).Scan(&value); err != nil { - t.Fatal(err) - } - return value - } - if address(active) != "https://core.example" || address(removed) != "" { - t.Fatal("migration did not record the enrolled address on the active node only") - } - if _, err := provider.DownTo(ctx, 74); err != nil { - t.Fatal(err) - } - var restored string - if err := db.QueryRowContext(ctx, "SELECT core_url FROM runtime_deployment").Scan(&restored); err != nil || restored != "https://core.example" { - t.Fatal("rollback lost the deployment address", restored, err) - } -} - -// An E2B deployment has no node to take its address back from, so the -// rollback refuses instead of leaving the deployment without one. -func TestPublicURLMigrationRollbackRefusesE2B(t *testing.T) { - ctx := t.Context() - db, provider := publicURLMigrationSchema(t) - if _, err := db.ExecContext(ctx, `UPDATE runtime_deployment SET installation_id=$1, web_managed=true, provider_kind='e2b', mode='direct', generation=1, - core_url='https://core.example', backend_fingerprint=$2, e2b_template='runtime:build', e2b_credential='\x01'`, uuid.NewString(), strings.Repeat("a", 64)); err != nil { - t.Fatal(err) - } - if _, err := provider.UpTo(ctx, 75); err != nil { - t.Fatal(err) - } - if _, err := provider.DownTo(ctx, 74); err == nil || !strings.Contains(err.Error(), "Cannot restore the sandbox deployment Core address") { - t.Fatal("rollback discarded the E2B deployment address", err) - } -} diff --git a/services/core/tests/integration/sandbox_generations_test.go b/services/core/tests/integration/sandbox_generations_test.go index 2b08131df..44aef4f80 100644 --- a/services/core/tests/integration/sandbox_generations_test.go +++ b/services/core/tests/integration/sandbox_generations_test.go @@ -1,16 +1,12 @@ package integration import ( - "database/sql" - "os" "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" ) func TestE2BGenerationsRetainOwnershipAndUseCurrentCredential(t *testing.T) { @@ -149,103 +145,6 @@ func TestPendingPlacementGenerationSurvivesRepeatedUpdates(t *testing.T) { } } -func TestGenerationMigrationBackfillsAndRejectsLossyDowngrade(t *testing.T) { - db, migrations := runtimeNamesMigrationSchema(t) - ctx := t.Context() - if _, err := migrations.UpTo(ctx, 80); err != nil { - t.Fatal(err) - } - installation, node := uuid.NewString(), uuid.NewString() - if _, err := db.ExecContext(ctx, `UPDATE runtime_deployment SET installation_id=$1,backend_fingerprint=repeat('a',64),provider_kind='docker',mode='nodes',generation=1`, installation); err != nil { - t.Fatal(err) - } - if _, err := db.ExecContext(ctx, `INSERT INTO runtime_nodes(id,installation_id,name,backend_fingerprint,credential_sha256,max_active,max_retained,deployment_generation) VALUES($1,$2,'old',repeat('a',64),repeat('b',64),1,1,1)`, node, installation); err != nil { - t.Fatal(err) - } - session, tenant, environment, device, allocation := uuid.NewString(), uuid.NewString(), uuid.NewString(), uuid.NewString(), uuid.NewString() - exec := func(query string, args ...any) { - t.Helper() - if _, err := db.ExecContext(ctx, query, args...); err != nil { - t.Fatal(err) - } - } - exec(`INSERT INTO sessions(id,tenant_id,engine,idempotency_key,request_hash,configuration) VALUES($1,$2,'codex','old','old','{}')`, session, tenant) - exec(`INSERT INTO environments(id,session_id) VALUES($1,$2)`, environment, session) - exec(`INSERT INTO devices(id,tenant_id,name,credential_hash) VALUES($1,$2,'old',repeat('b',64))`, device, tenant) - exec(`INSERT INTO runtime_placements(environment_id,node_id) VALUES($1,$2)`, environment, node) - exec(`INSERT INTO runtime_allocations(id,environment_id,device_id,provider_key,node_id) VALUES($1,$2,$3,$4,$5)`, allocation, environment, device, installation, node) - if _, err := migrations.UpTo(ctx, 81); err != nil { - t.Fatal(err) - } - for _, table := range []string{"runtime_allocations", "runtime_placements"} { - var generation int64 - if err := db.QueryRowContext(ctx, `SELECT deployment_generation FROM `+table).Scan(&generation); err != nil || generation != 1 { - t.Fatal("inexact migration backfill", table, generation, err) - } - } - var pin int64 - if err := db.QueryRowContext(ctx, `SELECT ready_generation FROM runtime_nodes WHERE id=$1`, node).Scan(&pin); err != nil || pin != 1 { - t.Fatal(pin, err) - } - if _, err := db.ExecContext(ctx, `UPDATE runtime_deployment SET generation=2`); err != nil { - t.Fatal(err) - } - if _, err := migrations.DownTo(ctx, 80); err == nil { - t.Fatal("downgrade erased offline serving pin") - } - if _, err := db.ExecContext(ctx, `UPDATE runtime_nodes SET removed_at=clock_timestamp(),ready_generation=NULL`); err != nil { - t.Fatal(err) - } - if _, err := migrations.DownTo(ctx, 80); err == nil { - t.Fatal("downgrade discarded held allocation/placement after pin removal") - } - exec(`UPDATE runtime_allocations SET state='released',released_at=clock_timestamp(),create_settled=true`) - if _, err := migrations.DownTo(ctx, 80); err == nil { - t.Fatal("downgrade discarded unreleased placement") - } - exec(`UPDATE runtime_placements SET released_at=clock_timestamp()`) - if _, err := migrations.DownTo(ctx, 80); err != nil { - t.Fatal("settled downgrade failed", err) - } -} - -func TestGenerationDowngradeRefusesOldAllocation(t *testing.T) { - s, w, view, input := webSpecificationFixture(t, "e2b") - tenant, session := managedArchiveSession(t, s, managerSessionInput(uuid.NewString())) - owner := archiveAllocation(t, w, tenant, session, view.InstallationID) - input.Configuration.(*e2b.DeploymentConfiguration).Template = "next:" + uuid.NewString() - input.ExpectedGeneration = 1 - if _, err := deploymentExecution(t, w).Update(SandboxResetTestContext(t.Context()), view.InstallationID, input); err != nil { - t.Fatal(err) - } - db := sql.OpenDB(stdlib.GetConnector(*s.pool.Config().ConnConfig)) - defer db.Close() - migrations, err := goose.NewProvider(goose.DialectPostgres, db, os.DirFS("../../migrations"), goose.WithTableName("agents_api_schema_version")) - if err != nil { - t.Fatal(err) - } - if _, err = migrations.DownTo(t.Context(), 80); err == nil { - t.Fatal("downgrade erased old owned allocation") - } - // Earlier down migrations can commit before the generation guard vetoes - // downgrade. Restore the current schema before invoking current Store code. - if _, err = migrations.Up(t.Context()); err != nil { - t.Fatal(err) - } - if _, err = deploymentExecution(t, w).RequestCleanup(t.Context(), owner); err != nil { - t.Fatal(err) - } - if _, err = deploymentExecution(t, w).SettleCreation(t.Context(), owner); err != nil { - t.Fatal(err) - } - if _, err = deploymentExecution(t, w).ReleaseAllocation(t.Context(), owner); err != nil { - t.Fatal(err) - } - if _, err = migrations.DownTo(t.Context(), 80); err != nil { - t.Fatal("released history prevented safe downgrade", err) - } -} - func TestGenerationUpdateSerializesWithAllocationAdmission(t *testing.T) { s, w, view, input := webSpecificationFixture(t, "e2b") changes, deployments := deploymentExecution(t, w), deploymentService(t, s) diff --git a/services/core/tests/integration/sandbox_reset_migration_test.go b/services/core/tests/integration/sandbox_reset_migration_test.go deleted file mode 100644 index 4ca85d199..000000000 --- a/services/core/tests/integration/sandbox_reset_migration_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package integration - -import ( - "strings" - "testing" -) - -func TestSandboxResetMigrationResumesAdmissionAndGuardsDowngrade(t *testing.T) { - db, provider := runtimeNamesMigrationSchema(t) - ctx := t.Context() - if _, err := provider.UpTo(ctx, 78); err != nil { - t.Fatal(err) - } - exec := func(q string, args ...any) { - t.Helper() - if _, err := db.ExecContext(ctx, q, args...); err != nil { - t.Fatal(err) - } - } - for _, paused := range []bool{false, true} { - exec(`UPDATE runtime_deployment SET web_managed=true,provider_kind='docker',mode='nodes',generation=1,maintenance=$1`, paused) - if _, err := provider.UpTo(ctx, 79); err != nil { - t.Fatal(err) - } - var resumed bool - if err := db.QueryRowContext(ctx, `SELECT NOT admission_paused AND reset_clear IS NULL AND generation=1 FROM runtime_deployment`).Scan(&resumed); err != nil || !resumed { - t.Fatal("migration invented a reset or kept maintenance", err) - } - if _, err := provider.DownTo(ctx, 78); err != nil { - t.Fatal(err) - } - } - if _, err := provider.UpTo(ctx, 79); err != nil { - t.Fatal(err) - } - exec(`UPDATE runtime_deployment SET admission_paused=true,reset_clear='auto',reset_requested_at=clock_timestamp(),reset_deadline_at=clock_timestamp()+interval '1 hour',reset_audit='{}'`) - if _, err := provider.DownTo(ctx, 78); err == nil || !strings.Contains(err.Error(), "reset is active") { - t.Fatal("active reset downgrade", err) - } - exec(`UPDATE runtime_deployment SET admission_paused=false,reset_clear=NULL,reset_requested_at=NULL,reset_deadline_at=NULL,reset_audit=NULL,provider_kind='',mode='',generation=2`) - if _, err := provider.DownTo(ctx, 78); err == nil || !strings.Contains(err.Error(), "Configure a sandbox provider") { - t.Fatal("completed reset lost generation", err) - } - var generation int64 - if err := db.QueryRowContext(ctx, `SELECT generation FROM runtime_deployment`).Scan(&generation); err != nil || generation != 2 { - t.Fatal(generation, err) - } - exec(`UPDATE runtime_deployment SET provider_kind='docker',mode='nodes',generation=3`) - if _, err := provider.DownTo(ctx, 78); err != nil { - t.Fatal("configured downgrade", err) - } -} diff --git a/services/core/tests/integration/skill_default_metadata_migration_test.go b/services/core/tests/integration/skill_default_metadata_migration_test.go deleted file mode 100644 index 1fe748407..000000000 --- a/services/core/tests/integration/skill_default_metadata_migration_test.go +++ /dev/null @@ -1,118 +0,0 @@ -package integration - -import ( - "context" - "database/sql" - "os" - "testing" - - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/stdlib" - "github.com/pressly/goose/v3" -) - -func TestSkillDefaultMetadataMigrationRepairsOnlyResourceMetadata(t *testing.T) { - _, pool := testStore(t) - ctx := t.Context() - schema := "skill_metadata_" + uuid.NewString()[:8] - quoted := pgx.Identifier{schema}.Sanitize() - if _, err := pool.Exec(ctx, "CREATE SCHEMA "+quoted); err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - if _, err := pool.Exec(context.Background(), "DROP SCHEMA "+quoted+" CASCADE"); err != nil { - t.Error(err) - } - }) - cfg := pool.Config().ConnConfig.Copy() - cfg.RuntimeParams["search_path"] = schema - db := sql.OpenDB(stdlib.GetConnector(*cfg)) - t.Cleanup(func() { _ = db.Close() }) - provider, err := goose.NewProvider(goose.DialectPostgres, db, os.DirFS("../../migrations"), goose.WithTableName("agents_api_schema_version")) - if err != nil { - t.Fatal(err) - } - if _, err := provider.UpTo(ctx, 54); err != nil { - t.Fatal(err) - } - tenant, foreign := uuid.NewString(), uuid.NewString() - bad, valid := uuid.NewString(), uuid.NewString() - tx, err := db.BeginTx(ctx, nil) - if err != nil { - t.Fatal(err) - } - defer tx.Rollback() - for _, fixture := range []struct { - id, tenant string - defaultVersion int - }{{bad, tenant, 2}, {valid, foreign, 1}} { - if _, err := tx.ExecContext(ctx, `INSERT INTO skills(id,tenant_id,name,description,default_version,latest_version,next_version) - VALUES ($1,$2,'first','First description.', $3,2,3)`, fixture.id, fixture.tenant, fixture.defaultVersion); err != nil { - t.Fatal(err) - } - for i, name := range []string{"first", "second"} { - description := []string{"First description.", "Second description."}[i] - if _, err := tx.ExecContext(ctx, `INSERT INTO skill_versions(id,tenant_id,skill_id,version,name,description,contents) - VALUES ($1,$2,$3,$4,$5,$6,$7)`, uuid.NewString(), fixture.tenant, fixture.id, i+1, name, description, []byte{73, byte(i + 1), 29}); err != nil { - t.Fatal(err) - } - } - } - session := uuid.NewString() - if _, err := tx.ExecContext(ctx, `INSERT INTO sessions(id,tenant_id,engine,idempotency_key,request_hash,configuration) - VALUES ($1,$2,'codex','historical','historical',jsonb_set('{"environment":{"skills":[{"type":"skill_reference","version":"1","name":"first","description":"First description."}]}}', '{environment,skills,0,skill_id}', to_jsonb($3::text)))`, session, tenant, "skill_"+bad); err != nil { - t.Fatal(err) - } - if _, err := tx.ExecContext(ctx, "INSERT INTO environment_setups(session_id,contents) VALUES ($1,$2)", session, []byte{94, 17, 28}); err != nil { - t.Fatal(err) - } - if err := tx.Commit(); err != nil { - t.Fatal(err) - } - snapshot := func(query string) string { - t.Helper() - var result string - if err := db.QueryRowContext(ctx, query).Scan(&result); err != nil { - t.Fatal(err) - } - return result - } - const preserved = `SELECT jsonb_build_object( - 'skills', (SELECT jsonb_agg(to_jsonb(s)-'name'-'description' ORDER BY id) FROM skills s), - 'versions', (SELECT jsonb_agg(to_jsonb(v) ORDER BY id) FROM skill_versions v), - 'sessions', (SELECT jsonb_agg(to_jsonb(s) ORDER BY id) FROM sessions s), - 'setups', (SELECT jsonb_agg(to_jsonb(e) ORDER BY session_id) FROM environment_setups e))::text` - before := snapshot(preserved) - assertMetadata := func() { - t.Helper() - for _, fixture := range []struct{ id, name, description string }{ - {bad, "second", "Second description."}, {valid, "first", "First description."}, - } { - var name, description string - if err := db.QueryRowContext(ctx, "SELECT name,description FROM skills WHERE id=$1", fixture.id).Scan(&name, &description); err != nil || name != fixture.name || description != fixture.description { - t.Fatal("incorrect repaired/control metadata", name, description, err) - } - } - if snapshot(preserved) != before { - t.Fatal("migration changed pointers, immutable versions or frozen Session data") - } - } - if _, err := provider.UpTo(ctx, 55); err != nil { - t.Fatal(err) - } - assertMetadata() - const skillRows = `SELECT jsonb_agg(to_jsonb(s) || jsonb_build_object('row_version', xmin::text) ORDER BY id)::text FROM skills s` - repaired := snapshot(skillRows) - if _, err := provider.DownTo(ctx, 54); err != nil { - t.Fatal(err) - } - assertMetadata() - if _, err := provider.UpTo(ctx, 55); err != nil { - t.Fatal(err) - } - assertMetadata() - if snapshot(skillRows) != repaired { - t.Fatal("reapplying data repair rewrote already correct rows") - } -} diff --git a/services/core/tests/integration/vault_status_migration_test.go b/services/core/tests/integration/vault_status_migration_test.go deleted file mode 100644 index 671568940..000000000 --- a/services/core/tests/integration/vault_status_migration_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package integration - -import ( - "context" - "database/sql" - "os" - "strings" - "testing" - - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/stdlib" - "github.com/pressly/goose/v3" -) - -func TestVaultStatusMigrationPreservesResources(t *testing.T) { - _, pool := testStore(t) - ctx := t.Context() - schema := "vault_status_" + uuid.NewString()[:8] - quoted := pgx.Identifier{schema}.Sanitize() - if _, err := pool.Exec(ctx, "CREATE SCHEMA "+quoted); err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - if _, err := pool.Exec(context.Background(), "DROP SCHEMA "+quoted+" CASCADE"); err != nil { - t.Error(err) - } - }) - cfg := pool.Config().ConnConfig.Copy() - cfg.RuntimeParams["search_path"] = schema - db := sql.OpenDB(stdlib.GetConnector(*cfg)) - t.Cleanup(func() { _ = db.Close() }) - provider, err := goose.NewProvider(goose.DialectPostgres, db, os.DirFS("../../migrations"), goose.WithTableName("agents_api_schema_version")) - if err != nil { - t.Fatal(err) - } - if _, err := provider.UpTo(ctx, 31); err != nil { - t.Fatal(err) - } - id, tenant := uuid.NewString(), uuid.NewString() - if _, err := db.ExecContext(ctx, `INSERT INTO vaults(id,tenant_id,name,metadata) VALUES ($1,$2,'Existing Vault','{"purpose":"retained"}')`, id, tenant); err != nil { - t.Fatal(err) - } - snapshot := func() string { - t.Helper() - var value string - if err := db.QueryRowContext(ctx, "SELECT (to_jsonb(v)-'status')::text FROM vaults v WHERE id=$1", id).Scan(&value); err != nil { - t.Fatal(err) - } - return value - } - before := snapshot() - if _, err := provider.UpTo(ctx, 32); err != nil { - t.Fatal(err) - } - var status string - if err := db.QueryRowContext(ctx, "SELECT status FROM vaults WHERE id=$1", id).Scan(&status); err != nil || status != "active" || snapshot() != before { - t.Fatal("migration changed historical resource", status, err) - } - if err := db.QueryRowContext(ctx, "INSERT INTO vaults(id,tenant_id,metadata) VALUES ($1,$2,'{}') RETURNING status", uuid.NewString(), tenant).Scan(&status); err != nil || status != "active" { - t.Fatal("new Vault default", status, err) - } - for _, invalid := range []any{nil, "deleted"} { - if _, err := db.ExecContext(ctx, "UPDATE vaults SET status=$1 WHERE id=$2", invalid, id); err == nil { - t.Fatal("invalid classification accepted") - } - } - if _, err := provider.DownTo(ctx, 31); err != nil || snapshot() != before { - t.Fatal("reversible active-only migration", err) - } - if _, err := provider.UpTo(ctx, 32); err != nil { - t.Fatal(err) - } - if _, err := db.ExecContext(ctx, "UPDATE vaults SET status='archived' WHERE id=$1", id); err != nil { - t.Fatal(err) - } - if _, err := provider.DownTo(ctx, 31); err == nil || !strings.Contains(err.Error(), "Cannot remove archived Vault classification") { - t.Fatal("downgrade lost archived classification", err) - } - if err := db.QueryRowContext(ctx, "SELECT status FROM vaults WHERE id=$1", id).Scan(&status); err != nil || status != "archived" || snapshot() != before { - t.Fatal("failed downgrade changed data", status, err) - } -} diff --git a/services/core/tests/integration/web_deployment_only_migration_test.go b/services/core/tests/integration/web_deployment_only_migration_test.go deleted file mode 100644 index 1bbec6ad9..000000000 --- a/services/core/tests/integration/web_deployment_only_migration_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package integration - -import ( - "strings" - "testing" - - "github.com/google/uuid" -) - -// A process-configured deployment refuses the upgrade; a Web-managed one -// keeps its installation and reset in both directions. -func TestWebDeploymentOnlyMigrationRefusesProcessDeployment(t *testing.T) { - db, provider := runtimeNamesMigrationSchema(t) - ctx := t.Context() - if _, err := provider.UpTo(ctx, 92); err != nil { - t.Fatal(err) - } - installation := uuid.NewString() - if _, err := db.ExecContext(ctx, `UPDATE runtime_deployment SET installation_id=$1, backend_fingerprint=$2`, installation, strings.Repeat("a", 64)); err != nil { - t.Fatal(err) - } - if _, err := provider.UpTo(ctx, 93); err == nil || !strings.Contains(err.Error(), "configured outside Web setup") { - t.Fatal("process deployment upgraded", err) - } - if _, err := db.ExecContext(ctx, `UPDATE runtime_deployment SET web_managed=true, provider_kind='docker', mode='nodes', generation=1, - admission_paused=true, reset_clear='force', reset_requested_at=now(), reset_forced_at=now(), reset_audit='{}'`); err != nil { - t.Fatal(err) - } - if _, err := provider.UpTo(ctx, 93); err != nil { - t.Fatal(err) - } - if _, err := provider.DownTo(ctx, 92); err != nil { - t.Fatal(err) - } - var restored bool - if err := db.QueryRowContext(ctx, `SELECT web_managed AND local_node_id IS NULL AND admission_paused AND installation_id=$1 FROM runtime_deployment`, installation).Scan(&restored); err != nil || !restored { - t.Fatal("downgrade lost the Web installation", err) - } -} diff --git a/website/tests/install.test.mjs b/website/tests/install.test.mjs index 5c1452ab7..8a8444284 100644 --- a/website/tests/install.test.mjs +++ b/website/tests/install.test.mjs @@ -27,7 +27,7 @@ exit "$DOWNLOAD_STATUS" writeFileSync(join(root, 'sleep'), '#!/bin/sh\n') chmodSync(join(root, 'sleep'), 0o700) chmodSync(join(root, 'curl'), 0o700) - const args = ['--upgrade', '--install-dir', '/path with spaces/core', '--version', 'v0.0.10'] + const args = ['--install-dir', '/path with spaces/core', '--version', 'v0.0.10'] for (const downloadStatus of [0, 22]) { const result = spawnSync('/bin/bash', ['-s', '--', ...args], { input: readFileSync(script), encoding: 'utf8', From e9ef1a63fd70fd2da47e2563febe4b12adc6af15 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Tue, 20 Oct 2026 07:03:13 +0000 Subject: [PATCH 2/2] Address review: forward migrations, reinstall guidance, leftovers --- AGENTS.md | 2 +- CONTRIBUTING.md | 2 +- deploy/install-source.py | 4 +-- deploy/test_install_source.py | 2 +- docs/configuration.md | 2 +- docs/getting-started/install-options.md | 2 +- docs/getting-started/troubleshooting.md | 4 +-- docs/maintainers.md | 2 +- docs/zh/configuration.md | 4 +-- docs/zh/getting-started/install-options.md | 4 +-- docs/zh/getting-started/troubleshooting.md | 6 ++-- docs/zh/maintainers.md | 4 +-- services/core/IMPLEMENTATION.md | 2 +- services/core/README.md | 2 +- services/core/cmd/oac/init.go | 2 +- services/core/cmd/oac/init_test.go | 15 +--------- services/core/cmd/oac/install.go | 2 +- services/core/cmd/oac/install_test.go | 28 ++++--------------- services/core/tools/e2b-provider/README.md | 2 +- services/core/tools/e2b-provider/state.py | 3 +- .../core/tools/e2b-provider/state_test.py | 10 +------ 21 files changed, 32 insertions(+), 72 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0294c0e47..052d766ca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -69,7 +69,7 @@ The categories are [process settings](docs/configuration.md#process-settings), [ ### Pre-release: no compatibility layers -OpenAgentCore has had no public release. Replace superseded interfaces, execution paths and files outright. Keep no version fallback, compatibility shim or migration for superseded behavior. Before the first public release, a schema change edits the baseline migration in place, and an installation created from an earlier baseline is reinstalled. Keep the pinned official public protocol, valid data and still-used, verified infrastructure. Do not rewrite working infrastructure only to rename it. +OpenAgentCore has had no public release. Replace superseded interfaces, execution paths and files outright. Keep no version fallback, compatibility shim or migration for superseded behavior. An installation of another release is reinstalled, never upgraded. A schema change adds a forward migration, so existing development and test databases follow it; the migrations may be folded into one baseline at any time. Keep the pinned official public protocol, valid data and still-used, verified infrastructure. Do not rewrite working infrastructure only to rename it. ## Documentation diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b3cc650b5..6901af58d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -95,7 +95,7 @@ The role needs `CREATE DATABASE`: tests of database-wide state, such as the exec - The standard fields are generated into `v1/official.gen.go`. `go-bindings.json` lists the types Core consumes and overrides only the Go representation or field order that existing storage or custom JSON encoding requires; shared shapes use one Go type. Selected discriminated unions also generate JSON serializers that keep required nullable fields for each variant. Go input projections exclude `packages.system` to preserve its explicit rejection. Other union serializers, Core's local limits, execution admission and state transitions remain implementation code. - `packages/agents-client/src/generated/public-api.ts` generates the TypeScript client's types, enum values and field names from the resulting public schema. The same generator projects the `/core/v1` document into `core-api.ts`, which imports the public types it references from `public-api.ts`. The internal `/core/v1` and `/api/v1` documents come from handler annotations: a response field Core always sends carries `binding:"required"`, a nullable field carries `extensions:"x-nullable"`, and a Core-owned closed set uses a named Go type with typed constants in its producing package, from which Swag derives one shared enum definition. Single-value discriminators keep `enums:` tags; fields mirroring official or Runtime-owned sets keep tags that contract tests check against their owners. The annotations only shape the documents, and the client rejects a response that breaks them. - Contract tests verify that the public schema preserves the official definitions, extensions stay 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 known gaps in [OpenAI compatibility](contracts/agents-api/index.md#known-gaps) still apply. Upstream upgrades update the OpenAPI and SDK pins together after comparison and compatibility tests. -- `make sqlc-generate` owns only `services/core/internal/db/sqlc` (sqlc v1.29.0). Before the first public release, schema changes edit the baseline migration in place, and an installation created from an earlier baseline is reinstalled. +- `make sqlc-generate` owns only `services/core/internal/db/sqlc` (sqlc v1.29.0). - `make check-runtime-contract` is the focused Core–Runtime contract entry point; see [Contract verification](docs/runtime-protocol.md#contract-verification). It also runs through `check-go` and `check-core`. ### Compatibility evidence diff --git a/deploy/install-source.py b/deploy/install-source.py index c3dd0dcc1..dd05469f7 100644 --- a/deploy/install-source.py +++ b/deploy/install-source.py @@ -121,7 +121,7 @@ def install(bundle, destination, host, port, public_url): if existing and not previous: raise ValueError(f'Incomplete installation: preserve {destination} and choose another directory') if previous and previous != revision: - raise ValueError(f'This installation runs release {previous[:12]}; this bundle is {revision[:12]}. Reinstall to use this release.') + raise ValueError(f'This installation runs release {previous[:12]}; this bundle is {revision[:12]}. To use this release, uninstall the installation, which deletes its data, then install again.') if not existing and public_url is None: raise ValueError('--public-url is required for a new installation') check_docker() @@ -153,7 +153,7 @@ def install(bundle, destination, host, port, public_url): for name in ('core', 'web', 'database'): template = template.replace('__OAC_' + name.upper() + '_IMAGE__', images[name]) (stage / 'compose.yaml').write_text(template) - # The installer owns only the service images and the payload binds; other entries are the operator's. + # The installer owns only the payload binds; other entries are the operator's. saved = destination / 'compose.override.yaml' try: override = json.loads(saved.read_text()) if saved.is_file() else {} diff --git a/deploy/test_install_source.py b/deploy/test_install_source.py index f88fe34f0..bef257254 100644 --- a/deploy/test_install_source.py +++ b/deploy/test_install_source.py @@ -158,7 +158,7 @@ def test_another_release_is_refused_and_a_rerun_keeps_operator_entries(self): self.manifest['source_commit'] = 'b' * 40 (self.bundle / 'manifest.json').write_text(json.dumps(self.manifest)) self.write_sums() - with self.assertRaisesRegex(ValueError, 'Reinstall to use this release'): + with self.assertRaisesRegex(ValueError, 'To use this release, uninstall the installation'): self.install(image='sha256:' + '2' * 64) self.assertEqual(self.snapshot(), before) diff --git a/docs/configuration.md b/docs/configuration.md index 897068315..2e2f1a133 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -127,7 +127,7 @@ The installer creates `~/.oac/core` by default (`$HOME/.oac/core` on Windows). I | --- | --- | --- | | `.env` | Process settings and the stable Compose project name | You, then `oac apply` | | `compose.yaml` | Verified release service definition | The installer, when it installs | -| `compose.override.yaml` | Optional local Compose entries. For a source bundle it is JSON and also holds the four service images and Web's `/node-payload` bind | You. `install.py` replaces only its own entries and requires JSON | +| `compose.override.yaml` | Optional local Compose entries. For a source bundle it is JSON and also holds Web's `/node-payload` bind and Core's native installer bind | You. `install.py` replaces only its own entries and requires JSON | | `oac` (`oac.exe` on Windows) | Native management command | The installer | The sibling `.lock` directory remains for synchronization; `.staging` holds unpublished installation files. Neither contains service data. On Unix the installer creates private directories with mode `0700` and configuration files with mode `0600`. diff --git a/docs/getting-started/install-options.md b/docs/getting-started/install-options.md index 28ccc0201..574ab8799 100644 --- a/docs/getting-started/install-options.md +++ b/docs/getting-started/install-options.md @@ -76,7 +76,7 @@ On either platform, open its server terminal and run `docker compose ls` to find After signing in, [choose a sandbox backend](./sandbox-backends.md) and add nodes using [Add and manage nodes](./nodes.md). The Compose stack deploys the control plane; execution machines remain separate. -Stop with `docker compose stop` using the same files and environment. Back up the data volume and configuration together while the services are stopped. To move to another release, [reinstall](./install.md#install). +Stop with `docker compose stop` using the same files and environment. Back up the data volume and configuration together while the services are stopped. To move to another release, remove the stack with its data volume (`docker compose down --volumes`), then deploy the new release. ## Offline hosts diff --git a/docs/getting-started/troubleshooting.md b/docs/getting-started/troubleshooting.md index 9432f9067..ebe07bb43 100644 --- a/docs/getting-started/troubleshooting.md +++ b/docs/getting-started/troubleshooting.md @@ -13,8 +13,8 @@ For a failing request or Turn, start with Web's **Monitor → Request log** and | `Web port unavailable; choose --web-port` | Another program holds that port. Stop it, or choose another `--web-port`. The installer does not move to a different port. When the message names a Compose project `oac-…`, an earlier installation whose directory is gone still runs: remove it as in [Uninstall](./operations.md#uninstall) | | `incomplete installation: preserve … and choose another directory` | Preserve the directory and choose another `--install-dir` | | `configuration check failed; no service was changed` | `.env` has a value Core rejects. The message names the variable and not the value. Fix `.env` and run `oac apply` again | -| `data volume check failed; no service was changed` | Initialization refused the data volume; the message names the cause, such as a volume that belongs to another release (`existing data belongs to release …`). Reinstall to use this release | -| `This installation runs release …` | The installer or initialization belongs to another release. Reinstall to use this release | +| `data volume check failed; no service was changed` | Initialization refused the data volume; the message names the cause, such as a volume that belongs to another release (`existing data belongs to release …`). To use this release, [uninstall](./operations.md#uninstall) the installation, which deletes its data, then install again | +| `This installation runs release …` | The installer or initialization belongs to another release. To use this release, [uninstall](./operations.md#uninstall) the installation, which deletes its data, then install again | | `This account cannot use Docker` | Add the account to the `docker` group and log in again, or use Docker Desktop or rootless Docker. Do not run the installer with `sudo` | | `This account cannot access …` | An earlier run with `sudo` left files owned by root. Run the `sudo chown` command the message prints, then rerun as your own account | | `Docker Compose 2.26 or newer is required` | Install or update the Docker Compose plugin | diff --git a/docs/maintainers.md b/docs/maintainers.md index 9f1a3fe33..956d888e8 100644 --- a/docs/maintainers.md +++ b/docs/maintainers.md @@ -175,7 +175,7 @@ The planner compares the PR event's tested merge commit with its verified first | Group | Checks and consumers | | --- | --- | -| `hygiene` | Names, repository links, bundled documentation integrity, and CI planner/gate tests; runs for every change | +| `hygiene` | Repository links, bundled documentation integrity, and CI planner/gate tests; runs for every change | | `distribution` | Harness catalog and installer schema, install/apply/recovery/cleanup tests, Compose parsing and initialization fixtures, release/download and bundle contracts, Go console tests and build; needs Docker Compose for template parsing, no pnpm install or browser | | `compose` | Real startup from empty volumes using images built from the checkout, sign-in and API access, file upload and node installer download, then URL reconfiguration and container recreation with preserved credentials and data; needs Docker, Go and network access, no model credentials | | `backend` | Parallel parts, each with a dedicated PostgreSQL guard: `runtime` (sqlc freshness, Runtime/shared Go tests, Linux microsandbox helper, daemon build), `core` (standalone Core build, Core service and client tests) and three `integration` shards of the serial Core integration test package | diff --git a/docs/zh/configuration.md b/docs/zh/configuration.md index 8a6409851..08418b96e 100644 --- a/docs/zh/configuration.md +++ b/docs/zh/configuration.md @@ -1,7 +1,7 @@ --- title: "配置参考" source: docs/configuration.md -source_hash: c1675b5ebd7f5cdda3d7acc5cea2663714a634220e9a59d0fc5cc6b6b0891e58 +source_hash: 6830daa88320510b51e74909169d4e49c7fa9017b344ba12a1456097cad29991 --- Core 安装的每项设置、其归属位置以及修改方式。安装程序写入初始值;之后请使用本参考。每项设置都恰好只有一个归属位置,分属以下三类: @@ -131,7 +131,7 @@ Session 创建可以在异步工作区准入之前成功。容量耗尽时,Cor | --- | --- | --- | | `.env` | 进程设置和固定的 Compose 项目名 | 用户修改后运行 `oac apply` | | `compose.yaml` | 已校验的发行版服务定义 | 安装程序,在安装时 | -| `compose.override.yaml` | 可选的本地 Compose 条目。源码包安装中它是 JSON,还保存四个服务镜像和 Web 的 `/node-payload` 绑定 | 用户。`install.py` 只替换自己的条目,并要求 JSON 格式 | +| `compose.override.yaml` | 可选的本地 Compose 条目。源码包安装中它是 JSON,还保存 Web 的 `/node-payload` 绑定和 Core 的原生安装程序绑定 | 用户。`install.py` 只替换自己的条目,并要求 JSON 格式 | | `oac`(Windows 为 `oac.exe`) | 原生管理命令 | 安装程序 | 同级 `.lock` 目录用于同步操作并一直保留;`.staging` 保存尚未就位的安装文件。两者都不保存服务数据。Unix 上安装程序以 `0700` 创建私有目录,以 `0600` 创建配置文件。 diff --git a/docs/zh/getting-started/install-options.md b/docs/zh/getting-started/install-options.md index e489dd72f..3d62e9724 100644 --- a/docs/zh/getting-started/install-options.md +++ b/docs/zh/getting-started/install-options.md @@ -1,7 +1,7 @@ --- title: "安装选项" source: docs/getting-started/install-options.md -source_hash: 8cfae2d39cc95a588bc231fe9f3661f3c22db7884b2278c6a1483c114a5185fd +source_hash: 432e372bf9bb4729edf27df889d4e45526c87fc39b9baa43c9b0e16d3fa54036 --- 本页是安装程序参数的参考,并说明如何用默认方式以外的方式安装:在已有反向代理之后、在托管平台上使用 Docker Compose,或在无法访问互联网的主机上。[默认安装](./install.md)无需任何选项。 @@ -78,7 +78,7 @@ docker compose exec web oac-web core-key 登录后,[选择沙箱提供商](./sandbox-backends.md),并按[添加和管理节点](./nodes.md)添加节点。Compose 堆栈部署控制平面;执行机器仍需单独部署。 -使用相同的文件和环境运行 `docker compose stop` 以停止服务。停止服务后,一并备份数据卷和配置。要换到另一个版本,请[重新安装](./install.md#install)。 +使用相同的文件和环境运行 `docker compose stop` 以停止服务。停止服务后,一并备份数据卷和配置。要换到另一个版本,请连同数据卷一起移除该栈(`docker compose down --volumes`),然后部署新版本。 ## 离线主机 {#offline-hosts} diff --git a/docs/zh/getting-started/troubleshooting.md b/docs/zh/getting-started/troubleshooting.md index 39fd40754..5cff44127 100644 --- a/docs/zh/getting-started/troubleshooting.md +++ b/docs/zh/getting-started/troubleshooting.md @@ -1,7 +1,7 @@ --- title: "故障排查" source: docs/getting-started/troubleshooting.md -source_hash: 8cbeb8f1ecce90c1b386e05fdca5239f39a153c51c1720567bc19980bfeb5c0d +source_hash: 468c8cbd13ee2a736db07f9239f56a49afad2916036a375260b59e377fc3a1e3 --- 按症状查找安装、访问、节点和 Session 问题的原因与解决方法。本页面向 Core 安装的运维人员。 @@ -15,8 +15,8 @@ source_hash: 8cbeb8f1ecce90c1b386e05fdca5239f39a153c51c1720567bc19980bfeb5c0d | `Web port unavailable; choose --web-port` | 其他程序占用该端口。停止它,或另选 `--web-port`。安装程序不会改用其他端口。如果消息点名了 Compose 项目 `oac-…`,说明目录已不存在的早期安装仍在运行:按[卸载](./operations.md#uninstall)移除它 | | `incomplete installation: preserve … and choose another directory` | 保留目录内容,选择另一个 `--install-dir` | | `configuration check failed; no service was changed` | `.env` 中有 Core 拒绝的值。消息只包含变量名。修正 `.env` 后再次运行 `oac apply` | -| `data volume check failed; no service was changed` | 初始化拒绝了数据卷;消息会说明原因,例如数据卷属于另一个版本(`existing data belongs to release …`)。请重新安装以使用此版本 | -| `This installation runs release …` | 安装程序或初始化属于另一个版本。请重新安装以使用此版本 | +| `data volume check failed; no service was changed` | 初始化拒绝了数据卷;消息会说明原因,例如数据卷属于另一个版本(`existing data belongs to release …`)。要使用此版本,请[卸载](./operations.md#uninstall)该安装(这会删除其数据),然后重新安装 | +| `This installation runs release …` | 安装程序或初始化属于另一个版本。要使用此版本,请[卸载](./operations.md#uninstall)该安装(这会删除其数据),然后重新安装 | | `This account cannot use Docker` | 将该账号加入 `docker` 组后重新登录,或使用 Docker Desktop 或 rootless Docker。不要用 `sudo` 运行安装程序 | | `This account cannot access …` | 之前用 `sudo` 运行留下了属于 root 的文件。运行消息给出的 `sudo chown` 命令,再以自己的账号重新运行 | | `Docker Compose 2.26 or newer is required` | 安装或更新 Docker Compose 插件 | diff --git a/docs/zh/maintainers.md b/docs/zh/maintainers.md index ca5f226e2..e3e4ab639 100644 --- a/docs/zh/maintainers.md +++ b/docs/zh/maintainers.md @@ -1,7 +1,7 @@ --- title: "构建并发布 OpenAgentCore" source: docs/maintainers.md -source_hash: bf140052262bf0255190a87767308af560c10fe4fd9eed93bd86836aefeb524a +source_hash: 16d591f49202d2f08b26e296ceccbaed72dfe6f706658cef6a11238de1f78d7c --- 本指南面向负责构建和发布 OpenAgentCore 的维护者。要安装 Core 和 Web,请按 [安装 Core 和 Web](getting-started/install.md) 操作。安装程序代码遵循的规则见 [部署](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/README.md) 和 [节点安装程序](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/node/README.md);必需检查见 [CONTRIBUTING](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/CONTRIBUTING.md#required-checks)。 @@ -177,7 +177,7 @@ gh workflow run core-release --repo MiniMax-AI/OpenAgentCore --ref main \ | 组 | 检查和使用方 | | --- | --- | -| `hygiene` | 名称、仓库链接、随包文档完整性以及 CI 计划器/门禁测试;每次变更都会运行 | +| `hygiene` | 仓库链接、随包文档完整性以及 CI 计划器/门禁测试;每次变更都会运行 | | `distribution` | Harness 目录和安装程序模式、安装/应用/恢复/清理测试、Compose 解析和初始化固定数据、发布/下载和捆绑包契约、Go 控制台测试与构建;模板解析需要 Docker Compose,不需要 pnpm install 或浏览器 | | `compose` | 使用从当前检出构建的镜像从空数据卷实际启动、登录和 API 访问、上传文件和下载节点安装程序,然后在保留凭据和数据的同时重新配置 URL 并重新创建容器;需要 Docker、Go 和网络访问,不需要模型凭据 | | `backend` | 并行部分,每部分都有专用 PostgreSQL 保护检查:`runtime`(sqlc 新鲜度、Runtime/共享 Go 测试、Linux microsandbox 辅助程序、守护进程构建)、`core`(独立 Core 构建、Core 服务和客户端测试),以及串行 Core 集成测试包的三个 `integration` 分片 | diff --git a/services/core/IMPLEMENTATION.md b/services/core/IMPLEMENTATION.md index b78fbc348..fe3a73ffb 100644 --- a/services/core/IMPLEMENTATION.md +++ b/services/core/IMPLEMENTATION.md @@ -58,7 +58,7 @@ On the Beta group, the OpenAI-Beta check (exactly one `agents=v1` value) runs be ## Source Files and Artifacts -Source Files are Project resources with a lifecycle independent of copied workspace files. `files` owns their vocabulary, the upload envelope and 512 MiB content bound, the list rules and the create and delete use cases; `filepg` stores immutable source metadata and PostgreSQL large objects in Core's database with the pinned pgx driver. Upload validation, metadata insertion, the bytes and the write audit commit atomically; deletion removes the metadata, unlinks the object and records the audit in one transaction. Keep OIDs private and authorize every metadata, content and delete lookup by tenant before opening a body. Write content through `pgunit`'s large-object writer, which streams bounded chunks and reports the size and SHA-256 digest; never hold an entire upload in memory or use a filename as a filesystem path. A direct download of the `user_data` purpose is rejected after the tenant-scoped metadata lookup, while workspace copies keep their authorized `filepg` read and Session creation copies an initial file's object inside its own transaction through `filepg.ReadSourceForCopy`, which locks the source and bounds the read. A read-only repeatable-read transaction preserves an admitted source across concurrent deletion; resolve that snapshot before entering the Environment write path, and a later deletion never undoes a completed workspace copy. Bound request and transaction lifetimes, roll back incomplete bodies and never retry an ambiguous commit automatically. Backups must include PostgreSQL large objects, and a schema rollback must not orphan them. +Source Files are Project resources with a lifecycle independent of copied workspace files. `files` owns their vocabulary, the upload envelope and 512 MiB content bound, the list rules and the create and delete use cases; `filepg` stores immutable source metadata and PostgreSQL large objects in Core's database with the pinned pgx driver. Upload validation, metadata insertion, the bytes and the write audit commit atomically; deletion removes the metadata, unlinks the object and records the audit in one transaction. Keep OIDs private and authorize every metadata, content and delete lookup by tenant before opening a body. Write content through `pgunit`'s large-object writer, which streams bounded chunks and reports the size and SHA-256 digest; never hold an entire upload in memory or use a filename as a filesystem path. A direct download of the `user_data` purpose is rejected after the tenant-scoped metadata lookup, while workspace copies keep their authorized `filepg` read and Session creation copies an initial file's object inside its own transaction through `filepg.ReadSourceForCopy`, which locks the source and bounds the read. A read-only repeatable-read transaction preserves an admitted source across concurrent deletion; resolve that snapshot before entering the Environment write path, and a later deletion never undoes a completed workspace copy. Bound request and transaction lifetimes, roll back incomplete bodies and never retry an ambiguous commit automatically. Backups must include PostgreSQL large objects. Session Artifacts are immutable published copies, separate from live workspace files and source Files. The private output exporter reuses the authorized workspace path boundary and streams bounded bytes; publication requires complete capture and confirmed helper and transport success, not merely valid archive syntax. The daemon owns and drains the exporter's stdout pipe separately from child reaping, so pull-transport backpressure cannot consume the process-exit I/O deadline; after helper exit, each pipe read has one second, reset after consumer delays, which rejects inherited pipes that never close. Cancellation closes the owned reader and the dispatch consumer, then waits for the child. Never extract an output archive into Core's filesystem or hold the execution lease through a large transfer. diff --git a/services/core/README.md b/services/core/README.md index 250095356..37977eeea 100644 --- a/services/core/README.md +++ b/services/core/README.md @@ -16,7 +16,7 @@ ## Database -Core uses its own PostgreSQL database and account and shares no tables with an application. Its schema is one embedded goose baseline migration in [`migrations/`](migrations), tracked in `oac_schema_version`. Before the first public release, schema changes edit the baseline in place, and an installation created from an earlier baseline is reinstalled. Core applies the baseline when it starts. Queries live in `internal/db/queries` and generate typed code with sqlc: run `make sqlc-generate` after changing a query and commit the generated files, which `make check-sqlc` compares. +Core uses its own PostgreSQL database and account and shares no tables with an application. Its schema is the embedded goose migrations in [`migrations/`](migrations), tracked in `oac_schema_version`; [AGENTS.md](../../AGENTS.md#pre-release-no-compatibility-layers) states how schema changes land. Core applies the migrations when it starts. Queries live in `internal/db/queries` and generate typed code with sqlc: run `make sqlc-generate` after changing a query and commit the generated files, which `make check-sqlc` compares. ## Run from source diff --git a/services/core/cmd/oac/init.go b/services/core/cmd/oac/init.go index 17d2201d2..3ed11fc77 100644 --- a/services/core/cmd/oac/init.go +++ b/services/core/cmd/oac/init.go @@ -219,7 +219,7 @@ func initialize(root string, release releaseIdentity, fetch func() (map[string][ logger.InfoContext(ctx, "Existing installation verified", "file_count", len(previous.Files)) return nil } - return fmt.Errorf("existing data belongs to release %.12s; reinstall to use release %.12s", previous.SourceCommit, release.revision) + return fmt.Errorf("existing data belongs to release %.12s; to use release %.12s, uninstall the installation, which deletes its data, then install again", previous.SourceCommit, release.revision) } nextStep("verify_bundled_metadata") files, err := fetch() diff --git a/services/core/cmd/oac/init_test.go b/services/core/cmd/oac/init_test.go index 2f1318ac3..b914c55e0 100644 --- a/services/core/cmd/oac/init_test.go +++ b/services/core/cmd/oac/init_test.go @@ -136,19 +136,6 @@ func TestInitializeRefusesChangedOrMissingFiles(t *testing.T) { } } -func readReceipt(t *testing.T, root string) installReceipt { - t.Helper() - var receipt installReceipt - raw, err := os.ReadFile(filepath.Join(root, "installation.json")) - if err != nil { - t.Fatal(err) - } - if err := json.Unmarshal(raw, &receipt); err != nil { - t.Fatal(err) - } - return receipt -} - func TestInitializeHealsTheReleasePointerAfterACrash(t *testing.T) { root, release, files := initFixture(t) if err := initialize(root, release, fixed(files)); err != nil { @@ -174,7 +161,7 @@ func TestInitializeRefusesDataOfAnotherRelease(t *testing.T) { } before := snapshot(t, root) other, otherFiles := releaseFixture(strings.Repeat("e", 40)) - want := "existing data belongs to release dddddddddddd; reinstall to use release eeeeeeeeeeee" + want := "existing data belongs to release dddddddddddd; to use release eeeeeeeeeeee, uninstall the installation, which deletes its data, then install again" if err := initialize(root, other, fixed(otherFiles)); err == nil || err.Error() != want { t.Fatalf("err = %v", err) } diff --git a/services/core/cmd/oac/install.go b/services/core/cmd/oac/install.go index 4e26d813d..2f948fa52 100644 --- a/services/core/cmd/oac/install.go +++ b/services/core/cmd/oac/install.go @@ -266,7 +266,7 @@ func (i installer) installLocked(ctx context.Context, o installOptions, detected return fmt.Errorf("incomplete installation: preserve %s and choose another directory", o.dir) } if installed != buildRevision { - return fmt.Errorf("This installation runs release %s; this installer is %s. Reinstall to use this release.", short(installed), short(buildRevision)) + return fmt.Errorf("This installation runs release %s; this installer is %s. To use this release, uninstall the installation, which deletes its data, then install again.", short(installed), short(buildRevision)) } } compose := installCompose{docker: i.docker, dir: o.dir} diff --git a/services/core/cmd/oac/install_test.go b/services/core/cmd/oac/install_test.go index b76106c30..1960aa2d9 100644 --- a/services/core/cmd/oac/install_test.go +++ b/services/core/cmd/oac/install_test.go @@ -68,31 +68,13 @@ func newInstallFixture(t *testing.T) *installFixture { } return compose, nil } - // Calls against the staged release are recorded with a "staged " prefix and without their file arguments. f.docker = func(_ context.Context, dir string, args ...string) ([]byte, error) { if _, err := os.Stat(dir); dir != "" && err != nil { t.Fatalf("docker %v ran in a missing directory", args) } - var words []string - staged := false - for i := 0; i < len(args); i++ { - if args[i] == "-f" || args[i] == "--project-directory" { - staged = staged || strings.HasPrefix(args[i+1], f.options.dir+".staging") - i++ - continue - } - words = append(words, args[i]) - } - command := strings.Join(words, " ") - if staged { - if f.installedRevision() == buildRevision { - t.Fatal("release published before its checks") - } - f.calls = append(f.calls, "staged "+command) - } else { - f.calls = append(f.calls, command) - } - if f.failure != "" && strings.HasPrefix(f.calls[len(f.calls)-1], f.failure) { + command := strings.Join(args, " ") + f.calls = append(f.calls, command) + if f.failure != "" && strings.HasPrefix(command, f.failure) { if f.failureErr != nil { return nil, f.failureErr } @@ -254,7 +236,7 @@ func TestInstallRefusesAnotherRelease(t *testing.T) { setBuildRevision(t, "development") f.calls = nil err := f.run() - if err == nil || err.Error() != "This installation runs release a1b2c3d4e5f6; this installer is development. Reinstall to use this release." { + if err == nil || err.Error() != "This installation runs release a1b2c3d4e5f6; this installer is development. To use this release, uninstall the installation, which deletes its data, then install again." { t.Fatal(err) } if len(f.calls) != 3 { @@ -443,7 +425,7 @@ func TestInstallReportsStartFailuresByCause(t *testing.T) { call, stderr, want, unwanted string }{ // A refusal is not fixed by rerunning, and only its own line is shown. - {"compose run --rm --no-deps init", "{\"msg\":\"Initialization started\"}\nthis data belongs to a newer release", "data volume check failed; no service was changed: this data belongs to a newer release", "startup failed"}, + {"compose run --rm --no-deps init", "{\"msg\":\"Initialization started\"}\nexisting data belongs to release 0123456789ab", "data volume check failed; no service was changed: existing data belongs to release 0123456789ab", "startup failed"}, {"compose run --rm --no-deps --entrypoint", "OAC_EXECUTION_CONCURRENCY must be positive", "configuration check failed; no service was changed: OAC_EXECUTION_CONCURRENCY must be positive", "startup failed"}, {"compose up", progress, "startup failed; data retained, rerun the installer: dependency failed to start: container x-core-1 is unhealthy\nNot healthy: core (unhealthy), web (starting)\nSee why: docker compose -f '", "Healthy"}, } { diff --git a/services/core/tools/e2b-provider/README.md b/services/core/tools/e2b-provider/README.md index 42cf052b4..b872e1f84 100644 --- a/services/core/tools/e2b-provider/README.md +++ b/services/core/tools/e2b-provider/README.md @@ -34,7 +34,7 @@ The boundary has version 1. The request and credentials arrive on standard input ## Receipts and state directory -`OAC_E2B_STATE_DIR` ([configuration](../../../../docs/configuration.md#appendix-core-environment-without-the-installer)) must already exist, be owned by the helper's user and grant no group or other access. Keep it on durable private storage across Core upgrades and restarts. Its receipts hold SDK connection material, ownership identities and one-shot creation claims; they are not Core execution state. Losing the directory cannot authorize recreation or successful cleanup; never delete receipts after an uncertain call. +`OAC_E2B_STATE_DIR` ([configuration](../../../../docs/configuration.md#appendix-core-environment-without-the-installer)) must already exist, be owned by the helper's user and grant no group or other access. Keep it on durable private storage across Core restarts. Its receipts hold SDK connection material, ownership identities and one-shot creation claims; they are not Core execution state. Losing the directory cannot authorize recreation or successful cleanup; never delete receipts after an uncertain call. A helper holds its allocation's lock until the SDK operation returns, even after Core's caller times out. Core tracks the helper's actual exit, and a credential change waits for running helpers without killing them. Helper exit never proves that a remote Create settled. Core serializes lifecycle requests per allocation and never replays Create. diff --git a/services/core/tools/e2b-provider/state.py b/services/core/tools/e2b-provider/state.py index 86eeba3c3..d44fffb39 100644 --- a/services/core/tools/e2b-provider/state.py +++ b/services/core/tools/e2b-provider/state.py @@ -55,8 +55,7 @@ def load_receipt(path, identity, config): data = json.load(source) if data.get('identity') != identity or data.get('version') != 1: raise Failure('ownership') - # Receipts from releases before custom endpoints belong to official E2B. - if data.get('endpoint', endpoint_identity({})) != endpoint_identity(config): + if data.get('endpoint') != endpoint_identity(config): raise Failure('ownership') return data diff --git a/services/core/tools/e2b-provider/state_test.py b/services/core/tools/e2b-provider/state_test.py index ae573370a..1c8bbe3fd 100644 --- a/services/core/tools/e2b-provider/state_test.py +++ b/services/core/tools/e2b-provider/state_test.py @@ -55,7 +55,7 @@ def test_receipt_symlink_is_never_followed(self): pass self.assertEqual(witness.read_text(), 'unchanged') - def test_receipt_is_bound_to_endpoint_and_legacy_is_official(self): + def test_receipt_is_bound_to_endpoint(self): with Receipt(self.request, lambda: 1) as receipt: receipt.save(status='create_pending') path = receipt.path @@ -65,14 +65,6 @@ def test_receipt_is_bound_to_endpoint_and_legacy_is_official(self): with self.assertRaises(Failure): with Receipt(custom, lambda: 1): pass - data = json.loads(path.read_text()) - del data['endpoint'] - path.write_text(json.dumps(data)) - with Receipt(self.request, lambda: 1) as receipt: - self.assertEqual(receipt.data['status'], 'create_pending') - with self.assertRaises(Failure): - with Receipt(custom, lambda: 1): - pass def test_foreign_identity_and_world_readable_receipt_rejected(self): with Receipt(self.request, lambda: 1) as receipt: