Skip to content

feat(server): Phase 0: add control version history and soft-delete unusable legacy controls#172

Merged
lan17 merged 3 commits intomainfrom
feature/control-phase-0
Apr 22, 2026
Merged

feat(server): Phase 0: add control version history and soft-delete unusable legacy controls#172
lan17 merged 3 commits intomainfrom
feature/control-phase-0

Conversation

@lan17
Copy link
Copy Markdown
Contributor

@lan17 lan17 commented Apr 16, 2026

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 new control_versions table. 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 with data: 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:

  • backfill snapshots and version numbering for existing rows
  • automatic cleanup for unusable legacy controls
  • soft-delete filtering across control reads, listings, and agent/policy associations
  • the non-null GetControlResponse.data contract

Validation

  • make check
  • make openapi-spec-check
  • make sdk-ts-generate
  • make sdk-ts-overlay-test
  • make sdk-ts-name-check
  • fresh review-loop pass against origin/main with no findings

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 16, 2026

Codecov Report

❌ Patch coverage is 98.93048% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...ver/src/agent_control_server/endpoints/controls.py 97.26% 2 Missing ⚠️
...rver/src/agent_control_server/services/controls.py 99.14% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@lan17 lan17 changed the title [codex] Implement control phase 0 cleanup Add control version history and soft-delete unusable legacy controls Apr 16, 2026
@lan17 lan17 changed the title Add control version history and soft-delete unusable legacy controls feat(server): add control version history and soft-delete unusable legacy controls Apr 16, 2026
@lan17 lan17 marked this pull request as ready for review April 16, 2026 03:18
@lan17 lan17 changed the title feat(server): add control version history and soft-delete unusable legacy controls feat(server): Phase 0: add control version history and soft-delete unusable legacy controls Apr 16, 2026
## 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
@lan17 lan17 merged commit e5b2b33 into main Apr 22, 2026
5 checks passed
@lan17 lan17 deleted the feature/control-phase-0 branch April 22, 2026 02:07
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))
@galileo-automation
Copy link
Copy Markdown
Collaborator

🎉 This PR is included in version 2.4.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants