Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ _site/
*.pyc
__pycache__/

# Generated local policy bundle artifacts
implementations/acr-control-plane/var/policy_bundles/

# Optional: uncomment if using a doc generator
# node_modules/
# .venv/
Expand Down
1 change: 1 addition & 0 deletions implementations/acr-control-plane/.env.production.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ JWT_ALGORITHM=HS256
JWT_TOKEN_EXPIRE_MINUTES=30

KILLSWITCH_SECRET=CHANGE_ME
AUDIT_SIGNING_SECRET=CHANGE_ME
OPERATOR_SESSION_SECRET=CHANGE_ME
WEBHOOK_HMAC_SECRET=CHANGE_ME
EXECUTOR_HMAC_SECRET=CHANGE_ME
Expand Down
2 changes: 1 addition & 1 deletion implementations/acr-control-plane/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "acr-control-plane"
version = "1.0.0"
version = "1.0.1"
description = "ACR Reference Control Plane — governance gateway for autonomous AI systems"
readme = "README.md"
requires-python = ">=3.11"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ def build_secret_bundle() -> dict[str, str]:
return {
"JWT_SECRET_KEY": _token_hex(),
"KILLSWITCH_SECRET": _token_hex(),
"AUDIT_SIGNING_SECRET": _token_hex(),
"OPERATOR_SESSION_SECRET": _token_hex(),
"WEBHOOK_HMAC_SECRET": _token_hex(),
"EXECUTOR_HMAC_SECRET": _token_hex(),
Expand Down
2 changes: 1 addition & 1 deletion implementations/acr-control-plane/src/acr/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# ACR Control Plane — Reference Implementation
# Apache 2.0 License
__version__ = "1.0.0"
__version__ = "1.0.1"
10 changes: 10 additions & 0 deletions implementations/acr-control-plane/src/acr/common/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,16 @@ class PolicyEngineError(ACRError):
error_code = "POLICY_ENGINE_ERROR"


class RuntimeControlDependencyError(ACRError):
status_code = 503
error_code = "RUNTIME_CONTROL_DEPENDENCY_ERROR"


class AuthoritativeSpendControlError(ACRError):
status_code = 503
error_code = "AUTHORITATIVE_SPEND_CONTROL_ERROR"


# ── Approval ──────────────────────────────────────────────────────────────────

class ApprovalPendingError(ACRError):
Expand Down
21 changes: 20 additions & 1 deletion implementations/acr-control-plane/src/acr/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class Settings(BaseSettings):
# ── Runtime ───────────────────────────────────────────────────────────────
acr_env: str = "development"
log_level: str = "INFO"
acr_version: str = "1.0"
acr_version: str = "1.0.1"
schema_bootstrap_mode: str = "auto"
strict_dependency_startup: bool = False

Expand All @@ -47,6 +47,7 @@ class Settings(BaseSettings):
# HMAC-SHA256 key for signing outbound webhook payloads.
# Receivers verify the X-ACR-Signature header to confirm authenticity.
webhook_hmac_secret: str = ""
audit_signing_secret: str = "dev_audit_signing_secret_change_me"

# ── OpenTelemetry ─────────────────────────────────────────────────────────
otel_exporter_otlp_endpoint: str = ""
Expand Down Expand Up @@ -109,6 +110,7 @@ class Settings(BaseSettings):
"changeme",
"password",
"jwt_secret",
"dev_audit_signing_secret_change_me",
}

_MIN_KEY_BYTES = 32 # 256 bits — minimum for HS256
Expand Down Expand Up @@ -148,6 +150,12 @@ def assert_production_secrets() -> None:
f"'{settings.acr_env}'. Set a strong random secret before deploying."
)

if settings.audit_signing_secret in _WEAK_KEYS or len(settings.audit_signing_secret.encode()) < _MIN_KEY_BYTES:
raise RuntimeError(
"AUDIT_SIGNING_SECRET must be set to a strong value before deploying "
"outside development/test."
)

if settings.execute_allowed_actions and len(settings.executor_hmac_secret.encode()) < _MIN_KEY_BYTES:
raise RuntimeError(
"EXECUTOR_HMAC_SECRET must be set to a strong value before enabling "
Expand All @@ -165,6 +173,12 @@ def assert_production_secrets() -> None:
"OPERATOR_API_KEYS_JSON must be set in non-development environments."
)

if not settings.service_operator_api_key:
raise RuntimeError(
"SERVICE_OPERATOR_API_KEY must be set in non-development environments "
"so the gateway can authenticate to the independent kill-switch service."
)

if settings.oidc_enabled:
if settings.operator_session_secret in _WEAK_KEYS or len(settings.operator_session_secret.encode()) < _MIN_KEY_BYTES:
raise RuntimeError(
Expand Down Expand Up @@ -199,6 +213,11 @@ def effective_schema_bootstrap_mode() -> str:
return "create" if settings.acr_env in ("development", "test") else "validate"


def runtime_dependencies_fail_closed() -> bool:
"""Enterprise posture: fail closed outside development/test."""
return settings.strict_dependency_startup or settings.acr_env not in ("development", "test")


@lru_cache(maxsize=1)
def operator_api_keys() -> dict[str, dict]:
if not settings.operator_api_keys_json:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,95 @@
"""
from __future__ import annotations

from datetime import date

from alembic import op
import sqlalchemy as sa

revision = "0011"
down_revision = "0010"
branch_labels = None
depends_on = None


def _first_of_month(value: date) -> date:
return value.replace(day=1)


def _add_months(value: date, months: int) -> date:
month_index = (value.year * 12 + value.month - 1) + months
year = month_index // 12
month = month_index % 12 + 1
return date(year, month, 1)


def _partition_bounds(conn, table: str) -> tuple[date, date]:
result = conn.execute(
sa.text(
f"SELECT MIN(created_at)::date AS min_created, "
f"MAX(created_at)::date AS max_created FROM {table}"
)
)
row = result.fetchone()
today = date.today()
min_created = row.min_created if row and row.min_created else today
max_created = row.max_created if row and row.max_created else today

start = _first_of_month(min_created)
# Keep a forward partition horizon even for mostly historical data.
end = _add_months(_first_of_month(max_created), 4)
return start, end


def _create_partitioned_copy(conn, table: str, tmp: str) -> None:
# Do not copy constraints or indexes onto the partitioned parent; Postgres
# rejects inherited PK/UNIQUE constraints that do not include created_at.
op.execute(
f"CREATE TABLE {tmp} ("
f"LIKE {table} INCLUDING DEFAULTS INCLUDING GENERATED "
f"INCLUDING IDENTITY INCLUDING STORAGE INCLUDING COMMENTS"
f") PARTITION BY RANGE (created_at)"
)

start, end = _partition_bounds(conn, table)
cursor = start
while cursor < end:
next_month = _add_months(cursor, 1)
suffix = f"y{cursor.year:04d}m{cursor.month:02d}"
part_name = f"{table}_{suffix}"
op.execute(
f"CREATE TABLE {part_name} PARTITION OF {tmp} "
f"FOR VALUES FROM ('{cursor.isoformat()}') TO ('{next_month.isoformat()}')"
)
cursor = next_month

op.execute(f"CREATE TABLE {table}_default PARTITION OF {tmp} DEFAULT")


def _create_indexes(table: str) -> None:
if table == "telemetry_events":
op.create_index("ix_telemetry_events_event_id", table, ["event_id"])
op.create_index("ix_telemetry_events_correlation_id", table, ["correlation_id"])
op.create_index("ix_telemetry_events_agent_id", table, ["agent_id"])
op.create_index("ix_telemetry_events_created_at", table, ["created_at"])
return

if table == "drift_metrics":
op.create_index("ix_drift_metrics_agent_id", table, ["agent_id"])
op.create_index("ix_drift_metrics_created_at", table, ["created_at"])
op.create_index("ix_drift_metrics_agent_created", table, ["agent_id", "created_at"])
return

raise ValueError(f"unsupported partitioned table: {table}")


def upgrade() -> None:
conn = op.get_bind()

for table in ("telemetry_events", "drift_metrics"):
# Check if table is already partitioned (Postgres-specific)
result = conn.execute(
__import__("sqlalchemy").text(
sa.text(
"SELECT c.relkind FROM pg_class c "
"JOIN pg_namespace n ON n.oid = c.relnamespace "
"WHERE c.relname = :tbl AND n.nspname = 'public'"
Expand All @@ -34,35 +108,16 @@ def upgrade() -> None:

tmp = f"{table}_new"

# 1. Create partitioned table with same schema
op.execute(
f"CREATE TABLE {tmp} (LIKE {table} INCLUDING ALL) "
f"PARTITION BY RANGE (created_at)"
)
# 1. Create a partitioned copy without invalid inherited constraints.
_create_partitioned_copy(conn, table, tmp)

# 2. Create initial partitions: current month + next 3 months
partitions = [
("2026-04-01", "2026-05-01", "y2026m04"),
("2026-05-01", "2026-06-01", "y2026m05"),
("2026-06-01", "2026-07-01", "y2026m06"),
("2026-07-01", "2026-08-01", "y2026m07"),
]
for start, end, suffix in partitions:
part_name = f"{table}_{suffix}"
op.execute(
f"CREATE TABLE {part_name} PARTITION OF {tmp} "
f"FOR VALUES FROM ('{start}') TO ('{end}')"
)
op.execute(
f"CREATE INDEX ix_{part_name}_created_at ON {part_name} (created_at)"
)

# 3. Migrate data
# 2. Migrate data
op.execute(f"INSERT INTO {tmp} SELECT * FROM {table}")

# 4. Swap tables
# 3. Swap tables and recreate parent indexes.
op.execute(f"DROP TABLE {table}")
op.execute(f"ALTER TABLE {tmp} RENAME TO {table}")
_create_indexes(table)


def downgrade() -> None:
Expand All @@ -72,7 +127,7 @@ def downgrade() -> None:

for table in ("telemetry_events", "drift_metrics"):
result = conn.execute(
__import__("sqlalchemy").text(
sa.text(
"SELECT c.relkind FROM pg_class c "
"JOIN pg_namespace n ON n.oid = c.relnamespace "
"WHERE c.relname = :tbl AND n.nspname = 'public'"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Allow MODIFY decisions in policy_decisions.

Revision ID: 0013
Revises: 0012
Create Date: 2026-04-08
"""
from __future__ import annotations

from alembic import op


revision = "0013"
down_revision = "0012"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.drop_constraint("ck_policy_decisions_decision", "policy_decisions", type_="check")
op.create_check_constraint(
"ck_policy_decisions_decision",
"policy_decisions",
"decision IN ('allow', 'deny', 'modify', 'escalate')",
)


def downgrade() -> None:
op.drop_constraint("ck_policy_decisions_decision", "policy_decisions", type_="check")
op.create_check_constraint(
"ck_policy_decisions_decision",
"policy_decisions",
"decision IN ('allow', 'deny', 'escalate')",
)
4 changes: 2 additions & 2 deletions implementations/acr-control-plane/src/acr/db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ class PolicyDecisionRecord(Base):
__tablename__ = "policy_decisions"
__table_args__ = (
CheckConstraint(
"decision IN ('allow', 'deny', 'escalate')",
"decision IN ('allow', 'deny', 'modify', 'escalate')",
name="ck_policy_decisions_decision",
),
)
Expand All @@ -108,7 +108,7 @@ class PolicyDecisionRecord(Base):
String(128), ForeignKey("agents.agent_id", ondelete="CASCADE"), nullable=False, index=True
)
policy_id: Mapped[str] = mapped_column(String(128), nullable=False)
decision: Mapped[str] = mapped_column(String(16), nullable=False) # allow | deny | escalate
decision: Mapped[str] = mapped_column(String(16), nullable=False) # allow | deny | modify | escalate
reason: Mapped[str | None] = mapped_column(Text, nullable=True)
tool_name: Mapped[str | None] = mapped_column(String(128), nullable=True)
latency_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
Expand Down
Loading
Loading