matrx-orm 3.1.39__tar.gz → 3.1.41__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/CLAUDE.md +3 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/PKG-INFO +1 -1
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/base.py +6 -6
- matrx_orm-3.1.41/matrx_orm/query/FEATURE.md +17 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/query/builder.py +9 -4
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/secrets_battery/FEATURE.md +24 -1
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/secrets_battery/__init__.py +2 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/secrets_battery/items.py +56 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/secrets_battery/org_service.py +38 -3
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/pyproject.toml +1 -1
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_query_builder.py +18 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/test_secrets_battery.py +193 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/.gitignore +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/AGENTS.md +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/BASE_CLASS_METHODS.md +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/MODEL_API.md +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/README.md +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/database/orm/extended/managers/ai_model_base.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/docs/migrations.md +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/MODULE_README.md +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/__init__.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/adapters/MODULE_README.md +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/adapters/__init__.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/adapters/async_postgresql.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/adapters/base_adapter.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/adapters/postgresql.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/adapters/supabase_adapter.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/admin/__init__.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/admin/router.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/api/__init__.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/api/auth.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/api/config.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/api/handlers.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/api/protocol.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/api/server.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/associations.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/cache_debug.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/catalog.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/client/MODULE_README.md +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/client/__init__.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/client/postgres_connection.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/client/postgrest.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/client/supabase_auth.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/client/supabase_config.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/client/supabase_manager.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/EXTENDED-TASK.md +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/MODULE_README.md +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/TASKS.md +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/__init__.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/async_db_manager.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/config.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/connections.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/db_function.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/diagnostics.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/expressions.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/extended.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/fields.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/introspection.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/listen.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/loop_filters.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/model_dto.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/model_view.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/paginator.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/pool_maintenance.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/pydantic_bridge.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/registry.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/relations.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/resilience.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/rls_session.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/signals.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/tls.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/transaction.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/types.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/write_queue.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/entity.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/error_handling.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/exceptions.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/extended/__init__.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/extended/app_error_handler.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/identity_rule.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/middleware/__init__.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/middleware/base.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/migrations/MODULE_README.md +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/migrations/__init__.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/migrations/cli.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/migrations/ddl.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/migrations/diff.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/migrations/executor.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/migrations/integration.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/migrations/loader.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/migrations/operations.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/migrations/state.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/migrations/table_filter.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/MODULE_README.md +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/__init__.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/bulk_update_values.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/conflict.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/conflict_writes.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/create.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/db_functions.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/delete.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/dynamic_admin.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/dynamic_crud.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/graph_walk.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/latest_rows.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/queue_claim.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/read.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/relational_backfill.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/staging_load.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/update.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/py.typed +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/python_sql/MODULE_README.md +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/python_sql/__init__.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/python_sql/db_objects.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/query/MODULE_README.md +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/query/__init__.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/query/executor.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/MODULE_README.md +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/__init__.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/code_handler.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/codegen_writer.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/columns.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/common.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/diff_preview.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/generator.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/package_wiring.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/relationships.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/runner.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/schema.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/schema_manager.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/tables.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/views.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/secrets_battery/crypto.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/secrets_battery/model.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/secrets_battery/service.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/session/README.md +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/session/__init__.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/session/coalesce.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/session/dag.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/session/errors.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/session/fallback.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/session/flush.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/session/lifecycle.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/session/managed.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/session/op.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/session/reads.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/session/session.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/session/telemetry.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/sql_executor/MODULE_README.md +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/sql_executor/__init__.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/sql_executor/executor.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/sql_executor/queries.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/sql_executor/registry.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/sql_executor/types.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/sql_executor/utils.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/state.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/utils/__init__.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/utils/sql_utils.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/utils/type_converters.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/scripts/git-branches.sh +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/scripts/publish.sh +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/scripts/release.sh +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/MODULE_README.md +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/__init__.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/conftest.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/MODULE_README.md +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/__init__.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_admin_db_columns.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_admin_search_negation.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_admin_search_sentinels.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_advisory_lock.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_agent_message.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_api_auth.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_api_config.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_api_handlers.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_api_protocol.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_array_agg_order_by.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_associations.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_bulk_update_by_pk.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_cache_debug.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_call_function.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_case_when.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_catalog.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_composite_pk_filter.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_composite_pk_write_paths.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_config.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_conflict_target.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_conflict_writes.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_connection_codecs.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_connection_poison_guard.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_connection_primitives.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_count_composite_distinct.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_db_function_field.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_ddl_generator.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_derived_table.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_dirty_tracking.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_dynamic_crud.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_exceptions.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_expression_primitives.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_fields.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_from_alias.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_fts.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_func_expression_args.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_graph_walk.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_having_annotate_alias.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_introspect_rls_policies.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_join_and_aggregate.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_latest_rows.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_listen_notify.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_managed_write_guard.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_matrx_entity.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_migration_diff_types.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_migration_loader.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_model_instance.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_model_meta.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_order_by_expression.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_patch_jsonb_path.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_pool_loop_recreation.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_pool_maintenance.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_postgrest_filters.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_pydantic_bridge.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_query_error_detail.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_query_executor_sql.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_query_timeout.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_queue_claim.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_registry.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_registry_multi_database.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_registry_multi_schema.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_relational_backfill.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_relations.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_resilience.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_schema_exists.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_session_advisory_lock.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_state_cache.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_subquery_filter_raw.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_subquery_in_filter.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_supabase_auth.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_supabase_config.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_tls.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_typed_join_projection.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_typed_predicates.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_update_case_expression.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_update_subquery.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_upsert_default_leak.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_upsert_with_conflict.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_values_jsonb_decode.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_watched_lifecycle.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_write_retry_semantics.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level2/MODULE_README.md +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level2/__init__.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level2/conftest.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level2/test_bulk_ops.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level2/test_cache_integration.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level2/test_crud.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level2/test_foreign_keys.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level2/test_m2m.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level2/test_manager.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level2/test_migrations_live.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level2/test_query_execution.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level2/test_schema_diff.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/sample_project/.env.example +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/sample_project/README.md +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/sample_project/__init__.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/sample_project/generate.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/sample_project/generated/.gitkeep +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/sample_project/matrx_orm.yaml +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/sample_project/test_schema_generation.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/sample_project_desktop/.env.example +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/sample_project_desktop/README.md +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/sample_project_desktop/__init__.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/sample_project_desktop/client_example.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/sample_project_desktop/client_example.ts +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/sample_project_desktop/client_supabase_example.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/sample_project_desktop/server.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/schema/entity_tests.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/schema/test_base_generation.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/schema/test_composite_pk_fk_generation.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/schema/test_composite_unique_generation.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/schema/test_dto_identity_generation.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/schema/test_entity_capability_generation.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/schema/test_generate_schema.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/schema/test_junction_analysis.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/schema/test_multi_schema_output.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/schema/test_sql_expression_defaults.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/schema/test_view_generation.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/schema_builder/test_focused_generation_plan.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/session/__init__.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/session/conftest.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/session/test_cache_no_downgrade.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/session/test_capture_poison_proof.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/session/test_coalesce.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/session/test_dag.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/session/test_disk_spill_fallback.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/session/test_flush_individual_fallback.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/session/test_governed_write.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/session/test_managed.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/session/test_op_enqueue_site.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/session/test_reads.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/session/test_session.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/test_database_name_alias.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/test_db_functions.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/test_model_cls_refactor.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/test_no_blocking_asyncpg_tls.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/test_sql_param_casts.py +0 -0
- {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/test_sync_twins_guard.py +0 -0
|
@@ -32,6 +32,9 @@ Every table that becomes a Model needs row identity: a single-column primary key
|
|
|
32
32
|
subqueries. SQL-shaped strings fail closed: plain strings name validated
|
|
33
33
|
columns only; compose casts, JSON paths, functions, ordering, joins, partial
|
|
34
34
|
conflict targets, and filtered aggregates with the exported typed objects.
|
|
35
|
+
Large reconciliation/export scans call `.all(use_cache=False)` so returned
|
|
36
|
+
models do not quadratically populate the process identity cache. Contract:
|
|
37
|
+
[`matrx_orm/query/FEATURE.md`](matrx_orm/query/FEATURE.md).
|
|
35
38
|
- **Migrations**: `MigrationDB`, `MigrationLoader`, `MigrationExecutor`, `makemigrations`, `migrate` — migrations declare explicit `dependencies` and apply in topological order (no multi-head/merge support yet).
|
|
36
39
|
- **Admin router** (FastAPI): `admin_router` exposes read/write endpoints for any registered model.
|
|
37
40
|
- **API layer** (optional `[api]` extra): `APIServer`, `APIConfig`, `TokenAuth`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: matrx-orm
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.41
|
|
4
4
|
Summary: Async-first PostgreSQL ORM with bidirectional migrations, schema introspection, many-to-many relationships, and built-in state caching
|
|
5
5
|
Project-URL: Homepage, https://github.com/AI-Matrix-Engine/aidream-current
|
|
6
6
|
Project-URL: Repository, https://github.com/AI-Matrix-Engine/aidream-current
|
|
@@ -1292,17 +1292,17 @@ class Model(RuntimeMixin, metaclass=ModelMeta):
|
|
|
1292
1292
|
return run_sync(cls.delete_where(**filters))
|
|
1293
1293
|
|
|
1294
1294
|
@classmethod
|
|
1295
|
-
async def all(cls) -> list[Self]:
|
|
1295
|
+
async def all(cls, *, use_cache: bool = True) -> list[Self]:
|
|
1296
|
+
"""Fetch every row, optionally bypassing identity-cache population."""
|
|
1297
|
+
|
|
1296
1298
|
try:
|
|
1297
|
-
|
|
1298
|
-
await StateManager.cache_bulk(model_class=cls, records=results)
|
|
1299
|
-
return results
|
|
1299
|
+
return await QueryBuilder(model_cls=cls).all(use_cache=use_cache)
|
|
1300
1300
|
except ORMException as e:
|
|
1301
1301
|
e.enrich(model=cls, operation="all")
|
|
1302
1302
|
raise
|
|
1303
1303
|
|
|
1304
1304
|
@classmethod
|
|
1305
|
-
def all_sync(cls) -> list[Self]:
|
|
1305
|
+
def all_sync(cls, *, use_cache: bool = True) -> list[Self]:
|
|
1306
1306
|
"""
|
|
1307
1307
|
Synchronous wrapper for all().
|
|
1308
1308
|
Use this in synchronous contexts to fetch all results without async/await.
|
|
@@ -1316,7 +1316,7 @@ class Model(RuntimeMixin, metaclass=ModelMeta):
|
|
|
1316
1316
|
if "no running event loop" not in str(e):
|
|
1317
1317
|
raise
|
|
1318
1318
|
|
|
1319
|
-
return run_sync(cls.all())
|
|
1319
|
+
return run_sync(cls.all(use_cache=use_cache))
|
|
1320
1320
|
|
|
1321
1321
|
async def save(self, **kwargs: Any) -> Self:
|
|
1322
1322
|
"""Save the current state of the model instance."""
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Query execution
|
|
2
|
+
|
|
3
|
+
`QueryBuilder` is the typed terminal-read and write layer behind every model.
|
|
4
|
+
Application packages compose queries here; they never bypass it with SQL.
|
|
5
|
+
|
|
6
|
+
## Large scans and the identity cache
|
|
7
|
+
|
|
8
|
+
`.all()` populates `StateManager` by default so ordinary repeated identity reads
|
|
9
|
+
remain fast. A large reconciliation, export, or historical scan must call
|
|
10
|
+
`.all(use_cache=False)`. This still returns fully hydrated models and still
|
|
11
|
+
merges pending session state, but it does not insert every result into the
|
|
12
|
+
process identity cache.
|
|
13
|
+
|
|
14
|
+
`Model.all(use_cache=False)` exposes the same behavior. The default remains
|
|
15
|
+
`True` for compatibility. The cache bypass controls only cache population; it
|
|
16
|
+
does not weaken database authorization, filters, hydration, or transaction
|
|
17
|
+
semantics.
|
|
@@ -778,7 +778,7 @@ class QueryBuilder(Generic[ModelT]):
|
|
|
778
778
|
if len(pks) == 1 and not self.excludes and set(combined) <= {pks[0], f"{pks[0]}__in"}:
|
|
779
779
|
values = combined.get(pks[0])
|
|
780
780
|
in_values = combined.get(f"{pks[0]}__in")
|
|
781
|
-
targets = list(in_values) if isinstance(in_values,
|
|
781
|
+
targets = list(in_values) if isinstance(in_values, list | tuple | set) else []
|
|
782
782
|
if values is not None:
|
|
783
783
|
targets.append(values)
|
|
784
784
|
if targets and StateManager.is_registered(self.model):
|
|
@@ -802,9 +802,12 @@ class QueryBuilder(Generic[ModelT]):
|
|
|
802
802
|
# Terminal async methods
|
|
803
803
|
# ------------------------------------------------------------------
|
|
804
804
|
|
|
805
|
-
async def all(self) -> list[ModelT]:
|
|
805
|
+
async def all(self, *, use_cache: bool = True) -> list[ModelT]:
|
|
806
806
|
"""Fetch all matching records.
|
|
807
807
|
|
|
808
|
+
Pass ``use_cache=False`` for large scans that must not populate the
|
|
809
|
+
process identity cache.
|
|
810
|
+
|
|
808
811
|
Session-aware: if an active Session has pending writes for this
|
|
809
812
|
Model, the result merges pending state with DB rows. Pending
|
|
810
813
|
INSERTs matching the filter appear in the result; pending UPDATEs
|
|
@@ -814,10 +817,12 @@ class QueryBuilder(Generic[ModelT]):
|
|
|
814
817
|
try:
|
|
815
818
|
executor = self._get_executor()
|
|
816
819
|
results = await executor.all()
|
|
817
|
-
|
|
820
|
+
if use_cache:
|
|
821
|
+
await StateManager.cache_bulk(self.model, results)
|
|
818
822
|
if debug:
|
|
819
823
|
vcprint(
|
|
820
|
-
f"[BUILDER] all()
|
|
824
|
+
f"[BUILDER] all() fetched {len(results)} records for "
|
|
825
|
+
f"{self.model.__name__} (cache={use_cache})",
|
|
821
826
|
color="bright_gold",
|
|
822
827
|
)
|
|
823
828
|
return _merge_pending_into_models(
|
|
@@ -37,6 +37,25 @@ row whose owner columns don't match its item's owner.
|
|
|
37
37
|
`editable=false` blocks human value changes (`FieldNotEditableError`); the owning
|
|
38
38
|
service rotates via `system_rotation=True` / `rotate_credential_fields`.
|
|
39
39
|
|
|
40
|
+
**`inject_into_sandbox=true` REQUIRES an env alias — refused otherwise.** A
|
|
41
|
+
container environment is a NAME→value map, so a field with `key IS NULL` has
|
|
42
|
+
nowhere to land and bulk resolution drops it. Accepting the flag anyway meant a
|
|
43
|
+
user could switch "inject into sandbox" on and get nothing, forever, with no
|
|
44
|
+
error — the silent-failure class this guard ends. Three layers, each sufficient
|
|
45
|
+
alone, each loud:
|
|
46
|
+
|
|
47
|
+
1. **Write refusal** — `create_credential_bundle` / `add_credential_field` /
|
|
48
|
+
`update_field_metadata` raise `SandboxInjectionWithoutEnvKeyError` (a
|
|
49
|
+
`ValueError`, so hosts answer 422 with the reason). `update_field_metadata`
|
|
50
|
+
judges the POST-patch state, so *clearing the alias of an already-injecting
|
|
51
|
+
field* is refused the same way. Setting `env_key` and the flag in ONE patch
|
|
52
|
+
is the supported fix and always passes.
|
|
53
|
+
2. **Resolution scream** — `_keyed_for_sandbox` logs an ERROR naming the row ids
|
|
54
|
+
and skips them, so anything that got in around layer 1 is visible.
|
|
55
|
+
3. **DB CHECK** `user_secrets_sandbox_injection_needs_key` (migration 0265) makes
|
|
56
|
+
the state unrepresentable; the column default moved `true` → `false` so the
|
|
57
|
+
database stops manufacturing violations on keyless inserts.
|
|
58
|
+
|
|
40
59
|
Item surface: `create_credential_item` · `create_credential_bundle` (item + N
|
|
41
60
|
fields, ONE transaction) · `get/list/update/delete_credential_item` (delete =
|
|
42
61
|
soft, atomic across item + fields + grants) · `add_credential_field` ·
|
|
@@ -118,7 +137,10 @@ EXCLUDES `sealed` fields, so a browser-facing caller can never receive sealed
|
|
|
118
137
|
plaintext through an env dump. Only trusted execution (service-token sandbox
|
|
119
138
|
injection, in-process context hydration, provider adapters) passes `True`.
|
|
120
139
|
Rows with `key IS NULL` (item fields without an env alias) never resolve into
|
|
121
|
-
an environment in either mode
|
|
140
|
+
an environment in either mode — silently when nothing promised otherwise (a
|
|
141
|
+
keyless item field is normal), but **LOUDLY when `for_sandbox=True`**: such a row
|
|
142
|
+
is flagged for injection and cannot deliver, which is a defect (see the
|
|
143
|
+
handling doctrine above).
|
|
122
144
|
|
|
123
145
|
**Legacy key-based writes respect item discipline:** `create_user_secret(upsert=True)`,
|
|
124
146
|
`update_user_secret`, and `delete_user_secret` REFUSE a target row that is
|
|
@@ -216,6 +238,7 @@ create, reveal, sealed denial, editable gate, rotate, item grants, soft delete).
|
|
|
216
238
|
|
|
217
239
|
## Change Log
|
|
218
240
|
|
|
241
|
+
- 2026-07-26 — Closed the silent sandbox-injection gap: `SandboxInjectionWithoutEnvKeyError` at every field write path, a loud ERROR-and-skip in `for_sandbox` bulk resolution, and DB CHECK `user_secrets_sandbox_injection_needs_key` (+ column default `true`→`false`). Live at the time of the fix: 0 broken rows, but 61 keyless item fields were one toggle away from it.
|
|
219
242
|
- 2026-07-23 — `update_field_metadata` primitive (metadata-only, version-preserving; `users.bump_user_secret_value_version` verified value-conditional live — no trigger change needed).
|
|
220
243
|
- 2026-07-23 — Adversarial-review hardening: include_sealed-aware bulk env resolution (+ keyless-row exclusion), ItemManagedSecretError legacy-write guards + legacy audit rows, ItemGrantSpec per-grantee grants, reveal liveness filters, rotate liveness re-check + audits inside owning transactions.
|
|
221
244
|
- 2026-07-23 — Phase 2: item layer (`items.py` — bundles, handling modes, stable resolution, item grants, transfer/fork), rotation primitives (`fernet_for_key`/`multi_fernet_for_keys`) + `scripts/rotate_credentials_key.py` (closes the no-rotation-script ticket), migration 0236 grants fixup (item-level grants possible).
|
|
@@ -48,6 +48,7 @@ from .items import (
|
|
|
48
48
|
FieldNotEditableError,
|
|
49
49
|
FieldSpec,
|
|
50
50
|
ItemGrantSpec,
|
|
51
|
+
SandboxInjectionWithoutEnvKeyError,
|
|
51
52
|
SealedFieldError,
|
|
52
53
|
add_credential_field,
|
|
53
54
|
add_item_grant,
|
|
@@ -150,6 +151,7 @@ __all__ = [
|
|
|
150
151
|
"FieldNotEditableError",
|
|
151
152
|
"FieldSpec",
|
|
152
153
|
"ItemGrantSpec",
|
|
154
|
+
"SandboxInjectionWithoutEnvKeyError",
|
|
153
155
|
"SealedFieldError",
|
|
154
156
|
"URI_MATCH_MODES",
|
|
155
157
|
"add_credential_field",
|
|
@@ -69,6 +69,18 @@ class FieldNotEditableError(PermissionError):
|
|
|
69
69
|
(``rotate_credential_fields``), never by hand."""
|
|
70
70
|
|
|
71
71
|
|
|
72
|
+
class SandboxInjectionWithoutEnvKeyError(ValueError):
|
|
73
|
+
"""``inject_into_sandbox=true`` on a field that has no environment-variable
|
|
74
|
+
alias (``key``). A container environment is a NAME→value map: with no name
|
|
75
|
+
the value has nowhere to land, so the flag is a promise the platform cannot
|
|
76
|
+
keep — the field would silently never appear in any sandbox.
|
|
77
|
+
|
|
78
|
+
Refused LOUDLY at every write path instead of accepted-and-dropped (hosts
|
|
79
|
+
map ``ValueError`` → 422). Set an env alias in the same request (the vault
|
|
80
|
+
field-metadata patch takes ``env_key`` and ``inject_into_sandbox``
|
|
81
|
+
together) or leave injection off."""
|
|
82
|
+
|
|
83
|
+
|
|
72
84
|
# ── Shapes ────────────────────────────────────────────────────────────────
|
|
73
85
|
|
|
74
86
|
|
|
@@ -175,6 +187,26 @@ def _validate_handling(handling: str) -> None:
|
|
|
175
187
|
raise ValueError(f"handling must be one of {HANDLING_MODES}; got {handling!r}")
|
|
176
188
|
|
|
177
189
|
|
|
190
|
+
def _validate_sandbox_injectable(
|
|
191
|
+
*,
|
|
192
|
+
field_key: str | None,
|
|
193
|
+
env_key: str | None,
|
|
194
|
+
inject_into_sandbox: bool,
|
|
195
|
+
) -> None:
|
|
196
|
+
"""LAYER 1 of the no-silent-injection guard: a field may only be flagged
|
|
197
|
+
for sandbox injection while it carries an env alias. See
|
|
198
|
+
:class:`SandboxInjectionWithoutEnvKeyError`. Layer 2 is the loud skip in
|
|
199
|
+
``org_service`` bulk resolution; layer 3 is the DB CHECK
|
|
200
|
+
``user_secrets_sandbox_injection_needs_key``."""
|
|
201
|
+
if inject_into_sandbox and not env_key:
|
|
202
|
+
raise SandboxInjectionWithoutEnvKeyError(
|
|
203
|
+
f"field {field_key or '<unnamed>'!r} cannot be injected into sandboxes without "
|
|
204
|
+
"an environment-variable alias (env_key) — a container env has no name to bind "
|
|
205
|
+
"the value to, so the flag would silently do nothing. Set env_key in the same "
|
|
206
|
+
"request, or leave inject_into_sandbox off."
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
178
210
|
def _validate_uri_match_mode(mode: str) -> None:
|
|
179
211
|
if mode not in URI_MATCH_MODES:
|
|
180
212
|
raise ValueError(f"uri_match_mode must be one of {URI_MATCH_MODES}; got {mode!r}")
|
|
@@ -457,6 +489,11 @@ async def create_credential_bundle(
|
|
|
457
489
|
for spec in fields:
|
|
458
490
|
_validate_field_key(spec.field_key)
|
|
459
491
|
_validate_handling(spec.handling)
|
|
492
|
+
_validate_sandbox_injectable(
|
|
493
|
+
field_key=spec.field_key,
|
|
494
|
+
env_key=spec.env_key,
|
|
495
|
+
inject_into_sandbox=spec.inject_into_sandbox,
|
|
496
|
+
)
|
|
460
497
|
if not isinstance(spec.value, str):
|
|
461
498
|
raise ValueError(f"field {spec.field_key!r}: value must be a string")
|
|
462
499
|
if spec.field_key in seen:
|
|
@@ -667,6 +704,11 @@ async def add_credential_field(
|
|
|
667
704
|
the item so field-level scoping stays consistent)."""
|
|
668
705
|
_validate_field_key(spec.field_key)
|
|
669
706
|
_validate_handling(spec.handling)
|
|
707
|
+
_validate_sandbox_injectable(
|
|
708
|
+
field_key=spec.field_key,
|
|
709
|
+
env_key=spec.env_key,
|
|
710
|
+
inject_into_sandbox=spec.inject_into_sandbox,
|
|
711
|
+
)
|
|
670
712
|
if not isinstance(spec.value, str):
|
|
671
713
|
raise ValueError("value must be a string")
|
|
672
714
|
raw = await _require_item_raw(item_id)
|
|
@@ -837,6 +879,19 @@ async def update_field_metadata(
|
|
|
837
879
|
"a one-way door; rotate/replace the field instead of unsealing it"
|
|
838
880
|
)
|
|
839
881
|
|
|
882
|
+
# The POST-patch state is what matters: enabling injection on a keyless
|
|
883
|
+
# field AND clearing the alias of an already-injecting field are the
|
|
884
|
+
# same silent breakage from opposite directions. Both are refused.
|
|
885
|
+
_validate_sandbox_injectable(
|
|
886
|
+
field_key=raw.get("field_key") or raw.get("key"),
|
|
887
|
+
env_key=(raw.get("key") if env_key is _UNSET else env_key),
|
|
888
|
+
inject_into_sandbox=(
|
|
889
|
+
bool(raw.get("inject_into_sandbox"))
|
|
890
|
+
if inject_into_sandbox is None
|
|
891
|
+
else inject_into_sandbox
|
|
892
|
+
),
|
|
893
|
+
)
|
|
894
|
+
|
|
840
895
|
updates: dict[str, Any] = {"updated_by": actor_id, "updated_at": datetime.now(UTC)}
|
|
841
896
|
if inject_into_sandbox is not None:
|
|
842
897
|
updates["inject_into_sandbox"] = inject_into_sandbox
|
|
@@ -1457,6 +1512,7 @@ __all__ = [
|
|
|
1457
1512
|
"FieldNotEditableError",
|
|
1458
1513
|
"FieldSpec",
|
|
1459
1514
|
"ItemGrantSpec",
|
|
1515
|
+
"SandboxInjectionWithoutEnvKeyError",
|
|
1460
1516
|
"SealedFieldError",
|
|
1461
1517
|
"add_credential_field",
|
|
1462
1518
|
"create_credential_bundle",
|
|
@@ -458,6 +458,31 @@ async def delete_org_secret(secret_id: str, *, actor_id: str) -> None:
|
|
|
458
458
|
)
|
|
459
459
|
|
|
460
460
|
|
|
461
|
+
def _keyed_for_sandbox(rows: list[dict[str, Any]], *, scope: str) -> list[dict[str, Any]]:
|
|
462
|
+
"""LAYER 2 of the no-silent-injection guard (loud).
|
|
463
|
+
|
|
464
|
+
A row flagged ``inject_into_sandbox`` but carrying no env alias can never
|
|
465
|
+
land in a container environment — there is no name to bind it to. Every
|
|
466
|
+
write path REFUSES that combination
|
|
467
|
+
(:class:`~matrx_orm.secrets_battery.items.SandboxInjectionWithoutEnvKeyError`)
|
|
468
|
+
and a DB CHECK backs it, so anything arriving here got in around both:
|
|
469
|
+
direct SQL, an unguarded migration, or data that predates the guards.
|
|
470
|
+
Dropping it quietly is precisely the failure this exists to end."""
|
|
471
|
+
keyed = [r for r in rows if r.get("key")]
|
|
472
|
+
orphans = len(rows) - len(keyed)
|
|
473
|
+
if orphans:
|
|
474
|
+
logger.error(
|
|
475
|
+
"[secrets_battery] %d secret(s) in scope %s are flagged "
|
|
476
|
+
"inject_into_sandbox but have NO env alias (key IS NULL) — they can never reach "
|
|
477
|
+
"a sandbox and are being SKIPPED. This state is refused by every write path; "
|
|
478
|
+
"these rows bypassed it. ids=%s",
|
|
479
|
+
orphans,
|
|
480
|
+
scope,
|
|
481
|
+
sorted(str(r["id"]) for r in rows if not r.get("key")),
|
|
482
|
+
)
|
|
483
|
+
return keyed
|
|
484
|
+
|
|
485
|
+
|
|
461
486
|
async def resolve_org_secrets(
|
|
462
487
|
organization_id: str,
|
|
463
488
|
*,
|
|
@@ -487,15 +512,21 @@ async def resolve_org_secrets(
|
|
|
487
512
|
"organization_id": organization_id,
|
|
488
513
|
"deleted_at__isnull": True,
|
|
489
514
|
"is_active": True,
|
|
490
|
-
"key__isnull": False,
|
|
491
515
|
}
|
|
492
516
|
if not include_sealed:
|
|
493
517
|
filters["handling__ne"] = "sealed"
|
|
494
518
|
if for_sandbox:
|
|
519
|
+
# Keyless rows are pulled deliberately here so the injection guard can
|
|
520
|
+
# SCREAM about them (a keyless field flagged for injection is a defect);
|
|
521
|
+
# everywhere else a keyless item field is normal and silently skipped.
|
|
495
522
|
filters["inject_into_sandbox"] = True
|
|
523
|
+
else:
|
|
524
|
+
filters["key__isnull"] = False
|
|
496
525
|
if keys:
|
|
497
526
|
filters["key__in"] = keys
|
|
498
527
|
rows = await UserSecret.filter(**filters).values("id", "key", "value_encrypted", "access_mode")
|
|
528
|
+
if for_sandbox:
|
|
529
|
+
rows = _keyed_for_sandbox(rows, scope=f"organization {organization_id}")
|
|
499
530
|
if not rows:
|
|
500
531
|
return {}
|
|
501
532
|
|
|
@@ -567,15 +598,19 @@ async def resolve_effective_secrets(
|
|
|
567
598
|
"user_id": user_id,
|
|
568
599
|
"deleted_at__isnull": True,
|
|
569
600
|
"is_active": True,
|
|
570
|
-
"key__isnull": False,
|
|
571
601
|
}
|
|
572
602
|
if not include_sealed:
|
|
573
603
|
filters["handling__ne"] = "sealed"
|
|
574
604
|
if for_sandbox:
|
|
605
|
+
# See resolve_org_secrets: keyless rows are pulled so the guard screams.
|
|
575
606
|
filters["inject_into_sandbox"] = True
|
|
607
|
+
else:
|
|
608
|
+
filters["key__isnull"] = False
|
|
576
609
|
if keys:
|
|
577
610
|
filters["key__in"] = keys
|
|
578
|
-
rows = await UserSecret.filter(**filters).values("key", "value_encrypted")
|
|
611
|
+
rows = await UserSecret.filter(**filters).values("id", "key", "value_encrypted")
|
|
612
|
+
if for_sandbox:
|
|
613
|
+
rows = _keyed_for_sandbox(rows, scope=f"user {user_id}")
|
|
579
614
|
for row in rows:
|
|
580
615
|
try:
|
|
581
616
|
resolved[str(row["key"])] = decrypt_value(row["value_encrypted"])
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "matrx-orm"
|
|
3
|
-
version = "3.1.
|
|
3
|
+
version = "3.1.41"
|
|
4
4
|
description = "Async-first PostgreSQL ORM with bidirectional migrations, schema introspection, many-to-many relationships, and built-in state caching"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
"""Level 1: QueryBuilder — filter, exclude, order_by, limit, offset, select, _build_query."""
|
|
2
2
|
|
|
3
|
+
from types import SimpleNamespace
|
|
4
|
+
from unittest.mock import AsyncMock
|
|
5
|
+
|
|
3
6
|
import pytest
|
|
4
7
|
from matrx_orm.core.base import Model, ModelMeta
|
|
5
8
|
from matrx_orm.core.fields import CharField, IntegerField, UUIDField
|
|
6
9
|
from matrx_orm.core.registry import ModelRegistry
|
|
7
10
|
from matrx_orm.exceptions import DoesNotExist, PermissionDeniedError
|
|
8
11
|
from matrx_orm.query.builder import QueryBuilder
|
|
12
|
+
from matrx_orm.state import StateManager
|
|
9
13
|
|
|
10
14
|
|
|
11
15
|
@pytest.fixture(autouse=True)
|
|
@@ -85,6 +89,20 @@ class TestQueryBuilderChaining:
|
|
|
85
89
|
qb = QueryBuilder(M).filter()
|
|
86
90
|
assert qb.filters == []
|
|
87
91
|
|
|
92
|
+
@pytest.mark.asyncio
|
|
93
|
+
async def test_all_can_skip_identity_cache_for_large_scans(self, monkeypatch):
|
|
94
|
+
M = _make_model()
|
|
95
|
+
rows = [M(id="00000000-0000-0000-0000-000000000001", name="Alice")]
|
|
96
|
+
executor = SimpleNamespace(all=AsyncMock(return_value=rows))
|
|
97
|
+
cache_bulk = AsyncMock()
|
|
98
|
+
monkeypatch.setattr(QueryBuilder, "_get_executor", lambda _self: executor)
|
|
99
|
+
monkeypatch.setattr(StateManager, "cache_bulk", cache_bulk)
|
|
100
|
+
|
|
101
|
+
result = await QueryBuilder(M).all(use_cache=False)
|
|
102
|
+
|
|
103
|
+
assert result == rows
|
|
104
|
+
cache_bulk.assert_not_awaited()
|
|
105
|
+
|
|
88
106
|
|
|
89
107
|
class TestBuildQuery:
|
|
90
108
|
def test_build_query_basic(self):
|
|
@@ -246,6 +246,199 @@ async def test_resolve_effective_secrets_personal_branch_is_handling_aware(monke
|
|
|
246
246
|
assert filters["key__isnull"] is False
|
|
247
247
|
|
|
248
248
|
|
|
249
|
+
# ── The no-silent-sandbox-injection guard ─────────────────────────────────
|
|
250
|
+
#
|
|
251
|
+
# A field flagged inject_into_sandbox with no env alias (key IS NULL) can never
|
|
252
|
+
# reach a container environment — the resolver has no name to bind it to. Before
|
|
253
|
+
# these guards the flag was accepted and the row silently dropped, so a user
|
|
254
|
+
# could switch "inject into sandbox" on and get nothing, forever, with no error.
|
|
255
|
+
# Layer 1 = write refusal (below), layer 2 = loud skip at resolution,
|
|
256
|
+
# layer 3 = DB CHECK user_secrets_sandbox_injection_needs_key.
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def test_injection_without_env_key_is_refused():
|
|
260
|
+
from matrx_orm.secrets_battery import SandboxInjectionWithoutEnvKeyError
|
|
261
|
+
from matrx_orm.secrets_battery.items import _validate_sandbox_injectable
|
|
262
|
+
|
|
263
|
+
with pytest.raises(SandboxInjectionWithoutEnvKeyError, match="env"):
|
|
264
|
+
_validate_sandbox_injectable(field_key="api_key", env_key=None, inject_into_sandbox=True)
|
|
265
|
+
with pytest.raises(SandboxInjectionWithoutEnvKeyError):
|
|
266
|
+
_validate_sandbox_injectable(field_key="api_key", env_key="", inject_into_sandbox=True)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def test_injection_with_env_key_is_allowed():
|
|
270
|
+
from matrx_orm.secrets_battery.items import _validate_sandbox_injectable
|
|
271
|
+
|
|
272
|
+
_validate_sandbox_injectable(
|
|
273
|
+
field_key="api_key", env_key="ACME_API_KEY", inject_into_sandbox=True
|
|
274
|
+
)
|
|
275
|
+
# Keyless is perfectly normal as long as nothing promised injection.
|
|
276
|
+
_validate_sandbox_injectable(field_key="password", env_key=None, inject_into_sandbox=False)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def test_injection_error_is_a_value_error_so_hosts_answer_422():
|
|
280
|
+
"""The vault router maps ValueError → 422 with the message; a user who
|
|
281
|
+
flips the switch must SEE why, not get a silent 200."""
|
|
282
|
+
from matrx_orm.secrets_battery import SandboxInjectionWithoutEnvKeyError
|
|
283
|
+
|
|
284
|
+
assert issubclass(SandboxInjectionWithoutEnvKeyError, ValueError)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@pytest.mark.asyncio
|
|
288
|
+
async def test_create_credential_bundle_refuses_injection_without_env_key():
|
|
289
|
+
from matrx_orm.secrets_battery import (
|
|
290
|
+
FieldSpec,
|
|
291
|
+
SandboxInjectionWithoutEnvKeyError,
|
|
292
|
+
create_credential_bundle,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# Validation runs before any DB work, so this needs no database.
|
|
296
|
+
with pytest.raises(SandboxInjectionWithoutEnvKeyError):
|
|
297
|
+
await create_credential_bundle(
|
|
298
|
+
user_id="u1",
|
|
299
|
+
display_name="Acme",
|
|
300
|
+
fields=[FieldSpec(field_key="api_key", value="v", inject_into_sandbox=True)],
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _noop_transaction(_name):
|
|
305
|
+
class _T:
|
|
306
|
+
async def __aenter__(self):
|
|
307
|
+
return self
|
|
308
|
+
|
|
309
|
+
async def __aexit__(self, *exc):
|
|
310
|
+
return False
|
|
311
|
+
|
|
312
|
+
return _T()
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@pytest.mark.parametrize(
|
|
316
|
+
"stored,patch",
|
|
317
|
+
[
|
|
318
|
+
# The FE's exact flow: flip the switch on a keyless item field.
|
|
319
|
+
({"key": None, "inject_into_sandbox": False}, {"inject_into_sandbox": True}),
|
|
320
|
+
# The same breakage from the other direction: clear the alias of a
|
|
321
|
+
# field that is already injecting.
|
|
322
|
+
({"key": "ACME_KEY", "inject_into_sandbox": True}, {"env_key": None}),
|
|
323
|
+
],
|
|
324
|
+
)
|
|
325
|
+
@pytest.mark.asyncio
|
|
326
|
+
async def test_update_field_metadata_refuses_a_no_op_injection(monkeypatch, stored, patch):
|
|
327
|
+
from matrx_orm.secrets_battery import SandboxInjectionWithoutEnvKeyError, items
|
|
328
|
+
|
|
329
|
+
row = {
|
|
330
|
+
"id": "f1",
|
|
331
|
+
"credential_item_id": "i1",
|
|
332
|
+
"field_key": "api_key",
|
|
333
|
+
"handling": "revealable",
|
|
334
|
+
"editable": True,
|
|
335
|
+
"value_hint": "…",
|
|
336
|
+
"value_version": 1,
|
|
337
|
+
"is_active": True,
|
|
338
|
+
"description": None,
|
|
339
|
+
"created_at": "",
|
|
340
|
+
"updated_at": "",
|
|
341
|
+
"user_id": "u1",
|
|
342
|
+
"organization_id": None,
|
|
343
|
+
**stored,
|
|
344
|
+
}
|
|
345
|
+
monkeypatch.setattr(items, "UserSecret", _fake_user_secret([row], []))
|
|
346
|
+
monkeypatch.setattr(items, "transaction", _noop_transaction)
|
|
347
|
+
monkeypatch.setattr(items, "record_secret_audit", _noop_audit)
|
|
348
|
+
|
|
349
|
+
with pytest.raises(SandboxInjectionWithoutEnvKeyError):
|
|
350
|
+
await items.update_field_metadata("f1", actor_id="u1", **patch)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
@pytest.mark.asyncio
|
|
354
|
+
async def test_update_field_metadata_allows_alias_and_injection_together(monkeypatch):
|
|
355
|
+
"""The refusal must never be a dead end: naming the alias and enabling
|
|
356
|
+
injection in ONE patch is the supported fix, so it has to pass."""
|
|
357
|
+
from matrx_orm.secrets_battery import items
|
|
358
|
+
|
|
359
|
+
row = {
|
|
360
|
+
"id": "f1",
|
|
361
|
+
"credential_item_id": "i1",
|
|
362
|
+
"field_key": "api_key",
|
|
363
|
+
"key": None,
|
|
364
|
+
"handling": "revealable",
|
|
365
|
+
"editable": True,
|
|
366
|
+
"inject_into_sandbox": False,
|
|
367
|
+
"value_hint": "…",
|
|
368
|
+
"value_version": 1,
|
|
369
|
+
"is_active": True,
|
|
370
|
+
"description": None,
|
|
371
|
+
"created_at": "",
|
|
372
|
+
"updated_at": "",
|
|
373
|
+
"user_id": "u1",
|
|
374
|
+
"organization_id": None,
|
|
375
|
+
}
|
|
376
|
+
class _FakeUserSecret:
|
|
377
|
+
@classmethod
|
|
378
|
+
def filter(cls, **kw):
|
|
379
|
+
# The env-alias uniqueness probe must come back empty.
|
|
380
|
+
return _FakeQB([] if kw.get("key") == "ACME_KEY" else [row])
|
|
381
|
+
|
|
382
|
+
@classmethod
|
|
383
|
+
async def update_where(cls, *a, **kw):
|
|
384
|
+
class _R:
|
|
385
|
+
rows_affected = 1
|
|
386
|
+
|
|
387
|
+
return _R()
|
|
388
|
+
|
|
389
|
+
monkeypatch.setattr(items, "UserSecret", _FakeUserSecret)
|
|
390
|
+
monkeypatch.setattr(items, "transaction", _noop_transaction)
|
|
391
|
+
monkeypatch.setattr(items, "record_secret_audit", _noop_audit)
|
|
392
|
+
|
|
393
|
+
out = await items.update_field_metadata(
|
|
394
|
+
"f1", actor_id="u1", env_key="ACME_KEY", inject_into_sandbox=True
|
|
395
|
+
)
|
|
396
|
+
assert out.field_key == "api_key"
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
@pytest.mark.asyncio
|
|
400
|
+
async def test_resolve_for_sandbox_screams_and_skips_keyless_rows(monkeypatch, caplog):
|
|
401
|
+
"""Layer 2: a row that got around the write guards is SKIPPED LOUDLY."""
|
|
402
|
+
import logging
|
|
403
|
+
|
|
404
|
+
from matrx_orm.secrets_battery import org_service
|
|
405
|
+
|
|
406
|
+
rows = [
|
|
407
|
+
{"id": "orphan-1", "key": None, "value_encrypted": b"x"},
|
|
408
|
+
{"id": "keyed-1", "key": "GOOD_KEY", "value_encrypted": b"y"},
|
|
409
|
+
]
|
|
410
|
+
captured: list[dict] = []
|
|
411
|
+
monkeypatch.setattr(org_service, "UserSecret", _fake_user_secret(rows, captured))
|
|
412
|
+
monkeypatch.setattr(org_service, "decrypt_value", lambda b: "plain")
|
|
413
|
+
|
|
414
|
+
with caplog.at_level(logging.ERROR, logger=org_service.logger.name):
|
|
415
|
+
out = await org_service.resolve_effective_secrets("u1", None, for_sandbox=True)
|
|
416
|
+
|
|
417
|
+
assert out == {"GOOD_KEY": "plain"}
|
|
418
|
+
# The keyless row is pulled (not filtered away) precisely so it can scream.
|
|
419
|
+
assert "key__isnull" not in captured[0]
|
|
420
|
+
assert captured[0]["inject_into_sandbox"] is True
|
|
421
|
+
assert any(
|
|
422
|
+
r.levelno >= logging.ERROR and "orphan-1" in r.getMessage() for r in caplog.records
|
|
423
|
+
), "a keyless injectable row must produce a loud ERROR naming it"
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
@pytest.mark.asyncio
|
|
427
|
+
async def test_non_sandbox_resolution_still_drops_keyless_rows_silently(monkeypatch, caplog):
|
|
428
|
+
"""Keyless item fields are NORMAL outside sandbox injection (61 live rows
|
|
429
|
+
at the time of writing) — they must not generate alarm noise."""
|
|
430
|
+
import logging
|
|
431
|
+
|
|
432
|
+
from matrx_orm.secrets_battery import org_service
|
|
433
|
+
|
|
434
|
+
captured: list[dict] = []
|
|
435
|
+
monkeypatch.setattr(org_service, "UserSecret", _fake_user_secret([], captured))
|
|
436
|
+
with caplog.at_level(logging.ERROR, logger=org_service.logger.name):
|
|
437
|
+
await org_service.resolve_effective_secrets("u1", None)
|
|
438
|
+
assert captured[0]["key__isnull"] is False
|
|
439
|
+
assert not [r for r in caplog.records if r.levelno >= logging.ERROR]
|
|
440
|
+
|
|
441
|
+
|
|
249
442
|
# ── Sealed reveal refusal + liveness filters ──────────────────────────────
|
|
250
443
|
|
|
251
444
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|