feat(agent): add durable existing-agent credential transitions

This commit is contained in:
lussierj committed 2026-10-14 13:50:07 +00:00
1 parent abf5654a25
commit 3cfb0d1abe
33 files changed
+3828 -54

No files matched your search

@@ -0,0 +1,308 @@
"""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")