"""bind one-time credential transitions to existing Agent identities Revision ID: 0020_existing_agent_transition Revises: 0019_agent_legacy_auth_retire Create Date: 2026-09-18 """ from typing import Sequence, Union import sqlalchemy as sa from alembic import op revision: str = "0020_existing_agent_transition" down_revision: Union[str, None] = "0019_agent_legacy_auth_retire" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # NULL continues to mean a normal new-Agent enrollment. A non-NULL value # is server-derived from the current Agent row and binds the one-time token # to that exact durable identity without accepting browser routing input. op.add_column( "agent_enrollments", sa.Column("target_agent_id", sa.Text(), nullable=True), ) op.create_foreign_key( "fk_agent_enrollments_target_agent", "agent_enrollments", "minerops_site_agents", ["target_agent_id"], ["agent_id"], ondelete="RESTRICT", ) op.create_check_constraint( "ck_agent_enrollments_target_agent", "agent_enrollments", "target_agent_id IS NULL OR btrim(target_agent_id) <> ''", ) op.create_check_constraint( "ck_agent_enrollments_target_result", "agent_enrollments", """ target_agent_id IS NULL OR enrolled_agent_id IS NULL OR enrolled_agent_id = target_agent_id """, ) op.create_index( "uq_agent_enrollments_open_target_agent", "agent_enrollments", ["target_agent_id"], unique=True, postgresql_where=sa.text( "target_agent_id IS NOT NULL " "AND used_at IS NULL AND revoked_at IS NULL" ), ) def downgrade() -> None: connection = op.get_bind() transition_count = connection.execute( sa.text( """ SELECT count(*) FROM agent_enrollments WHERE target_agent_id IS NOT NULL """ ) ).scalar_one() if transition_count: raise RuntimeError( "Cannot downgrade 0020: existing-Agent credential transition " "history exists. Restore the pre-upgrade database backup rather " "than discarding the identity binding." ) op.drop_index( "uq_agent_enrollments_open_target_agent", table_name="agent_enrollments", ) op.drop_constraint( "ck_agent_enrollments_target_result", "agent_enrollments", type_="check", ) op.drop_constraint( "ck_agent_enrollments_target_agent", "agent_enrollments", type_="check", ) op.drop_constraint( "fk_agent_enrollments_target_agent", "agent_enrollments", type_="foreignkey", ) op.drop_column("agent_enrollments", "target_agent_id")