Skip to content

Co-located SDK tables trap alembic autogenerate into proposing DROPs #6

@ryanwi

Description

@ryanwi

Discovered during: dogfooding integration in suspension (v0.14.2)

Behavior

When a SQLAlchemy/alembic consumer co-locates ACP tables in their primary application database — the natural first deployment shape, and what ControlPlaneSetup produces by default — alembic autogenerate sees a foot-gun: the 12 SDK tables exist in the live DB but are not in the consumer's Base.metadata.

Result:

$ uv run alembic revision --autogenerate -m "my_unrelated_change"
INFO  [alembic.autogenerate.compare.tables] Detected removed table 'token_budget_configs'
INFO  [alembic.autogenerate.compare.tables] Detected removed table 'token_budget_states'
INFO  [alembic.autogenerate.compare.tables] Detected removed table 'token_usage_ledger'
INFO  [alembic.autogenerate.compare.tables] Detected removed table 'control_sessions'
... (all 12 SDK tables proposed for DROP)

A consumer who runs alembic upgrade head against the generated migration without auditing it would wipe persistent budget state, session history, command ledger — everything ACP is responsible for.

Why this matters

The SDK is sold as "embeddable governance" and the default ControlPlaneSetup produces a SQLite-backed configuration that co-locates everything. Consumers reading the README will reach for the same database (their own) when adopting the SDK. They will then run alembic autogenerate at some point during normal development. The destructive default is one careless upgrade head away.

Workaround landed in suspension

alembic/env.py include_object() filter that uses agent_control_plane.models.reference.Base.metadata.tables.keys() as the source of truth for "tables I don't manage." Stays in sync with whatever SDK version is installed; filters tables, FKs, indexes, and columns attached to ACP-owned tables.

suspension/alembic/env.py — see the _ACP_TABLE_NAMES and include_object block.

Possible SDK-side fixes

  1. Ship a documented helper. agent_control_plane.alembic.include_acp_tables(include_object: Callable) -> Callable that wraps a consumer's existing filter. Drop-in three-line addition.
  2. Support Postgres schemas natively. If ACP tables lived in their own schema (e.g., cp.token_budget_configs), autogenerate by default ignores tables outside the consumer's target_metadata.schema. Requires the SDK to support Base.metadata.schema = "cp" configuration, which is straightforward with SQLAlchemy.
  3. Document the trap. A "Coexisting with alembic" section in the README with the include_object snippet ready to copy-paste. Lowest-effort, but every consumer still hits the trap once.

Option 2 is the cleanest long-term — it pushes the namespace boundary into the database instead of into consumer-side filter code. Option 1 keeps the current schema and removes the boilerplate. Option 3 is a band-aid.

Severity: Medium-High. Affects every SQLAlchemy/alembic consumer who co-locates ACP tables. The destructive failure mode (silent DROPs that wipe governance state) makes this higher-stakes than the other open issues.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions