- signing module: canonical content hashes + approval_message sha256(cayce-ops:<type>:<deployment_id>:<id>:<content_hash>) - approve/approve-draft/publisher-approve REQUIRE signature; without CAYCE_OPS_OPERATOR_PUBKEY+DEPLOYMENT_ID the daemons refuse to execute/send/publish (no degraded mode) - run-time re-verification at execute/send/publish: unsigned rows (raw SQL approval) or edited content revert to pre-approval state, audited as error - FULL signatures stored on rows (operator_signature, migration 0003) and in audit detail — never truncated - signature failures audited from CLI wrappers too (1f) - AUTONOMOUS_MODE approval path removed (D13): no non-human approvals tests: SQL-approved row refused at execute/send/publish; to_addr change blocks send; foreign deployment/content signatures refused; full-signature persistence. 65 tests green.
43 lines
1.3 KiB
Python
43 lines
1.3 KiB
Python
"""approval hardening: mandatory operator signatures bound to content
|
|
|
|
Revision ID: 0003
|
|
Revises: 0002
|
|
Create Date: 2026-10-20
|
|
|
|
- proposals.operator_signature
|
|
- mail_messages.operator_signature
|
|
- content_items.operator_signature
|
|
|
|
Full signatures (128 hex chars) — never truncated. Existing NULL rows are
|
|
refused at execution/send/publish time by the signing gate (no degraded mode).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sqlalchemy as sa
|
|
|
|
from alembic import op
|
|
|
|
revision = "0003"
|
|
down_revision = "0002"
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
with op.batch_alter_table("proposals") as batch:
|
|
batch.add_column(sa.Column("operator_signature", sa.Text(), nullable=True))
|
|
with op.batch_alter_table("mail_messages") as batch:
|
|
batch.add_column(sa.Column("operator_signature", sa.Text(), nullable=True))
|
|
with op.batch_alter_table("content_items") as batch:
|
|
batch.add_column(sa.Column("operator_signature", sa.Text(), nullable=True))
|
|
|
|
|
|
def downgrade() -> None:
|
|
with op.batch_alter_table("content_items") as batch:
|
|
batch.drop_column("operator_signature")
|
|
with op.batch_alter_table("mail_messages") as batch:
|
|
batch.drop_column("operator_signature")
|
|
with op.batch_alter_table("proposals") as batch:
|
|
batch.drop_column("operator_signature")
|