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
- 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.
- 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.
- 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.
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
ControlPlaneSetupproduces by default — alembic autogenerate sees a foot-gun: the 12 SDK tables exist in the live DB but are not in the consumer'sBase.metadata.Result:
A consumer who runs
alembic upgrade headagainst 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
ControlPlaneSetupproduces 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 carelessupgrade headaway.Workaround landed in suspension
alembic/env.pyinclude_object()filter that usesagent_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_NAMESandinclude_objectblock.Possible SDK-side fixes
agent_control_plane.alembic.include_acp_tables(include_object: Callable) -> Callablethat wraps a consumer's existing filter. Drop-in three-line addition.cp.token_budget_configs), autogenerate by default ignores tables outside the consumer'starget_metadata.schema. Requires the SDK to supportBase.metadata.schema = "cp"configuration, which is straightforward with SQLAlchemy.include_objectsnippet 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.