"""make legacy Agent authentication retirement durable and one-way Revision ID: 0019_agent_legacy_auth_retire Revises: 0018_legacy_agent_routing Create Date: 2026-08-31 """ from typing import Sequence, Union import sqlalchemy as sa from alembic import op revision: str = "0019_agent_legacy_auth_retire" down_revision: Union[str, None] = "0018_legacy_agent_routing" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # Keep the row-level value as a projection for operators and defense in # depth. The independent identity table below is the authoritative record: # it deliberately has no foreign key to the deletable current Agent row. op.add_column( "minerops_site_agents", sa.Column( "legacy_auth_disabled_at", sa.DateTime(timezone=True), nullable=True, ), ) op.create_table( "agent_legacy_auth_retirements", sa.Column("agent_id", sa.Text(), primary_key=True), sa.Column( "retired_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("clock_timestamp()"), ), sa.CheckConstraint( "btrim(agent_id) <> ''", name="ck_agent_legacy_auth_retirements_agent_id", ), ) connection = op.get_bind() # Stabilize both credential sources until backfill and all enforcement # triggers are installed. This preserves a single atomic migration view. connection.execute( sa.text( """ LOCK TABLE minerops_site_agents, agent_enrollments IN SHARE ROW EXCLUSIVE MODE """ ) ) # Backfill every reliable indication that a per-Agent secret was issued. # A used enrollment remains evidence even if credential columns were later # cleared. Merely existing as an uncredentialed Agent is not evidence, so # the production-shaped legacy Plexus identity remains eligible. op.execute( """ WITH credential_evidence AS ( SELECT agent_id, coalesce( credential_created_at, credential_revoked_at, clock_timestamp() ) AS retired_at FROM minerops_site_agents WHERE agent_secret_hash IS NOT NULL OR credential_created_at IS NOT NULL OR credential_revoked_at IS NOT NULL UNION ALL SELECT enrolled_agent_id AS agent_id, used_at AS retired_at FROM agent_enrollments WHERE enrolled_agent_id IS NOT NULL AND used_at IS NOT NULL ) INSERT INTO agent_legacy_auth_retirements (agent_id, retired_at) SELECT agent_id, min(retired_at) FROM credential_evidence GROUP BY agent_id """ ) op.execute( """ UPDATE minerops_site_agents AS agent SET legacy_auth_disabled_at = retirement.retired_at FROM agent_legacy_auth_retirements AS retirement WHERE retirement.agent_id = agent.agent_id """ ) op.create_check_constraint( "ck_minerops_site_agents_credential_retires_legacy_auth", "minerops_site_agents", """ legacy_auth_disabled_at IS NOT NULL OR ( agent_secret_hash IS NULL AND credential_created_at IS NULL AND credential_revoked_at IS NULL ) """, ) # The retirement ledger is append-only for ordinary database operations. # Migration rollback can still DROP the empty table, and a DBA retains the # usual PostgreSQL superuser disaster-recovery capabilities. op.execute( """ CREATE FUNCTION minerops_prevent_legacy_auth_retirement_mutation() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'legacy Agent authentication retirement history is append-only' USING ERRCODE = '23514'; END; $$ """ ) op.execute( """ CREATE TRIGGER trg_agent_legacy_auth_retirements_immutable BEFORE UPDATE OR DELETE ON agent_legacy_auth_retirements FOR EACH ROW EXECUTE FUNCTION minerops_prevent_legacy_auth_retirement_mutation() """ ) op.execute( """ CREATE TRIGGER trg_agent_legacy_auth_retirements_no_truncate BEFORE TRUNCATE ON agent_legacy_auth_retirements FOR EACH STATEMENT EXECUTE FUNCTION minerops_prevent_legacy_auth_retirement_mutation() """ ) # Any database path that stores credential evidence also creates the # identity tombstone in the same transaction. The row marker is always # synchronized from that tombstone, including on recreation of an Agent # row whose identity was previously retired. op.execute( """ CREATE FUNCTION minerops_sync_agent_legacy_auth_retirement() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE identity_retired_at timestamptz; BEGIN IF NEW.agent_secret_hash IS NOT NULL OR NEW.credential_created_at IS NOT NULL OR NEW.credential_revoked_at IS NOT NULL OR NEW.legacy_auth_disabled_at IS NOT NULL THEN INSERT INTO agent_legacy_auth_retirements ( agent_id, retired_at ) VALUES ( NEW.agent_id, coalesce( NEW.credential_created_at, NEW.legacy_auth_disabled_at, NEW.credential_revoked_at, clock_timestamp() ) ) ON CONFLICT (agent_id) DO NOTHING; END IF; SELECT retired_at INTO identity_retired_at FROM agent_legacy_auth_retirements WHERE agent_id = NEW.agent_id; IF identity_retired_at IS NOT NULL THEN IF TG_OP = 'UPDATE' AND NEW.agent_id = OLD.agent_id AND NEW.legacy_auth_disabled_at IS DISTINCT FROM OLD.legacy_auth_disabled_at AND NEW.legacy_auth_disabled_at IS DISTINCT FROM identity_retired_at THEN RAISE EXCEPTION 'legacy Agent authentication retirement is permanent' USING ERRCODE = '23514'; END IF; NEW.legacy_auth_disabled_at := identity_retired_at; ELSE NEW.legacy_auth_disabled_at := NULL; END IF; RETURN NEW; END; $$ """ ) op.execute( """ CREATE TRIGGER trg_minerops_site_agents_legacy_auth_retirement BEFORE INSERT OR UPDATE OF agent_id, agent_secret_hash, credential_created_at, credential_revoked_at, legacy_auth_disabled_at ON minerops_site_agents FOR EACH ROW EXECUTE FUNCTION minerops_sync_agent_legacy_auth_retirement() """ ) # Direct, one-way ledger inserts also refresh the optional row projection. # Skip the nested case where the Agent trigger itself inserted the ledger # row; that outer trigger already has NEW and will set the projection. op.execute( """ CREATE FUNCTION minerops_project_agent_legacy_auth_retirement() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF pg_trigger_depth() = 1 THEN UPDATE minerops_site_agents SET legacy_auth_disabled_at = NEW.retired_at WHERE agent_id = NEW.agent_id; END IF; RETURN NEW; END; $$ """ ) op.execute( """ CREATE TRIGGER trg_agent_legacy_auth_retirements_project AFTER INSERT ON agent_legacy_auth_retirements FOR EACH ROW EXECUTE FUNCTION minerops_project_agent_legacy_auth_retirement() """ ) def downgrade() -> None: connection = op.get_bind() retired_count = connection.execute( sa.text( """ SELECT count(*) FROM agent_legacy_auth_retirements """ ) ).scalar_one() if retired_count: raise RuntimeError( "Cannot downgrade 0019: durable identity-level legacy-auth " "retirement history exists. Restore the pre-upgrade database " "backup rather than making retired Agent identities eligible for " "legacy auth again." ) op.execute( """ DROP TRIGGER trg_agent_legacy_auth_retirements_project ON agent_legacy_auth_retirements """ ) op.execute("DROP FUNCTION minerops_project_agent_legacy_auth_retirement()") op.execute( """ DROP TRIGGER trg_minerops_site_agents_legacy_auth_retirement ON minerops_site_agents """ ) op.execute("DROP FUNCTION minerops_sync_agent_legacy_auth_retirement()") op.execute( """ DROP TRIGGER trg_agent_legacy_auth_retirements_no_truncate ON agent_legacy_auth_retirements """ ) op.execute( """ DROP TRIGGER trg_agent_legacy_auth_retirements_immutable ON agent_legacy_auth_retirements """ ) op.execute( "DROP FUNCTION minerops_prevent_legacy_auth_retirement_mutation()" ) op.drop_constraint( "ck_minerops_site_agents_credential_retires_legacy_auth", "minerops_site_agents", type_="check", ) op.drop_column("minerops_site_agents", "legacy_auth_disabled_at") op.drop_table("agent_legacy_auth_retirements")