feat(server): Phase 0: add control version history and soft-delete unusable legacy controls#172
Merged
feat(server): Phase 0: add control version history and soft-delete unusable legacy controls#172
Conversation
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
## Summary This is Phase 1 of the control lifecycle work. It takes `control_versions` from migration-only/backfill state into active runtime use by recording new version rows on control mutations and exposing version history through read APIs. This PR is stacked on top of #172. Please review it against `feature/control-phase-0`, not against `main`. Design doc and implementation plan: https://gist.github.com/lan17/6a08282243576f096626bb10996c024b ## What changed The server now records a version row whenever a control is created, updated, patched, or soft-deleted. Those writes happen in the same transaction as the control mutation, so the live control row and its latest audit snapshot stay in sync. To keep that logic in one place, this PR introduces a `ControlService` that owns active-control lookup, version creation, and version-history reads. The control endpoints now use that service instead of each path reaching into the database on its own, and the agent/policy association endpoints use the same service for active-control existence checks. This PR also adds two version history endpoints: - `GET /api/v1/controls/{control_id}/versions` - `GET /api/v1/controls/{control_id}/versions/{version_num}` Those endpoints follow the repo's cursor-pagination conventions. The list endpoint returns summaries only, while the detail endpoint returns the full stored snapshot for audit and diffing. The shared API models, Python SDK wrapper, and generated TypeScript SDK were updated to match the new endpoints and response shapes. ## Review follow-up A review-loop pass found a concurrency hole in version number allocation and one misleading error mapping on `PATCH /controls/{id}`. This branch now serializes version creation with a row-level control lock and only reports `CONTROL_NAME_CONFLICT` for actual control-name uniqueness failures. ## Why this shape Phase 0 created the schema and backfilled history, but runtime writes still were not participating in version tracking. That left `control_versions` useful for migration cleanup, but not yet trustworthy as the live audit trail. This PR closes that gap before the later store/clone work. It also establishes `ControlService` as the boundary for control persistence concerns, which keeps the next phase from duplicating versioning and lookup logic again. ## Reviewer notes The important areas to look at are: - version-row creation on all control mutation paths - `ControlService` transaction behavior, row locking, and snapshot shape - version history endpoint pagination and deleted-control behavior - the shared-model / SDK surface added for version-history reads ## Validation - `make check` - `make openapi-spec-check` - `make sdk-ts-generate` - `make sdk-ts-overlay-test` - `make sdk-ts-name-check` - targeted server slice for control/version endpoints - targeted Python SDK controls wrapper slice
galileo-automation
pushed a commit
that referenced
this pull request
Apr 22, 2026
## [2.4.0](ts-sdk-v2.3.0...ts-sdk-v2.4.0) (2026-04-22) ### Features * **evaluators:** add built-in budget evaluator for per-agent cost tracking ([#144](#144)) ([d4ce113](d4ce113)), closes [#130](#130) * **sdk:** add external sink ([#175](#175)) ([45f3645](45f3645)) * **server:** Align condition and template depth limits ([#166](#166)) ([03f402e](03f402e)) * **server:** Phase 0: add control version history and soft-delete unusable legacy controls ([#172](#172)) ([e5b2b33](e5b2b33)) ### Bug Fixes * **examples:** fix crewai examples ([#179](#179)) ([9004ea3](9004ea3))
Collaborator
|
🎉 This PR is included in version 2.4.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This is the first step in the control lifecycle work. It introduces version history for controls and cleans up legacy rows that no longer satisfy the control contract, so later phases can build on a consistent base.
Design doc and implementation plan: https://gist.github.com/lan17/6a08282243576f096626bb10996c024b
What changed
The migration adds
controls.deleted_at, replaces the old global name uniqueness constraint with an active-only unique index, and creates the newcontrol_versionstable. Every existing control is backfilled into version history as version 1.During that backfill, we validate each existing control against the shapes we still support today: normal control definitions and unrendered templates. Controls that are empty, corrupted, or otherwise unusable are automatically soft-deleted. Before tombstoning them, the migration removes any policy, agent, or store associations so we do not leave active references behind.
At runtime, active control lookups now consistently ignore soft-deleted rows.
GET /api/v1/controls/{id}no longer returns a successful response withdata: null; if an active row is corrupted, we surface that as corrupted data instead. Deletes now tombstone the control rather than hard-deleting it, which keeps the newly backfilled version history intact.The shared API model and generated SDK surfaces were updated to match that tighter contract, and the server tests now cover migration backfill, automatic legacy cleanup, and soft-delete filtering across the affected paths.
Why this shape
We want later phases to assume a clean, explicit control lifecycle instead of carrying around
{}controls and other invalid legacy cases. Doing that cleanup here keeps the follow-on work simpler and makes the new version table immediately trustworthy.Reviewer notes
The migration is the part worth the closest look. In particular:
GetControlResponse.datacontractValidation
make checkmake openapi-spec-checkmake sdk-ts-generatemake sdk-ts-overlay-testmake sdk-ts-name-checkorigin/mainwith no findings