Browse Source

feat: complete enterprise edge gateway

马小龙 2 months ago
parent
commit
84dc0adc7c
100 changed files with 28941 additions and 115 deletions
  1. 7 2
      app/__init__.py
  2. 3 0
      app/api/data_source/__init__.py
  3. 299 0
      app/api/data_source/edge_routes.py
  4. 141 24
      app/config/config.py
  5. 121 0
      app/config/database_urls.py
  6. 63 0
      app/core/edge_gateway/__init__.py
  7. 706 0
      app/core/edge_gateway/contracts.py
  8. 475 0
      app/core/edge_gateway/policy.py
  9. 2189 0
      app/core/edge_gateway/queue.py
  10. 609 0
      app/core/edge_gateway/repository.py
  11. 99 0
      app/core/edge_gateway/runtime_roles.py
  12. 700 0
      app/core/edge_gateway/service.py
  13. 26 0
      app/core/system/permissions.py
  14. 19 0
      app/edge_gateway/__init__.py
  15. 5 0
      app/edge_gateway/__main__.py
  16. 1273 0
      app/edge_gateway/agent.py
  17. 333 0
      app/edge_gateway/bootstrap.py
  18. 32 0
      app/edge_gateway/healthcheck.py
  19. 276 0
      app/edge_gateway/runtime.py
  20. 427 0
      app/edge_gateway/transport.py
  21. 20 0
      app/models/__init__.py
  22. 133 0
      app/models/edge_gateway.py
  23. 51 0
      app/runner/api.py
  24. 15 1
      app/runner/bootstrap.py
  25. 8 5
      deploy/docker/README.md
  26. 12 2
      deploy/docker/backend.Dockerfile
  27. 98 6
      deploy/docker/docker-compose.yml
  28. 33 0
      deploy/docker/edge-mtls-nginx.conf
  29. 3 4
      deploy/docker/runner.Dockerfile
  30. 20 0
      deploy/edge/Dockerfile.edge
  31. 44 0
      deploy/edge/README.md
  32. 71 0
      deploy/edge/docker-compose.edge.yml
  33. 21 0
      deploy/edge/edge.config.example.json
  34. 78 0
      deploy/edge/edge.config.schema.json
  35. 29 0
      deploy/edge/edge.env.example
  36. 17 2
      deployment/CHECKLIST.md
  37. 3 1
      deployment/DEPLOYMENT_GUIDE.md
  38. 11 0
      deployment/MANIFEST.md
  39. 2 1
      deployment/README.md
  40. 7 2
      deployment/app/__init__.py
  41. 3 0
      deployment/app/api/data_source/__init__.py
  42. 299 0
      deployment/app/api/data_source/edge_routes.py
  43. 141 24
      deployment/app/config/config.py
  44. 121 0
      deployment/app/config/database_urls.py
  45. 63 0
      deployment/app/core/edge_gateway/__init__.py
  46. 706 0
      deployment/app/core/edge_gateway/contracts.py
  47. 475 0
      deployment/app/core/edge_gateway/policy.py
  48. 2189 0
      deployment/app/core/edge_gateway/queue.py
  49. 609 0
      deployment/app/core/edge_gateway/repository.py
  50. 99 0
      deployment/app/core/edge_gateway/runtime_roles.py
  51. 700 0
      deployment/app/core/edge_gateway/service.py
  52. 26 0
      deployment/app/core/system/permissions.py
  53. 19 0
      deployment/app/edge_gateway/__init__.py
  54. 5 0
      deployment/app/edge_gateway/__main__.py
  55. 1273 0
      deployment/app/edge_gateway/agent.py
  56. 333 0
      deployment/app/edge_gateway/bootstrap.py
  57. 32 0
      deployment/app/edge_gateway/healthcheck.py
  58. 276 0
      deployment/app/edge_gateway/runtime.py
  59. 427 0
      deployment/app/edge_gateway/transport.py
  60. 20 0
      deployment/app/models/__init__.py
  61. 133 0
      deployment/app/models/edge_gateway.py
  62. 51 0
      deployment/app/runner/api.py
  63. 15 1
      deployment/app/runner/bootstrap.py
  64. 33 0
      deployment/config/nginx-dataops-platform.conf
  65. 13 2
      deployment/dataops.env
  66. 7 0
      deployment/deploy_dataops.sh
  67. 5 4
      deployment/migrations/env.py
  68. 532 0
      deployment/migrations/versions/20260809_477_edge_gateway_control_plane.py
  69. 22 0
      deployment/migrations/versions/20260809_478_edge_gateway_security_cleanup.py
  70. 187 0
      deployment/migrations/versions/20260809_479_edge_gateway_signed_authority.py
  71. 34 2
      deployment/package_release.sh
  72. 5 0
      deployment/scripts/deploy_dataops.sh
  73. 1314 0
      deployment/scripts/edge_gateway_admin.py
  74. 13 1
      deployment/scripts/run_dataops.sh
  75. 73 0
      deployment/scripts/scan_release_secrets.py
  76. 1 1
      deployment/sync_release.sh
  77. 13 8
      docs/DATAOPS_PHASE3_6_MONTH_DEVELOPMENT_PLAN_20260802.md
  78. 982 2
      docs/architecture/OPENAPI.yaml
  79. 15 2
      docs/phase3/P3_WP00_REQUIREMENTS.json
  80. 72 0
      docs/phase3/P3_WP04_EDGE_GATEWAY.md
  81. 229 0
      docs/phase3/P3_WP04_EDGE_GATEWAY_IMPLEMENTATION_PLAN.md
  82. 616 0
      docs/runbooks/EDGE_GATEWAY_OPERATIONS.md
  83. 115 0
      docs/validation/P3_WP04_EDGE_GATEWAY_EVIDENCE.md
  84. 107 0
      docs/validation/P3_WP04_FAILURE_INJECTION.json
  85. 11 1
      env.example
  86. 5 4
      migrations/env.py
  87. 532 0
      migrations/versions/20260809_477_edge_gateway_control_plane.py
  88. 22 0
      migrations/versions/20260809_478_edge_gateway_security_cleanup.py
  89. 187 0
      migrations/versions/20260809_479_edge_gateway_signed_authority.py
  90. 5 0
      scripts/deploy_dataops.sh
  91. 1314 0
      scripts/edge_gateway_admin.py
  92. 307 12
      scripts/generate_openapi.py
  93. 13 1
      scripts/run_dataops.sh
  94. 1186 0
      tests/integration/test_phase3_wp04_edge_agent.py
  95. 1945 0
      tests/integration/test_phase3_wp04_edge_gateway_postgres.py
  96. 497 0
      tests/integration/test_phase3_wp04_edge_runtime.py
  97. 291 0
      tests/integration/test_phase3_wp04_nginx_mtls.py
  98. 561 0
      tests/security/test_phase3_wp04_data_boundary.py
  99. 18 0
      tests/test_permission_matrix.py
  100. 1165 0
      tests/test_phase3_wp04_delivery_contract.py

+ 7 - 2
app/__init__.py

@@ -12,6 +12,7 @@ from app.config.config import (
     current_env,
     log_llm_env_status,
     log_service_env_status,
+    validate_production_database_identity,
 )
 from app.config.cors import CORS_OPTIONS
 
@@ -30,6 +31,8 @@ def create_app():
     # 配置CORS以解决跨域问题
     CORS(app, **CORS_OPTIONS)
     db.init_app(app)
+    with app.app_context():
+        validate_production_database_identity(app, db.engine)
 
     # 注册蓝图
     from app.api.business_domain import bp as business_domain_bp
@@ -139,7 +142,8 @@ def configure_response_headers(app):
                 headers = (
                     "Content-Type, Authorization, X-Requested-With, "
                     "Accept, Origin, Cache-Control, X-File-Name, If-Match, "
-                    "X-Agent-Credential"
+                    "X-Agent-Credential, X-Edge-Enrollment, X-Edge-Credential, "
+                    "X-Edge-Certificate-SHA256"
                 )
                 response.headers["Access-Control-Allow-Headers"] = headers
                 response.headers["Access-Control-Max-Age"] = "86400"
@@ -161,7 +165,8 @@ def configure_response_headers(app):
             if "Access-Control-Allow-Headers" not in response.headers:
                 headers = (
                     "Content-Type, Authorization, X-Requested-With, Accept, Origin, "
-                    "If-Match, X-Agent-Credential"
+                    "If-Match, X-Agent-Credential, X-Edge-Enrollment, "
+                    "X-Edge-Credential, X-Edge-Certificate-SHA256"
                 )
                 response.headers["Access-Control-Allow-Headers"] = headers
 

+ 3 - 0
app/api/data_source/__init__.py

@@ -1,5 +1,8 @@
+# ruff: noqa: E402, F401, I001
+
 from flask import Blueprint
 
 bp = Blueprint("data_source", __name__)
 
+from app.api.data_source import edge_routes  # noqa: E402, F401
 from app.api.data_source import routes  # noqa: E402, F401

+ 299 - 0
app/api/data_source/edge_routes.py

@@ -0,0 +1,299 @@
+"""Bounded HTTP boundary for human and machine edge-gateway operations."""
+
+from __future__ import annotations
+
+import hmac
+import ipaddress
+import logging
+import re
+from datetime import UTC, datetime
+from urllib.parse import unquote
+
+from cryptography import x509
+from cryptography.hazmat.primitives import hashes
+from flask import current_app, g, jsonify, request
+
+from app.api.data_source import bp
+from app.core.edge_gateway.contracts import EdgeContractError, preflight_json
+from app.core.edge_gateway.service import (
+    EdgeGatewayAuthenticationError,
+    EdgeGatewayError,
+    EdgeGatewayPayloadTooLargeError,
+    EdgeGatewayService,
+    EdgeGatewayValidationError,
+)
+from app.models.result import failed, success
+
+logger = logging.getLogger(__name__)
+MAX_EDGE_REQUEST_BYTES = 262_144
+MAX_EDGE_REQUEST_NODES = 5_000
+MAX_EDGE_RESPONSE_BYTES = 1_048_576
+
+
+def _service() -> EdgeGatewayService:
+    from app import db
+    from app.core.edge_gateway.repository import EdgeGatewayRepository
+
+    return EdgeGatewayService(
+        EdgeGatewayRepository(db.session),
+        signing_private_key=current_app.config.get(
+            "EDGE_GATEWAY_SIGNING_PRIVATE_KEY"
+        ),
+        signing_private_key_file=current_app.config.get(
+            "EDGE_GATEWAY_SIGNING_PRIVATE_KEY_FILE"
+        ),
+        signing_public_key=current_app.config.get(
+            "EDGE_GATEWAY_SIGNING_PUBLIC_KEY"
+        ),
+        signing_key_id=current_app.config.get("EDGE_GATEWAY_SIGNING_KEY_ID"),
+        production=(
+            str(current_app.config.get("FLASK_ENV", "")).lower() == "production"
+        ),
+    )
+
+
+def _actor_uid():
+    identity = getattr(g, "current_user", {}) or {}
+    return identity.get("id") or identity.get("sub")
+
+
+def _payload() -> dict:
+    if request.content_length is not None and request.content_length > MAX_EDGE_REQUEST_BYTES:
+        raise EdgeGatewayPayloadTooLargeError("edge request is too large")
+    raw = request.get_data(cache=True)
+    if len(raw) > MAX_EDGE_REQUEST_BYTES:
+        raise EdgeGatewayPayloadTooLargeError("edge request is too large")
+    value = request.get_json(silent=True)
+    if value is None and not raw:
+        return {}
+    if not isinstance(value, dict):
+        raise EdgeGatewayValidationError("edge request body is invalid")
+    try:
+        preflight_json(
+            value, max_depth=32, max_nodes=MAX_EDGE_REQUEST_NODES,
+            max_string_bytes=131_072, max_total_bytes=MAX_EDGE_REQUEST_BYTES,
+        )
+    except EdgeContractError as exc:
+        raise EdgeGatewayPayloadTooLargeError("edge request exceeds safe limits") from exc
+    return value
+
+
+def _tls_certificate_sha256() -> str:
+    configured = current_app.config.get("EDGE_MTLS_TRUSTED_PROXY_IPS", ())
+    if isinstance(configured, str):
+        configured = tuple(
+            item.strip() for item in configured.split(",") if item.strip()
+        )
+    if not isinstance(configured, (tuple, list, set, frozenset)) or not configured:
+        raise EdgeGatewayAuthenticationError(
+            "trusted mTLS proxy is not configured"
+        )
+    try:
+        trusted = {str(ipaddress.ip_address(item)) for item in configured}
+        remote = str(ipaddress.ip_address(request.remote_addr or ""))
+    except ValueError as exc:
+        raise EdgeGatewayAuthenticationError(
+            "request did not originate from a trusted mTLS proxy"
+        ) from exc
+    if remote not in trusted:
+        raise EdgeGatewayAuthenticationError(
+            "request did not originate from a trusted mTLS proxy"
+        )
+    if request.headers.get("X-DataOps-Edge-Client-Verify") != "SUCCESS":
+        raise EdgeGatewayAuthenticationError(
+            "verified TLS client certificate is required"
+        )
+    escaped_certificate = request.headers.get("X-DataOps-Edge-Client-Cert", "")
+    if not escaped_certificate or len(escaped_certificate.encode()) > 32_768:
+        raise EdgeGatewayAuthenticationError(
+            "verified TLS client certificate is required"
+        )
+    try:
+        certificate = x509.load_pem_x509_certificate(
+            unquote(escaped_certificate).encode()
+        )
+    except ValueError as exc:
+        raise EdgeGatewayAuthenticationError(
+            "verified TLS client certificate is invalid"
+        ) from exc
+    now = datetime.now(UTC)
+    if (
+        certificate.not_valid_before_utc > now
+        or certificate.not_valid_after_utc <= now
+    ):
+        raise EdgeGatewayAuthenticationError(
+            "verified TLS client certificate is not currently valid"
+        )
+    proof = certificate.fingerprint(hashes.SHA256()).hex()
+    header = request.headers.get("X-Edge-Certificate-SHA256")
+    if header and (
+        not re.fullmatch(r"[0-9a-f]{64}", header)
+        or not hmac.compare_digest(header, proof)
+    ):
+        raise EdgeGatewayAuthenticationError(
+            "TLS client certificate proof does not match"
+        )
+    return proof
+
+
+def _machine_auth(gateway_id: str, payload: dict) -> dict:
+    return {
+        "credential": request.headers.get("X-Edge-Credential", ""),
+        "certificate_sha256": _tls_certificate_sha256(),
+        "gateway_id": gateway_id,
+        "environment": payload.get("environment"),
+        "network_zone": payload.get("network_zone"),
+        "generation": payload.get("generation"),
+    }
+
+
+def _respond(call, created: bool = False):
+    try:
+        result = call()
+        response = jsonify(success(result))
+        if len(response.get_data()) > MAX_EDGE_RESPONSE_BYTES:
+            response = jsonify(failed(
+                "边缘网关响应超过安全上限", code=500,
+                error={"code": "EDGE_GATEWAY_RESPONSE_LIMIT"},
+            ))
+            response.headers["Cache-Control"] = "no-store"
+            return response, 500
+        response.headers["Cache-Control"] = "no-store"
+        return response, 201 if created else 200
+    except EdgeGatewayError as exc:
+        logger.warning("edge gateway request rejected: code=%s", exc.code)
+        response = jsonify(failed("边缘网关请求被拒绝", code=exc.status_code, error={"code": exc.code}))
+        response.headers["Cache-Control"] = "no-store"
+        return response, exc.status_code
+    except Exception:
+        logger.exception("edge gateway request failed")
+        response = jsonify(failed("边缘网关操作失败", code=500, error={"code": "EDGE_GATEWAY_INTERNAL"}))
+        response.headers["Cache-Control"] = "no-store"
+        return response, 500
+
+
+@bp.errorhandler(EdgeGatewayError)
+def edge_gateway_error(error):
+    response = jsonify(failed("边缘网关请求被拒绝", code=error.status_code, error={"code": error.code}))
+    response.headers["Cache-Control"] = "no-store"
+    return response, error.status_code
+
+
+@bp.route("/edge/enrollments", methods=["POST"])
+def edge_create_enrollment():
+    payload = _payload()
+    return _respond(lambda: _service().create_enrollment(
+        gateway_name=payload.get("gateway_name"), environment=payload.get("environment"),
+        network_zone=payload.get("network_zone"), policy_digest=payload.get("policy_digest"),
+        allowed_control_hosts=payload.get("allowed_control_hosts"),
+        allowed_proxy_hosts=payload.get("allowed_proxy_hosts", []),
+        expected_certificate_sha256=payload.get("expected_certificate_sha256"),
+        ttl_seconds=payload.get("ttl_seconds", 600), actor_uid=_actor_uid(),
+    ), created=True)
+
+
+@bp.route("/edge/register", methods=["POST"])
+def edge_register():
+    payload = _payload()
+    return _respond(lambda: _service().register(
+        enrollment_token=request.headers.get("X-Edge-Enrollment", ""),
+        certificate_sha256=_tls_certificate_sha256(),
+        gateway_id=payload.get("gateway_id"), environment=payload.get("environment"),
+        network_zone=payload.get("network_zone"), policy_digest=payload.get("policy_digest"),
+        allowed_control_hosts=payload.get("allowed_control_hosts"),
+        allowed_proxy_hosts=payload.get("allowed_proxy_hosts", []), version=payload.get("version"),
+    ), created=True)
+
+
+@bp.route("/edge/gateways", methods=["GET"])
+def edge_list_gateways():
+    return _respond(lambda: {"gateways": _service().list_gateways(
+        limit=request.args.get("limit", 50), offset=request.args.get("offset", 0),
+    )})
+
+
+@bp.route("/edge/gateways/<gateway_id>/heartbeat", methods=["POST"])
+def edge_heartbeat(gateway_id):
+    payload = _payload()
+    return _respond(lambda: _service().heartbeat(
+        **_machine_auth(gateway_id, payload), version=payload.get("version"),
+        safe_summary=payload.get("safe_summary", {}),
+    ))
+
+
+@bp.route("/edge/gateways/<gateway_id>/rotate", methods=["POST"])
+def edge_rotate(gateway_id):
+    payload = _payload()
+    return _respond(lambda: _service().rotate(
+        gateway_id, certificate_sha256=payload.get("certificate_sha256"), actor_uid=_actor_uid(),
+        request_id=payload.get("request_id"),
+    ))
+
+
+@bp.route("/edge/gateways/<gateway_id>/revoke", methods=["POST"])
+def edge_revoke(gateway_id):
+    return _respond(lambda: {"revoked": _service().revoke(gateway_id, actor_uid=_actor_uid())})
+
+
+@bp.route("/edge/tasks", methods=["POST"])
+def edge_issue_task():
+    return _respond(lambda: _service().issue_task(_payload(), actor_uid=_actor_uid()), created=True)
+
+
+@bp.route("/edge/tasks/<task_id>/cancel", methods=["POST"])
+def edge_cancel_task(task_id):
+    return _respond(lambda: {"cancel_requested": _service().cancel_task(task_id, actor_uid=_actor_uid())})
+
+
+@bp.route("/edge/gateways/<gateway_id>/tasks/pull", methods=["POST"])
+def edge_pull_task(gateway_id):
+    payload = _payload()
+    return _respond(lambda: _service().pull_task(**_machine_auth(gateway_id, payload)))
+
+
+@bp.route("/edge/gateways/<gateway_id>/tasks/<task_id>/outcome", methods=["POST"])
+def edge_task_outcome(gateway_id, task_id):
+    payload = _payload()
+    return _respond(lambda: _service().task_outcome(
+        task_id, outcome=payload.get("outcome"), lease_token=payload.get("lease_token"),
+        safe_summary=payload.get("safe_summary", {}), **_machine_auth(gateway_id, payload),
+    ))
+
+
+@bp.route("/edge/gateways/<gateway_id>/events", methods=["POST"])
+def edge_accept_event(gateway_id):
+    payload = _payload()
+    return _respond(lambda: _service().accept_event(
+        event=payload.get("event"), lease_token=payload.get("lease_token"),
+        **_machine_auth(gateway_id, payload),
+    ))
+
+
+@bp.route("/edge/gateways/<gateway_id>/reconcile", methods=["POST"])
+def edge_reconcile(gateway_id):
+    payload = _payload()
+    return _respond(lambda: _service().reconcile(
+        limit=payload.get("limit", 50),
+        cancel_cursor=payload.get("cancel_cursor"),
+        release_cursor=payload.get("release_cursor"),
+        **_machine_auth(gateway_id, payload),
+    ))
+
+
+@bp.route("/edge/gateways/<gateway_id>/releases", methods=["POST"])
+def edge_offer_release(gateway_id):
+    payload = _payload()
+    return _respond(lambda: _service().offer_release(
+        gateway_id, version=payload.get("version"), artifact_digest=payload.get("artifact_digest"),
+        artifact_name=payload.get("artifact_name"), deadline_at=payload.get("deadline_at"),
+        rollback_version=payload.get("rollback_version"), request_id=payload.get("request_id"), actor_uid=_actor_uid(),
+    ), created=True)
+
+
+@bp.route("/edge/gateways/<gateway_id>/releases/ack", methods=["POST"])
+def edge_ack_release(gateway_id):
+    payload = _payload()
+    return _respond(lambda: _service().acknowledge_release(
+        release_id=payload.get("release_id"), outcome=payload.get("outcome"),
+        safe_summary=payload.get("safe_summary", {}), **_machine_auth(gateway_id, payload),
+    ))

+ 141 - 24
app/config/config.py

@@ -2,6 +2,8 @@ import os
 import platform
 from collections.abc import Mapping
 
+from app.config.database_urls import validate_postgresql_url
+
 
 def get_bool_env(name: str, default: bool = False) -> bool:
     """Return a boolean value from an environment variable."""
@@ -160,7 +162,16 @@ def _apply_minio_config(app, defaults: dict) -> None:
     password = _clean_env("MINIO_PASSWORD")
     bucket = _clean_env("MINIO_BUCKET")
     prefix = _clean_env("MINIO_PREFIX")
-    is_production = os.environ.get("FLASK_ENV", "").lower() == "production"
+    is_production = str(app.config.get("FLASK_ENV", "")).lower() == "production"
+
+    if is_production:
+        app.config["MINIO_HOST"] = host
+        app.config["MINIO_USER"] = user
+        app.config["MINIO_PASSWORD"] = password
+        app.config["MINIO_BUCKET"] = bucket
+        app.config["PREFIX"] = prefix
+        app.config["MINIO_SECURE"] = get_bool_env("MINIO_SECURE", False)
+        return
 
     use_defaults = (
         is_placeholder_env_value(user)
@@ -201,6 +212,32 @@ PRODUCTION_SERVICE_DEFAULTS = {
     "MINIO_PREFIX": "",
 }
 
+_PRODUCTION_REQUIRED_SERVICE_ENV = (
+    "DATABASE_URL",
+    "NEO4J_URI",
+    "NEO4J_HTTP_URI",
+    "NEO4J_USER",
+    "NEO4J_PASSWORD",
+    "MINIO_HOST",
+    "MINIO_USER",
+    "MINIO_PASSWORD",
+    "MINIO_BUCKET",
+)
+
+
+def _validate_production_service_env() -> None:
+    """Reject missing, template, or malformed production service settings."""
+    values = {name: _clean_env(name) for name in _PRODUCTION_REQUIRED_SERVICE_ENV}
+    validate_postgresql_url(values["DATABASE_URL"], "DATABASE_URL")
+    for name, value in values.items():
+        if name == "DATABASE_URL":
+            continue
+        placeholder = is_placeholder_env_value(value)
+        if name in {"NEO4J_URI", "NEO4J_HTTP_URI", "MINIO_HOST"}:
+            placeholder = not value or value.lower().startswith("replace-")
+        if placeholder:
+            raise RuntimeError(f"production {name} is required and cannot be a placeholder")
+
 
 def _apply_config_from_env(app, config_key: str, env_name: str, default):
     value = _clean_env(env_name)
@@ -216,21 +253,35 @@ def apply_runtime_env_config(app) -> None:
     load_production_env_file()
     defaults = PRODUCTION_SERVICE_DEFAULTS
 
-    _apply_config_from_env(
-        app, "SQLALCHEMY_DATABASE_URI", "DATABASE_URL", defaults["SQLALCHEMY_DATABASE_URI"]
-    )
-    _apply_config_from_env(app, "NEO4J_URI", "NEO4J_URI", defaults["NEO4J_URI"])
-    _apply_config_from_env(
-        app, "NEO4J_HTTP_URI", "NEO4J_HTTP_URI", defaults["NEO4J_HTTP_URI"]
-    )
-    _apply_config_from_env(app, "NEO4J_USER", "NEO4J_USER", defaults["NEO4J_USER"])
-    _apply_config_from_env(
-        app, "NEO4J_PASSWORD", "NEO4J_PASSWORD", defaults["NEO4J_PASSWORD"]
-    )
-    app.config["NEO4J_ENCRYPTED"] = get_bool_env(
-        "NEO4J_ENCRYPTED", defaults["NEO4J_ENCRYPTED"]
-    )
-    _apply_minio_config(app, defaults)
+    is_production = str(app.config.get("FLASK_ENV", "")).lower() == "production"
+    if is_production:
+        _validate_production_service_env()
+        app.config["SQLALCHEMY_DATABASE_URI"] = _clean_env("DATABASE_URL")
+        for config_key, env_name in (
+            ("NEO4J_URI", "NEO4J_URI"),
+            ("NEO4J_HTTP_URI", "NEO4J_HTTP_URI"),
+            ("NEO4J_USER", "NEO4J_USER"),
+            ("NEO4J_PASSWORD", "NEO4J_PASSWORD"),
+        ):
+            app.config[config_key] = _clean_env(env_name)
+        app.config["NEO4J_ENCRYPTED"] = get_bool_env("NEO4J_ENCRYPTED", True)
+        _apply_minio_config(app, defaults)
+    else:
+        _apply_config_from_env(
+            app, "SQLALCHEMY_DATABASE_URI", "DATABASE_URL", defaults["SQLALCHEMY_DATABASE_URI"]
+        )
+        _apply_config_from_env(app, "NEO4J_URI", "NEO4J_URI", defaults["NEO4J_URI"])
+        _apply_config_from_env(
+            app, "NEO4J_HTTP_URI", "NEO4J_HTTP_URI", defaults["NEO4J_HTTP_URI"]
+        )
+        _apply_config_from_env(app, "NEO4J_USER", "NEO4J_USER", defaults["NEO4J_USER"])
+        _apply_config_from_env(
+            app, "NEO4J_PASSWORD", "NEO4J_PASSWORD", defaults["NEO4J_PASSWORD"]
+        )
+        app.config["NEO4J_ENCRYPTED"] = get_bool_env(
+            "NEO4J_ENCRYPTED", defaults["NEO4J_ENCRYPTED"]
+        )
+        _apply_minio_config(app, defaults)
 
     secret_key = _clean_env("SECRET_KEY")
     if secret_key and not is_placeholder_env_value(secret_key):
@@ -293,6 +344,55 @@ def apply_runtime_env_config(app) -> None:
     )
 
 
+def validate_production_database_identity(app, engine) -> None:
+    """Verify the connected production login has runtime-only database powers."""
+    if str(app.config.get("FLASK_ENV", "")).lower() != "production":
+        return
+    from sqlalchemy import text
+
+    try:
+        with engine.connect() as connection:
+            identity = connection.execute(text("""
+                SELECT current_user AS role_name,
+                       role.rolsuper,role.rolcreaterole,role.rolcreatedb,
+                       role.rolreplication,role.rolbypassrls,
+                       EXISTS (
+                         SELECT 1 FROM pg_class object
+                         JOIN pg_namespace namespace ON namespace.oid=object.relnamespace
+                         WHERE object.relowner=role.oid
+                           AND namespace.nspname NOT IN ('pg_catalog','information_schema')
+                           AND namespace.nspname NOT LIKE 'pg_toast%'
+                       ) OR EXISTS (
+                         SELECT 1 FROM pg_proc function
+                         JOIN pg_namespace namespace ON namespace.oid=function.pronamespace
+                         WHERE function.proowner=role.oid
+                           AND namespace.nspname NOT IN ('pg_catalog','information_schema')
+                       ) OR EXISTS (
+                         SELECT 1 FROM pg_namespace namespace
+                         WHERE namespace.nspowner=role.oid
+                           AND namespace.nspname NOT IN ('pg_catalog','information_schema')
+                           AND namespace.nspname NOT LIKE 'pg_%'
+                       ) AS owns_application_objects,
+                       EXISTS (
+                         SELECT 1 FROM pg_roles evidence_owner
+                         WHERE evidence_owner.rolname='dataops_edge_evidence_owner'
+                           AND pg_has_role(current_user,evidence_owner.oid,'MEMBER')
+                       ) AS evidence_owner_member
+                  FROM pg_roles role WHERE role.rolname=current_user
+            """)).mappings().one()
+    except Exception as exc:
+        raise RuntimeError("production runtime database identity verification failed") from exc
+    forbidden = (
+        identity["rolsuper"] or identity["rolcreaterole"] or identity["rolcreatedb"]
+        or identity["rolreplication"] or identity["rolbypassrls"]
+        or identity["owns_application_objects"] or identity["evidence_owner_member"]
+    )
+    if forbidden:
+        raise RuntimeError(
+            "production DATABASE_URL must use a non-owner, least-privilege runtime identity"
+        )
+
+
 def log_llm_env_status(app) -> None:
     """Log LLM env load result after logging is configured."""
     deepseek_key = _clean_env("DEEPSEEK_API_KEY") or _clean_env("LLM_API_KEY")
@@ -335,6 +435,8 @@ def log_service_env_status(app) -> None:
         app.config.get("MINIO_HOST"),
         _mask_secret(str(app.config.get("MINIO_USER", ""))),
     )
+    if str(app.config.get("FLASK_ENV", "")).lower() == "production":
+        return
     if is_placeholder_env_value(_clean_env("DATABASE_URL")):
         app.logger.warning(
             "DATABASE_URL 仍为模板占位符,已回退到默认生产库 "
@@ -434,6 +536,23 @@ class BaseConfig:
         "DATASOURCE_CREDENTIAL_KEY_VERSION", "v1"
     )
     AGENT_CREDENTIAL_SECRET = os.environ.get("AGENT_CREDENTIAL_SECRET", "")
+    # Ed25519 raw-private-key hex or PEM. It is loaded only into process memory
+    # and must never be persisted in a task, release, audit, or HTTP response.
+    EDGE_GATEWAY_SIGNING_PRIVATE_KEY = os.environ.get(
+        "EDGE_GATEWAY_SIGNING_PRIVATE_KEY", ""
+    )
+    EDGE_GATEWAY_SIGNING_PRIVATE_KEY_FILE = os.environ.get(
+        "EDGE_GATEWAY_SIGNING_PRIVATE_KEY_FILE", ""
+    )
+    EDGE_GATEWAY_SIGNING_PUBLIC_KEY = os.environ.get(
+        "EDGE_GATEWAY_SIGNING_PUBLIC_KEY", ""
+    )
+    EDGE_GATEWAY_SIGNING_KEY_ID = os.environ.get(
+        "EDGE_GATEWAY_SIGNING_KEY_ID", ""
+    )
+    EDGE_MTLS_TRUSTED_PROXY_IPS = os.environ.get(
+        "EDGE_MTLS_TRUSTED_PROXY_IPS", ""
+    )
     DATASOURCE_CERT_DIR = os.environ.get(
         "DATASOURCE_CERT_DIR",
         "/etc/dataops-platform/datasource-certs",
@@ -555,23 +674,21 @@ class ProductionConfig(BaseConfig):
     PORT = int(os.environ.get("LISTEN_PORT", os.environ.get("PORT", "5500")))
 
     # 生产环境 MinIO 配置
-    MINIO_HOST = os.environ.get("MINIO_HOST", "127.0.0.1:9000")
-    MINIO_USER = os.environ.get("MINIO_USER", "citu-test")
-    MINIO_PASSWORD = os.environ.get("MINIO_PASSWORD", "citu-test")
+    MINIO_HOST = os.environ.get("MINIO_HOST", "")
+    MINIO_USER = os.environ.get("MINIO_USER", "")
+    MINIO_PASSWORD = os.environ.get("MINIO_PASSWORD", "")
     MINIO_SECURE = get_bool_env("MINIO_SECURE", False)
     MINIO_BUCKET = os.environ.get("MINIO_BUCKET", "dataops-bucket")
     PREFIX = os.environ.get("MINIO_PREFIX", "")
 
     # 生产环境 PostgreSQL 配置
-    SQLALCHEMY_DATABASE_URI = os.environ.get(
-        "DATABASE_URL", "postgresql://postgres:postgres@127.0.0.1:5432/dataops"
-    )
+    SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL", "")
 
     # 生产环境 Neo4j 配置
     NEO4J_URI = os.environ.get("NEO4J_URI", "bolt://127.0.0.1:7687")
     NEO4J_HTTP_URI = os.environ.get("NEO4J_HTTP_URI", "http://127.0.0.1:7474")
-    NEO4J_USER = os.environ.get("NEO4J_USER", "neo4j")
-    NEO4J_PASSWORD = os.environ.get("NEO4J_PASSWORD", "Passw0rd")
+    NEO4J_USER = os.environ.get("NEO4J_USER", "")
+    NEO4J_PASSWORD = os.environ.get("NEO4J_PASSWORD", "")
     NEO4J_ENCRYPTED = get_bool_env("NEO4J_ENCRYPTED", False)
 
     # 生产环境文件路径配置

+ 121 - 0
app/config/database_urls.py

@@ -0,0 +1,121 @@
+"""Fail-closed validation for production PostgreSQL identities and templates."""
+
+from __future__ import annotations
+
+import argparse
+import re
+import sys
+from dataclasses import dataclass
+from pathlib import Path
+from urllib.parse import unquote, urlsplit
+
+_BAD_PERCENT_ESCAPE = re.compile(r"%(?![0-9a-fA-F]{2})")
+_PLACEHOLDER_VALUES = {
+    "change-me",
+    "changeme",
+    "database",
+    "database_name",
+    "dbname",
+    "host",
+    "hostname",
+    "pass",
+    "password",
+    "replace",
+    "user",
+    "username",
+}
+
+
+@dataclass(frozen=True)
+class DatabaseUrlParts:
+    username: str
+    password: str
+    host: str
+    database: str
+
+
+def is_template_secret(value: str) -> bool:
+    """Return true only for empty or explicit template values, after decoding."""
+    normalized = unquote(str(value or "")).strip().lower()
+    return (
+        not normalized
+        or normalized in _PLACEHOLDER_VALUES
+        or normalized.startswith(("replace-", "your-"))
+        or "${" in normalized
+        or normalized.startswith("<")
+        or normalized.endswith(">")
+    )
+
+
+def validate_postgresql_url(value: str, field_name: str) -> DatabaseUrlParts:
+    """Parse a PostgreSQL URL and reject empty/template identity components."""
+    raw = str(value or "").strip()
+    if not raw or _BAD_PERCENT_ESCAPE.search(raw):
+        raise RuntimeError(f"{field_name} is missing or malformed")
+    try:
+        parsed = urlsplit(raw)
+        port = parsed.port
+    except (TypeError, ValueError) as exc:
+        raise RuntimeError(f"{field_name} is malformed") from exc
+    if parsed.scheme.split("+", 1)[0].lower() not in {"postgres", "postgresql"}:
+        raise RuntimeError(f"{field_name} must use PostgreSQL")
+    values = {
+        "username": unquote(parsed.username or ""),
+        "password": unquote(parsed.password or ""),
+        "host": unquote(parsed.hostname or ""),
+        "database": unquote(parsed.path.lstrip("/")),
+    }
+    if port is not None and not 1 <= port <= 65535:
+        raise RuntimeError(f"{field_name} has an invalid port")
+    for component, component_value in values.items():
+        if is_template_secret(component_value):
+            raise RuntimeError(f"{field_name} has an invalid {component}")
+    return DatabaseUrlParts(**values)
+
+
+def _read_env_file(path: str | Path) -> dict[str, str]:
+    values: dict[str, str] = {}
+    for raw_line in Path(path).read_text(encoding="utf-8-sig").splitlines():
+        line = raw_line.strip()
+        if not line or line.startswith("#") or "=" not in line:
+            continue
+        name, value = line.split("=", 1)
+        values[name.strip()] = value.strip().strip('"').strip("'")
+    return values
+
+
+def validate_database_environment(values: dict[str, str]) -> None:
+    runtime_parts = None
+    for field_name in (
+        "DB_ROLE_INIT_DATABASE_URL",
+        "MIGRATION_DATABASE_URL",
+        "DATABASE_URL",
+    ):
+        parts = validate_postgresql_url(values.get(field_name, ""), field_name)
+        if field_name == "DATABASE_URL":
+            runtime_parts = parts
+    runtime_password = values.get("DATAOPS_RUNTIME_PASSWORD", "")
+    if is_template_secret(runtime_password):
+        raise RuntimeError("DATAOPS_RUNTIME_PASSWORD is missing or a template value")
+    if runtime_parts is None or runtime_parts.password != runtime_password:
+        raise RuntimeError("DATAOPS_RUNTIME_PASSWORD must match DATABASE_URL")
+
+
+def load_and_validate_database_env(path: str | Path) -> None:
+    validate_database_environment(_read_env_file(path))
+
+
+def main(argv: list[str] | None = None) -> int:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--env-file", required=True)
+    args = parser.parse_args(argv)
+    try:
+        load_and_validate_database_env(args.env_file)
+    except (OSError, RuntimeError) as exc:
+        print(f"database configuration rejected: {exc}", file=sys.stderr)
+        return 2
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())

+ 63 - 0
app/core/edge_gateway/__init__.py

@@ -0,0 +1,63 @@
+"""Enterprise edge gateway contracts, policy, and durable local queue."""
+
+from .contracts import (
+    EVENT_CLASSIFICATIONS,
+    TASK_CLASSIFICATIONS,
+    TASK_OPERATIONS,
+    EdgeContractError,
+    EdgeEventContract,
+    EdgeTaskContract,
+    SignedTaskEnvelope,
+    canonical_json_bytes,
+    canonical_sha256,
+    stable_event_id,
+)
+from .policy import (
+    CONTROL_PLANE_ALLOWED,
+    EDGE_ONLY,
+    RETENTION_DAYS,
+    EdgeEgressPolicy,
+    EdgePolicyError,
+)
+from .queue import (
+    SCHEMA_VERSION,
+    EdgeQueueConflictError,
+    EdgeQueueError,
+    EdgeQueueLeaseError,
+    EdgeQueueSchemaError,
+    LocalArtifact,
+    QueuedEvent,
+    QueuedOutcome,
+    QueuedTask,
+    ReleaseState,
+    SqliteEdgeQueue,
+)
+
+__all__ = [
+    "CONTROL_PLANE_ALLOWED",
+    "EDGE_ONLY",
+    "EVENT_CLASSIFICATIONS",
+    "RETENTION_DAYS",
+    "TASK_CLASSIFICATIONS",
+    "TASK_OPERATIONS",
+    "EdgeContractError",
+    "EdgeEgressPolicy",
+    "EdgeEventContract",
+    "EdgePolicyError",
+    "EdgeQueueConflictError",
+    "EdgeQueueError",
+    "EdgeQueueLeaseError",
+    "EdgeQueueSchemaError",
+    "EdgeTaskContract",
+    "SignedTaskEnvelope",
+    "LocalArtifact",
+    "QueuedEvent",
+    "QueuedOutcome",
+    "QueuedTask",
+    "ReleaseState",
+    "SqliteEdgeQueue",
+    "SCHEMA_VERSION",
+    "canonical_json_bytes",
+    "canonical_sha256",
+    "stable_event_id",
+]

+ 706 - 0
app/core/edge_gateway/contracts.py

@@ -0,0 +1,706 @@
+"""Immutable, fail-closed contracts for the enterprise edge boundary."""
+
+from __future__ import annotations
+
+import hashlib
+import json
+import math
+import re
+from collections.abc import Mapping
+from dataclasses import dataclass
+from dataclasses import field as dataclass_field
+from datetime import UTC, datetime, timedelta
+from decimal import Decimal, InvalidOperation
+from types import MappingProxyType
+from typing import ClassVar
+
+from cryptography.exceptions import InvalidSignature
+from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
+
+
+class EdgeContractError(ValueError):
+    """Raised when an edge contract is malformed or ambiguous."""
+
+
+TASK_OPERATIONS = frozenset(
+    {"collect", "profile", "quality", "lineage", "controlled_query"}
+)
+TASK_CLASSIFICATIONS = frozenset(
+    {
+        "raw",
+        "recent_detail",
+        "restricted",
+        "desensitized_metadata",
+        "statistics",
+        "lineage",
+        "evidence",
+    }
+)
+EVENT_CLASSIFICATIONS = frozenset(
+    {
+        "desensitized_metadata",
+        "statistics",
+        "lineage",
+        "evidence",
+        "health_summary",
+        "diagnostic_summary",
+    }
+)
+CONTRACT_VERSION = 1
+MAX_ATTEMPT = 5
+PREFLIGHT_MAX_DEPTH = 64
+PREFLIGHT_MAX_NODES = 10_000
+PREFLIGHT_MAX_STRING_BYTES = 131_072
+PREFLIGHT_MAX_TOTAL_BYTES = 1_048_576
+
+_IDENTIFIER = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,254}$")
+_SHA256 = re.compile(r"^[0-9a-f]{64}$")
+_ED25519_SIGNATURE = re.compile(r"^[0-9a-f]{128}$")
+_RFC3339 = re.compile(
+    r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|[+-]\d{2}:\d{2})$"
+)
+
+
+def preflight_json(
+    value: object,
+    *,
+    max_depth: int = PREFLIGHT_MAX_DEPTH,
+    max_nodes: int = PREFLIGHT_MAX_NODES,
+    max_string_bytes: int = PREFLIGHT_MAX_STRING_BYTES,
+    max_total_bytes: int = PREFLIGHT_MAX_TOTAL_BYTES,
+) -> None:
+    """Bound a JSON-like graph iteratively before copying or encoding it."""
+
+    nodes = 0
+    estimated_bytes = 0
+    active: set[int] = set()
+    stack: list[tuple[bool, object, int]] = [(False, value, 0)]
+    try:
+        while stack:
+            exiting, current, depth = stack.pop()
+            if exiting:
+                active.remove(id(current))
+                continue
+            if depth > max_depth:
+                raise EdgeContractError("contract exceeds the maximum depth")
+            nodes += 1
+            if nodes > max_nodes:
+                raise EdgeContractError("contract exceeds the maximum node count")
+            if isinstance(current, str):
+                size = len(current.encode("utf-8"))
+                estimated_bytes += size + 16
+                if estimated_bytes > max_total_bytes:
+                    raise EdgeContractError(
+                        "contract exceeds the conservative byte limit"
+                    )
+                if size > max_string_bytes:
+                    raise EdgeContractError("contract string exceeds the byte limit")
+            elif current is None or isinstance(current, bool):
+                estimated_bytes += 8
+            elif isinstance(current, int):
+                if current.bit_length() > 4096:
+                    raise EdgeContractError("contract integer exceeds the byte limit")
+                estimated_bytes += max(8, current.bit_length() // 3 + 4)
+            elif isinstance(current, float):
+                if not math.isfinite(current):
+                    raise EdgeContractError("contract numbers must be finite")
+                estimated_bytes += 32
+            elif isinstance(current, Mapping):
+                identity = id(current)
+                if identity in active:
+                    raise EdgeContractError("contract contains a cycle")
+                active.add(identity)
+                stack.append((True, current, depth))
+                estimated_bytes += 16
+                children: list[object] = []
+                for key, item in current.items():
+                    if not isinstance(key, str):
+                        raise EdgeContractError("contract property names must be strings")
+                    key_size = len(key.encode("utf-8"))
+                    if key_size > max_string_bytes:
+                        raise EdgeContractError("contract key exceeds the byte limit")
+                    estimated_bytes += key_size + 8
+                    children.append(item)
+                stack.extend((False, item, depth + 1) for item in reversed(children))
+            elif isinstance(current, (list, tuple)):
+                identity = id(current)
+                if identity in active:
+                    raise EdgeContractError("contract contains a cycle")
+                active.add(identity)
+                stack.append((True, current, depth))
+                estimated_bytes += 16
+                stack.extend((False, item, depth + 1) for item in reversed(current))
+            else:
+                raise EdgeContractError("contract contains a non-JSON value")
+            if estimated_bytes > max_total_bytes:
+                raise EdgeContractError("contract exceeds the conservative byte limit")
+    except (MemoryError, RecursionError) as exc:
+        raise EdgeContractError("contract exceeds safe structural limits") from exc
+
+
+def _length_prefixed(tag: bytes, content: bytes) -> bytes:
+    return tag + str(len(content)).encode("ascii") + b":" + content
+
+
+def _number_text(value: int | float) -> str:
+    try:
+        number = Decimal(value if isinstance(value, int) else str(value))
+    except (InvalidOperation, ValueError) as exc:
+        raise EdgeContractError("contract number is invalid") from exc
+    if not number.is_finite():
+        raise EdgeContractError("contract numbers must be finite")
+    if number == 0:
+        return "0"
+    normalized = format(number.normalize(), "f")
+    if "." in normalized:
+        normalized = normalized.rstrip("0").rstrip(".")
+    return normalized
+
+
+def _canonical_encode(value: object) -> bytes:
+    if value is None:
+        return b"n"
+    if isinstance(value, bool):
+        return b"b1" if value else b"b0"
+    if isinstance(value, (int, float)):
+        return _length_prefixed(b"d", _number_text(value).encode("ascii"))
+    if isinstance(value, str):
+        return _length_prefixed(b"s", value.encode("utf-8"))
+    if isinstance(value, Mapping):
+        encoded = bytearray(b"m" + str(len(value)).encode("ascii") + b":")
+        for key in sorted(value, key=lambda item: item.encode("utf-8")):
+            key_bytes = key.encode("utf-8")
+            encoded.extend(_length_prefixed(b"k", key_bytes))
+            encoded.extend(_canonical_encode(value[key]))
+        return bytes(encoded)
+    if isinstance(value, (list, tuple)):
+        encoded = bytearray(b"l" + str(len(value)).encode("ascii") + b":")
+        for item in value:
+            encoded.extend(_canonical_encode(item))
+        return bytes(encoded)
+    raise EdgeContractError("contract contains a non-JSON value")
+
+
+def canonical_json_bytes(value: object) -> bytes:
+    """Encode the explicit type-tagged, length-prefixed canonical protocol."""
+
+    preflight_json(value)
+    try:
+        return _canonical_encode(value)
+    except (MemoryError, RecursionError) as exc:
+        raise EdgeContractError("contract exceeds safe encoding limits") from exc
+
+
+def canonical_sha256(value: object) -> str:
+    return hashlib.sha256(canonical_json_bytes(value)).hexdigest()
+
+
+def strict_json_bytes(value: object) -> bytes:
+    """Serialize validated data as strict transport/storage JSON."""
+
+    preflight_json(value)
+    try:
+        return json.dumps(
+            value,
+            sort_keys=True,
+            separators=(",", ":"),
+            ensure_ascii=False,
+            allow_nan=False,
+        ).encode("utf-8")
+    except (MemoryError, RecursionError, TypeError, ValueError) as exc:
+        raise EdgeContractError("contract cannot be serialized as strict JSON") from exc
+
+
+def stable_event_id(identity: Mapping[str, object]) -> str:
+    if not isinstance(identity, Mapping):
+        raise EdgeContractError("event identity must be a mapping")
+    normalized = dict(identity)
+    if "occurred_at" in normalized:
+        normalized["occurred_at"] = _timestamp(
+            normalized["occurred_at"], "occurred_at"
+        )
+    return f"evt_{canonical_sha256(normalized)}"
+
+
+def _strict_mapping(value: object, *, label: str) -> dict[str, object]:
+    if not isinstance(value, Mapping):
+        raise EdgeContractError(f"{label} must be a mapping")
+    preflight_json(value)
+    return dict(value)
+
+
+def _require_exact_properties(
+    value: Mapping[str, object],
+    *,
+    required: frozenset[str],
+    optional: frozenset[str] = frozenset(),
+) -> None:
+    keys = frozenset(value)
+    unknown = keys - required - optional
+    missing = required - keys
+    if unknown:
+        raise EdgeContractError(
+            f"contract has unknown properties: {', '.join(sorted(unknown))}"
+        )
+    if missing:
+        raise EdgeContractError(
+            f"contract is missing properties: {', '.join(sorted(missing))}"
+        )
+
+
+def _text(value: object, label: str, *, maximum: int = 255) -> str:
+    if (
+        not isinstance(value, str)
+        or not value
+        or value.strip() != value
+        or len(value.encode("utf-8")) > maximum
+        or "\x00" in value
+    ):
+        raise EdgeContractError(f"{label} is invalid")
+    return value
+
+
+def _identifier(value: object, label: str) -> str:
+    candidate = _text(value, label)
+    if not _IDENTIFIER.fullmatch(candidate):
+        raise EdgeContractError(f"{label} is invalid")
+    return candidate
+
+
+def _attempt(value: object) -> int:
+    if isinstance(value, bool) or not isinstance(value, int) or not 1 <= value <= 5:
+        raise EdgeContractError("attempt must be between 1 and 5")
+    return value
+
+
+def _version(value: object) -> int:
+    if isinstance(value, bool) or value != CONTRACT_VERSION:
+        raise EdgeContractError("contract_version is not supported")
+    return CONTRACT_VERSION
+
+
+def _timestamp(value: object, label: str) -> str:
+    candidate = _text(value, label, maximum=64)
+    if not _RFC3339.fullmatch(candidate):
+        raise EdgeContractError(f"{label} must be an RFC3339 timestamp")
+    try:
+        parsed = datetime.fromisoformat(candidate.replace("Z", "+00:00"))
+    except ValueError as exc:
+        raise EdgeContractError(f"{label} must be an RFC3339 timestamp") from exc
+    if parsed.tzinfo is None or parsed.utcoffset() is None:
+        raise EdgeContractError(f"{label} must be an RFC3339 timestamp")
+    utc = parsed.astimezone(UTC)
+    canonical = utc.strftime("%Y-%m-%dT%H:%M:%S")
+    if utc.microsecond:
+        canonical += f".{utc.microsecond:06d}".rstrip("0")
+    return f"{canonical}Z"
+
+
+def canonical_timestamp(value: object, label: str = "timestamp") -> str:
+    return _timestamp(value, label)
+
+
+def _digest(value: object, label: str) -> str:
+    candidate = _text(value, label, maximum=64)
+    if not _SHA256.fullmatch(candidate):
+        raise EdgeContractError(f"{label} must be a lowercase SHA256 digest")
+    return candidate
+
+
+def _json_copy(value: object) -> object:
+    try:
+        encoded = strict_json_bytes(value)
+        return json.loads(encoded.decode("utf-8"))
+    except (MemoryError, RecursionError, TypeError, ValueError) as exc:
+        raise EdgeContractError("contract cannot be copied as strict JSON") from exc
+
+
+def _deep_freeze(value: object) -> object:
+    if isinstance(value, dict):
+        return MappingProxyType({key: _deep_freeze(item) for key, item in value.items()})
+    if isinstance(value, list):
+        return tuple(_deep_freeze(item) for item in value)
+    return value
+
+
+def _deep_thaw(value: object) -> object:
+    if isinstance(value, Mapping):
+        return {key: _deep_thaw(item) for key, item in value.items()}
+    if isinstance(value, tuple):
+        return [_deep_thaw(item) for item in value]
+    return value
+
+
+@dataclass(frozen=True, slots=True)
+class EdgeTaskContract:
+    task_id: str
+    gateway_id: str
+    environment: str
+    network_zone: str
+    purpose: str
+    classification: str
+    operation: str
+    contract_version: int
+    deadline_at: str
+    attempt: int
+    idempotency_key: str
+    policy_digest: str
+
+    def __post_init__(self) -> None:
+        values = {
+            "task_id": _identifier(self.task_id, "task_id"),
+            "gateway_id": _identifier(self.gateway_id, "gateway_id"),
+            "environment": _identifier(self.environment, "environment"),
+            "network_zone": _identifier(self.network_zone, "network_zone"),
+            "purpose": _identifier(self.purpose, "purpose"),
+            "contract_version": _version(self.contract_version),
+            "deadline_at": _timestamp(self.deadline_at, "deadline_at"),
+            "attempt": _attempt(self.attempt),
+            "idempotency_key": _identifier(self.idempotency_key, "idempotency_key"),
+            "policy_digest": _digest(self.policy_digest, "policy_digest"),
+        }
+        if self.operation not in TASK_OPERATIONS:
+            raise EdgeContractError("task operation is not approved")
+        if self.classification not in TASK_CLASSIFICATIONS:
+            raise EdgeContractError("task classification is invalid")
+        for field, value in values.items():
+            object.__setattr__(self, field, value)
+
+    @property
+    def task_type(self) -> str:
+        return self.operation
+
+    @classmethod
+    def from_mapping(cls, value: Mapping[str, object]) -> EdgeTaskContract:
+        data = _strict_mapping(value, label="task contract")
+        required = frozenset(
+            {
+                "task_id",
+                "gateway_id",
+                "environment",
+                "network_zone",
+                "purpose",
+                "classification",
+                "contract_version",
+                "deadline_at",
+                "attempt",
+                "idempotency_key",
+                "policy_digest",
+            }
+        )
+        _require_exact_properties(
+            data,
+            required=required,
+            optional=frozenset({"task_type", "operation"}),
+        )
+        if ("task_type" in data) == ("operation" in data):
+            raise EdgeContractError(
+                "task contract requires exactly one operation property"
+            )
+        return cls(
+            task_id=data["task_id"],  # type: ignore[arg-type]
+            gateway_id=data["gateway_id"],  # type: ignore[arg-type]
+            environment=data["environment"],  # type: ignore[arg-type]
+            network_zone=data["network_zone"],  # type: ignore[arg-type]
+            purpose=data["purpose"],  # type: ignore[arg-type]
+            classification=data["classification"],  # type: ignore[arg-type]
+            operation=data.get("task_type", data.get("operation")),  # type: ignore[arg-type]
+            contract_version=data["contract_version"],  # type: ignore[arg-type]
+            deadline_at=data["deadline_at"],  # type: ignore[arg-type]
+            attempt=data["attempt"],  # type: ignore[arg-type]
+            idempotency_key=data["idempotency_key"],  # type: ignore[arg-type]
+            policy_digest=data["policy_digest"],  # type: ignore[arg-type]
+        )
+
+    def to_mapping(self) -> dict[str, object]:
+        return {
+            "task_id": self.task_id,
+            "gateway_id": self.gateway_id,
+            "environment": self.environment,
+            "network_zone": self.network_zone,
+            "purpose": self.purpose,
+            "classification": self.classification,
+            "task_type": self.operation,
+            "contract_version": self.contract_version,
+            "deadline_at": self.deadline_at,
+            "attempt": self.attempt,
+            "idempotency_key": self.idempotency_key,
+            "policy_digest": self.policy_digest,
+        }
+
+    @property
+    def digest(self) -> str:
+        return canonical_sha256(self.to_mapping())
+
+
+@dataclass(frozen=True, slots=True)
+class SignedTaskEnvelope:
+    """Control-plane authority over an exact, context-bound task contract."""
+
+    task: EdgeTaskContract
+    authority_key_id: str
+    signature_algorithm: str
+    contract_digest: str
+    gateway_id: str
+    environment: str
+    network_zone: str
+    policy_digest: str
+    purpose: str
+    issued_at: str
+    expires_at: str
+    signature: str
+    _verified: bool = dataclass_field(default=False, init=False, repr=False, compare=False)
+
+    _UNSIGNED_FIELDS: ClassVar[frozenset[str]] = frozenset(
+        {
+            "task", "authority_key_id", "signature_algorithm", "contract_digest",
+            "gateway_id", "environment", "network_zone", "policy_digest", "purpose",
+            "issued_at", "expires_at",
+        }
+    )
+
+    def __post_init__(self) -> None:
+        if not isinstance(self.task, EdgeTaskContract):
+            raise EdgeContractError("signed task envelope requires a task contract")
+        normalized_task = EdgeTaskContract.from_mapping(self.task.to_mapping())
+        unsigned = {
+            "task": normalized_task.to_mapping(),
+            "authority_key_id": self.authority_key_id,
+            "signature_algorithm": self.signature_algorithm,
+            "contract_digest": self.contract_digest,
+            "gateway_id": self.gateway_id,
+            "environment": self.environment,
+            "network_zone": self.network_zone,
+            "policy_digest": self.policy_digest,
+            "purpose": self.purpose,
+            "issued_at": self.issued_at,
+            "expires_at": self.expires_at,
+        }
+        self.canonical_unsigned_bytes(unsigned)
+        if not isinstance(self.signature, str) or not _ED25519_SIGNATURE.fullmatch(self.signature):
+            raise EdgeContractError("signature must be a lowercase Ed25519 signature")
+        object.__setattr__(self, "task", normalized_task)
+
+    @classmethod
+    def canonical_unsigned_bytes(cls, value: Mapping[str, object]) -> bytes:
+        data = _strict_mapping(value, label="signed task envelope")
+        _require_exact_properties(data, required=cls._UNSIGNED_FIELDS)
+        task_raw = data["task"]
+        if isinstance(task_raw, EdgeTaskContract):
+            task_raw = task_raw.to_mapping()
+        task = EdgeTaskContract.from_mapping(task_raw)  # type: ignore[arg-type]
+        normalized = {
+            "task": task.to_mapping(),
+            "authority_key_id": _identifier(data["authority_key_id"], "authority_key_id"),
+            "signature_algorithm": data["signature_algorithm"],
+            "contract_digest": _digest(data["contract_digest"], "contract_digest"),
+            "gateway_id": _identifier(data["gateway_id"], "gateway_id"),
+            "environment": _identifier(data["environment"], "environment"),
+            "network_zone": _identifier(data["network_zone"], "network_zone"),
+            "policy_digest": _digest(data["policy_digest"], "policy_digest"),
+            "purpose": _identifier(data["purpose"], "purpose"),
+            "issued_at": _timestamp(data["issued_at"], "issued_at"),
+            "expires_at": _timestamp(data["expires_at"], "expires_at"),
+        }
+        if normalized["signature_algorithm"] != "Ed25519":
+            raise EdgeContractError("signature_algorithm is not supported")
+        for field in ("gateway_id", "environment", "network_zone", "policy_digest", "purpose"):
+            if normalized[field] != task.to_mapping()[field]:
+                raise EdgeContractError(f"signed authority {field} does not bind the task")
+        if normalized["contract_digest"] != task.digest:
+            raise EdgeContractError("contract_digest does not bind the task")
+        issued = datetime.fromisoformat(str(normalized["issued_at"]).replace("Z", "+00:00"))
+        expires = datetime.fromisoformat(str(normalized["expires_at"]).replace("Z", "+00:00"))
+        deadline = datetime.fromisoformat(task.deadline_at.replace("Z", "+00:00"))
+        if expires <= issued or expires > deadline:
+            raise EdgeContractError("signed authority validity window is invalid")
+        return canonical_json_bytes(normalized)
+
+    @classmethod
+    def verify_mapping(
+        cls,
+        value: Mapping[str, object],
+        *,
+        authority_keys: Mapping[str, bytes | Ed25519PublicKey],
+        now: datetime,
+        allowed_future_skew_seconds: int = 0,
+    ) -> SignedTaskEnvelope:
+        data = _strict_mapping(value, label="signed task envelope")
+        _require_exact_properties(data, required=cls._UNSIGNED_FIELDS | {"signature"})
+        if not isinstance(now, datetime) or now.tzinfo is None or now.utcoffset() is None:
+            raise EdgeContractError("verification time must be timezone-aware")
+        if (
+            isinstance(allowed_future_skew_seconds, bool)
+            or not isinstance(allowed_future_skew_seconds, int)
+            or not 0 <= allowed_future_skew_seconds <= 300
+        ):
+            raise EdgeContractError("task authority clock skew is invalid")
+        signature = _text(data["signature"], "signature", maximum=128)
+        if not _ED25519_SIGNATURE.fullmatch(signature):
+            raise EdgeContractError("signature must be a lowercase Ed25519 signature")
+        unsigned = {key: data[key] for key in cls._UNSIGNED_FIELDS}
+        signed_bytes = cls.canonical_unsigned_bytes(unsigned)
+        key_id = _identifier(data["authority_key_id"], "authority_key_id")
+        key = authority_keys.get(key_id)
+        if key is None:
+            raise EdgeContractError("task authority key is not trusted")
+        try:
+            public_key = (
+                key if isinstance(key, Ed25519PublicKey)
+                else Ed25519PublicKey.from_public_bytes(bytes(key))
+            )
+            public_key.verify(bytes.fromhex(signature), signed_bytes)
+        except (InvalidSignature, TypeError, ValueError) as exc:
+            raise EdgeContractError("task authority signature is invalid") from exc
+        issued_at = _timestamp(data["issued_at"], "issued_at")
+        expires_at = _timestamp(data["expires_at"], "expires_at")
+        instant = now.astimezone(UTC)
+        issued = datetime.fromisoformat(issued_at.replace("Z", "+00:00"))
+        expires = datetime.fromisoformat(expires_at.replace("Z", "+00:00"))
+        if instant + timedelta(seconds=allowed_future_skew_seconds) < issued:
+            raise EdgeContractError("signed task is not yet valid")
+        if instant >= expires:
+            raise EdgeContractError("signed task authority has expired")
+        task = EdgeTaskContract.from_mapping(data["task"])  # type: ignore[arg-type]
+        envelope = cls(
+            task=task,
+            authority_key_id=key_id,
+            signature_algorithm="Ed25519",
+            contract_digest=task.digest,
+            gateway_id=task.gateway_id,
+            environment=task.environment,
+            network_zone=task.network_zone,
+            policy_digest=task.policy_digest,
+            purpose=task.purpose,
+            issued_at=issued_at,
+            expires_at=expires_at,
+            signature=signature,
+        )
+        object.__setattr__(envelope, "_verified", True)
+        return envelope
+
+    @property
+    def is_verified(self) -> bool:
+        return self._verified
+
+    def to_mapping(self) -> dict[str, object]:
+        return {
+            "task": self.task.to_mapping(),
+            "authority_key_id": self.authority_key_id,
+            "signature_algorithm": self.signature_algorithm,
+            "contract_digest": self.contract_digest,
+            "gateway_id": self.gateway_id,
+            "environment": self.environment,
+            "network_zone": self.network_zone,
+            "policy_digest": self.policy_digest,
+            "purpose": self.purpose,
+            "issued_at": self.issued_at,
+            "expires_at": self.expires_at,
+            "signature": self.signature,
+        }
+
+    @property
+    def digest(self) -> str:
+        return canonical_sha256(self.to_mapping())
+
+
+@dataclass(frozen=True, slots=True)
+class EdgeEventContract:
+    event_id: str
+    task_id: str
+    gateway_id: str
+    environment: str
+    network_zone: str
+    purpose: str
+    classification: str
+    contract_version: int
+    occurred_at: str
+    attempt: int
+    idempotency_key: str
+    policy_digest: str
+    payload: Mapping[str, object]
+
+    def __post_init__(self) -> None:
+        payload = _strict_mapping(self.payload, label="event payload")
+        payload_copy = _json_copy(payload)
+        if not isinstance(payload_copy, dict):
+            raise EdgeContractError("event payload must be a mapping")
+        values = {
+            "task_id": _identifier(self.task_id, "task_id"),
+            "gateway_id": _identifier(self.gateway_id, "gateway_id"),
+            "environment": _identifier(self.environment, "environment"),
+            "network_zone": _identifier(self.network_zone, "network_zone"),
+            "purpose": _identifier(self.purpose, "purpose"),
+            "contract_version": _version(self.contract_version),
+            "occurred_at": _timestamp(self.occurred_at, "occurred_at"),
+            "attempt": _attempt(self.attempt),
+            "idempotency_key": _identifier(self.idempotency_key, "idempotency_key"),
+            "policy_digest": _digest(self.policy_digest, "policy_digest"),
+        }
+        if self.classification not in EVENT_CLASSIFICATIONS:
+            raise EdgeContractError("event classification is not approved")
+        identity = {
+            "task_id": values["task_id"],
+            "gateway_id": values["gateway_id"],
+            "environment": values["environment"],
+            "network_zone": values["network_zone"],
+            "purpose": values["purpose"],
+            "classification": self.classification,
+            "contract_version": values["contract_version"],
+            "occurred_at": values["occurred_at"],
+            "attempt": values["attempt"],
+            "idempotency_key": values["idempotency_key"],
+            "policy_digest": values["policy_digest"],
+            "payload": payload_copy,
+        }
+        expected_id = stable_event_id(identity)
+        if self.event_id != expected_id:
+            raise EdgeContractError("event_id is not the stable event identifier")
+        object.__setattr__(self, "event_id", expected_id)
+        object.__setattr__(self, "payload", _deep_freeze(payload_copy))
+        for field, value in values.items():
+            object.__setattr__(self, field, value)
+
+    @classmethod
+    def from_mapping(cls, value: Mapping[str, object]) -> EdgeEventContract:
+        data = _strict_mapping(value, label="event contract")
+        required = frozenset(
+            {
+                "event_id",
+                "task_id",
+                "gateway_id",
+                "environment",
+                "network_zone",
+                "purpose",
+                "classification",
+                "contract_version",
+                "occurred_at",
+                "attempt",
+                "idempotency_key",
+                "policy_digest",
+                "payload",
+            }
+        )
+        _require_exact_properties(data, required=required)
+        return cls(**data)  # type: ignore[arg-type]
+
+    def to_mapping(self) -> dict[str, object]:
+        return {
+            "event_id": self.event_id,
+            "task_id": self.task_id,
+            "gateway_id": self.gateway_id,
+            "environment": self.environment,
+            "network_zone": self.network_zone,
+            "purpose": self.purpose,
+            "classification": self.classification,
+            "contract_version": self.contract_version,
+            "occurred_at": self.occurred_at,
+            "attempt": self.attempt,
+            "idempotency_key": self.idempotency_key,
+            "policy_digest": self.policy_digest,
+            "payload": _deep_thaw(self.payload),
+        }
+
+    @property
+    def digest(self) -> str:
+        return canonical_sha256(self.to_mapping())

+ 475 - 0
app/core/edge_gateway/policy.py

@@ -0,0 +1,475 @@
+"""Default-deny egress and retention policy for enterprise edge events."""
+
+from __future__ import annotations
+
+import ipaddress
+import re
+import unicodedata
+from collections.abc import Mapping
+from urllib.parse import urlsplit
+
+from .contracts import EdgeContractError, preflight_json, strict_json_bytes
+
+
+class EdgePolicyError(ValueError):
+    """Raised when content or transport violates the edge boundary."""
+
+
+EDGE_ONLY = {"raw", "recent_detail", "restricted"}
+CONTROL_PLANE_ALLOWED = {
+    "desensitized_metadata",
+    "statistics",
+    "lineage",
+    "evidence",
+    "health_summary",
+    "diagnostic_summary",
+}
+RETENTION_DAYS = {
+    "raw": 0,
+    "recent_detail": 365,
+    "metadata": 1095,
+    "evidence": 2190,
+}
+
+DEFAULT_BYTE_LIMITS = {
+    "desensitized_metadata": 65_536,
+    "statistics": 32_768,
+    "lineage": 65_536,
+    "evidence": 131_072,
+    "health_summary": 8_192,
+    "diagnostic_summary": 16_384,
+}
+DEFAULT_COUNT_LIMITS = {
+    "desensitized_metadata": 2_000,
+    "statistics": 1_000,
+    "lineage": 2_000,
+    "evidence": 4_000,
+    "health_summary": 256,
+    "diagnostic_summary": 512,
+}
+
+_SENSITIVE_KEYS = {
+    "api_key",
+    "apikey",
+    "authorization",
+    "access_token",
+    "cookie",
+    "credential",
+    "credentials",
+    "password",
+    "passwd",
+    "private_key",
+    "query",
+    "query_text",
+    "raw_record",
+    "raw_records",
+    "raw_row",
+    "raw_rows",
+    "secret",
+    "sql",
+    "sql_text",
+    "statement",
+    "token",
+}
+_SENSITIVE_KEY_PARTS = ("password", "passwd", "secret", "private_key", "api_key")
+_RAW_ROW_CONTAINER_KEYS = {
+    "data",
+    "items",
+    "item",
+    "record",
+    "records",
+    "result",
+    "row",
+    "rows",
+    "sample",
+    "samples",
+}
+_PRIVATE_KEY = re.compile(r"-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----", re.IGNORECASE)
+_BEARER = re.compile(r"\bBearer\s+[A-Za-z0-9._~+/=-]{8,}", re.IGNORECASE)
+_CREDENTIAL_ASSIGNMENT = re.compile(
+    r"\b(?:[a-z0-9]+[_-])*(?:password|passwd|pwd|secret|token|api[_-]?key|"
+    r"authorization|credential)"
+    r"\b\s*(?:=|:)\s*[^\s,;]+",
+    re.IGNORECASE,
+)
+_SQL = re.compile(
+    r"(?:"
+    r"\bSELECT\b.{0,4096}?\bFROM\b"
+    r"|\bINSERT\s+INTO\b"
+    r"|\bUPDATE\b.{0,4096}?\bSET\b"
+    r"|\bDELETE\s+FROM\b"
+    r"|\bMERGE(?:\s+INTO)?\b"
+    r"|\b(?:CREATE|ALTER|DROP|TRUNCATE)\s+"
+    r"(?:TABLE|VIEW|SCHEMA|DATABASE|INDEX|FUNCTION|PROCEDURE)\b"
+    r"|\b(?:GRANT|REVOKE)\b.{0,4096}?\b(?:TO|FROM)\b"
+    r"|\bCOPY\b"
+    r")",
+    re.IGNORECASE | re.DOTALL,
+)
+
+
+def _normalized_key(value: str) -> str:
+    compatible = unicodedata.normalize("NFKC", value)
+    words = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", compatible)
+    return re.sub(r"[^a-z0-9]+", "_", words.lower()).strip("_")
+
+
+def _is_sensitive_key(value: str) -> bool:
+    folded = value.replace("_", "")
+    return (
+        value in _SENSITIVE_KEYS
+        or folded
+        in {
+            "accesstoken",
+            "apikey",
+            "authorization",
+            "clientsecret",
+            "credential",
+            "credentials",
+            "password",
+            "passwd",
+            "privatekey",
+            "rawrecords",
+            "rawrows",
+            "secret",
+            "token",
+        }
+        or any(part in value for part in _SENSITIVE_KEY_PARTS)
+        or value.startswith(("raw_", "recent_detail_", "token_"))
+        or value.endswith("_token")
+    )
+
+
+def _is_raw_row_container(key: str, value: object) -> bool:
+    return (
+        key in _RAW_ROW_CONTAINER_KEYS
+        or key.endswith(("_rows", "_records"))
+    ) and (
+        isinstance(value, Mapping)
+        or (
+            isinstance(value, (list, tuple))
+            and any(isinstance(child, Mapping) for child in value)
+        )
+    )
+
+
+def _contains_obfuscated_sql(value: str) -> bool:
+    compatible = unicodedata.normalize("NFKC", value)
+    without_comments = re.sub(
+        r"/\*.*?\*/|--[^\r\n]*", "", compatible, flags=re.DOTALL
+    )
+    normalized = re.sub(r"\s+", " ", without_comments).upper()
+    return bool(
+        re.search(
+            r"(?:\bSELECT\b.{0,4096}\bFROM\b|\bINSERT\s+INTO\b|"
+            r"\bUPDATE\b.{0,4096}\bSET\b|\bDELETE\s+FROM\b|"
+            r"\bMERGE(?:\s+INTO)?\b|\b(?:CREATE|ALTER|DROP|TRUNCATE)\s+"
+            r"(?:TABLE|VIEW|SCHEMA|DATABASE|INDEX|FUNCTION|PROCEDURE)\b|"
+            r"\b(?:GRANT|REVOKE)\b.{0,4096}\b(?:TO|FROM)\b|\bCOPY\b|"
+            r"\bEXEC(?:UTE)?\b\s+[A-Z_\[][A-Z0-9_.$\[\]]*|"
+            r"\bCALL\b\s+[A-Z_][A-Z0-9_.$]*\s*\()",
+            normalized,
+        )
+    )
+
+
+def _approved_hosts(values: set[str] | frozenset[str], label: str) -> frozenset[str]:
+    if not isinstance(values, (set, frozenset)):
+        raise ValueError(f"{label} must be an explicit set")
+    approved: set[str] = set()
+    for value in values:
+        if (
+            not isinstance(value, str)
+            or not value
+            or value != value.lower()
+            or value.endswith(".")
+            or ":" in value
+            or "/" in value
+            or "@" in value
+        ):
+            raise ValueError(f"{label} contains an invalid host")
+        try:
+            value.encode("ascii")
+            ipaddress.ip_address(value)
+        except UnicodeEncodeError as exc:
+            raise ValueError(f"{label} contains an invalid host") from exc
+        except ValueError:
+            pass
+        else:
+            raise ValueError(f"{label} cannot contain an IP literal")
+        if not re.fullmatch(
+            r"(?=.{1,253}\Z)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*"
+            r"[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?",
+            value,
+        ):
+            raise ValueError(f"{label} contains an invalid host")
+        approved.add(value)
+    return frozenset(approved)
+
+
+def _approved_origins(
+    values: set[str] | frozenset[str] | None,
+    hosts: frozenset[str],
+    label: str,
+) -> frozenset[str]:
+    if values is None:
+        return frozenset(f"https://{host}" for host in hosts)
+    if not isinstance(values, (set, frozenset)):
+        raise ValueError(f"{label} must be an explicit set")
+    origins: set[str] = set()
+    for value in values:
+        if not isinstance(value, str):
+            raise ValueError(f"{label} contains an invalid origin")
+        try:
+            parsed = urlsplit(value)
+            port = parsed.port
+        except ValueError as exc:
+            raise ValueError(f"{label} contains an invalid origin") from exc
+        if (
+            parsed.scheme != "https"
+            or parsed.hostname not in hosts
+            or parsed.username is not None
+            or parsed.password is not None
+            or parsed.path not in {"", "/"}
+            or parsed.query
+            or parsed.fragment
+            or port is None and parsed.netloc.endswith(":")
+        ):
+            raise ValueError(f"{label} contains an invalid origin")
+        effective_port = port or 443
+        canonical = (
+            f"https://{parsed.hostname}"
+            if effective_port == 443
+            else f"https://{parsed.hostname}:{effective_port}"
+        )
+        if value.rstrip("/") != canonical:
+            raise ValueError(f"{label} contains a non-canonical origin")
+        origins.add(canonical)
+    return frozenset(origins)
+
+
+def _limit_map(
+    defaults: Mapping[str, int], overrides: Mapping[str, int] | None, label: str
+) -> dict[str, int]:
+    result = dict(defaults)
+    if overrides is None:
+        return result
+    if not isinstance(overrides, Mapping):
+        raise ValueError(f"{label} must be a mapping")
+    unknown = set(overrides) - CONTROL_PLANE_ALLOWED
+    if unknown:
+        raise ValueError(f"{label} contains an unknown classification")
+    for classification, limit in overrides.items():
+        if isinstance(limit, bool) or not isinstance(limit, int) or limit < 1:
+            raise ValueError(f"{label} values must be positive integers")
+        result[classification] = limit
+    return result
+
+
+class EdgeEgressPolicy:
+    def __init__(
+        self,
+        *,
+        allowed_control_hosts: set[str] | frozenset[str],
+        allowed_proxy_hosts: set[str] | frozenset[str] | None = None,
+        allowed_control_origins: set[str] | frozenset[str] | None = None,
+        allowed_proxy_origins: set[str] | frozenset[str] | None = None,
+        byte_limits: Mapping[str, int] | None = None,
+        count_limits: Mapping[str, int] | None = None,
+        max_depth: int = 12,
+        max_string_bytes: int = 4_096,
+    ) -> None:
+        self.allowed_control_hosts = _approved_hosts(
+            allowed_control_hosts, "allowed_control_hosts"
+        )
+        self.allowed_proxy_hosts = _approved_hosts(
+            allowed_proxy_hosts or set(), "allowed_proxy_hosts"
+        )
+        self.allowed_control_origins = _approved_origins(
+            allowed_control_origins,
+            self.allowed_control_hosts,
+            "allowed_control_origins",
+        )
+        self.allowed_proxy_origins = _approved_origins(
+            allowed_proxy_origins,
+            self.allowed_proxy_hosts,
+            "allowed_proxy_origins",
+        )
+        self.byte_limits = _limit_map(
+            DEFAULT_BYTE_LIMITS, byte_limits, "byte_limits"
+        )
+        self.count_limits = _limit_map(
+            DEFAULT_COUNT_LIMITS, count_limits, "count_limits"
+        )
+        if isinstance(max_depth, bool) or not isinstance(max_depth, int) or max_depth < 1:
+            raise ValueError("max_depth must be a positive integer")
+        if (
+            isinstance(max_string_bytes, bool)
+            or not isinstance(max_string_bytes, int)
+            or max_string_bytes < 1
+        ):
+            raise ValueError("max_string_bytes must be a positive integer")
+        self.max_depth = max_depth
+        self.max_string_bytes = max_string_bytes
+
+    def approve_event(self, event: Mapping[str, object]) -> dict[str, object]:
+        if not isinstance(event, Mapping):
+            raise EdgePolicyError("event must be a mapping")
+        unknown = set(event) - {
+            "event_id",
+            "task_id",
+            "gateway_id",
+            "environment",
+            "network_zone",
+            "purpose",
+            "classification",
+            "contract_version",
+            "occurred_at",
+            "attempt",
+            "idempotency_key",
+            "policy_digest",
+            "payload",
+        }
+        if unknown:
+            raise EdgePolicyError("event contains unapproved properties")
+        classification = event.get("classification")
+        if classification in EDGE_ONLY:
+            raise EdgePolicyError("classification is edge-only")
+        if classification not in CONTROL_PLANE_ALLOWED:
+            raise EdgePolicyError("classification is not control-plane approved")
+        payload = event.get("payload")
+        if not isinstance(payload, Mapping):
+            raise EdgePolicyError("approved event payload must be a mapping")
+        self.validate_approved_payload(str(classification), payload)
+        try:
+            return dict(event)
+        except (TypeError, ValueError) as exc:  # pragma: no cover - Mapping guard
+            raise EdgePolicyError("event is invalid") from exc
+
+    def validate_approved_payload(
+        self, classification: str, payload: Mapping[str, object]
+    ) -> None:
+        if classification not in CONTROL_PLANE_ALLOWED:
+            raise EdgePolicyError("classification is not control-plane approved")
+        try:
+            preflight_json(
+                payload,
+                max_depth=self.max_depth,
+                max_nodes=self.count_limits[classification] + 1,
+                max_string_bytes=self.max_string_bytes,
+                max_total_bytes=self.byte_limits[classification],
+            )
+            encoded = strict_json_bytes(payload)
+        except EdgeContractError as exc:
+            detail = str(exc)
+            if "node count" in detail:
+                detail = "payload exceeds its independent item count limit"
+            elif "string exceeds" in detail:
+                detail = "payload string exceeds the string limit"
+            elif "depth" in detail:
+                detail = "payload exceeds the maximum depth"
+            elif "byte limit" in detail:
+                detail = "payload exceeds its independent byte limit"
+            raise EdgePolicyError(detail) from exc
+        if len(encoded) > self.byte_limits[classification]:
+            raise EdgePolicyError("payload exceeds its independent byte limit")
+        item_count = self._count_items(payload)
+        if item_count > self.count_limits[classification]:
+            raise EdgePolicyError("payload exceeds its independent item count limit")
+        self._inspect(payload, depth=1)
+
+    def _count_items(self, value: object) -> int:
+        if isinstance(value, Mapping):
+            return len(value) + sum(self._count_items(item) for item in value.values())
+        if isinstance(value, (list, tuple)):
+            return len(value) + sum(self._count_items(item) for item in value)
+        return 0
+
+    def _inspect(self, value: object, *, depth: int) -> None:
+        if depth > self.max_depth:
+            raise EdgePolicyError("payload exceeds the maximum depth")
+        if isinstance(value, Mapping):
+            for key, item in value.items():
+                if not isinstance(key, str):
+                    raise EdgePolicyError("payload contains a non-string key")
+                normalized = _normalized_key(key)
+                if _is_sensitive_key(normalized):
+                    raise EdgePolicyError("payload contains sensitive content")
+                if _is_raw_row_container(normalized, item):
+                    raise EdgePolicyError("payload contains sensitive raw-row content")
+                self._inspect(item, depth=depth + 1)
+            return
+        if isinstance(value, (list, tuple)):
+            for item in value:
+                self._inspect(item, depth=depth + 1)
+            return
+        if isinstance(value, str):
+            compatible = unicodedata.normalize("NFKC", value)
+            if len(value.encode("utf-8")) > self.max_string_bytes:
+                raise EdgePolicyError("payload string exceeds the string limit")
+            if (
+                _PRIVATE_KEY.search(compatible)
+                or _BEARER.search(compatible)
+                or _CREDENTIAL_ASSIGNMENT.search(compatible)
+                or _SQL.search(compatible)
+                or _contains_obfuscated_sql(compatible)
+            ):
+                raise EdgePolicyError("payload contains sensitive content")
+
+    def validate_destination(
+        self, control_url: str, *, proxy_url: str | None = None
+    ) -> None:
+        self._validate_url(
+            control_url,
+            approved_hosts=self.allowed_control_hosts,
+            approved_origins=self.allowed_control_origins,
+            label="destination",
+        )
+        if proxy_url is not None:
+            self._validate_url(
+                proxy_url,
+                approved_hosts=self.allowed_proxy_hosts,
+                approved_origins=self.allowed_proxy_origins,
+                label="proxy",
+            )
+
+    @staticmethod
+    def _validate_url(
+        url: str,
+        *,
+        approved_hosts: frozenset[str],
+        approved_origins: frozenset[str],
+        label: str,
+    ) -> None:
+        if not isinstance(url, str):
+            raise EdgePolicyError(f"{label} is not approved")
+        try:
+            parsed = urlsplit(url)
+            port = parsed.port
+        except ValueError as exc:
+            raise EdgePolicyError(f"{label} is not approved") from exc
+        if (
+            parsed.scheme != "https"
+            or not parsed.hostname
+            or parsed.hostname not in approved_hosts
+            or parsed.username is not None
+            or parsed.password is not None
+            or parsed.fragment
+            or port is None and parsed.netloc.endswith(":")
+        ):
+            raise EdgePolicyError(f"{label} is not approved")
+        effective_port = port or 443
+        origin = (
+            f"https://{parsed.hostname}"
+            if effective_port == 443
+            else f"https://{parsed.hostname}:{effective_port}"
+        )
+        if origin not in approved_origins:
+            raise EdgePolicyError(f"{label} origin is not approved")
+
+    @staticmethod
+    def retention_days(classification: str) -> int:
+        try:
+            return RETENTION_DAYS[classification]
+        except (KeyError, TypeError) as exc:
+            raise EdgePolicyError("retention classification is invalid") from exc

+ 2189 - 0
app/core/edge_gateway/queue.py

@@ -0,0 +1,2189 @@
+"""Durable SQLite task and outbound-event queue for an offline edge gateway."""
+
+from __future__ import annotations
+
+import hashlib
+import json
+import re
+import sqlite3
+import uuid
+from collections.abc import Callable, Iterator, Mapping
+from contextlib import contextmanager
+from dataclasses import dataclass
+from datetime import UTC, datetime, timedelta
+from pathlib import Path
+from types import MappingProxyType
+
+from .contracts import (
+    EdgeContractError,
+    EdgeEventContract,
+    EdgeTaskContract,
+    SignedTaskEnvelope,
+    canonical_sha256,
+    canonical_timestamp,
+    strict_json_bytes,
+)
+from .policy import EdgeEgressPolicy, EdgePolicyError
+
+
+class EdgeQueueError(RuntimeError):
+    """Base error for durable edge queue operations."""
+
+
+class EdgeQueueConflictError(EdgeQueueError):
+    """Raised when an existing stable ID is replayed with different content."""
+
+
+class EdgeQueueLeaseError(EdgeQueueError):
+    """Raised when a mutation is attempted without the current lease token."""
+
+
+class EdgeQueueSchemaError(EdgeQueueError):
+    """Raised when an edge queue database schema cannot be trusted."""
+
+
+SCHEMA_VERSION = 2
+
+_V1_TASK_TABLE_SQL = """CREATE TABLE edge_tasks (
+    task_id TEXT PRIMARY KEY,
+    digest TEXT NOT NULL,
+    task_json TEXT NOT NULL,
+    status TEXT NOT NULL CHECK (
+        status IN ('pending', 'leased', 'completed', 'failed', 'cancelled')
+    ),
+    attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
+    available_at TEXT NOT NULL,
+    deadline_at TEXT NOT NULL,
+    deadline_epoch_us INTEGER NOT NULL,
+    lease_owner TEXT,
+    lease_token TEXT,
+    lease_expires_at TEXT,
+    error_code TEXT,
+    created_at TEXT NOT NULL,
+    updated_at TEXT NOT NULL
+)"""
+_TASK_TABLE_SQL = _V1_TASK_TABLE_SQL.rstrip()[:-1].rstrip() + ",\n" + (
+    "    authority_key_id TEXT,\n"
+    "    authority_digest TEXT,\n"
+    "    authority_json TEXT,\n"
+    "    remote_lease_token_digest TEXT,\n"
+    "    remote_lease_ciphertext TEXT,\n"
+    "    remote_lease_expires_at TEXT,\n"
+    "    remote_attempt INTEGER CHECK (remote_attempt IS NULL OR remote_attempt >= 1)\n"
+    ")"
+)
+_TASK_INDEX_SQL = """CREATE INDEX edge_tasks_claim_idx ON edge_tasks(
+    status, deadline_epoch_us, available_at, lease_expires_at, created_at
+)"""
+_V1_EVENT_TABLE_SQL = """CREATE TABLE edge_outbound_events (
+    event_id TEXT PRIMARY KEY,
+    digest TEXT NOT NULL,
+    event_json TEXT NOT NULL,
+    status TEXT NOT NULL CHECK (
+        status IN ('pending', 'sending', 'acknowledged', 'failed')
+    ),
+    attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
+    available_at TEXT NOT NULL,
+    lease_owner TEXT,
+    lease_token TEXT,
+    lease_expires_at TEXT,
+    error_code TEXT,
+    acknowledgement_json TEXT,
+    ack_digest TEXT,
+    lease_token_digest TEXT,
+    created_at TEXT NOT NULL,
+    updated_at TEXT NOT NULL
+)"""
+_EVENT_TABLE_SQL = _V1_EVENT_TABLE_SQL.rstrip()[:-1].rstrip() + ",\n" + (
+    "    task_digest TEXT,\n"
+    "    remote_lease_token_digest TEXT,\n"
+    "    control_lease_token_digest TEXT\n"
+    ")"
+)
+_EVENT_INDEX_SQL = """CREATE INDEX edge_events_claim_idx ON edge_outbound_events(
+    status, available_at, lease_expires_at, created_at
+)"""
+_ARTIFACT_TABLE_SQL = """CREATE TABLE edge_local_artifacts (
+    artifact_digest TEXT PRIMARY KEY,
+    artifact_ref TEXT,
+    artifact_ref_hash TEXT NOT NULL,
+    classification TEXT NOT NULL CHECK (
+        classification IN ('raw', 'recent_detail', 'restricted',
+                           'desensitized_metadata', 'statistics', 'lineage', 'evidence')
+    ),
+    retention_until TEXT NOT NULL,
+    task_id TEXT NOT NULL,
+    event_id TEXT NOT NULL,
+    cleanup_status TEXT NOT NULL CHECK (
+        cleanup_status IN ('pending', 'deleting', 'deleted', 'failed')
+    ),
+    attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
+    available_at TEXT NOT NULL,
+    lease_owner TEXT,
+    lease_token TEXT,
+    lease_expires_at TEXT,
+    error_code TEXT,
+    deletion_receipt_json TEXT,
+    created_at TEXT NOT NULL,
+    updated_at TEXT NOT NULL,
+    FOREIGN KEY(task_id) REFERENCES edge_tasks(task_id),
+    FOREIGN KEY(event_id) REFERENCES edge_outbound_events(event_id)
+)"""
+_ARTIFACT_INDEX_SQL = """CREATE INDEX edge_artifacts_task_idx ON edge_local_artifacts(
+    cleanup_status, retention_until, available_at, lease_expires_at, created_at
+)"""
+_RELEASE_TABLE_SQL = """CREATE TABLE edge_release_state (
+    release_id TEXT PRIMARY KEY,
+    manifest_digest TEXT NOT NULL,
+    version TEXT NOT NULL,
+    rollback_version TEXT NOT NULL,
+    status TEXT NOT NULL CHECK (
+        status IN ('offered', 'accepted', 'candidate', 'installed', 'failed', 'rolled_back')
+    ),
+    previous_version TEXT,
+    current_version TEXT,
+    updated_at TEXT NOT NULL
+)"""
+_RELEASE_INDEX_SQL = """CREATE INDEX edge_release_status_idx ON edge_release_state(
+    status, updated_at, release_id
+)"""
+_OUTCOME_TABLE_SQL = """CREATE TABLE edge_task_outcomes (
+    task_id TEXT PRIMARY KEY,
+    digest TEXT NOT NULL,
+    outcome_json TEXT NOT NULL,
+    status TEXT NOT NULL CHECK (
+        status IN ('pending', 'sending', 'acknowledged', 'failed')
+    ),
+    attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
+    available_at TEXT NOT NULL,
+    lease_owner TEXT,
+    lease_token TEXT,
+    lease_expires_at TEXT,
+    error_code TEXT,
+    acknowledgement_json TEXT,
+    ack_digest TEXT,
+    remote_lease_token_digest TEXT NOT NULL,
+    created_at TEXT NOT NULL,
+    updated_at TEXT NOT NULL,
+    FOREIGN KEY(task_id) REFERENCES edge_tasks(task_id)
+)"""
+_OUTCOME_INDEX_SQL = """CREATE INDEX edge_outcomes_claim_idx ON edge_task_outcomes(
+    status, available_at, lease_expires_at, created_at
+)"""
+_EXPECTED_TABLE_INFO = {
+    "edge_tasks": (
+        ("task_id", "TEXT", 0, None, 1),
+        ("digest", "TEXT", 1, None, 0),
+        ("task_json", "TEXT", 1, None, 0),
+        ("status", "TEXT", 1, None, 0),
+        ("attempt_count", "INTEGER", 1, "0", 0),
+        ("available_at", "TEXT", 1, None, 0),
+        ("deadline_at", "TEXT", 1, None, 0),
+        ("deadline_epoch_us", "INTEGER", 1, None, 0),
+        ("lease_owner", "TEXT", 0, None, 0),
+        ("lease_token", "TEXT", 0, None, 0),
+        ("lease_expires_at", "TEXT", 0, None, 0),
+        ("error_code", "TEXT", 0, None, 0),
+        ("created_at", "TEXT", 1, None, 0),
+        ("updated_at", "TEXT", 1, None, 0),
+        ("authority_key_id", "TEXT", 0, None, 0),
+        ("authority_digest", "TEXT", 0, None, 0),
+        ("authority_json", "TEXT", 0, None, 0),
+        ("remote_lease_token_digest", "TEXT", 0, None, 0),
+        ("remote_lease_ciphertext", "TEXT", 0, None, 0),
+        ("remote_lease_expires_at", "TEXT", 0, None, 0),
+        ("remote_attempt", "INTEGER", 0, None, 0),
+    ),
+    "edge_outbound_events": (
+        ("event_id", "TEXT", 0, None, 1),
+        ("digest", "TEXT", 1, None, 0),
+        ("event_json", "TEXT", 1, None, 0),
+        ("status", "TEXT", 1, None, 0),
+        ("attempt_count", "INTEGER", 1, "0", 0),
+        ("available_at", "TEXT", 1, None, 0),
+        ("lease_owner", "TEXT", 0, None, 0),
+        ("lease_token", "TEXT", 0, None, 0),
+        ("lease_expires_at", "TEXT", 0, None, 0),
+        ("error_code", "TEXT", 0, None, 0),
+        ("acknowledgement_json", "TEXT", 0, None, 0),
+        ("ack_digest", "TEXT", 0, None, 0),
+        ("lease_token_digest", "TEXT", 0, None, 0),
+        ("created_at", "TEXT", 1, None, 0),
+        ("updated_at", "TEXT", 1, None, 0),
+        ("task_digest", "TEXT", 0, None, 0),
+        ("remote_lease_token_digest", "TEXT", 0, None, 0),
+        ("control_lease_token_digest", "TEXT", 0, None, 0),
+    ),
+    "edge_local_artifacts": (
+        ("artifact_digest", "TEXT", 0, None, 1),
+        ("artifact_ref", "TEXT", 0, None, 0),
+        ("artifact_ref_hash", "TEXT", 1, None, 0),
+        ("classification", "TEXT", 1, None, 0),
+        ("retention_until", "TEXT", 1, None, 0),
+        ("task_id", "TEXT", 1, None, 0),
+        ("event_id", "TEXT", 1, None, 0),
+        ("cleanup_status", "TEXT", 1, None, 0),
+        ("attempt_count", "INTEGER", 1, "0", 0),
+        ("available_at", "TEXT", 1, None, 0),
+        ("lease_owner", "TEXT", 0, None, 0),
+        ("lease_token", "TEXT", 0, None, 0),
+        ("lease_expires_at", "TEXT", 0, None, 0),
+        ("error_code", "TEXT", 0, None, 0),
+        ("deletion_receipt_json", "TEXT", 0, None, 0),
+        ("created_at", "TEXT", 1, None, 0),
+        ("updated_at", "TEXT", 1, None, 0),
+    ),
+    "edge_release_state": (
+        ("release_id", "TEXT", 0, None, 1),
+        ("manifest_digest", "TEXT", 1, None, 0),
+        ("version", "TEXT", 1, None, 0),
+        ("rollback_version", "TEXT", 1, None, 0),
+        ("status", "TEXT", 1, None, 0),
+        ("previous_version", "TEXT", 0, None, 0),
+        ("current_version", "TEXT", 0, None, 0),
+        ("updated_at", "TEXT", 1, None, 0),
+    ),
+    "edge_task_outcomes": (
+        ("task_id", "TEXT", 0, None, 1),
+        ("digest", "TEXT", 1, None, 0),
+        ("outcome_json", "TEXT", 1, None, 0),
+        ("status", "TEXT", 1, None, 0),
+        ("attempt_count", "INTEGER", 1, "0", 0),
+        ("available_at", "TEXT", 1, None, 0),
+        ("lease_owner", "TEXT", 0, None, 0),
+        ("lease_token", "TEXT", 0, None, 0),
+        ("lease_expires_at", "TEXT", 0, None, 0),
+        ("error_code", "TEXT", 0, None, 0),
+        ("acknowledgement_json", "TEXT", 0, None, 0),
+        ("ack_digest", "TEXT", 0, None, 0),
+        ("remote_lease_token_digest", "TEXT", 1, None, 0),
+        ("created_at", "TEXT", 1, None, 0),
+        ("updated_at", "TEXT", 1, None, 0),
+    ),
+}
+_EXPECTED_INDEXES = {
+    "edge_tasks": {
+        "edge_tasks_claim_idx": (
+            0,
+            "c",
+            0,
+            ("status", "deadline_epoch_us", "available_at", "lease_expires_at", "created_at"),
+        ),
+    },
+    "edge_outbound_events": {
+        "edge_events_claim_idx": (
+            0,
+            "c",
+            0,
+            ("status", "available_at", "lease_expires_at", "created_at"),
+        ),
+    },
+    "edge_local_artifacts": {
+        "edge_artifacts_task_idx": (
+            0, "c", 0,
+            ("cleanup_status", "retention_until", "available_at", "lease_expires_at", "created_at"),
+        ),
+    },
+    "edge_release_state": {
+        "edge_release_status_idx": (0, "c", 0, ("status", "updated_at", "release_id")),
+    },
+    "edge_task_outcomes": {
+        "edge_outcomes_claim_idx": (
+            0, "c", 0, ("status", "available_at", "lease_expires_at", "created_at")
+        ),
+    },
+}
+_EXPECTED_TABLE_SQL = {
+    "edge_tasks": _TASK_TABLE_SQL,
+    "edge_outbound_events": _EVENT_TABLE_SQL,
+    "edge_local_artifacts": _ARTIFACT_TABLE_SQL,
+    "edge_release_state": _RELEASE_TABLE_SQL,
+    "edge_task_outcomes": _OUTCOME_TABLE_SQL,
+}
+
+_V1_EXPECTED_TABLE_INFO = {
+    table: tuple(row for row in info if row[0] not in {
+        "authority_key_id", "authority_digest", "authority_json",
+        "remote_lease_token_digest", "remote_lease_ciphertext", "remote_lease_expires_at", "remote_attempt",
+        "task_digest", "control_lease_token_digest",
+    })
+    for table, info in _EXPECTED_TABLE_INFO.items()
+    if table not in {"edge_local_artifacts", "edge_release_state", "edge_task_outcomes"}
+}
+_V1_EXPECTED_TABLE_SQL = {
+    "edge_tasks": _V1_TASK_TABLE_SQL,
+    "edge_outbound_events": _V1_EVENT_TABLE_SQL,
+}
+
+
+@dataclass(frozen=True, slots=True)
+class QueuedTask:
+    task_id: str
+    digest: str
+    status: str
+    task: Mapping[str, object]
+    attempt_count: int
+    available_at: str
+    lease_owner: str | None
+    lease_token: str | None
+    lease_expires_at: str | None
+    error_code: str | None
+    authority_key_id: str | None
+    authority_digest: str | None
+    remote_lease_token_digest: str | None
+    remote_lease_expires_at: str | None
+    remote_attempt: int | None
+    created_at: str
+    updated_at: str
+
+
+@dataclass(frozen=True, slots=True)
+class QueuedEvent:
+    event_id: str
+    digest: str
+    status: str
+    event: Mapping[str, object]
+    attempt_count: int
+    available_at: str
+    lease_owner: str | None
+    lease_token: str | None
+    lease_expires_at: str | None
+    error_code: str | None
+    acknowledgement: Mapping[str, object] | None
+    task_digest: str | None
+    remote_lease_token_digest: str | None
+    control_lease_token_digest: str | None
+    created_at: str
+    updated_at: str
+
+
+@dataclass(frozen=True, slots=True)
+class LocalArtifact:
+    artifact_digest: str
+    artifact_ref: str | None
+    artifact_ref_hash: str
+    classification: str
+    retention_until: str
+    task_id: str
+    event_id: str
+    cleanup_status: str
+    attempt_count: int
+    available_at: str
+    lease_owner: str | None
+    lease_token: str | None
+    lease_expires_at: str | None
+    error_code: str | None
+    deletion_receipt: Mapping[str, object] | None
+    created_at: str
+    updated_at: str
+
+
+@dataclass(frozen=True, slots=True)
+class ReleaseState:
+    release_id: str
+    manifest_digest: str
+    version: str
+    rollback_version: str
+    status: str
+    previous_version: str | None
+    current_version: str | None
+    updated_at: str
+
+
+@dataclass(frozen=True, slots=True)
+class QueuedOutcome:
+    task_id: str
+    digest: str
+    status: str
+    outcome: Mapping[str, object]
+    attempt_count: int
+    available_at: str
+    lease_owner: str | None
+    lease_token: str | None
+    lease_expires_at: str | None
+    error_code: str | None
+    acknowledgement: Mapping[str, object] | None
+    remote_lease_token_digest: str
+    created_at: str
+    updated_at: str
+
+
+_ERROR_CODE = re.compile(r"^[a-z][a-z0-9_.-]{0,127}$")
+_SENSITIVE_ERROR_PARTS = ("password", "passwd", "secret", "token", "credential")
+
+
+def _utc_now(clock: Callable[[], object]) -> datetime:
+    value = clock()
+    if isinstance(value, bool):
+        raise ValueError("queue clock returned an invalid value")
+    if isinstance(value, (int, float)):
+        value = datetime.fromtimestamp(value, tz=UTC)
+    if not isinstance(value, datetime):
+        raise ValueError("queue clock must return a datetime or timestamp")
+    if value.tzinfo is None or value.utcoffset() is None:
+        raise ValueError("queue clock must return a timezone-aware value")
+    return value.astimezone(UTC)
+
+
+def _timestamp(value: datetime) -> str:
+    utc = value.astimezone(UTC)
+    canonical = utc.strftime("%Y-%m-%dT%H:%M:%S")
+    if utc.microsecond:
+        canonical += f".{utc.microsecond:06d}".rstrip("0")
+    return f"{canonical}Z"
+
+
+def _epoch_microseconds(value: datetime) -> int:
+    epoch = datetime(1970, 1, 1, tzinfo=UTC)
+    delta = value.astimezone(UTC) - epoch
+    return (
+        (delta.days * 86_400 + delta.seconds) * 1_000_000
+        + delta.microseconds
+    )
+
+
+def _normalize_schema_sql(value: str | None) -> str:
+    if not isinstance(value, str):
+        return ""
+    normalized = re.sub(r"\s+", " ", value.strip()).lower()
+    return re.sub(r"\s*([(),])\s*", r"\1", normalized)
+
+
+def _freeze_mapping(value: Mapping[str, object]) -> Mapping[str, object]:
+    def freeze(item: object) -> object:
+        if isinstance(item, dict):
+            return MappingProxyType({key: freeze(child) for key, child in item.items()})
+        if isinstance(item, list):
+            return tuple(freeze(child) for child in item)
+        return item
+
+    return freeze(dict(value))  # type: ignore[return-value]
+
+
+class SqliteEdgeQueue:
+    """An on-disk, WAL-backed queue with fenced leases and exact replay."""
+
+    def __init__(
+        self,
+        db_path: str | Path,
+        *,
+        clock: Callable[[], object] | None = None,
+        lease_seconds: int = 60,
+        max_attempts: int = 5,
+        retry_base_seconds: int = 2,
+        retry_max_seconds: int = 300,
+        busy_timeout_ms: int = 5_000,
+        egress_policy: EdgeEgressPolicy | None = None,
+        random_source: Callable[[], float] | None = None,
+        remote_lease_encoder: Callable[[str], str] | None = None,
+        remote_lease_decoder: Callable[[str], str] | None = None,
+    ) -> None:
+        if not isinstance(db_path, (str, Path)) or not str(db_path).strip():
+            raise ValueError("an explicit SQLite database path is required")
+        path_text = str(db_path)
+        if path_text == ":memory:" or "mode=memory" in path_text:
+            raise ValueError("an in-memory database is not allowed")
+        for label, value in {
+            "lease_seconds": lease_seconds,
+            "max_attempts": max_attempts,
+            "retry_base_seconds": retry_base_seconds,
+            "retry_max_seconds": retry_max_seconds,
+            "busy_timeout_ms": busy_timeout_ms,
+        }.items():
+            if isinstance(value, bool) or not isinstance(value, int) or value < 1:
+                raise ValueError(f"{label} must be a positive integer")
+        if retry_base_seconds > retry_max_seconds:
+            raise ValueError("retry_base_seconds cannot exceed retry_max_seconds")
+        self.db_path = str(Path(db_path).expanduser().resolve())
+        self._clock = clock or (lambda: datetime.now(UTC))
+        self.lease_seconds = lease_seconds
+        self.max_attempts = max_attempts
+        self.retry_base_seconds = retry_base_seconds
+        self.retry_max_seconds = retry_max_seconds
+        self.busy_timeout_ms = busy_timeout_ms
+        self.egress_policy = egress_policy or EdgeEgressPolicy(
+            allowed_control_hosts=set()
+        )
+        # The injectable source makes retry timing testable; 0.5 is neutral jitter.
+        self._random = random_source or (lambda: 0.5)
+        if (remote_lease_encoder is None) != (remote_lease_decoder is None):
+            raise ValueError("remote lease encoder and decoder must be paired")
+        self._remote_lease_encoder = remote_lease_encoder
+        self._remote_lease_decoder = remote_lease_decoder
+        Path(self.db_path).parent.mkdir(parents=True, exist_ok=True)
+        self._initialize()
+
+    def _connect(self) -> sqlite3.Connection:
+        connection = sqlite3.connect(
+            self.db_path,
+            timeout=self.busy_timeout_ms / 1_000,
+            isolation_level=None,
+        )
+        connection.row_factory = sqlite3.Row
+        connection.execute(f"PRAGMA busy_timeout = {self.busy_timeout_ms}")
+        connection.execute("PRAGMA foreign_keys = ON")
+        return connection
+
+    def _initialize(self) -> None:
+        connection = self._connect()
+        try:
+            version = connection.execute("PRAGMA user_version").fetchone()[0]
+            edge_tables = {
+                row[0]
+                for row in connection.execute(
+                    """
+                    SELECT name FROM sqlite_master
+                    WHERE type = 'table' AND name LIKE 'edge_%'
+                    """
+                ).fetchall()
+            }
+            if version == 0 and edge_tables:
+                raise EdgeQueueSchemaError("unversioned edge queue schema is unsafe")
+            if version not in {0, 1, SCHEMA_VERSION}:
+                raise EdgeQueueSchemaError("edge queue schema version is unsupported")
+            connection.execute("PRAGMA journal_mode = WAL")
+            connection.execute("PRAGMA synchronous = FULL")
+            connection.execute("BEGIN IMMEDIATE")
+            if version == 0:
+                for statement in (
+                    _TASK_TABLE_SQL,
+                    _TASK_INDEX_SQL,
+                    _EVENT_TABLE_SQL,
+                    _EVENT_INDEX_SQL,
+                    _ARTIFACT_TABLE_SQL,
+                    _ARTIFACT_INDEX_SQL,
+                    _RELEASE_TABLE_SQL,
+                    _RELEASE_INDEX_SQL,
+                    _OUTCOME_TABLE_SQL,
+                    _OUTCOME_INDEX_SQL,
+                ):
+                    connection.execute(statement)
+                connection.execute(f"PRAGMA user_version = {SCHEMA_VERSION}")
+            elif version == 1:
+                self._validate_schema(
+                    connection,
+                    expected_info=_V1_EXPECTED_TABLE_INFO,
+                    expected_sql=_V1_EXPECTED_TABLE_SQL,
+                    expected_indexes={
+                        key: _EXPECTED_INDEXES[key]
+                        for key in ("edge_tasks", "edge_outbound_events")
+                    },
+                )
+                active_unsigned = connection.execute(
+                    """SELECT EXISTS(
+                    SELECT 1 FROM edge_tasks WHERE status IN ('pending','leased')
+                    )"""
+                ).fetchone()[0]
+                unacknowledged_events = connection.execute(
+                    """SELECT EXISTS(
+                    SELECT 1 FROM edge_outbound_events WHERE status <> 'acknowledged'
+                    )"""
+                ).fetchone()[0]
+                if active_unsigned or unacknowledged_events:
+                    raise EdgeQueueSchemaError(
+                        "v1 edge queue must drain active tasks and unacknowledged events"
+                    )
+                for statement in (
+                    "ALTER TABLE edge_tasks ADD COLUMN authority_key_id TEXT",
+                    "ALTER TABLE edge_tasks ADD COLUMN authority_digest TEXT",
+                    "ALTER TABLE edge_tasks ADD COLUMN authority_json TEXT",
+                    "ALTER TABLE edge_tasks ADD COLUMN remote_lease_token_digest TEXT",
+                    "ALTER TABLE edge_tasks ADD COLUMN remote_lease_ciphertext TEXT",
+                    "ALTER TABLE edge_tasks ADD COLUMN remote_lease_expires_at TEXT",
+                    "ALTER TABLE edge_tasks ADD COLUMN remote_attempt INTEGER CHECK (remote_attempt IS NULL OR remote_attempt >= 1)",
+                    "ALTER TABLE edge_outbound_events ADD COLUMN task_digest TEXT",
+                    "ALTER TABLE edge_outbound_events ADD COLUMN remote_lease_token_digest TEXT",
+                    "ALTER TABLE edge_outbound_events ADD COLUMN control_lease_token_digest TEXT",
+                    _ARTIFACT_TABLE_SQL,
+                    _ARTIFACT_INDEX_SQL,
+                    _RELEASE_TABLE_SQL,
+                    _RELEASE_INDEX_SQL,
+                    _OUTCOME_TABLE_SQL,
+                    _OUTCOME_INDEX_SQL,
+                ):
+                    connection.execute(statement)
+                connection.execute(f"PRAGMA user_version = {SCHEMA_VERSION}")
+            self._validate_schema(connection)
+            if connection.in_transaction:
+                connection.execute("COMMIT")
+        except Exception:
+            if connection.in_transaction:
+                connection.execute("ROLLBACK")
+            raise
+        finally:
+            connection.close()
+
+    @staticmethod
+    def _validate_schema(
+        connection: sqlite3.Connection,
+        *,
+        expected_info: Mapping[str, tuple] = _EXPECTED_TABLE_INFO,
+        expected_sql: Mapping[str, str] = _EXPECTED_TABLE_SQL,
+        expected_indexes: Mapping[str, Mapping[str, tuple]] = _EXPECTED_INDEXES,
+    ) -> None:
+        primary_keys = {
+            "edge_tasks": "task_id",
+            "edge_outbound_events": "event_id",
+            "edge_local_artifacts": "artifact_digest",
+            "edge_release_state": "release_id",
+            "edge_task_outcomes": "task_id",
+        }
+        for table, table_expected_info in expected_info.items():
+            rows = connection.execute(f"PRAGMA table_info({table})").fetchall()
+            actual_info = tuple(
+                (row[1], row[2].upper(), row[3], row[4], row[5]) for row in rows
+            )
+            if actual_info != table_expected_info:
+                raise EdgeQueueSchemaError("edge queue schema columns are invalid")
+            table_sql = connection.execute(
+                "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?",
+                (table,),
+            ).fetchone()
+            if table_sql is None or _normalize_schema_sql(
+                table_sql[0]
+            ) != _normalize_schema_sql(expected_sql[table]):
+                raise EdgeQueueSchemaError(
+                    "edge queue schema constraints are invalid"
+                )
+
+            index_rows = connection.execute(f"PRAGMA index_list({table})").fetchall()
+            custom_rows = {row[1]: row for row in index_rows if row[3] == "c"}
+            table_expected_indexes = expected_indexes[table]
+            if set(custom_rows) != set(table_expected_indexes):
+                raise EdgeQueueSchemaError("edge queue schema indexes are invalid")
+            for name, (unique, origin, partial, columns) in table_expected_indexes.items():
+                row = custom_rows[name]
+                actual_columns = tuple(
+                    item[2]
+                    for item in connection.execute(
+                        f"PRAGMA index_info({name})"
+                    ).fetchall()
+                )
+                if (row[2], row[3], row[4], actual_columns) != (
+                    unique,
+                    origin,
+                    partial,
+                    columns,
+                ):
+                    raise EdgeQueueSchemaError(
+                        "edge queue schema index definition is invalid"
+                    )
+            primary_indexes = [row for row in index_rows if row[3] == "pk"]
+            if len(primary_indexes) != 1:
+                raise EdgeQueueSchemaError("edge queue schema primary key is invalid")
+            primary = primary_indexes[0]
+            primary_columns = tuple(
+                item[2]
+                for item in connection.execute(
+                    f"PRAGMA index_info({primary[1]})"
+                ).fetchall()
+            )
+            if (primary[2], primary[4], primary_columns) != (
+                1,
+                0,
+                (primary_keys[table],),
+            ):
+                raise EdgeQueueSchemaError("edge queue schema primary key is invalid")
+
+    @contextmanager
+    def _transaction(self) -> Iterator[sqlite3.Connection]:
+        connection = self._connect()
+        try:
+            connection.execute("BEGIN IMMEDIATE")
+            yield connection
+            connection.execute("COMMIT")
+        except Exception:
+            if connection.in_transaction:
+                connection.execute("ROLLBACK")
+            raise
+        finally:
+            connection.close()
+
+    def enqueue(self, task: EdgeTaskContract | Mapping[str, object]) -> QueuedTask:
+        contract = EdgeTaskContract.from_mapping(
+            task.to_mapping() if isinstance(task, EdgeTaskContract) else task
+        )
+        return self._enqueue_contract(contract, authority=None)
+
+    def enqueue_signed(self, envelope: SignedTaskEnvelope) -> QueuedTask:
+        """Atomically persist a previously verified authority envelope and task."""
+
+        if not isinstance(envelope, SignedTaskEnvelope) or not envelope.is_verified:
+            raise EdgeContractError("a verified SignedTaskEnvelope is required")
+        # Reconstruct the task so object.__setattr__ cannot bypass validation.
+        contract = EdgeTaskContract.from_mapping(envelope.task.to_mapping())
+        if envelope.contract_digest != contract.digest:
+            raise EdgeContractError("signed authority no longer binds the task")
+        return self._enqueue_contract(contract, authority=envelope)
+
+    def accept_signed_task(
+        self,
+        envelope: SignedTaskEnvelope,
+        *,
+        remote_lease_token: str,
+        remote_lease_expires_at: str,
+        remote_attempt: int,
+    ) -> QueuedTask:
+        """Atomically accept authority and an encrypted recoverable control lease."""
+
+        if self._remote_lease_encoder is None:
+            raise EdgeQueueLeaseError("secure remote lease codec is required")
+        if not isinstance(envelope, SignedTaskEnvelope) or not envelope.is_verified:
+            raise EdgeContractError("a verified SignedTaskEnvelope is required")
+        return self._enqueue_contract(
+            EdgeTaskContract.from_mapping(envelope.task.to_mapping()),
+            authority=envelope,
+            remote_binding=self._normalize_remote_binding(
+                remote_lease_token, remote_lease_expires_at, remote_attempt
+            ),
+        )
+
+    def _enqueue_contract(
+        self,
+        contract: EdgeTaskContract,
+        *,
+        authority: SignedTaskEnvelope | None,
+        remote_binding: tuple[str, str | None, str, int] | None = None,
+    ) -> QueuedTask:
+        task_mapping = contract.to_mapping()
+        encoded = strict_json_bytes(task_mapping).decode("utf-8")
+        digest = canonical_sha256(task_mapping)
+        if remote_binding is not None and remote_binding[3] != contract.attempt:
+            raise EdgeQueueLeaseError(
+                "remote lease attempt does not bind the task contract"
+            )
+        with self._transaction() as connection:
+            existing = connection.execute(
+                "SELECT * FROM edge_tasks WHERE task_id = ?", (contract.task_id,)
+            ).fetchone()
+            if existing is not None:
+                if existing["digest"] != digest:
+                    raise EdgeQueueConflictError(
+                        "task ID already exists with a different digest"
+                    )
+                if authority is not None and existing["authority_digest"] != authority.digest:
+                    raise EdgeQueueConflictError(
+                        "task ID already exists with a different authority digest"
+                    )
+                if remote_binding is not None:
+                    now = _utc_now(self._clock)
+                    old_expiry_text = existing["remote_lease_expires_at"]
+                    old_expiry = (
+                        datetime.fromisoformat(old_expiry_text.replace("Z", "+00:00"))
+                        if old_expiry_text is not None else None
+                    )
+                    if (
+                        existing["remote_lease_token_digest"] != remote_binding[0]
+                        and old_expiry is not None and old_expiry > now
+                    ):
+                        raise EdgeQueueConflictError("active remote lease binding conflicts")
+                    new_expiry = datetime.fromisoformat(remote_binding[2].replace("Z", "+00:00"))
+                    if new_expiry <= now:
+                        raise EdgeQueueLeaseError("remote lease has expired")
+                    if (
+                        existing["status"] in {"failed", "cancelled"}
+                        and not (
+                            existing["status"] == "failed"
+                            and existing["error_code"] == "execution_lease_expired"
+                        )
+                    ):
+                        raise EdgeQueueConflictError(
+                            "terminal task cannot be rebound to a remote lease"
+                        )
+                    changed_remote_lease = (
+                        existing["remote_lease_token_digest"] != remote_binding[0]
+                    )
+                    if changed_remote_lease:
+                        event_rows = connection.execute(
+                            """SELECT * FROM edge_outbound_events
+                            WHERE status IN ('pending','sending')
+                              AND json_extract(event_json,'$.task_id')=?""",
+                            (contract.task_id,),
+                        ).fetchall()
+                        for event_row in event_rows:
+                            event_mapping = json.loads(event_row["event_json"])
+                            if (
+                                event_row["task_digest"] != digest
+                                or event_row["remote_lease_token_digest"]
+                                != existing["remote_lease_token_digest"]
+                                or event_mapping.get("task_id") != contract.task_id
+                                or event_mapping.get("gateway_id") != contract.gateway_id
+                                or canonical_sha256(event_mapping) != event_row["digest"]
+                            ):
+                                raise EdgeQueueConflictError(
+                                    "pending event does not bind the task lease"
+                                )
+                        connection.execute(
+                            """UPDATE edge_outbound_events
+                            SET remote_lease_token_digest=?,updated_at=?
+                            WHERE status IN ('pending','sending')
+                              AND json_extract(event_json,'$.task_id')=?""",
+                            (
+                                remote_binding[0],
+                                _timestamp(now),
+                                contract.task_id,
+                            ),
+                        )
+                    recoverable_failure = (
+                        existing["status"] == "failed"
+                        and existing["error_code"] == "execution_lease_expired"
+                    )
+                    connection.execute(
+                        """UPDATE edge_tasks SET remote_lease_token_digest=?,
+                        remote_lease_ciphertext=?,remote_lease_expires_at=?,remote_attempt=?,
+                        status=CASE WHEN ? THEN 'pending' ELSE status END,
+                        available_at=CASE WHEN ? THEN ? ELSE available_at END,
+                        error_code=CASE WHEN ? THEN NULL ELSE error_code END,
+                        updated_at=?
+                        WHERE task_id=? AND digest=? AND authority_digest=?""",
+                        (
+                            remote_binding[0], remote_binding[1], remote_binding[2],
+                            remote_binding[3], recoverable_failure,
+                            recoverable_failure, _timestamp(now), recoverable_failure,
+                            _timestamp(now), contract.task_id,
+                            digest, authority.digest if authority is not None else None,
+                        ),
+                    )
+                    existing = connection.execute(
+                        "SELECT * FROM edge_tasks WHERE task_id=?", (contract.task_id,)
+                    ).fetchone()
+                return self._task_record(existing)
+            now = _utc_now(self._clock)
+            deadline = datetime.fromisoformat(
+                contract.deadline_at.replace("Z", "+00:00")
+            ).astimezone(UTC)
+            if deadline <= now:
+                raise EdgeContractError("task deadline has expired")
+            if remote_binding is not None:
+                remote_expiry = datetime.fromisoformat(remote_binding[2].replace("Z", "+00:00"))
+                if remote_expiry <= now or remote_expiry > deadline:
+                    raise EdgeQueueLeaseError("remote lease validity window is invalid")
+            now_text = _timestamp(now)
+            connection.execute(
+                """
+                INSERT INTO edge_tasks (
+                    task_id, digest, task_json, status, attempt_count,
+                    available_at, deadline_at, deadline_epoch_us,
+                    authority_key_id, authority_digest, authority_json,
+                    remote_lease_token_digest, remote_lease_ciphertext,
+                    remote_lease_expires_at, remote_attempt,
+                    created_at, updated_at
+                ) VALUES (?, ?, ?, 'pending', 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+                """,
+                (
+                    contract.task_id,
+                    digest,
+                    encoded,
+                    now_text,
+                    contract.deadline_at,
+                    _epoch_microseconds(deadline),
+                    authority.authority_key_id if authority is not None else None,
+                    authority.digest if authority is not None else None,
+                    strict_json_bytes(authority.to_mapping()).decode("utf-8")
+                    if authority is not None else None,
+                    remote_binding[0] if remote_binding is not None else None,
+                    remote_binding[1] if remote_binding is not None else None,
+                    remote_binding[2] if remote_binding is not None else None,
+                    remote_binding[3] if remote_binding is not None else None,
+                    now_text,
+                    now_text,
+                ),
+            )
+            row = connection.execute(
+                "SELECT * FROM edge_tasks WHERE task_id = ?", (contract.task_id,)
+            ).fetchone()
+            return self._task_record(row)
+
+    def claim(self, lease_owner: str) -> QueuedTask | None:
+        owner = self._lease_owner(lease_owner)
+        with self._transaction() as connection:
+            now = _utc_now(self._clock)
+            now_text = _timestamp(now)
+            now_epoch_us = _epoch_microseconds(now)
+            lease_token = str(uuid.uuid4())
+            expires_at = _timestamp(now + timedelta(seconds=self.lease_seconds))
+            connection.execute(
+                """
+                UPDATE edge_tasks
+                SET status = 'failed', lease_owner = NULL, lease_token = NULL,
+                    lease_expires_at = NULL,
+                    error_code = 'task_deadline_expired', updated_at = ?
+                WHERE status IN ('pending', 'leased') AND deadline_epoch_us <= ?
+                """,
+                (now_text, now_epoch_us),
+            )
+            connection.execute(
+                """
+                UPDATE edge_tasks
+                SET status = 'failed', lease_owner = NULL, lease_token = NULL,
+                    lease_expires_at = NULL,
+                    error_code = 'lease_attempts_exhausted', updated_at = ?
+                WHERE status = 'leased'
+                  AND julianday(lease_expires_at) <= julianday(?)
+                  AND attempt_count >= ?
+                """,
+                (now_text, now_text, self.max_attempts),
+            )
+            row = connection.execute(
+                """
+                SELECT * FROM edge_tasks
+                WHERE deadline_epoch_us > ? AND attempt_count < ? AND (
+                    (status = 'pending' AND julianday(available_at) <= julianday(?))
+                    OR (status = 'leased'
+                        AND julianday(lease_expires_at) <= julianday(?))
+                )
+                ORDER BY created_at, task_id
+                LIMIT 1
+                """,
+                (now_epoch_us, self.max_attempts, now_text, now_text),
+            ).fetchone()
+            if row is None:
+                return None
+            updated = connection.execute(
+                """
+                UPDATE edge_tasks
+                SET status = 'leased', attempt_count = attempt_count + 1,
+                    lease_owner = ?, lease_token = ?, lease_expires_at = ?,
+                    error_code = NULL, updated_at = ?
+                WHERE task_id = ? AND deadline_epoch_us > ?
+                  AND attempt_count = ? AND (
+                    (status = 'pending' AND julianday(available_at) <= julianday(?))
+                    OR (status = 'leased'
+                        AND julianday(lease_expires_at) <= julianday(?))
+                )
+                """,
+                (
+                    owner,
+                    lease_token,
+                    expires_at,
+                    now_text,
+                    row["task_id"],
+                    now_epoch_us,
+                    row["attempt_count"],
+                    now_text,
+                    now_text,
+                ),
+            )
+            if updated.rowcount != 1:
+                return None
+            claimed = connection.execute(
+                "SELECT * FROM edge_tasks WHERE task_id = ?", (row["task_id"],)
+            ).fetchone()
+            return self._task_record(claimed)
+
+    def bind_remote_lease(
+        self,
+        task_id: str,
+        *,
+        lease_token: str,
+        remote_lease_token: str,
+        remote_lease_expires_at: str,
+        remote_attempt: int,
+    ) -> QueuedTask:
+        """Bind control-plane authority to the signed contract attempt."""
+
+        digest, ciphertext, expiry_text, remote_attempt = self._normalize_remote_binding(
+            remote_lease_token, remote_lease_expires_at, remote_attempt
+        )
+        expiry = datetime.fromisoformat(expiry_text.replace("Z", "+00:00"))
+        with self._transaction() as connection:
+            row = self._leased_row(connection, "edge_tasks", "task_id", task_id, lease_token)
+            now = _utc_now(self._clock)
+            if expiry <= now:
+                raise EdgeQueueLeaseError("remote lease has expired")
+            task_mapping = json.loads(row["task_json"])
+            if remote_attempt != task_mapping["attempt"]:
+                raise EdgeQueueLeaseError("remote lease attempt does not bind task contract")
+            existing = row["remote_lease_token_digest"]
+            if existing is not None and (
+                existing != digest
+                or row["remote_lease_expires_at"] != expiry_text
+                or row["remote_attempt"] != remote_attempt
+            ):
+                raise EdgeQueueConflictError("remote lease binding conflicts")
+            connection.execute(
+                """
+                UPDATE edge_tasks
+                SET remote_lease_token_digest=?, remote_lease_ciphertext=?, remote_lease_expires_at=?,
+                    remote_attempt=?, updated_at=?
+                WHERE task_id=? AND status='leased' AND lease_token=?
+                """,
+                (digest, ciphertext, expiry_text, remote_attempt, _timestamp(now), task_id, lease_token),
+            )
+            updated = connection.execute(
+                "SELECT * FROM edge_tasks WHERE task_id=?", (task_id,)
+            ).fetchone()
+            return self._task_record(updated)
+
+    def recover_remote_lease(self, task_id: str) -> str:
+        """Recover a control lease through the injected secure decoder without exposing it in records."""
+
+        if self._remote_lease_decoder is None:
+            raise EdgeQueueLeaseError("secure remote lease codec is required")
+        connection = self._connect()
+        try:
+            row = connection.execute(
+                "SELECT remote_lease_ciphertext,remote_lease_token_digest FROM edge_tasks WHERE task_id=?",
+                (task_id,),
+            ).fetchone()
+        finally:
+            connection.close()
+        if row is None or row["remote_lease_ciphertext"] is None:
+            raise EdgeQueueLeaseError("recoverable remote lease does not exist")
+        try:
+            token = self._remote_lease_decoder(row["remote_lease_ciphertext"])
+        except Exception as exc:
+            raise EdgeQueueLeaseError("remote lease recovery failed") from exc
+        if not isinstance(token, str) or self._opaque_token_digest(token) != row["remote_lease_token_digest"]:
+            raise EdgeQueueLeaseError("recovered remote lease failed integrity validation")
+        return token
+
+    def complete_with_event(
+        self,
+        task_id: str,
+        *,
+        lease_token: str,
+        remote_lease_token: str,
+        event: EdgeEventContract | Mapping[str, object],
+        artifacts: list[Mapping[str, object]] | tuple[Mapping[str, object], ...] = (),
+    ) -> tuple[QueuedTask, QueuedEvent]:
+        """Atomically commit a safe event, local artifact metadata, and task terminal state."""
+
+        contract = EdgeEventContract.from_mapping(
+            event.to_mapping() if isinstance(event, EdgeEventContract) else event
+        )
+        approved = self.egress_policy.approve_event(contract.to_mapping())
+        encoded_event = strict_json_bytes(approved).decode("utf-8")
+        normalized_artifacts = [self._validate_local_artifact(item) for item in artifacts]
+        if not isinstance(remote_lease_token, str) or not remote_lease_token:
+            raise EdgeQueueLeaseError("remote lease token is invalid")
+        remote_digest = self._opaque_token_digest(remote_lease_token)
+        with self._transaction() as connection:
+            now = _utc_now(self._clock)
+            now_text = _timestamp(now)
+            row = self._leased_row(connection, "edge_tasks", "task_id", task_id, lease_token)
+            if row["deadline_epoch_us"] <= _epoch_microseconds(now):
+                raise EdgeQueueLeaseError("task deadline has expired")
+            if row["remote_lease_token_digest"] != remote_digest:
+                raise EdgeQueueLeaseError("current remote lease token is required")
+            remote_expiry = row["remote_lease_expires_at"]
+            if remote_expiry is None or datetime.fromisoformat(
+                remote_expiry.replace("Z", "+00:00")
+            ) <= now:
+                raise EdgeQueueLeaseError("remote lease has expired")
+            task_mapping = json.loads(row["task_json"])
+            if row["remote_attempt"] != task_mapping["attempt"]:
+                raise EdgeQueueLeaseError("remote lease attempt is stale")
+            for field in ("task_id", "gateway_id", "environment", "network_zone", "purpose", "policy_digest"):
+                if contract.to_mapping()[field] != task_mapping[field]:
+                    raise EdgeQueueConflictError(f"event {field} does not bind the task")
+            if contract.attempt != task_mapping["attempt"]:
+                raise EdgeQueueConflictError("event attempt does not bind the task")
+            existing = connection.execute(
+                "SELECT digest FROM edge_outbound_events WHERE event_id=?", (contract.event_id,)
+            ).fetchone()
+            if existing is not None:
+                raise EdgeQueueConflictError("event ID already exists before task completion")
+            connection.execute(
+                """
+                INSERT INTO edge_outbound_events (
+                    event_id,digest,event_json,status,attempt_count,available_at,
+                    task_digest,remote_lease_token_digest,created_at,updated_at
+                ) VALUES (?,?,?,'pending',0,?,?,?,?,?)
+                """,
+                (
+                    contract.event_id, contract.digest, encoded_event, now_text,
+                    row["digest"], remote_digest, now_text, now_text,
+                ),
+            )
+            for artifact in normalized_artifacts:
+                connection.execute(
+                    """
+                    INSERT INTO edge_local_artifacts (
+                        artifact_digest,artifact_ref,artifact_ref_hash,classification,
+                        retention_until,task_id,event_id,cleanup_status,attempt_count,
+                        available_at,created_at,updated_at
+                    ) VALUES (?,?,?,?,?,?,?,'pending',0,?,?,?)
+                    """,
+                    (
+                        artifact["artifact_digest"], artifact["artifact_ref"],
+                        artifact["artifact_ref_hash"], artifact["classification"],
+                        artifact["retention_until"], task_id, contract.event_id,
+                        now_text, now_text, now_text,
+                    ),
+                )
+            updated = connection.execute(
+                """
+                UPDATE edge_tasks SET status='completed', lease_owner=NULL,
+                    lease_token=NULL, lease_expires_at=NULL, error_code=NULL, updated_at=?
+                WHERE task_id=? AND status='leased' AND lease_token=?
+                """,
+                (now_text, task_id, lease_token),
+            )
+            if updated.rowcount != 1:
+                raise EdgeQueueLeaseError("task completion lost its lease")
+            task_row = connection.execute("SELECT * FROM edge_tasks WHERE task_id=?", (task_id,)).fetchone()
+            event_row = connection.execute("SELECT * FROM edge_outbound_events WHERE event_id=?", (contract.event_id,)).fetchone()
+            return self._task_record(task_row), self._event_record(event_row)
+
+    def list_local_artifacts(self, *, task_id: str) -> tuple[LocalArtifact, ...]:
+        connection = self._connect()
+        try:
+            rows = connection.execute(
+                "SELECT * FROM edge_local_artifacts WHERE task_id=? ORDER BY created_at,artifact_digest",
+                (task_id,),
+            ).fetchall()
+        finally:
+            connection.close()
+        return tuple(self._artifact_record(row) for row in rows)
+
+    def claim_artifact_cleanup(self, lease_owner: str) -> LocalArtifact | None:
+        owner = self._lease_owner(lease_owner)
+        with self._transaction() as connection:
+            now = _utc_now(self._clock)
+            now_text = _timestamp(now)
+            expires_at = _timestamp(now + timedelta(seconds=self.lease_seconds))
+            connection.execute(
+                """UPDATE edge_local_artifacts SET cleanup_status='failed',
+                lease_owner=NULL,lease_token=NULL,lease_expires_at=NULL,
+                error_code='lease_attempts_exhausted',updated_at=?
+                WHERE cleanup_status='deleting'
+                AND julianday(lease_expires_at)<=julianday(?) AND attempt_count>=?""",
+                (now_text, now_text, self.max_attempts),
+            )
+            row = connection.execute(
+                """SELECT * FROM edge_local_artifacts WHERE attempt_count<?
+                AND julianday(retention_until)<=julianday(?) AND (
+                  (cleanup_status='pending' AND julianday(available_at)<=julianday(?))
+                  OR (cleanup_status='deleting'
+                      AND julianday(lease_expires_at)<=julianday(?)))
+                ORDER BY retention_until,created_at,artifact_digest LIMIT 1""",
+                (self.max_attempts, now_text, now_text, now_text),
+            ).fetchone()
+            if row is None:
+                return None
+            lease_token = str(uuid.uuid4())
+            connection.execute(
+                """UPDATE edge_local_artifacts SET cleanup_status='deleting',
+                attempt_count=attempt_count+1,lease_owner=?,lease_token=?,
+                lease_expires_at=?,error_code=NULL,updated_at=?
+                WHERE artifact_digest=? AND attempt_count=? AND (
+                  (cleanup_status='pending' AND julianday(available_at)<=julianday(?))
+                  OR (cleanup_status='deleting'
+                      AND julianday(lease_expires_at)<=julianday(?)))""",
+                (
+                    owner, lease_token, expires_at, now_text, row["artifact_digest"],
+                    row["attempt_count"], now_text, now_text,
+                ),
+            )
+            claimed = connection.execute(
+                "SELECT * FROM edge_local_artifacts WHERE artifact_digest=?",
+                (row["artifact_digest"],),
+            ).fetchone()
+            return self._artifact_record(claimed)
+
+    def acknowledge_artifact_cleanup(
+        self,
+        artifact_digest: str,
+        *,
+        lease_token: str,
+        receipt: Mapping[str, object],
+    ) -> LocalArtifact:
+        required = {"artifact_digest", "artifact_ref_hash", "deleted_at", "status"}
+        with self._transaction() as connection:
+            row = self._leased_row(
+                connection, "edge_local_artifacts", "artifact_digest",
+                artifact_digest, lease_token, status="deleting",
+                status_column="cleanup_status",
+            )
+            if (
+                not isinstance(receipt, Mapping)
+                or set(receipt) != required
+                or receipt.get("artifact_digest") != artifact_digest
+                or receipt.get("artifact_ref_hash") != row["artifact_ref_hash"]
+                or receipt.get("status") != "deleted"
+            ):
+                raise EdgePolicyError("artifact deletion receipt is invalid")
+            try:
+                deleted_at = canonical_timestamp(receipt["deleted_at"], "deleted_at")
+            except EdgeContractError as exc:
+                raise EdgePolicyError("artifact deletion receipt is invalid") from exc
+            normalized = {
+                "artifact_digest": artifact_digest,
+                "artifact_ref_hash": row["artifact_ref_hash"],
+                "deleted_at": deleted_at,
+                "status": "deleted",
+            }
+            now_text = _timestamp(_utc_now(self._clock))
+            connection.execute(
+                """UPDATE edge_local_artifacts SET artifact_ref=NULL,
+                cleanup_status='deleted',deletion_receipt_json=?,lease_owner=NULL,
+                lease_token=NULL,lease_expires_at=NULL,error_code=NULL,updated_at=?
+                WHERE artifact_digest=? AND cleanup_status='deleting' AND lease_token=?""",
+                (
+                    strict_json_bytes(normalized).decode("utf-8"), now_text,
+                    artifact_digest, lease_token,
+                ),
+            )
+            updated = connection.execute(
+                "SELECT * FROM edge_local_artifacts WHERE artifact_digest=?",
+                (artifact_digest,),
+            ).fetchone()
+            return self._artifact_record(updated)
+
+    def fail_artifact_cleanup(
+        self, artifact_digest: str, *, lease_token: str, error_code: str
+    ) -> LocalArtifact:
+        safe_error = self._error_code(error_code)
+        with self._transaction() as connection:
+            now = _utc_now(self._clock)
+            now_text = _timestamp(now)
+            row = self._leased_row(
+                connection, "edge_local_artifacts", "artifact_digest",
+                artifact_digest, lease_token, status="deleting",
+                status_column="cleanup_status",
+            )
+            terminal = row["attempt_count"] >= self.max_attempts
+            status = "failed" if terminal else "pending"
+            available_at = now_text if terminal else _timestamp(
+                now + timedelta(seconds=self._retry_delay(row["attempt_count"]))
+            )
+            connection.execute(
+                """UPDATE edge_local_artifacts SET cleanup_status=?,available_at=?,
+                lease_owner=NULL,lease_token=NULL,lease_expires_at=NULL,error_code=?,
+                updated_at=? WHERE artifact_digest=? AND cleanup_status='deleting'
+                AND lease_token=?""",
+                (
+                    status, available_at, safe_error, now_text,
+                    artifact_digest, lease_token,
+                ),
+            )
+            updated = connection.execute(
+                "SELECT * FROM edge_local_artifacts WHERE artifact_digest=?",
+                (artifact_digest,),
+            ).fetchone()
+            return self._artifact_record(updated)
+
+    def put_release_state(
+        self,
+        *,
+        release_id: str,
+        manifest_digest: str,
+        version: str,
+        rollback_version: str,
+    ) -> ReleaseState:
+        release_id = self._bounded_identifier(release_id, "release_id")
+        manifest_digest = self._sha256(manifest_digest, "manifest_digest")
+        version = self._bounded_identifier(version, "version")
+        rollback_version = self._bounded_identifier(rollback_version, "rollback_version")
+        with self._transaction() as connection:
+            existing = connection.execute(
+                "SELECT * FROM edge_release_state WHERE release_id=?", (release_id,)
+            ).fetchone()
+            if existing is not None:
+                if (
+                    existing["manifest_digest"], existing["version"], existing["rollback_version"]
+                ) != (manifest_digest, version, rollback_version):
+                    raise EdgeQueueConflictError("release ID has different immutable content")
+                return ReleaseState(**dict(existing))
+            now_text = _timestamp(_utc_now(self._clock))
+            connection.execute(
+                """INSERT INTO edge_release_state
+                (release_id,manifest_digest,version,rollback_version,status,updated_at)
+                VALUES (?,?,?,?,'offered',?)""",
+                (release_id, manifest_digest, version, rollback_version, now_text),
+            )
+            row = connection.execute(
+                "SELECT * FROM edge_release_state WHERE release_id=?", (release_id,)
+            ).fetchone()
+            return ReleaseState(**dict(row))
+
+    def compare_and_set_release_state(
+        self,
+        release_id: str,
+        *,
+        expected_status: str,
+        target_status: str,
+        previous_version: str | None = None,
+        current_version: str | None = None,
+    ) -> ReleaseState:
+        release_id = self._bounded_identifier(release_id, "release_id")
+        allowed = {"offered", "accepted", "candidate", "installed", "failed", "rolled_back"}
+        if expected_status not in allowed or target_status not in allowed:
+            raise EdgeQueueConflictError("release status is invalid")
+        transitions = {
+            "offered": {"accepted", "failed"},
+            "accepted": {"candidate", "failed"},
+            "candidate": {"installed", "failed", "rolled_back"},
+            "installed": {"rolled_back", "failed"},
+            "failed": {"rolled_back"},
+            "rolled_back": set(),
+        }
+        if target_status not in transitions[expected_status]:
+            raise EdgeQueueConflictError("release state transition is not approved")
+        if previous_version is not None:
+            previous_version = self._bounded_identifier(previous_version, "previous_version")
+        if current_version is not None:
+            current_version = self._bounded_identifier(current_version, "current_version")
+        with self._transaction() as connection:
+            now_text = _timestamp(_utc_now(self._clock))
+            updated = connection.execute(
+                """UPDATE edge_release_state
+                SET status=?,previous_version=COALESCE(?,previous_version),
+                    current_version=COALESCE(?,current_version),updated_at=?
+                WHERE release_id=? AND status=?""",
+                (
+                    target_status, previous_version, current_version, now_text,
+                    release_id, expected_status,
+                ),
+            )
+            if updated.rowcount != 1:
+                row = connection.execute(
+                    "SELECT status FROM edge_release_state WHERE release_id=?", (release_id,)
+                ).fetchone()
+                if row is None:
+                    raise EdgeQueueError("release does not exist")
+                raise EdgeQueueConflictError("release state compare-and-set lost")
+            row = connection.execute(
+                "SELECT * FROM edge_release_state WHERE release_id=?", (release_id,)
+            ).fetchone()
+            return ReleaseState(**dict(row))
+
+    def get_release_state(self, release_id: str) -> ReleaseState | None:
+        release_id = self._bounded_identifier(release_id, "release_id")
+        connection = self._connect()
+        try:
+            row = connection.execute(
+                "SELECT * FROM edge_release_state WHERE release_id=?", (release_id,)
+            ).fetchone()
+        finally:
+            connection.close()
+        return ReleaseState(**dict(row)) if row is not None else None
+
+    def list_release_states(self) -> tuple[ReleaseState, ...]:
+        connection = self._connect()
+        try:
+            rows = connection.execute(
+                "SELECT * FROM edge_release_state ORDER BY updated_at,release_id"
+            ).fetchall()
+        finally:
+            connection.close()
+        return tuple(ReleaseState(**dict(row)) for row in rows)
+
+    def complete(self, task_id: str, *, lease_token: str) -> QueuedTask:
+        return self._finish_task(
+            task_id,
+            lease_token=lease_token,
+            target_status="completed",
+            error_code=None,
+        )
+
+    def fail(
+        self,
+        task_id: str,
+        *,
+        lease_token: str,
+        error_code: str,
+        retryable: bool = True,
+    ) -> QueuedTask:
+        safe_error = self._error_code(error_code)
+        if not isinstance(retryable, bool):
+            raise ValueError("retryable must be a boolean")
+        with self._transaction() as connection:
+            now = _utc_now(self._clock)
+            now_text = _timestamp(now)
+            row = self._leased_row(
+                connection, "edge_tasks", "task_id", task_id, lease_token
+            )
+            terminal = not retryable or row["attempt_count"] >= self.max_attempts
+            status = "failed" if terminal else "pending"
+            available_at = now_text
+            if not terminal:
+                available_at = _timestamp(
+                    now + timedelta(seconds=self._retry_delay(row["attempt_count"]))
+                )
+            connection.execute(
+                """
+                UPDATE edge_tasks
+                SET status = ?, available_at = ?, lease_owner = NULL,
+                    lease_token = NULL, lease_expires_at = NULL,
+                    error_code = ?, updated_at = ?
+                WHERE task_id = ? AND status = 'leased' AND lease_token = ?
+                """,
+                (status, available_at, safe_error, now_text, task_id, lease_token),
+            )
+            updated = connection.execute(
+                "SELECT * FROM edge_tasks WHERE task_id = ?", (task_id,)
+            ).fetchone()
+            return self._task_record(updated)
+
+    def _finish_task(
+        self,
+        task_id: str,
+        *,
+        lease_token: str,
+        target_status: str,
+        error_code: str | None,
+    ) -> QueuedTask:
+        with self._transaction() as connection:
+            now_text = _timestamp(_utc_now(self._clock))
+            self._leased_row(
+                connection, "edge_tasks", "task_id", task_id, lease_token
+            )
+            connection.execute(
+                """
+                UPDATE edge_tasks
+                SET status = ?, lease_owner = NULL, lease_token = NULL,
+                    lease_expires_at = NULL, error_code = ?, updated_at = ?
+                WHERE task_id = ? AND status = 'leased' AND lease_token = ?
+                """,
+                (target_status, error_code, now_text, task_id, lease_token),
+            )
+            row = connection.execute(
+                "SELECT * FROM edge_tasks WHERE task_id = ?", (task_id,)
+            ).fetchone()
+            return self._task_record(row)
+
+    def cancel(self, task_id: str) -> QueuedTask:
+        with self._transaction() as connection:
+            now_text = _timestamp(_utc_now(self._clock))
+            row = connection.execute(
+                "SELECT * FROM edge_tasks WHERE task_id = ?", (task_id,)
+            ).fetchone()
+            if row is None:
+                raise EdgeQueueError("task does not exist")
+            if row["status"] in {"pending", "leased"}:
+                connection.execute(
+                    """
+                    UPDATE edge_tasks
+                    SET status = 'cancelled', lease_owner = NULL,
+                        lease_token = NULL, lease_expires_at = NULL,
+                        updated_at = ?
+                    WHERE task_id = ? AND status IN ('pending', 'leased')
+                    """,
+                    (now_text, task_id),
+                )
+                row = connection.execute(
+                    "SELECT * FROM edge_tasks WHERE task_id = ?", (task_id,)
+                ).fetchone()
+            return self._task_record(row)
+
+    def cancel_with_outcome(self, task_id: str) -> QueuedOutcome:
+        """Atomically fence local work and persist the cancelled control outcome."""
+
+        task_id = self._bounded_identifier(task_id, "task_id")
+        with self._transaction() as connection:
+            now_text = _timestamp(_utc_now(self._clock))
+            row = connection.execute(
+                "SELECT * FROM edge_tasks WHERE task_id = ?", (task_id,)
+            ).fetchone()
+            if row is None:
+                raise EdgeQueueError("task does not exist")
+            if row["status"] in {"pending", "leased"}:
+                connection.execute(
+                    """UPDATE edge_tasks SET status='cancelled',lease_owner=NULL,
+                    lease_token=NULL,lease_expires_at=NULL,updated_at=?
+                    WHERE task_id=? AND status IN ('pending','leased')""",
+                    (now_text, task_id),
+                )
+                row = connection.execute(
+                    "SELECT * FROM edge_tasks WHERE task_id=?", (task_id,)
+                ).fetchone()
+            if row["status"] != "cancelled":
+                raise EdgeQueueConflictError("only a cancelled task can emit cancellation")
+            remote_digest = row["remote_lease_token_digest"]
+            if not isinstance(remote_digest, str):
+                raise EdgeQueueLeaseError("cancel outcome requires a remote lease binding")
+            outcome = {
+                "task_id": task_id,
+                "outcome": "cancelled",
+                "safe_summary": {"task_status": "cancelled"},
+            }
+            digest = canonical_sha256(outcome)
+            encoded = strict_json_bytes(outcome).decode("utf-8")
+            existing = connection.execute(
+                "SELECT * FROM edge_task_outcomes WHERE task_id=?", (task_id,)
+            ).fetchone()
+            if existing is not None:
+                if (
+                    existing["digest"] != digest
+                    or existing["remote_lease_token_digest"] != remote_digest
+                ):
+                    raise EdgeQueueConflictError("cancel outcome replay conflicts")
+                return self._outcome_record(existing)
+            connection.execute(
+                """INSERT INTO edge_task_outcomes(
+                task_id,digest,outcome_json,status,attempt_count,available_at,
+                remote_lease_token_digest,created_at,updated_at
+                ) VALUES (?,?,?,'pending',0,?,?,?,?)""",
+                (task_id, digest, encoded, now_text, remote_digest, now_text, now_text),
+            )
+            inserted = connection.execute(
+                "SELECT * FROM edge_task_outcomes WHERE task_id=?", (task_id,)
+            ).fetchone()
+            return self._outcome_record(inserted)
+
+    def claim_outcome(self, lease_owner: str) -> QueuedOutcome | None:
+        owner = self._lease_owner(lease_owner)
+        with self._transaction() as connection:
+            now = _utc_now(self._clock)
+            now_text = _timestamp(now)
+            expires_at = _timestamp(now + timedelta(seconds=self.lease_seconds))
+            connection.execute(
+                """UPDATE edge_task_outcomes SET status='failed',lease_owner=NULL,
+                lease_token=NULL,lease_expires_at=NULL,error_code='lease_attempts_exhausted',
+                updated_at=? WHERE status='sending'
+                AND julianday(lease_expires_at)<=julianday(?) AND attempt_count>=?""",
+                (now_text, now_text, self.max_attempts),
+            )
+            row = connection.execute(
+                """SELECT * FROM edge_task_outcomes WHERE attempt_count<? AND (
+                (status='pending' AND julianday(available_at)<=julianday(?)) OR
+                (status='sending' AND julianday(lease_expires_at)<=julianday(?)))
+                ORDER BY created_at,task_id LIMIT 1""",
+                (self.max_attempts, now_text, now_text),
+            ).fetchone()
+            if row is None:
+                return None
+            lease_token = str(uuid.uuid4())
+            connection.execute(
+                """UPDATE edge_task_outcomes SET status='sending',
+                attempt_count=attempt_count+1,lease_owner=?,lease_token=?,
+                lease_expires_at=?,error_code=NULL,updated_at=?
+                WHERE task_id=? AND attempt_count=? AND (
+                (status='pending' AND julianday(available_at)<=julianday(?)) OR
+                (status='sending' AND julianday(lease_expires_at)<=julianday(?)))""",
+                (
+                    owner, lease_token, expires_at, now_text, row["task_id"],
+                    row["attempt_count"], now_text, now_text,
+                ),
+            )
+            claimed = connection.execute(
+                "SELECT * FROM edge_task_outcomes WHERE task_id=?", (row["task_id"],)
+            ).fetchone()
+            return self._outcome_record(claimed)
+
+    def acknowledge_outcome(
+        self,
+        task_id: str,
+        *,
+        lease_token: str,
+        acknowledgement: Mapping[str, object],
+    ) -> QueuedOutcome:
+        required = {"task_id", "status", "replayed"}
+        if (
+            not isinstance(acknowledgement, Mapping)
+            or set(acknowledgement) != required
+            or acknowledgement.get("task_id") != task_id
+            or acknowledgement.get("status") != "cancelled"
+            or not isinstance(acknowledgement.get("replayed"), bool)
+        ):
+            raise EdgePolicyError("cancel outcome acknowledgement is invalid")
+        encoded = strict_json_bytes(acknowledgement).decode("utf-8")
+        ack_digest = canonical_sha256(acknowledgement)
+        with self._transaction() as connection:
+            now_text = _timestamp(_utc_now(self._clock))
+            self._leased_row(
+                connection, "edge_task_outcomes", "task_id", task_id, lease_token,
+                status="sending",
+            )
+            connection.execute(
+                """UPDATE edge_task_outcomes SET status='acknowledged',
+                acknowledgement_json=?,ack_digest=?,lease_owner=NULL,lease_token=NULL,
+                lease_expires_at=NULL,updated_at=?
+                WHERE task_id=? AND status='sending' AND lease_token=?""",
+                (encoded, ack_digest, now_text, task_id, lease_token),
+            )
+            row = connection.execute(
+                "SELECT * FROM edge_task_outcomes WHERE task_id=?", (task_id,)
+            ).fetchone()
+            return self._outcome_record(row)
+
+    def fail_outcome(
+        self, task_id: str, *, lease_token: str, error_code: str
+    ) -> QueuedOutcome:
+        safe_error = self._error_code(error_code)
+        with self._transaction() as connection:
+            now = _utc_now(self._clock)
+            now_text = _timestamp(now)
+            row = self._leased_row(
+                connection, "edge_task_outcomes", "task_id", task_id, lease_token,
+                status="sending",
+            )
+            terminal = row["attempt_count"] >= self.max_attempts
+            status = "failed" if terminal else "pending"
+            available_at = now_text if terminal else _timestamp(
+                now + timedelta(seconds=self._retry_delay(row["attempt_count"]))
+            )
+            connection.execute(
+                """UPDATE edge_task_outcomes SET status=?,available_at=?,lease_owner=NULL,
+                lease_token=NULL,lease_expires_at=NULL,error_code=?,updated_at=?
+                WHERE task_id=? AND status='sending' AND lease_token=?""",
+                (
+                    status, available_at, safe_error, now_text, task_id, lease_token,
+                ),
+            )
+            updated = connection.execute(
+                "SELECT * FROM edge_task_outcomes WHERE task_id=?", (task_id,)
+            ).fetchone()
+            return self._outcome_record(updated)
+
+    def get_outcome(self, task_id: str) -> QueuedOutcome | None:
+        connection = self._connect()
+        try:
+            row = connection.execute(
+                "SELECT * FROM edge_task_outcomes WHERE task_id=?", (task_id,)
+            ).fetchone()
+        finally:
+            connection.close()
+        return self._outcome_record(row) if row is not None else None
+
+    def get_task(self, task_id: str) -> QueuedTask | None:
+        connection = self._connect()
+        try:
+            row = connection.execute(
+                "SELECT * FROM edge_tasks WHERE task_id = ?", (task_id,)
+            ).fetchone()
+        finally:
+            connection.close()
+        return self._task_record(row) if row is not None else None
+
+    def persist_event(
+        self, event: EdgeEventContract | Mapping[str, object]
+    ) -> QueuedEvent:
+        raw = event.to_mapping() if isinstance(event, EdgeEventContract) else dict(event)
+        try:
+            contract = EdgeEventContract.from_mapping(raw)
+        except EdgeContractError as exc:
+            raw_id = raw.get("event_id")
+            if isinstance(raw_id, str) and raw_id:
+                with self._transaction() as connection:
+                    existing = connection.execute(
+                        "SELECT 1 FROM edge_outbound_events WHERE event_id = ?",
+                        (raw_id,),
+                    ).fetchone()
+                if existing is not None:
+                    raise EdgeQueueConflictError(
+                        "event ID already exists with a different digest"
+                    ) from exc
+            raise
+        normalized = contract.to_mapping()
+        digest = contract.digest
+        with self._transaction() as connection:
+            now_text = _timestamp(_utc_now(self._clock))
+            existing = connection.execute(
+                "SELECT * FROM edge_outbound_events WHERE event_id = ?",
+                (contract.event_id,),
+            ).fetchone()
+            if existing is not None:
+                if existing["digest"] != digest:
+                    raise EdgeQueueConflictError(
+                        "event ID already exists with a different digest"
+                    )
+                return self._event_record(existing)
+            approved = self.egress_policy.approve_event(normalized)
+            encoded = strict_json_bytes(approved).decode("utf-8")
+            connection.execute(
+                """
+                INSERT INTO edge_outbound_events (
+                    event_id, digest, event_json, status, attempt_count,
+                    available_at, created_at, updated_at
+                ) VALUES (?, ?, ?, 'pending', 0, ?, ?, ?)
+                """,
+                (contract.event_id, digest, encoded, now_text, now_text, now_text),
+            )
+            row = connection.execute(
+                "SELECT * FROM edge_outbound_events WHERE event_id = ?",
+                (contract.event_id,),
+            ).fetchone()
+            return self._event_record(row)
+
+    def claim_event(self, lease_owner: str) -> QueuedEvent | None:
+        owner = self._lease_owner(lease_owner)
+        with self._transaction() as connection:
+            now = _utc_now(self._clock)
+            now_text = _timestamp(now)
+            lease_token = str(uuid.uuid4())
+            expires_at = _timestamp(now + timedelta(seconds=self.lease_seconds))
+            connection.execute(
+                """
+                UPDATE edge_outbound_events
+                SET status = 'failed', lease_owner = NULL, lease_token = NULL,
+                    lease_expires_at = NULL,
+                    error_code = 'lease_attempts_exhausted', updated_at = ?
+                WHERE status = 'sending'
+                  AND julianday(lease_expires_at) <= julianday(?)
+                  AND attempt_count >= ?
+                """,
+                (now_text, now_text, self.max_attempts),
+            )
+            row = connection.execute(
+                """
+                SELECT * FROM edge_outbound_events
+                WHERE attempt_count < ? AND (
+                    (status = 'pending' AND julianday(available_at) <= julianday(?))
+                    OR (status = 'sending'
+                        AND julianday(lease_expires_at) <= julianday(?))
+                )
+                ORDER BY created_at, event_id
+                LIMIT 1
+                """,
+                (self.max_attempts, now_text, now_text),
+            ).fetchone()
+            if row is None:
+                return None
+            connection.execute(
+                """
+                UPDATE edge_outbound_events
+                SET status = 'sending', attempt_count = attempt_count + 1,
+                    lease_owner = ?, lease_token = ?, lease_expires_at = ?,
+                    error_code = NULL, updated_at = ?
+                WHERE event_id = ? AND attempt_count = ? AND (
+                    (status = 'pending' AND julianday(available_at) <= julianday(?))
+                    OR (status = 'sending'
+                        AND julianday(lease_expires_at) <= julianday(?))
+                )
+                """,
+                (
+                    owner,
+                    lease_token,
+                    expires_at,
+                    now_text,
+                    row["event_id"],
+                    row["attempt_count"],
+                    now_text,
+                    now_text,
+                ),
+            )
+            claimed = connection.execute(
+                "SELECT * FROM edge_outbound_events WHERE event_id = ?",
+                (row["event_id"],),
+            ).fetchone()
+            return self._event_record(claimed)
+
+    def acknowledge_event(
+        self,
+        event_id: str,
+        *,
+        lease_token: str,
+        acknowledgement: Mapping[str, object],
+    ) -> QueuedEvent:
+        token_digest = canonical_sha256(lease_token)
+        with self._transaction() as connection:
+            now_text = _timestamp(_utc_now(self._clock))
+            existing = connection.execute(
+                "SELECT * FROM edge_outbound_events WHERE event_id = ?", (event_id,)
+            ).fetchone()
+            if existing is None:
+                raise EdgeQueueError("event does not exist")
+            normalized_ack = self._validate_acknowledgement(
+                acknowledgement,
+                event_id=event_id,
+                event_digest=existing["digest"],
+                remote_lease_token_digest=existing["remote_lease_token_digest"],
+            )
+            encoded = strict_json_bytes(normalized_ack)
+            if len(encoded) > 4_096:
+                raise EdgePolicyError("acknowledgement exceeds the byte limit")
+            ack_digest = canonical_sha256(normalized_ack)
+            if existing["status"] == "acknowledged":
+                if existing["lease_token_digest"] != token_digest:
+                    raise EdgeQueueLeaseError(
+                        "acknowledgement replay requires the original lease token"
+                    )
+                if existing["ack_digest"] != ack_digest:
+                    raise EdgeQueueConflictError(
+                        "acknowledgement replay has a different digest"
+                    )
+                return self._event_record(existing)
+            self._leased_row(
+                connection,
+                "edge_outbound_events",
+                "event_id",
+                event_id,
+                lease_token,
+                status="sending",
+            )
+            connection.execute(
+                """
+                UPDATE edge_outbound_events
+                SET status = 'acknowledged', acknowledgement_json = ?,
+                    ack_digest = ?, lease_token_digest = ?,
+                    control_lease_token_digest = ?,
+                    lease_owner = NULL, lease_token = NULL,
+                    lease_expires_at = NULL, updated_at = ?
+                WHERE event_id = ? AND status = 'sending' AND lease_token = ?
+                """,
+                (
+                    encoded.decode("utf-8"),
+                    ack_digest,
+                    token_digest,
+                    token_digest,
+                    now_text,
+                    event_id,
+                    lease_token,
+                ),
+            )
+            row = connection.execute(
+                "SELECT * FROM edge_outbound_events WHERE event_id = ?", (event_id,)
+            ).fetchone()
+            return self._event_record(row)
+
+    def fail_event(
+        self, event_id: str, *, lease_token: str, error_code: str
+    ) -> QueuedEvent:
+        safe_error = self._error_code(error_code)
+        with self._transaction() as connection:
+            now = _utc_now(self._clock)
+            now_text = _timestamp(now)
+            row = self._leased_row(
+                connection,
+                "edge_outbound_events",
+                "event_id",
+                event_id,
+                lease_token,
+                status="sending",
+            )
+            terminal = row["attempt_count"] >= self.max_attempts
+            status = "failed" if terminal else "pending"
+            available_at = now_text
+            if not terminal:
+                available_at = _timestamp(
+                    now + timedelta(seconds=self._retry_delay(row["attempt_count"]))
+                )
+            connection.execute(
+                """
+                UPDATE edge_outbound_events
+                SET status = ?, available_at = ?, lease_owner = NULL,
+                    lease_token = NULL, lease_expires_at = NULL,
+                    error_code = ?, updated_at = ?
+                WHERE event_id = ? AND status = 'sending' AND lease_token = ?
+                """,
+                (status, available_at, safe_error, now_text, event_id, lease_token),
+            )
+            updated = connection.execute(
+                "SELECT * FROM edge_outbound_events WHERE event_id = ?", (event_id,)
+            ).fetchone()
+            return self._event_record(updated)
+
+    def get_event(self, event_id: str) -> QueuedEvent | None:
+        connection = self._connect()
+        try:
+            row = connection.execute(
+                "SELECT * FROM edge_outbound_events WHERE event_id = ?", (event_id,)
+            ).fetchone()
+        finally:
+            connection.close()
+        return self._event_record(row) if row is not None else None
+
+    @staticmethod
+    def _validate_local_artifact(value: Mapping[str, object]) -> dict[str, str]:
+        required = {
+            "artifact_digest", "artifact_ref", "artifact_ref_hash",
+            "classification", "retention_until",
+        }
+        if not isinstance(value, Mapping) or set(value) != required:
+            raise EdgePolicyError("local artifact requires exact metadata fields")
+        digest = value["artifact_digest"]
+        reference = value["artifact_ref"]
+        reference_hash = value["artifact_ref_hash"]
+        classification = value["classification"]
+        if not isinstance(digest, str) or not re.fullmatch(r"[0-9a-f]{64}", digest):
+            raise EdgePolicyError("local artifact digest is invalid")
+        if (
+            not isinstance(reference, str)
+            or not Path(reference).is_absolute()
+            or len(reference.encode("utf-8")) > 2_048
+            or not isinstance(reference_hash, str)
+            or not re.fullmatch(r"[0-9a-f]{64}", reference_hash)
+            or reference_hash != canonical_sha256(reference)
+        ):
+            raise EdgePolicyError("local artifact reference is invalid")
+        if classification not in {
+            "raw", "recent_detail", "restricted", "desensitized_metadata",
+            "statistics", "lineage", "evidence",
+        }:
+            raise EdgePolicyError("local artifact classification is invalid")
+        try:
+            retention = canonical_timestamp(value["retention_until"], "retention_until")
+        except EdgeContractError as exc:
+            raise EdgePolicyError("local artifact retention is invalid") from exc
+        return {
+            "artifact_digest": digest,
+            "artifact_ref": reference,  # type: ignore[dict-item]
+            "artifact_ref_hash": reference_hash,  # type: ignore[dict-item]
+            "classification": classification,  # type: ignore[dict-item]
+            "retention_until": retention,
+        }
+
+    def _normalize_remote_binding(
+        self, token: object, expires_at: object, attempt: object
+    ) -> tuple[str, str | None, str, int]:
+        if (
+            not isinstance(token, str) or not token.strip()
+            or len(token.encode("utf-8")) > 1024
+        ):
+            raise EdgeQueueLeaseError("remote lease token is invalid")
+        if isinstance(attempt, bool) or not isinstance(attempt, int) or attempt < 1:
+            raise EdgeQueueLeaseError("remote lease attempt is invalid")
+        try:
+            expiry = canonical_timestamp(expires_at, "remote_lease_expires_at")
+        except EdgeContractError as exc:
+            raise EdgeQueueLeaseError("remote lease expiry is invalid") from exc
+        ciphertext: str | None = None
+        if self._remote_lease_encoder is not None:
+            try:
+                ciphertext = self._remote_lease_encoder(token)
+            except Exception as exc:
+                raise EdgeQueueLeaseError("remote lease protection failed") from exc
+            if (
+                not isinstance(ciphertext, str) or not ciphertext
+                or ciphertext == token or len(ciphertext.encode("utf-8")) > 4096
+            ):
+                raise EdgeQueueLeaseError("remote lease protection produced invalid ciphertext")
+        return self._opaque_token_digest(token), ciphertext, expiry, attempt
+
+    @staticmethod
+    def _opaque_token_digest(token: str) -> str:
+        return hashlib.sha256(token.encode("utf-8")).hexdigest()
+
+    def _retry_delay(self, attempt_count: int) -> int:
+        base = min(
+            self.retry_max_seconds,
+            self.retry_base_seconds * (2 ** max(0, attempt_count - 1)),
+        )
+        sample = self._random()
+        if isinstance(sample, bool) or not isinstance(sample, (int, float)) or not 0 <= sample <= 1:
+            raise EdgeQueueError("retry random source returned an invalid value")
+        return min(
+            self.retry_max_seconds,
+            max(1, round(base * (0.5 + float(sample)))),
+        )
+
+    @staticmethod
+    def _bounded_identifier(value: object, label: str) -> str:
+        if (
+            not isinstance(value, str) or not value or value.strip() != value
+            or len(value.encode("utf-8")) > 255
+            or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._:-]{0,254}", value)
+        ):
+            raise EdgeQueueError(f"{label} is invalid")
+        return value
+
+    @staticmethod
+    def _sha256(value: object, label: str) -> str:
+        if not isinstance(value, str) or not re.fullmatch(r"[0-9a-f]{64}", value):
+            raise EdgeQueueError(f"{label} is invalid")
+        return value
+
+    @staticmethod
+    def _lease_owner(value: object) -> str:
+        if (
+            not isinstance(value, str)
+            or not value
+            or len(value.encode("utf-8")) > 255
+            or value.strip() != value
+        ):
+            raise ValueError("lease_owner is invalid")
+        return value
+
+    @staticmethod
+    def _error_code(value: object) -> str:
+        if not isinstance(value, str) or not _ERROR_CODE.fullmatch(value):
+            raise EdgePolicyError("error_code must be a bounded code")
+        if any(part in value for part in _SENSITIVE_ERROR_PARTS):
+            raise EdgePolicyError("error_code contains sensitive content")
+        return value
+
+    def _leased_row(
+        self,
+        connection: sqlite3.Connection,
+        table: str,
+        id_column: str,
+        identity: str,
+        lease_token: str,
+        *,
+        status: str = "leased",
+        status_column: str = "status",
+    ) -> sqlite3.Row:
+        row = connection.execute(
+            f"SELECT * FROM {table} WHERE {id_column} = ?",  # noqa: S608
+            (identity,),
+        ).fetchone()
+        if row is None:
+            raise EdgeQueueError("queue item does not exist")
+        if row[status_column] != status or row["lease_token"] != lease_token:
+            raise EdgeQueueLeaseError("current lease token is required")
+        lease_expires_at = row["lease_expires_at"]
+        if lease_expires_at is None:
+            raise EdgeQueueLeaseError("lease has expired")
+        parsed_expiry = datetime.fromisoformat(lease_expires_at.replace("Z", "+00:00"))
+        if parsed_expiry <= _utc_now(self._clock):
+            raise EdgeQueueLeaseError("lease has expired")
+        return row
+
+    @staticmethod
+    def _validate_acknowledgement(
+        value: Mapping[str, object],
+        *,
+        event_id: str,
+        event_digest: str,
+        remote_lease_token_digest: str | None,
+    ) -> dict[str, str]:
+        if remote_lease_token_digest is not None:
+            required = {"event_id", "event_digest", "remote_lease_digest", "received_at", "status"}
+            if not isinstance(value, Mapping) or set(value) != required:
+                raise EdgePolicyError("bound acknowledgement requires exact approved fields")
+            if value["event_id"] != event_id or value["event_digest"] != event_digest:
+                raise EdgePolicyError("acknowledgement does not bind the event")
+            if value["remote_lease_digest"] != remote_lease_token_digest:
+                raise EdgePolicyError("acknowledgement remote lease is invalid")
+            if value["status"] != "accepted":
+                raise EdgePolicyError("acknowledgement status is invalid")
+            try:
+                received_at = canonical_timestamp(value["received_at"], "received_at")
+            except EdgeContractError as exc:
+                raise EdgePolicyError("acknowledgement received_at is invalid") from exc
+            return {
+                "event_id": event_id,
+                "event_digest": event_digest,
+                "remote_lease_digest": remote_lease_token_digest,
+                "received_at": received_at,
+                "status": "accepted",
+            }
+        if not isinstance(value, Mapping) or set(value) != {
+            "message_id",
+            "received_at",
+            "status",
+        }:
+            raise EdgePolicyError("acknowledgement requires exact approved fields")
+        message_id = value["message_id"]
+        status = value["status"]
+        if (
+            not isinstance(message_id, str)
+            or not message_id
+            or message_id.strip() != message_id
+            or len(message_id.encode("utf-8")) > 255
+        ):
+            raise EdgePolicyError("acknowledgement message_id is invalid")
+        if status != "accepted":
+            raise EdgePolicyError("acknowledgement status is invalid")
+        try:
+            received_at = canonical_timestamp(value["received_at"], "received_at")
+        except EdgeContractError as exc:
+            raise EdgePolicyError("acknowledgement received_at is invalid") from exc
+        return {
+            "message_id": message_id,
+            "received_at": received_at,
+            "status": status,
+        }
+
+    @staticmethod
+    def _task_record(row: sqlite3.Row) -> QueuedTask:
+        task = json.loads(row["task_json"])
+        return QueuedTask(
+            task_id=row["task_id"],
+            digest=row["digest"],
+            status=row["status"],
+            task=_freeze_mapping(task),
+            attempt_count=row["attempt_count"],
+            available_at=row["available_at"],
+            lease_owner=row["lease_owner"],
+            lease_token=row["lease_token"],
+            lease_expires_at=row["lease_expires_at"],
+            error_code=row["error_code"],
+            authority_key_id=row["authority_key_id"],
+            authority_digest=row["authority_digest"],
+            remote_lease_token_digest=row["remote_lease_token_digest"],
+            remote_lease_expires_at=row["remote_lease_expires_at"],
+            remote_attempt=row["remote_attempt"],
+            created_at=row["created_at"],
+            updated_at=row["updated_at"],
+        )
+
+    @staticmethod
+    def _event_record(row: sqlite3.Row) -> QueuedEvent:
+        event = json.loads(row["event_json"])
+        acknowledgement = (
+            json.loads(row["acknowledgement_json"])
+            if row["acknowledgement_json"] is not None
+            else None
+        )
+        return QueuedEvent(
+            event_id=row["event_id"],
+            digest=row["digest"],
+            status=row["status"],
+            event=_freeze_mapping(event),
+            attempt_count=row["attempt_count"],
+            available_at=row["available_at"],
+            lease_owner=row["lease_owner"],
+            lease_token=row["lease_token"],
+            lease_expires_at=row["lease_expires_at"],
+            error_code=row["error_code"],
+            acknowledgement=(
+                _freeze_mapping(acknowledgement)
+                if acknowledgement is not None
+                else None
+            ),
+            task_digest=row["task_digest"],
+            remote_lease_token_digest=row["remote_lease_token_digest"],
+            control_lease_token_digest=row["control_lease_token_digest"],
+            created_at=row["created_at"],
+            updated_at=row["updated_at"],
+        )
+
+    @staticmethod
+    def _artifact_record(row: sqlite3.Row) -> LocalArtifact:
+        receipt = (
+            json.loads(row["deletion_receipt_json"])
+            if row["deletion_receipt_json"] is not None
+            else None
+        )
+        return LocalArtifact(
+            artifact_digest=row["artifact_digest"],
+            artifact_ref=row["artifact_ref"],
+            artifact_ref_hash=row["artifact_ref_hash"],
+            classification=row["classification"],
+            retention_until=row["retention_until"],
+            task_id=row["task_id"],
+            event_id=row["event_id"],
+            cleanup_status=row["cleanup_status"],
+            attempt_count=row["attempt_count"],
+            available_at=row["available_at"],
+            lease_owner=row["lease_owner"],
+            lease_token=row["lease_token"],
+            lease_expires_at=row["lease_expires_at"],
+            error_code=row["error_code"],
+            deletion_receipt=_freeze_mapping(receipt) if receipt is not None else None,
+            created_at=row["created_at"],
+            updated_at=row["updated_at"],
+        )
+
+    @staticmethod
+    def _outcome_record(row: sqlite3.Row) -> QueuedOutcome:
+        outcome = json.loads(row["outcome_json"])
+        acknowledgement = (
+            json.loads(row["acknowledgement_json"])
+            if row["acknowledgement_json"] is not None
+            else None
+        )
+        return QueuedOutcome(
+            task_id=row["task_id"],
+            digest=row["digest"],
+            status=row["status"],
+            outcome=_freeze_mapping(outcome),
+            attempt_count=row["attempt_count"],
+            available_at=row["available_at"],
+            lease_owner=row["lease_owner"],
+            lease_token=row["lease_token"],
+            lease_expires_at=row["lease_expires_at"],
+            error_code=row["error_code"],
+            acknowledgement=(
+                _freeze_mapping(acknowledgement)
+                if acknowledgement is not None
+                else None
+            ),
+            remote_lease_token_digest=row["remote_lease_token_digest"],
+            created_at=row["created_at"],
+            updated_at=row["updated_at"],
+        )

+ 609 - 0
app/core/edge_gateway/repository.py

@@ -0,0 +1,609 @@
+"""Transactional PostgreSQL repository for the edge control plane."""
+
+from __future__ import annotations
+
+import hashlib
+import json
+
+from sqlalchemy import text
+
+from app.core.common.identifiers import new_governance_uid
+
+_OVERFLOW_FINGERPRINT = hashlib.sha256(b"edge-failure-window-capacity").hexdigest()
+
+
+def _json(value: object) -> str:
+    return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
+
+
+class EdgeGatewayRepository:
+    """Owns atomic state transitions; secrets passed here are already hashed."""
+
+    def __init__(self, session):
+        self.session = session
+
+    def rollback(self) -> None:
+        self.session.rollback()
+
+    def create_enrollment(self, values: dict) -> None:
+        self.session.execute(text("""
+            INSERT INTO public.edge_gateway_enrollments
+              (uid,gateway_id,gateway_name,environment,network_zone,policy_digest,
+               expected_certificate_sha256,allowed_control_hosts,allowed_proxy_hosts,
+               token_hash,status,expires_at,created_by)
+            VALUES (CAST(:uid AS uuid),CAST(:gateway_id AS uuid),:gateway_name,:environment,
+                    :network_zone,:policy_digest,:expected_certificate_sha256,
+                    CAST(:control_hosts AS text[]),
+                    CAST(:proxy_hosts AS text[]),:token_hash,'pending',
+                    CURRENT_TIMESTAMP + (:ttl * INTERVAL '1 second'),CAST(:actor_uid AS uuid))
+        """), values)
+        self._audit(None, "enrollment_created", values["actor_uid"], True, {"gateway_id": values["gateway_id"], "ttl_seconds": values["ttl"]})
+        self.session.commit()
+
+    def lock_enrollment(self, token_hash: str):
+        return self.session.execute(text("""
+            SELECT uid::text,gateway_id::text,gateway_name,environment,network_zone,
+                   policy_digest,expected_certificate_sha256,allowed_control_hosts,
+                   allowed_proxy_hosts,status,expires_at
+              FROM public.edge_gateway_enrollments WHERE token_hash=:token_hash FOR UPDATE
+        """), {"token_hash": token_hash}).mappings().one_or_none()
+
+    def complete_registration(self, enrollment, credential_hash: str, certificate_sha256: str, version: str) -> dict:
+        credential_uid = new_governance_uid()
+        gateway_id = enrollment["gateway_id"]
+        self.session.execute(text("""
+            INSERT INTO public.edge_gateways
+              (uid,name,environment,network_zone,status,current_generation,policy_digest,
+               allowed_control_hosts,allowed_proxy_hosts,runtime_version)
+            VALUES (CAST(:gateway_id AS uuid),:name,:environment,:zone,'pending',1,:policy,
+                    CAST(:control_hosts AS text[]),CAST(:proxy_hosts AS text[]),:version)
+        """), {
+            "gateway_id": gateway_id, "name": enrollment["gateway_name"],
+            "environment": enrollment["environment"], "zone": enrollment["network_zone"],
+            "policy": enrollment["policy_digest"], "control_hosts": enrollment["allowed_control_hosts"],
+            "proxy_hosts": enrollment["allowed_proxy_hosts"], "version": version,
+        })
+        self.session.execute(text("""
+            INSERT INTO public.edge_gateway_credentials
+              (uid,gateway_id,generation,credential_hash,certificate_sha256,status,expires_at)
+            VALUES (CAST(:uid AS uuid),CAST(:gateway_id AS uuid),1,:credential_hash,
+                    :certificate,'active',CURRENT_TIMESTAMP + INTERVAL '365 days')
+        """), {"uid": credential_uid, "gateway_id": gateway_id, "credential_hash": credential_hash, "certificate": certificate_sha256})
+        self.session.execute(text("UPDATE public.edge_gateways SET status='active',updated_at=CURRENT_TIMESTAMP WHERE uid=CAST(:gateway_id AS uuid)"), {"gateway_id": gateway_id})
+        consumed = self.session.execute(text("""
+            UPDATE public.edge_gateway_enrollments SET status='consumed',consumed_at=CURRENT_TIMESTAMP
+             WHERE uid=CAST(:uid AS uuid) AND status='pending' AND expires_at>CURRENT_TIMESTAMP
+             RETURNING uid
+        """), {"uid": enrollment["uid"]}).scalar_one_or_none()
+        if consumed is None:
+            raise RuntimeError("enrollment is no longer consumable")
+        self._audit(gateway_id, "gateway_registered", None, True, {"generation": 1})
+        self.session.commit()
+        return {"gateway_id": gateway_id, "credential_uid": credential_uid, "generation": 1}
+
+    def authenticate(self, credential_hash: str, *, lock: bool = False):
+        if lock:
+            gateway_id = self.session.execute(text("""
+                SELECT gateway_id::text
+                  FROM public.edge_gateway_credentials
+                 WHERE credential_hash=:credential_hash
+            """), {"credential_hash": credential_hash}).scalar_one_or_none()
+            if gateway_id is None:
+                return None
+            gateway = self.session.execute(text("""
+                SELECT uid::text AS gateway_id,environment,network_zone,policy_digest,
+                       allowed_control_hosts,allowed_proxy_hosts,status,current_generation
+                  FROM public.edge_gateways
+                 WHERE uid=CAST(:gateway_id AS uuid)
+                 FOR UPDATE
+            """), {"gateway_id": gateway_id}).mappings().one_or_none()
+            if gateway is None or gateway["status"] not in {"active", "offline"}:
+                return None
+            credential = self.session.execute(text("""
+                SELECT uid::text,gateway_id::text,generation,certificate_sha256
+                  FROM public.edge_gateway_credentials
+                 WHERE credential_hash=:credential_hash AND status='active'
+                   AND expires_at>CURRENT_TIMESTAMP
+                   AND gateway_id=CAST(:gateway_id AS uuid)
+                   AND generation=:generation
+                 FOR UPDATE
+            """), {
+                "credential_hash": credential_hash,
+                "gateway_id": gateway_id,
+                "generation": gateway["current_generation"],
+            }).mappings().one_or_none()
+            if credential is None:
+                return None
+            return {**dict(credential), **{
+                key: gateway[key] for key in (
+                    "environment", "network_zone", "policy_digest",
+                    "allowed_control_hosts", "allowed_proxy_hosts", "status",
+                )
+            }}
+        return self.session.execute(text("""
+            SELECT c.uid::text,c.gateway_id::text,c.generation,c.certificate_sha256,
+                   g.environment,g.network_zone,g.policy_digest,g.allowed_control_hosts,
+                   g.allowed_proxy_hosts,g.status
+              FROM public.edge_gateway_credentials c
+              JOIN public.edge_gateways g ON g.uid=c.gateway_id
+             WHERE c.credential_hash=:credential_hash AND c.status='active'
+               AND c.expires_at>CURRENT_TIMESTAMP AND g.status IN ('active','offline')
+               AND g.current_generation=c.generation
+        """), {"credential_hash": credential_hash}).mappings().one_or_none()
+
+    def commit(self) -> None:
+        self.session.commit()
+
+    def list_gateways(self, *, limit: int, offset: int) -> list[dict]:
+        return [dict(row) for row in self.session.execute(text("""
+            SELECT uid::text AS gateway_id,name,environment,network_zone,status,current_generation,
+                   policy_digest,allowed_control_hosts,allowed_proxy_hosts,last_heartbeat_at,runtime_version
+              FROM public.edge_gateways ORDER BY created_at,uid LIMIT :limit OFFSET :offset
+        """), {"limit": limit, "offset": offset}).mappings()]
+
+    def gateway_binding(self, gateway_id: str):
+        return self.session.execute(text("""
+            SELECT uid::text AS gateway_id,environment,network_zone,policy_digest,status
+              FROM public.edge_gateways WHERE uid=CAST(:gateway_id AS uuid)
+        """), {"gateway_id": gateway_id}).mappings().one_or_none()
+
+    def heartbeat(self, gateway_id: str, version: str, safe_summary: dict) -> dict:
+        row = self.session.execute(text("""
+            UPDATE public.edge_gateways SET status='active',runtime_version=:version,
+                   safe_health_summary=CAST(:summary AS jsonb),last_heartbeat_at=CURRENT_TIMESTAMP,
+                   updated_at=CURRENT_TIMESTAMP WHERE uid=CAST(:gateway_id AS uuid)
+             RETURNING uid::text AS gateway_id,status,last_heartbeat_at,runtime_version
+        """), {"gateway_id": gateway_id, "version": version, "summary": _json(safe_summary)}).mappings().one()
+        self._audit(gateway_id, "heartbeat_accepted", None, True, {"version": version})
+        self.session.commit()
+        return dict(row)
+
+    def rotate(self, gateway_id: str, credential_hash: str, certificate_sha256: str, actor_uid: str, request_id: str, request_digest: str) -> dict | None:
+        gateway = self.session.execute(text("""
+            SELECT current_generation,status FROM public.edge_gateways
+             WHERE uid=CAST(:gateway_id AS uuid) FOR UPDATE
+        """), {"gateway_id": gateway_id}).mappings().one_or_none()
+        if gateway is None or gateway["status"] not in {"active", "offline"}:
+            return None
+        prior = self.session.execute(text("""
+            SELECT request_digest,generation FROM edge_gateway_rotation_requests
+             WHERE gateway_id=CAST(:gateway_id AS uuid) AND request_id=:request_id
+        """), {"gateway_id": gateway_id, "request_id": request_id}).mappings().one_or_none()
+        if prior:
+            self.session.commit()
+            if prior["request_digest"] != request_digest:
+                return {"conflict": True}
+            return {"generation": prior["generation"], "replayed": True}
+        old = self.session.execute(text("""
+            UPDATE public.edge_gateway_credentials SET status='rotated',revoked_at=CURRENT_TIMESTAMP
+             WHERE gateway_id=CAST(:gateway_id AS uuid) AND generation=:generation AND status='active'
+             RETURNING uid::text
+        """), {"gateway_id": gateway_id, "generation": gateway["current_generation"]}).scalar_one()
+        generation = int(gateway["current_generation"]) + 1
+        uid = new_governance_uid()
+        self.session.execute(text("""
+            INSERT INTO public.edge_gateway_credentials
+              (uid,gateway_id,generation,credential_hash,certificate_sha256,status,expires_at,rotated_from_uid)
+            VALUES (CAST(:uid AS uuid),CAST(:gateway_id AS uuid),:generation,:credential_hash,
+                    :certificate,'active',CURRENT_TIMESTAMP + INTERVAL '365 days',CAST(:old AS uuid))
+        """), {"uid": uid, "gateway_id": gateway_id, "generation": generation, "credential_hash": credential_hash, "certificate": certificate_sha256, "old": old})
+        self.session.execute(text("UPDATE public.edge_gateways SET current_generation=:generation,status='active',updated_at=CURRENT_TIMESTAMP WHERE uid=CAST(:gateway_id AS uuid)"), {"gateway_id": gateway_id, "generation": generation})
+        self.session.execute(text("""
+            INSERT INTO edge_gateway_rotation_requests(gateway_id,request_id,request_digest,generation)
+            VALUES (CAST(:gateway_id AS uuid),:request_id,:request_digest,:generation)
+        """), {"gateway_id": gateway_id, "request_id": request_id, "request_digest": request_digest, "generation": generation})
+        self._audit(gateway_id, "credential_rotated", actor_uid, True, {"generation": generation})
+        self.session.commit()
+        return {"credential_uid": uid, "generation": generation}
+
+    def revoke(self, gateway_id: str, actor_uid: str) -> bool:
+        gateway = self.session.execute(text("""
+            UPDATE public.edge_gateways SET status='revoked',revoked_at=CURRENT_TIMESTAMP,
+                   updated_at=CURRENT_TIMESTAMP WHERE uid=CAST(:gateway_id AS uuid)
+                   AND status <> 'revoked' RETURNING uid::text
+        """), {"gateway_id": gateway_id}).scalar_one_or_none()
+        if gateway is None:
+            self.session.rollback()
+            return False
+        self.session.execute(text("UPDATE public.edge_gateway_credentials SET status='revoked',revoked_at=CURRENT_TIMESTAMP WHERE gateway_id=CAST(:gateway_id AS uuid) AND status='active'"), {"gateway_id": gateway_id})
+        self._audit(gateway_id, "gateway_revoked", actor_uid, True, {})
+        self.session.commit()
+        return True
+
+    def insert_task(
+        self, contract: dict, digest: str, authority: dict, actor_uid: str,
+    ) -> dict:
+        existing = self.session.execute(text("""
+            SELECT task_id,contract_digest,contract,authority FROM public.edge_gateway_tasks
+             WHERE gateway_id=CAST(:gateway_id AS uuid) AND idempotency_key=:idempotency_key FOR UPDATE
+        """), {"gateway_id": contract["gateway_id"], "idempotency_key": contract["idempotency_key"]}).mappings().one_or_none()
+        if existing:
+            result = dict(existing)
+            self.session.commit()
+            return result
+        inserted = self.session.execute(text("""
+            INSERT INTO public.edge_gateway_tasks
+              (task_id,gateway_id,idempotency_key,contract,contract_digest,authority,status,created_by)
+            SELECT :task_id,uid,:idempotency_key,CAST(:contract AS jsonb),:digest,
+                   CAST(:authority AS jsonb),'pending',CAST(:actor AS uuid)
+              FROM public.edge_gateways WHERE uid=CAST(:gateway_id AS uuid) AND status IN ('active','offline')
+            ON CONFLICT DO NOTHING
+            RETURNING task_id
+        """), {"task_id": contract["task_id"], "gateway_id": contract["gateway_id"], "idempotency_key": contract["idempotency_key"], "contract": _json(contract), "digest": digest, "authority": _json(authority), "actor": actor_uid})
+        if inserted.scalar_one_or_none() is None:
+            concurrent = self.session.execute(text("""
+                SELECT task_id,contract_digest,contract,authority FROM public.edge_gateway_tasks
+                 WHERE task_id=:task_id OR
+                   (gateway_id=CAST(:gateway_id AS uuid) AND idempotency_key=:idempotency_key)
+            """), {
+                "task_id": contract["task_id"], "gateway_id": contract["gateway_id"],
+                "idempotency_key": contract["idempotency_key"],
+            }).mappings().one_or_none()
+            self.session.commit()
+            return dict(concurrent) if concurrent else {}
+        self._audit(contract["gateway_id"], "task_issued", actor_uid, True, {"task_id": contract["task_id"]})
+        self.session.commit()
+        return {"task_id": contract["task_id"], "contract_digest": digest, "contract": contract, "authority": authority}
+
+    def pull_task(self, gateway_id: str, lease_hash: str, lease_seconds: int = 120):
+        row = self.session.execute(text("""
+            WITH expired AS (
+              UPDATE public.edge_gateway_tasks
+                 SET status='failed',lease_token_hash=NULL,lease_expires_at=NULL,
+                     result_summary='{"reason_code":"task_deadline_expired"}'::jsonb,
+                     updated_at=CURRENT_TIMESTAMP
+               WHERE gateway_id=CAST(:gateway_id AS uuid)
+                 AND status IN ('pending','leased')
+                 AND (contract->>'deadline_at')::timestamptz<=CURRENT_TIMESTAMP
+               RETURNING task_id
+            ), candidate AS (
+              SELECT task_id FROM public.edge_gateway_tasks
+               WHERE gateway_id=CAST(:gateway_id AS uuid)
+                 AND (status='pending' OR (status='leased' AND lease_expires_at<=CURRENT_TIMESTAMP))
+                 AND (contract->>'deadline_at')::timestamptz>CURRENT_TIMESTAMP
+               ORDER BY created_at,task_id FOR UPDATE SKIP LOCKED LIMIT 1
+            )
+            UPDATE public.edge_gateway_tasks t SET status='leased',lease_token_hash=:lease_hash,
+                   lease_expires_at=LEAST(
+                     (t.contract->>'deadline_at')::timestamptz,
+                     CURRENT_TIMESTAMP + (:lease_seconds * INTERVAL '1 second')
+                   ),
+                   updated_at=CURRENT_TIMESTAMP FROM candidate c WHERE t.task_id=c.task_id
+             RETURNING t.task_id,t.contract,t.authority,t.contract_digest,t.lease_expires_at
+        """), {"gateway_id": gateway_id, "lease_hash": lease_hash, "lease_seconds": lease_seconds}).mappings().one_or_none()
+        self.session.commit()
+        return dict(row) if row else None
+
+    def cancel_task(self, task_id: str, actor_uid: str) -> bool:
+        gateway_id = self.session.execute(text("""
+            SELECT g.uid::text
+              FROM public.edge_gateway_tasks t
+              JOIN public.edge_gateways g ON g.uid=t.gateway_id
+             WHERE t.task_id=:task_id
+             FOR UPDATE OF g
+        """), {"task_id": task_id}).scalar_one_or_none()
+        if gateway_id is None:
+            self.session.rollback()
+            return False
+        row = self.session.execute(text("""
+            UPDATE public.edge_gateway_tasks SET
+                   status=CASE WHEN status='pending' THEN 'cancelled' ELSE 'cancel_requested' END,
+                   cancel_requested_at=CURRENT_TIMESTAMP,updated_at=CURRENT_TIMESTAMP
+             WHERE task_id=:task_id AND status IN ('pending','leased') RETURNING gateway_id::text
+        """), {"task_id": task_id}).scalar_one_or_none()
+        if row:
+            self._audit(gateway_id, "task_cancel_requested", actor_uid, True, {"task_id": task_id})
+        self.session.commit()
+        return row is not None
+
+    def task_outcome(self, gateway_id: str, task_id: str, lease_hash: str, outcome: str, summary: dict):
+        row = self.session.execute(text("""
+            SELECT status,lease_token_hash,lease_expires_at,result_summary,
+                   lease_expires_at>CURRENT_TIMESTAMP AS lease_current
+              FROM edge_gateway_tasks WHERE gateway_id=CAST(:gateway_id AS uuid)
+               AND task_id=:task_id FOR UPDATE
+        """), {"gateway_id": gateway_id, "task_id": task_id}).mappings().one_or_none()
+        if row is None:
+            self.session.rollback()
+            return None
+        if row["status"] == outcome:
+            if dict(row["result_summary"] or {}) != summary:
+                self.session.rollback()
+                return None
+            self.session.commit()
+            return {"task_id": task_id, "status": outcome, "replayed": True}
+        allowed = (
+            outcome in {"completed", "failed"} and row["status"] == "leased"
+            and bool(row["lease_current"])
+        ) or (outcome == "cancelled" and row["status"] == "cancel_requested")
+        if not allowed or row["lease_token_hash"] != lease_hash:
+            self.session.rollback()
+            return None
+        try:
+            self.session.execute(text("""
+                UPDATE edge_gateway_tasks SET status=:outcome,result_summary=CAST(:summary AS jsonb),
+                       lease_token_hash=NULL,lease_expires_at=NULL,updated_at=CURRENT_TIMESTAMP
+                 WHERE gateway_id=CAST(:gateway_id AS uuid) AND task_id=:task_id
+            """), {"gateway_id": gateway_id, "task_id": task_id, "outcome": outcome,
+                     "summary": _json(summary)})
+            self._audit(
+                gateway_id, "task_outcome_recorded", None, True,
+                {"task_id": task_id, "outcome": outcome},
+            )
+            self.session.commit()
+        except Exception:
+            self.session.rollback()
+            raise
+        return {"task_id": task_id, "status": outcome, "replayed": False}
+
+    def cancelled_tasks(
+        self, gateway_id: str, *, limit: int, cursor: str | None = None
+    ) -> dict:
+        rows = list(self.session.execute(text("""
+            SELECT task_id FROM public.edge_gateway_tasks
+             WHERE gateway_id=CAST(:gateway_id AS uuid) AND status='cancel_requested'
+               AND (
+                 CAST(:cursor AS text) IS NULL OR (created_at,task_id) > (
+                   SELECT created_at,task_id FROM public.edge_gateway_tasks
+                    WHERE gateway_id=CAST(:gateway_id AS uuid) AND task_id=:cursor
+                 )
+               )
+             ORDER BY created_at,task_id LIMIT :page_limit
+        """), {
+            "gateway_id": gateway_id,
+            "cursor": cursor,
+            "page_limit": limit + 1,
+        }).scalars())
+        page = rows[:limit]
+        return {
+            "items": page,
+            "next_cursor": page[-1] if len(rows) > limit else None,
+        }
+
+    def event_by_id(self, gateway_id: str, event_id: str):
+        return self.session.execute(text("""
+            SELECT contract_digest,ack FROM public.edge_gateway_events
+             WHERE gateway_id=CAST(:gateway_id AS uuid) AND event_id=:event_id
+        """), {"gateway_id": gateway_id, "event_id": event_id}).mappings().one_or_none()
+
+    def task_lease_binding(self, task_id: str, gateway_id: str, lease_hash: str):
+        return self.session.execute(text("""
+              SELECT contract FROM public.edge_gateway_tasks
+               WHERE task_id=:task_id AND gateway_id=CAST(:gateway_id AS uuid)
+                 AND status='leased' AND lease_token_hash=:lease_hash
+                 AND lease_expires_at>CURRENT_TIMESTAMP
+               FOR UPDATE
+        """), {"task_id": task_id, "gateway_id": gateway_id, "lease_hash": lease_hash}).scalar_one_or_none()
+
+    def insert_event(self, contract: dict, digest: str, ack: dict) -> dict | None:
+        inserted = self.session.execute(text("""
+            INSERT INTO public.edge_gateway_events
+              (event_id,gateway_id,task_id,classification,contract,contract_digest,ack)
+            VALUES (:event_id,CAST(:gateway_id AS uuid),:task_id,:classification,
+                    CAST(:contract AS jsonb),:digest,CAST(:ack AS jsonb))
+            ON CONFLICT (event_id) DO NOTHING RETURNING event_id
+        """), {"event_id": contract["event_id"], "gateway_id": contract["gateway_id"], "task_id": contract["task_id"], "classification": contract["classification"], "contract": _json(contract), "digest": digest, "ack": _json(ack)})
+        if inserted.scalar_one_or_none() is None:
+            existing = self.event_by_id(contract["gateway_id"], contract["event_id"])
+            if existing is None or existing["contract_digest"] != digest:
+                self.session.rollback()
+                return None
+            self.session.commit()
+            return dict(existing["ack"])
+        self._audit(contract["gateway_id"], "event_accepted", None, True, {"event_id": contract["event_id"], "classification": contract["classification"]})
+        self.session.execute(text("""
+            UPDATE edge_gateway_tasks SET status='completed',lease_token_hash=NULL,
+                   lease_expires_at=NULL,result_summary=CAST(:summary AS jsonb),updated_at=CURRENT_TIMESTAMP
+             WHERE task_id=:task_id AND gateway_id=CAST(:gateway_id AS uuid) AND status='leased'
+        """), {"task_id": contract["task_id"], "gateway_id": contract["gateway_id"],
+                 "summary": _json({"event_id": contract["event_id"], "classification": contract["classification"]})})
+        self.session.commit()
+        return ack
+
+    def offer_release(
+        self, gateway_id: str, manifest: dict, actor_uid: str,
+        request_id: str, request_digest: str,
+    ) -> dict:
+        gateway = self.session.execute(text("""
+            SELECT status FROM public.edge_gateways
+             WHERE uid=CAST(:gateway_id AS uuid) FOR UPDATE
+        """), {"gateway_id": gateway_id}).mappings().one_or_none()
+        if gateway is None or gateway["status"] not in {"active", "offline"}:
+            self.session.rollback()
+            return {}
+        prior_request = self.session.execute(text("""
+            SELECT uid::text AS release_id,status,version,artifact_digest,rollback_version,
+                   request_id,request_digest,signed_manifest
+              FROM edge_gateway_releases WHERE gateway_id=CAST(:gateway_id AS uuid)
+               AND request_id=:request_id
+        """), {"gateway_id": gateway_id, "request_id": request_id}).mappings().one_or_none()
+        prior_version = self.session.execute(text("""
+            SELECT uid::text AS release_id,request_id,request_digest
+              FROM edge_gateway_releases WHERE gateway_id=CAST(:gateway_id AS uuid)
+               AND version=:version
+        """), {"gateway_id": gateway_id, "version": manifest["version"]}).mappings().one_or_none()
+        if prior_request:
+            self.session.commit()
+            if (
+                prior_request["request_digest"] != request_digest
+                or prior_request["version"] != manifest["version"]
+                or (prior_version is not None and prior_version["request_id"] != request_id)
+            ):
+                return {"conflict": True}
+            return {
+                **{key: prior_request[key] for key in (
+                    "release_id", "status", "version", "artifact_digest",
+                    "rollback_version",
+                )},
+                "manifest": dict(prior_request["signed_manifest"]),
+            }
+        if prior_version:
+            self.session.commit()
+            return {"conflict": True}
+        self.session.execute(text("""
+            INSERT INTO public.edge_gateway_releases
+              (uid,gateway_id,version,artifact_digest,rollback_version,status,request_id,
+               request_digest,created_by,artifact_name,deadline_at,signature_algorithm,
+               key_id,manifest_digest,signature,signed_manifest)
+            VALUES (CAST(:uid AS uuid),CAST(:gateway_id AS uuid),:version,:digest,:rollback,
+                    :status,:request_id,:request_digest,CAST(:actor AS uuid),:artifact_name,
+                    CAST(:deadline_at AS timestamptz),:signature_algorithm,:key_id,
+                    :manifest_digest,:signature,CAST(:signed_manifest AS jsonb))
+        """), {
+            "uid": manifest["release_id"], "gateway_id": gateway_id,
+            "version": manifest["version"], "digest": manifest["artifact_digest"],
+            "rollback": manifest["rollback_version"], "status": manifest["status"],
+            "request_id": request_id, "request_digest": request_digest,
+            "actor": actor_uid, "artifact_name": manifest["artifact_name"],
+            "deadline_at": manifest["deadline_at"],
+            "signature_algorithm": manifest["signature_algorithm"],
+            "key_id": manifest["key_id"], "manifest_digest": manifest["manifest_digest"],
+            "signature": manifest["signature"], "signed_manifest": _json(manifest),
+        })
+        self._audit(gateway_id, "release_offered", actor_uid, True, {
+            "release_id": manifest["release_id"], "version": manifest["version"],
+        })
+        self.session.commit()
+        return {
+            "release_id": manifest["release_id"], "status": "offered",
+            "version": manifest["version"], "artifact_digest": manifest["artifact_digest"],
+            "rollback_version": manifest["rollback_version"], "manifest": manifest,
+        }
+
+    def release_for_ack(self, gateway_id: str, release_id: str):
+        return self.session.execute(text("""
+            SELECT uid::text AS release_id,status,safe_summary,signed_manifest
+              FROM public.edge_gateway_releases WHERE uid=CAST(:uid AS uuid)
+               AND gateway_id=CAST(:gateway_id AS uuid) FOR UPDATE
+        """), {"uid": release_id, "gateway_id": gateway_id}).mappings().one_or_none()
+
+    def acknowledge_release(
+        self, gateway_id: str, release_id: str, status: str, safe_summary: dict,
+        signed_manifest: dict,
+    ) -> dict | None:
+        current = self.session.execute(text("""
+            SELECT uid::text AS release_id,status,version,rollback_version,safe_summary
+              FROM public.edge_gateway_releases WHERE uid=CAST(:uid AS uuid)
+               AND gateway_id=CAST(:gateway_id AS uuid) FOR UPDATE
+        """), {"uid": release_id, "gateway_id": gateway_id}).mappings().one_or_none()
+        if current is None:
+            self.session.rollback()
+            return None
+        if current["status"] == status:
+            if dict(current["safe_summary"] or {}) != safe_summary:
+                self.session.rollback()
+                return None
+            self.session.commit()
+            return {key: current[key] for key in ("release_id", "status", "version", "rollback_version")}
+        allowed = {
+            "offered": {"accepted"},
+            "accepted": {"installed", "failed"},
+            "failed": {"rolled_back"},
+            "installed": {"rolled_back"},
+            "rolled_back": set(),
+        }
+        if status not in allowed[current["status"]]:
+            self.session.rollback()
+            return None
+        row = self.session.execute(text("""
+            UPDATE public.edge_gateway_releases SET status=:status,safe_summary=CAST(:summary AS jsonb),
+                   acknowledged_at=CURRENT_TIMESTAMP,signature_algorithm=:signature_algorithm,
+                   key_id=:key_id,manifest_digest=:manifest_digest,signature=:signature,
+                   signed_manifest=CAST(:signed_manifest AS jsonb)
+             WHERE uid=CAST(:uid AS uuid)
+             RETURNING uid::text AS release_id,status,version,rollback_version
+        """), {
+            "uid": release_id, "status": status, "summary": _json(safe_summary),
+            "signature_algorithm": signed_manifest["signature_algorithm"],
+            "key_id": signed_manifest["key_id"],
+            "manifest_digest": signed_manifest["manifest_digest"],
+            "signature": signed_manifest["signature"],
+            "signed_manifest": _json(signed_manifest),
+        }).mappings().one()
+        if row:
+            self._audit(gateway_id, "release_acknowledged", None, True, {"release_id": release_id, "outcome": status})
+        self.session.commit()
+        return dict(row) if row else None
+
+    def unfinished_releases(
+        self, gateway_id: str, *, limit: int, cursor: str | None = None
+    ) -> dict:
+        rows = list(self.session.execute(text("""
+            SELECT uid::text AS release_id,signed_manifest
+              FROM public.edge_gateway_releases
+             WHERE gateway_id=CAST(:gateway_id AS uuid)
+               AND status IN ('offered','accepted','failed')
+               AND (
+                 CAST(:cursor AS text) IS NULL OR (created_at,uid) > (
+                   SELECT created_at,uid FROM public.edge_gateway_releases
+                    WHERE gateway_id=CAST(:gateway_id AS uuid)
+                      AND uid=CAST(:cursor AS uuid)
+                 )
+               )
+             ORDER BY created_at,uid LIMIT :page_limit
+        """), {
+            "gateway_id": gateway_id,
+            "cursor": cursor,
+            "page_limit": limit + 1,
+        }).mappings())
+        page = rows[:limit]
+        return {
+            "items": [dict(row["signed_manifest"]) for row in page],
+            "next_cursor": page[-1]["release_id"] if len(rows) > limit else None,
+        }
+
+    def latest_release_baseline(self, gateway_id: str) -> dict | None:
+        row = self.session.execute(text("""
+            SELECT signed_manifest FROM public.edge_gateway_releases
+             WHERE gateway_id=CAST(:gateway_id AS uuid)
+               AND status IN ('installed','rolled_back')
+             ORDER BY COALESCE(acknowledged_at,created_at) DESC,
+                      created_at DESC,uid DESC LIMIT 1
+        """), {"gateway_id": gateway_id}).scalar_one_or_none()
+        return dict(row) if row is not None else None
+
+    def audit_failure(
+        self, gateway_id: str | None, event_type: str, reason_code: str, *, trusted: bool,
+    ) -> int:
+        allowed = {
+            "registration_rejected", "authentication_rejected", "policy_rejected",
+            "binding_rejected", "idempotency_conflict", "release_rejected",
+        }
+        if event_type not in allowed or not reason_code.isidentifier() or len(reason_code) > 64:
+            raise ValueError("failure audit metadata is invalid")
+        if not trusted:
+            gateway_id = None
+            event_type = "untrusted_request_rejected"
+            reason_code = "untrusted_failure"
+        self.session.rollback()
+        engine = self.session.get_bind()
+        fingerprint = hashlib.sha256(f"{gateway_id or 'unknown'}:{event_type}:{reason_code}".encode()).hexdigest()
+        with engine.begin() as connection:
+            window = connection.execute(text("""
+                SELECT * FROM public.record_edge_gateway_failure(
+                  CAST(:audit_uid AS uuid),
+                  :fingerprint,
+                  CASE WHEN EXISTS (SELECT 1 FROM edge_gateways WHERE uid=CAST(:gateway AS uuid))
+                       THEN CAST(:gateway AS uuid) ELSE NULL END,
+                  :event_type,
+                  :reason_code
+                )
+            """), {
+                "audit_uid": new_governance_uid(), "fingerprint": fingerprint,
+                "gateway": gateway_id, "event_type": event_type,
+                "reason_code": reason_code,
+            }).mappings().one()
+            fingerprint = window["failure_fingerprint"]
+            gateway_id = window["gateway_id"]
+            event_type = window["event_type"]
+            if fingerprint == _OVERFLOW_FINGERPRINT:
+                reason_code = "capacity_overflow"
+        return int(window["occurrence_count"])
+
+    def _audit(self, gateway_id, event_type: str, actor_uid, success: bool, detail: dict) -> None:
+        self.session.execute(text("""
+            SELECT public.record_edge_gateway_audit(
+              CAST(:uid AS uuid),CAST(:gateway_id AS uuid),:event_type,
+              CAST(:actor_uid AS uuid),:success,CAST(:detail AS jsonb)
+            )
+        """), {"uid": new_governance_uid(), "gateway_id": gateway_id, "event_type": event_type, "actor_uid": actor_uid, "success": success, "detail": _json(detail)})

+ 99 - 0
app/core/edge_gateway/runtime_roles.py

@@ -0,0 +1,99 @@
+"""Provision the login used by long-running DataOps application processes."""
+
+from __future__ import annotations
+
+import os
+import re
+
+from sqlalchemy import create_engine, text
+from sqlalchemy.pool import NullPool
+
+from app.config.database_urls import validate_database_environment
+
+_ROLE_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_]{0,62}$")
+
+
+def provision_runtime_login() -> None:
+    role_init_url = os.environ.get("DB_ROLE_INIT_DATABASE_URL", "")
+    migration_url = os.environ.get("MIGRATION_DATABASE_URL", "")
+    runtime_url = os.environ.get("DATABASE_URL", "")
+    runtime_user = os.environ.get("DATAOPS_RUNTIME_USER", "")
+    runtime_password = os.environ.get("DATAOPS_RUNTIME_PASSWORD", "")
+    migrator_user = os.environ.get("DATAOPS_MIGRATOR_USER", "")
+    if (
+        not _ROLE_NAME.fullmatch(runtime_user)
+        or not _ROLE_NAME.fullmatch(migrator_user)
+    ):
+        raise RuntimeError("runtime database role provisioning is not configured")
+    validate_database_environment({
+        "DB_ROLE_INIT_DATABASE_URL": role_init_url,
+        "MIGRATION_DATABASE_URL": migration_url,
+        "DATABASE_URL": runtime_url,
+        "DATAOPS_RUNTIME_PASSWORD": runtime_password,
+    })
+    engine = create_engine(role_init_url, poolclass=NullPool)
+    quoted_user = engine.dialect.identifier_preparer.quote(runtime_user)
+    quoted_migrator = engine.dialect.identifier_preparer.quote(migrator_user)
+    try:
+        with engine.begin() as connection:
+            bootstrap_identity = connection.execute(text("""
+                SELECT rolsuper OR rolcreaterole
+                  FROM pg_roles WHERE rolname=current_user
+            """)).scalar_one()
+            if not bootstrap_identity:
+                raise RuntimeError("DB role-init identity requires CREATEROLE or superuser")
+            connection.execute(text("""
+                DO $$ BEGIN
+                  IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='dataops_app_runtime') THEN
+                    CREATE ROLE dataops_app_runtime NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION;
+                  END IF;
+                  IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='dataops_edge_evidence_owner') THEN
+                    CREATE ROLE dataops_edge_evidence_owner NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION;
+                  END IF;
+                END $$;
+                ALTER ROLE dataops_app_runtime NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION;
+                ALTER ROLE dataops_edge_evidence_owner NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION;
+            """))
+            migrator_exists = connection.execute(
+                text("SELECT EXISTS (SELECT 1 FROM pg_roles WHERE rolname=:role)"),
+                {"role": migrator_user},
+            ).scalar_one()
+            if not migrator_exists:
+                raise RuntimeError("DATAOPS_MIGRATOR_USER must already exist")
+            exists = connection.execute(
+                text("SELECT EXISTS (SELECT 1 FROM pg_roles WHERE rolname=:role)"),
+                {"role": runtime_user},
+            ).scalar_one()
+            action = "ALTER" if exists else "CREATE"
+            connection.execute(text(
+                f"{action} ROLE {quoted_user} LOGIN NOSUPERUSER NOCREATEDB "
+                "NOCREATEROLE NOREPLICATION PASSWORD :password"
+            ), {"password": runtime_password})
+            connection.execute(text(f"GRANT dataops_app_runtime TO {quoted_user}"))
+            connection.execute(text(
+                f"GRANT dataops_edge_evidence_owner TO {quoted_migrator}"
+            ))
+            connection.execute(text(
+                "GRANT USAGE,CREATE ON SCHEMA public TO dataops_edge_evidence_owner"
+            ))
+            connection.execute(text("GRANT USAGE ON SCHEMA public TO dataops_app_runtime"))
+            connection.execute(text(
+                "GRANT SELECT,INSERT,UPDATE,DELETE ON ALL TABLES IN SCHEMA public "
+                "TO dataops_app_runtime"
+            ))
+            connection.execute(text(
+                "GRANT USAGE,SELECT ON ALL SEQUENCES IN SCHEMA public TO dataops_app_runtime"
+            ))
+            is_evidence_member = connection.execute(text("""
+                SELECT pg_has_role(:role,'dataops_edge_evidence_owner','MEMBER')
+            """), {"role": runtime_user}).scalar_one()
+            if is_evidence_member:
+                connection.execute(text(
+                    f"REVOKE dataops_edge_evidence_owner FROM {quoted_user}"
+                ))
+    finally:
+        engine.dispose()
+
+
+if __name__ == "__main__":
+    provision_runtime_login()

+ 700 - 0
app/core/edge_gateway/service.py

@@ -0,0 +1,700 @@
+"""Fail-closed application service for enterprise edge gateways."""
+
+from __future__ import annotations
+
+import hashlib
+import hmac
+import re
+import secrets
+import stat
+import uuid
+from collections.abc import Mapping, Sequence
+from contextlib import suppress
+from datetime import UTC, datetime, timedelta
+from pathlib import Path
+
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
+
+from app.core.edge_gateway.contracts import (
+    EdgeContractError,
+    EdgeEventContract,
+    EdgeTaskContract,
+    SignedTaskEnvelope,
+    canonical_json_bytes,
+    canonical_sha256,
+    canonical_timestamp,
+)
+from app.core.edge_gateway.policy import EdgeEgressPolicy, EdgePolicyError
+
+_SHA256 = re.compile(r"^[0-9a-f]{64}$")
+_VERSION = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._+-]{0,79}$")
+_RELEASE_VERSION = re.compile(
+    r"^(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})$"
+)
+_HOST = re.compile(r"^(?=.{1,253}$)[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$")
+
+
+class EdgeGatewayError(Exception):
+    status_code = 400
+    code = "EDGE_GATEWAY_ERROR"
+
+
+class EdgeGatewayValidationError(EdgeGatewayError):
+    code = "EDGE_GATEWAY_INVALID"
+
+
+class EdgeGatewayAuthenticationError(EdgeGatewayError):
+    status_code = 401
+    code = "EDGE_GATEWAY_AUTHENTICATION_FAILED"
+
+
+class EdgeGatewayConflictError(EdgeGatewayError):
+    status_code = 409
+    code = "EDGE_GATEWAY_CONFLICT"
+
+
+class EdgeGatewayPayloadTooLargeError(EdgeGatewayValidationError):
+    status_code = 413
+    code = "EDGE_GATEWAY_PAYLOAD_TOO_LARGE"
+
+
+class EdgeGatewayRateLimitError(EdgeGatewayError):
+    status_code = 429
+    code = "EDGE_GATEWAY_RATE_LIMITED"
+
+
+class EdgeGatewayNotFoundError(EdgeGatewayError):
+    status_code = 404
+    code = "EDGE_GATEWAY_NOT_FOUND"
+
+
+class EdgeGatewayConfigurationError(EdgeGatewayError):
+    status_code = 503
+    code = "EDGE_GATEWAY_SIGNING_UNAVAILABLE"
+
+
+def _digest(secret: str) -> str:
+    return hashlib.sha256(secret.encode("utf-8")).hexdigest()
+
+
+def _uuid(value: object, label: str) -> str:
+    try:
+        return str(uuid.UUID(str(value)))
+    except (ValueError, TypeError, AttributeError) as exc:
+        raise EdgeGatewayValidationError(f"{label} is invalid") from exc
+
+
+def _sha(value: object, label: str) -> str:
+    if not isinstance(value, str) or not _SHA256.fullmatch(value):
+        raise EdgeGatewayValidationError(f"{label} is invalid")
+    return value
+
+
+def _text(value: object, label: str, maximum: int) -> str:
+    if not isinstance(value, str) or not value or value.strip() != value or len(value.encode()) > maximum or "\x00" in value:
+        raise EdgeGatewayValidationError(f"{label} is invalid")
+    return value
+
+
+def _version(value: object) -> str:
+    value = _text(value, "version", 80)
+    if not _VERSION.fullmatch(value):
+        raise EdgeGatewayValidationError("version is invalid")
+    return value
+
+
+def _release_version(value: object, label: str) -> str:
+    value = _text(value, label, 80)
+    if (
+        not _RELEASE_VERSION.fullmatch(value)
+        or any(int(part) > 2_147_483_647 for part in value.split("."))
+    ):
+        raise EdgeGatewayValidationError(f"{label} is invalid")
+    return value
+
+
+def _release_version_tuple(value: str) -> tuple[int, ...]:
+    return tuple(int(item) for item in value.split("."))
+
+
+def _artifact_name(value: object) -> str:
+    value = _text(value, "artifact_name", 255)
+    if (
+        value in {".", ".."}
+        or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._+-]{0,254}", value)
+        or "/" in value
+        or "\\" in value
+    ):
+        raise EdgeGatewayValidationError("artifact_name is invalid")
+    return value
+
+
+def _future_deadline(value: object, *, now: datetime) -> str:
+    try:
+        canonical = canonical_timestamp(value, "deadline_at")
+        deadline = datetime.fromisoformat(canonical.replace("Z", "+00:00"))
+    except (EdgeContractError, TypeError, ValueError) as exc:
+        raise EdgeGatewayValidationError("deadline_at is invalid") from exc
+    if deadline <= now or deadline > now + timedelta(days=30):
+        raise EdgeGatewayValidationError("deadline_at is invalid")
+    return canonical
+
+
+def _signing_private_key(value: object) -> Ed25519PrivateKey | None:
+    if value is None or value == "":
+        return None
+    if isinstance(value, Ed25519PrivateKey):
+        return value
+    if isinstance(value, str):
+        encoded = value.encode("utf-8")
+        if re.fullmatch(r"[0-9a-fA-F]{64}", value):
+            encoded = bytes.fromhex(value)
+    elif isinstance(value, bytes):
+        encoded = value
+    else:
+        raise EdgeGatewayConfigurationError("edge signing authority is invalid")
+    try:
+        if isinstance(encoded, bytes) and len(encoded) == 32:
+            return Ed25519PrivateKey.from_private_bytes(encoded)
+        key = serialization.load_pem_private_key(encoded, password=None)
+    except (TypeError, ValueError) as exc:
+        raise EdgeGatewayConfigurationError("edge signing authority is invalid") from exc
+    if not isinstance(key, Ed25519PrivateKey):
+        raise EdgeGatewayConfigurationError("edge signing authority is invalid")
+    return key
+
+
+def _hosts(values: object, label: str, *, required: bool) -> list[str]:
+    if not isinstance(values, Sequence) or isinstance(values, (str, bytes)):
+        raise EdgeGatewayValidationError(f"{label} is invalid")
+    result = []
+    for item in values:
+        if not isinstance(item, str) or item != item.lower() or not _HOST.fullmatch(item) or ".." in item:
+            raise EdgeGatewayValidationError(f"{label} is invalid")
+        result.append(item)
+    if required and not result:
+        raise EdgeGatewayValidationError(f"{label} is required")
+    if len(result) != len(set(result)) or len(result) > 32:
+        raise EdgeGatewayValidationError(f"{label} is invalid")
+    return sorted(result)
+
+
+def _environment(value: object) -> str:
+    if value not in {"development", "staging", "production"}:
+        raise EdgeGatewayValidationError("environment is invalid")
+    return str(value)
+
+
+def _page(limit: object = 50, offset: object = 0) -> tuple[int, int]:
+    try:
+        limit, offset = int(limit), int(offset)
+    except (TypeError, ValueError) as exc:
+        raise EdgeGatewayValidationError("pagination is invalid") from exc
+    if not 1 <= limit <= 100 or not 0 <= offset <= 10_000:
+        raise EdgeGatewayValidationError("pagination is invalid")
+    return limit, offset
+
+
+def _safe_summary(value: object, classification: str = "diagnostic_summary") -> dict:
+    if not isinstance(value, Mapping):
+        raise EdgeGatewayValidationError("safe_summary is invalid")
+    candidate = dict(value)
+    try:
+        EdgeEgressPolicy(allowed_control_hosts={"validation.invalid"}).validate_approved_payload(classification, candidate)
+    except (EdgePolicyError, ValueError) as exc:
+        raise EdgeGatewayValidationError("safe_summary contains unapproved content") from exc
+    return candidate
+
+
+class EdgeGatewayService:
+    def __init__(
+        self, repository, *, signing_private_key=None,
+        signing_private_key_file=None, signing_key_provider=None,
+        signing_public_key=None, signing_key_id=None, production=False, clock=None,
+    ):
+        self.repository = repository
+        sources = sum(
+            item not in {None, ""}
+            for item in (signing_private_key, signing_private_key_file)
+        ) + int(signing_key_provider is not None)
+        if sources > 1:
+            raise EdgeGatewayConfigurationError("edge signing authority source is ambiguous")
+        if production and signing_private_key not in {None, ""}:
+            raise EdgeGatewayConfigurationError(
+                "production inline edge signing private key is forbidden"
+            )
+        key_material = signing_private_key
+        if signing_private_key_file not in {None, ""}:
+            if not isinstance(signing_private_key_file, str):
+                raise EdgeGatewayConfigurationError("edge signing key file is invalid")
+            path = Path(signing_private_key_file)
+            try:
+                if (
+                    not path.is_absolute()
+                    or not path.is_file()
+                    or path.is_symlink()
+                    or stat.S_IMODE(path.stat().st_mode) != 0o600
+                ):
+                    raise EdgeGatewayConfigurationError(
+                        "edge signing key file permissions must be 0600"
+                    )
+                key_material = path.read_bytes()
+            except OSError as exc:
+                raise EdgeGatewayConfigurationError(
+                    "edge signing key file is unavailable"
+                ) from exc
+        elif signing_key_provider is not None:
+            if not callable(signing_key_provider):
+                raise EdgeGatewayConfigurationError("edge signing key provider is invalid")
+            try:
+                key_material = signing_key_provider()
+            except Exception as exc:
+                raise EdgeGatewayConfigurationError(
+                    "edge signing key provider is unavailable"
+                ) from exc
+        self._signing_private_key = _signing_private_key(key_material)
+        if self._signing_private_key is not None and signing_public_key not in {None, ""}:
+            if (
+                not isinstance(signing_public_key, str)
+                or not re.fullmatch(r"[0-9a-f]{64}", signing_public_key)
+            ):
+                raise EdgeGatewayConfigurationError("edge signing public key is invalid")
+            actual_public = self._signing_private_key.public_key().public_bytes(
+                serialization.Encoding.Raw,
+                serialization.PublicFormat.Raw,
+            ).hex()
+            if not hmac.compare_digest(actual_public, signing_public_key):
+                raise EdgeGatewayConfigurationError(
+                    "edge signing public key does not match private key"
+                )
+        elif production and self._signing_private_key is not None:
+            raise EdgeGatewayConfigurationError(
+                "production edge signing public key is required"
+            )
+        self._signing_key_id = signing_key_id
+        self._clock = clock or (lambda: datetime.now(UTC))
+
+    def _require_signer(self) -> tuple[Ed25519PrivateKey, str]:
+        key_id = self._signing_key_id
+        if (
+            self._signing_private_key is None
+            or not isinstance(key_id, str)
+            or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._:-]{0,127}", key_id)
+        ):
+            self.repository.rollback()
+            raise EdgeGatewayConfigurationError(
+                "edge signing authority is not configured"
+            )
+        return self._signing_private_key, key_id
+
+    def _signed_task_envelope(self, contract: EdgeTaskContract) -> dict:
+        private_key, key_id = self._require_signer()
+        issued_at = canonical_timestamp(
+            self._clock().astimezone(UTC).isoformat().replace("+00:00", "Z")
+        )
+        unsigned = {
+            "task": contract.to_mapping(),
+            "authority_key_id": key_id,
+            "signature_algorithm": "Ed25519",
+            "contract_digest": contract.digest,
+            "gateway_id": contract.gateway_id,
+            "environment": contract.environment,
+            "network_zone": contract.network_zone,
+            "policy_digest": contract.policy_digest,
+            "purpose": contract.purpose,
+            "issued_at": issued_at,
+            "expires_at": contract.deadline_at,
+        }
+        try:
+            signed_bytes = SignedTaskEnvelope.canonical_unsigned_bytes(unsigned)
+        except EdgeContractError as exc:
+            self.repository.rollback()
+            raise EdgeGatewayValidationError(
+                "edge task authority validity is invalid"
+            ) from exc
+        return {**unsigned, "signature": private_key.sign(signed_bytes).hex()}
+
+    def _signed_release_manifest(self, unsigned: Mapping[str, object]) -> dict:
+        private_key, key_id = self._require_signer()
+        values = dict(unsigned)
+        values["signature_algorithm"] = "Ed25519"
+        values["key_id"] = key_id
+        digest = canonical_sha256(values)
+        return {
+            **values,
+            "manifest_digest": digest,
+            "signature": private_key.sign(canonical_json_bytes(values)).hex(),
+        }
+
+    def _failure(
+        self, gateway_id, event_type: str, reason_code: str, *, trusted: bool = False,
+    ) -> int:
+        count = self.repository.audit_failure(
+            gateway_id, event_type, reason_code, trusted=trusted,
+        )
+        if count >= 10:
+            raise EdgeGatewayRateLimitError("edge failure rate limit exceeded")
+        return count
+
+    def create_enrollment(
+        self, *, gateway_name, environment, network_zone, policy_digest,
+        allowed_control_hosts, allowed_proxy_hosts, ttl_seconds, actor_uid,
+        expected_certificate_sha256,
+    ) -> dict:
+        ttl = int(ttl_seconds)
+        if ttl < 60 or ttl > 86_400:
+            raise EdgeGatewayValidationError("enrollment TTL is invalid")
+        gateway_id, uid = str(uuid.uuid4()), str(uuid.uuid4())
+        token = "dope_" + secrets.token_urlsafe(32)
+        values = {
+            "uid": uid, "gateway_id": gateway_id,
+            "gateway_name": _text(gateway_name, "gateway_name", 200),
+            "environment": _environment(environment),
+            "network_zone": _text(network_zone, "network_zone", 120),
+            "policy_digest": _sha(policy_digest, "policy_digest"),
+            "expected_certificate_sha256": _sha(expected_certificate_sha256, "expected_certificate_sha256"),
+            "control_hosts": _hosts(allowed_control_hosts, "allowed_control_hosts", required=True),
+            "proxy_hosts": _hosts(allowed_proxy_hosts, "allowed_proxy_hosts", required=False),
+            "token_hash": _digest(token), "ttl": ttl, "actor_uid": _uuid(actor_uid, "actor_uid"),
+        }
+        self.repository.create_enrollment(values)
+        return {"enrollment_id": uid, "gateway_id": gateway_id, "enrollment_token": token, "expires_in": ttl, "returned_once": True}
+
+    def register(
+        self, *, enrollment_token, certificate_sha256, gateway_id, environment,
+        network_zone, policy_digest, allowed_control_hosts, allowed_proxy_hosts, version,
+    ) -> dict:
+        gateway_id = _uuid(gateway_id, "gateway_id")
+        if not isinstance(enrollment_token, str) or not enrollment_token.startswith("dope_") or len(enrollment_token) > 128:
+            self._failure(gateway_id, "registration_rejected", "enrollment_invalid")
+            raise EdgeGatewayAuthenticationError("edge enrollment is invalid or expired")
+        try:
+            certificate = _sha(certificate_sha256, "certificate_sha256")
+            supplied = {
+                "gateway_id": gateway_id, "environment": _environment(environment),
+                "network_zone": _text(network_zone, "network_zone", 120),
+                "policy_digest": _sha(policy_digest, "policy_digest"),
+                "allowed_control_hosts": _hosts(allowed_control_hosts, "allowed_control_hosts", required=True),
+                "allowed_proxy_hosts": _hosts(allowed_proxy_hosts, "allowed_proxy_hosts", required=False),
+            }
+        except EdgeGatewayValidationError as exc:
+            self._failure(gateway_id, "registration_rejected", "binding_invalid")
+            raise EdgeGatewayAuthenticationError("edge enrollment binding was rejected") from exc
+        enrollment = self.repository.lock_enrollment(_digest(enrollment_token))
+        now = datetime.now(UTC)
+        if enrollment is None or enrollment["status"] != "pending" or enrollment["expires_at"] <= now:
+            self._failure(supplied["gateway_id"], "registration_rejected", "enrollment_invalid")
+            raise EdgeGatewayAuthenticationError("edge enrollment is invalid or expired")
+        expected = {key: enrollment[key] for key in supplied}
+        expected["allowed_control_hosts"] = sorted(expected["allowed_control_hosts"])
+        expected["allowed_proxy_hosts"] = sorted(expected["allowed_proxy_hosts"])
+        if any(not hmac.compare_digest(str(expected[key]), str(supplied[key])) for key in supplied) or not hmac.compare_digest(enrollment["expected_certificate_sha256"], certificate):
+            self._failure(supplied["gateway_id"], "registration_rejected", "binding_mismatch")
+            raise EdgeGatewayAuthenticationError("edge enrollment binding was rejected")
+        credential = "dopg_" + secrets.token_urlsafe(40)
+        try:
+            result = self.repository.complete_registration(enrollment, _digest(credential), certificate, _version(version))
+        except Exception:
+            self.repository.rollback()
+            raise
+        return {**result, "credential": credential, "credential_returned_once": True, "certificate_sha256": certificate}
+
+    def authenticate(self, *, credential, certificate_sha256, gateway_id, environment, network_zone, generation, lock=False) -> dict:
+        try:
+            gateway_id = _uuid(gateway_id, "gateway_id")
+            generation = int(generation)
+            supplied = (
+                gateway_id, _environment(environment),
+                _text(network_zone, "network_zone", 120), generation,
+                _sha(certificate_sha256, "certificate_sha256"),
+            )
+        except EdgeGatewayValidationError as exc:
+            with suppress(EdgeGatewayValidationError):
+                self._failure(_uuid(gateway_id, "gateway_id"), "authentication_rejected", "binding_invalid")
+            raise EdgeGatewayAuthenticationError("edge credential binding was rejected") from exc
+        except (TypeError, ValueError) as exc:
+            self._failure(gateway_id, "authentication_rejected", "binding_invalid")
+            raise EdgeGatewayAuthenticationError("edge credential binding was rejected") from exc
+        if not isinstance(credential, str) or not credential.startswith("dopg_") or len(credential) > 160:
+            self._failure(gateway_id, "authentication_rejected", "credential_invalid")
+            raise EdgeGatewayAuthenticationError("edge credential binding was rejected")
+        row = self.repository.authenticate(_digest(credential), lock=lock)
+        if row is None:
+            self._failure(supplied[0], "authentication_rejected", "credential_invalid")
+            raise EdgeGatewayAuthenticationError("edge credential binding was rejected")
+        expected = (row["gateway_id"], row["environment"], row["network_zone"], row["generation"], row["certificate_sha256"])
+        if any(not hmac.compare_digest(str(a), str(b)) for a, b in zip(supplied, expected, strict=True)):
+            self._failure(row["gateway_id"], "authentication_rejected", "binding_mismatch")
+            raise EdgeGatewayAuthenticationError("edge credential binding was rejected")
+        return dict(row)
+
+    def list_gateways(self, *, limit=50, offset=0) -> list[dict]:
+        limit, offset = _page(limit, offset)
+        return self.repository.list_gateways(limit=limit, offset=offset)
+
+    def heartbeat(self, *, version, safe_summary, **auth) -> dict:
+        version = _version(version)
+        safe_summary = _safe_summary(safe_summary, "health_summary")
+        identity = self.authenticate(**auth, lock=True)
+        result = self.repository.heartbeat(identity["gateway_id"], version, safe_summary)
+        result["status"] = "online"
+        return result
+
+    def rotate(self, gateway_id, *, certificate_sha256, request_id, actor_uid) -> dict:
+        request_id = _text(request_id, "request_id", 255)
+        certificate_sha256 = _sha(certificate_sha256, "certificate_sha256")
+        request_digest = canonical_sha256({"gateway_id": str(gateway_id), "certificate_sha256": certificate_sha256})
+        token = "dopg_" + secrets.token_urlsafe(40)
+        gateway_id = _uuid(gateway_id, "gateway_id")
+        result = self.repository.rotate(gateway_id, _digest(token), certificate_sha256, _uuid(actor_uid, "actor_uid"), request_id, request_digest)
+        if result is None:
+            raise EdgeGatewayNotFoundError("edge gateway was not found")
+        if result.get("conflict"):
+            raise EdgeGatewayConflictError("rotation request conflicts with an existing request")
+        if result.get("replayed"):
+            return {"gateway_id": gateway_id, "generation": result["generation"], "credential": None, "credential_returned_once": False, "replayed": True}
+        return {**result, "gateway_id": gateway_id, "credential": token, "credential_returned_once": True}
+
+    def revoke(self, gateway_id, *, actor_uid) -> bool:
+        return self.repository.revoke(_uuid(gateway_id, "gateway_id"), _uuid(actor_uid, "actor_uid"))
+
+    def issue_task(self, value: Mapping[str, object], *, actor_uid) -> dict:
+        try:
+            contract = EdgeTaskContract.from_mapping(value)
+        except EdgeContractError as exc:
+            raise EdgeGatewayValidationError("edge task contract is invalid") from exc
+        if _uuid(contract.gateway_id, "gateway_id") != contract.gateway_id:
+            raise EdgeGatewayValidationError("gateway_id is invalid")
+        gateway = self.repository.gateway_binding(contract.gateway_id)
+        if gateway is None or gateway["status"] not in {"active", "offline"}:
+            self.repository.rollback()
+            raise EdgeGatewayNotFoundError("edge gateway was not found")
+        if any(
+            getattr(contract, field) != gateway[field]
+            for field in ("environment", "network_zone", "policy_digest")
+        ):
+            self._failure(
+                contract.gateway_id, "binding_rejected", "task_binding_mismatch",
+                trusted=True,
+            )
+            raise EdgeGatewayValidationError("edge task gateway binding is invalid")
+        authority = self._signed_task_envelope(contract)
+        row = self.repository.insert_task(
+            contract.to_mapping(), contract.digest, authority,
+            _uuid(actor_uid, "actor_uid"),
+        )
+        if not row:
+            raise EdgeGatewayNotFoundError("edge gateway was not found")
+        if row["contract_digest"] != contract.digest:
+            self._failure(
+                contract.gateway_id, "idempotency_conflict", "task_contract_conflict",
+                trusted=True,
+            )
+            raise EdgeGatewayConflictError("task idempotency key conflicts with an existing contract")
+        return {"task_id": row["task_id"], "contract_digest": row["contract_digest"]}
+
+    def pull_task(self, **auth) -> dict:
+        identity = self.authenticate(**auth, lock=True)
+        token = "dopl_" + secrets.token_urlsafe(32)
+        row = self.repository.pull_task(identity["gateway_id"], _digest(token))
+        if row is None:
+            return {"task": None}
+        envelope = dict(row["authority"])
+        if envelope.get("task") != row["contract"]:
+            self.repository.rollback()
+            raise EdgeGatewayConfigurationError("stored task authority is invalid")
+        return {
+            "task": row["contract"],
+            "signed_task_envelope": envelope,
+            "lease_token": token,
+            "lease_expires_at": row["lease_expires_at"].isoformat(),
+        }
+
+    def cancel_task(self, task_id, *, actor_uid) -> bool:
+        return self.repository.cancel_task(_text(task_id, "task_id", 255), _uuid(actor_uid, "actor_uid"))
+
+    def task_outcome(self, task_id, *, outcome, lease_token, safe_summary, **auth) -> dict:
+        if outcome not in {"completed", "failed", "cancelled"}:
+            raise EdgeGatewayValidationError("task outcome is invalid")
+        if not isinstance(lease_token, str) or not lease_token.startswith("dopl_") or len(lease_token) > 128:
+            raise EdgeGatewayAuthenticationError("edge task lease binding was rejected")
+        summary = _safe_summary(safe_summary)
+        identity = self.authenticate(**auth, lock=True)
+        result = self.repository.task_outcome(
+            identity["gateway_id"], _text(task_id, "task_id", 255),
+            _digest(lease_token), outcome, summary,
+        )
+        if result is None:
+            self._failure(
+                identity["gateway_id"], "binding_rejected", "task_outcome_rejected",
+                trusted=True,
+            )
+            raise EdgeGatewayConflictError("task outcome was rejected")
+        return result
+
+    def reconcile(
+        self, *, limit=50, cancel_cursor=None, release_cursor=None, **auth
+    ) -> dict:
+        limit, _ = _page(limit, 0)
+        if cancel_cursor is not None:
+            cancel_cursor = _text(cancel_cursor, "cancel_cursor", 255)
+        if release_cursor is not None:
+            release_cursor = _uuid(release_cursor, "release_cursor")
+        identity = self.authenticate(**auth, lock=True)
+        cancellations = self.repository.cancelled_tasks(
+            identity["gateway_id"], limit=limit, cursor=cancel_cursor
+        )
+        releases = self.repository.unfinished_releases(
+            identity["gateway_id"], limit=limit, cursor=release_cursor
+        )
+        result = {
+            "cancelled_task_ids": cancellations["items"],
+            "cancel_next_cursor": cancellations["next_cursor"],
+            "release_offers": releases["items"],
+            "release_next_cursor": releases["next_cursor"],
+            "release_baseline": self.repository.latest_release_baseline(
+                identity["gateway_id"]
+            ),
+        }
+        self.repository.commit()
+        return result
+
+    def accept_event(self, *, event, lease_token=None, **auth) -> dict:
+        identity = self.authenticate(**auth, lock=True)
+        if not isinstance(event, Mapping):
+            self.repository.rollback()
+            raise EdgeGatewayValidationError("edge event contract is invalid")
+        policy = EdgeEgressPolicy(allowed_control_hosts=set(identity["allowed_control_hosts"]), allowed_proxy_hosts=set(identity["allowed_proxy_hosts"]))
+        try:
+            approved = policy.approve_event(event)
+        except EdgePolicyError as exc:
+            self._failure(identity["gateway_id"], "policy_rejected", "egress_policy_rejected", trusted=True)
+            raise EdgeGatewayValidationError("edge event is not approved for control-plane egress") from exc
+        try:
+            contract = EdgeEventContract.from_mapping(approved)
+        except EdgeContractError as exc:
+            self._failure(identity["gateway_id"], "binding_rejected", "event_contract_invalid", trusted=True)
+            raise EdgeGatewayValidationError("edge event contract is invalid") from exc
+        if contract.gateway_id != identity["gateway_id"] or contract.environment != identity["environment"] or contract.network_zone != identity["network_zone"] or contract.policy_digest != identity["policy_digest"]:
+            self._failure(identity["gateway_id"], "binding_rejected", "event_gateway_mismatch", trusted=True)
+            raise EdgeGatewayAuthenticationError("edge event binding was rejected")
+        supplied_digest = contract.digest
+        existing = self.repository.event_by_id(identity["gateway_id"], contract.event_id)
+        if existing is not None:
+            if not hmac.compare_digest(existing["contract_digest"], supplied_digest):
+                self._failure(identity["gateway_id"], "idempotency_conflict", "event_contract_conflict", trusted=True)
+                raise EdgeGatewayConflictError("event identifier conflicts with an existing contract")
+            self.repository.rollback()
+            return dict(existing["ack"])
+        if not isinstance(lease_token, str) or not lease_token.startswith("dopl_") or len(lease_token) > 128:
+            self._failure(identity["gateway_id"], "binding_rejected", "lease_invalid", trusted=True)
+            raise EdgeGatewayAuthenticationError("edge task lease binding was rejected")
+        remote_lease_digest = _digest(lease_token)
+        task_contract = self.repository.task_lease_binding(
+            contract.task_id, contract.gateway_id, remote_lease_digest
+        )
+        if task_contract is None:
+            existing = self.repository.event_by_id(identity["gateway_id"], contract.event_id)
+            if existing is not None and hmac.compare_digest(existing["contract_digest"], supplied_digest):
+                self.repository.rollback()
+                return dict(existing["ack"])
+            self._failure(identity["gateway_id"], "binding_rejected", "lease_mismatch", trusted=True)
+            raise EdgeGatewayAuthenticationError("edge task lease binding was rejected")
+        if any(contract.to_mapping()[field] != task_contract[field] for field in (
+            "gateway_id", "environment", "network_zone", "purpose", "policy_digest",
+            "attempt",
+        )):
+            self._failure(identity["gateway_id"], "binding_rejected", "event_task_mismatch", trusted=True)
+            raise EdgeGatewayValidationError("edge event task binding is invalid")
+        ack = {
+            "event_id": contract.event_id,
+            "event_digest": contract.digest,
+            "remote_lease_digest": remote_lease_digest,
+            "received_at": datetime.now(UTC).isoformat().replace("+00:00", "Z"),
+            "status": "accepted",
+        }
+        accepted = self.repository.insert_event(contract.to_mapping(), contract.digest, ack)
+        if accepted is None:
+            self._failure(identity["gateway_id"], "idempotency_conflict", "event_contract_conflict", trusted=True)
+            raise EdgeGatewayConflictError("event identifier conflicts with an existing contract")
+        return accepted
+
+    def offer_release(
+        self, gateway_id, *, version, artifact_digest, rollback_version,
+        request_id, actor_uid, artifact_name=None, deadline_at=None,
+    ) -> dict:
+        gateway_id = _uuid(gateway_id, "gateway_id")
+        version = _release_version(version, "version")
+        artifact_digest = _sha(artifact_digest, "artifact_digest")
+        rollback_version = _release_version(rollback_version, "rollback_version")
+        if _release_version_tuple(version) <= _release_version_tuple(rollback_version):
+            raise EdgeGatewayValidationError(
+                "release version must be newer than rollback_version"
+            )
+        request_id = _text(request_id, "request_id", 255)
+        now = self._clock().astimezone(UTC)
+        supplied_deadline = deadline_at
+        artifact_name = _artifact_name(
+            artifact_name or f"edge-agent-{version}.bin"
+        )
+        deadline_at = _future_deadline(
+            deadline_at
+            or (now + timedelta(hours=24)).isoformat().replace("+00:00", "Z"),
+            now=now,
+        )
+        unsigned = {
+            "release_id": str(uuid.uuid4()), "version": version,
+            "rollback_version": rollback_version,
+            "artifact_digest": artifact_digest, "artifact_name": artifact_name,
+            "deadline_at": deadline_at, "status": "offered",
+        }
+        manifest = self._signed_release_manifest(unsigned)
+        request_digest = canonical_sha256({
+            "gateway_id": gateway_id, "version": version,
+            "artifact_digest": artifact_digest, "rollback_version": rollback_version,
+            "artifact_name": artifact_name,
+            "deadline_at": deadline_at if supplied_deadline is not None else None,
+        })
+        result = self.repository.offer_release(
+            gateway_id, manifest, _uuid(actor_uid, "actor_uid"), request_id,
+            request_digest,
+        )
+        if result.get("conflict"):
+            raise EdgeGatewayConflictError("release request conflicts with an existing request")
+        if not result:
+            raise EdgeGatewayNotFoundError("edge gateway was not found")
+        return result
+
+    def acknowledge_release(self, *, release_id, outcome, safe_summary, **auth) -> dict:
+        statuses = {"accepted": "accepted", "installed": "installed", "failed": "failed", "rollback": "rolled_back"}
+        if outcome not in statuses:
+            raise EdgeGatewayValidationError("release outcome is invalid")
+        release_id = _uuid(release_id, "release_id")
+        safe_summary = _safe_summary(safe_summary)
+        identity = self.authenticate(**auth, lock=True)
+        current = self.repository.release_for_ack(identity["gateway_id"], release_id)
+        if current is None:
+            self.repository.rollback()
+            self._failure(
+                identity["gateway_id"], "release_rejected",
+                "release_transition_invalid", trusted=True,
+            )
+            raise EdgeGatewayConflictError("release acknowledgement was rejected")
+        stored = dict(current["signed_manifest"])
+        unsigned = {
+            key: value for key, value in stored.items()
+            if key not in {
+                "manifest_digest", "signature", "signature_algorithm", "key_id"
+            }
+        }
+        unsigned["status"] = statuses[outcome]
+        manifest = self._signed_release_manifest(unsigned)
+        row = self.repository.acknowledge_release(
+            identity["gateway_id"], release_id, statuses[outcome], safe_summary,
+            manifest,
+        )
+        if row is None:
+            self._failure(identity["gateway_id"], "release_rejected", "release_transition_invalid", trusted=True)
+            raise EdgeGatewayConflictError("release acknowledgement was rejected")
+        return row

+ 26 - 0
app/core/system/permissions.py

@@ -76,6 +76,9 @@ IDENTITY_OPERATE = "identity:operate"
 CONNECTORS_READ = "connectors:read"
 CONNECTORS_OPERATE = "connectors:operate"
 CONNECTORS_MANAGE = "connectors:manage"
+EDGE_GATEWAYS_READ = "edge-gateways:read"
+EDGE_GATEWAYS_OPERATE = "edge-gateways:operate"
+EDGE_GATEWAYS_MANAGE = "edge-gateways:manage"
 
 ROLE_PERMISSIONS = {
     "viewer": frozenset(
@@ -92,6 +95,7 @@ ROLE_PERMISSIONS = {
             SECURITY_GOVERNANCE_READ,
             IDENTITY_READ,
             CONNECTORS_READ,
+            EDGE_GATEWAYS_READ,
         }
     ),
     "editor": frozenset(
@@ -133,6 +137,8 @@ ROLE_PERMISSIONS = {
             IDENTITY_OPERATE,
             CONNECTORS_READ,
             CONNECTORS_OPERATE,
+            EDGE_GATEWAYS_READ,
+            EDGE_GATEWAYS_OPERATE,
         }
     ),
     "admin": frozenset(
@@ -206,6 +212,9 @@ ROLE_PERMISSIONS = {
             CONNECTORS_READ,
             CONNECTORS_OPERATE,
             CONNECTORS_MANAGE,
+            EDGE_GATEWAYS_READ,
+            EDGE_GATEWAYS_OPERATE,
+            EDGE_GATEWAYS_MANAGE,
         }
     ),
 }
@@ -225,6 +234,7 @@ def permission_for_request(path: str, method: str) -> tuple[str, ...]:
         ("/api/system/identity/exchange", "POST"),
         ("/api/system/identity/refresh", "POST"),
         ("/api/datasource/connectors/machine/runs", "POST"),
+        ("/api/datasource/edge/register", "POST"),
     }
     if (path, method) in public_methods:
         return (PUBLIC,)
@@ -234,6 +244,22 @@ def permission_for_request(path: str, method: str) -> tuple[str, ...]:
         and method == "POST"
     ):
         return (PUBLIC,)
+    if path.startswith("/api/datasource/edge/gateways/") and method == "POST":
+        suffix = path.rsplit("/", 1)[-1]
+        if suffix in {"heartbeat", "pull", "events", "ack", "reconcile", "outcome"}:
+            return (PUBLIC,)
+    if path.startswith("/api/datasource/edge"):
+        if path == "/api/datasource/edge/gateways" and method == "GET":
+            return (EDGE_GATEWAYS_READ,)
+        if path == "/api/datasource/edge/enrollments" and method == "POST":
+            return (EDGE_GATEWAYS_MANAGE,)
+        if path == "/api/datasource/edge/tasks" and method == "POST":
+            return (EDGE_GATEWAYS_OPERATE,)
+        if path.startswith("/api/datasource/edge/tasks/") and path.endswith("/cancel") and method == "POST":
+            return (EDGE_GATEWAYS_OPERATE,)
+        if method == "GET":
+            return (EDGE_GATEWAYS_READ,)
+        return (EDGE_GATEWAYS_MANAGE,)
     if path.startswith("/api/system/identity"):
         if path == "/api/system/identity/logout" and method == "POST":
             return (IDENTITY_READ,)

+ 19 - 0
app/edge_gateway/__init__.py

@@ -0,0 +1,19 @@
+"""Pull-only enterprise edge runtime.
+
+The package intentionally exposes no listener or inbound server.  All control
+plane communication is initiated by :class:`EdgeTransport`.
+"""
+
+from .agent import EdgeAgent, EdgeRunnerAdapter, SignedReleaseManager
+from .bootstrap import EdgeBootstrapConfig
+from .transport import EdgeAuthenticationStopped, EdgeTransport, EdgeTransportError
+
+__all__ = [
+    "EdgeAgent",
+    "EdgeAuthenticationStopped",
+    "EdgeBootstrapConfig",
+    "EdgeRunnerAdapter",
+    "EdgeTransport",
+    "EdgeTransportError",
+    "SignedReleaseManager",
+]

+ 5 - 0
app/edge_gateway/__main__.py

@@ -0,0 +1,5 @@
+"""Run the enterprise edge process."""
+
+from app.edge_gateway.runtime import run
+
+raise SystemExit(run())

+ 1273 - 0
app/edge_gateway/agent.py

@@ -0,0 +1,1273 @@
+"""Durable pull-only edge agent, closed runner adapter, and release verifier."""
+
+from __future__ import annotations
+
+import base64
+import hashlib
+import os
+import re
+import sqlite3
+import stat
+from collections.abc import Callable, Mapping
+from datetime import UTC, datetime, timedelta
+from pathlib import Path, PurePosixPath
+from types import MappingProxyType
+
+from cryptography.exceptions import InvalidSignature
+from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
+from cryptography.hazmat.primitives.ciphers.aead import AESGCM
+from cryptography.hazmat.primitives.kdf.hkdf import HKDF
+
+from app.core.edge_gateway.contracts import (
+    EdgeContractError,
+    EdgeEventContract,
+    EdgeTaskContract,
+    SignedTaskEnvelope,
+    canonical_json_bytes,
+    canonical_sha256,
+    canonical_timestamp,
+    stable_event_id,
+)
+from app.core.edge_gateway.policy import EdgeEgressPolicy, EdgePolicyError
+from app.core.edge_gateway.queue import (
+    EdgeQueueConflictError,
+    EdgeQueueLeaseError,
+    SqliteEdgeQueue,
+)
+from app.edge_gateway.bootstrap import EdgeBootstrapConfig
+from app.edge_gateway.transport import EdgeAuthenticationStopped, EdgeTransportError
+
+_SHA256 = re.compile(r"^[0-9a-f]{64}$")
+_VERSION = re.compile(r"^(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})$")
+_RELEASE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,254}$")
+
+EDGE_NODE_BY_OPERATION = MappingProxyType(
+    {
+        "collect": "edge.collect",
+        "profile": "edge.profile",
+        "quality": "quality.check",
+        "lineage": "edge.lineage",
+        "controlled_query": "edge.controlled_query",
+    }
+)
+EVENT_CLASSIFICATION_BY_OPERATION = MappingProxyType(
+    {
+        "collect": "desensitized_metadata",
+        "profile": "statistics",
+        "quality": "evidence",
+        "lineage": "lineage",
+        "controlled_query": "statistics",
+    }
+)
+DEFAULT_GOVERNED_PURPOSES = frozenset(
+    {
+        "inventory",
+        "governed-inventory",
+        "metadata-inventory",
+        "profile-statistics",
+        "quality-evaluation",
+        "lineage-capture",
+        "controlled-query",
+    }
+)
+_RUNNER_REQUEST_FIELDS = frozenset(
+    {
+        "task_id",
+        "operation",
+        "purpose",
+        "classification",
+        "environment",
+        "network_zone",
+        "idempotency_key",
+        "deadline_at",
+    }
+)
+_RUNNER_RESULT_FIELDS = frozenset(
+    {"classification", "payload", "local_artifact_digest", "local_artifact_ref"}
+)
+
+
+class EdgeAgentError(RuntimeError):
+    """Bounded edge runtime failure."""
+
+
+class EdgeTaskCancelled(EdgeAgentError):
+    """Cooperative cancellation was observed before a terminal event."""
+
+
+class EdgeReleaseError(EdgeAgentError):
+    """A candidate release failed a closed verification or activation gate."""
+
+
+class LocalArtifactStore:
+    """Validate and delete local artifacts without exposing their paths outbound."""
+
+    def __init__(self, root: str | Path, *, clock: Callable[[], object] | None = None):
+        candidate = Path(root)
+        if not candidate.is_absolute() or not candidate.is_dir() or candidate.is_symlink():
+            raise ValueError("artifact root must be an existing absolute directory")
+        self.root = candidate.resolve()
+        self.clock = clock or (lambda: datetime.now(UTC))
+
+    def inspect(self, reference: object, expected_digest: object) -> dict[str, str]:
+        if (
+            not isinstance(reference, str)
+            or not reference
+            or "\x00" in reference
+            or len(reference.encode("utf-8")) > 2_048
+            or not isinstance(expected_digest, str)
+            or not _SHA256.fullmatch(expected_digest)
+        ):
+            raise EdgePolicyError("local artifact reference is invalid")
+        raw = Path(reference)
+        path = raw if raw.is_absolute() else self.root / raw
+        try:
+            resolved = path.resolve(strict=True)
+            resolved.relative_to(self.root)
+            current = self.root
+            for part in resolved.relative_to(self.root).parts:
+                current = current / part
+                if current.is_symlink():
+                    raise EdgePolicyError("local artifact symlink is forbidden")
+            metadata = resolved.lstat()
+        except (OSError, ValueError) as exc:
+            raise EdgePolicyError("local artifact escapes its configured root") from exc
+        if not stat.S_ISREG(metadata.st_mode):
+            raise EdgePolicyError("local artifact must be a regular file")
+        digest = hashlib.sha256()
+        try:
+            with resolved.open("rb") as stream:
+                for chunk in iter(lambda: stream.read(1_048_576), b""):
+                    digest.update(chunk)
+        except OSError as exc:
+            raise EdgePolicyError("local artifact cannot be read") from exc
+        if digest.hexdigest() != expected_digest:
+            raise EdgePolicyError("local artifact digest does not match content")
+        canonical_ref = str(resolved)
+        return {
+            "artifact_digest": expected_digest,
+            "artifact_ref": canonical_ref,
+            "artifact_ref_hash": canonical_sha256(canonical_ref),
+        }
+
+    def delete(self, reference: object, expected_digest: object) -> dict[str, str]:
+        inspected = self.inspect(reference, expected_digest)
+        try:
+            Path(inspected["artifact_ref"]).unlink()
+        except OSError as exc:
+            raise EdgePolicyError("local artifact deletion failed") from exc
+        return {
+            "artifact_digest": inspected["artifact_digest"],
+            "artifact_ref_hash": inspected["artifact_ref_hash"],
+            "deleted_at": canonical_timestamp(
+                _now(self.clock).isoformat().replace("+00:00", "Z")
+            ),
+            "status": "deleted",
+        }
+
+    def receipt_for_missing(
+        self, reference: object, expected_digest: object, expected_ref_hash: object
+    ) -> dict[str, str]:
+        if (
+            not isinstance(reference, str)
+            or not isinstance(expected_digest, str)
+            or not _SHA256.fullmatch(expected_digest)
+            or not isinstance(expected_ref_hash, str)
+            or not _SHA256.fullmatch(expected_ref_hash)
+        ):
+            raise EdgePolicyError("missing artifact recovery metadata is invalid")
+        path = Path(reference)
+        try:
+            path.parent.resolve(strict=True).relative_to(self.root)
+            canonical_ref = str(path.resolve(strict=False))
+        except (OSError, ValueError) as exc:
+            raise EdgePolicyError("missing artifact escapes its configured root") from exc
+        if path.exists() or path.is_symlink() or canonical_sha256(canonical_ref) != expected_ref_hash:
+            raise EdgePolicyError("missing artifact recovery proof is invalid")
+        return {
+            "artifact_digest": expected_digest,
+            "artifact_ref_hash": expected_ref_hash,
+            "deleted_at": canonical_timestamp(
+                _now(self.clock).isoformat().replace("+00:00", "Z")
+            ),
+            "status": "deleted",
+        }
+
+
+def _now(clock: Callable[[], object]) -> datetime:
+    value = clock()
+    if not isinstance(value, datetime) or value.tzinfo is None or value.utcoffset() is None:
+        raise ValueError("edge clock must return a timezone-aware datetime")
+    return value.astimezone(UTC)
+
+
+def _timestamp(clock: Callable[[], object]) -> str:
+    return canonical_timestamp(_now(clock).isoformat().replace("+00:00", "Z"))
+
+
+def _remote_lease_codec(config: EdgeBootstrapConfig):
+    """Protect recoverable control leases without persisting the credential."""
+
+    key = HKDF(
+        algorithm=hashes.SHA256(),
+        length=32,
+        salt=bytes.fromhex(config.certificate_sha256),
+        info=(
+            f"dataops-edge-remote-lease-v1:{config.gateway_id}:"
+            f"{config.environment}:{config.network_zone}"
+        ).encode(),
+    ).derive(config.credential.encode())
+    cipher = AESGCM(key)
+    aad = f"{config.gateway_id}:{config.policy_digest}".encode()
+
+    def encode(token: str) -> str:
+        nonce = os.urandom(12)
+        protected = cipher.encrypt(nonce, token.encode("utf-8"), aad)
+        return base64.urlsafe_b64encode(nonce + protected).decode("ascii")
+
+    def decode(value: str) -> str:
+        raw = base64.b64decode(value.encode("ascii"), altchars=b"-_", validate=True)
+        if len(raw) < 29:
+            raise ValueError("protected remote lease is invalid")
+        return cipher.decrypt(raw[:12], raw[12:], aad).decode("utf-8")
+
+    return encode, decode
+
+
+class EdgeRunnerAdapter:
+    """Map governed operations to exact local runner handlers.
+
+    No control-plane supplied parameters, module names, commands, SQL, paths, or
+    network destinations are accepted by this adapter.  Local handlers resolve
+    their governed configuration using the immutable task identity.
+    """
+
+    def __init__(
+        self,
+        executor: Callable[[str, Mapping[str, object], Callable[[], bool]], object],
+        *,
+        policy: EdgeEgressPolicy | None = None,
+        allowed_purposes: set[str] | frozenset[str] = DEFAULT_GOVERNED_PURPOSES,
+    ) -> None:
+        if not callable(executor):
+            raise ValueError("an explicit edge runner executor is required")
+        self._executor = executor
+        self.policy = policy or EdgeEgressPolicy(allowed_control_hosts=set())
+        if not isinstance(allowed_purposes, (set, frozenset)) or not allowed_purposes:
+            raise ValueError("explicit governed purposes are required")
+        if any(
+            not isinstance(item, str)
+            or not item
+            or len(item.encode("utf-8")) > 255
+            for item in allowed_purposes
+        ):
+            raise ValueError("governed purpose allowlist is invalid")
+        self.allowed_purposes = frozenset(allowed_purposes)
+
+    def execute(
+        self, task: EdgeTaskContract, cancel_requested: Callable[[], bool]
+    ) -> dict[str, object]:
+        task = EdgeTaskContract.from_mapping(task.to_mapping())
+        node = EDGE_NODE_BY_OPERATION.get(task.operation)
+        if node is None:
+            raise EdgePolicyError("edge task operation is not runner-approved")
+        if task.purpose not in self.allowed_purposes:
+            raise EdgePolicyError("edge task purpose is not runner-approved")
+        if cancel_requested():
+            raise EdgeTaskCancelled("edge task was cancelled")
+        request = {
+            "task_id": task.task_id,
+            "operation": task.operation,
+            "purpose": task.purpose,
+            "classification": task.classification,
+            "environment": task.environment,
+            "network_zone": task.network_zone,
+            "idempotency_key": task.idempotency_key,
+            "deadline_at": task.deadline_at,
+        }
+        if set(request) != _RUNNER_REQUEST_FIELDS:  # pragma: no cover - invariant
+            raise EdgePolicyError("edge runner request schema is invalid")
+        try:
+            raw = self._executor(node, MappingProxyType(request), cancel_requested)
+        except EdgeTaskCancelled:
+            raise
+        except (MemoryError, RecursionError) as exc:
+            raise EdgePolicyError("edge runner result exceeds safe limits") from exc
+        if cancel_requested():
+            raise EdgeTaskCancelled("edge task was cancelled")
+        if not isinstance(raw, Mapping):
+            raise EdgePolicyError("edge runner result must be a mapping")
+        unknown = set(raw) - _RUNNER_RESULT_FIELDS
+        missing = {"classification", "payload"} - set(raw)
+        if unknown or missing:
+            raise EdgePolicyError("edge runner result schema is invalid")
+        classification = raw["classification"]
+        expected = EVENT_CLASSIFICATION_BY_OPERATION[task.operation]
+        if classification != expected:
+            raise EdgePolicyError("edge runner result classification is invalid")
+        payload = raw["payload"]
+        if not isinstance(payload, Mapping):
+            raise EdgePolicyError("edge runner result payload must be a mapping")
+        try:
+            self.policy.validate_approved_payload(expected, payload)
+        except (EdgePolicyError, EdgeContractError, MemoryError, RecursionError) as exc:
+            raise EdgePolicyError("edge runner result is not approved for egress") from exc
+        artifact_digest = raw.get("local_artifact_digest")
+        artifact_ref = raw.get("local_artifact_ref")
+        if (artifact_digest is None) != (artifact_ref is None):
+            raise EdgePolicyError("local artifact digest and reference must be paired")
+        if artifact_digest is not None:
+            if not isinstance(artifact_digest, str) or not _SHA256.fullmatch(artifact_digest):
+                raise EdgePolicyError("local artifact digest is invalid")
+            if (
+                not isinstance(artifact_ref, str)
+                or not artifact_ref
+                or "\x00" in artifact_ref
+                or len(artifact_ref.encode("utf-8")) > 2_048
+            ):
+                raise EdgePolicyError("local artifact reference is invalid")
+        return {
+            "classification": expected,
+            "payload": dict(payload),
+            "local_artifact_digest": artifact_digest,
+            "local_artifact_ref": artifact_ref,
+        }
+
+
+class SignedReleaseManager:
+    """Verify Ed25519 manifests and activate candidates through injected primitives."""
+
+    _MANIFEST_FIELDS = frozenset(
+        {
+            "release_id",
+            "version",
+            "rollback_version",
+            "artifact_digest",
+            "artifact_name",
+            "deadline_at",
+            "signature_algorithm",
+            "key_id",
+            "manifest_digest",
+            "signature",
+            "status",
+        }
+    )
+
+    def __init__(
+        self,
+        *,
+        current_version: str,
+        trusted_keys: Mapping[str, str],
+        artifact_loader: Callable[[Mapping[str, object]], bytes],
+        activator: Callable[[str, bytes], bool],
+        rollback: Callable[[str], bool],
+        clock: Callable[[], object] | None = None,
+    ) -> None:
+        if not _VERSION.fullmatch(current_version):
+            raise ValueError("current release version is invalid")
+        if any(int(part) > 2_147_483_647 for part in current_version.split(".")):
+            raise ValueError("current release version is invalid")
+        if not trusted_keys:
+            raise ValueError("trusted release keys are required")
+        self.current_version = current_version
+        self.trusted_keys = dict(trusted_keys)
+        self.artifact_loader = artifact_loader
+        self.activator = activator
+        self.rollback = rollback
+        self.clock = clock or (lambda: datetime.now(UTC))
+        self._seen: dict[str, str] = {}
+
+    @staticmethod
+    def _version_tuple(value: str) -> tuple[int, ...]:
+        return tuple(int(item) for item in value.split("."))
+
+    def verify(
+        self,
+        value: Mapping[str, object],
+        *,
+        allow_recovery: bool = False,
+    ) -> dict[str, object]:
+        if not isinstance(value, Mapping) or set(value) != self._MANIFEST_FIELDS:
+            raise EdgeReleaseError("release manifest schema is invalid")
+        manifest = dict(value)
+        release_id = manifest["release_id"]
+        version = manifest["version"]
+        rollback_version = manifest["rollback_version"]
+        if not isinstance(release_id, str) or not _RELEASE_ID.fullmatch(release_id):
+            raise EdgeReleaseError("release identifier is invalid")
+        status = manifest.get("status")
+        if status not in {"offered", "accepted", "failed", "installed", "rolled_back"}:
+            raise EdgeReleaseError("release state is invalid")
+        if (
+            not isinstance(version, str)
+            or not isinstance(rollback_version, str)
+            or not _VERSION.fullmatch(version)
+            or not _VERSION.fullmatch(rollback_version)
+            or any(int(part) > 2_147_483_647 for part in version.split("."))
+            or any(int(part) > 2_147_483_647 for part in rollback_version.split("."))
+        ):
+            raise EdgeReleaseError("release downgrade or rollback binding was rejected")
+        if not allow_recovery and status in {"offered", "accepted"} and (
+            rollback_version != self.current_version
+            or self._version_tuple(version) <= self._version_tuple(self.current_version)
+        ):
+            raise EdgeReleaseError("release downgrade or rollback binding was rejected")
+        artifact_name = manifest["artifact_name"]
+        if (
+            not isinstance(artifact_name, str)
+            or not artifact_name
+            or len(artifact_name.encode("utf-8")) > 255
+            or PurePosixPath(artifact_name).name != artifact_name
+            or artifact_name in {".", ".."}
+            or any(character in artifact_name for character in ("/", "\\", "\x00"))
+        ):
+            raise EdgeReleaseError("release artifact path was rejected")
+        if manifest["signature_algorithm"] != "Ed25519":
+            raise EdgeReleaseError("release signature algorithm is not approved")
+        key_id = manifest["key_id"]
+        key_hex = self.trusted_keys.get(key_id) if isinstance(key_id, str) else None
+        if key_hex is None:
+            raise EdgeReleaseError("release signing key is not trusted")
+        for field in ("artifact_digest", "manifest_digest"):
+            if not isinstance(manifest[field], str) or not _SHA256.fullmatch(manifest[field]):
+                raise EdgeReleaseError("release digest is invalid")
+        try:
+            deadline = datetime.fromisoformat(str(manifest["deadline_at"]).replace("Z", "+00:00"))
+        except ValueError as exc:
+            raise EdgeReleaseError("release deadline is invalid") from exc
+        if deadline.tzinfo is None or deadline.astimezone(UTC) <= _now(self.clock):
+            raise EdgeReleaseError("release deadline has expired")
+        try:
+            canonical_deadline = canonical_timestamp(manifest["deadline_at"], "deadline_at")
+        except EdgeContractError as exc:
+            raise EdgeReleaseError("release deadline is invalid") from exc
+        if canonical_deadline != manifest["deadline_at"]:
+            raise EdgeReleaseError("release deadline must use canonical UTC form")
+        unsigned = {
+            key: manifest[key]
+            for key in sorted(self._MANIFEST_FIELDS - {"manifest_digest", "signature"})
+        }
+        expected_digest = canonical_sha256(unsigned)
+        if expected_digest != manifest["manifest_digest"]:
+            raise EdgeReleaseError("release manifest digest does not match")
+        content_digest = canonical_sha256(
+            {
+                key: manifest[key]
+                for key in (
+                    "release_id",
+                    "version",
+                    "rollback_version",
+                    "artifact_digest",
+                    "artifact_name",
+                    "deadline_at",
+                )
+            }
+        )
+        prior = self._seen.get(release_id)
+        if prior is not None and prior != content_digest:
+            raise EdgeReleaseError("release identifier replay changed its digest")
+        try:
+            signature = bytes.fromhex(str(manifest["signature"]))
+            Ed25519PublicKey.from_public_bytes(bytes.fromhex(key_hex)).verify(
+                signature, canonical_json_bytes(unsigned)
+            )
+        except (ValueError, InvalidSignature) as exc:
+            raise EdgeReleaseError("release signature verification failed") from exc
+        self._seen[release_id] = content_digest
+        return manifest
+
+    def install(self, value: Mapping[str, object]) -> dict[str, object]:
+        manifest = self.verify(value)
+        release_id = str(manifest["release_id"])
+        previous = self.current_version
+        try:
+            artifact = self.artifact_loader(MappingProxyType(manifest))
+            if not isinstance(artifact, bytes) or hashlib.sha256(artifact).hexdigest() != manifest["artifact_digest"]:
+                raise EdgeReleaseError("release artifact digest does not match")
+            if not self.activator(str(manifest["version"]), artifact):
+                raise EdgeReleaseError("release candidate health confirmation failed")
+        except Exception:
+            rolled_back = bool(self.rollback(previous))
+            return {
+                "release_id": release_id,
+                "outcome": "rollback" if rolled_back else "failed",
+                "summary": {
+                    "release_status": "rolled_back" if rolled_back else "failed",
+                    "version": previous,
+                },
+            }
+        self.current_version = str(manifest["version"])
+        return {
+            "release_id": release_id,
+            "outcome": "installed",
+            "summary": {"release_status": "installed", "version": self.current_version},
+        }
+
+
+class EdgeAgent:
+    """One durable worker for pull, execute, persist, and acknowledge cycles."""
+
+    def __init__(
+        self,
+        config: EdgeBootstrapConfig,
+        transport,
+        runner: EdgeRunnerAdapter,
+        *,
+        worker_id: str = "edge-worker-1",
+        clock: Callable[[], object] | None = None,
+        random: Callable[[], float] | None = None,
+        release_manager: SignedReleaseManager | None = None,
+        artifact_store: LocalArtifactStore | None = None,
+    ) -> None:
+        if not isinstance(config, EdgeBootstrapConfig):
+            raise ValueError("validated edge bootstrap configuration is required")
+        if not isinstance(runner, EdgeRunnerAdapter):
+            raise ValueError("validated edge runner adapter is required")
+        if not isinstance(worker_id, str) or not worker_id or len(worker_id.encode()) > 255:
+            raise ValueError("worker_id is invalid")
+        self.config = config
+        self.transport = transport
+        self.runner = runner
+        self.worker_id = worker_id
+        self.clock = clock or (lambda: datetime.now(UTC))
+        self.random = random or __import__("random").random
+        self.policy = EdgeEgressPolicy(
+            allowed_control_hosts=set(config.allowed_control_hosts),
+            allowed_proxy_hosts=set(config.allowed_proxy_hosts),
+        )
+        lease_encoder, lease_decoder = _remote_lease_codec(config)
+        self.queue = SqliteEdgeQueue(
+            config.queue_path,
+            clock=self.clock,
+            egress_policy=self.policy,
+            random_source=self.random,
+            remote_lease_encoder=lease_encoder,
+            remote_lease_decoder=lease_decoder,
+        )
+        self.artifact_store = artifact_store or LocalArtifactStore(
+            config.artifact_root, clock=self.clock
+        )
+        self.release_manager = release_manager
+        self._cancelled: set[str] = set()
+        self._stopped = False
+        self._needs_remote_lease_task: str | None = None
+        self._trusted_task_keys = {
+            key_id: bytes.fromhex(public_key)
+            for key_id, public_key in config.trusted_task_keys.items()
+        }
+        self.gateway_id_digest = hashlib.sha256(config.gateway_id.encode()).hexdigest()
+
+    def retry_delay(self, attempt: int) -> float:
+        if isinstance(attempt, bool) or not isinstance(attempt, int) or attempt < 1:
+            raise ValueError("retry attempt is invalid")
+        base = min(300.0, float(2 ** min(attempt - 1, 8)))
+        jitter = self.random()
+        if not isinstance(jitter, (int, float)) or not 0 <= jitter <= 1:
+            raise ValueError("random source returned an invalid value")
+        return min(300.0, base * (0.75 + 0.5 * float(jitter)))
+
+    def _validate_binding(self, contract: EdgeTaskContract) -> None:
+        expected = (
+            self.config.gateway_id,
+            self.config.environment,
+            self.config.network_zone,
+            self.config.policy_digest,
+        )
+        actual = (
+            contract.gateway_id,
+            contract.environment,
+            contract.network_zone,
+            contract.policy_digest,
+        )
+        if actual != expected:
+            raise EdgePolicyError("pulled task binding does not match this edge gateway")
+
+    def accept_pulled_task(
+        self,
+        envelope_value: Mapping[str, object],
+        lease_token: str,
+        lease_expires_at: str,
+    ):
+        if not isinstance(lease_token, str) or not lease_token.startswith("dopl_") or len(lease_token) > 128:
+            raise EdgePolicyError("control task lease is invalid")
+        try:
+            envelope = SignedTaskEnvelope.verify_mapping(
+                envelope_value,
+                authority_keys=self._trusted_task_keys,
+                now=_now(self.clock),
+                allowed_future_skew_seconds=(
+                    self.config.task_authority_clock_skew_seconds
+                ),
+            )
+        except EdgeContractError as exc:
+            raise EdgePolicyError("signed task authority was rejected") from exc
+        self._validate_binding(envelope.task)
+        return self.queue.accept_signed_task(
+            envelope,
+            remote_lease_token=lease_token,
+            remote_lease_expires_at=lease_expires_at,
+            remote_attempt=envelope.task.attempt,
+        )
+
+    def _cancel_requested(self, task_id: str) -> bool:
+        if task_id in self._cancelled:
+            return True
+        probe = getattr(self.transport, "cancel_requested", None)
+        if callable(probe):
+            try:
+                if bool(probe(task_id)):
+                    self._cancelled.add(task_id)
+                    return True
+            except EdgeAuthenticationStopped:
+                self._stopped = True
+                raise
+            except EdgeTransportError:
+                return task_id in self._cancelled
+        return False
+
+    def _event(self, task: EdgeTaskContract, result: Mapping[str, object]) -> EdgeEventContract:
+        identity = {
+            "task_id": task.task_id,
+            "gateway_id": task.gateway_id,
+            "environment": task.environment,
+            "network_zone": task.network_zone,
+            "purpose": task.purpose,
+            "classification": result["classification"],
+            "contract_version": task.contract_version,
+            "occurred_at": _timestamp(self.clock),
+            "attempt": task.attempt,
+            "idempotency_key": task.idempotency_key,
+            "policy_digest": task.policy_digest,
+            "payload": result["payload"],
+        }
+        return EdgeEventContract(event_id=stable_event_id(identity), **identity)  # type: ignore[arg-type]
+
+    def _artifact_metadata(
+        self,
+        task: EdgeTaskContract,
+        result: Mapping[str, object],
+    ) -> tuple[dict[str, object], ...]:
+        digest = result.get("local_artifact_digest")
+        reference = result.get("local_artifact_ref")
+        if digest is None or reference is None:
+            return ()
+        inspected = self.artifact_store.inspect(reference, digest)
+        retention_class = (
+            task.classification
+            if task.classification in {"raw", "recent_detail", "evidence"}
+            else "raw"
+            if task.classification == "restricted"
+            else "metadata"
+        )
+        retention_until = _now(self.clock) + timedelta(
+            days=self.policy.retention_days(retention_class)
+        )
+        return (
+            {
+                "artifact_digest": inspected["artifact_digest"],
+                "artifact_ref": inspected["artifact_ref"],
+                "artifact_ref_hash": inspected["artifact_ref_hash"],
+                "classification": task.classification,
+                "retention_until": canonical_timestamp(
+                    retention_until.isoformat().replace("+00:00", "Z")
+                ),
+            },
+        )
+
+    def _drain_artifact_cleanup(self) -> str | None:
+        artifact = self.queue.claim_artifact_cleanup(self.worker_id)
+        if artifact is None:
+            return None
+        if artifact.artifact_ref is None:
+            self.queue.fail_artifact_cleanup(
+                artifact.artifact_digest,
+                lease_token=str(artifact.lease_token),
+                error_code="artifact_reference_missing",
+            )
+            return None
+        try:
+            receipt = self.artifact_store.delete(
+                artifact.artifact_ref, artifact.artifact_digest
+            )
+            self.queue.acknowledge_artifact_cleanup(
+                artifact.artifact_digest,
+                lease_token=str(artifact.lease_token),
+                receipt=receipt,
+            )
+            return artifact.artifact_digest
+        except EdgePolicyError:
+            try:
+                receipt = self.artifact_store.receipt_for_missing(
+                    artifact.artifact_ref,
+                    artifact.artifact_digest,
+                    artifact.artifact_ref_hash,
+                )
+                self.queue.acknowledge_artifact_cleanup(
+                    artifact.artifact_digest,
+                    lease_token=str(artifact.lease_token),
+                    receipt=receipt,
+                )
+                return artifact.artifact_digest
+            except EdgePolicyError:
+                pass
+            self.queue.fail_artifact_cleanup(
+                artifact.artifact_digest,
+                lease_token=str(artifact.lease_token),
+                error_code="artifact_cleanup_failed",
+            )
+            return None
+
+    def _drain_event(self) -> str | None:
+        event = self.queue.claim_event(self.worker_id)
+        if event is None:
+            return None
+        task_id = str(event.event["task_id"])
+        task = self.queue.get_task(task_id)
+        if task is None:
+            self.queue.fail_event(
+                event.event_id,
+                lease_token=str(event.lease_token),
+                error_code="control_lease_unavailable",
+            )
+            return None
+        expiry = task.remote_lease_expires_at
+        expired = (
+            expiry is None
+            or datetime.fromisoformat(expiry.replace("Z", "+00:00")) <= _now(self.clock)
+        )
+        if expired:
+            self._needs_remote_lease_task = task_id
+            self.queue.fail_event(
+                event.event_id,
+                lease_token=str(event.lease_token),
+                error_code="remote_lease_expired",
+            )
+            return None
+        try:
+            control_lease = self.queue.recover_remote_lease(task_id)
+        except EdgeQueueLeaseError:
+            self._needs_remote_lease_task = task_id
+            self.queue.fail_event(
+                event.event_id,
+                lease_token=str(event.lease_token),
+                error_code="control_lease_unavailable",
+            )
+            return None
+        try:
+            ack = self.transport.send_event(event.event, control_lease)
+            self.queue.acknowledge_event(
+                event.event_id,
+                lease_token=str(event.lease_token),
+                acknowledgement=ack,
+            )
+            return event.event_id
+        except EdgeAuthenticationStopped:
+            if not expired:
+                self._stopped = True
+            else:
+                self._needs_remote_lease_task = task_id
+            self.queue.fail_event(
+                event.event_id,
+                lease_token=str(event.lease_token),
+                error_code=(
+                    "credential_stopped" if not expired else "remote_lease_expired"
+                ),
+            )
+            if not expired:
+                raise
+            return None
+        except EdgeTransportError:
+            self.queue.fail_event(
+                event.event_id,
+                lease_token=str(event.lease_token),
+                error_code="control_unavailable",
+            )
+            return None
+
+    def _has_unresolved_events(self) -> bool:
+        connection = sqlite3.connect(f"file:{self.queue.db_path}?mode=ro", uri=True)
+        try:
+            count = connection.execute(
+                "SELECT COUNT(*) FROM edge_outbound_events WHERE status IN ('pending','sending')"
+            ).fetchone()[0]
+        finally:
+            connection.close()
+        return bool(count)
+
+    def _expired_pending_remote_task(self) -> str | None:
+        connection = sqlite3.connect(f"file:{self.queue.db_path}?mode=ro", uri=True)
+        try:
+            row = connection.execute(
+                """SELECT task_id FROM edge_tasks
+                WHERE status='pending' AND (
+                    remote_lease_expires_at IS NULL
+                    OR julianday(remote_lease_expires_at) <= julianday(?)
+                ) ORDER BY created_at,task_id LIMIT 1""",
+                (_timestamp(self.clock),),
+            ).fetchone()
+        finally:
+            connection.close()
+        return str(row[0]) if row is not None else None
+
+    def _apply_reconcile(self, value: Mapping[str, object]) -> tuple[list[str], list[dict]]:
+        legacy = {"cancelled_task_ids", "release_offers"}
+        paged = {
+            "cancelled_task_ids", "cancel_next_cursor", "release_offers",
+            "release_next_cursor", "release_baseline",
+        }
+        if (
+            not isinstance(value, Mapping)
+            or (set(value) != legacy and set(value) != paged)
+        ):
+            raise EdgeTransportError("reconcile response schema is invalid")
+        cancelled = value["cancelled_task_ids"]
+        releases = value["release_offers"]
+        if not isinstance(cancelled, list) or not isinstance(releases, list) or len(cancelled) > 100 or len(releases) > 100:
+            raise EdgeTransportError("reconcile response exceeds safe limits")
+        applied: list[str] = []
+        for task_id in cancelled:
+            if not isinstance(task_id, str) or len(task_id.encode()) > 255:
+                raise EdgeTransportError("cancel reconciliation is invalid")
+            self._cancelled.add(task_id)
+            queued = self.queue.get_task(task_id)
+            if queued is not None and queued.status in {"pending", "leased"}:
+                self.queue.cancel_with_outcome(task_id)
+                applied.append(task_id)
+        release_values: list[dict] = []
+        for release in releases:
+            if not isinstance(release, Mapping):
+                raise EdgeTransportError("release reconciliation is invalid")
+            release_values.append(dict(release))
+        if set(value) == paged and value["release_baseline"] is not None:
+            baseline = value["release_baseline"]
+            if not isinstance(baseline, Mapping):
+                raise EdgeTransportError("release baseline is invalid")
+            if all(
+                item.get("release_id") != baseline.get("release_id")
+                for item in release_values
+            ):
+                release_values.append(dict(baseline))
+        return applied, release_values
+
+    def _drain_outcome(self) -> str | None:
+        outcome = self.queue.claim_outcome(self.worker_id)
+        if outcome is None:
+            return None
+        try:
+            control_lease = self.queue.recover_remote_lease(outcome.task_id)
+            sender = getattr(self.transport, "task_outcome", None)
+            if not callable(sender):
+                raise EdgeTransportError("task outcome transport is unavailable")
+            acknowledgement = sender(
+                outcome.task_id,
+                outcome.outcome["outcome"],
+                control_lease,
+                outcome.outcome["safe_summary"],
+            )
+            self.queue.acknowledge_outcome(
+                outcome.task_id,
+                lease_token=str(outcome.lease_token),
+                acknowledgement=acknowledgement,
+            )
+            return outcome.task_id
+        except EdgeAuthenticationStopped:
+            self._stopped = True
+            self.queue.fail_outcome(
+                outcome.task_id,
+                lease_token=str(outcome.lease_token),
+                error_code="credential_stopped",
+            )
+            raise
+        except (EdgeTransportError, EdgeQueueLeaseError):
+            self.queue.fail_outcome(
+                outcome.task_id,
+                lease_token=str(outcome.lease_token),
+                error_code="control_unavailable",
+            )
+            return None
+
+    def _process_releases(self, releases: list[dict]) -> None:
+        if not releases:
+            return
+        if self.release_manager is None:
+            raise EdgeReleaseError("signed release manager is not configured")
+        for release in releases:
+            verified = self.release_manager.verify(release, allow_recovery=True)
+            release_id = str(verified["release_id"])
+            version = str(verified["version"])
+            rollback_version = str(verified["rollback_version"])
+            content_digest = canonical_sha256(
+                {
+                    key: verified[key]
+                    for key in (
+                        "release_id",
+                        "version",
+                        "rollback_version",
+                        "artifact_digest",
+                        "artifact_name",
+                        "deadline_at",
+                    )
+                }
+            )
+            existing = self.queue.get_release_state(release_id)
+            state = self.queue.put_release_state(
+                release_id=release_id,
+                manifest_digest=content_digest,
+                version=version,
+                rollback_version=rollback_version,
+            )
+            server_status = str(verified["status"])
+            if (
+                existing is None
+                and server_status == "offered"
+                and (
+                    rollback_version != self.release_manager.current_version
+                    or self.release_manager._version_tuple(version)
+                    <= self.release_manager._version_tuple(
+                        self.release_manager.current_version
+                    )
+                )
+            ):
+                raise EdgeReleaseError(
+                    "release downgrade or rollback binding was rejected"
+                )
+
+            if server_status == "rolled_back":
+                while state.status != "rolled_back":
+                    target = (
+                        "accepted"
+                        if state.status == "offered"
+                        else "candidate"
+                        if state.status == "accepted"
+                        else "failed"
+                        if state.status in {"candidate", "installed"}
+                        else "rolled_back"
+                    )
+                    state = self.queue.compare_and_set_release_state(
+                        release_id,
+                        expected_status=state.status,
+                        target_status=target,
+                        previous_version=rollback_version,
+                        current_version=rollback_version,
+                    )
+                self.release_manager.current_version = rollback_version
+                continue
+
+            if server_status == "installed":
+                if state.status in {"offered", "accepted"}:
+                    if state.status == "offered":
+                        state = self.queue.compare_and_set_release_state(
+                            release_id,
+                            expected_status="offered",
+                            target_status="accepted",
+                            previous_version=rollback_version,
+                        )
+                    state = self.queue.compare_and_set_release_state(
+                        release_id,
+                        expected_status="accepted",
+                        target_status="candidate",
+                        previous_version=rollback_version,
+                    )
+                if state.status == "candidate":
+                    state = self.queue.compare_and_set_release_state(
+                        release_id,
+                        expected_status="candidate",
+                        target_status="installed",
+                        previous_version=rollback_version,
+                        current_version=version,
+                    )
+                self.release_manager.current_version = version
+                continue
+
+            if server_status == "offered":
+                if state.status == "offered":
+                    state = self.queue.compare_and_set_release_state(
+                        release_id,
+                        expected_status="offered",
+                        target_status="accepted",
+                        previous_version=rollback_version,
+                    )
+                self.transport.acknowledge_release(
+                    release_id,
+                    "accepted",
+                    {"release_status": "accepted", "version": version},
+                )
+
+            if state.status == "installed":
+                self.transport.acknowledge_release(
+                    release_id,
+                    "installed",
+                    {"release_status": "installed", "version": version},
+                )
+                self.release_manager.current_version = version
+                continue
+            if state.status == "rolled_back":
+                if server_status == "accepted":
+                    self.transport.acknowledge_release(
+                        release_id,
+                        "failed",
+                        {"release_status": "failed", "version": version},
+                    )
+                self.transport.acknowledge_release(
+                    release_id,
+                    "rollback",
+                    {"release_status": "rolled_back", "version": rollback_version},
+                )
+                continue
+            if state.status == "failed" or server_status == "failed":
+                if state.status != "failed":
+                    state = self.queue.compare_and_set_release_state(
+                        release_id,
+                        expected_status=state.status,
+                        target_status="failed",
+                        previous_version=rollback_version,
+                    )
+                if not self.release_manager.rollback(rollback_version):
+                    raise EdgeReleaseError("release rollback health confirmation failed")
+                state = self.queue.compare_and_set_release_state(
+                    release_id,
+                    expected_status="failed",
+                    target_status="rolled_back",
+                    previous_version=rollback_version,
+                    current_version=rollback_version,
+                )
+                self.release_manager.current_version = rollback_version
+                if server_status != "failed":
+                    self.transport.acknowledge_release(
+                        release_id,
+                        "failed",
+                        {"release_status": "failed", "version": version},
+                    )
+                self.transport.acknowledge_release(
+                    release_id,
+                    "rollback",
+                    {"release_status": "rolled_back", "version": rollback_version},
+                )
+                continue
+
+            if state.status == "candidate":
+                if not self.release_manager.rollback(rollback_version):
+                    raise EdgeReleaseError("uncertain candidate rollback failed")
+                state = self.queue.compare_and_set_release_state(
+                    release_id,
+                    expected_status="candidate",
+                    target_status="failed",
+                    previous_version=rollback_version,
+                )
+                state = self.queue.compare_and_set_release_state(
+                    release_id,
+                    expected_status="failed",
+                    target_status="rolled_back",
+                    previous_version=rollback_version,
+                    current_version=rollback_version,
+                )
+                self.transport.acknowledge_release(
+                    release_id,
+                    "failed",
+                    {"release_status": "failed", "version": version},
+                )
+                self.transport.acknowledge_release(
+                    release_id,
+                    "rollback",
+                    {"release_status": "rolled_back", "version": rollback_version},
+                )
+                continue
+
+            state = self.queue.compare_and_set_release_state(
+                release_id,
+                expected_status="accepted",
+                target_status="candidate",
+                previous_version=rollback_version,
+            )
+            result = self.release_manager.install(release)
+            if result["outcome"] == "installed":
+                self.queue.compare_and_set_release_state(
+                    release_id,
+                    expected_status="candidate",
+                    target_status="installed",
+                    previous_version=rollback_version,
+                    current_version=version,
+                )
+                self.transport.acknowledge_release(
+                    release_id, "installed", result["summary"]
+                )
+            else:
+                self.queue.compare_and_set_release_state(
+                    release_id,
+                    expected_status="candidate",
+                    target_status="failed",
+                    previous_version=rollback_version,
+                )
+                self.queue.compare_and_set_release_state(
+                    release_id,
+                    expected_status="failed",
+                    target_status="rolled_back",
+                    previous_version=rollback_version,
+                    current_version=rollback_version,
+                )
+                self.transport.acknowledge_release(
+                    release_id,
+                    "failed",
+                    {"release_status": "failed", "version": version},
+                )
+                self.transport.acknowledge_release(
+                    release_id, "rollback", result["summary"]
+                )
+
+    def heartbeat_once(self) -> Mapping[str, object]:
+        """Send only the policy-approved bounded health summary."""
+
+        summary = self.safe_diagnostic()
+        self.policy.validate_approved_payload("health_summary", summary)
+        return self.transport.heartbeat(summary)
+
+    def run_once(self) -> dict[str, object]:
+        if self._stopped:
+            raise EdgeAuthenticationStopped("edge agent is stopped")
+        self._drain_artifact_cleanup()
+        try:
+            cancelled, releases = self._apply_reconcile(self.transport.reconcile())
+            self._drain_outcome()
+            self._process_releases(releases)
+            sent = self._drain_event()
+            if sent is not None:
+                return {"cancelled": cancelled, "sent": sent, "executed": None}
+            if self._has_unresolved_events() and self._needs_remote_lease_task is None:
+                return {"cancelled": cancelled, "sent": None, "executed": None}
+            pulled = self.transport.pull_task()
+            if pulled is not None:
+                if not isinstance(pulled, tuple) or len(pulled) != 3:
+                    raise EdgeTransportError("pulled task response is invalid")
+                accepted = self.accept_pulled_task(pulled[0], pulled[1], pulled[2])
+                if self._needs_remote_lease_task is not None:
+                    if accepted.task_id == self._needs_remote_lease_task:
+                        self._needs_remote_lease_task = None
+                    return {
+                        "cancelled": cancelled,
+                        "sent": None,
+                        "executed": None,
+                    }
+            elif self._needs_remote_lease_task is not None:
+                return {"cancelled": cancelled, "sent": None, "executed": None}
+            expired_pending = self._expired_pending_remote_task()
+            if expired_pending is not None:
+                self._needs_remote_lease_task = expired_pending
+                return {"cancelled": cancelled, "sent": None, "executed": None}
+        except EdgeAuthenticationStopped:
+            self._stopped = True
+            raise
+        task_row = self.queue.claim(self.worker_id)
+        if task_row is None:
+            return {"cancelled": cancelled, "sent": None, "executed": None}
+        contract = EdgeTaskContract.from_mapping(task_row.task)
+        now = _now(self.clock)
+        if (
+            datetime.fromisoformat(contract.deadline_at.replace("Z", "+00:00")) <= now
+            or task_row.lease_expires_at is None
+            or datetime.fromisoformat(
+                task_row.lease_expires_at.replace("Z", "+00:00")
+            ) <= now
+            or task_row.remote_lease_expires_at is None
+            or datetime.fromisoformat(
+                task_row.remote_lease_expires_at.replace("Z", "+00:00")
+            ) <= now
+        ):
+            self.queue.fail(
+                contract.task_id,
+                lease_token=str(task_row.lease_token),
+                error_code="execution_lease_expired",
+                retryable=False,
+            )
+            self._needs_remote_lease_task = contract.task_id
+            return {"cancelled": cancelled, "sent": None, "executed": None}
+        try:
+            control_lease = self.queue.recover_remote_lease(contract.task_id)
+        except EdgeQueueLeaseError:
+            self.queue.fail(
+                contract.task_id,
+                lease_token=str(task_row.lease_token),
+                error_code="control_lease_unavailable",
+            )
+            return {"cancelled": cancelled, "sent": None, "executed": None}
+        try:
+            result = self.runner.execute(
+                contract, lambda: self._cancel_requested(contract.task_id)
+            )
+            if self._cancel_requested(contract.task_id):
+                raise EdgeTaskCancelled("edge task was cancelled")
+            event = self._event(contract, result)
+            self.queue.complete_with_event(
+                contract.task_id,
+                lease_token=str(task_row.lease_token),
+                remote_lease_token=control_lease,
+                event=event,
+                artifacts=self._artifact_metadata(contract, result),
+            )
+            self._drain_artifact_cleanup()
+        except EdgeTaskCancelled:
+            self.queue.cancel_with_outcome(contract.task_id)
+            self._drain_outcome()
+            return {"cancelled": sorted(set(cancelled) | {contract.task_id}), "sent": None, "executed": None}
+        except (EdgePolicyError, EdgeContractError, EdgeQueueConflictError):
+            self.queue.fail(
+                contract.task_id,
+                lease_token=str(task_row.lease_token),
+                error_code="execution_boundary_rejected",
+                retryable=False,
+            )
+            raise
+        except EdgeQueueLeaseError:
+            current = self.queue.get_task(contract.task_id)
+            if current is not None and current.status == "cancelled":
+                return {
+                    "cancelled": sorted(set(cancelled) | {contract.task_id}),
+                    "sent": None,
+                    "executed": None,
+                }
+            if current is not None and current.status == "leased":
+                self.queue.fail(
+                    contract.task_id,
+                    lease_token=str(task_row.lease_token),
+                    error_code="execution_lease_expired",
+                    retryable=False,
+                )
+            self._needs_remote_lease_task = contract.task_id
+            return {"cancelled": cancelled, "sent": None, "executed": None}
+        except EdgeAuthenticationStopped:
+            self._stopped = True
+            self.queue.fail(
+                contract.task_id,
+                lease_token=str(task_row.lease_token),
+                error_code="credential_stopped",
+            )
+            raise
+        except Exception as exc:
+            self.queue.fail(
+                contract.task_id,
+                lease_token=str(task_row.lease_token),
+                error_code="runner_execution_failed",
+                retryable=False,
+            )
+            raise EdgeAgentError("edge runner execution failed") from exc
+        sent = self._drain_event()
+        return {"cancelled": cancelled, "sent": sent, "executed": contract.task_id}
+
+    def safe_diagnostic(self) -> dict[str, object]:
+        connection = sqlite3.connect(f"file:{self.queue.db_path}?mode=ro", uri=True)
+        try:
+            pending = connection.execute(
+                "SELECT COUNT(*) FROM edge_tasks WHERE status IN ('pending','leased')"
+            ).fetchone()[0]
+            events = connection.execute(
+                "SELECT COUNT(*) FROM edge_outbound_events WHERE status != 'acknowledged'"
+            ).fetchone()[0]
+        finally:
+            connection.close()
+        return {
+            "agent_status": "stopped" if self._stopped else "ready",
+            "gateway_id_digest": self.gateway_id_digest,
+            "queue_pending_count": int(pending),
+            "queue_unacknowledged_count": int(events),
+            "version": self.config.version,
+        }

+ 333 - 0
app/edge_gateway/bootstrap.py

@@ -0,0 +1,333 @@
+"""Fail-closed bootstrap configuration for an enterprise edge agent."""
+
+from __future__ import annotations
+
+import re
+import ssl
+import stat
+from collections.abc import Mapping, Sequence
+from dataclasses import dataclass, field
+from datetime import UTC, datetime
+from pathlib import Path
+from types import MappingProxyType
+
+from cryptography import x509
+from cryptography.hazmat.primitives import hashes, serialization
+from cryptography.hazmat.primitives.asymmetric import ec, ed448, ed25519, padding, rsa
+
+from app.core.edge_gateway.policy import EdgeEgressPolicy
+
+_SHA256 = re.compile(r"^[0-9a-f]{64}$")
+_IDENTIFIER = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,254}$")
+_VERSION = re.compile(r"^(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})$")
+_PLACEHOLDERS = {"change-me", "changeme", "example", "placeholder", "tbd", "todo"}
+_REQUIRED = frozenset(
+    {
+        "queue_path",
+        "artifact_root",
+        "gateway_id",
+        "credential",
+        "certificate_sha256",
+        "client_certificate_path",
+        "client_private_key_path",
+        "ca_bundle_path",
+        "generation",
+        "environment",
+        "network_zone",
+        "policy_digest",
+        "control_url",
+        "proxy_url",
+        "allowed_control_hosts",
+        "allowed_proxy_hosts",
+        "trusted_release_keys",
+        "trusted_task_keys",
+        "task_authority_clock_skew_seconds",
+        "version",
+    }
+)
+_OPTIONAL = frozenset({"server_crl_path", "allowed_control_origins", "allowed_proxy_origins"})
+
+
+def _text(value: object, label: str, maximum: int = 255) -> str:
+    if (
+        not isinstance(value, str)
+        or not value
+        or value.strip() != value
+        or "\x00" in value
+        or len(value.encode("utf-8")) > maximum
+        or value.casefold() in _PLACEHOLDERS
+    ):
+        raise ValueError(f"{label} is not explicitly configured")
+    return value
+
+
+def _identifier(value: object, label: str) -> str:
+    candidate = _text(value, label)
+    if not _IDENTIFIER.fullmatch(candidate):
+        raise ValueError(f"{label} is invalid")
+    return candidate
+
+
+def _hosts(value: object, label: str, *, required: bool) -> frozenset[str]:
+    if not isinstance(value, Sequence) or isinstance(value, (str, bytes)):
+        raise ValueError(f"{label} must be an explicit sequence")
+    items = [_text(item, label) for item in value]
+    if any(item != item.lower() for item in items):
+        raise ValueError(f"{label} must contain lowercase exact hosts")
+    result = frozenset(items)
+    if required and not result:
+        raise ValueError(f"{label} is required")
+    if len(result) != len(value):
+        raise ValueError(f"{label} contains duplicates")
+    return result
+
+
+@dataclass(frozen=True, slots=True)
+class EdgeBootstrapConfig:
+    queue_path: str
+    artifact_root: str
+    gateway_id: str
+    credential: str = field(repr=False)
+    certificate_sha256: str
+    client_certificate_path: str
+    client_private_key_path: str = field(repr=False)
+    ca_bundle_path: str
+    generation: int
+    environment: str
+    network_zone: str
+    policy_digest: str
+    control_url: str
+    proxy_url: str | None
+    allowed_control_hosts: frozenset[str]
+    allowed_proxy_hosts: frozenset[str]
+    trusted_release_keys: Mapping[str, str] = field(repr=False)
+    trusted_task_keys: Mapping[str, str] = field(repr=False)
+    version: str
+    task_authority_clock_skew_seconds: int = 0
+    server_crl_path: str | None = None
+    allowed_control_origins: frozenset[str] = frozenset()
+    allowed_proxy_origins: frozenset[str] = frozenset()
+
+    @classmethod
+    def from_mapping(cls, value: Mapping[str, object]) -> EdgeBootstrapConfig:
+        if (
+            not isinstance(value, Mapping)
+            or not set(value) >= _REQUIRED
+            or not set(value) <= _REQUIRED | _OPTIONAL
+        ):
+            raise ValueError("edge bootstrap requires the exact configuration fields")
+        queue = _text(value["queue_path"], "queue_path", 2_048)
+        if queue == ":memory:" or "mode=memory" in queue:
+            raise ValueError("edge queue must use an explicit disk path")
+        queue_path = Path(queue).expanduser()
+        if not queue_path.is_absolute():
+            raise ValueError("edge queue path must be absolute")
+        artifact_root = Path(
+            _text(value["artifact_root"], "artifact_root", 2_048)
+        ).expanduser()
+        if (
+            not artifact_root.is_absolute()
+            or not artifact_root.is_dir()
+            or artifact_root.is_symlink()
+        ):
+            raise ValueError("artifact_root must be an existing absolute directory")
+        credential = _text(value["credential"], "credential", 160)
+        if (
+            not credential.startswith("dopg_")
+            or len(credential) < 16
+            or any(marker in credential.casefold() for marker in _PLACEHOLDERS)
+        ):
+            raise ValueError("credential is invalid")
+        certificate = _text(value["certificate_sha256"], "certificate_sha256", 64)
+        policy_digest = _text(value["policy_digest"], "policy_digest", 64)
+        if not _SHA256.fullmatch(certificate) or not _SHA256.fullmatch(policy_digest):
+            raise ValueError("edge digest binding is invalid")
+        material: dict[str, Path] = {}
+        for field_name in (
+            "client_certificate_path",
+            "client_private_key_path",
+            "ca_bundle_path",
+        ):
+            candidate = Path(_text(value[field_name], field_name, 2_048)).expanduser()
+            if not candidate.is_absolute() or not candidate.is_file():
+                raise ValueError(f"{field_name} must be an existing absolute file")
+            material[field_name] = candidate.resolve()
+        key_mode = stat.S_IMODE(material["client_private_key_path"].stat().st_mode)
+        if key_mode & 0o077:
+            raise ValueError("client private key permissions are not secure")
+        try:
+            certificate_object = x509.load_pem_x509_certificate(
+                material["client_certificate_path"].read_bytes()
+            )
+            ca_object = x509.load_pem_x509_certificate(
+                material["ca_bundle_path"].read_bytes()
+            )
+            private_key = serialization.load_pem_private_key(
+                material["client_private_key_path"].read_bytes(), password=None
+            )
+        except (ValueError, OSError) as exc:
+            raise ValueError("mTLS material is invalid") from exc
+        actual_fingerprint = certificate_object.fingerprint(hashes.SHA256()).hex()
+        if actual_fingerprint != certificate:
+            raise ValueError("client certificate fingerprint does not match configuration")
+        public_encoding = {
+            "encoding": serialization.Encoding.DER,
+            "format": serialization.PublicFormat.SubjectPublicKeyInfo,
+        }
+        if certificate_object.public_key().public_bytes(**public_encoding) != private_key.public_key().public_bytes(**public_encoding):
+            raise ValueError("client private key does not match certificate")
+        now = datetime.now(UTC)
+        if certificate_object.not_valid_before_utc > now or certificate_object.not_valid_after_utc <= now:
+            raise ValueError("client certificate is not currently valid")
+        if ca_object.not_valid_after_utc <= now:
+            raise ValueError("CA bundle certificate is expired")
+        generation = value["generation"]
+        if isinstance(generation, bool) or not isinstance(generation, int) or generation < 1:
+            raise ValueError("generation is invalid")
+        control_hosts = _hosts(value["allowed_control_hosts"], "allowed_control_hosts", required=True)
+        proxy_hosts = _hosts(value["allowed_proxy_hosts"], "allowed_proxy_hosts", required=False)
+        proxy_url = value["proxy_url"]
+        if proxy_url is not None:
+            proxy_url = _text(proxy_url, "proxy_url", 2_048)
+        control_url = _text(value["control_url"], "control_url", 2_048)
+        def origins(field_name: str) -> frozenset[str]:
+            raw = value.get(field_name, [])
+            if not isinstance(raw, Sequence) or isinstance(raw, (str, bytes)):
+                raise ValueError(f"{field_name} must be an explicit sequence")
+            result = frozenset(_text(item, field_name, 2_048) for item in raw)
+            if len(result) != len(raw) or any(not item.startswith("https://") for item in result):
+                raise ValueError(f"{field_name} is invalid")
+            return result
+
+        control_origins = origins("allowed_control_origins")
+        proxy_origins = origins("allowed_proxy_origins")
+        EdgeEgressPolicy(
+            allowed_control_hosts=set(control_hosts),
+            allowed_proxy_hosts=set(proxy_hosts),
+            allowed_control_origins=set(control_origins) or None,
+            allowed_proxy_origins=set(proxy_origins) or None,
+        ).validate_destination(control_url, proxy_url=proxy_url)
+        def trusted_keys(field_name: str) -> Mapping[str, str]:
+            raw_keys = value[field_name]
+            if not isinstance(raw_keys, Mapping) or not raw_keys:
+                raise ValueError(f"{field_name} is required")
+            keys: dict[str, str] = {}
+            for raw_key_id, raw_public_key in raw_keys.items():
+                key_id = _identifier(raw_key_id, "signing key id")
+                public_key = _text(raw_public_key, "signing public key", 128)
+                try:
+                    decoded = bytes.fromhex(public_key)
+                except ValueError as exc:
+                    raise ValueError("signing public key must be lowercase hex") from exc
+                if (
+                    public_key != public_key.lower()
+                    or len(decoded) != 32
+                    or decoded == bytes(32)
+                ):
+                    raise ValueError("signing public key must be a 32-byte Ed25519 key")
+                keys[key_id] = public_key
+            return MappingProxyType(keys)
+
+        release_keys = trusted_keys("trusted_release_keys")
+        task_keys = trusted_keys("trusted_task_keys")
+        task_authority_clock_skew_seconds = value[
+            "task_authority_clock_skew_seconds"
+        ]
+        if (
+            isinstance(task_authority_clock_skew_seconds, bool)
+            or not isinstance(task_authority_clock_skew_seconds, int)
+            or not 0 <= task_authority_clock_skew_seconds <= 300
+        ):
+            raise ValueError("task authority clock skew is invalid")
+        version = _text(value["version"], "version", 80)
+        if (
+            not _VERSION.fullmatch(version)
+            or any(int(part) > 2_147_483_647 for part in version.split("."))
+        ):
+            raise ValueError("version is invalid")
+        server_crl_path: str | None = None
+        if "server_crl_path" in value:
+            candidate = Path(
+                _text(value["server_crl_path"], "server_crl_path", 2_048)
+            ).expanduser()
+            if not candidate.is_absolute() or not candidate.is_file():
+                raise ValueError("server_crl_path must be an existing absolute file")
+            server_crl_path = str(candidate.resolve())
+            validate_server_crl(str(material["ca_bundle_path"]), server_crl_path)
+        return cls(
+            queue_path=str(queue_path.resolve()),
+            artifact_root=str(artifact_root.resolve()),
+            gateway_id=_identifier(value["gateway_id"], "gateway_id"),
+            credential=credential,
+            certificate_sha256=certificate,
+            client_certificate_path=str(material["client_certificate_path"]),
+            client_private_key_path=str(material["client_private_key_path"]),
+            ca_bundle_path=str(material["ca_bundle_path"]),
+            generation=generation,
+            environment=_identifier(value["environment"], "environment"),
+            network_zone=_identifier(value["network_zone"], "network_zone"),
+            policy_digest=policy_digest,
+            control_url=control_url.rstrip("/"),
+            proxy_url=proxy_url,
+            allowed_control_hosts=control_hosts,
+            allowed_proxy_hosts=proxy_hosts,
+            trusted_release_keys=release_keys,
+            trusted_task_keys=task_keys,
+            version=version,
+            task_authority_clock_skew_seconds=task_authority_clock_skew_seconds,
+            server_crl_path=server_crl_path,
+            allowed_control_origins=control_origins,
+            allowed_proxy_origins=proxy_origins,
+        )
+
+
+def _verify_crl_signature(crl: x509.CertificateRevocationList, issuer) -> None:
+    public_key = issuer.public_key()
+    try:
+        if isinstance(public_key, rsa.RSAPublicKey):
+            public_key.verify(
+                crl.signature, crl.tbs_certlist_bytes, padding.PKCS1v15(),
+                crl.signature_hash_algorithm,
+            )
+        elif isinstance(public_key, ec.EllipticCurvePublicKey):
+            public_key.verify(
+                crl.signature, crl.tbs_certlist_bytes,
+                ec.ECDSA(crl.signature_hash_algorithm),
+            )
+        elif isinstance(public_key, (ed25519.Ed25519PublicKey, ed448.Ed448PublicKey)):
+            public_key.verify(crl.signature, crl.tbs_certlist_bytes)
+        else:
+            raise ValueError("server CRL issuer key is unsupported")
+    except Exception as exc:
+        raise ValueError("server CRL signature is invalid") from exc
+
+
+def validate_server_crl(ca_bundle_path: str, server_crl_path: str) -> datetime:
+    try:
+        authorities = x509.load_pem_x509_certificates(Path(ca_bundle_path).read_bytes())
+        crl = x509.load_pem_x509_crl(Path(server_crl_path).read_bytes())
+    except (OSError, ValueError) as exc:
+        raise ValueError("server CRL material is invalid") from exc
+    issuer = next((item for item in authorities if item.subject == crl.issuer), None)
+    if issuer is None:
+        raise ValueError("server CRL issuer is not in the CA bundle")
+    _verify_crl_signature(crl, issuer)
+    now = datetime.now(UTC)
+    if (
+        crl.last_update_utc > now
+        or crl.next_update_utc is None
+        or crl.next_update_utc <= now
+    ):
+        raise ValueError("server CRL is not fresh")
+    return crl.next_update_utc
+
+
+def build_server_crl_ssl_context(
+    ca_bundle_path: str, server_crl_path: str
+) -> ssl.SSLContext:
+    validate_server_crl(ca_bundle_path, server_crl_path)
+    context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=ca_bundle_path)
+    context.load_verify_locations(cafile=server_crl_path)
+    context.verify_flags |= ssl.VERIFY_CRL_CHECK_LEAF
+    context.check_hostname = True
+    return context

+ 32 - 0
app/edge_gateway/healthcheck.py

@@ -0,0 +1,32 @@
+"""Bounded container health probe for the edge runtime."""
+
+from __future__ import annotations
+
+import json
+import os
+from datetime import UTC, datetime, timedelta
+from pathlib import Path
+
+
+def main() -> int:
+    path = Path(os.environ.get("EDGE_HEALTH_PATH", "/run/edge/tmp/health.json"))
+    try:
+        value = json.loads(path.read_text(encoding="utf-8"))
+        checked = datetime.fromisoformat(str(value["checked_at"]).replace("Z", "+00:00"))
+        if (
+            set(value) - {
+                "checked_at", "crl_next_update", "gateway_id_digest", "last_cycle",
+                "queue", "status",
+            }
+            or value["status"] not in {"healthy", "degraded"}
+            or checked.tzinfo is None
+            or datetime.now(UTC) - checked.astimezone(UTC) > timedelta(seconds=90)
+        ):
+            return 1
+    except (OSError, ValueError, KeyError, TypeError, json.JSONDecodeError):
+        return 1
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())

+ 276 - 0
app/edge_gateway/runtime.py

@@ -0,0 +1,276 @@
+"""Runnable pull-only edge process with mTLS server-CRL fail-stop semantics."""
+
+from __future__ import annotations
+
+import hashlib
+import json
+import os
+import signal
+import stat
+import threading
+from datetime import UTC, datetime
+from pathlib import Path
+
+import requests
+from requests.adapters import HTTPAdapter
+
+from app.core.edge_gateway.policy import EdgePolicyError
+from app.edge_gateway.agent import EdgeAgent, EdgeRunnerAdapter
+from app.edge_gateway.bootstrap import (
+    EdgeBootstrapConfig,
+    build_server_crl_ssl_context,
+    validate_server_crl,
+)
+from app.edge_gateway.transport import (
+    EdgeAuthenticationStopped,
+    EdgeTransport,
+    EdgeTransportError,
+)
+
+MAX_CONFIG_BYTES = 262_144
+
+
+def _now() -> str:
+    return datetime.now(UTC).isoformat().replace("+00:00", "Z")
+
+
+def _read_regular(path: Path, *, maximum: int, private: bool = False) -> bytes:
+    if not path.is_absolute() or path.is_symlink():
+        raise ValueError("runtime material path is invalid")
+    flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0)
+    if hasattr(os, "O_NOFOLLOW"):
+        flags |= os.O_NOFOLLOW
+    descriptor = os.open(path, flags)
+    try:
+        metadata = os.fstat(descriptor)
+        if (
+            not stat.S_ISREG(metadata.st_mode)
+            or metadata.st_nlink != 1
+            or metadata.st_size > maximum
+            or (private and stat.S_IMODE(metadata.st_mode) & 0o077)
+        ):
+            raise ValueError("runtime material metadata is unsafe")
+        chunks = bytearray()
+        while len(chunks) <= maximum:
+            chunk = os.read(descriptor, min(65_536, maximum + 1 - len(chunks)))
+            if not chunk:
+                break
+            chunks.extend(chunk)
+        if len(chunks) > maximum or os.fstat(descriptor) != metadata:
+            raise ValueError("runtime material changed while reading")
+        return bytes(chunks)
+    finally:
+        os.close(descriptor)
+
+
+def _read_json(path: Path) -> dict[str, object]:
+    try:
+        value = json.loads(_read_regular(path, maximum=MAX_CONFIG_BYTES).decode("utf-8"))
+    except (UnicodeError, json.JSONDecodeError) as exc:
+        raise ValueError("runtime JSON is invalid") from exc
+    if not isinstance(value, dict):
+        raise ValueError("runtime JSON must be an object")
+    return value
+
+
+def _atomic_health(path: Path, value: dict[str, object]) -> None:
+    encoded = json.dumps(value, sort_keys=True, separators=(",", ":")).encode() + b"\n"
+    path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
+    temporary = path.parent / f".{path.name}.{os.getpid()}.tmp"
+    flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_CLOEXEC", 0)
+    if hasattr(os, "O_NOFOLLOW"):
+        flags |= os.O_NOFOLLOW
+    descriptor = os.open(temporary, flags, 0o600)
+    try:
+        os.write(descriptor, encoded)
+        os.fsync(descriptor)
+    finally:
+        os.close(descriptor)
+    os.replace(temporary, path)
+    directory = os.open(path.parent, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
+    try:
+        os.fsync(directory)
+    finally:
+        os.close(directory)
+
+
+class ServerCrlHttpAdapter(HTTPAdapter):
+    def __init__(self, ca_bundle_path: str, server_crl_path: str):
+        self._ssl_context = build_server_crl_ssl_context(
+            ca_bundle_path, server_crl_path
+        )
+        super().__init__()
+
+    def init_poolmanager(self, connections, maxsize, block=False, **pool_kwargs):
+        pool_kwargs["ssl_context"] = self._ssl_context
+        return super().init_poolmanager(connections, maxsize, block, **pool_kwargs)
+
+
+class ServerCrlSession(requests.Session):
+    """Reload the TLS pool whenever the operator atomically replaces the CRL."""
+
+    def __init__(self, ca_bundle_path: str, server_crl_path: str):
+        super().__init__()
+        self.trust_env = False
+        self._ca_bundle_path = ca_bundle_path
+        self._server_crl_path = server_crl_path
+        self._server_crl_digest = ""
+        self.refresh_server_crl()
+
+    @property
+    def server_crl_digest(self) -> str:
+        return self._server_crl_digest
+
+    def refresh_server_crl(self) -> bool:
+        validate_server_crl(self._ca_bundle_path, self._server_crl_path)
+        digest = hashlib.sha256(
+            _read_regular(Path(self._server_crl_path), maximum=1_048_576)
+        ).hexdigest()
+        if digest == self._server_crl_digest:
+            return False
+        replacement = ServerCrlHttpAdapter(
+            self._ca_bundle_path, self._server_crl_path
+        )
+        previous = self.adapters.get("https://")
+        self.mount("https://", replacement)
+        self._server_crl_digest = digest
+        if previous is not None:
+            previous.close()
+        return True
+
+    def request(self, *args, **kwargs):
+        self.refresh_server_crl()
+        return super().request(*args, **kwargs)
+
+
+def _runtime_self_check(node, request, cancel_requested):
+    if (
+        node != "quality.check"
+        or request.get("operation") != "quality"
+        or request.get("purpose") != "quality-evaluation"
+        or request.get("classification") != "evidence"
+    ):
+        raise EdgePolicyError("enterprise source runner handler is not provisioned")
+    if cancel_requested():
+        raise EdgePolicyError("runtime self-check was cancelled")
+    return {
+        "classification": "evidence",
+        "payload": {
+            "check_count": 1,
+            "passed_count": 1,
+            "scope": "edge_runtime",
+            "status": "passed",
+        },
+    }
+
+
+def build_runtime_runner() -> EdgeRunnerAdapter:
+    """Provide one closed engineering self-check; source handlers remain external."""
+
+    return EdgeRunnerAdapter(_runtime_self_check)
+
+
+def build_runtime_from_environment() -> tuple[EdgeAgent, Path, float]:
+    config_path = Path(os.environ.get("EDGE_CONFIG_PATH", ""))
+    credential_path = Path(os.environ.get("EDGE_CREDENTIAL_FILE", ""))
+    task_keys_path = Path(os.environ.get("EDGE_TASK_SIGNING_KEYS_PATH", ""))
+    release_keys_path = Path(os.environ.get("EDGE_RELEASE_SIGNING_KEYS_PATH", ""))
+    server_crl_path = Path(os.environ.get("EDGE_SERVER_CRL_PATH", ""))
+    health_path = Path(os.environ.get("EDGE_HEALTH_PATH", "/run/edge/tmp/health.json"))
+    if any(not path.is_absolute() for path in (
+        config_path, credential_path, task_keys_path, release_keys_path,
+        server_crl_path, health_path,
+    )):
+        raise ValueError("edge runtime paths must be absolute")
+    raw = _read_json(config_path)
+    credential = _read_regular(
+        credential_path, maximum=160, private=True
+    ).decode("utf-8").strip()
+    task_keys = _read_json(task_keys_path)
+    release_keys = _read_json(release_keys_path)
+    raw.update({
+        "credential": credential,
+        "trusted_task_keys": task_keys,
+        "trusted_release_keys": release_keys,
+        "server_crl_path": str(server_crl_path),
+    })
+    config = EdgeBootstrapConfig.from_mapping(raw)
+    if config.server_crl_path is None:
+        raise ValueError("server CRL is required by the runtime")
+    session = ServerCrlSession(config.ca_bundle_path, config.server_crl_path)
+    transport = EdgeTransport(
+        base_url=config.control_url,
+        gateway_id=config.gateway_id,
+        environment=config.environment,
+        network_zone=config.network_zone,
+        generation=config.generation,
+        credential=config.credential,
+        certificate_sha256=config.certificate_sha256,
+        allowed_control_hosts=config.allowed_control_hosts,
+        allowed_proxy_hosts=config.allowed_proxy_hosts,
+        allowed_control_origins=config.allowed_control_origins or None,
+        allowed_proxy_origins=config.allowed_proxy_origins or None,
+        proxy_url=config.proxy_url,
+        client_certificate_path=config.client_certificate_path,
+        client_private_key_path=config.client_private_key_path,
+        ca_bundle_path=config.ca_bundle_path,
+        client=session,
+        server_crl_path=config.server_crl_path,
+    )
+    runner = build_runtime_runner()
+    interval = float(os.environ.get("EDGE_POLL_INTERVAL_SECONDS", "5"))
+    if not 1 <= interval <= 60:
+        raise ValueError("EDGE_POLL_INTERVAL_SECONDS must be between 1 and 60")
+    return EdgeAgent(config, transport, runner), health_path, interval
+
+
+def run() -> int:
+    stop = threading.Event()
+    for event in (signal.SIGINT, signal.SIGTERM):
+        signal.signal(event, lambda _signum, _frame: stop.set())
+    try:
+        agent, health_path, interval = build_runtime_from_environment()
+    except Exception:
+        return 2
+    stopped = False
+    while not stop.is_set():
+        try:
+            if agent.config.server_crl_path is None:  # pragma: no cover - invariant
+                raise EdgeAuthenticationStopped("server CRL is missing")
+            crl_next_update = validate_server_crl(
+                agent.config.ca_bundle_path, agent.config.server_crl_path
+            )
+            agent.heartbeat_once()
+            cycle = agent.run_once()
+            _atomic_health(health_path, {
+                "checked_at": _now(),
+                "crl_next_update": crl_next_update.isoformat().replace("+00:00", "Z"),
+                "gateway_id_digest": agent.gateway_id_digest,
+                "last_cycle": cycle,
+                "queue": agent.safe_diagnostic(),
+                "status": "healthy",
+            })
+        except EdgeAuthenticationStopped:
+            stopped = True
+            _atomic_health(health_path, {
+                "checked_at": _now(), "gateway_id_digest": agent.gateway_id_digest,
+                "status": "stopped",
+            })
+        except (EdgeTransportError, OSError, ValueError):
+            _atomic_health(health_path, {
+                "checked_at": _now(), "gateway_id_digest": agent.gateway_id_digest,
+                "status": "degraded",
+            })
+        if stopped:
+            stop.wait(interval)
+        else:
+            stop.wait(interval)
+    _atomic_health(health_path, {
+        "checked_at": _now(), "gateway_id_digest": agent.gateway_id_digest,
+        "status": "stopped",
+    })
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(run())

+ 427 - 0
app/edge_gateway/transport.py

@@ -0,0 +1,427 @@
+"""Bounded pull-only HTTPS transport for the enterprise edge agent."""
+
+from __future__ import annotations
+
+import hashlib
+import json
+import logging
+import re
+from collections.abc import Mapping
+from urllib.parse import quote, urlsplit
+
+from app.core.edge_gateway.contracts import (
+    EdgeContractError,
+    EdgeEventContract,
+    canonical_sha256,
+    preflight_json,
+    strict_json_bytes,
+)
+from app.core.edge_gateway.policy import EdgeEgressPolicy, EdgePolicyError
+from app.edge_gateway.bootstrap import validate_server_crl
+
+LOGGER = logging.getLogger(__name__)
+
+_IDENTIFIER = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,254}$")
+_SHA256 = re.compile(r"^[0-9a-f]{64}$")
+
+
+class EdgeTransportError(RuntimeError):
+    """A bounded transport failure safe for logs and retry decisions."""
+
+
+class EdgeAuthenticationStopped(EdgeTransportError):
+    """Credential/certificate binding is no longer usable; stop the agent."""
+
+
+class EdgeTransportConflict(EdgeTransportError):
+    """The control plane rejected an idempotent replay with changed content."""
+
+
+class EdgeTransport:
+    MAX_REQUEST_BYTES = 262_144
+    MAX_RESPONSE_BYTES = 1_048_576
+
+    def __init__(
+        self,
+        *,
+        base_url: str,
+        gateway_id: str,
+        environment: str,
+        network_zone: str,
+        generation: int,
+        credential: str,
+        certificate_sha256: str,
+        allowed_control_hosts: set[str] | frozenset[str],
+        allowed_proxy_hosts: set[str] | frozenset[str] | None = None,
+        allowed_control_origins: set[str] | frozenset[str] | None = None,
+        allowed_proxy_origins: set[str] | frozenset[str] | None = None,
+        proxy_url: str | None = None,
+        client_certificate_path: str,
+        client_private_key_path: str,
+        ca_bundle_path: str,
+        client,
+        server_crl_path: str | None = None,
+        connect_timeout_seconds: int = 5,
+        read_timeout_seconds: int = 20,
+    ) -> None:
+        self.policy = EdgeEgressPolicy(
+            allowed_control_hosts=set(allowed_control_hosts),
+            allowed_proxy_hosts=set(allowed_proxy_hosts or set()),
+            allowed_control_origins=allowed_control_origins,
+            allowed_proxy_origins=allowed_proxy_origins,
+        )
+        self.policy.validate_destination(base_url, proxy_url=proxy_url)
+        parsed = urlsplit(base_url)
+        if parsed.query or parsed.fragment or parsed.path not in {"", "/"}:
+            raise EdgePolicyError("control base URL must not contain a path or query")
+        if not isinstance(gateway_id, str) or not _IDENTIFIER.fullmatch(gateway_id):
+            raise ValueError("gateway_id is invalid")
+        if not isinstance(certificate_sha256, str) or not _SHA256.fullmatch(certificate_sha256):
+            raise ValueError("certificate_sha256 is invalid")
+        if (
+            not isinstance(credential, str)
+            or not credential.startswith("dopg_")
+            or len(credential) > 160
+        ):
+            raise ValueError("credential is invalid")
+        if isinstance(generation, bool) or not isinstance(generation, int) or generation < 1:
+            raise ValueError("generation is invalid")
+        for label, timeout in {
+            "connect_timeout_seconds": connect_timeout_seconds,
+            "read_timeout_seconds": read_timeout_seconds,
+        }.items():
+            if isinstance(timeout, bool) or not isinstance(timeout, int) or not 1 <= timeout <= 120:
+                raise ValueError(f"{label} is invalid")
+        if client is None or not callable(getattr(client, "request", None)):
+            raise ValueError("an explicit bounded HTTP client is required")
+        if hasattr(client, "trust_env") and client.trust_env is not False:
+            raise ValueError("HTTP client must disable implicit environment proxies")
+        self.base_url = base_url.rstrip("/")
+        self.proxy_url = proxy_url
+        self.gateway_id = gateway_id
+        self.environment = environment
+        self.network_zone = network_zone
+        self.generation = generation
+        self._credential = credential
+        self.certificate_sha256 = certificate_sha256
+        self.client = client
+        for label, path in {
+            "client_certificate_path": client_certificate_path,
+            "client_private_key_path": client_private_key_path,
+            "ca_bundle_path": ca_bundle_path,
+        }.items():
+            if not isinstance(path, str) or not path.startswith("/") or "\x00" in path:
+                raise ValueError(f"{label} is invalid")
+        self.client_certificate_path = client_certificate_path
+        self._client_private_key_path = client_private_key_path
+        self.ca_bundle_path = ca_bundle_path
+        if server_crl_path is not None and (
+            not isinstance(server_crl_path, str)
+            or not server_crl_path.startswith("/")
+            or "\x00" in server_crl_path
+        ):
+            raise ValueError("server_crl_path is invalid")
+        self.server_crl_path = server_crl_path
+        if server_crl_path is not None:
+            validate_server_crl(ca_bundle_path, server_crl_path)
+        self.timeout = (connect_timeout_seconds, read_timeout_seconds)
+
+    def _binding(self) -> dict[str, object]:
+        return {
+            "environment": self.environment,
+            "network_zone": self.network_zone,
+            "generation": self.generation,
+        }
+
+    def _request(self, path: str, payload: Mapping[str, object]) -> dict[str, object]:
+        if self.server_crl_path is not None:
+            try:
+                validate_server_crl(self.ca_bundle_path, self.server_crl_path)
+            except ValueError as exc:
+                raise EdgeAuthenticationStopped("control-plane server CRL is not fresh") from exc
+        if not path.startswith("/") or ".." in path or "//" in path:
+            raise EdgeTransportError("control path is invalid")
+        url = f"{self.base_url}{path}"
+        self.policy.validate_destination(url, proxy_url=self.proxy_url)
+        try:
+            encoded = strict_json_bytes(payload)
+        except EdgeContractError as exc:
+            raise EdgeTransportError("control request is invalid") from exc
+        if len(encoded) > self.MAX_REQUEST_BYTES:
+            raise EdgeTransportError("control request exceeds the byte limit")
+        headers = {
+            "Accept": "application/json",
+            "Content-Type": "application/json",
+            "X-Edge-Credential": self._credential,
+            "X-Edge-Certificate-SHA256": self.certificate_sha256,
+        }
+        try:
+            response = self.client.request(
+                "POST",
+                url,
+                data=encoded,
+                headers=headers,
+                timeout=self.timeout,
+                allow_redirects=False,
+                proxies=(
+                    {"https": self.proxy_url}
+                    if self.proxy_url is not None
+                    else {"http": None, "https": None}
+                ),
+                verify=self.ca_bundle_path,
+                cert=(self.client_certificate_path, self._client_private_key_path),
+                stream=True,
+            )
+        except Exception as exc:
+            LOGGER.warning(
+                "edge control request failed category=%s", type(exc).__name__
+            )
+            raise EdgeTransportError("control request failed") from exc
+        try:
+            return self._read_response(response)
+        finally:
+            close = getattr(response, "close", None)
+            if callable(close):
+                close()
+
+    def _read_response(self, response) -> dict[str, object]:
+        status = getattr(response, "status_code", None)
+        if not isinstance(status, int):
+            raise EdgeTransportError("control response is invalid")
+        if 300 <= status < 400:
+            raise EdgeTransportError("control redirects are forbidden")
+        headers_value = getattr(response, "headers", {})
+        content_length = headers_value.get("Content-Length") if isinstance(headers_value, Mapping) else None
+        if content_length is not None:
+            try:
+                declared_length = int(content_length)
+            except (TypeError, ValueError) as exc:
+                raise EdgeTransportError("control response content length is invalid") from exc
+            if declared_length < 0 or declared_length > self.MAX_RESPONSE_BYTES:
+                raise EdgeTransportError("control response exceeds the byte limit")
+        iterator = getattr(response, "iter_content", None)
+        if not callable(iterator):
+            raise EdgeTransportError("control response streaming is unavailable")
+        chunks = bytearray()
+        try:
+            for chunk in iterator(chunk_size=65_536):
+                if not isinstance(chunk, bytes):
+                    raise EdgeTransportError("control response chunk is invalid")
+                chunks.extend(chunk)
+                if len(chunks) > self.MAX_RESPONSE_BYTES:
+                    raise EdgeTransportError("control response exceeds the byte limit")
+        except EdgeTransportError:
+            raise
+        except Exception as exc:
+            raise EdgeTransportError("control response stream failed") from exc
+        body = bytes(chunks)
+        if len(body) > self.MAX_RESPONSE_BYTES:
+            raise EdgeTransportError("control response exceeds the byte limit")
+        if status in {401, 403}:
+            raise EdgeAuthenticationStopped("edge credential or certificate binding was rejected")
+        if status == 409:
+            raise EdgeTransportConflict("control plane idempotency conflict")
+        if not 200 <= status < 300:
+            raise EdgeTransportError("control plane rejected the request")
+        try:
+            value = json.loads(body.decode("utf-8"))
+            preflight_json(
+                value,
+                max_depth=32,
+                max_nodes=5_000,
+                max_string_bytes=131_072,
+                max_total_bytes=self.MAX_RESPONSE_BYTES,
+            )
+        except (UnicodeDecodeError, json.JSONDecodeError, EdgeContractError) as exc:
+            raise EdgeTransportError("control response is invalid") from exc
+        if not isinstance(value, dict):
+            raise EdgeTransportError("control response is invalid")
+        if set(value) == {"code", "message", "data"}:
+            if value["code"] != 200:
+                raise EdgeTransportError("control response envelope is invalid")
+            value = value["data"]
+        if not isinstance(value, dict):
+            raise EdgeTransportError("control response data is invalid")
+        return value
+
+    def reconcile(self) -> dict[str, object]:
+        cancelled: list[str] = []
+        releases: list[dict[str, object]] = []
+        release_digests: dict[str, str] = {}
+        cancel_cursor: str | None = None
+        release_cursor: str | None = None
+        cancel_done = False
+        release_done = False
+        for _page in range(100):
+            value = self._request(
+                f"/api/datasource/edge/gateways/{quote(self.gateway_id, safe='')}/reconcile",
+                {
+                    **self._binding(),
+                    "limit": 50,
+                    "cancel_cursor": cancel_cursor,
+                    "release_cursor": release_cursor,
+                },
+            )
+            required = {
+                "cancelled_task_ids", "cancel_next_cursor", "release_offers",
+                "release_next_cursor", "release_baseline",
+            }
+            if set(value) != required:
+                raise EdgeTransportError("reconcile response schema is invalid")
+            page_cancelled = value["cancelled_task_ids"]
+            page_releases = value["release_offers"]
+            if (
+                not isinstance(page_cancelled, list)
+                or not isinstance(page_releases, list)
+                or len(page_cancelled) > 50
+                or len(page_releases) > 50
+            ):
+                raise EdgeTransportError("reconcile page exceeds safe limits")
+            for task_id in page_cancelled:
+                if not isinstance(task_id, str) or not _IDENTIFIER.fullmatch(task_id):
+                    raise EdgeTransportError("cancel reconciliation is invalid")
+                if task_id not in cancelled:
+                    cancelled.append(task_id)
+            baseline = value["release_baseline"]
+            candidates = list(page_releases)
+            if baseline is not None:
+                candidates.append(baseline)
+            for release in candidates:
+                if not isinstance(release, Mapping):
+                    raise EdgeTransportError("release reconciliation is invalid")
+                release_id = release.get("release_id")
+                if not isinstance(release_id, str) or not _IDENTIFIER.fullmatch(release_id):
+                    raise EdgeTransportError("release reconciliation is invalid")
+                digest = canonical_sha256(release)
+                prior = release_digests.get(release_id)
+                if prior is not None and prior != digest:
+                    raise EdgeTransportConflict("release page replay changed content")
+                if prior is None:
+                    release_digests[release_id] = digest
+                    releases.append(dict(release))
+            next_cancel = value["cancel_next_cursor"]
+            next_release = value["release_next_cursor"]
+            if next_cancel is not None and (
+                not isinstance(next_cancel, str) or not _IDENTIFIER.fullmatch(next_cancel)
+            ):
+                raise EdgeTransportError("cancel cursor is invalid")
+            if next_release is not None and (
+                not isinstance(next_release, str) or not _IDENTIFIER.fullmatch(next_release)
+            ):
+                raise EdgeTransportError("release cursor is invalid")
+            cancel_done = next_cancel is None
+            release_done = next_release is None
+            cancel_cursor = next_cancel or (
+                page_cancelled[-1] if page_cancelled else cancel_cursor
+            )
+            release_cursor = next_release or (
+                str(page_releases[-1].get("release_id"))
+                if page_releases and isinstance(page_releases[-1], Mapping)
+                else release_cursor
+            )
+            if len(cancelled) > 5_000 or len(releases) > 5_000:
+                raise EdgeTransportError("reconcile result exceeds safe limits")
+            if cancel_done and release_done:
+                return {
+                    "cancelled_task_ids": cancelled,
+                    "release_offers": releases,
+                }
+        raise EdgeTransportError("reconcile pagination did not converge")
+
+    def cancel_requested(self, task_id: str) -> bool:
+        if not isinstance(task_id, str) or not _IDENTIFIER.fullmatch(task_id):
+            raise EdgeTransportError("cancel task identity is invalid")
+        value = self.reconcile()
+        cancelled = value.get("cancelled_task_ids")
+        if not isinstance(cancelled, list) or len(cancelled) > 100:
+            raise EdgeTransportError("cancel reconciliation is invalid")
+        if any(not isinstance(item, str) or not _IDENTIFIER.fullmatch(item) for item in cancelled):
+            raise EdgeTransportError("cancel reconciliation is invalid")
+        return task_id in cancelled
+
+    def pull_task(self):
+        value = self._request(
+            f"/api/datasource/edge/gateways/{quote(self.gateway_id, safe='')}/tasks/pull",
+            self._binding(),
+        )
+        envelope = value.get("signed_task_envelope")
+        task = value.get("task")
+        if envelope is None and task is None:
+            return None
+        lease_token = value.get("lease_token")
+        lease_expires_at = value.get("lease_expires_at")
+        if (
+            not isinstance(envelope, Mapping)
+            or not isinstance(task, Mapping)
+            or envelope.get("task") != task
+            or not isinstance(lease_token, str)
+            or not isinstance(lease_expires_at, str)
+        ):
+            raise EdgeTransportError("pulled task response is invalid")
+        return dict(envelope), lease_token, lease_expires_at
+
+    def send_event(self, event: Mapping[str, object], lease_token: str) -> dict[str, object]:
+        try:
+            normalized_event = EdgeEventContract.from_mapping(event).to_mapping()
+        except EdgeContractError as exc:
+            raise EdgeTransportError("outbound event contract is invalid") from exc
+        value = self._request(
+            f"/api/datasource/edge/gateways/{quote(self.gateway_id, safe='')}/events",
+            {
+                **self._binding(),
+                "event": normalized_event,
+                "lease_token": lease_token,
+            },
+        )
+        expected_digest = hashlib.sha256(lease_token.encode("utf-8")).hexdigest()
+        if (
+            value.get("status") != "accepted"
+            or value.get("event_id") != normalized_event.get("event_id")
+            or value.get("event_digest") != canonical_sha256(normalized_event)
+            or value.get("remote_lease_digest") != expected_digest
+            or not isinstance(value.get("received_at"), str)
+        ):
+            raise EdgeTransportError("event acknowledgement is invalid")
+        return {
+            "event_id": value["event_id"],
+            "event_digest": value["event_digest"],
+            "remote_lease_digest": value["remote_lease_digest"],
+            "received_at": value["received_at"],
+            "status": "accepted",
+        }
+
+    def task_outcome(self, task_id: str, outcome: str, lease_token: str, summary: Mapping[str, object]):
+        value = self._request(
+            f"/api/datasource/edge/gateways/{quote(self.gateway_id, safe='')}/tasks/{quote(task_id, safe='')}/outcome",
+            {
+                **self._binding(),
+                "outcome": outcome,
+                "lease_token": lease_token,
+                "safe_summary": dict(summary),
+            },
+        )
+        if (
+            set(value) != {"task_id", "status", "replayed"}
+            or value.get("task_id") != task_id
+            or value.get("status") != outcome
+            or not isinstance(value.get("replayed"), bool)
+        ):
+            raise EdgeTransportError("task outcome acknowledgement is invalid")
+        return value
+
+    def heartbeat(self, summary: Mapping[str, object]):
+        return self._request(
+            f"/api/datasource/edge/gateways/{quote(self.gateway_id, safe='')}/heartbeat",
+            {**self._binding(), "version": summary.get("version"), "safe_summary": dict(summary)},
+        )
+
+    def acknowledge_release(self, release_id: str, outcome: str, summary: Mapping[str, object]):
+        return self._request(
+            f"/api/datasource/edge/gateways/{quote(self.gateway_id, safe='')}/releases/ack",
+            {
+                **self._binding(),
+                "release_id": release_id,
+                "outcome": outcome,
+                "safe_summary": dict(summary),
+            },
+        )

+ 20 - 0
app/models/__init__.py

@@ -33,6 +33,17 @@ from app.models.data_research import (
     OntologyVersionModel,
     SourceArtifact,
 )
+from app.models.edge_gateway import (
+    EdgeGateway,
+    EdgeGatewayAudit,
+    EdgeGatewayCredential,
+    EdgeGatewayEnrollment,
+    EdgeGatewayEvent,
+    EdgeGatewayFailureWindow,
+    EdgeGatewayRelease,
+    EdgeGatewayRotationRequest,
+    EdgeGatewayTask,
+)
 from app.models.governance_template import (
     GovernanceDomainTemplate,
     GovernanceDomainTemplateImport,
@@ -65,6 +76,15 @@ __all__ = [
     "ConnectorPrincipal",
     "ConnectorRun",
     "ConnectorSourceBinding",
+    "EdgeGatewayEnrollment",
+    "EdgeGateway",
+    "EdgeGatewayCredential",
+    "EdgeGatewayRotationRequest",
+    "EdgeGatewayTask",
+    "EdgeGatewayEvent",
+    "EdgeGatewayRelease",
+    "EdgeGatewayAudit",
+    "EdgeGatewayFailureWindow",
     "MetadataReviewRecord",
     "MetadataVersionHistory",
     "SemanticAsset",

+ 133 - 0
app/models/edge_gateway.py

@@ -0,0 +1,133 @@
+"""Read models for the enterprise edge-gateway control plane."""
+
+from sqlalchemy.dialects.postgresql import ARRAY, JSONB, UUID
+
+from app import db
+from app.core.common.identifiers import new_governance_uid
+
+
+class EdgeGatewayEnrollment(db.Model):
+    __tablename__ = "edge_gateway_enrollments"
+    __table_args__ = {"schema": "public"}
+    uid = db.Column(UUID(as_uuid=False), primary_key=True, default=new_governance_uid)
+    gateway_id = db.Column(UUID(as_uuid=False), nullable=False, unique=True)
+    gateway_name = db.Column(db.String(200), nullable=False)
+    environment = db.Column(db.String(20), nullable=False)
+    network_zone = db.Column(db.String(120), nullable=False)
+    policy_digest = db.Column(db.String(64), nullable=False)
+    expected_certificate_sha256 = db.Column(db.String(64), nullable=False)
+    allowed_control_hosts = db.Column(ARRAY(db.Text), nullable=False)
+    allowed_proxy_hosts = db.Column(ARRAY(db.Text), nullable=False)
+    token_hash = db.Column(db.String(64), nullable=False, unique=True)
+    status = db.Column(db.String(20), nullable=False)
+    expires_at = db.Column(db.DateTime(timezone=True), nullable=False)
+    consumed_at = db.Column(db.DateTime(timezone=True))
+
+
+class EdgeGateway(db.Model):
+    __tablename__ = "edge_gateways"
+    __table_args__ = {"schema": "public"}
+    uid = db.Column(UUID(as_uuid=False), primary_key=True, default=new_governance_uid)
+    name = db.Column(db.String(200), nullable=False)
+    environment = db.Column(db.String(20), nullable=False)
+    network_zone = db.Column(db.String(120), nullable=False)
+    status = db.Column(db.String(20), nullable=False)
+    current_generation = db.Column(db.Integer, nullable=False)
+    policy_digest = db.Column(db.String(64), nullable=False)
+    allowed_control_hosts = db.Column(ARRAY(db.Text), nullable=False)
+    allowed_proxy_hosts = db.Column(ARRAY(db.Text), nullable=False)
+    last_heartbeat_at = db.Column(db.DateTime(timezone=True))
+    runtime_version = db.Column(db.String(80))
+    safe_health_summary = db.Column(JSONB, nullable=False)
+
+
+class EdgeGatewayCredential(db.Model):
+    __tablename__ = "edge_gateway_credentials"
+    __table_args__ = {"schema": "public"}
+    uid = db.Column(UUID(as_uuid=False), primary_key=True, default=new_governance_uid)
+    gateway_id = db.Column(UUID(as_uuid=False), nullable=False)
+    generation = db.Column(db.Integer, nullable=False)
+    credential_hash = db.Column(db.String(64), nullable=False, unique=True)
+    certificate_sha256 = db.Column(db.String(64), nullable=False)
+    status = db.Column(db.String(20), nullable=False)
+    expires_at = db.Column(db.DateTime(timezone=True), nullable=False)
+
+
+class EdgeGatewayRotationRequest(db.Model):
+    __tablename__ = "edge_gateway_rotation_requests"
+    __table_args__ = {"schema": "public"}
+    gateway_id = db.Column(UUID(as_uuid=False), primary_key=True)
+    request_id = db.Column(db.String(255), primary_key=True)
+    request_digest = db.Column(db.String(64), nullable=False)
+    generation = db.Column(db.Integer, nullable=False)
+
+
+class EdgeGatewayTask(db.Model):
+    __tablename__ = "edge_gateway_tasks"
+    __table_args__ = {"schema": "public"}
+    task_id = db.Column(db.String(255), primary_key=True)
+    gateway_id = db.Column(UUID(as_uuid=False), nullable=False)
+    idempotency_key = db.Column(db.String(255), nullable=False)
+    contract = db.Column(JSONB, nullable=False)
+    contract_digest = db.Column(db.String(64), nullable=False)
+    authority = db.Column(JSONB, nullable=False)
+    status = db.Column(db.String(24), nullable=False)
+    result_summary = db.Column(JSONB)
+
+
+class EdgeGatewayEvent(db.Model):
+    __tablename__ = "edge_gateway_events"
+    __table_args__ = {"schema": "public"}
+    event_id = db.Column(db.String(80), primary_key=True)
+    gateway_id = db.Column(UUID(as_uuid=False), nullable=False)
+    task_id = db.Column(db.String(255), nullable=False)
+    classification = db.Column(db.String(40), nullable=False)
+    contract = db.Column(JSONB, nullable=False)
+    contract_digest = db.Column(db.String(64), nullable=False)
+    ack = db.Column(JSONB, nullable=False)
+
+
+class EdgeGatewayRelease(db.Model):
+    __tablename__ = "edge_gateway_releases"
+    __table_args__ = {"schema": "public"}
+    uid = db.Column(UUID(as_uuid=False), primary_key=True, default=new_governance_uid)
+    gateway_id = db.Column(UUID(as_uuid=False), nullable=False)
+    version = db.Column(db.String(80), nullable=False)
+    artifact_digest = db.Column(db.String(64), nullable=False)
+    rollback_version = db.Column(db.String(80), nullable=False)
+    request_id = db.Column(db.String(255), nullable=False)
+    request_digest = db.Column(db.String(64), nullable=False)
+    status = db.Column(db.String(24), nullable=False)
+    safe_summary = db.Column(JSONB, nullable=False)
+    artifact_name = db.Column(db.String(255), nullable=False)
+    deadline_at = db.Column(db.DateTime(timezone=True), nullable=False)
+    signature_algorithm = db.Column(db.String(20), nullable=False)
+    key_id = db.Column(db.String(128), nullable=False)
+    manifest_digest = db.Column(db.String(64), nullable=False)
+    signature = db.Column(db.String(128), nullable=False)
+    signed_manifest = db.Column(JSONB, nullable=False)
+
+
+class EdgeGatewayAudit(db.Model):
+    __tablename__ = "edge_gateway_audits"
+    __table_args__ = {"schema": "public"}
+    uid = db.Column(UUID(as_uuid=False), primary_key=True, default=new_governance_uid)
+    gateway_id = db.Column(UUID(as_uuid=False))
+    event_type = db.Column(db.String(80), nullable=False)
+    actor_uid = db.Column(UUID(as_uuid=False))
+    success = db.Column(db.Boolean, nullable=False)
+    safe_detail = db.Column(JSONB, nullable=False)
+    failure_fingerprint = db.Column(db.String(64))
+    window_started_at = db.Column(db.DateTime(timezone=True))
+
+
+class EdgeGatewayFailureWindow(db.Model):
+    __tablename__ = "edge_gateway_failure_windows"
+    __table_args__ = {"schema": "public"}
+    failure_fingerprint = db.Column(db.String(64), primary_key=True)
+    window_started_at = db.Column(db.DateTime(timezone=True), primary_key=True)
+    gateway_id = db.Column(UUID(as_uuid=False))
+    event_type = db.Column(db.String(80), nullable=False)
+    occurrence_count = db.Column(db.Integer, nullable=False)
+    first_seen_at = db.Column(db.DateTime(timezone=True), nullable=False)
+    last_seen_at = db.Column(db.DateTime(timezone=True), nullable=False)

+ 51 - 0
app/runner/api.py

@@ -9,6 +9,57 @@ from flask import Flask, jsonify, request
 from app.runner.auth import TaskTokenExpired, TaskTokenInvalid
 from app.runner.nodes import NodeExecutionError
 
+EDGE_RUNNER_NODES = frozenset(
+    {
+        "edge.collect",
+        "edge.profile",
+        "quality.check",
+        "edge.lineage",
+        "edge.controlled_query",
+    }
+)
+EDGE_RUNNER_REQUEST_FIELDS = frozenset(
+    {
+        "task_id",
+        "operation",
+        "purpose",
+        "classification",
+        "environment",
+        "network_zone",
+        "idempotency_key",
+        "deadline_at",
+    }
+)
+
+
+def execute_edge_adapter(handlers, node, edge_request, cancel_requested):
+    """Execute a closed local edge request without opening an HTTP surface.
+
+    Handlers are locally configured callables.  The control plane cannot name a
+    module, command, path, network target, script, or SQL statement through
+    this schema.
+    """
+
+    if node not in EDGE_RUNNER_NODES:
+        raise NodeExecutionError("edge runner node is not approved")
+    if not isinstance(edge_request, dict) or set(edge_request) != EDGE_RUNNER_REQUEST_FIELDS:
+        raise NodeExecutionError("edge runner request is invalid")
+    if not callable(cancel_requested):
+        raise NodeExecutionError("edge cancellation probe is invalid")
+    handler = handlers.get(node) if isinstance(handlers, dict) else None
+    if not callable(handler):
+        raise NodeExecutionError("edge runner node is unavailable")
+    if cancel_requested():
+        from app.edge_gateway.agent import EdgeTaskCancelled
+
+        raise EdgeTaskCancelled("edge task was cancelled")
+    result = handler(dict(edge_request), cancel_requested)
+    if cancel_requested():
+        from app.edge_gateway.agent import EdgeTaskCancelled
+
+        raise EdgeTaskCancelled("edge task was cancelled")
+    return result
+
 
 def create_runner_app(*, verifier, ledger, registry):
     app = Flask("dataops-runner")

+ 15 - 1
app/runner/bootstrap.py

@@ -12,7 +12,7 @@ from app.core.data_source.runtime import (
     DataSourceRuntimeConfig,
     build_standalone_data_source_runtime,
 )
-from app.runner.api import create_runner_app
+from app.runner.api import create_runner_app, execute_edge_adapter
 from app.runner.artifacts import ArtifactStore, PostgresArtifactResolver
 from app.runner.auth import TaskTokenVerifier
 from app.runner.ledger import PostgresTaskLedger
@@ -79,6 +79,20 @@ class RunnerSettings:
     max_query_rows: int = 1000
 
 
+def build_edge_runner_adapter(handlers):
+    """Build the local-only edge adapter around explicitly wired handlers."""
+
+    if not isinstance(handlers, dict) or not handlers:
+        raise ValueError("explicit edge runner handlers are required")
+    from app.edge_gateway.agent import EdgeRunnerAdapter
+
+    return EdgeRunnerAdapter(
+        lambda node, request, cancel: execute_edge_adapter(
+            handlers, node, dict(request), cancel
+        )
+    )
+
+
 def register_artifact_cleanup_cli(
     app, artifact_resolver, evidence_writer=None
 ):

+ 8 - 5
deploy/docker/README.md

@@ -179,17 +179,20 @@ docker compose -f deploy/docker/docker-compose.yml down -v
 
 ## 数据库迁移与事件一致性
 
-后端容器每次启动时都会先执行 `alembic upgrade head`,迁移成功后才启动
-Gunicorn。初始化 SQL 只负责新测试卷的基础建库;后续结构变化统一由 Alembic
-增量迁移管理。
+一次性 `db-role-init` 使用 `DB_ROLE_INIT_DATABASE_URL`(DBA/CREATEROLE)预置
+NOLOGIN 角色、迁移成员关系和非 owner 运行账号;`db-migrate` 仅持有
+`MIGRATION_DATABASE_URL` 并执行 `alembic upgrade head`。完成后两种高权限凭据都
+不会进入常驻容器;只有迁移成功后,使用运行账号的 Backend、Runner 和 MCP 才会
+启动。运行镜像不包含 Alembic 或迁移文件,角色初始化完成后应回收 role-init 凭据。
+安全清理迁移 `20260809_478` 的 downgrade 是非恢复性的,不会重建已删除的旧函数。
 
 ```bash
 # 查看当前版本
-docker compose -f deploy/docker/docker-compose.yml exec -T backend \
+docker compose -f deploy/docker/docker-compose.yml run --rm db-migrate \
   alembic -c alembic.ini current
 
 # 手工升级(正常启动时无需重复执行)
-docker compose -f deploy/docker/docker-compose.yml exec -T backend \
+docker compose -f deploy/docker/docker-compose.yml run --rm db-migrate \
   alembic -c alembic.ini upgrade head
 
 # 查看 Outbox 积压、失败数和最老事件年龄(不返回事件载荷)

+ 12 - 2
deploy/docker/backend.Dockerfile

@@ -1,4 +1,4 @@
-FROM python:3.11-slim
+FROM python:3.11-slim AS dependencies
 
 ENV PYTHONDONTWRITEBYTECODE=1 \
     PYTHONUNBUFFERED=1 \
@@ -20,9 +20,19 @@ RUN pip install --no-cache-dir -r requirements.txt \
     && apt-get autoremove -y \
     && rm -rf /var/lib/apt/lists/*
 
+FROM dependencies AS migrator
+
 COPY app/ ./app/
 COPY migrations/ ./migrations/
 COPY alembic.ini ./alembic.ini
+
+CMD ["alembic", "-c", "alembic.ini", "upgrade", "head"]
+
+FROM dependencies AS runtime
+
+RUN pip uninstall -y alembic
+
+COPY app/ ./app/
 COPY datafactory/ ./datafactory/
 COPY wsgi.py gunicorn_config.py ./
 
@@ -30,4 +40,4 @@ RUN mkdir -p /app/logs /data/upload /data/archive
 
 EXPOSE 5500
 
-CMD ["/bin/sh", "-c", "alembic -c alembic.ini upgrade head && exec gunicorn --config gunicorn_config.py wsgi:application"]
+CMD ["gunicorn", "--config", "gunicorn_config.py", "wsgi:application"]

+ 98 - 6
deploy/docker/docker-compose.yml

@@ -46,6 +46,45 @@ services:
     networks:
       - dataops-test-net
 
+  db-role-init:
+    image: ${DATAOPS_MIGRATOR_IMAGE:-dataops-platform-migrator:local}
+    build:
+      context: ../..
+      dockerfile: deploy/docker/backend.Dockerfile
+      target: migrator
+    environment:
+      DB_ROLE_INIT_DATABASE_URL: postgresql://dataops:dataops-test-password@postgres:5432/dataops
+      MIGRATION_DATABASE_URL: postgresql://dataops:dataops-test-password@postgres:5432/dataops
+      DATABASE_URL: postgresql://dataops_app:${DATAOPS_RUNTIME_PASSWORD:-dataops-app-test-password}@postgres:5432/dataops
+      DATAOPS_MIGRATOR_USER: dataops
+      DATAOPS_RUNTIME_USER: dataops_app
+      DATAOPS_RUNTIME_PASSWORD: ${DATAOPS_RUNTIME_PASSWORD:-dataops-app-test-password}
+    command: ["python", "-m", "app.core.edge_gateway.runtime_roles"]
+    depends_on:
+      postgres:
+        condition: service_healthy
+    restart: "no"
+    networks:
+      - dataops-test-net
+
+  db-migrate:
+    image: ${DATAOPS_MIGRATOR_IMAGE:-dataops-platform-migrator:local}
+    build:
+      context: ../..
+      dockerfile: deploy/docker/backend.Dockerfile
+      target: migrator
+    command: ["alembic", "-c", "alembic.ini", "upgrade", "head"]
+    environment:
+      MIGRATION_DATABASE_URL: postgresql://dataops:dataops-test-password@postgres:5432/dataops
+    depends_on:
+      postgres:
+        condition: service_healthy
+      db-role-init:
+        condition: service_completed_successfully
+    restart: "no"
+    networks:
+      - dataops-test-net
+
   source-postgres:
     image: postgres:16-alpine
     environment:
@@ -247,7 +286,7 @@ services:
       DATAOPS_MCP_BUSINESS_DOMAINS: ${DATAOPS_MCP_BUSINESS_DOMAINS:-}
       DATAOPS_MCP_ENVIRONMENTS: ${DATAOPS_MCP_ENVIRONMENTS:-}
       DATAOPS_MCP_CORRELATION_ID: ${DATAOPS_MCP_CORRELATION_ID:-}
-      DATAOPS_MCP_DATABASE_URL: postgresql://dataops:dataops-test-password@postgres:5432/dataops
+      DATAOPS_MCP_DATABASE_URL: postgresql://dataops_app:${DATAOPS_RUNTIME_PASSWORD:-dataops-app-test-password}@postgres:5432/dataops
       KESTRA_BASE_URL: http://kestra:8080/api/v1
       KESTRA_TENANT_ID: main
       KESTRA_USERNAME: ${KESTRA_USERNAME:-admin@dataops.local}
@@ -262,6 +301,8 @@ services:
     depends_on:
       postgres:
         condition: service_healthy
+      db-migrate:
+        condition: service_completed_successfully
       kestra:
         condition: service_healthy
       runner:
@@ -287,7 +328,7 @@ services:
       DATAOPS_MCP_BUSINESS_DOMAINS: ${DATAOPS_MCP_BUSINESS_DOMAINS:-}
       DATAOPS_MCP_ENVIRONMENTS: ${DATAOPS_MCP_ENVIRONMENTS:-}
       DATAOPS_MCP_CORRELATION_ID: ${DATAOPS_MCP_CORRELATION_ID:-}
-      DATAOPS_MCP_DATABASE_URL: postgresql://dataops:dataops-test-password@postgres:5432/dataops
+      DATAOPS_MCP_DATABASE_URL: postgresql://dataops_app:${DATAOPS_RUNTIME_PASSWORD:-dataops-app-test-password}@postgres:5432/dataops
       DATAOPS_MCP_CONTEXT_MAX_CONCURRENCY: 10
     command:
       - --kind
@@ -297,6 +338,8 @@ services:
     depends_on:
       postgres:
         condition: service_healthy
+      db-migrate:
+        condition: service_completed_successfully
     networks:
       - dataops-test-net
 
@@ -305,6 +348,7 @@ services:
     build:
       context: ../..
       dockerfile: deploy/docker/runner.Dockerfile
+      target: runtime
     environment:
       RUNNER_LISTEN_HOST: 0.0.0.0
       RUNNER_LISTEN_PORT: 5600
@@ -326,7 +370,7 @@ services:
       RUNNER_ARTIFACT_MAX_ROWS: 100000
       RUNNER_ARTIFACT_MEMORY_LIMIT_BYTES: 268435456
       RUNNER_ARTIFACT_TTL_SECONDS: 3600
-      DATABASE_URL: postgresql://dataops:dataops-test-password@postgres:5432/dataops
+      DATABASE_URL: postgresql://dataops_app:${DATAOPS_RUNTIME_PASSWORD:-dataops-app-test-password}@postgres:5432/dataops
       NEO4J_URI: bolt://neo4j:7687
       NEO4J_USER: neo4j
       NEO4J_PASSWORD: Passw0rd
@@ -334,6 +378,7 @@ services:
       DATASOURCE_CREDENTIAL_KEY_VERSION: ${DATASOURCE_CREDENTIAL_KEY_VERSION:-v1}
     ports:
       - "127.0.0.1:${RUNNER_PORT:-15600}:5600"
+    command: ["gunicorn", "--config", "app/runner/gunicorn_config.py", "app.runner.wsgi:application"]
     read_only: true
     tmpfs:
       - /tmp:size=64m,mode=1777
@@ -347,6 +392,8 @@ services:
     depends_on:
       postgres:
         condition: service_healthy
+      db-migrate:
+        condition: service_completed_successfully
       neo4j:
         condition: service_healthy
       minio-init:
@@ -371,12 +418,13 @@ services:
     build:
       context: ../..
       dockerfile: deploy/docker/backend.Dockerfile
+      target: runtime
     environment:
       FLASK_ENV: production
       LISTEN_HOST: 0.0.0.0
       LISTEN_PORT: 5500
       GUNICORN_WORKERS: 2
-      DATABASE_URL: postgresql://dataops:dataops-test-password@postgres:5432/dataops
+      DATABASE_URL: postgresql://dataops_app:${DATAOPS_RUNTIME_PASSWORD:-dataops-app-test-password}@postgres:5432/dataops
       NEO4J_URI: bolt://neo4j:7687
       NEO4J_HTTP_URI: http://neo4j:7474
       NEO4J_USER: neo4j
@@ -400,6 +448,10 @@ services:
       RUNNER_TASK_TOKEN_SECRET: ${RUNNER_TASK_TOKEN_SECRET:-dataops-local-runner-task-token-secret-change-me}
       RUNNER_TASK_TOKEN_TTL_SECONDS: 60
       DATA_FACTORY_ACTIVATION_ENABLED: ${DATA_FACTORY_ACTIVATION_ENABLED:-false}
+      EDGE_MTLS_TRUSTED_PROXY_IPS: 172.31.0.10
+      EDGE_GATEWAY_SIGNING_PRIVATE_KEY_FILE: /run/secrets/edge_gateway_signing_private_key
+      EDGE_GATEWAY_SIGNING_PUBLIC_KEY: ${EDGE_GATEWAY_SIGNING_PUBLIC_KEY:-}
+      EDGE_GATEWAY_SIGNING_KEY_ID: ${EDGE_GATEWAY_SIGNING_KEY_ID:-}
       API_BASE_URL: http://backend:5500/api
       DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY:-}
       LLM_BASE_URL: https://api.deepseek.com
@@ -427,9 +479,13 @@ services:
       - dataops-test-backend-logs:/app/logs
       - dataops-test-upload:/data/upload
       - dataops-test-archive:/data/archive
+    secrets:
+      - edge_gateway_signing_private_key
     depends_on:
       postgres:
         condition: service_healthy
+      db-migrate:
+        condition: service_completed_successfully
       neo4j:
         condition: service_healthy
       minio-init:
@@ -449,7 +505,30 @@ services:
       retries: 20
       start_period: 20s
     networks:
-      - dataops-test-net
+      dataops-test-net:
+      edge-control-net:
+        ipv4_address: 172.31.0.20
+
+  edge-mtls-proxy:
+    image: nginx:1.27-alpine
+    profiles:
+      - enterprise-edge
+    ports:
+      - "${DATAOPS_EDGE_MTLS_PORT:-18443}:8443"
+    volumes:
+      - ./edge-mtls-nginx.conf:/etc/nginx/conf.d/default.conf:ro
+      - ${DATAOPS_EDGE_MTLS_CERT_DIR:-./edge-mtls-certs}:/etc/nginx/edge-mtls:ro
+    read_only: true
+    tmpfs:
+      - /var/cache/nginx:size=16m
+      - /var/run:size=1m
+      - /tmp:size=8m
+    depends_on:
+      backend:
+        condition: service_healthy
+    networks:
+      edge-control-net:
+        ipv4_address: 172.31.0.10
 
   lightrag-neo4j:
     image: neo4j:5.26-community
@@ -523,11 +602,12 @@ services:
     build:
       context: ../..
       dockerfile: deploy/docker/backend.Dockerfile
+      target: runtime
     profiles:
       - knowledge-shadow
     command: ["python", "-m", "app.core.knowledge.lightrag.worker"]
     environment:
-      DATABASE_URL: postgresql://dataops:dataops-test-password@postgres:5432/dataops
+      DATABASE_URL: postgresql://dataops_app:${DATAOPS_RUNTIME_PASSWORD:-dataops-app-test-password}@postgres:5432/dataops
       KNOWLEDGE_LIGHTRAG_BASE_URL: http://lightrag:9621
       KNOWLEDGE_LIGHTRAG_API_KEY: ${LIGHTRAG_API_KEY:-local-shadow-only-key}
       KNOWLEDGE_LIGHTRAG_TIMEOUT_SECONDS: 15
@@ -535,6 +615,8 @@ services:
     depends_on:
       postgres:
         condition: service_healthy
+      db-migrate:
+        condition: service_completed_successfully
       lightrag:
         condition: service_healthy
     restart: unless-stopped
@@ -567,6 +649,16 @@ services:
 networks:
   dataops-test-net:
     name: ${DATAOPS_NETWORK_NAME:-dataops-test-net}
+  edge-control-net:
+    name: ${DATAOPS_EDGE_NETWORK_NAME:-dataops-edge-control-net}
+    internal: true
+    ipam:
+      config:
+        - subnet: 172.31.0.0/24
+
+secrets:
+  edge_gateway_signing_private_key:
+    file: ${EDGE_GATEWAY_SIGNING_PRIVATE_KEY_FILE:-./edge-secrets/edge-gateway-signing.key}
 
 volumes:
   dataops-test-postgres:

+ 33 - 0
deploy/docker/edge-mtls-nginx.conf

@@ -0,0 +1,33 @@
+server {
+    listen 8443 ssl;
+    server_name _;
+
+    ssl_protocols TLSv1.2 TLSv1.3;
+    ssl_certificate /etc/nginx/edge-mtls/server.crt;
+    ssl_certificate_key /etc/nginx/edge-mtls/server.key;
+    ssl_client_certificate /etc/nginx/edge-mtls/client-ca.pem;
+    ssl_crl /etc/nginx/edge-mtls/client-ca.crl;
+    ssl_verify_client on;
+    ssl_verify_depth 3;
+
+    client_max_body_size 256k;
+
+    location /api/datasource/edge/ {
+        proxy_pass http://backend:5500;
+        proxy_http_version 1.1;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto https;
+        # These assignments overwrite any client-supplied names with TLS state.
+        proxy_set_header X-DataOps-Edge-Client-Cert $ssl_client_escaped_cert;
+        proxy_set_header X-DataOps-Edge-Client-Verify $ssl_client_verify;
+        proxy_connect_timeout 5s;
+        proxy_send_timeout 30s;
+        proxy_read_timeout 30s;
+    }
+
+    location / {
+        return 404;
+    }
+}

+ 3 - 4
deploy/docker/runner.Dockerfile

@@ -1,4 +1,4 @@
-FROM python:3.11-slim
+FROM python:3.11-slim AS runtime
 
 ENV PYTHONDONTWRITEBYTECODE=1 \
     PYTHONUNBUFFERED=1 \
@@ -16,14 +16,13 @@ RUN apt-get update \
 
 COPY requirements.txt ./requirements.txt
 RUN pip install --no-cache-dir -r requirements.txt \
+    && pip uninstall -y alembic \
     && apt-get purge -y gcc libc6-dev linux-libc-dev \
     && apt-get autoremove -y \
     && rm -rf /var/lib/apt/lists/*
 
 COPY app/ ./app/
-COPY migrations/ ./migrations/
-COPY alembic.ini ./alembic.ini
 
 EXPOSE 5600
 
-CMD ["/bin/sh", "-c", "alembic -c alembic.ini upgrade head && exec gunicorn --config app/runner/gunicorn_config.py app.runner.wsgi:application"]
+CMD ["gunicorn", "--config", "app/runner/gunicorn_config.py", "app.runner.wsgi:application"]

+ 20 - 0
deploy/edge/Dockerfile.edge

@@ -0,0 +1,20 @@
+FROM python:3.11-slim
+
+ENV PYTHONDONTWRITEBYTECODE=1 \
+    PYTHONUNBUFFERED=1
+
+WORKDIR /app
+
+RUN pip install --no-cache-dir \
+    cryptography==43.0.3 \
+    Flask==2.3.3 \
+    Flask-CORS==4.0.0 \
+    Flask-SQLAlchemy==3.1.1 \
+    python-dotenv==1.0.0 \
+    requests==2.31.0
+
+COPY app/ ./app/
+
+USER 65532:65532
+
+CMD ["python", "-m", "app.edge_gateway"]

+ 44 - 0
deploy/edge/README.md

@@ -0,0 +1,44 @@
+# Enterprise edge deployment contract
+
+This directory is an engineering deployment contract, not an enterprise UAT
+record. Its highest state is
+`ENGINEERING_COMPLETE_ENTERPRISE_EDGE_UAT_BLOCKED`; enterprise source,
+network, and security/legal evidence remain `TBD_EXTERNAL`.
+
+The container initiates pull-only HTTPS/mTLS traffic. It exposes no inbound
+control port. The network team must restrict egress to the exact control and
+approved proxy origins in `edge.env`; DNS wildcard, direct HTTP, arbitrary
+proxy, and arbitrary command execution are forbidden. Compose hardens the
+runtime with a non-root UID, read-only root filesystem, all capabilities
+dropped, no-new-privileges, bounded CPU/memory/PIDs/logs, and minimal mounts.
+
+Private keys, control credentials, CA/CRL material, and Ed25519 task/release
+public-key sets are mounted from operator-owned directories. Run `init` with
+the approved numeric runtime UID/GID and set the same `EDGE_RUNTIME_UID/GID`
+in the environment; Compose reuses those values for the process and writable
+tmpfs. The client key is always `secrets/client.key`, with the complete secrets
+directory mounted read-only at `/run/edge/secrets`. These materials are not
+embedded in Compose or environment examples. Certificate and signing mounts
+are read-only. Queue, local artifacts, and verified releases are the only
+writable persistent mounts. 原始数据和近期明细 remain local; only the approved
+数据分级 summaries leave the edge under 默认拒绝 policy.
+
+## Local render check
+
+Set temporary absolute directories and a syntactically valid immutable image,
+then run `docker compose --env-file <operator-env> -f docker-compose.edge.yml
+config`. This validates interpolation only. It does not start a real enterprise
+source or prove firewall behavior.
+
+Installation, CSR, 证书轮换, 证书撤销, v1-to-v2 migration, 断网/重连, 取消,
+诊断, 备份/恢复, upgrade and 回滚 steps are in the
+[edge operations runbook](../../docs/runbooks/EDGE_GATEWAY_OPERATIONS.md).
+In the release archive the same file is at
+`dataops-platform/docs/runbooks/EDGE_GATEWAY_OPERATIONS.md`. CRL
+distribution and firewall approval are enterprise responsibilities.
+
+`release-check` copies a fully verified manifest and artifact into the
+fsync-backed `releases/verified/<manifest_digest>/` bundle. `rollback`
+revalidates both current and target Ed25519 manifests and artifact hashes, then
+atomically commits `releases/current.json`; an intent/history record supports
+deterministic recovery if the process stops during the directory-fsync window.

+ 71 - 0
deploy/edge/docker-compose.edge.yml

@@ -0,0 +1,71 @@
+name: dataops-enterprise-edge
+
+services:
+  edge-gateway:
+    build:
+      context: ../..
+      dockerfile: deploy/edge/Dockerfile.edge
+    image: "${EDGE_IMAGE:-dataops-edge:local}"
+    pull_policy: "${EDGE_PULL_POLICY:-build}"
+    command: ["python", "-m", "app.edge_gateway"]
+    user: "${EDGE_RUNTIME_UID:?}:${EDGE_RUNTIME_GID:?}"
+    read_only: true
+    cap_drop:
+      - ALL
+    security_opt:
+      - no-new-privileges:true
+    pids_limit: 128
+    mem_limit: 1g
+    cpus: "1.0"
+    restart: unless-stopped
+    init: true
+    stop_grace_period: 30s
+    environment:
+      EDGE_CONFIG_PATH: "${EDGE_CONFIG_PATH:-/run/edge/config/edge.json}"
+      EDGE_CREDENTIAL_FILE: /run/edge/secrets/control-credential
+      EDGE_CLIENT_CERTIFICATE_PATH: /run/edge/certificates/client.pem
+      EDGE_CLIENT_PRIVATE_KEY_PATH: "${EDGE_CLIENT_PRIVATE_KEY_PATH:-/run/edge/secrets/client.key}"
+      EDGE_CA_BUNDLE_PATH: /run/edge/certificates/ca.pem
+      EDGE_SERVER_CRL_PATH: "${EDGE_SERVER_CRL_PATH:-/run/edge/certificates/control-plane-server.crl.pem}"
+      EDGE_HEALTH_PATH: "${EDGE_HEALTH_PATH:-/run/edge/tmp/health.json}"
+      EDGE_RELEASE_SIGNING_KEYS_PATH: /run/edge/signing/release-public-keys.json
+      EDGE_TASK_SIGNING_KEYS_PATH: /run/edge/signing/task-public-keys.json
+      EDGE_CONTROL_ORIGIN: "${EDGE_CONTROL_ORIGIN:?exact https control origin required}"
+      EDGE_PROXY_ORIGIN: "${EDGE_PROXY_ORIGIN:-}"
+      EDGE_ALLOWED_CONTROL_HOSTS: "${EDGE_ALLOWED_CONTROL_HOSTS:?exact control hosts required}"
+      EDGE_ALLOWED_PROXY_HOSTS: "${EDGE_ALLOWED_PROXY_HOSTS:-}"
+      EDGE_LOG_LEVEL: "${EDGE_LOG_LEVEL:-INFO}"
+      EDGE_LOG_MAX_BYTES: "${EDGE_LOG_MAX_BYTES:-10485760}"
+      EDGE_LOG_BACKUP_COUNT: "${EDGE_LOG_BACKUP_COUNT:-3}"
+      EDGE_POLL_INTERVAL_SECONDS: "${EDGE_POLL_INTERVAL_SECONDS:-5}"
+    volumes:
+      - "${EDGE_CONFIG_DIR:?}:/run/edge/config:ro"
+      - "${EDGE_QUEUE_DIR:?}:/var/lib/dataops-edge/queue:rw"
+      - "${EDGE_ARTIFACT_DIR:?}:/var/lib/dataops-edge/artifacts:rw"
+      - "${EDGE_RELEASE_DIR:?}:/var/lib/dataops-edge/releases:rw"
+      - "${EDGE_SECRET_DIR:?}:/run/edge/secrets:ro"
+      - "${EDGE_CERTIFICATE_DIR:?}:/run/edge/certificates:ro"
+      - "${EDGE_SIGNING_KEY_DIR:?}:/run/edge/signing:ro"
+    tmpfs:
+      - /tmp:rw,noexec,nosuid,nodev,size=64m,mode=1777
+      - /run/edge/tmp:rw,noexec,nosuid,nodev,size=16m,mode=0700,uid=${EDGE_RUNTIME_UID:?},gid=${EDGE_RUNTIME_GID:?}
+    healthcheck:
+      test: ["CMD", "python", "-m", "app.edge_gateway.healthcheck"]
+      interval: 30s
+      timeout: 5s
+      retries: 3
+      start_period: 20s
+    logging:
+      driver: json-file
+      options:
+        max-size: "10m"
+        max-file: "3"
+    networks:
+      - edge-egress
+
+networks:
+  edge-egress:
+    driver: bridge
+    internal: false
+    labels:
+      dataops.network-contract: "outbound-only; enforce exact egress destinations outside Compose"

+ 21 - 0
deploy/edge/edge.config.example.json

@@ -0,0 +1,21 @@
+{
+  "allowed_control_hosts": ["control.example.invalid"],
+  "allowed_control_origins": ["https://control.example.invalid"],
+  "allowed_proxy_hosts": [],
+  "allowed_proxy_origins": [],
+  "artifact_root": "/var/lib/dataops-edge/artifacts",
+  "ca_bundle_path": "/run/edge/certificates/ca.pem",
+  "certificate_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+  "client_certificate_path": "/run/edge/certificates/client.pem",
+  "client_private_key_path": "/run/edge/secrets/client.key",
+  "control_url": "https://control.example.invalid",
+  "environment": "production",
+  "gateway_id": "11111111-1111-4111-8111-111111111111",
+  "generation": 1,
+  "network_zone": "zone-a",
+  "policy_digest": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+  "proxy_url": null,
+  "queue_path": "/var/lib/dataops-edge/queue/edge-queue-v2.sqlite3",
+  "task_authority_clock_skew_seconds": 30,
+  "version": "1.0.0"
+}

+ 78 - 0
deploy/edge/edge.config.schema.json

@@ -0,0 +1,78 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "title": "DataOps Edge Runtime File Configuration",
+  "type": "object",
+  "additionalProperties": false,
+  "required": [
+    "allowed_control_hosts",
+    "allowed_control_origins",
+    "allowed_proxy_hosts",
+    "allowed_proxy_origins",
+    "artifact_root",
+    "ca_bundle_path",
+    "certificate_sha256",
+    "client_certificate_path",
+    "client_private_key_path",
+    "control_url",
+    "environment",
+    "gateway_id",
+    "generation",
+    "network_zone",
+    "policy_digest",
+    "proxy_url",
+    "queue_path",
+    "task_authority_clock_skew_seconds",
+    "version"
+  ],
+  "properties": {
+    "allowed_control_hosts": {
+      "type": "array",
+      "minItems": 1,
+      "maxItems": 32,
+      "uniqueItems": true,
+      "items": {"type": "string", "pattern": "^[a-z0-9][a-z0-9.-]*[a-z0-9]$"}
+    },
+    "allowed_control_origins": {
+      "type": "array",
+      "minItems": 1,
+      "maxItems": 32,
+      "uniqueItems": true,
+      "items": {"type": "string", "pattern": "^https://[^/]+$", "maxLength": 2048}
+    },
+    "allowed_proxy_hosts": {
+      "type": "array",
+      "maxItems": 32,
+      "uniqueItems": true,
+      "items": {"type": "string", "pattern": "^[a-z0-9][a-z0-9.-]*[a-z0-9]$"}
+    },
+    "allowed_proxy_origins": {
+      "type": "array",
+      "maxItems": 32,
+      "uniqueItems": true,
+      "items": {"type": "string", "pattern": "^https://[^/]+$", "maxLength": 2048}
+    },
+    "artifact_root": {"type": "string", "pattern": "^/", "maxLength": 2048},
+    "ca_bundle_path": {"type": "string", "pattern": "^/", "maxLength": 2048},
+    "certificate_sha256": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
+    "client_certificate_path": {"type": "string", "pattern": "^/", "maxLength": 2048},
+    "client_private_key_path": {"type": "string", "pattern": "^/", "maxLength": 2048},
+    "control_url": {"type": "string", "pattern": "^https://[^/]+$", "maxLength": 2048},
+    "environment": {"type": "string", "enum": ["development", "staging", "production"]},
+    "gateway_id": {"type": "string", "format": "uuid"},
+    "generation": {"type": "integer", "minimum": 1, "maximum": 2147483647},
+    "network_zone": {"type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,254}$"},
+    "policy_digest": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
+    "proxy_url": {
+      "oneOf": [
+        {"type": "null"},
+        {"type": "string", "pattern": "^https://[^/]+$", "maxLength": 2048}
+      ]
+    },
+    "queue_path": {"type": "string", "pattern": "^/", "maxLength": 2048},
+    "task_authority_clock_skew_seconds": {"type": "integer", "minimum": 0, "maximum": 300},
+    "version": {
+      "type": "string",
+      "pattern": "^(0|[1-9][0-9]{0,9})\\.(0|[1-9][0-9]{0,9})\\.(0|[1-9][0-9]{0,9})$"
+    }
+  }
+}

+ 29 - 0
deploy/edge/edge.env.example

@@ -0,0 +1,29 @@
+# Copy to an operator-controlled 0600 file outside source control.
+# Default local engineering build. Enterprise delivery may set an approved
+# immutable digest and EDGE_PULL_POLICY=always after artifact verification.
+EDGE_IMAGE=dataops-edge:local
+EDGE_PULL_POLICY=build
+EDGE_CONFIG_DIR=/opt/dataops-edge/config
+EDGE_QUEUE_DIR=/opt/dataops-edge/queue
+EDGE_ARTIFACT_DIR=/opt/dataops-edge/artifacts
+EDGE_RELEASE_DIR=/opt/dataops-edge/releases
+EDGE_SECRET_DIR=/opt/dataops-edge/secrets
+EDGE_CERTIFICATE_DIR=/opt/dataops-edge/certificates
+EDGE_SIGNING_KEY_DIR=/opt/dataops-edge/signing
+EDGE_RUNTIME_UID=65532
+EDGE_RUNTIME_GID=65532
+
+# Exact origins and host allowlists; wildcard and HTTP values are rejected by runtime policy.
+EDGE_CONTROL_ORIGIN=https://control.example.invalid
+EDGE_ALLOWED_CONTROL_HOSTS=control.example.invalid
+EDGE_PROXY_ORIGIN=
+EDGE_ALLOWED_PROXY_HOSTS=
+
+# Paths are inside the container. Credential bytes are mounted, never placed here.
+EDGE_CLIENT_PRIVATE_KEY_PATH=/run/edge/secrets/client.key
+EDGE_SERVER_CRL_PATH=/run/edge/certificates/control-plane-server.crl.pem
+EDGE_HEALTH_PATH=/run/edge/tmp/health.json
+EDGE_POLL_INTERVAL_SECONDS=5
+EDGE_LOG_LEVEL=INFO
+EDGE_LOG_MAX_BYTES=10485760
+EDGE_LOG_BACKUP_COUNT=3

+ 17 - 2
deployment/CHECKLIST.md

@@ -5,7 +5,9 @@
 - [ ] 服务器为 Ubuntu 20.04/22.04,可 sudo
 - [ ] Python 3.8+ 已安装(`python3 --version`)
 - [ ] Supervisor 已安装并运行(`systemctl status supervisor`)
-- [ ] PostgreSQL 可连接(`DATABASE_URL` 测试通过)
+- [ ] PostgreSQL 三身份分离:role-init(DBA/CREATEROLE)、migrator(无 CREATEROLE)、runtime(非 owner)
+- [ ] `DB_ROLE_INIT_DATABASE_URL` 仅在一次性角色初始化期间可用,完成后已回收
+- [ ] `MIGRATION_DATABASE_URL` 与 runtime `DATABASE_URL` 分别测试通过
 - [ ] Neo4j 可连接(Bolt 7687 / HTTP 7474)
 - [ ] MinIO 可访问,bucket `dataops-bucket` 已创建
 - [ ] MinIO access key / secret key 与 `dataops.env` 一致
@@ -22,7 +24,7 @@
   - [ ] `SECRET_KEY` 已改为长随机字符串
   - [ ] `DEEPSEEK_API_KEY` 已填入真实密钥
   - [ ] `N8N_API_KEY` 已填入真实密钥
-  - [ ] `DATABASE_URL` / `NEO4J_*` / `MINIO_*` 与生产环境一致
+  - [ ] `DB_ROLE_INIT_DATABASE_URL` / `MIGRATION_DATABASE_URL` / runtime `DATABASE_URL` / `NEO4J_*` / `MINIO_*` 与生产环境一致且无占位值
   - [ ] `LLM_BASE_URL=https://api.deepseek.com`(无 `/v1` 后缀)
   - [ ] `LLM_MODEL_NAME=deepseek-v4-pro`(或实际使用模型)
   - [ ] `LOG_FILE` 为绝对路径
@@ -57,6 +59,19 @@
 - [ ] 未将 API Key 提交到 Git
 - [ ] 防火墙仅开放必要端口(18183 / 22 等)
 
+## P3-WP04 企业边缘门禁
+
+- [ ] `scripts/edge_gateway_admin.py init/csr` 使用批准绝对路径和数值 runtime UID/GID,根目录 `0700`、`secrets/client.key` 为 `0600`,日志/工单没有私钥或 credential
+- [ ] `init` 的 UID/GID、`edge.env` 的 `EDGE_RUNTIME_UID/GID` 和 Compose `user` 完全一致;真实容器进程可读只读挂载的 client key
+- [ ] 边缘 Compose 无 `ports`/`expose`,non-root、read-only、cap-drop ALL、no-new-privileges 和资源/日志上限已复核
+- [ ] Edge 镜像从归档内 `edge/Dockerfile.edge` 构建或按已验签 immutable digest 拉取,pull-only runtime 在 120 秒内 healthy,断网仅 degraded、SIGTERM 干净退出
+- [ ] credential 与整个 secrets 目录只读挂载;证书/CA/CRL/task+release Ed25519 公钥只读挂载,env/Compose 不含 secret
+- [ ] 控制 origin、可选 proxy origin 和 host allowlist 精确一致;企业防火墙、DNS、代理和抓包由网络负责人批准
+- [ ] PostgreSQL migration current/head 均为 `20260809_479`,runtime/migrator/evidence owner role 最小权限已验证
+- [ ] 注册固定材料、证书轮换、canonical Ed25519 撤销状态、断网/重连、取消、签名升级、verified bundle 回滚与 fsync 崩溃恢复、queue 备份恢复已按归档内运行手册演练
+- [ ] 包内 `SHA256SUMS` 全通过,OpenAPI、Failure Injection、edge config schema/示例完整;admin `status`、queue v2、retention/legal hold/delete receipt、DB 三身份和 client/server CRL 步骤已演练
+- [ ] `enterprise_source_1`、`enterprise_source_2`、`network`、`security_legal` 有企业证据;否则保持 `ENGINEERING_COMPLETE_ENTERPRISE_EDGE_UAT_BLOCKED`
+
 ## 回滚准备
 
 - [ ] 已保存上一版离线镜像包和 SHA-256

+ 3 - 1
deployment/DEPLOYMENT_GUIDE.md

@@ -98,7 +98,9 @@ sudo chmod 640 /etc/dataops-platform/dataops.env
 | `SECRET_KEY` | Flask 密钥,长随机字符串 |
 | `DEEPSEEK_API_KEY` | DeepSeek API 密钥 |
 | `N8N_API_KEY` | n8n API 密钥 |
-| `DATABASE_URL` | PostgreSQL 连接串 |
+| `DB_ROLE_INIT_DATABASE_URL` | 仅一次性角色预置使用的 DBA/CREATEROLE 连接串;完成后回收 |
+| `MIGRATION_DATABASE_URL` | 仅 schema migration 使用;无需 CREATEROLE,但须为 evidence owner 成员 |
+| `DATABASE_URL` | Gunicorn 常驻进程使用的非 owner、非超级用户 runtime 连接串 |
 | `NEO4J_PASSWORD` | Neo4j 密码 |
 | `MINIO_*` | MinIO 四项须匹配同一实例 |
 

+ 11 - 0
deployment/MANIFEST.md

@@ -39,6 +39,7 @@
 | `scripts/stop_dataops.sh` | 停止服务 |
 | `scripts/restart_dataops.sh` | 重启服务 |
 | `scripts/deploy_dataops.sh` | 已安装环境下的增量部署(可选) |
+| `scripts/edge_gateway_admin.py` | P3-WP04 边缘本地 init/CSR/注册材料/轮换/撤销状态/签名发布/回滚工具;私钥不出边缘 |
 
 ## 配置模板
 
@@ -69,6 +70,9 @@
 | `compose/.env.rehearsal.example` | 隔离恢复演练的端口和网络模板 |
 | `environments/` | 开发、测试和预生产隔离配置及差异检查输入 |
 | `compose/README.md` | 离线交付、备份、恢复、回滚和企业门禁 |
+| `edge/` | P3-WP04 pull-only 边缘 Compose、无秘密 env 示例和工程部署契约 |
+| `edge/Dockerfile.edge` | P3-WP04 可运行的 pull-only Edge 镜像入口,无入站监听 |
+| `edge/edge.config.schema.json` | Edge 运行文件的闭合 JSON Schema;credential/signing keys/CRL 由只读文件注入 |
 
 ## 文档
 
@@ -78,6 +82,12 @@
 | `DEPLOYMENT_GUIDE.md` | 完整部署手册 |
 | `CHECKLIST.md` | 部署检查清单 |
 | `MANIFEST.md` | 本文件 |
+| `docs/runbooks/EDGE_GATEWAY_OPERATIONS.md` | P3-WP04 安装、CSR、注册、轮换、撤销、升级、verified bundle 回滚与恢复步骤 |
+| `docs/phase3/P3_WP04_EDGE_GATEWAY.md` | P3-WP04 架构、数据边界和企业 UAT 边界 |
+| `docs/validation/P3_WP04_EDGE_GATEWAY_EVIDENCE.md` | P3-WP04 本地定向验证证据与剩余门禁 |
+| `docs/validation/P3_WP04_FAILURE_INJECTION.json` | 机器可读故障注入台账;企业项保持 `TBD_EXTERNAL` |
+| `docs/architecture/OPENAPI.yaml` | 完整 API 合同,含 Edge human bearer 与 machine mTLS 组合 |
+| `SHA256SUMS` | 发布归档内全部文件的 SHA-256 闭包(打包时生成) |
 
 ## 部署目标路径
 
@@ -97,6 +107,7 @@
 - 备份恢复默认失败关闭、禁止同项目原位恢复、禁止数据库 downgrade
 - 开发/测试/预生产隔离配置、六类就绪监控、受控晋级和客户迁移 dry-run
 - 固定构建时间的可复现离线归档,以及 OpenAPI、SBOM、迁移和源码互相追溯
+- P3-WP04 发布包包含与源码精确一致的边缘 admin/status 工具、可运行 pull-only Edge 镜像、闭合配置 schema、完整 OpenAPI、Failure Injection、运行手册、架构边界和验证证据;包内 `SHA256SUMS` 覆盖全部文件且打包前执行秘密形态扫描;工具与 Compose 统一 runtime UID/GID 和 `secrets/client.key` 路径,企业来源、网络和安全法务 UAT 仍为外部门禁
 
 ## 历史重要变更(2026-05)
 

+ 2 - 1
deployment/README.md

@@ -70,7 +70,8 @@ sudo bash deploy_dataops.sh
 ```bash
 sudo vim /etc/dataops-platform/dataops.env
 # 必改: SECRET_KEY, RULE_GENERATION_RECEIPT_SECRET, DEEPSEEK_API_KEY, N8N_API_KEY
-# 核对: DATABASE_URL, NEO4J_*, MINIO_*, API_BASE_URL
+# 核对: DB_ROLE_INIT_DATABASE_URL, MIGRATION_DATABASE_URL, DATABASE_URL,
+#       DATAOPS_MIGRATOR_USER, DATAOPS_RUNTIME_USER, NEO4J_*, MINIO_*, API_BASE_URL
 
 sudo bash deploy_dataops.sh
 ```

+ 7 - 2
deployment/app/__init__.py

@@ -12,6 +12,7 @@ from app.config.config import (
     current_env,
     log_llm_env_status,
     log_service_env_status,
+    validate_production_database_identity,
 )
 from app.config.cors import CORS_OPTIONS
 
@@ -30,6 +31,8 @@ def create_app():
     # 配置CORS以解决跨域问题
     CORS(app, **CORS_OPTIONS)
     db.init_app(app)
+    with app.app_context():
+        validate_production_database_identity(app, db.engine)
 
     # 注册蓝图
     from app.api.business_domain import bp as business_domain_bp
@@ -139,7 +142,8 @@ def configure_response_headers(app):
                 headers = (
                     "Content-Type, Authorization, X-Requested-With, "
                     "Accept, Origin, Cache-Control, X-File-Name, If-Match, "
-                    "X-Agent-Credential"
+                    "X-Agent-Credential, X-Edge-Enrollment, X-Edge-Credential, "
+                    "X-Edge-Certificate-SHA256"
                 )
                 response.headers["Access-Control-Allow-Headers"] = headers
                 response.headers["Access-Control-Max-Age"] = "86400"
@@ -161,7 +165,8 @@ def configure_response_headers(app):
             if "Access-Control-Allow-Headers" not in response.headers:
                 headers = (
                     "Content-Type, Authorization, X-Requested-With, Accept, Origin, "
-                    "If-Match, X-Agent-Credential"
+                    "If-Match, X-Agent-Credential, X-Edge-Enrollment, "
+                    "X-Edge-Credential, X-Edge-Certificate-SHA256"
                 )
                 response.headers["Access-Control-Allow-Headers"] = headers
 

+ 3 - 0
deployment/app/api/data_source/__init__.py

@@ -1,5 +1,8 @@
+# ruff: noqa: E402, F401, I001
+
 from flask import Blueprint
 
 bp = Blueprint("data_source", __name__)
 
+from app.api.data_source import edge_routes  # noqa: E402, F401
 from app.api.data_source import routes  # noqa: E402, F401

+ 299 - 0
deployment/app/api/data_source/edge_routes.py

@@ -0,0 +1,299 @@
+"""Bounded HTTP boundary for human and machine edge-gateway operations."""
+
+from __future__ import annotations
+
+import hmac
+import ipaddress
+import logging
+import re
+from datetime import UTC, datetime
+from urllib.parse import unquote
+
+from cryptography import x509
+from cryptography.hazmat.primitives import hashes
+from flask import current_app, g, jsonify, request
+
+from app.api.data_source import bp
+from app.core.edge_gateway.contracts import EdgeContractError, preflight_json
+from app.core.edge_gateway.service import (
+    EdgeGatewayAuthenticationError,
+    EdgeGatewayError,
+    EdgeGatewayPayloadTooLargeError,
+    EdgeGatewayService,
+    EdgeGatewayValidationError,
+)
+from app.models.result import failed, success
+
+logger = logging.getLogger(__name__)
+MAX_EDGE_REQUEST_BYTES = 262_144
+MAX_EDGE_REQUEST_NODES = 5_000
+MAX_EDGE_RESPONSE_BYTES = 1_048_576
+
+
+def _service() -> EdgeGatewayService:
+    from app import db
+    from app.core.edge_gateway.repository import EdgeGatewayRepository
+
+    return EdgeGatewayService(
+        EdgeGatewayRepository(db.session),
+        signing_private_key=current_app.config.get(
+            "EDGE_GATEWAY_SIGNING_PRIVATE_KEY"
+        ),
+        signing_private_key_file=current_app.config.get(
+            "EDGE_GATEWAY_SIGNING_PRIVATE_KEY_FILE"
+        ),
+        signing_public_key=current_app.config.get(
+            "EDGE_GATEWAY_SIGNING_PUBLIC_KEY"
+        ),
+        signing_key_id=current_app.config.get("EDGE_GATEWAY_SIGNING_KEY_ID"),
+        production=(
+            str(current_app.config.get("FLASK_ENV", "")).lower() == "production"
+        ),
+    )
+
+
+def _actor_uid():
+    identity = getattr(g, "current_user", {}) or {}
+    return identity.get("id") or identity.get("sub")
+
+
+def _payload() -> dict:
+    if request.content_length is not None and request.content_length > MAX_EDGE_REQUEST_BYTES:
+        raise EdgeGatewayPayloadTooLargeError("edge request is too large")
+    raw = request.get_data(cache=True)
+    if len(raw) > MAX_EDGE_REQUEST_BYTES:
+        raise EdgeGatewayPayloadTooLargeError("edge request is too large")
+    value = request.get_json(silent=True)
+    if value is None and not raw:
+        return {}
+    if not isinstance(value, dict):
+        raise EdgeGatewayValidationError("edge request body is invalid")
+    try:
+        preflight_json(
+            value, max_depth=32, max_nodes=MAX_EDGE_REQUEST_NODES,
+            max_string_bytes=131_072, max_total_bytes=MAX_EDGE_REQUEST_BYTES,
+        )
+    except EdgeContractError as exc:
+        raise EdgeGatewayPayloadTooLargeError("edge request exceeds safe limits") from exc
+    return value
+
+
+def _tls_certificate_sha256() -> str:
+    configured = current_app.config.get("EDGE_MTLS_TRUSTED_PROXY_IPS", ())
+    if isinstance(configured, str):
+        configured = tuple(
+            item.strip() for item in configured.split(",") if item.strip()
+        )
+    if not isinstance(configured, (tuple, list, set, frozenset)) or not configured:
+        raise EdgeGatewayAuthenticationError(
+            "trusted mTLS proxy is not configured"
+        )
+    try:
+        trusted = {str(ipaddress.ip_address(item)) for item in configured}
+        remote = str(ipaddress.ip_address(request.remote_addr or ""))
+    except ValueError as exc:
+        raise EdgeGatewayAuthenticationError(
+            "request did not originate from a trusted mTLS proxy"
+        ) from exc
+    if remote not in trusted:
+        raise EdgeGatewayAuthenticationError(
+            "request did not originate from a trusted mTLS proxy"
+        )
+    if request.headers.get("X-DataOps-Edge-Client-Verify") != "SUCCESS":
+        raise EdgeGatewayAuthenticationError(
+            "verified TLS client certificate is required"
+        )
+    escaped_certificate = request.headers.get("X-DataOps-Edge-Client-Cert", "")
+    if not escaped_certificate or len(escaped_certificate.encode()) > 32_768:
+        raise EdgeGatewayAuthenticationError(
+            "verified TLS client certificate is required"
+        )
+    try:
+        certificate = x509.load_pem_x509_certificate(
+            unquote(escaped_certificate).encode()
+        )
+    except ValueError as exc:
+        raise EdgeGatewayAuthenticationError(
+            "verified TLS client certificate is invalid"
+        ) from exc
+    now = datetime.now(UTC)
+    if (
+        certificate.not_valid_before_utc > now
+        or certificate.not_valid_after_utc <= now
+    ):
+        raise EdgeGatewayAuthenticationError(
+            "verified TLS client certificate is not currently valid"
+        )
+    proof = certificate.fingerprint(hashes.SHA256()).hex()
+    header = request.headers.get("X-Edge-Certificate-SHA256")
+    if header and (
+        not re.fullmatch(r"[0-9a-f]{64}", header)
+        or not hmac.compare_digest(header, proof)
+    ):
+        raise EdgeGatewayAuthenticationError(
+            "TLS client certificate proof does not match"
+        )
+    return proof
+
+
+def _machine_auth(gateway_id: str, payload: dict) -> dict:
+    return {
+        "credential": request.headers.get("X-Edge-Credential", ""),
+        "certificate_sha256": _tls_certificate_sha256(),
+        "gateway_id": gateway_id,
+        "environment": payload.get("environment"),
+        "network_zone": payload.get("network_zone"),
+        "generation": payload.get("generation"),
+    }
+
+
+def _respond(call, created: bool = False):
+    try:
+        result = call()
+        response = jsonify(success(result))
+        if len(response.get_data()) > MAX_EDGE_RESPONSE_BYTES:
+            response = jsonify(failed(
+                "边缘网关响应超过安全上限", code=500,
+                error={"code": "EDGE_GATEWAY_RESPONSE_LIMIT"},
+            ))
+            response.headers["Cache-Control"] = "no-store"
+            return response, 500
+        response.headers["Cache-Control"] = "no-store"
+        return response, 201 if created else 200
+    except EdgeGatewayError as exc:
+        logger.warning("edge gateway request rejected: code=%s", exc.code)
+        response = jsonify(failed("边缘网关请求被拒绝", code=exc.status_code, error={"code": exc.code}))
+        response.headers["Cache-Control"] = "no-store"
+        return response, exc.status_code
+    except Exception:
+        logger.exception("edge gateway request failed")
+        response = jsonify(failed("边缘网关操作失败", code=500, error={"code": "EDGE_GATEWAY_INTERNAL"}))
+        response.headers["Cache-Control"] = "no-store"
+        return response, 500
+
+
+@bp.errorhandler(EdgeGatewayError)
+def edge_gateway_error(error):
+    response = jsonify(failed("边缘网关请求被拒绝", code=error.status_code, error={"code": error.code}))
+    response.headers["Cache-Control"] = "no-store"
+    return response, error.status_code
+
+
+@bp.route("/edge/enrollments", methods=["POST"])
+def edge_create_enrollment():
+    payload = _payload()
+    return _respond(lambda: _service().create_enrollment(
+        gateway_name=payload.get("gateway_name"), environment=payload.get("environment"),
+        network_zone=payload.get("network_zone"), policy_digest=payload.get("policy_digest"),
+        allowed_control_hosts=payload.get("allowed_control_hosts"),
+        allowed_proxy_hosts=payload.get("allowed_proxy_hosts", []),
+        expected_certificate_sha256=payload.get("expected_certificate_sha256"),
+        ttl_seconds=payload.get("ttl_seconds", 600), actor_uid=_actor_uid(),
+    ), created=True)
+
+
+@bp.route("/edge/register", methods=["POST"])
+def edge_register():
+    payload = _payload()
+    return _respond(lambda: _service().register(
+        enrollment_token=request.headers.get("X-Edge-Enrollment", ""),
+        certificate_sha256=_tls_certificate_sha256(),
+        gateway_id=payload.get("gateway_id"), environment=payload.get("environment"),
+        network_zone=payload.get("network_zone"), policy_digest=payload.get("policy_digest"),
+        allowed_control_hosts=payload.get("allowed_control_hosts"),
+        allowed_proxy_hosts=payload.get("allowed_proxy_hosts", []), version=payload.get("version"),
+    ), created=True)
+
+
+@bp.route("/edge/gateways", methods=["GET"])
+def edge_list_gateways():
+    return _respond(lambda: {"gateways": _service().list_gateways(
+        limit=request.args.get("limit", 50), offset=request.args.get("offset", 0),
+    )})
+
+
+@bp.route("/edge/gateways/<gateway_id>/heartbeat", methods=["POST"])
+def edge_heartbeat(gateway_id):
+    payload = _payload()
+    return _respond(lambda: _service().heartbeat(
+        **_machine_auth(gateway_id, payload), version=payload.get("version"),
+        safe_summary=payload.get("safe_summary", {}),
+    ))
+
+
+@bp.route("/edge/gateways/<gateway_id>/rotate", methods=["POST"])
+def edge_rotate(gateway_id):
+    payload = _payload()
+    return _respond(lambda: _service().rotate(
+        gateway_id, certificate_sha256=payload.get("certificate_sha256"), actor_uid=_actor_uid(),
+        request_id=payload.get("request_id"),
+    ))
+
+
+@bp.route("/edge/gateways/<gateway_id>/revoke", methods=["POST"])
+def edge_revoke(gateway_id):
+    return _respond(lambda: {"revoked": _service().revoke(gateway_id, actor_uid=_actor_uid())})
+
+
+@bp.route("/edge/tasks", methods=["POST"])
+def edge_issue_task():
+    return _respond(lambda: _service().issue_task(_payload(), actor_uid=_actor_uid()), created=True)
+
+
+@bp.route("/edge/tasks/<task_id>/cancel", methods=["POST"])
+def edge_cancel_task(task_id):
+    return _respond(lambda: {"cancel_requested": _service().cancel_task(task_id, actor_uid=_actor_uid())})
+
+
+@bp.route("/edge/gateways/<gateway_id>/tasks/pull", methods=["POST"])
+def edge_pull_task(gateway_id):
+    payload = _payload()
+    return _respond(lambda: _service().pull_task(**_machine_auth(gateway_id, payload)))
+
+
+@bp.route("/edge/gateways/<gateway_id>/tasks/<task_id>/outcome", methods=["POST"])
+def edge_task_outcome(gateway_id, task_id):
+    payload = _payload()
+    return _respond(lambda: _service().task_outcome(
+        task_id, outcome=payload.get("outcome"), lease_token=payload.get("lease_token"),
+        safe_summary=payload.get("safe_summary", {}), **_machine_auth(gateway_id, payload),
+    ))
+
+
+@bp.route("/edge/gateways/<gateway_id>/events", methods=["POST"])
+def edge_accept_event(gateway_id):
+    payload = _payload()
+    return _respond(lambda: _service().accept_event(
+        event=payload.get("event"), lease_token=payload.get("lease_token"),
+        **_machine_auth(gateway_id, payload),
+    ))
+
+
+@bp.route("/edge/gateways/<gateway_id>/reconcile", methods=["POST"])
+def edge_reconcile(gateway_id):
+    payload = _payload()
+    return _respond(lambda: _service().reconcile(
+        limit=payload.get("limit", 50),
+        cancel_cursor=payload.get("cancel_cursor"),
+        release_cursor=payload.get("release_cursor"),
+        **_machine_auth(gateway_id, payload),
+    ))
+
+
+@bp.route("/edge/gateways/<gateway_id>/releases", methods=["POST"])
+def edge_offer_release(gateway_id):
+    payload = _payload()
+    return _respond(lambda: _service().offer_release(
+        gateway_id, version=payload.get("version"), artifact_digest=payload.get("artifact_digest"),
+        artifact_name=payload.get("artifact_name"), deadline_at=payload.get("deadline_at"),
+        rollback_version=payload.get("rollback_version"), request_id=payload.get("request_id"), actor_uid=_actor_uid(),
+    ), created=True)
+
+
+@bp.route("/edge/gateways/<gateway_id>/releases/ack", methods=["POST"])
+def edge_ack_release(gateway_id):
+    payload = _payload()
+    return _respond(lambda: _service().acknowledge_release(
+        release_id=payload.get("release_id"), outcome=payload.get("outcome"),
+        safe_summary=payload.get("safe_summary", {}), **_machine_auth(gateway_id, payload),
+    ))

+ 141 - 24
deployment/app/config/config.py

@@ -2,6 +2,8 @@ import os
 import platform
 from collections.abc import Mapping
 
+from app.config.database_urls import validate_postgresql_url
+
 
 def get_bool_env(name: str, default: bool = False) -> bool:
     """Return a boolean value from an environment variable."""
@@ -160,7 +162,16 @@ def _apply_minio_config(app, defaults: dict) -> None:
     password = _clean_env("MINIO_PASSWORD")
     bucket = _clean_env("MINIO_BUCKET")
     prefix = _clean_env("MINIO_PREFIX")
-    is_production = os.environ.get("FLASK_ENV", "").lower() == "production"
+    is_production = str(app.config.get("FLASK_ENV", "")).lower() == "production"
+
+    if is_production:
+        app.config["MINIO_HOST"] = host
+        app.config["MINIO_USER"] = user
+        app.config["MINIO_PASSWORD"] = password
+        app.config["MINIO_BUCKET"] = bucket
+        app.config["PREFIX"] = prefix
+        app.config["MINIO_SECURE"] = get_bool_env("MINIO_SECURE", False)
+        return
 
     use_defaults = (
         is_placeholder_env_value(user)
@@ -201,6 +212,32 @@ PRODUCTION_SERVICE_DEFAULTS = {
     "MINIO_PREFIX": "",
 }
 
+_PRODUCTION_REQUIRED_SERVICE_ENV = (
+    "DATABASE_URL",
+    "NEO4J_URI",
+    "NEO4J_HTTP_URI",
+    "NEO4J_USER",
+    "NEO4J_PASSWORD",
+    "MINIO_HOST",
+    "MINIO_USER",
+    "MINIO_PASSWORD",
+    "MINIO_BUCKET",
+)
+
+
+def _validate_production_service_env() -> None:
+    """Reject missing, template, or malformed production service settings."""
+    values = {name: _clean_env(name) for name in _PRODUCTION_REQUIRED_SERVICE_ENV}
+    validate_postgresql_url(values["DATABASE_URL"], "DATABASE_URL")
+    for name, value in values.items():
+        if name == "DATABASE_URL":
+            continue
+        placeholder = is_placeholder_env_value(value)
+        if name in {"NEO4J_URI", "NEO4J_HTTP_URI", "MINIO_HOST"}:
+            placeholder = not value or value.lower().startswith("replace-")
+        if placeholder:
+            raise RuntimeError(f"production {name} is required and cannot be a placeholder")
+
 
 def _apply_config_from_env(app, config_key: str, env_name: str, default):
     value = _clean_env(env_name)
@@ -216,21 +253,35 @@ def apply_runtime_env_config(app) -> None:
     load_production_env_file()
     defaults = PRODUCTION_SERVICE_DEFAULTS
 
-    _apply_config_from_env(
-        app, "SQLALCHEMY_DATABASE_URI", "DATABASE_URL", defaults["SQLALCHEMY_DATABASE_URI"]
-    )
-    _apply_config_from_env(app, "NEO4J_URI", "NEO4J_URI", defaults["NEO4J_URI"])
-    _apply_config_from_env(
-        app, "NEO4J_HTTP_URI", "NEO4J_HTTP_URI", defaults["NEO4J_HTTP_URI"]
-    )
-    _apply_config_from_env(app, "NEO4J_USER", "NEO4J_USER", defaults["NEO4J_USER"])
-    _apply_config_from_env(
-        app, "NEO4J_PASSWORD", "NEO4J_PASSWORD", defaults["NEO4J_PASSWORD"]
-    )
-    app.config["NEO4J_ENCRYPTED"] = get_bool_env(
-        "NEO4J_ENCRYPTED", defaults["NEO4J_ENCRYPTED"]
-    )
-    _apply_minio_config(app, defaults)
+    is_production = str(app.config.get("FLASK_ENV", "")).lower() == "production"
+    if is_production:
+        _validate_production_service_env()
+        app.config["SQLALCHEMY_DATABASE_URI"] = _clean_env("DATABASE_URL")
+        for config_key, env_name in (
+            ("NEO4J_URI", "NEO4J_URI"),
+            ("NEO4J_HTTP_URI", "NEO4J_HTTP_URI"),
+            ("NEO4J_USER", "NEO4J_USER"),
+            ("NEO4J_PASSWORD", "NEO4J_PASSWORD"),
+        ):
+            app.config[config_key] = _clean_env(env_name)
+        app.config["NEO4J_ENCRYPTED"] = get_bool_env("NEO4J_ENCRYPTED", True)
+        _apply_minio_config(app, defaults)
+    else:
+        _apply_config_from_env(
+            app, "SQLALCHEMY_DATABASE_URI", "DATABASE_URL", defaults["SQLALCHEMY_DATABASE_URI"]
+        )
+        _apply_config_from_env(app, "NEO4J_URI", "NEO4J_URI", defaults["NEO4J_URI"])
+        _apply_config_from_env(
+            app, "NEO4J_HTTP_URI", "NEO4J_HTTP_URI", defaults["NEO4J_HTTP_URI"]
+        )
+        _apply_config_from_env(app, "NEO4J_USER", "NEO4J_USER", defaults["NEO4J_USER"])
+        _apply_config_from_env(
+            app, "NEO4J_PASSWORD", "NEO4J_PASSWORD", defaults["NEO4J_PASSWORD"]
+        )
+        app.config["NEO4J_ENCRYPTED"] = get_bool_env(
+            "NEO4J_ENCRYPTED", defaults["NEO4J_ENCRYPTED"]
+        )
+        _apply_minio_config(app, defaults)
 
     secret_key = _clean_env("SECRET_KEY")
     if secret_key and not is_placeholder_env_value(secret_key):
@@ -293,6 +344,55 @@ def apply_runtime_env_config(app) -> None:
     )
 
 
+def validate_production_database_identity(app, engine) -> None:
+    """Verify the connected production login has runtime-only database powers."""
+    if str(app.config.get("FLASK_ENV", "")).lower() != "production":
+        return
+    from sqlalchemy import text
+
+    try:
+        with engine.connect() as connection:
+            identity = connection.execute(text("""
+                SELECT current_user AS role_name,
+                       role.rolsuper,role.rolcreaterole,role.rolcreatedb,
+                       role.rolreplication,role.rolbypassrls,
+                       EXISTS (
+                         SELECT 1 FROM pg_class object
+                         JOIN pg_namespace namespace ON namespace.oid=object.relnamespace
+                         WHERE object.relowner=role.oid
+                           AND namespace.nspname NOT IN ('pg_catalog','information_schema')
+                           AND namespace.nspname NOT LIKE 'pg_toast%'
+                       ) OR EXISTS (
+                         SELECT 1 FROM pg_proc function
+                         JOIN pg_namespace namespace ON namespace.oid=function.pronamespace
+                         WHERE function.proowner=role.oid
+                           AND namespace.nspname NOT IN ('pg_catalog','information_schema')
+                       ) OR EXISTS (
+                         SELECT 1 FROM pg_namespace namespace
+                         WHERE namespace.nspowner=role.oid
+                           AND namespace.nspname NOT IN ('pg_catalog','information_schema')
+                           AND namespace.nspname NOT LIKE 'pg_%'
+                       ) AS owns_application_objects,
+                       EXISTS (
+                         SELECT 1 FROM pg_roles evidence_owner
+                         WHERE evidence_owner.rolname='dataops_edge_evidence_owner'
+                           AND pg_has_role(current_user,evidence_owner.oid,'MEMBER')
+                       ) AS evidence_owner_member
+                  FROM pg_roles role WHERE role.rolname=current_user
+            """)).mappings().one()
+    except Exception as exc:
+        raise RuntimeError("production runtime database identity verification failed") from exc
+    forbidden = (
+        identity["rolsuper"] or identity["rolcreaterole"] or identity["rolcreatedb"]
+        or identity["rolreplication"] or identity["rolbypassrls"]
+        or identity["owns_application_objects"] or identity["evidence_owner_member"]
+    )
+    if forbidden:
+        raise RuntimeError(
+            "production DATABASE_URL must use a non-owner, least-privilege runtime identity"
+        )
+
+
 def log_llm_env_status(app) -> None:
     """Log LLM env load result after logging is configured."""
     deepseek_key = _clean_env("DEEPSEEK_API_KEY") or _clean_env("LLM_API_KEY")
@@ -335,6 +435,8 @@ def log_service_env_status(app) -> None:
         app.config.get("MINIO_HOST"),
         _mask_secret(str(app.config.get("MINIO_USER", ""))),
     )
+    if str(app.config.get("FLASK_ENV", "")).lower() == "production":
+        return
     if is_placeholder_env_value(_clean_env("DATABASE_URL")):
         app.logger.warning(
             "DATABASE_URL 仍为模板占位符,已回退到默认生产库 "
@@ -434,6 +536,23 @@ class BaseConfig:
         "DATASOURCE_CREDENTIAL_KEY_VERSION", "v1"
     )
     AGENT_CREDENTIAL_SECRET = os.environ.get("AGENT_CREDENTIAL_SECRET", "")
+    # Ed25519 raw-private-key hex or PEM. It is loaded only into process memory
+    # and must never be persisted in a task, release, audit, or HTTP response.
+    EDGE_GATEWAY_SIGNING_PRIVATE_KEY = os.environ.get(
+        "EDGE_GATEWAY_SIGNING_PRIVATE_KEY", ""
+    )
+    EDGE_GATEWAY_SIGNING_PRIVATE_KEY_FILE = os.environ.get(
+        "EDGE_GATEWAY_SIGNING_PRIVATE_KEY_FILE", ""
+    )
+    EDGE_GATEWAY_SIGNING_PUBLIC_KEY = os.environ.get(
+        "EDGE_GATEWAY_SIGNING_PUBLIC_KEY", ""
+    )
+    EDGE_GATEWAY_SIGNING_KEY_ID = os.environ.get(
+        "EDGE_GATEWAY_SIGNING_KEY_ID", ""
+    )
+    EDGE_MTLS_TRUSTED_PROXY_IPS = os.environ.get(
+        "EDGE_MTLS_TRUSTED_PROXY_IPS", ""
+    )
     DATASOURCE_CERT_DIR = os.environ.get(
         "DATASOURCE_CERT_DIR",
         "/etc/dataops-platform/datasource-certs",
@@ -555,23 +674,21 @@ class ProductionConfig(BaseConfig):
     PORT = int(os.environ.get("LISTEN_PORT", os.environ.get("PORT", "5500")))
 
     # 生产环境 MinIO 配置
-    MINIO_HOST = os.environ.get("MINIO_HOST", "127.0.0.1:9000")
-    MINIO_USER = os.environ.get("MINIO_USER", "citu-test")
-    MINIO_PASSWORD = os.environ.get("MINIO_PASSWORD", "citu-test")
+    MINIO_HOST = os.environ.get("MINIO_HOST", "")
+    MINIO_USER = os.environ.get("MINIO_USER", "")
+    MINIO_PASSWORD = os.environ.get("MINIO_PASSWORD", "")
     MINIO_SECURE = get_bool_env("MINIO_SECURE", False)
     MINIO_BUCKET = os.environ.get("MINIO_BUCKET", "dataops-bucket")
     PREFIX = os.environ.get("MINIO_PREFIX", "")
 
     # 生产环境 PostgreSQL 配置
-    SQLALCHEMY_DATABASE_URI = os.environ.get(
-        "DATABASE_URL", "postgresql://postgres:postgres@127.0.0.1:5432/dataops"
-    )
+    SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL", "")
 
     # 生产环境 Neo4j 配置
     NEO4J_URI = os.environ.get("NEO4J_URI", "bolt://127.0.0.1:7687")
     NEO4J_HTTP_URI = os.environ.get("NEO4J_HTTP_URI", "http://127.0.0.1:7474")
-    NEO4J_USER = os.environ.get("NEO4J_USER", "neo4j")
-    NEO4J_PASSWORD = os.environ.get("NEO4J_PASSWORD", "Passw0rd")
+    NEO4J_USER = os.environ.get("NEO4J_USER", "")
+    NEO4J_PASSWORD = os.environ.get("NEO4J_PASSWORD", "")
     NEO4J_ENCRYPTED = get_bool_env("NEO4J_ENCRYPTED", False)
 
     # 生产环境文件路径配置

+ 121 - 0
deployment/app/config/database_urls.py

@@ -0,0 +1,121 @@
+"""Fail-closed validation for production PostgreSQL identities and templates."""
+
+from __future__ import annotations
+
+import argparse
+import re
+import sys
+from dataclasses import dataclass
+from pathlib import Path
+from urllib.parse import unquote, urlsplit
+
+_BAD_PERCENT_ESCAPE = re.compile(r"%(?![0-9a-fA-F]{2})")
+_PLACEHOLDER_VALUES = {
+    "change-me",
+    "changeme",
+    "database",
+    "database_name",
+    "dbname",
+    "host",
+    "hostname",
+    "pass",
+    "password",
+    "replace",
+    "user",
+    "username",
+}
+
+
+@dataclass(frozen=True)
+class DatabaseUrlParts:
+    username: str
+    password: str
+    host: str
+    database: str
+
+
+def is_template_secret(value: str) -> bool:
+    """Return true only for empty or explicit template values, after decoding."""
+    normalized = unquote(str(value or "")).strip().lower()
+    return (
+        not normalized
+        or normalized in _PLACEHOLDER_VALUES
+        or normalized.startswith(("replace-", "your-"))
+        or "${" in normalized
+        or normalized.startswith("<")
+        or normalized.endswith(">")
+    )
+
+
+def validate_postgresql_url(value: str, field_name: str) -> DatabaseUrlParts:
+    """Parse a PostgreSQL URL and reject empty/template identity components."""
+    raw = str(value or "").strip()
+    if not raw or _BAD_PERCENT_ESCAPE.search(raw):
+        raise RuntimeError(f"{field_name} is missing or malformed")
+    try:
+        parsed = urlsplit(raw)
+        port = parsed.port
+    except (TypeError, ValueError) as exc:
+        raise RuntimeError(f"{field_name} is malformed") from exc
+    if parsed.scheme.split("+", 1)[0].lower() not in {"postgres", "postgresql"}:
+        raise RuntimeError(f"{field_name} must use PostgreSQL")
+    values = {
+        "username": unquote(parsed.username or ""),
+        "password": unquote(parsed.password or ""),
+        "host": unquote(parsed.hostname or ""),
+        "database": unquote(parsed.path.lstrip("/")),
+    }
+    if port is not None and not 1 <= port <= 65535:
+        raise RuntimeError(f"{field_name} has an invalid port")
+    for component, component_value in values.items():
+        if is_template_secret(component_value):
+            raise RuntimeError(f"{field_name} has an invalid {component}")
+    return DatabaseUrlParts(**values)
+
+
+def _read_env_file(path: str | Path) -> dict[str, str]:
+    values: dict[str, str] = {}
+    for raw_line in Path(path).read_text(encoding="utf-8-sig").splitlines():
+        line = raw_line.strip()
+        if not line or line.startswith("#") or "=" not in line:
+            continue
+        name, value = line.split("=", 1)
+        values[name.strip()] = value.strip().strip('"').strip("'")
+    return values
+
+
+def validate_database_environment(values: dict[str, str]) -> None:
+    runtime_parts = None
+    for field_name in (
+        "DB_ROLE_INIT_DATABASE_URL",
+        "MIGRATION_DATABASE_URL",
+        "DATABASE_URL",
+    ):
+        parts = validate_postgresql_url(values.get(field_name, ""), field_name)
+        if field_name == "DATABASE_URL":
+            runtime_parts = parts
+    runtime_password = values.get("DATAOPS_RUNTIME_PASSWORD", "")
+    if is_template_secret(runtime_password):
+        raise RuntimeError("DATAOPS_RUNTIME_PASSWORD is missing or a template value")
+    if runtime_parts is None or runtime_parts.password != runtime_password:
+        raise RuntimeError("DATAOPS_RUNTIME_PASSWORD must match DATABASE_URL")
+
+
+def load_and_validate_database_env(path: str | Path) -> None:
+    validate_database_environment(_read_env_file(path))
+
+
+def main(argv: list[str] | None = None) -> int:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--env-file", required=True)
+    args = parser.parse_args(argv)
+    try:
+        load_and_validate_database_env(args.env_file)
+    except (OSError, RuntimeError) as exc:
+        print(f"database configuration rejected: {exc}", file=sys.stderr)
+        return 2
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())

+ 63 - 0
deployment/app/core/edge_gateway/__init__.py

@@ -0,0 +1,63 @@
+"""Enterprise edge gateway contracts, policy, and durable local queue."""
+
+from .contracts import (
+    EVENT_CLASSIFICATIONS,
+    TASK_CLASSIFICATIONS,
+    TASK_OPERATIONS,
+    EdgeContractError,
+    EdgeEventContract,
+    EdgeTaskContract,
+    SignedTaskEnvelope,
+    canonical_json_bytes,
+    canonical_sha256,
+    stable_event_id,
+)
+from .policy import (
+    CONTROL_PLANE_ALLOWED,
+    EDGE_ONLY,
+    RETENTION_DAYS,
+    EdgeEgressPolicy,
+    EdgePolicyError,
+)
+from .queue import (
+    SCHEMA_VERSION,
+    EdgeQueueConflictError,
+    EdgeQueueError,
+    EdgeQueueLeaseError,
+    EdgeQueueSchemaError,
+    LocalArtifact,
+    QueuedEvent,
+    QueuedOutcome,
+    QueuedTask,
+    ReleaseState,
+    SqliteEdgeQueue,
+)
+
+__all__ = [
+    "CONTROL_PLANE_ALLOWED",
+    "EDGE_ONLY",
+    "EVENT_CLASSIFICATIONS",
+    "RETENTION_DAYS",
+    "TASK_CLASSIFICATIONS",
+    "TASK_OPERATIONS",
+    "EdgeContractError",
+    "EdgeEgressPolicy",
+    "EdgeEventContract",
+    "EdgePolicyError",
+    "EdgeQueueConflictError",
+    "EdgeQueueError",
+    "EdgeQueueLeaseError",
+    "EdgeQueueSchemaError",
+    "EdgeTaskContract",
+    "SignedTaskEnvelope",
+    "LocalArtifact",
+    "QueuedEvent",
+    "QueuedOutcome",
+    "QueuedTask",
+    "ReleaseState",
+    "SqliteEdgeQueue",
+    "SCHEMA_VERSION",
+    "canonical_json_bytes",
+    "canonical_sha256",
+    "stable_event_id",
+]

+ 706 - 0
deployment/app/core/edge_gateway/contracts.py

@@ -0,0 +1,706 @@
+"""Immutable, fail-closed contracts for the enterprise edge boundary."""
+
+from __future__ import annotations
+
+import hashlib
+import json
+import math
+import re
+from collections.abc import Mapping
+from dataclasses import dataclass
+from dataclasses import field as dataclass_field
+from datetime import UTC, datetime, timedelta
+from decimal import Decimal, InvalidOperation
+from types import MappingProxyType
+from typing import ClassVar
+
+from cryptography.exceptions import InvalidSignature
+from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
+
+
+class EdgeContractError(ValueError):
+    """Raised when an edge contract is malformed or ambiguous."""
+
+
+TASK_OPERATIONS = frozenset(
+    {"collect", "profile", "quality", "lineage", "controlled_query"}
+)
+TASK_CLASSIFICATIONS = frozenset(
+    {
+        "raw",
+        "recent_detail",
+        "restricted",
+        "desensitized_metadata",
+        "statistics",
+        "lineage",
+        "evidence",
+    }
+)
+EVENT_CLASSIFICATIONS = frozenset(
+    {
+        "desensitized_metadata",
+        "statistics",
+        "lineage",
+        "evidence",
+        "health_summary",
+        "diagnostic_summary",
+    }
+)
+CONTRACT_VERSION = 1
+MAX_ATTEMPT = 5
+PREFLIGHT_MAX_DEPTH = 64
+PREFLIGHT_MAX_NODES = 10_000
+PREFLIGHT_MAX_STRING_BYTES = 131_072
+PREFLIGHT_MAX_TOTAL_BYTES = 1_048_576
+
+_IDENTIFIER = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,254}$")
+_SHA256 = re.compile(r"^[0-9a-f]{64}$")
+_ED25519_SIGNATURE = re.compile(r"^[0-9a-f]{128}$")
+_RFC3339 = re.compile(
+    r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|[+-]\d{2}:\d{2})$"
+)
+
+
+def preflight_json(
+    value: object,
+    *,
+    max_depth: int = PREFLIGHT_MAX_DEPTH,
+    max_nodes: int = PREFLIGHT_MAX_NODES,
+    max_string_bytes: int = PREFLIGHT_MAX_STRING_BYTES,
+    max_total_bytes: int = PREFLIGHT_MAX_TOTAL_BYTES,
+) -> None:
+    """Bound a JSON-like graph iteratively before copying or encoding it."""
+
+    nodes = 0
+    estimated_bytes = 0
+    active: set[int] = set()
+    stack: list[tuple[bool, object, int]] = [(False, value, 0)]
+    try:
+        while stack:
+            exiting, current, depth = stack.pop()
+            if exiting:
+                active.remove(id(current))
+                continue
+            if depth > max_depth:
+                raise EdgeContractError("contract exceeds the maximum depth")
+            nodes += 1
+            if nodes > max_nodes:
+                raise EdgeContractError("contract exceeds the maximum node count")
+            if isinstance(current, str):
+                size = len(current.encode("utf-8"))
+                estimated_bytes += size + 16
+                if estimated_bytes > max_total_bytes:
+                    raise EdgeContractError(
+                        "contract exceeds the conservative byte limit"
+                    )
+                if size > max_string_bytes:
+                    raise EdgeContractError("contract string exceeds the byte limit")
+            elif current is None or isinstance(current, bool):
+                estimated_bytes += 8
+            elif isinstance(current, int):
+                if current.bit_length() > 4096:
+                    raise EdgeContractError("contract integer exceeds the byte limit")
+                estimated_bytes += max(8, current.bit_length() // 3 + 4)
+            elif isinstance(current, float):
+                if not math.isfinite(current):
+                    raise EdgeContractError("contract numbers must be finite")
+                estimated_bytes += 32
+            elif isinstance(current, Mapping):
+                identity = id(current)
+                if identity in active:
+                    raise EdgeContractError("contract contains a cycle")
+                active.add(identity)
+                stack.append((True, current, depth))
+                estimated_bytes += 16
+                children: list[object] = []
+                for key, item in current.items():
+                    if not isinstance(key, str):
+                        raise EdgeContractError("contract property names must be strings")
+                    key_size = len(key.encode("utf-8"))
+                    if key_size > max_string_bytes:
+                        raise EdgeContractError("contract key exceeds the byte limit")
+                    estimated_bytes += key_size + 8
+                    children.append(item)
+                stack.extend((False, item, depth + 1) for item in reversed(children))
+            elif isinstance(current, (list, tuple)):
+                identity = id(current)
+                if identity in active:
+                    raise EdgeContractError("contract contains a cycle")
+                active.add(identity)
+                stack.append((True, current, depth))
+                estimated_bytes += 16
+                stack.extend((False, item, depth + 1) for item in reversed(current))
+            else:
+                raise EdgeContractError("contract contains a non-JSON value")
+            if estimated_bytes > max_total_bytes:
+                raise EdgeContractError("contract exceeds the conservative byte limit")
+    except (MemoryError, RecursionError) as exc:
+        raise EdgeContractError("contract exceeds safe structural limits") from exc
+
+
+def _length_prefixed(tag: bytes, content: bytes) -> bytes:
+    return tag + str(len(content)).encode("ascii") + b":" + content
+
+
+def _number_text(value: int | float) -> str:
+    try:
+        number = Decimal(value if isinstance(value, int) else str(value))
+    except (InvalidOperation, ValueError) as exc:
+        raise EdgeContractError("contract number is invalid") from exc
+    if not number.is_finite():
+        raise EdgeContractError("contract numbers must be finite")
+    if number == 0:
+        return "0"
+    normalized = format(number.normalize(), "f")
+    if "." in normalized:
+        normalized = normalized.rstrip("0").rstrip(".")
+    return normalized
+
+
+def _canonical_encode(value: object) -> bytes:
+    if value is None:
+        return b"n"
+    if isinstance(value, bool):
+        return b"b1" if value else b"b0"
+    if isinstance(value, (int, float)):
+        return _length_prefixed(b"d", _number_text(value).encode("ascii"))
+    if isinstance(value, str):
+        return _length_prefixed(b"s", value.encode("utf-8"))
+    if isinstance(value, Mapping):
+        encoded = bytearray(b"m" + str(len(value)).encode("ascii") + b":")
+        for key in sorted(value, key=lambda item: item.encode("utf-8")):
+            key_bytes = key.encode("utf-8")
+            encoded.extend(_length_prefixed(b"k", key_bytes))
+            encoded.extend(_canonical_encode(value[key]))
+        return bytes(encoded)
+    if isinstance(value, (list, tuple)):
+        encoded = bytearray(b"l" + str(len(value)).encode("ascii") + b":")
+        for item in value:
+            encoded.extend(_canonical_encode(item))
+        return bytes(encoded)
+    raise EdgeContractError("contract contains a non-JSON value")
+
+
+def canonical_json_bytes(value: object) -> bytes:
+    """Encode the explicit type-tagged, length-prefixed canonical protocol."""
+
+    preflight_json(value)
+    try:
+        return _canonical_encode(value)
+    except (MemoryError, RecursionError) as exc:
+        raise EdgeContractError("contract exceeds safe encoding limits") from exc
+
+
+def canonical_sha256(value: object) -> str:
+    return hashlib.sha256(canonical_json_bytes(value)).hexdigest()
+
+
+def strict_json_bytes(value: object) -> bytes:
+    """Serialize validated data as strict transport/storage JSON."""
+
+    preflight_json(value)
+    try:
+        return json.dumps(
+            value,
+            sort_keys=True,
+            separators=(",", ":"),
+            ensure_ascii=False,
+            allow_nan=False,
+        ).encode("utf-8")
+    except (MemoryError, RecursionError, TypeError, ValueError) as exc:
+        raise EdgeContractError("contract cannot be serialized as strict JSON") from exc
+
+
+def stable_event_id(identity: Mapping[str, object]) -> str:
+    if not isinstance(identity, Mapping):
+        raise EdgeContractError("event identity must be a mapping")
+    normalized = dict(identity)
+    if "occurred_at" in normalized:
+        normalized["occurred_at"] = _timestamp(
+            normalized["occurred_at"], "occurred_at"
+        )
+    return f"evt_{canonical_sha256(normalized)}"
+
+
+def _strict_mapping(value: object, *, label: str) -> dict[str, object]:
+    if not isinstance(value, Mapping):
+        raise EdgeContractError(f"{label} must be a mapping")
+    preflight_json(value)
+    return dict(value)
+
+
+def _require_exact_properties(
+    value: Mapping[str, object],
+    *,
+    required: frozenset[str],
+    optional: frozenset[str] = frozenset(),
+) -> None:
+    keys = frozenset(value)
+    unknown = keys - required - optional
+    missing = required - keys
+    if unknown:
+        raise EdgeContractError(
+            f"contract has unknown properties: {', '.join(sorted(unknown))}"
+        )
+    if missing:
+        raise EdgeContractError(
+            f"contract is missing properties: {', '.join(sorted(missing))}"
+        )
+
+
+def _text(value: object, label: str, *, maximum: int = 255) -> str:
+    if (
+        not isinstance(value, str)
+        or not value
+        or value.strip() != value
+        or len(value.encode("utf-8")) > maximum
+        or "\x00" in value
+    ):
+        raise EdgeContractError(f"{label} is invalid")
+    return value
+
+
+def _identifier(value: object, label: str) -> str:
+    candidate = _text(value, label)
+    if not _IDENTIFIER.fullmatch(candidate):
+        raise EdgeContractError(f"{label} is invalid")
+    return candidate
+
+
+def _attempt(value: object) -> int:
+    if isinstance(value, bool) or not isinstance(value, int) or not 1 <= value <= 5:
+        raise EdgeContractError("attempt must be between 1 and 5")
+    return value
+
+
+def _version(value: object) -> int:
+    if isinstance(value, bool) or value != CONTRACT_VERSION:
+        raise EdgeContractError("contract_version is not supported")
+    return CONTRACT_VERSION
+
+
+def _timestamp(value: object, label: str) -> str:
+    candidate = _text(value, label, maximum=64)
+    if not _RFC3339.fullmatch(candidate):
+        raise EdgeContractError(f"{label} must be an RFC3339 timestamp")
+    try:
+        parsed = datetime.fromisoformat(candidate.replace("Z", "+00:00"))
+    except ValueError as exc:
+        raise EdgeContractError(f"{label} must be an RFC3339 timestamp") from exc
+    if parsed.tzinfo is None or parsed.utcoffset() is None:
+        raise EdgeContractError(f"{label} must be an RFC3339 timestamp")
+    utc = parsed.astimezone(UTC)
+    canonical = utc.strftime("%Y-%m-%dT%H:%M:%S")
+    if utc.microsecond:
+        canonical += f".{utc.microsecond:06d}".rstrip("0")
+    return f"{canonical}Z"
+
+
+def canonical_timestamp(value: object, label: str = "timestamp") -> str:
+    return _timestamp(value, label)
+
+
+def _digest(value: object, label: str) -> str:
+    candidate = _text(value, label, maximum=64)
+    if not _SHA256.fullmatch(candidate):
+        raise EdgeContractError(f"{label} must be a lowercase SHA256 digest")
+    return candidate
+
+
+def _json_copy(value: object) -> object:
+    try:
+        encoded = strict_json_bytes(value)
+        return json.loads(encoded.decode("utf-8"))
+    except (MemoryError, RecursionError, TypeError, ValueError) as exc:
+        raise EdgeContractError("contract cannot be copied as strict JSON") from exc
+
+
+def _deep_freeze(value: object) -> object:
+    if isinstance(value, dict):
+        return MappingProxyType({key: _deep_freeze(item) for key, item in value.items()})
+    if isinstance(value, list):
+        return tuple(_deep_freeze(item) for item in value)
+    return value
+
+
+def _deep_thaw(value: object) -> object:
+    if isinstance(value, Mapping):
+        return {key: _deep_thaw(item) for key, item in value.items()}
+    if isinstance(value, tuple):
+        return [_deep_thaw(item) for item in value]
+    return value
+
+
+@dataclass(frozen=True, slots=True)
+class EdgeTaskContract:
+    task_id: str
+    gateway_id: str
+    environment: str
+    network_zone: str
+    purpose: str
+    classification: str
+    operation: str
+    contract_version: int
+    deadline_at: str
+    attempt: int
+    idempotency_key: str
+    policy_digest: str
+
+    def __post_init__(self) -> None:
+        values = {
+            "task_id": _identifier(self.task_id, "task_id"),
+            "gateway_id": _identifier(self.gateway_id, "gateway_id"),
+            "environment": _identifier(self.environment, "environment"),
+            "network_zone": _identifier(self.network_zone, "network_zone"),
+            "purpose": _identifier(self.purpose, "purpose"),
+            "contract_version": _version(self.contract_version),
+            "deadline_at": _timestamp(self.deadline_at, "deadline_at"),
+            "attempt": _attempt(self.attempt),
+            "idempotency_key": _identifier(self.idempotency_key, "idempotency_key"),
+            "policy_digest": _digest(self.policy_digest, "policy_digest"),
+        }
+        if self.operation not in TASK_OPERATIONS:
+            raise EdgeContractError("task operation is not approved")
+        if self.classification not in TASK_CLASSIFICATIONS:
+            raise EdgeContractError("task classification is invalid")
+        for field, value in values.items():
+            object.__setattr__(self, field, value)
+
+    @property
+    def task_type(self) -> str:
+        return self.operation
+
+    @classmethod
+    def from_mapping(cls, value: Mapping[str, object]) -> EdgeTaskContract:
+        data = _strict_mapping(value, label="task contract")
+        required = frozenset(
+            {
+                "task_id",
+                "gateway_id",
+                "environment",
+                "network_zone",
+                "purpose",
+                "classification",
+                "contract_version",
+                "deadline_at",
+                "attempt",
+                "idempotency_key",
+                "policy_digest",
+            }
+        )
+        _require_exact_properties(
+            data,
+            required=required,
+            optional=frozenset({"task_type", "operation"}),
+        )
+        if ("task_type" in data) == ("operation" in data):
+            raise EdgeContractError(
+                "task contract requires exactly one operation property"
+            )
+        return cls(
+            task_id=data["task_id"],  # type: ignore[arg-type]
+            gateway_id=data["gateway_id"],  # type: ignore[arg-type]
+            environment=data["environment"],  # type: ignore[arg-type]
+            network_zone=data["network_zone"],  # type: ignore[arg-type]
+            purpose=data["purpose"],  # type: ignore[arg-type]
+            classification=data["classification"],  # type: ignore[arg-type]
+            operation=data.get("task_type", data.get("operation")),  # type: ignore[arg-type]
+            contract_version=data["contract_version"],  # type: ignore[arg-type]
+            deadline_at=data["deadline_at"],  # type: ignore[arg-type]
+            attempt=data["attempt"],  # type: ignore[arg-type]
+            idempotency_key=data["idempotency_key"],  # type: ignore[arg-type]
+            policy_digest=data["policy_digest"],  # type: ignore[arg-type]
+        )
+
+    def to_mapping(self) -> dict[str, object]:
+        return {
+            "task_id": self.task_id,
+            "gateway_id": self.gateway_id,
+            "environment": self.environment,
+            "network_zone": self.network_zone,
+            "purpose": self.purpose,
+            "classification": self.classification,
+            "task_type": self.operation,
+            "contract_version": self.contract_version,
+            "deadline_at": self.deadline_at,
+            "attempt": self.attempt,
+            "idempotency_key": self.idempotency_key,
+            "policy_digest": self.policy_digest,
+        }
+
+    @property
+    def digest(self) -> str:
+        return canonical_sha256(self.to_mapping())
+
+
+@dataclass(frozen=True, slots=True)
+class SignedTaskEnvelope:
+    """Control-plane authority over an exact, context-bound task contract."""
+
+    task: EdgeTaskContract
+    authority_key_id: str
+    signature_algorithm: str
+    contract_digest: str
+    gateway_id: str
+    environment: str
+    network_zone: str
+    policy_digest: str
+    purpose: str
+    issued_at: str
+    expires_at: str
+    signature: str
+    _verified: bool = dataclass_field(default=False, init=False, repr=False, compare=False)
+
+    _UNSIGNED_FIELDS: ClassVar[frozenset[str]] = frozenset(
+        {
+            "task", "authority_key_id", "signature_algorithm", "contract_digest",
+            "gateway_id", "environment", "network_zone", "policy_digest", "purpose",
+            "issued_at", "expires_at",
+        }
+    )
+
+    def __post_init__(self) -> None:
+        if not isinstance(self.task, EdgeTaskContract):
+            raise EdgeContractError("signed task envelope requires a task contract")
+        normalized_task = EdgeTaskContract.from_mapping(self.task.to_mapping())
+        unsigned = {
+            "task": normalized_task.to_mapping(),
+            "authority_key_id": self.authority_key_id,
+            "signature_algorithm": self.signature_algorithm,
+            "contract_digest": self.contract_digest,
+            "gateway_id": self.gateway_id,
+            "environment": self.environment,
+            "network_zone": self.network_zone,
+            "policy_digest": self.policy_digest,
+            "purpose": self.purpose,
+            "issued_at": self.issued_at,
+            "expires_at": self.expires_at,
+        }
+        self.canonical_unsigned_bytes(unsigned)
+        if not isinstance(self.signature, str) or not _ED25519_SIGNATURE.fullmatch(self.signature):
+            raise EdgeContractError("signature must be a lowercase Ed25519 signature")
+        object.__setattr__(self, "task", normalized_task)
+
+    @classmethod
+    def canonical_unsigned_bytes(cls, value: Mapping[str, object]) -> bytes:
+        data = _strict_mapping(value, label="signed task envelope")
+        _require_exact_properties(data, required=cls._UNSIGNED_FIELDS)
+        task_raw = data["task"]
+        if isinstance(task_raw, EdgeTaskContract):
+            task_raw = task_raw.to_mapping()
+        task = EdgeTaskContract.from_mapping(task_raw)  # type: ignore[arg-type]
+        normalized = {
+            "task": task.to_mapping(),
+            "authority_key_id": _identifier(data["authority_key_id"], "authority_key_id"),
+            "signature_algorithm": data["signature_algorithm"],
+            "contract_digest": _digest(data["contract_digest"], "contract_digest"),
+            "gateway_id": _identifier(data["gateway_id"], "gateway_id"),
+            "environment": _identifier(data["environment"], "environment"),
+            "network_zone": _identifier(data["network_zone"], "network_zone"),
+            "policy_digest": _digest(data["policy_digest"], "policy_digest"),
+            "purpose": _identifier(data["purpose"], "purpose"),
+            "issued_at": _timestamp(data["issued_at"], "issued_at"),
+            "expires_at": _timestamp(data["expires_at"], "expires_at"),
+        }
+        if normalized["signature_algorithm"] != "Ed25519":
+            raise EdgeContractError("signature_algorithm is not supported")
+        for field in ("gateway_id", "environment", "network_zone", "policy_digest", "purpose"):
+            if normalized[field] != task.to_mapping()[field]:
+                raise EdgeContractError(f"signed authority {field} does not bind the task")
+        if normalized["contract_digest"] != task.digest:
+            raise EdgeContractError("contract_digest does not bind the task")
+        issued = datetime.fromisoformat(str(normalized["issued_at"]).replace("Z", "+00:00"))
+        expires = datetime.fromisoformat(str(normalized["expires_at"]).replace("Z", "+00:00"))
+        deadline = datetime.fromisoformat(task.deadline_at.replace("Z", "+00:00"))
+        if expires <= issued or expires > deadline:
+            raise EdgeContractError("signed authority validity window is invalid")
+        return canonical_json_bytes(normalized)
+
+    @classmethod
+    def verify_mapping(
+        cls,
+        value: Mapping[str, object],
+        *,
+        authority_keys: Mapping[str, bytes | Ed25519PublicKey],
+        now: datetime,
+        allowed_future_skew_seconds: int = 0,
+    ) -> SignedTaskEnvelope:
+        data = _strict_mapping(value, label="signed task envelope")
+        _require_exact_properties(data, required=cls._UNSIGNED_FIELDS | {"signature"})
+        if not isinstance(now, datetime) or now.tzinfo is None or now.utcoffset() is None:
+            raise EdgeContractError("verification time must be timezone-aware")
+        if (
+            isinstance(allowed_future_skew_seconds, bool)
+            or not isinstance(allowed_future_skew_seconds, int)
+            or not 0 <= allowed_future_skew_seconds <= 300
+        ):
+            raise EdgeContractError("task authority clock skew is invalid")
+        signature = _text(data["signature"], "signature", maximum=128)
+        if not _ED25519_SIGNATURE.fullmatch(signature):
+            raise EdgeContractError("signature must be a lowercase Ed25519 signature")
+        unsigned = {key: data[key] for key in cls._UNSIGNED_FIELDS}
+        signed_bytes = cls.canonical_unsigned_bytes(unsigned)
+        key_id = _identifier(data["authority_key_id"], "authority_key_id")
+        key = authority_keys.get(key_id)
+        if key is None:
+            raise EdgeContractError("task authority key is not trusted")
+        try:
+            public_key = (
+                key if isinstance(key, Ed25519PublicKey)
+                else Ed25519PublicKey.from_public_bytes(bytes(key))
+            )
+            public_key.verify(bytes.fromhex(signature), signed_bytes)
+        except (InvalidSignature, TypeError, ValueError) as exc:
+            raise EdgeContractError("task authority signature is invalid") from exc
+        issued_at = _timestamp(data["issued_at"], "issued_at")
+        expires_at = _timestamp(data["expires_at"], "expires_at")
+        instant = now.astimezone(UTC)
+        issued = datetime.fromisoformat(issued_at.replace("Z", "+00:00"))
+        expires = datetime.fromisoformat(expires_at.replace("Z", "+00:00"))
+        if instant + timedelta(seconds=allowed_future_skew_seconds) < issued:
+            raise EdgeContractError("signed task is not yet valid")
+        if instant >= expires:
+            raise EdgeContractError("signed task authority has expired")
+        task = EdgeTaskContract.from_mapping(data["task"])  # type: ignore[arg-type]
+        envelope = cls(
+            task=task,
+            authority_key_id=key_id,
+            signature_algorithm="Ed25519",
+            contract_digest=task.digest,
+            gateway_id=task.gateway_id,
+            environment=task.environment,
+            network_zone=task.network_zone,
+            policy_digest=task.policy_digest,
+            purpose=task.purpose,
+            issued_at=issued_at,
+            expires_at=expires_at,
+            signature=signature,
+        )
+        object.__setattr__(envelope, "_verified", True)
+        return envelope
+
+    @property
+    def is_verified(self) -> bool:
+        return self._verified
+
+    def to_mapping(self) -> dict[str, object]:
+        return {
+            "task": self.task.to_mapping(),
+            "authority_key_id": self.authority_key_id,
+            "signature_algorithm": self.signature_algorithm,
+            "contract_digest": self.contract_digest,
+            "gateway_id": self.gateway_id,
+            "environment": self.environment,
+            "network_zone": self.network_zone,
+            "policy_digest": self.policy_digest,
+            "purpose": self.purpose,
+            "issued_at": self.issued_at,
+            "expires_at": self.expires_at,
+            "signature": self.signature,
+        }
+
+    @property
+    def digest(self) -> str:
+        return canonical_sha256(self.to_mapping())
+
+
+@dataclass(frozen=True, slots=True)
+class EdgeEventContract:
+    event_id: str
+    task_id: str
+    gateway_id: str
+    environment: str
+    network_zone: str
+    purpose: str
+    classification: str
+    contract_version: int
+    occurred_at: str
+    attempt: int
+    idempotency_key: str
+    policy_digest: str
+    payload: Mapping[str, object]
+
+    def __post_init__(self) -> None:
+        payload = _strict_mapping(self.payload, label="event payload")
+        payload_copy = _json_copy(payload)
+        if not isinstance(payload_copy, dict):
+            raise EdgeContractError("event payload must be a mapping")
+        values = {
+            "task_id": _identifier(self.task_id, "task_id"),
+            "gateway_id": _identifier(self.gateway_id, "gateway_id"),
+            "environment": _identifier(self.environment, "environment"),
+            "network_zone": _identifier(self.network_zone, "network_zone"),
+            "purpose": _identifier(self.purpose, "purpose"),
+            "contract_version": _version(self.contract_version),
+            "occurred_at": _timestamp(self.occurred_at, "occurred_at"),
+            "attempt": _attempt(self.attempt),
+            "idempotency_key": _identifier(self.idempotency_key, "idempotency_key"),
+            "policy_digest": _digest(self.policy_digest, "policy_digest"),
+        }
+        if self.classification not in EVENT_CLASSIFICATIONS:
+            raise EdgeContractError("event classification is not approved")
+        identity = {
+            "task_id": values["task_id"],
+            "gateway_id": values["gateway_id"],
+            "environment": values["environment"],
+            "network_zone": values["network_zone"],
+            "purpose": values["purpose"],
+            "classification": self.classification,
+            "contract_version": values["contract_version"],
+            "occurred_at": values["occurred_at"],
+            "attempt": values["attempt"],
+            "idempotency_key": values["idempotency_key"],
+            "policy_digest": values["policy_digest"],
+            "payload": payload_copy,
+        }
+        expected_id = stable_event_id(identity)
+        if self.event_id != expected_id:
+            raise EdgeContractError("event_id is not the stable event identifier")
+        object.__setattr__(self, "event_id", expected_id)
+        object.__setattr__(self, "payload", _deep_freeze(payload_copy))
+        for field, value in values.items():
+            object.__setattr__(self, field, value)
+
+    @classmethod
+    def from_mapping(cls, value: Mapping[str, object]) -> EdgeEventContract:
+        data = _strict_mapping(value, label="event contract")
+        required = frozenset(
+            {
+                "event_id",
+                "task_id",
+                "gateway_id",
+                "environment",
+                "network_zone",
+                "purpose",
+                "classification",
+                "contract_version",
+                "occurred_at",
+                "attempt",
+                "idempotency_key",
+                "policy_digest",
+                "payload",
+            }
+        )
+        _require_exact_properties(data, required=required)
+        return cls(**data)  # type: ignore[arg-type]
+
+    def to_mapping(self) -> dict[str, object]:
+        return {
+            "event_id": self.event_id,
+            "task_id": self.task_id,
+            "gateway_id": self.gateway_id,
+            "environment": self.environment,
+            "network_zone": self.network_zone,
+            "purpose": self.purpose,
+            "classification": self.classification,
+            "contract_version": self.contract_version,
+            "occurred_at": self.occurred_at,
+            "attempt": self.attempt,
+            "idempotency_key": self.idempotency_key,
+            "policy_digest": self.policy_digest,
+            "payload": _deep_thaw(self.payload),
+        }
+
+    @property
+    def digest(self) -> str:
+        return canonical_sha256(self.to_mapping())

+ 475 - 0
deployment/app/core/edge_gateway/policy.py

@@ -0,0 +1,475 @@
+"""Default-deny egress and retention policy for enterprise edge events."""
+
+from __future__ import annotations
+
+import ipaddress
+import re
+import unicodedata
+from collections.abc import Mapping
+from urllib.parse import urlsplit
+
+from .contracts import EdgeContractError, preflight_json, strict_json_bytes
+
+
+class EdgePolicyError(ValueError):
+    """Raised when content or transport violates the edge boundary."""
+
+
+EDGE_ONLY = {"raw", "recent_detail", "restricted"}
+CONTROL_PLANE_ALLOWED = {
+    "desensitized_metadata",
+    "statistics",
+    "lineage",
+    "evidence",
+    "health_summary",
+    "diagnostic_summary",
+}
+RETENTION_DAYS = {
+    "raw": 0,
+    "recent_detail": 365,
+    "metadata": 1095,
+    "evidence": 2190,
+}
+
+DEFAULT_BYTE_LIMITS = {
+    "desensitized_metadata": 65_536,
+    "statistics": 32_768,
+    "lineage": 65_536,
+    "evidence": 131_072,
+    "health_summary": 8_192,
+    "diagnostic_summary": 16_384,
+}
+DEFAULT_COUNT_LIMITS = {
+    "desensitized_metadata": 2_000,
+    "statistics": 1_000,
+    "lineage": 2_000,
+    "evidence": 4_000,
+    "health_summary": 256,
+    "diagnostic_summary": 512,
+}
+
+_SENSITIVE_KEYS = {
+    "api_key",
+    "apikey",
+    "authorization",
+    "access_token",
+    "cookie",
+    "credential",
+    "credentials",
+    "password",
+    "passwd",
+    "private_key",
+    "query",
+    "query_text",
+    "raw_record",
+    "raw_records",
+    "raw_row",
+    "raw_rows",
+    "secret",
+    "sql",
+    "sql_text",
+    "statement",
+    "token",
+}
+_SENSITIVE_KEY_PARTS = ("password", "passwd", "secret", "private_key", "api_key")
+_RAW_ROW_CONTAINER_KEYS = {
+    "data",
+    "items",
+    "item",
+    "record",
+    "records",
+    "result",
+    "row",
+    "rows",
+    "sample",
+    "samples",
+}
+_PRIVATE_KEY = re.compile(r"-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----", re.IGNORECASE)
+_BEARER = re.compile(r"\bBearer\s+[A-Za-z0-9._~+/=-]{8,}", re.IGNORECASE)
+_CREDENTIAL_ASSIGNMENT = re.compile(
+    r"\b(?:[a-z0-9]+[_-])*(?:password|passwd|pwd|secret|token|api[_-]?key|"
+    r"authorization|credential)"
+    r"\b\s*(?:=|:)\s*[^\s,;]+",
+    re.IGNORECASE,
+)
+_SQL = re.compile(
+    r"(?:"
+    r"\bSELECT\b.{0,4096}?\bFROM\b"
+    r"|\bINSERT\s+INTO\b"
+    r"|\bUPDATE\b.{0,4096}?\bSET\b"
+    r"|\bDELETE\s+FROM\b"
+    r"|\bMERGE(?:\s+INTO)?\b"
+    r"|\b(?:CREATE|ALTER|DROP|TRUNCATE)\s+"
+    r"(?:TABLE|VIEW|SCHEMA|DATABASE|INDEX|FUNCTION|PROCEDURE)\b"
+    r"|\b(?:GRANT|REVOKE)\b.{0,4096}?\b(?:TO|FROM)\b"
+    r"|\bCOPY\b"
+    r")",
+    re.IGNORECASE | re.DOTALL,
+)
+
+
+def _normalized_key(value: str) -> str:
+    compatible = unicodedata.normalize("NFKC", value)
+    words = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", compatible)
+    return re.sub(r"[^a-z0-9]+", "_", words.lower()).strip("_")
+
+
+def _is_sensitive_key(value: str) -> bool:
+    folded = value.replace("_", "")
+    return (
+        value in _SENSITIVE_KEYS
+        or folded
+        in {
+            "accesstoken",
+            "apikey",
+            "authorization",
+            "clientsecret",
+            "credential",
+            "credentials",
+            "password",
+            "passwd",
+            "privatekey",
+            "rawrecords",
+            "rawrows",
+            "secret",
+            "token",
+        }
+        or any(part in value for part in _SENSITIVE_KEY_PARTS)
+        or value.startswith(("raw_", "recent_detail_", "token_"))
+        or value.endswith("_token")
+    )
+
+
+def _is_raw_row_container(key: str, value: object) -> bool:
+    return (
+        key in _RAW_ROW_CONTAINER_KEYS
+        or key.endswith(("_rows", "_records"))
+    ) and (
+        isinstance(value, Mapping)
+        or (
+            isinstance(value, (list, tuple))
+            and any(isinstance(child, Mapping) for child in value)
+        )
+    )
+
+
+def _contains_obfuscated_sql(value: str) -> bool:
+    compatible = unicodedata.normalize("NFKC", value)
+    without_comments = re.sub(
+        r"/\*.*?\*/|--[^\r\n]*", "", compatible, flags=re.DOTALL
+    )
+    normalized = re.sub(r"\s+", " ", without_comments).upper()
+    return bool(
+        re.search(
+            r"(?:\bSELECT\b.{0,4096}\bFROM\b|\bINSERT\s+INTO\b|"
+            r"\bUPDATE\b.{0,4096}\bSET\b|\bDELETE\s+FROM\b|"
+            r"\bMERGE(?:\s+INTO)?\b|\b(?:CREATE|ALTER|DROP|TRUNCATE)\s+"
+            r"(?:TABLE|VIEW|SCHEMA|DATABASE|INDEX|FUNCTION|PROCEDURE)\b|"
+            r"\b(?:GRANT|REVOKE)\b.{0,4096}\b(?:TO|FROM)\b|\bCOPY\b|"
+            r"\bEXEC(?:UTE)?\b\s+[A-Z_\[][A-Z0-9_.$\[\]]*|"
+            r"\bCALL\b\s+[A-Z_][A-Z0-9_.$]*\s*\()",
+            normalized,
+        )
+    )
+
+
+def _approved_hosts(values: set[str] | frozenset[str], label: str) -> frozenset[str]:
+    if not isinstance(values, (set, frozenset)):
+        raise ValueError(f"{label} must be an explicit set")
+    approved: set[str] = set()
+    for value in values:
+        if (
+            not isinstance(value, str)
+            or not value
+            or value != value.lower()
+            or value.endswith(".")
+            or ":" in value
+            or "/" in value
+            or "@" in value
+        ):
+            raise ValueError(f"{label} contains an invalid host")
+        try:
+            value.encode("ascii")
+            ipaddress.ip_address(value)
+        except UnicodeEncodeError as exc:
+            raise ValueError(f"{label} contains an invalid host") from exc
+        except ValueError:
+            pass
+        else:
+            raise ValueError(f"{label} cannot contain an IP literal")
+        if not re.fullmatch(
+            r"(?=.{1,253}\Z)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*"
+            r"[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?",
+            value,
+        ):
+            raise ValueError(f"{label} contains an invalid host")
+        approved.add(value)
+    return frozenset(approved)
+
+
+def _approved_origins(
+    values: set[str] | frozenset[str] | None,
+    hosts: frozenset[str],
+    label: str,
+) -> frozenset[str]:
+    if values is None:
+        return frozenset(f"https://{host}" for host in hosts)
+    if not isinstance(values, (set, frozenset)):
+        raise ValueError(f"{label} must be an explicit set")
+    origins: set[str] = set()
+    for value in values:
+        if not isinstance(value, str):
+            raise ValueError(f"{label} contains an invalid origin")
+        try:
+            parsed = urlsplit(value)
+            port = parsed.port
+        except ValueError as exc:
+            raise ValueError(f"{label} contains an invalid origin") from exc
+        if (
+            parsed.scheme != "https"
+            or parsed.hostname not in hosts
+            or parsed.username is not None
+            or parsed.password is not None
+            or parsed.path not in {"", "/"}
+            or parsed.query
+            or parsed.fragment
+            or port is None and parsed.netloc.endswith(":")
+        ):
+            raise ValueError(f"{label} contains an invalid origin")
+        effective_port = port or 443
+        canonical = (
+            f"https://{parsed.hostname}"
+            if effective_port == 443
+            else f"https://{parsed.hostname}:{effective_port}"
+        )
+        if value.rstrip("/") != canonical:
+            raise ValueError(f"{label} contains a non-canonical origin")
+        origins.add(canonical)
+    return frozenset(origins)
+
+
+def _limit_map(
+    defaults: Mapping[str, int], overrides: Mapping[str, int] | None, label: str
+) -> dict[str, int]:
+    result = dict(defaults)
+    if overrides is None:
+        return result
+    if not isinstance(overrides, Mapping):
+        raise ValueError(f"{label} must be a mapping")
+    unknown = set(overrides) - CONTROL_PLANE_ALLOWED
+    if unknown:
+        raise ValueError(f"{label} contains an unknown classification")
+    for classification, limit in overrides.items():
+        if isinstance(limit, bool) or not isinstance(limit, int) or limit < 1:
+            raise ValueError(f"{label} values must be positive integers")
+        result[classification] = limit
+    return result
+
+
+class EdgeEgressPolicy:
+    def __init__(
+        self,
+        *,
+        allowed_control_hosts: set[str] | frozenset[str],
+        allowed_proxy_hosts: set[str] | frozenset[str] | None = None,
+        allowed_control_origins: set[str] | frozenset[str] | None = None,
+        allowed_proxy_origins: set[str] | frozenset[str] | None = None,
+        byte_limits: Mapping[str, int] | None = None,
+        count_limits: Mapping[str, int] | None = None,
+        max_depth: int = 12,
+        max_string_bytes: int = 4_096,
+    ) -> None:
+        self.allowed_control_hosts = _approved_hosts(
+            allowed_control_hosts, "allowed_control_hosts"
+        )
+        self.allowed_proxy_hosts = _approved_hosts(
+            allowed_proxy_hosts or set(), "allowed_proxy_hosts"
+        )
+        self.allowed_control_origins = _approved_origins(
+            allowed_control_origins,
+            self.allowed_control_hosts,
+            "allowed_control_origins",
+        )
+        self.allowed_proxy_origins = _approved_origins(
+            allowed_proxy_origins,
+            self.allowed_proxy_hosts,
+            "allowed_proxy_origins",
+        )
+        self.byte_limits = _limit_map(
+            DEFAULT_BYTE_LIMITS, byte_limits, "byte_limits"
+        )
+        self.count_limits = _limit_map(
+            DEFAULT_COUNT_LIMITS, count_limits, "count_limits"
+        )
+        if isinstance(max_depth, bool) or not isinstance(max_depth, int) or max_depth < 1:
+            raise ValueError("max_depth must be a positive integer")
+        if (
+            isinstance(max_string_bytes, bool)
+            or not isinstance(max_string_bytes, int)
+            or max_string_bytes < 1
+        ):
+            raise ValueError("max_string_bytes must be a positive integer")
+        self.max_depth = max_depth
+        self.max_string_bytes = max_string_bytes
+
+    def approve_event(self, event: Mapping[str, object]) -> dict[str, object]:
+        if not isinstance(event, Mapping):
+            raise EdgePolicyError("event must be a mapping")
+        unknown = set(event) - {
+            "event_id",
+            "task_id",
+            "gateway_id",
+            "environment",
+            "network_zone",
+            "purpose",
+            "classification",
+            "contract_version",
+            "occurred_at",
+            "attempt",
+            "idempotency_key",
+            "policy_digest",
+            "payload",
+        }
+        if unknown:
+            raise EdgePolicyError("event contains unapproved properties")
+        classification = event.get("classification")
+        if classification in EDGE_ONLY:
+            raise EdgePolicyError("classification is edge-only")
+        if classification not in CONTROL_PLANE_ALLOWED:
+            raise EdgePolicyError("classification is not control-plane approved")
+        payload = event.get("payload")
+        if not isinstance(payload, Mapping):
+            raise EdgePolicyError("approved event payload must be a mapping")
+        self.validate_approved_payload(str(classification), payload)
+        try:
+            return dict(event)
+        except (TypeError, ValueError) as exc:  # pragma: no cover - Mapping guard
+            raise EdgePolicyError("event is invalid") from exc
+
+    def validate_approved_payload(
+        self, classification: str, payload: Mapping[str, object]
+    ) -> None:
+        if classification not in CONTROL_PLANE_ALLOWED:
+            raise EdgePolicyError("classification is not control-plane approved")
+        try:
+            preflight_json(
+                payload,
+                max_depth=self.max_depth,
+                max_nodes=self.count_limits[classification] + 1,
+                max_string_bytes=self.max_string_bytes,
+                max_total_bytes=self.byte_limits[classification],
+            )
+            encoded = strict_json_bytes(payload)
+        except EdgeContractError as exc:
+            detail = str(exc)
+            if "node count" in detail:
+                detail = "payload exceeds its independent item count limit"
+            elif "string exceeds" in detail:
+                detail = "payload string exceeds the string limit"
+            elif "depth" in detail:
+                detail = "payload exceeds the maximum depth"
+            elif "byte limit" in detail:
+                detail = "payload exceeds its independent byte limit"
+            raise EdgePolicyError(detail) from exc
+        if len(encoded) > self.byte_limits[classification]:
+            raise EdgePolicyError("payload exceeds its independent byte limit")
+        item_count = self._count_items(payload)
+        if item_count > self.count_limits[classification]:
+            raise EdgePolicyError("payload exceeds its independent item count limit")
+        self._inspect(payload, depth=1)
+
+    def _count_items(self, value: object) -> int:
+        if isinstance(value, Mapping):
+            return len(value) + sum(self._count_items(item) for item in value.values())
+        if isinstance(value, (list, tuple)):
+            return len(value) + sum(self._count_items(item) for item in value)
+        return 0
+
+    def _inspect(self, value: object, *, depth: int) -> None:
+        if depth > self.max_depth:
+            raise EdgePolicyError("payload exceeds the maximum depth")
+        if isinstance(value, Mapping):
+            for key, item in value.items():
+                if not isinstance(key, str):
+                    raise EdgePolicyError("payload contains a non-string key")
+                normalized = _normalized_key(key)
+                if _is_sensitive_key(normalized):
+                    raise EdgePolicyError("payload contains sensitive content")
+                if _is_raw_row_container(normalized, item):
+                    raise EdgePolicyError("payload contains sensitive raw-row content")
+                self._inspect(item, depth=depth + 1)
+            return
+        if isinstance(value, (list, tuple)):
+            for item in value:
+                self._inspect(item, depth=depth + 1)
+            return
+        if isinstance(value, str):
+            compatible = unicodedata.normalize("NFKC", value)
+            if len(value.encode("utf-8")) > self.max_string_bytes:
+                raise EdgePolicyError("payload string exceeds the string limit")
+            if (
+                _PRIVATE_KEY.search(compatible)
+                or _BEARER.search(compatible)
+                or _CREDENTIAL_ASSIGNMENT.search(compatible)
+                or _SQL.search(compatible)
+                or _contains_obfuscated_sql(compatible)
+            ):
+                raise EdgePolicyError("payload contains sensitive content")
+
+    def validate_destination(
+        self, control_url: str, *, proxy_url: str | None = None
+    ) -> None:
+        self._validate_url(
+            control_url,
+            approved_hosts=self.allowed_control_hosts,
+            approved_origins=self.allowed_control_origins,
+            label="destination",
+        )
+        if proxy_url is not None:
+            self._validate_url(
+                proxy_url,
+                approved_hosts=self.allowed_proxy_hosts,
+                approved_origins=self.allowed_proxy_origins,
+                label="proxy",
+            )
+
+    @staticmethod
+    def _validate_url(
+        url: str,
+        *,
+        approved_hosts: frozenset[str],
+        approved_origins: frozenset[str],
+        label: str,
+    ) -> None:
+        if not isinstance(url, str):
+            raise EdgePolicyError(f"{label} is not approved")
+        try:
+            parsed = urlsplit(url)
+            port = parsed.port
+        except ValueError as exc:
+            raise EdgePolicyError(f"{label} is not approved") from exc
+        if (
+            parsed.scheme != "https"
+            or not parsed.hostname
+            or parsed.hostname not in approved_hosts
+            or parsed.username is not None
+            or parsed.password is not None
+            or parsed.fragment
+            or port is None and parsed.netloc.endswith(":")
+        ):
+            raise EdgePolicyError(f"{label} is not approved")
+        effective_port = port or 443
+        origin = (
+            f"https://{parsed.hostname}"
+            if effective_port == 443
+            else f"https://{parsed.hostname}:{effective_port}"
+        )
+        if origin not in approved_origins:
+            raise EdgePolicyError(f"{label} origin is not approved")
+
+    @staticmethod
+    def retention_days(classification: str) -> int:
+        try:
+            return RETENTION_DAYS[classification]
+        except (KeyError, TypeError) as exc:
+            raise EdgePolicyError("retention classification is invalid") from exc

+ 2189 - 0
deployment/app/core/edge_gateway/queue.py

@@ -0,0 +1,2189 @@
+"""Durable SQLite task and outbound-event queue for an offline edge gateway."""
+
+from __future__ import annotations
+
+import hashlib
+import json
+import re
+import sqlite3
+import uuid
+from collections.abc import Callable, Iterator, Mapping
+from contextlib import contextmanager
+from dataclasses import dataclass
+from datetime import UTC, datetime, timedelta
+from pathlib import Path
+from types import MappingProxyType
+
+from .contracts import (
+    EdgeContractError,
+    EdgeEventContract,
+    EdgeTaskContract,
+    SignedTaskEnvelope,
+    canonical_sha256,
+    canonical_timestamp,
+    strict_json_bytes,
+)
+from .policy import EdgeEgressPolicy, EdgePolicyError
+
+
+class EdgeQueueError(RuntimeError):
+    """Base error for durable edge queue operations."""
+
+
+class EdgeQueueConflictError(EdgeQueueError):
+    """Raised when an existing stable ID is replayed with different content."""
+
+
+class EdgeQueueLeaseError(EdgeQueueError):
+    """Raised when a mutation is attempted without the current lease token."""
+
+
+class EdgeQueueSchemaError(EdgeQueueError):
+    """Raised when an edge queue database schema cannot be trusted."""
+
+
+SCHEMA_VERSION = 2
+
+_V1_TASK_TABLE_SQL = """CREATE TABLE edge_tasks (
+    task_id TEXT PRIMARY KEY,
+    digest TEXT NOT NULL,
+    task_json TEXT NOT NULL,
+    status TEXT NOT NULL CHECK (
+        status IN ('pending', 'leased', 'completed', 'failed', 'cancelled')
+    ),
+    attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
+    available_at TEXT NOT NULL,
+    deadline_at TEXT NOT NULL,
+    deadline_epoch_us INTEGER NOT NULL,
+    lease_owner TEXT,
+    lease_token TEXT,
+    lease_expires_at TEXT,
+    error_code TEXT,
+    created_at TEXT NOT NULL,
+    updated_at TEXT NOT NULL
+)"""
+_TASK_TABLE_SQL = _V1_TASK_TABLE_SQL.rstrip()[:-1].rstrip() + ",\n" + (
+    "    authority_key_id TEXT,\n"
+    "    authority_digest TEXT,\n"
+    "    authority_json TEXT,\n"
+    "    remote_lease_token_digest TEXT,\n"
+    "    remote_lease_ciphertext TEXT,\n"
+    "    remote_lease_expires_at TEXT,\n"
+    "    remote_attempt INTEGER CHECK (remote_attempt IS NULL OR remote_attempt >= 1)\n"
+    ")"
+)
+_TASK_INDEX_SQL = """CREATE INDEX edge_tasks_claim_idx ON edge_tasks(
+    status, deadline_epoch_us, available_at, lease_expires_at, created_at
+)"""
+_V1_EVENT_TABLE_SQL = """CREATE TABLE edge_outbound_events (
+    event_id TEXT PRIMARY KEY,
+    digest TEXT NOT NULL,
+    event_json TEXT NOT NULL,
+    status TEXT NOT NULL CHECK (
+        status IN ('pending', 'sending', 'acknowledged', 'failed')
+    ),
+    attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
+    available_at TEXT NOT NULL,
+    lease_owner TEXT,
+    lease_token TEXT,
+    lease_expires_at TEXT,
+    error_code TEXT,
+    acknowledgement_json TEXT,
+    ack_digest TEXT,
+    lease_token_digest TEXT,
+    created_at TEXT NOT NULL,
+    updated_at TEXT NOT NULL
+)"""
+_EVENT_TABLE_SQL = _V1_EVENT_TABLE_SQL.rstrip()[:-1].rstrip() + ",\n" + (
+    "    task_digest TEXT,\n"
+    "    remote_lease_token_digest TEXT,\n"
+    "    control_lease_token_digest TEXT\n"
+    ")"
+)
+_EVENT_INDEX_SQL = """CREATE INDEX edge_events_claim_idx ON edge_outbound_events(
+    status, available_at, lease_expires_at, created_at
+)"""
+_ARTIFACT_TABLE_SQL = """CREATE TABLE edge_local_artifacts (
+    artifact_digest TEXT PRIMARY KEY,
+    artifact_ref TEXT,
+    artifact_ref_hash TEXT NOT NULL,
+    classification TEXT NOT NULL CHECK (
+        classification IN ('raw', 'recent_detail', 'restricted',
+                           'desensitized_metadata', 'statistics', 'lineage', 'evidence')
+    ),
+    retention_until TEXT NOT NULL,
+    task_id TEXT NOT NULL,
+    event_id TEXT NOT NULL,
+    cleanup_status TEXT NOT NULL CHECK (
+        cleanup_status IN ('pending', 'deleting', 'deleted', 'failed')
+    ),
+    attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
+    available_at TEXT NOT NULL,
+    lease_owner TEXT,
+    lease_token TEXT,
+    lease_expires_at TEXT,
+    error_code TEXT,
+    deletion_receipt_json TEXT,
+    created_at TEXT NOT NULL,
+    updated_at TEXT NOT NULL,
+    FOREIGN KEY(task_id) REFERENCES edge_tasks(task_id),
+    FOREIGN KEY(event_id) REFERENCES edge_outbound_events(event_id)
+)"""
+_ARTIFACT_INDEX_SQL = """CREATE INDEX edge_artifacts_task_idx ON edge_local_artifacts(
+    cleanup_status, retention_until, available_at, lease_expires_at, created_at
+)"""
+_RELEASE_TABLE_SQL = """CREATE TABLE edge_release_state (
+    release_id TEXT PRIMARY KEY,
+    manifest_digest TEXT NOT NULL,
+    version TEXT NOT NULL,
+    rollback_version TEXT NOT NULL,
+    status TEXT NOT NULL CHECK (
+        status IN ('offered', 'accepted', 'candidate', 'installed', 'failed', 'rolled_back')
+    ),
+    previous_version TEXT,
+    current_version TEXT,
+    updated_at TEXT NOT NULL
+)"""
+_RELEASE_INDEX_SQL = """CREATE INDEX edge_release_status_idx ON edge_release_state(
+    status, updated_at, release_id
+)"""
+_OUTCOME_TABLE_SQL = """CREATE TABLE edge_task_outcomes (
+    task_id TEXT PRIMARY KEY,
+    digest TEXT NOT NULL,
+    outcome_json TEXT NOT NULL,
+    status TEXT NOT NULL CHECK (
+        status IN ('pending', 'sending', 'acknowledged', 'failed')
+    ),
+    attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
+    available_at TEXT NOT NULL,
+    lease_owner TEXT,
+    lease_token TEXT,
+    lease_expires_at TEXT,
+    error_code TEXT,
+    acknowledgement_json TEXT,
+    ack_digest TEXT,
+    remote_lease_token_digest TEXT NOT NULL,
+    created_at TEXT NOT NULL,
+    updated_at TEXT NOT NULL,
+    FOREIGN KEY(task_id) REFERENCES edge_tasks(task_id)
+)"""
+_OUTCOME_INDEX_SQL = """CREATE INDEX edge_outcomes_claim_idx ON edge_task_outcomes(
+    status, available_at, lease_expires_at, created_at
+)"""
+_EXPECTED_TABLE_INFO = {
+    "edge_tasks": (
+        ("task_id", "TEXT", 0, None, 1),
+        ("digest", "TEXT", 1, None, 0),
+        ("task_json", "TEXT", 1, None, 0),
+        ("status", "TEXT", 1, None, 0),
+        ("attempt_count", "INTEGER", 1, "0", 0),
+        ("available_at", "TEXT", 1, None, 0),
+        ("deadline_at", "TEXT", 1, None, 0),
+        ("deadline_epoch_us", "INTEGER", 1, None, 0),
+        ("lease_owner", "TEXT", 0, None, 0),
+        ("lease_token", "TEXT", 0, None, 0),
+        ("lease_expires_at", "TEXT", 0, None, 0),
+        ("error_code", "TEXT", 0, None, 0),
+        ("created_at", "TEXT", 1, None, 0),
+        ("updated_at", "TEXT", 1, None, 0),
+        ("authority_key_id", "TEXT", 0, None, 0),
+        ("authority_digest", "TEXT", 0, None, 0),
+        ("authority_json", "TEXT", 0, None, 0),
+        ("remote_lease_token_digest", "TEXT", 0, None, 0),
+        ("remote_lease_ciphertext", "TEXT", 0, None, 0),
+        ("remote_lease_expires_at", "TEXT", 0, None, 0),
+        ("remote_attempt", "INTEGER", 0, None, 0),
+    ),
+    "edge_outbound_events": (
+        ("event_id", "TEXT", 0, None, 1),
+        ("digest", "TEXT", 1, None, 0),
+        ("event_json", "TEXT", 1, None, 0),
+        ("status", "TEXT", 1, None, 0),
+        ("attempt_count", "INTEGER", 1, "0", 0),
+        ("available_at", "TEXT", 1, None, 0),
+        ("lease_owner", "TEXT", 0, None, 0),
+        ("lease_token", "TEXT", 0, None, 0),
+        ("lease_expires_at", "TEXT", 0, None, 0),
+        ("error_code", "TEXT", 0, None, 0),
+        ("acknowledgement_json", "TEXT", 0, None, 0),
+        ("ack_digest", "TEXT", 0, None, 0),
+        ("lease_token_digest", "TEXT", 0, None, 0),
+        ("created_at", "TEXT", 1, None, 0),
+        ("updated_at", "TEXT", 1, None, 0),
+        ("task_digest", "TEXT", 0, None, 0),
+        ("remote_lease_token_digest", "TEXT", 0, None, 0),
+        ("control_lease_token_digest", "TEXT", 0, None, 0),
+    ),
+    "edge_local_artifacts": (
+        ("artifact_digest", "TEXT", 0, None, 1),
+        ("artifact_ref", "TEXT", 0, None, 0),
+        ("artifact_ref_hash", "TEXT", 1, None, 0),
+        ("classification", "TEXT", 1, None, 0),
+        ("retention_until", "TEXT", 1, None, 0),
+        ("task_id", "TEXT", 1, None, 0),
+        ("event_id", "TEXT", 1, None, 0),
+        ("cleanup_status", "TEXT", 1, None, 0),
+        ("attempt_count", "INTEGER", 1, "0", 0),
+        ("available_at", "TEXT", 1, None, 0),
+        ("lease_owner", "TEXT", 0, None, 0),
+        ("lease_token", "TEXT", 0, None, 0),
+        ("lease_expires_at", "TEXT", 0, None, 0),
+        ("error_code", "TEXT", 0, None, 0),
+        ("deletion_receipt_json", "TEXT", 0, None, 0),
+        ("created_at", "TEXT", 1, None, 0),
+        ("updated_at", "TEXT", 1, None, 0),
+    ),
+    "edge_release_state": (
+        ("release_id", "TEXT", 0, None, 1),
+        ("manifest_digest", "TEXT", 1, None, 0),
+        ("version", "TEXT", 1, None, 0),
+        ("rollback_version", "TEXT", 1, None, 0),
+        ("status", "TEXT", 1, None, 0),
+        ("previous_version", "TEXT", 0, None, 0),
+        ("current_version", "TEXT", 0, None, 0),
+        ("updated_at", "TEXT", 1, None, 0),
+    ),
+    "edge_task_outcomes": (
+        ("task_id", "TEXT", 0, None, 1),
+        ("digest", "TEXT", 1, None, 0),
+        ("outcome_json", "TEXT", 1, None, 0),
+        ("status", "TEXT", 1, None, 0),
+        ("attempt_count", "INTEGER", 1, "0", 0),
+        ("available_at", "TEXT", 1, None, 0),
+        ("lease_owner", "TEXT", 0, None, 0),
+        ("lease_token", "TEXT", 0, None, 0),
+        ("lease_expires_at", "TEXT", 0, None, 0),
+        ("error_code", "TEXT", 0, None, 0),
+        ("acknowledgement_json", "TEXT", 0, None, 0),
+        ("ack_digest", "TEXT", 0, None, 0),
+        ("remote_lease_token_digest", "TEXT", 1, None, 0),
+        ("created_at", "TEXT", 1, None, 0),
+        ("updated_at", "TEXT", 1, None, 0),
+    ),
+}
+_EXPECTED_INDEXES = {
+    "edge_tasks": {
+        "edge_tasks_claim_idx": (
+            0,
+            "c",
+            0,
+            ("status", "deadline_epoch_us", "available_at", "lease_expires_at", "created_at"),
+        ),
+    },
+    "edge_outbound_events": {
+        "edge_events_claim_idx": (
+            0,
+            "c",
+            0,
+            ("status", "available_at", "lease_expires_at", "created_at"),
+        ),
+    },
+    "edge_local_artifacts": {
+        "edge_artifacts_task_idx": (
+            0, "c", 0,
+            ("cleanup_status", "retention_until", "available_at", "lease_expires_at", "created_at"),
+        ),
+    },
+    "edge_release_state": {
+        "edge_release_status_idx": (0, "c", 0, ("status", "updated_at", "release_id")),
+    },
+    "edge_task_outcomes": {
+        "edge_outcomes_claim_idx": (
+            0, "c", 0, ("status", "available_at", "lease_expires_at", "created_at")
+        ),
+    },
+}
+_EXPECTED_TABLE_SQL = {
+    "edge_tasks": _TASK_TABLE_SQL,
+    "edge_outbound_events": _EVENT_TABLE_SQL,
+    "edge_local_artifacts": _ARTIFACT_TABLE_SQL,
+    "edge_release_state": _RELEASE_TABLE_SQL,
+    "edge_task_outcomes": _OUTCOME_TABLE_SQL,
+}
+
+_V1_EXPECTED_TABLE_INFO = {
+    table: tuple(row for row in info if row[0] not in {
+        "authority_key_id", "authority_digest", "authority_json",
+        "remote_lease_token_digest", "remote_lease_ciphertext", "remote_lease_expires_at", "remote_attempt",
+        "task_digest", "control_lease_token_digest",
+    })
+    for table, info in _EXPECTED_TABLE_INFO.items()
+    if table not in {"edge_local_artifacts", "edge_release_state", "edge_task_outcomes"}
+}
+_V1_EXPECTED_TABLE_SQL = {
+    "edge_tasks": _V1_TASK_TABLE_SQL,
+    "edge_outbound_events": _V1_EVENT_TABLE_SQL,
+}
+
+
+@dataclass(frozen=True, slots=True)
+class QueuedTask:
+    task_id: str
+    digest: str
+    status: str
+    task: Mapping[str, object]
+    attempt_count: int
+    available_at: str
+    lease_owner: str | None
+    lease_token: str | None
+    lease_expires_at: str | None
+    error_code: str | None
+    authority_key_id: str | None
+    authority_digest: str | None
+    remote_lease_token_digest: str | None
+    remote_lease_expires_at: str | None
+    remote_attempt: int | None
+    created_at: str
+    updated_at: str
+
+
+@dataclass(frozen=True, slots=True)
+class QueuedEvent:
+    event_id: str
+    digest: str
+    status: str
+    event: Mapping[str, object]
+    attempt_count: int
+    available_at: str
+    lease_owner: str | None
+    lease_token: str | None
+    lease_expires_at: str | None
+    error_code: str | None
+    acknowledgement: Mapping[str, object] | None
+    task_digest: str | None
+    remote_lease_token_digest: str | None
+    control_lease_token_digest: str | None
+    created_at: str
+    updated_at: str
+
+
+@dataclass(frozen=True, slots=True)
+class LocalArtifact:
+    artifact_digest: str
+    artifact_ref: str | None
+    artifact_ref_hash: str
+    classification: str
+    retention_until: str
+    task_id: str
+    event_id: str
+    cleanup_status: str
+    attempt_count: int
+    available_at: str
+    lease_owner: str | None
+    lease_token: str | None
+    lease_expires_at: str | None
+    error_code: str | None
+    deletion_receipt: Mapping[str, object] | None
+    created_at: str
+    updated_at: str
+
+
+@dataclass(frozen=True, slots=True)
+class ReleaseState:
+    release_id: str
+    manifest_digest: str
+    version: str
+    rollback_version: str
+    status: str
+    previous_version: str | None
+    current_version: str | None
+    updated_at: str
+
+
+@dataclass(frozen=True, slots=True)
+class QueuedOutcome:
+    task_id: str
+    digest: str
+    status: str
+    outcome: Mapping[str, object]
+    attempt_count: int
+    available_at: str
+    lease_owner: str | None
+    lease_token: str | None
+    lease_expires_at: str | None
+    error_code: str | None
+    acknowledgement: Mapping[str, object] | None
+    remote_lease_token_digest: str
+    created_at: str
+    updated_at: str
+
+
+_ERROR_CODE = re.compile(r"^[a-z][a-z0-9_.-]{0,127}$")
+_SENSITIVE_ERROR_PARTS = ("password", "passwd", "secret", "token", "credential")
+
+
+def _utc_now(clock: Callable[[], object]) -> datetime:
+    value = clock()
+    if isinstance(value, bool):
+        raise ValueError("queue clock returned an invalid value")
+    if isinstance(value, (int, float)):
+        value = datetime.fromtimestamp(value, tz=UTC)
+    if not isinstance(value, datetime):
+        raise ValueError("queue clock must return a datetime or timestamp")
+    if value.tzinfo is None or value.utcoffset() is None:
+        raise ValueError("queue clock must return a timezone-aware value")
+    return value.astimezone(UTC)
+
+
+def _timestamp(value: datetime) -> str:
+    utc = value.astimezone(UTC)
+    canonical = utc.strftime("%Y-%m-%dT%H:%M:%S")
+    if utc.microsecond:
+        canonical += f".{utc.microsecond:06d}".rstrip("0")
+    return f"{canonical}Z"
+
+
+def _epoch_microseconds(value: datetime) -> int:
+    epoch = datetime(1970, 1, 1, tzinfo=UTC)
+    delta = value.astimezone(UTC) - epoch
+    return (
+        (delta.days * 86_400 + delta.seconds) * 1_000_000
+        + delta.microseconds
+    )
+
+
+def _normalize_schema_sql(value: str | None) -> str:
+    if not isinstance(value, str):
+        return ""
+    normalized = re.sub(r"\s+", " ", value.strip()).lower()
+    return re.sub(r"\s*([(),])\s*", r"\1", normalized)
+
+
+def _freeze_mapping(value: Mapping[str, object]) -> Mapping[str, object]:
+    def freeze(item: object) -> object:
+        if isinstance(item, dict):
+            return MappingProxyType({key: freeze(child) for key, child in item.items()})
+        if isinstance(item, list):
+            return tuple(freeze(child) for child in item)
+        return item
+
+    return freeze(dict(value))  # type: ignore[return-value]
+
+
+class SqliteEdgeQueue:
+    """An on-disk, WAL-backed queue with fenced leases and exact replay."""
+
+    def __init__(
+        self,
+        db_path: str | Path,
+        *,
+        clock: Callable[[], object] | None = None,
+        lease_seconds: int = 60,
+        max_attempts: int = 5,
+        retry_base_seconds: int = 2,
+        retry_max_seconds: int = 300,
+        busy_timeout_ms: int = 5_000,
+        egress_policy: EdgeEgressPolicy | None = None,
+        random_source: Callable[[], float] | None = None,
+        remote_lease_encoder: Callable[[str], str] | None = None,
+        remote_lease_decoder: Callable[[str], str] | None = None,
+    ) -> None:
+        if not isinstance(db_path, (str, Path)) or not str(db_path).strip():
+            raise ValueError("an explicit SQLite database path is required")
+        path_text = str(db_path)
+        if path_text == ":memory:" or "mode=memory" in path_text:
+            raise ValueError("an in-memory database is not allowed")
+        for label, value in {
+            "lease_seconds": lease_seconds,
+            "max_attempts": max_attempts,
+            "retry_base_seconds": retry_base_seconds,
+            "retry_max_seconds": retry_max_seconds,
+            "busy_timeout_ms": busy_timeout_ms,
+        }.items():
+            if isinstance(value, bool) or not isinstance(value, int) or value < 1:
+                raise ValueError(f"{label} must be a positive integer")
+        if retry_base_seconds > retry_max_seconds:
+            raise ValueError("retry_base_seconds cannot exceed retry_max_seconds")
+        self.db_path = str(Path(db_path).expanduser().resolve())
+        self._clock = clock or (lambda: datetime.now(UTC))
+        self.lease_seconds = lease_seconds
+        self.max_attempts = max_attempts
+        self.retry_base_seconds = retry_base_seconds
+        self.retry_max_seconds = retry_max_seconds
+        self.busy_timeout_ms = busy_timeout_ms
+        self.egress_policy = egress_policy or EdgeEgressPolicy(
+            allowed_control_hosts=set()
+        )
+        # The injectable source makes retry timing testable; 0.5 is neutral jitter.
+        self._random = random_source or (lambda: 0.5)
+        if (remote_lease_encoder is None) != (remote_lease_decoder is None):
+            raise ValueError("remote lease encoder and decoder must be paired")
+        self._remote_lease_encoder = remote_lease_encoder
+        self._remote_lease_decoder = remote_lease_decoder
+        Path(self.db_path).parent.mkdir(parents=True, exist_ok=True)
+        self._initialize()
+
+    def _connect(self) -> sqlite3.Connection:
+        connection = sqlite3.connect(
+            self.db_path,
+            timeout=self.busy_timeout_ms / 1_000,
+            isolation_level=None,
+        )
+        connection.row_factory = sqlite3.Row
+        connection.execute(f"PRAGMA busy_timeout = {self.busy_timeout_ms}")
+        connection.execute("PRAGMA foreign_keys = ON")
+        return connection
+
+    def _initialize(self) -> None:
+        connection = self._connect()
+        try:
+            version = connection.execute("PRAGMA user_version").fetchone()[0]
+            edge_tables = {
+                row[0]
+                for row in connection.execute(
+                    """
+                    SELECT name FROM sqlite_master
+                    WHERE type = 'table' AND name LIKE 'edge_%'
+                    """
+                ).fetchall()
+            }
+            if version == 0 and edge_tables:
+                raise EdgeQueueSchemaError("unversioned edge queue schema is unsafe")
+            if version not in {0, 1, SCHEMA_VERSION}:
+                raise EdgeQueueSchemaError("edge queue schema version is unsupported")
+            connection.execute("PRAGMA journal_mode = WAL")
+            connection.execute("PRAGMA synchronous = FULL")
+            connection.execute("BEGIN IMMEDIATE")
+            if version == 0:
+                for statement in (
+                    _TASK_TABLE_SQL,
+                    _TASK_INDEX_SQL,
+                    _EVENT_TABLE_SQL,
+                    _EVENT_INDEX_SQL,
+                    _ARTIFACT_TABLE_SQL,
+                    _ARTIFACT_INDEX_SQL,
+                    _RELEASE_TABLE_SQL,
+                    _RELEASE_INDEX_SQL,
+                    _OUTCOME_TABLE_SQL,
+                    _OUTCOME_INDEX_SQL,
+                ):
+                    connection.execute(statement)
+                connection.execute(f"PRAGMA user_version = {SCHEMA_VERSION}")
+            elif version == 1:
+                self._validate_schema(
+                    connection,
+                    expected_info=_V1_EXPECTED_TABLE_INFO,
+                    expected_sql=_V1_EXPECTED_TABLE_SQL,
+                    expected_indexes={
+                        key: _EXPECTED_INDEXES[key]
+                        for key in ("edge_tasks", "edge_outbound_events")
+                    },
+                )
+                active_unsigned = connection.execute(
+                    """SELECT EXISTS(
+                    SELECT 1 FROM edge_tasks WHERE status IN ('pending','leased')
+                    )"""
+                ).fetchone()[0]
+                unacknowledged_events = connection.execute(
+                    """SELECT EXISTS(
+                    SELECT 1 FROM edge_outbound_events WHERE status <> 'acknowledged'
+                    )"""
+                ).fetchone()[0]
+                if active_unsigned or unacknowledged_events:
+                    raise EdgeQueueSchemaError(
+                        "v1 edge queue must drain active tasks and unacknowledged events"
+                    )
+                for statement in (
+                    "ALTER TABLE edge_tasks ADD COLUMN authority_key_id TEXT",
+                    "ALTER TABLE edge_tasks ADD COLUMN authority_digest TEXT",
+                    "ALTER TABLE edge_tasks ADD COLUMN authority_json TEXT",
+                    "ALTER TABLE edge_tasks ADD COLUMN remote_lease_token_digest TEXT",
+                    "ALTER TABLE edge_tasks ADD COLUMN remote_lease_ciphertext TEXT",
+                    "ALTER TABLE edge_tasks ADD COLUMN remote_lease_expires_at TEXT",
+                    "ALTER TABLE edge_tasks ADD COLUMN remote_attempt INTEGER CHECK (remote_attempt IS NULL OR remote_attempt >= 1)",
+                    "ALTER TABLE edge_outbound_events ADD COLUMN task_digest TEXT",
+                    "ALTER TABLE edge_outbound_events ADD COLUMN remote_lease_token_digest TEXT",
+                    "ALTER TABLE edge_outbound_events ADD COLUMN control_lease_token_digest TEXT",
+                    _ARTIFACT_TABLE_SQL,
+                    _ARTIFACT_INDEX_SQL,
+                    _RELEASE_TABLE_SQL,
+                    _RELEASE_INDEX_SQL,
+                    _OUTCOME_TABLE_SQL,
+                    _OUTCOME_INDEX_SQL,
+                ):
+                    connection.execute(statement)
+                connection.execute(f"PRAGMA user_version = {SCHEMA_VERSION}")
+            self._validate_schema(connection)
+            if connection.in_transaction:
+                connection.execute("COMMIT")
+        except Exception:
+            if connection.in_transaction:
+                connection.execute("ROLLBACK")
+            raise
+        finally:
+            connection.close()
+
+    @staticmethod
+    def _validate_schema(
+        connection: sqlite3.Connection,
+        *,
+        expected_info: Mapping[str, tuple] = _EXPECTED_TABLE_INFO,
+        expected_sql: Mapping[str, str] = _EXPECTED_TABLE_SQL,
+        expected_indexes: Mapping[str, Mapping[str, tuple]] = _EXPECTED_INDEXES,
+    ) -> None:
+        primary_keys = {
+            "edge_tasks": "task_id",
+            "edge_outbound_events": "event_id",
+            "edge_local_artifacts": "artifact_digest",
+            "edge_release_state": "release_id",
+            "edge_task_outcomes": "task_id",
+        }
+        for table, table_expected_info in expected_info.items():
+            rows = connection.execute(f"PRAGMA table_info({table})").fetchall()
+            actual_info = tuple(
+                (row[1], row[2].upper(), row[3], row[4], row[5]) for row in rows
+            )
+            if actual_info != table_expected_info:
+                raise EdgeQueueSchemaError("edge queue schema columns are invalid")
+            table_sql = connection.execute(
+                "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?",
+                (table,),
+            ).fetchone()
+            if table_sql is None or _normalize_schema_sql(
+                table_sql[0]
+            ) != _normalize_schema_sql(expected_sql[table]):
+                raise EdgeQueueSchemaError(
+                    "edge queue schema constraints are invalid"
+                )
+
+            index_rows = connection.execute(f"PRAGMA index_list({table})").fetchall()
+            custom_rows = {row[1]: row for row in index_rows if row[3] == "c"}
+            table_expected_indexes = expected_indexes[table]
+            if set(custom_rows) != set(table_expected_indexes):
+                raise EdgeQueueSchemaError("edge queue schema indexes are invalid")
+            for name, (unique, origin, partial, columns) in table_expected_indexes.items():
+                row = custom_rows[name]
+                actual_columns = tuple(
+                    item[2]
+                    for item in connection.execute(
+                        f"PRAGMA index_info({name})"
+                    ).fetchall()
+                )
+                if (row[2], row[3], row[4], actual_columns) != (
+                    unique,
+                    origin,
+                    partial,
+                    columns,
+                ):
+                    raise EdgeQueueSchemaError(
+                        "edge queue schema index definition is invalid"
+                    )
+            primary_indexes = [row for row in index_rows if row[3] == "pk"]
+            if len(primary_indexes) != 1:
+                raise EdgeQueueSchemaError("edge queue schema primary key is invalid")
+            primary = primary_indexes[0]
+            primary_columns = tuple(
+                item[2]
+                for item in connection.execute(
+                    f"PRAGMA index_info({primary[1]})"
+                ).fetchall()
+            )
+            if (primary[2], primary[4], primary_columns) != (
+                1,
+                0,
+                (primary_keys[table],),
+            ):
+                raise EdgeQueueSchemaError("edge queue schema primary key is invalid")
+
+    @contextmanager
+    def _transaction(self) -> Iterator[sqlite3.Connection]:
+        connection = self._connect()
+        try:
+            connection.execute("BEGIN IMMEDIATE")
+            yield connection
+            connection.execute("COMMIT")
+        except Exception:
+            if connection.in_transaction:
+                connection.execute("ROLLBACK")
+            raise
+        finally:
+            connection.close()
+
+    def enqueue(self, task: EdgeTaskContract | Mapping[str, object]) -> QueuedTask:
+        contract = EdgeTaskContract.from_mapping(
+            task.to_mapping() if isinstance(task, EdgeTaskContract) else task
+        )
+        return self._enqueue_contract(contract, authority=None)
+
+    def enqueue_signed(self, envelope: SignedTaskEnvelope) -> QueuedTask:
+        """Atomically persist a previously verified authority envelope and task."""
+
+        if not isinstance(envelope, SignedTaskEnvelope) or not envelope.is_verified:
+            raise EdgeContractError("a verified SignedTaskEnvelope is required")
+        # Reconstruct the task so object.__setattr__ cannot bypass validation.
+        contract = EdgeTaskContract.from_mapping(envelope.task.to_mapping())
+        if envelope.contract_digest != contract.digest:
+            raise EdgeContractError("signed authority no longer binds the task")
+        return self._enqueue_contract(contract, authority=envelope)
+
+    def accept_signed_task(
+        self,
+        envelope: SignedTaskEnvelope,
+        *,
+        remote_lease_token: str,
+        remote_lease_expires_at: str,
+        remote_attempt: int,
+    ) -> QueuedTask:
+        """Atomically accept authority and an encrypted recoverable control lease."""
+
+        if self._remote_lease_encoder is None:
+            raise EdgeQueueLeaseError("secure remote lease codec is required")
+        if not isinstance(envelope, SignedTaskEnvelope) or not envelope.is_verified:
+            raise EdgeContractError("a verified SignedTaskEnvelope is required")
+        return self._enqueue_contract(
+            EdgeTaskContract.from_mapping(envelope.task.to_mapping()),
+            authority=envelope,
+            remote_binding=self._normalize_remote_binding(
+                remote_lease_token, remote_lease_expires_at, remote_attempt
+            ),
+        )
+
+    def _enqueue_contract(
+        self,
+        contract: EdgeTaskContract,
+        *,
+        authority: SignedTaskEnvelope | None,
+        remote_binding: tuple[str, str | None, str, int] | None = None,
+    ) -> QueuedTask:
+        task_mapping = contract.to_mapping()
+        encoded = strict_json_bytes(task_mapping).decode("utf-8")
+        digest = canonical_sha256(task_mapping)
+        if remote_binding is not None and remote_binding[3] != contract.attempt:
+            raise EdgeQueueLeaseError(
+                "remote lease attempt does not bind the task contract"
+            )
+        with self._transaction() as connection:
+            existing = connection.execute(
+                "SELECT * FROM edge_tasks WHERE task_id = ?", (contract.task_id,)
+            ).fetchone()
+            if existing is not None:
+                if existing["digest"] != digest:
+                    raise EdgeQueueConflictError(
+                        "task ID already exists with a different digest"
+                    )
+                if authority is not None and existing["authority_digest"] != authority.digest:
+                    raise EdgeQueueConflictError(
+                        "task ID already exists with a different authority digest"
+                    )
+                if remote_binding is not None:
+                    now = _utc_now(self._clock)
+                    old_expiry_text = existing["remote_lease_expires_at"]
+                    old_expiry = (
+                        datetime.fromisoformat(old_expiry_text.replace("Z", "+00:00"))
+                        if old_expiry_text is not None else None
+                    )
+                    if (
+                        existing["remote_lease_token_digest"] != remote_binding[0]
+                        and old_expiry is not None and old_expiry > now
+                    ):
+                        raise EdgeQueueConflictError("active remote lease binding conflicts")
+                    new_expiry = datetime.fromisoformat(remote_binding[2].replace("Z", "+00:00"))
+                    if new_expiry <= now:
+                        raise EdgeQueueLeaseError("remote lease has expired")
+                    if (
+                        existing["status"] in {"failed", "cancelled"}
+                        and not (
+                            existing["status"] == "failed"
+                            and existing["error_code"] == "execution_lease_expired"
+                        )
+                    ):
+                        raise EdgeQueueConflictError(
+                            "terminal task cannot be rebound to a remote lease"
+                        )
+                    changed_remote_lease = (
+                        existing["remote_lease_token_digest"] != remote_binding[0]
+                    )
+                    if changed_remote_lease:
+                        event_rows = connection.execute(
+                            """SELECT * FROM edge_outbound_events
+                            WHERE status IN ('pending','sending')
+                              AND json_extract(event_json,'$.task_id')=?""",
+                            (contract.task_id,),
+                        ).fetchall()
+                        for event_row in event_rows:
+                            event_mapping = json.loads(event_row["event_json"])
+                            if (
+                                event_row["task_digest"] != digest
+                                or event_row["remote_lease_token_digest"]
+                                != existing["remote_lease_token_digest"]
+                                or event_mapping.get("task_id") != contract.task_id
+                                or event_mapping.get("gateway_id") != contract.gateway_id
+                                or canonical_sha256(event_mapping) != event_row["digest"]
+                            ):
+                                raise EdgeQueueConflictError(
+                                    "pending event does not bind the task lease"
+                                )
+                        connection.execute(
+                            """UPDATE edge_outbound_events
+                            SET remote_lease_token_digest=?,updated_at=?
+                            WHERE status IN ('pending','sending')
+                              AND json_extract(event_json,'$.task_id')=?""",
+                            (
+                                remote_binding[0],
+                                _timestamp(now),
+                                contract.task_id,
+                            ),
+                        )
+                    recoverable_failure = (
+                        existing["status"] == "failed"
+                        and existing["error_code"] == "execution_lease_expired"
+                    )
+                    connection.execute(
+                        """UPDATE edge_tasks SET remote_lease_token_digest=?,
+                        remote_lease_ciphertext=?,remote_lease_expires_at=?,remote_attempt=?,
+                        status=CASE WHEN ? THEN 'pending' ELSE status END,
+                        available_at=CASE WHEN ? THEN ? ELSE available_at END,
+                        error_code=CASE WHEN ? THEN NULL ELSE error_code END,
+                        updated_at=?
+                        WHERE task_id=? AND digest=? AND authority_digest=?""",
+                        (
+                            remote_binding[0], remote_binding[1], remote_binding[2],
+                            remote_binding[3], recoverable_failure,
+                            recoverable_failure, _timestamp(now), recoverable_failure,
+                            _timestamp(now), contract.task_id,
+                            digest, authority.digest if authority is not None else None,
+                        ),
+                    )
+                    existing = connection.execute(
+                        "SELECT * FROM edge_tasks WHERE task_id=?", (contract.task_id,)
+                    ).fetchone()
+                return self._task_record(existing)
+            now = _utc_now(self._clock)
+            deadline = datetime.fromisoformat(
+                contract.deadline_at.replace("Z", "+00:00")
+            ).astimezone(UTC)
+            if deadline <= now:
+                raise EdgeContractError("task deadline has expired")
+            if remote_binding is not None:
+                remote_expiry = datetime.fromisoformat(remote_binding[2].replace("Z", "+00:00"))
+                if remote_expiry <= now or remote_expiry > deadline:
+                    raise EdgeQueueLeaseError("remote lease validity window is invalid")
+            now_text = _timestamp(now)
+            connection.execute(
+                """
+                INSERT INTO edge_tasks (
+                    task_id, digest, task_json, status, attempt_count,
+                    available_at, deadline_at, deadline_epoch_us,
+                    authority_key_id, authority_digest, authority_json,
+                    remote_lease_token_digest, remote_lease_ciphertext,
+                    remote_lease_expires_at, remote_attempt,
+                    created_at, updated_at
+                ) VALUES (?, ?, ?, 'pending', 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+                """,
+                (
+                    contract.task_id,
+                    digest,
+                    encoded,
+                    now_text,
+                    contract.deadline_at,
+                    _epoch_microseconds(deadline),
+                    authority.authority_key_id if authority is not None else None,
+                    authority.digest if authority is not None else None,
+                    strict_json_bytes(authority.to_mapping()).decode("utf-8")
+                    if authority is not None else None,
+                    remote_binding[0] if remote_binding is not None else None,
+                    remote_binding[1] if remote_binding is not None else None,
+                    remote_binding[2] if remote_binding is not None else None,
+                    remote_binding[3] if remote_binding is not None else None,
+                    now_text,
+                    now_text,
+                ),
+            )
+            row = connection.execute(
+                "SELECT * FROM edge_tasks WHERE task_id = ?", (contract.task_id,)
+            ).fetchone()
+            return self._task_record(row)
+
+    def claim(self, lease_owner: str) -> QueuedTask | None:
+        owner = self._lease_owner(lease_owner)
+        with self._transaction() as connection:
+            now = _utc_now(self._clock)
+            now_text = _timestamp(now)
+            now_epoch_us = _epoch_microseconds(now)
+            lease_token = str(uuid.uuid4())
+            expires_at = _timestamp(now + timedelta(seconds=self.lease_seconds))
+            connection.execute(
+                """
+                UPDATE edge_tasks
+                SET status = 'failed', lease_owner = NULL, lease_token = NULL,
+                    lease_expires_at = NULL,
+                    error_code = 'task_deadline_expired', updated_at = ?
+                WHERE status IN ('pending', 'leased') AND deadline_epoch_us <= ?
+                """,
+                (now_text, now_epoch_us),
+            )
+            connection.execute(
+                """
+                UPDATE edge_tasks
+                SET status = 'failed', lease_owner = NULL, lease_token = NULL,
+                    lease_expires_at = NULL,
+                    error_code = 'lease_attempts_exhausted', updated_at = ?
+                WHERE status = 'leased'
+                  AND julianday(lease_expires_at) <= julianday(?)
+                  AND attempt_count >= ?
+                """,
+                (now_text, now_text, self.max_attempts),
+            )
+            row = connection.execute(
+                """
+                SELECT * FROM edge_tasks
+                WHERE deadline_epoch_us > ? AND attempt_count < ? AND (
+                    (status = 'pending' AND julianday(available_at) <= julianday(?))
+                    OR (status = 'leased'
+                        AND julianday(lease_expires_at) <= julianday(?))
+                )
+                ORDER BY created_at, task_id
+                LIMIT 1
+                """,
+                (now_epoch_us, self.max_attempts, now_text, now_text),
+            ).fetchone()
+            if row is None:
+                return None
+            updated = connection.execute(
+                """
+                UPDATE edge_tasks
+                SET status = 'leased', attempt_count = attempt_count + 1,
+                    lease_owner = ?, lease_token = ?, lease_expires_at = ?,
+                    error_code = NULL, updated_at = ?
+                WHERE task_id = ? AND deadline_epoch_us > ?
+                  AND attempt_count = ? AND (
+                    (status = 'pending' AND julianday(available_at) <= julianday(?))
+                    OR (status = 'leased'
+                        AND julianday(lease_expires_at) <= julianday(?))
+                )
+                """,
+                (
+                    owner,
+                    lease_token,
+                    expires_at,
+                    now_text,
+                    row["task_id"],
+                    now_epoch_us,
+                    row["attempt_count"],
+                    now_text,
+                    now_text,
+                ),
+            )
+            if updated.rowcount != 1:
+                return None
+            claimed = connection.execute(
+                "SELECT * FROM edge_tasks WHERE task_id = ?", (row["task_id"],)
+            ).fetchone()
+            return self._task_record(claimed)
+
+    def bind_remote_lease(
+        self,
+        task_id: str,
+        *,
+        lease_token: str,
+        remote_lease_token: str,
+        remote_lease_expires_at: str,
+        remote_attempt: int,
+    ) -> QueuedTask:
+        """Bind control-plane authority to the signed contract attempt."""
+
+        digest, ciphertext, expiry_text, remote_attempt = self._normalize_remote_binding(
+            remote_lease_token, remote_lease_expires_at, remote_attempt
+        )
+        expiry = datetime.fromisoformat(expiry_text.replace("Z", "+00:00"))
+        with self._transaction() as connection:
+            row = self._leased_row(connection, "edge_tasks", "task_id", task_id, lease_token)
+            now = _utc_now(self._clock)
+            if expiry <= now:
+                raise EdgeQueueLeaseError("remote lease has expired")
+            task_mapping = json.loads(row["task_json"])
+            if remote_attempt != task_mapping["attempt"]:
+                raise EdgeQueueLeaseError("remote lease attempt does not bind task contract")
+            existing = row["remote_lease_token_digest"]
+            if existing is not None and (
+                existing != digest
+                or row["remote_lease_expires_at"] != expiry_text
+                or row["remote_attempt"] != remote_attempt
+            ):
+                raise EdgeQueueConflictError("remote lease binding conflicts")
+            connection.execute(
+                """
+                UPDATE edge_tasks
+                SET remote_lease_token_digest=?, remote_lease_ciphertext=?, remote_lease_expires_at=?,
+                    remote_attempt=?, updated_at=?
+                WHERE task_id=? AND status='leased' AND lease_token=?
+                """,
+                (digest, ciphertext, expiry_text, remote_attempt, _timestamp(now), task_id, lease_token),
+            )
+            updated = connection.execute(
+                "SELECT * FROM edge_tasks WHERE task_id=?", (task_id,)
+            ).fetchone()
+            return self._task_record(updated)
+
+    def recover_remote_lease(self, task_id: str) -> str:
+        """Recover a control lease through the injected secure decoder without exposing it in records."""
+
+        if self._remote_lease_decoder is None:
+            raise EdgeQueueLeaseError("secure remote lease codec is required")
+        connection = self._connect()
+        try:
+            row = connection.execute(
+                "SELECT remote_lease_ciphertext,remote_lease_token_digest FROM edge_tasks WHERE task_id=?",
+                (task_id,),
+            ).fetchone()
+        finally:
+            connection.close()
+        if row is None or row["remote_lease_ciphertext"] is None:
+            raise EdgeQueueLeaseError("recoverable remote lease does not exist")
+        try:
+            token = self._remote_lease_decoder(row["remote_lease_ciphertext"])
+        except Exception as exc:
+            raise EdgeQueueLeaseError("remote lease recovery failed") from exc
+        if not isinstance(token, str) or self._opaque_token_digest(token) != row["remote_lease_token_digest"]:
+            raise EdgeQueueLeaseError("recovered remote lease failed integrity validation")
+        return token
+
+    def complete_with_event(
+        self,
+        task_id: str,
+        *,
+        lease_token: str,
+        remote_lease_token: str,
+        event: EdgeEventContract | Mapping[str, object],
+        artifacts: list[Mapping[str, object]] | tuple[Mapping[str, object], ...] = (),
+    ) -> tuple[QueuedTask, QueuedEvent]:
+        """Atomically commit a safe event, local artifact metadata, and task terminal state."""
+
+        contract = EdgeEventContract.from_mapping(
+            event.to_mapping() if isinstance(event, EdgeEventContract) else event
+        )
+        approved = self.egress_policy.approve_event(contract.to_mapping())
+        encoded_event = strict_json_bytes(approved).decode("utf-8")
+        normalized_artifacts = [self._validate_local_artifact(item) for item in artifacts]
+        if not isinstance(remote_lease_token, str) or not remote_lease_token:
+            raise EdgeQueueLeaseError("remote lease token is invalid")
+        remote_digest = self._opaque_token_digest(remote_lease_token)
+        with self._transaction() as connection:
+            now = _utc_now(self._clock)
+            now_text = _timestamp(now)
+            row = self._leased_row(connection, "edge_tasks", "task_id", task_id, lease_token)
+            if row["deadline_epoch_us"] <= _epoch_microseconds(now):
+                raise EdgeQueueLeaseError("task deadline has expired")
+            if row["remote_lease_token_digest"] != remote_digest:
+                raise EdgeQueueLeaseError("current remote lease token is required")
+            remote_expiry = row["remote_lease_expires_at"]
+            if remote_expiry is None or datetime.fromisoformat(
+                remote_expiry.replace("Z", "+00:00")
+            ) <= now:
+                raise EdgeQueueLeaseError("remote lease has expired")
+            task_mapping = json.loads(row["task_json"])
+            if row["remote_attempt"] != task_mapping["attempt"]:
+                raise EdgeQueueLeaseError("remote lease attempt is stale")
+            for field in ("task_id", "gateway_id", "environment", "network_zone", "purpose", "policy_digest"):
+                if contract.to_mapping()[field] != task_mapping[field]:
+                    raise EdgeQueueConflictError(f"event {field} does not bind the task")
+            if contract.attempt != task_mapping["attempt"]:
+                raise EdgeQueueConflictError("event attempt does not bind the task")
+            existing = connection.execute(
+                "SELECT digest FROM edge_outbound_events WHERE event_id=?", (contract.event_id,)
+            ).fetchone()
+            if existing is not None:
+                raise EdgeQueueConflictError("event ID already exists before task completion")
+            connection.execute(
+                """
+                INSERT INTO edge_outbound_events (
+                    event_id,digest,event_json,status,attempt_count,available_at,
+                    task_digest,remote_lease_token_digest,created_at,updated_at
+                ) VALUES (?,?,?,'pending',0,?,?,?,?,?)
+                """,
+                (
+                    contract.event_id, contract.digest, encoded_event, now_text,
+                    row["digest"], remote_digest, now_text, now_text,
+                ),
+            )
+            for artifact in normalized_artifacts:
+                connection.execute(
+                    """
+                    INSERT INTO edge_local_artifacts (
+                        artifact_digest,artifact_ref,artifact_ref_hash,classification,
+                        retention_until,task_id,event_id,cleanup_status,attempt_count,
+                        available_at,created_at,updated_at
+                    ) VALUES (?,?,?,?,?,?,?,'pending',0,?,?,?)
+                    """,
+                    (
+                        artifact["artifact_digest"], artifact["artifact_ref"],
+                        artifact["artifact_ref_hash"], artifact["classification"],
+                        artifact["retention_until"], task_id, contract.event_id,
+                        now_text, now_text, now_text,
+                    ),
+                )
+            updated = connection.execute(
+                """
+                UPDATE edge_tasks SET status='completed', lease_owner=NULL,
+                    lease_token=NULL, lease_expires_at=NULL, error_code=NULL, updated_at=?
+                WHERE task_id=? AND status='leased' AND lease_token=?
+                """,
+                (now_text, task_id, lease_token),
+            )
+            if updated.rowcount != 1:
+                raise EdgeQueueLeaseError("task completion lost its lease")
+            task_row = connection.execute("SELECT * FROM edge_tasks WHERE task_id=?", (task_id,)).fetchone()
+            event_row = connection.execute("SELECT * FROM edge_outbound_events WHERE event_id=?", (contract.event_id,)).fetchone()
+            return self._task_record(task_row), self._event_record(event_row)
+
+    def list_local_artifacts(self, *, task_id: str) -> tuple[LocalArtifact, ...]:
+        connection = self._connect()
+        try:
+            rows = connection.execute(
+                "SELECT * FROM edge_local_artifacts WHERE task_id=? ORDER BY created_at,artifact_digest",
+                (task_id,),
+            ).fetchall()
+        finally:
+            connection.close()
+        return tuple(self._artifact_record(row) for row in rows)
+
+    def claim_artifact_cleanup(self, lease_owner: str) -> LocalArtifact | None:
+        owner = self._lease_owner(lease_owner)
+        with self._transaction() as connection:
+            now = _utc_now(self._clock)
+            now_text = _timestamp(now)
+            expires_at = _timestamp(now + timedelta(seconds=self.lease_seconds))
+            connection.execute(
+                """UPDATE edge_local_artifacts SET cleanup_status='failed',
+                lease_owner=NULL,lease_token=NULL,lease_expires_at=NULL,
+                error_code='lease_attempts_exhausted',updated_at=?
+                WHERE cleanup_status='deleting'
+                AND julianday(lease_expires_at)<=julianday(?) AND attempt_count>=?""",
+                (now_text, now_text, self.max_attempts),
+            )
+            row = connection.execute(
+                """SELECT * FROM edge_local_artifacts WHERE attempt_count<?
+                AND julianday(retention_until)<=julianday(?) AND (
+                  (cleanup_status='pending' AND julianday(available_at)<=julianday(?))
+                  OR (cleanup_status='deleting'
+                      AND julianday(lease_expires_at)<=julianday(?)))
+                ORDER BY retention_until,created_at,artifact_digest LIMIT 1""",
+                (self.max_attempts, now_text, now_text, now_text),
+            ).fetchone()
+            if row is None:
+                return None
+            lease_token = str(uuid.uuid4())
+            connection.execute(
+                """UPDATE edge_local_artifacts SET cleanup_status='deleting',
+                attempt_count=attempt_count+1,lease_owner=?,lease_token=?,
+                lease_expires_at=?,error_code=NULL,updated_at=?
+                WHERE artifact_digest=? AND attempt_count=? AND (
+                  (cleanup_status='pending' AND julianday(available_at)<=julianday(?))
+                  OR (cleanup_status='deleting'
+                      AND julianday(lease_expires_at)<=julianday(?)))""",
+                (
+                    owner, lease_token, expires_at, now_text, row["artifact_digest"],
+                    row["attempt_count"], now_text, now_text,
+                ),
+            )
+            claimed = connection.execute(
+                "SELECT * FROM edge_local_artifacts WHERE artifact_digest=?",
+                (row["artifact_digest"],),
+            ).fetchone()
+            return self._artifact_record(claimed)
+
+    def acknowledge_artifact_cleanup(
+        self,
+        artifact_digest: str,
+        *,
+        lease_token: str,
+        receipt: Mapping[str, object],
+    ) -> LocalArtifact:
+        required = {"artifact_digest", "artifact_ref_hash", "deleted_at", "status"}
+        with self._transaction() as connection:
+            row = self._leased_row(
+                connection, "edge_local_artifacts", "artifact_digest",
+                artifact_digest, lease_token, status="deleting",
+                status_column="cleanup_status",
+            )
+            if (
+                not isinstance(receipt, Mapping)
+                or set(receipt) != required
+                or receipt.get("artifact_digest") != artifact_digest
+                or receipt.get("artifact_ref_hash") != row["artifact_ref_hash"]
+                or receipt.get("status") != "deleted"
+            ):
+                raise EdgePolicyError("artifact deletion receipt is invalid")
+            try:
+                deleted_at = canonical_timestamp(receipt["deleted_at"], "deleted_at")
+            except EdgeContractError as exc:
+                raise EdgePolicyError("artifact deletion receipt is invalid") from exc
+            normalized = {
+                "artifact_digest": artifact_digest,
+                "artifact_ref_hash": row["artifact_ref_hash"],
+                "deleted_at": deleted_at,
+                "status": "deleted",
+            }
+            now_text = _timestamp(_utc_now(self._clock))
+            connection.execute(
+                """UPDATE edge_local_artifacts SET artifact_ref=NULL,
+                cleanup_status='deleted',deletion_receipt_json=?,lease_owner=NULL,
+                lease_token=NULL,lease_expires_at=NULL,error_code=NULL,updated_at=?
+                WHERE artifact_digest=? AND cleanup_status='deleting' AND lease_token=?""",
+                (
+                    strict_json_bytes(normalized).decode("utf-8"), now_text,
+                    artifact_digest, lease_token,
+                ),
+            )
+            updated = connection.execute(
+                "SELECT * FROM edge_local_artifacts WHERE artifact_digest=?",
+                (artifact_digest,),
+            ).fetchone()
+            return self._artifact_record(updated)
+
+    def fail_artifact_cleanup(
+        self, artifact_digest: str, *, lease_token: str, error_code: str
+    ) -> LocalArtifact:
+        safe_error = self._error_code(error_code)
+        with self._transaction() as connection:
+            now = _utc_now(self._clock)
+            now_text = _timestamp(now)
+            row = self._leased_row(
+                connection, "edge_local_artifacts", "artifact_digest",
+                artifact_digest, lease_token, status="deleting",
+                status_column="cleanup_status",
+            )
+            terminal = row["attempt_count"] >= self.max_attempts
+            status = "failed" if terminal else "pending"
+            available_at = now_text if terminal else _timestamp(
+                now + timedelta(seconds=self._retry_delay(row["attempt_count"]))
+            )
+            connection.execute(
+                """UPDATE edge_local_artifacts SET cleanup_status=?,available_at=?,
+                lease_owner=NULL,lease_token=NULL,lease_expires_at=NULL,error_code=?,
+                updated_at=? WHERE artifact_digest=? AND cleanup_status='deleting'
+                AND lease_token=?""",
+                (
+                    status, available_at, safe_error, now_text,
+                    artifact_digest, lease_token,
+                ),
+            )
+            updated = connection.execute(
+                "SELECT * FROM edge_local_artifacts WHERE artifact_digest=?",
+                (artifact_digest,),
+            ).fetchone()
+            return self._artifact_record(updated)
+
+    def put_release_state(
+        self,
+        *,
+        release_id: str,
+        manifest_digest: str,
+        version: str,
+        rollback_version: str,
+    ) -> ReleaseState:
+        release_id = self._bounded_identifier(release_id, "release_id")
+        manifest_digest = self._sha256(manifest_digest, "manifest_digest")
+        version = self._bounded_identifier(version, "version")
+        rollback_version = self._bounded_identifier(rollback_version, "rollback_version")
+        with self._transaction() as connection:
+            existing = connection.execute(
+                "SELECT * FROM edge_release_state WHERE release_id=?", (release_id,)
+            ).fetchone()
+            if existing is not None:
+                if (
+                    existing["manifest_digest"], existing["version"], existing["rollback_version"]
+                ) != (manifest_digest, version, rollback_version):
+                    raise EdgeQueueConflictError("release ID has different immutable content")
+                return ReleaseState(**dict(existing))
+            now_text = _timestamp(_utc_now(self._clock))
+            connection.execute(
+                """INSERT INTO edge_release_state
+                (release_id,manifest_digest,version,rollback_version,status,updated_at)
+                VALUES (?,?,?,?,'offered',?)""",
+                (release_id, manifest_digest, version, rollback_version, now_text),
+            )
+            row = connection.execute(
+                "SELECT * FROM edge_release_state WHERE release_id=?", (release_id,)
+            ).fetchone()
+            return ReleaseState(**dict(row))
+
+    def compare_and_set_release_state(
+        self,
+        release_id: str,
+        *,
+        expected_status: str,
+        target_status: str,
+        previous_version: str | None = None,
+        current_version: str | None = None,
+    ) -> ReleaseState:
+        release_id = self._bounded_identifier(release_id, "release_id")
+        allowed = {"offered", "accepted", "candidate", "installed", "failed", "rolled_back"}
+        if expected_status not in allowed or target_status not in allowed:
+            raise EdgeQueueConflictError("release status is invalid")
+        transitions = {
+            "offered": {"accepted", "failed"},
+            "accepted": {"candidate", "failed"},
+            "candidate": {"installed", "failed", "rolled_back"},
+            "installed": {"rolled_back", "failed"},
+            "failed": {"rolled_back"},
+            "rolled_back": set(),
+        }
+        if target_status not in transitions[expected_status]:
+            raise EdgeQueueConflictError("release state transition is not approved")
+        if previous_version is not None:
+            previous_version = self._bounded_identifier(previous_version, "previous_version")
+        if current_version is not None:
+            current_version = self._bounded_identifier(current_version, "current_version")
+        with self._transaction() as connection:
+            now_text = _timestamp(_utc_now(self._clock))
+            updated = connection.execute(
+                """UPDATE edge_release_state
+                SET status=?,previous_version=COALESCE(?,previous_version),
+                    current_version=COALESCE(?,current_version),updated_at=?
+                WHERE release_id=? AND status=?""",
+                (
+                    target_status, previous_version, current_version, now_text,
+                    release_id, expected_status,
+                ),
+            )
+            if updated.rowcount != 1:
+                row = connection.execute(
+                    "SELECT status FROM edge_release_state WHERE release_id=?", (release_id,)
+                ).fetchone()
+                if row is None:
+                    raise EdgeQueueError("release does not exist")
+                raise EdgeQueueConflictError("release state compare-and-set lost")
+            row = connection.execute(
+                "SELECT * FROM edge_release_state WHERE release_id=?", (release_id,)
+            ).fetchone()
+            return ReleaseState(**dict(row))
+
+    def get_release_state(self, release_id: str) -> ReleaseState | None:
+        release_id = self._bounded_identifier(release_id, "release_id")
+        connection = self._connect()
+        try:
+            row = connection.execute(
+                "SELECT * FROM edge_release_state WHERE release_id=?", (release_id,)
+            ).fetchone()
+        finally:
+            connection.close()
+        return ReleaseState(**dict(row)) if row is not None else None
+
+    def list_release_states(self) -> tuple[ReleaseState, ...]:
+        connection = self._connect()
+        try:
+            rows = connection.execute(
+                "SELECT * FROM edge_release_state ORDER BY updated_at,release_id"
+            ).fetchall()
+        finally:
+            connection.close()
+        return tuple(ReleaseState(**dict(row)) for row in rows)
+
+    def complete(self, task_id: str, *, lease_token: str) -> QueuedTask:
+        return self._finish_task(
+            task_id,
+            lease_token=lease_token,
+            target_status="completed",
+            error_code=None,
+        )
+
+    def fail(
+        self,
+        task_id: str,
+        *,
+        lease_token: str,
+        error_code: str,
+        retryable: bool = True,
+    ) -> QueuedTask:
+        safe_error = self._error_code(error_code)
+        if not isinstance(retryable, bool):
+            raise ValueError("retryable must be a boolean")
+        with self._transaction() as connection:
+            now = _utc_now(self._clock)
+            now_text = _timestamp(now)
+            row = self._leased_row(
+                connection, "edge_tasks", "task_id", task_id, lease_token
+            )
+            terminal = not retryable or row["attempt_count"] >= self.max_attempts
+            status = "failed" if terminal else "pending"
+            available_at = now_text
+            if not terminal:
+                available_at = _timestamp(
+                    now + timedelta(seconds=self._retry_delay(row["attempt_count"]))
+                )
+            connection.execute(
+                """
+                UPDATE edge_tasks
+                SET status = ?, available_at = ?, lease_owner = NULL,
+                    lease_token = NULL, lease_expires_at = NULL,
+                    error_code = ?, updated_at = ?
+                WHERE task_id = ? AND status = 'leased' AND lease_token = ?
+                """,
+                (status, available_at, safe_error, now_text, task_id, lease_token),
+            )
+            updated = connection.execute(
+                "SELECT * FROM edge_tasks WHERE task_id = ?", (task_id,)
+            ).fetchone()
+            return self._task_record(updated)
+
+    def _finish_task(
+        self,
+        task_id: str,
+        *,
+        lease_token: str,
+        target_status: str,
+        error_code: str | None,
+    ) -> QueuedTask:
+        with self._transaction() as connection:
+            now_text = _timestamp(_utc_now(self._clock))
+            self._leased_row(
+                connection, "edge_tasks", "task_id", task_id, lease_token
+            )
+            connection.execute(
+                """
+                UPDATE edge_tasks
+                SET status = ?, lease_owner = NULL, lease_token = NULL,
+                    lease_expires_at = NULL, error_code = ?, updated_at = ?
+                WHERE task_id = ? AND status = 'leased' AND lease_token = ?
+                """,
+                (target_status, error_code, now_text, task_id, lease_token),
+            )
+            row = connection.execute(
+                "SELECT * FROM edge_tasks WHERE task_id = ?", (task_id,)
+            ).fetchone()
+            return self._task_record(row)
+
+    def cancel(self, task_id: str) -> QueuedTask:
+        with self._transaction() as connection:
+            now_text = _timestamp(_utc_now(self._clock))
+            row = connection.execute(
+                "SELECT * FROM edge_tasks WHERE task_id = ?", (task_id,)
+            ).fetchone()
+            if row is None:
+                raise EdgeQueueError("task does not exist")
+            if row["status"] in {"pending", "leased"}:
+                connection.execute(
+                    """
+                    UPDATE edge_tasks
+                    SET status = 'cancelled', lease_owner = NULL,
+                        lease_token = NULL, lease_expires_at = NULL,
+                        updated_at = ?
+                    WHERE task_id = ? AND status IN ('pending', 'leased')
+                    """,
+                    (now_text, task_id),
+                )
+                row = connection.execute(
+                    "SELECT * FROM edge_tasks WHERE task_id = ?", (task_id,)
+                ).fetchone()
+            return self._task_record(row)
+
+    def cancel_with_outcome(self, task_id: str) -> QueuedOutcome:
+        """Atomically fence local work and persist the cancelled control outcome."""
+
+        task_id = self._bounded_identifier(task_id, "task_id")
+        with self._transaction() as connection:
+            now_text = _timestamp(_utc_now(self._clock))
+            row = connection.execute(
+                "SELECT * FROM edge_tasks WHERE task_id = ?", (task_id,)
+            ).fetchone()
+            if row is None:
+                raise EdgeQueueError("task does not exist")
+            if row["status"] in {"pending", "leased"}:
+                connection.execute(
+                    """UPDATE edge_tasks SET status='cancelled',lease_owner=NULL,
+                    lease_token=NULL,lease_expires_at=NULL,updated_at=?
+                    WHERE task_id=? AND status IN ('pending','leased')""",
+                    (now_text, task_id),
+                )
+                row = connection.execute(
+                    "SELECT * FROM edge_tasks WHERE task_id=?", (task_id,)
+                ).fetchone()
+            if row["status"] != "cancelled":
+                raise EdgeQueueConflictError("only a cancelled task can emit cancellation")
+            remote_digest = row["remote_lease_token_digest"]
+            if not isinstance(remote_digest, str):
+                raise EdgeQueueLeaseError("cancel outcome requires a remote lease binding")
+            outcome = {
+                "task_id": task_id,
+                "outcome": "cancelled",
+                "safe_summary": {"task_status": "cancelled"},
+            }
+            digest = canonical_sha256(outcome)
+            encoded = strict_json_bytes(outcome).decode("utf-8")
+            existing = connection.execute(
+                "SELECT * FROM edge_task_outcomes WHERE task_id=?", (task_id,)
+            ).fetchone()
+            if existing is not None:
+                if (
+                    existing["digest"] != digest
+                    or existing["remote_lease_token_digest"] != remote_digest
+                ):
+                    raise EdgeQueueConflictError("cancel outcome replay conflicts")
+                return self._outcome_record(existing)
+            connection.execute(
+                """INSERT INTO edge_task_outcomes(
+                task_id,digest,outcome_json,status,attempt_count,available_at,
+                remote_lease_token_digest,created_at,updated_at
+                ) VALUES (?,?,?,'pending',0,?,?,?,?)""",
+                (task_id, digest, encoded, now_text, remote_digest, now_text, now_text),
+            )
+            inserted = connection.execute(
+                "SELECT * FROM edge_task_outcomes WHERE task_id=?", (task_id,)
+            ).fetchone()
+            return self._outcome_record(inserted)
+
+    def claim_outcome(self, lease_owner: str) -> QueuedOutcome | None:
+        owner = self._lease_owner(lease_owner)
+        with self._transaction() as connection:
+            now = _utc_now(self._clock)
+            now_text = _timestamp(now)
+            expires_at = _timestamp(now + timedelta(seconds=self.lease_seconds))
+            connection.execute(
+                """UPDATE edge_task_outcomes SET status='failed',lease_owner=NULL,
+                lease_token=NULL,lease_expires_at=NULL,error_code='lease_attempts_exhausted',
+                updated_at=? WHERE status='sending'
+                AND julianday(lease_expires_at)<=julianday(?) AND attempt_count>=?""",
+                (now_text, now_text, self.max_attempts),
+            )
+            row = connection.execute(
+                """SELECT * FROM edge_task_outcomes WHERE attempt_count<? AND (
+                (status='pending' AND julianday(available_at)<=julianday(?)) OR
+                (status='sending' AND julianday(lease_expires_at)<=julianday(?)))
+                ORDER BY created_at,task_id LIMIT 1""",
+                (self.max_attempts, now_text, now_text),
+            ).fetchone()
+            if row is None:
+                return None
+            lease_token = str(uuid.uuid4())
+            connection.execute(
+                """UPDATE edge_task_outcomes SET status='sending',
+                attempt_count=attempt_count+1,lease_owner=?,lease_token=?,
+                lease_expires_at=?,error_code=NULL,updated_at=?
+                WHERE task_id=? AND attempt_count=? AND (
+                (status='pending' AND julianday(available_at)<=julianday(?)) OR
+                (status='sending' AND julianday(lease_expires_at)<=julianday(?)))""",
+                (
+                    owner, lease_token, expires_at, now_text, row["task_id"],
+                    row["attempt_count"], now_text, now_text,
+                ),
+            )
+            claimed = connection.execute(
+                "SELECT * FROM edge_task_outcomes WHERE task_id=?", (row["task_id"],)
+            ).fetchone()
+            return self._outcome_record(claimed)
+
+    def acknowledge_outcome(
+        self,
+        task_id: str,
+        *,
+        lease_token: str,
+        acknowledgement: Mapping[str, object],
+    ) -> QueuedOutcome:
+        required = {"task_id", "status", "replayed"}
+        if (
+            not isinstance(acknowledgement, Mapping)
+            or set(acknowledgement) != required
+            or acknowledgement.get("task_id") != task_id
+            or acknowledgement.get("status") != "cancelled"
+            or not isinstance(acknowledgement.get("replayed"), bool)
+        ):
+            raise EdgePolicyError("cancel outcome acknowledgement is invalid")
+        encoded = strict_json_bytes(acknowledgement).decode("utf-8")
+        ack_digest = canonical_sha256(acknowledgement)
+        with self._transaction() as connection:
+            now_text = _timestamp(_utc_now(self._clock))
+            self._leased_row(
+                connection, "edge_task_outcomes", "task_id", task_id, lease_token,
+                status="sending",
+            )
+            connection.execute(
+                """UPDATE edge_task_outcomes SET status='acknowledged',
+                acknowledgement_json=?,ack_digest=?,lease_owner=NULL,lease_token=NULL,
+                lease_expires_at=NULL,updated_at=?
+                WHERE task_id=? AND status='sending' AND lease_token=?""",
+                (encoded, ack_digest, now_text, task_id, lease_token),
+            )
+            row = connection.execute(
+                "SELECT * FROM edge_task_outcomes WHERE task_id=?", (task_id,)
+            ).fetchone()
+            return self._outcome_record(row)
+
+    def fail_outcome(
+        self, task_id: str, *, lease_token: str, error_code: str
+    ) -> QueuedOutcome:
+        safe_error = self._error_code(error_code)
+        with self._transaction() as connection:
+            now = _utc_now(self._clock)
+            now_text = _timestamp(now)
+            row = self._leased_row(
+                connection, "edge_task_outcomes", "task_id", task_id, lease_token,
+                status="sending",
+            )
+            terminal = row["attempt_count"] >= self.max_attempts
+            status = "failed" if terminal else "pending"
+            available_at = now_text if terminal else _timestamp(
+                now + timedelta(seconds=self._retry_delay(row["attempt_count"]))
+            )
+            connection.execute(
+                """UPDATE edge_task_outcomes SET status=?,available_at=?,lease_owner=NULL,
+                lease_token=NULL,lease_expires_at=NULL,error_code=?,updated_at=?
+                WHERE task_id=? AND status='sending' AND lease_token=?""",
+                (
+                    status, available_at, safe_error, now_text, task_id, lease_token,
+                ),
+            )
+            updated = connection.execute(
+                "SELECT * FROM edge_task_outcomes WHERE task_id=?", (task_id,)
+            ).fetchone()
+            return self._outcome_record(updated)
+
+    def get_outcome(self, task_id: str) -> QueuedOutcome | None:
+        connection = self._connect()
+        try:
+            row = connection.execute(
+                "SELECT * FROM edge_task_outcomes WHERE task_id=?", (task_id,)
+            ).fetchone()
+        finally:
+            connection.close()
+        return self._outcome_record(row) if row is not None else None
+
+    def get_task(self, task_id: str) -> QueuedTask | None:
+        connection = self._connect()
+        try:
+            row = connection.execute(
+                "SELECT * FROM edge_tasks WHERE task_id = ?", (task_id,)
+            ).fetchone()
+        finally:
+            connection.close()
+        return self._task_record(row) if row is not None else None
+
+    def persist_event(
+        self, event: EdgeEventContract | Mapping[str, object]
+    ) -> QueuedEvent:
+        raw = event.to_mapping() if isinstance(event, EdgeEventContract) else dict(event)
+        try:
+            contract = EdgeEventContract.from_mapping(raw)
+        except EdgeContractError as exc:
+            raw_id = raw.get("event_id")
+            if isinstance(raw_id, str) and raw_id:
+                with self._transaction() as connection:
+                    existing = connection.execute(
+                        "SELECT 1 FROM edge_outbound_events WHERE event_id = ?",
+                        (raw_id,),
+                    ).fetchone()
+                if existing is not None:
+                    raise EdgeQueueConflictError(
+                        "event ID already exists with a different digest"
+                    ) from exc
+            raise
+        normalized = contract.to_mapping()
+        digest = contract.digest
+        with self._transaction() as connection:
+            now_text = _timestamp(_utc_now(self._clock))
+            existing = connection.execute(
+                "SELECT * FROM edge_outbound_events WHERE event_id = ?",
+                (contract.event_id,),
+            ).fetchone()
+            if existing is not None:
+                if existing["digest"] != digest:
+                    raise EdgeQueueConflictError(
+                        "event ID already exists with a different digest"
+                    )
+                return self._event_record(existing)
+            approved = self.egress_policy.approve_event(normalized)
+            encoded = strict_json_bytes(approved).decode("utf-8")
+            connection.execute(
+                """
+                INSERT INTO edge_outbound_events (
+                    event_id, digest, event_json, status, attempt_count,
+                    available_at, created_at, updated_at
+                ) VALUES (?, ?, ?, 'pending', 0, ?, ?, ?)
+                """,
+                (contract.event_id, digest, encoded, now_text, now_text, now_text),
+            )
+            row = connection.execute(
+                "SELECT * FROM edge_outbound_events WHERE event_id = ?",
+                (contract.event_id,),
+            ).fetchone()
+            return self._event_record(row)
+
+    def claim_event(self, lease_owner: str) -> QueuedEvent | None:
+        owner = self._lease_owner(lease_owner)
+        with self._transaction() as connection:
+            now = _utc_now(self._clock)
+            now_text = _timestamp(now)
+            lease_token = str(uuid.uuid4())
+            expires_at = _timestamp(now + timedelta(seconds=self.lease_seconds))
+            connection.execute(
+                """
+                UPDATE edge_outbound_events
+                SET status = 'failed', lease_owner = NULL, lease_token = NULL,
+                    lease_expires_at = NULL,
+                    error_code = 'lease_attempts_exhausted', updated_at = ?
+                WHERE status = 'sending'
+                  AND julianday(lease_expires_at) <= julianday(?)
+                  AND attempt_count >= ?
+                """,
+                (now_text, now_text, self.max_attempts),
+            )
+            row = connection.execute(
+                """
+                SELECT * FROM edge_outbound_events
+                WHERE attempt_count < ? AND (
+                    (status = 'pending' AND julianday(available_at) <= julianday(?))
+                    OR (status = 'sending'
+                        AND julianday(lease_expires_at) <= julianday(?))
+                )
+                ORDER BY created_at, event_id
+                LIMIT 1
+                """,
+                (self.max_attempts, now_text, now_text),
+            ).fetchone()
+            if row is None:
+                return None
+            connection.execute(
+                """
+                UPDATE edge_outbound_events
+                SET status = 'sending', attempt_count = attempt_count + 1,
+                    lease_owner = ?, lease_token = ?, lease_expires_at = ?,
+                    error_code = NULL, updated_at = ?
+                WHERE event_id = ? AND attempt_count = ? AND (
+                    (status = 'pending' AND julianday(available_at) <= julianday(?))
+                    OR (status = 'sending'
+                        AND julianday(lease_expires_at) <= julianday(?))
+                )
+                """,
+                (
+                    owner,
+                    lease_token,
+                    expires_at,
+                    now_text,
+                    row["event_id"],
+                    row["attempt_count"],
+                    now_text,
+                    now_text,
+                ),
+            )
+            claimed = connection.execute(
+                "SELECT * FROM edge_outbound_events WHERE event_id = ?",
+                (row["event_id"],),
+            ).fetchone()
+            return self._event_record(claimed)
+
+    def acknowledge_event(
+        self,
+        event_id: str,
+        *,
+        lease_token: str,
+        acknowledgement: Mapping[str, object],
+    ) -> QueuedEvent:
+        token_digest = canonical_sha256(lease_token)
+        with self._transaction() as connection:
+            now_text = _timestamp(_utc_now(self._clock))
+            existing = connection.execute(
+                "SELECT * FROM edge_outbound_events WHERE event_id = ?", (event_id,)
+            ).fetchone()
+            if existing is None:
+                raise EdgeQueueError("event does not exist")
+            normalized_ack = self._validate_acknowledgement(
+                acknowledgement,
+                event_id=event_id,
+                event_digest=existing["digest"],
+                remote_lease_token_digest=existing["remote_lease_token_digest"],
+            )
+            encoded = strict_json_bytes(normalized_ack)
+            if len(encoded) > 4_096:
+                raise EdgePolicyError("acknowledgement exceeds the byte limit")
+            ack_digest = canonical_sha256(normalized_ack)
+            if existing["status"] == "acknowledged":
+                if existing["lease_token_digest"] != token_digest:
+                    raise EdgeQueueLeaseError(
+                        "acknowledgement replay requires the original lease token"
+                    )
+                if existing["ack_digest"] != ack_digest:
+                    raise EdgeQueueConflictError(
+                        "acknowledgement replay has a different digest"
+                    )
+                return self._event_record(existing)
+            self._leased_row(
+                connection,
+                "edge_outbound_events",
+                "event_id",
+                event_id,
+                lease_token,
+                status="sending",
+            )
+            connection.execute(
+                """
+                UPDATE edge_outbound_events
+                SET status = 'acknowledged', acknowledgement_json = ?,
+                    ack_digest = ?, lease_token_digest = ?,
+                    control_lease_token_digest = ?,
+                    lease_owner = NULL, lease_token = NULL,
+                    lease_expires_at = NULL, updated_at = ?
+                WHERE event_id = ? AND status = 'sending' AND lease_token = ?
+                """,
+                (
+                    encoded.decode("utf-8"),
+                    ack_digest,
+                    token_digest,
+                    token_digest,
+                    now_text,
+                    event_id,
+                    lease_token,
+                ),
+            )
+            row = connection.execute(
+                "SELECT * FROM edge_outbound_events WHERE event_id = ?", (event_id,)
+            ).fetchone()
+            return self._event_record(row)
+
+    def fail_event(
+        self, event_id: str, *, lease_token: str, error_code: str
+    ) -> QueuedEvent:
+        safe_error = self._error_code(error_code)
+        with self._transaction() as connection:
+            now = _utc_now(self._clock)
+            now_text = _timestamp(now)
+            row = self._leased_row(
+                connection,
+                "edge_outbound_events",
+                "event_id",
+                event_id,
+                lease_token,
+                status="sending",
+            )
+            terminal = row["attempt_count"] >= self.max_attempts
+            status = "failed" if terminal else "pending"
+            available_at = now_text
+            if not terminal:
+                available_at = _timestamp(
+                    now + timedelta(seconds=self._retry_delay(row["attempt_count"]))
+                )
+            connection.execute(
+                """
+                UPDATE edge_outbound_events
+                SET status = ?, available_at = ?, lease_owner = NULL,
+                    lease_token = NULL, lease_expires_at = NULL,
+                    error_code = ?, updated_at = ?
+                WHERE event_id = ? AND status = 'sending' AND lease_token = ?
+                """,
+                (status, available_at, safe_error, now_text, event_id, lease_token),
+            )
+            updated = connection.execute(
+                "SELECT * FROM edge_outbound_events WHERE event_id = ?", (event_id,)
+            ).fetchone()
+            return self._event_record(updated)
+
+    def get_event(self, event_id: str) -> QueuedEvent | None:
+        connection = self._connect()
+        try:
+            row = connection.execute(
+                "SELECT * FROM edge_outbound_events WHERE event_id = ?", (event_id,)
+            ).fetchone()
+        finally:
+            connection.close()
+        return self._event_record(row) if row is not None else None
+
+    @staticmethod
+    def _validate_local_artifact(value: Mapping[str, object]) -> dict[str, str]:
+        required = {
+            "artifact_digest", "artifact_ref", "artifact_ref_hash",
+            "classification", "retention_until",
+        }
+        if not isinstance(value, Mapping) or set(value) != required:
+            raise EdgePolicyError("local artifact requires exact metadata fields")
+        digest = value["artifact_digest"]
+        reference = value["artifact_ref"]
+        reference_hash = value["artifact_ref_hash"]
+        classification = value["classification"]
+        if not isinstance(digest, str) or not re.fullmatch(r"[0-9a-f]{64}", digest):
+            raise EdgePolicyError("local artifact digest is invalid")
+        if (
+            not isinstance(reference, str)
+            or not Path(reference).is_absolute()
+            or len(reference.encode("utf-8")) > 2_048
+            or not isinstance(reference_hash, str)
+            or not re.fullmatch(r"[0-9a-f]{64}", reference_hash)
+            or reference_hash != canonical_sha256(reference)
+        ):
+            raise EdgePolicyError("local artifact reference is invalid")
+        if classification not in {
+            "raw", "recent_detail", "restricted", "desensitized_metadata",
+            "statistics", "lineage", "evidence",
+        }:
+            raise EdgePolicyError("local artifact classification is invalid")
+        try:
+            retention = canonical_timestamp(value["retention_until"], "retention_until")
+        except EdgeContractError as exc:
+            raise EdgePolicyError("local artifact retention is invalid") from exc
+        return {
+            "artifact_digest": digest,
+            "artifact_ref": reference,  # type: ignore[dict-item]
+            "artifact_ref_hash": reference_hash,  # type: ignore[dict-item]
+            "classification": classification,  # type: ignore[dict-item]
+            "retention_until": retention,
+        }
+
+    def _normalize_remote_binding(
+        self, token: object, expires_at: object, attempt: object
+    ) -> tuple[str, str | None, str, int]:
+        if (
+            not isinstance(token, str) or not token.strip()
+            or len(token.encode("utf-8")) > 1024
+        ):
+            raise EdgeQueueLeaseError("remote lease token is invalid")
+        if isinstance(attempt, bool) or not isinstance(attempt, int) or attempt < 1:
+            raise EdgeQueueLeaseError("remote lease attempt is invalid")
+        try:
+            expiry = canonical_timestamp(expires_at, "remote_lease_expires_at")
+        except EdgeContractError as exc:
+            raise EdgeQueueLeaseError("remote lease expiry is invalid") from exc
+        ciphertext: str | None = None
+        if self._remote_lease_encoder is not None:
+            try:
+                ciphertext = self._remote_lease_encoder(token)
+            except Exception as exc:
+                raise EdgeQueueLeaseError("remote lease protection failed") from exc
+            if (
+                not isinstance(ciphertext, str) or not ciphertext
+                or ciphertext == token or len(ciphertext.encode("utf-8")) > 4096
+            ):
+                raise EdgeQueueLeaseError("remote lease protection produced invalid ciphertext")
+        return self._opaque_token_digest(token), ciphertext, expiry, attempt
+
+    @staticmethod
+    def _opaque_token_digest(token: str) -> str:
+        return hashlib.sha256(token.encode("utf-8")).hexdigest()
+
+    def _retry_delay(self, attempt_count: int) -> int:
+        base = min(
+            self.retry_max_seconds,
+            self.retry_base_seconds * (2 ** max(0, attempt_count - 1)),
+        )
+        sample = self._random()
+        if isinstance(sample, bool) or not isinstance(sample, (int, float)) or not 0 <= sample <= 1:
+            raise EdgeQueueError("retry random source returned an invalid value")
+        return min(
+            self.retry_max_seconds,
+            max(1, round(base * (0.5 + float(sample)))),
+        )
+
+    @staticmethod
+    def _bounded_identifier(value: object, label: str) -> str:
+        if (
+            not isinstance(value, str) or not value or value.strip() != value
+            or len(value.encode("utf-8")) > 255
+            or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._:-]{0,254}", value)
+        ):
+            raise EdgeQueueError(f"{label} is invalid")
+        return value
+
+    @staticmethod
+    def _sha256(value: object, label: str) -> str:
+        if not isinstance(value, str) or not re.fullmatch(r"[0-9a-f]{64}", value):
+            raise EdgeQueueError(f"{label} is invalid")
+        return value
+
+    @staticmethod
+    def _lease_owner(value: object) -> str:
+        if (
+            not isinstance(value, str)
+            or not value
+            or len(value.encode("utf-8")) > 255
+            or value.strip() != value
+        ):
+            raise ValueError("lease_owner is invalid")
+        return value
+
+    @staticmethod
+    def _error_code(value: object) -> str:
+        if not isinstance(value, str) or not _ERROR_CODE.fullmatch(value):
+            raise EdgePolicyError("error_code must be a bounded code")
+        if any(part in value for part in _SENSITIVE_ERROR_PARTS):
+            raise EdgePolicyError("error_code contains sensitive content")
+        return value
+
+    def _leased_row(
+        self,
+        connection: sqlite3.Connection,
+        table: str,
+        id_column: str,
+        identity: str,
+        lease_token: str,
+        *,
+        status: str = "leased",
+        status_column: str = "status",
+    ) -> sqlite3.Row:
+        row = connection.execute(
+            f"SELECT * FROM {table} WHERE {id_column} = ?",  # noqa: S608
+            (identity,),
+        ).fetchone()
+        if row is None:
+            raise EdgeQueueError("queue item does not exist")
+        if row[status_column] != status or row["lease_token"] != lease_token:
+            raise EdgeQueueLeaseError("current lease token is required")
+        lease_expires_at = row["lease_expires_at"]
+        if lease_expires_at is None:
+            raise EdgeQueueLeaseError("lease has expired")
+        parsed_expiry = datetime.fromisoformat(lease_expires_at.replace("Z", "+00:00"))
+        if parsed_expiry <= _utc_now(self._clock):
+            raise EdgeQueueLeaseError("lease has expired")
+        return row
+
+    @staticmethod
+    def _validate_acknowledgement(
+        value: Mapping[str, object],
+        *,
+        event_id: str,
+        event_digest: str,
+        remote_lease_token_digest: str | None,
+    ) -> dict[str, str]:
+        if remote_lease_token_digest is not None:
+            required = {"event_id", "event_digest", "remote_lease_digest", "received_at", "status"}
+            if not isinstance(value, Mapping) or set(value) != required:
+                raise EdgePolicyError("bound acknowledgement requires exact approved fields")
+            if value["event_id"] != event_id or value["event_digest"] != event_digest:
+                raise EdgePolicyError("acknowledgement does not bind the event")
+            if value["remote_lease_digest"] != remote_lease_token_digest:
+                raise EdgePolicyError("acknowledgement remote lease is invalid")
+            if value["status"] != "accepted":
+                raise EdgePolicyError("acknowledgement status is invalid")
+            try:
+                received_at = canonical_timestamp(value["received_at"], "received_at")
+            except EdgeContractError as exc:
+                raise EdgePolicyError("acknowledgement received_at is invalid") from exc
+            return {
+                "event_id": event_id,
+                "event_digest": event_digest,
+                "remote_lease_digest": remote_lease_token_digest,
+                "received_at": received_at,
+                "status": "accepted",
+            }
+        if not isinstance(value, Mapping) or set(value) != {
+            "message_id",
+            "received_at",
+            "status",
+        }:
+            raise EdgePolicyError("acknowledgement requires exact approved fields")
+        message_id = value["message_id"]
+        status = value["status"]
+        if (
+            not isinstance(message_id, str)
+            or not message_id
+            or message_id.strip() != message_id
+            or len(message_id.encode("utf-8")) > 255
+        ):
+            raise EdgePolicyError("acknowledgement message_id is invalid")
+        if status != "accepted":
+            raise EdgePolicyError("acknowledgement status is invalid")
+        try:
+            received_at = canonical_timestamp(value["received_at"], "received_at")
+        except EdgeContractError as exc:
+            raise EdgePolicyError("acknowledgement received_at is invalid") from exc
+        return {
+            "message_id": message_id,
+            "received_at": received_at,
+            "status": status,
+        }
+
+    @staticmethod
+    def _task_record(row: sqlite3.Row) -> QueuedTask:
+        task = json.loads(row["task_json"])
+        return QueuedTask(
+            task_id=row["task_id"],
+            digest=row["digest"],
+            status=row["status"],
+            task=_freeze_mapping(task),
+            attempt_count=row["attempt_count"],
+            available_at=row["available_at"],
+            lease_owner=row["lease_owner"],
+            lease_token=row["lease_token"],
+            lease_expires_at=row["lease_expires_at"],
+            error_code=row["error_code"],
+            authority_key_id=row["authority_key_id"],
+            authority_digest=row["authority_digest"],
+            remote_lease_token_digest=row["remote_lease_token_digest"],
+            remote_lease_expires_at=row["remote_lease_expires_at"],
+            remote_attempt=row["remote_attempt"],
+            created_at=row["created_at"],
+            updated_at=row["updated_at"],
+        )
+
+    @staticmethod
+    def _event_record(row: sqlite3.Row) -> QueuedEvent:
+        event = json.loads(row["event_json"])
+        acknowledgement = (
+            json.loads(row["acknowledgement_json"])
+            if row["acknowledgement_json"] is not None
+            else None
+        )
+        return QueuedEvent(
+            event_id=row["event_id"],
+            digest=row["digest"],
+            status=row["status"],
+            event=_freeze_mapping(event),
+            attempt_count=row["attempt_count"],
+            available_at=row["available_at"],
+            lease_owner=row["lease_owner"],
+            lease_token=row["lease_token"],
+            lease_expires_at=row["lease_expires_at"],
+            error_code=row["error_code"],
+            acknowledgement=(
+                _freeze_mapping(acknowledgement)
+                if acknowledgement is not None
+                else None
+            ),
+            task_digest=row["task_digest"],
+            remote_lease_token_digest=row["remote_lease_token_digest"],
+            control_lease_token_digest=row["control_lease_token_digest"],
+            created_at=row["created_at"],
+            updated_at=row["updated_at"],
+        )
+
+    @staticmethod
+    def _artifact_record(row: sqlite3.Row) -> LocalArtifact:
+        receipt = (
+            json.loads(row["deletion_receipt_json"])
+            if row["deletion_receipt_json"] is not None
+            else None
+        )
+        return LocalArtifact(
+            artifact_digest=row["artifact_digest"],
+            artifact_ref=row["artifact_ref"],
+            artifact_ref_hash=row["artifact_ref_hash"],
+            classification=row["classification"],
+            retention_until=row["retention_until"],
+            task_id=row["task_id"],
+            event_id=row["event_id"],
+            cleanup_status=row["cleanup_status"],
+            attempt_count=row["attempt_count"],
+            available_at=row["available_at"],
+            lease_owner=row["lease_owner"],
+            lease_token=row["lease_token"],
+            lease_expires_at=row["lease_expires_at"],
+            error_code=row["error_code"],
+            deletion_receipt=_freeze_mapping(receipt) if receipt is not None else None,
+            created_at=row["created_at"],
+            updated_at=row["updated_at"],
+        )
+
+    @staticmethod
+    def _outcome_record(row: sqlite3.Row) -> QueuedOutcome:
+        outcome = json.loads(row["outcome_json"])
+        acknowledgement = (
+            json.loads(row["acknowledgement_json"])
+            if row["acknowledgement_json"] is not None
+            else None
+        )
+        return QueuedOutcome(
+            task_id=row["task_id"],
+            digest=row["digest"],
+            status=row["status"],
+            outcome=_freeze_mapping(outcome),
+            attempt_count=row["attempt_count"],
+            available_at=row["available_at"],
+            lease_owner=row["lease_owner"],
+            lease_token=row["lease_token"],
+            lease_expires_at=row["lease_expires_at"],
+            error_code=row["error_code"],
+            acknowledgement=(
+                _freeze_mapping(acknowledgement)
+                if acknowledgement is not None
+                else None
+            ),
+            remote_lease_token_digest=row["remote_lease_token_digest"],
+            created_at=row["created_at"],
+            updated_at=row["updated_at"],
+        )

+ 609 - 0
deployment/app/core/edge_gateway/repository.py

@@ -0,0 +1,609 @@
+"""Transactional PostgreSQL repository for the edge control plane."""
+
+from __future__ import annotations
+
+import hashlib
+import json
+
+from sqlalchemy import text
+
+from app.core.common.identifiers import new_governance_uid
+
+_OVERFLOW_FINGERPRINT = hashlib.sha256(b"edge-failure-window-capacity").hexdigest()
+
+
+def _json(value: object) -> str:
+    return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
+
+
+class EdgeGatewayRepository:
+    """Owns atomic state transitions; secrets passed here are already hashed."""
+
+    def __init__(self, session):
+        self.session = session
+
+    def rollback(self) -> None:
+        self.session.rollback()
+
+    def create_enrollment(self, values: dict) -> None:
+        self.session.execute(text("""
+            INSERT INTO public.edge_gateway_enrollments
+              (uid,gateway_id,gateway_name,environment,network_zone,policy_digest,
+               expected_certificate_sha256,allowed_control_hosts,allowed_proxy_hosts,
+               token_hash,status,expires_at,created_by)
+            VALUES (CAST(:uid AS uuid),CAST(:gateway_id AS uuid),:gateway_name,:environment,
+                    :network_zone,:policy_digest,:expected_certificate_sha256,
+                    CAST(:control_hosts AS text[]),
+                    CAST(:proxy_hosts AS text[]),:token_hash,'pending',
+                    CURRENT_TIMESTAMP + (:ttl * INTERVAL '1 second'),CAST(:actor_uid AS uuid))
+        """), values)
+        self._audit(None, "enrollment_created", values["actor_uid"], True, {"gateway_id": values["gateway_id"], "ttl_seconds": values["ttl"]})
+        self.session.commit()
+
+    def lock_enrollment(self, token_hash: str):
+        return self.session.execute(text("""
+            SELECT uid::text,gateway_id::text,gateway_name,environment,network_zone,
+                   policy_digest,expected_certificate_sha256,allowed_control_hosts,
+                   allowed_proxy_hosts,status,expires_at
+              FROM public.edge_gateway_enrollments WHERE token_hash=:token_hash FOR UPDATE
+        """), {"token_hash": token_hash}).mappings().one_or_none()
+
+    def complete_registration(self, enrollment, credential_hash: str, certificate_sha256: str, version: str) -> dict:
+        credential_uid = new_governance_uid()
+        gateway_id = enrollment["gateway_id"]
+        self.session.execute(text("""
+            INSERT INTO public.edge_gateways
+              (uid,name,environment,network_zone,status,current_generation,policy_digest,
+               allowed_control_hosts,allowed_proxy_hosts,runtime_version)
+            VALUES (CAST(:gateway_id AS uuid),:name,:environment,:zone,'pending',1,:policy,
+                    CAST(:control_hosts AS text[]),CAST(:proxy_hosts AS text[]),:version)
+        """), {
+            "gateway_id": gateway_id, "name": enrollment["gateway_name"],
+            "environment": enrollment["environment"], "zone": enrollment["network_zone"],
+            "policy": enrollment["policy_digest"], "control_hosts": enrollment["allowed_control_hosts"],
+            "proxy_hosts": enrollment["allowed_proxy_hosts"], "version": version,
+        })
+        self.session.execute(text("""
+            INSERT INTO public.edge_gateway_credentials
+              (uid,gateway_id,generation,credential_hash,certificate_sha256,status,expires_at)
+            VALUES (CAST(:uid AS uuid),CAST(:gateway_id AS uuid),1,:credential_hash,
+                    :certificate,'active',CURRENT_TIMESTAMP + INTERVAL '365 days')
+        """), {"uid": credential_uid, "gateway_id": gateway_id, "credential_hash": credential_hash, "certificate": certificate_sha256})
+        self.session.execute(text("UPDATE public.edge_gateways SET status='active',updated_at=CURRENT_TIMESTAMP WHERE uid=CAST(:gateway_id AS uuid)"), {"gateway_id": gateway_id})
+        consumed = self.session.execute(text("""
+            UPDATE public.edge_gateway_enrollments SET status='consumed',consumed_at=CURRENT_TIMESTAMP
+             WHERE uid=CAST(:uid AS uuid) AND status='pending' AND expires_at>CURRENT_TIMESTAMP
+             RETURNING uid
+        """), {"uid": enrollment["uid"]}).scalar_one_or_none()
+        if consumed is None:
+            raise RuntimeError("enrollment is no longer consumable")
+        self._audit(gateway_id, "gateway_registered", None, True, {"generation": 1})
+        self.session.commit()
+        return {"gateway_id": gateway_id, "credential_uid": credential_uid, "generation": 1}
+
+    def authenticate(self, credential_hash: str, *, lock: bool = False):
+        if lock:
+            gateway_id = self.session.execute(text("""
+                SELECT gateway_id::text
+                  FROM public.edge_gateway_credentials
+                 WHERE credential_hash=:credential_hash
+            """), {"credential_hash": credential_hash}).scalar_one_or_none()
+            if gateway_id is None:
+                return None
+            gateway = self.session.execute(text("""
+                SELECT uid::text AS gateway_id,environment,network_zone,policy_digest,
+                       allowed_control_hosts,allowed_proxy_hosts,status,current_generation
+                  FROM public.edge_gateways
+                 WHERE uid=CAST(:gateway_id AS uuid)
+                 FOR UPDATE
+            """), {"gateway_id": gateway_id}).mappings().one_or_none()
+            if gateway is None or gateway["status"] not in {"active", "offline"}:
+                return None
+            credential = self.session.execute(text("""
+                SELECT uid::text,gateway_id::text,generation,certificate_sha256
+                  FROM public.edge_gateway_credentials
+                 WHERE credential_hash=:credential_hash AND status='active'
+                   AND expires_at>CURRENT_TIMESTAMP
+                   AND gateway_id=CAST(:gateway_id AS uuid)
+                   AND generation=:generation
+                 FOR UPDATE
+            """), {
+                "credential_hash": credential_hash,
+                "gateway_id": gateway_id,
+                "generation": gateway["current_generation"],
+            }).mappings().one_or_none()
+            if credential is None:
+                return None
+            return {**dict(credential), **{
+                key: gateway[key] for key in (
+                    "environment", "network_zone", "policy_digest",
+                    "allowed_control_hosts", "allowed_proxy_hosts", "status",
+                )
+            }}
+        return self.session.execute(text("""
+            SELECT c.uid::text,c.gateway_id::text,c.generation,c.certificate_sha256,
+                   g.environment,g.network_zone,g.policy_digest,g.allowed_control_hosts,
+                   g.allowed_proxy_hosts,g.status
+              FROM public.edge_gateway_credentials c
+              JOIN public.edge_gateways g ON g.uid=c.gateway_id
+             WHERE c.credential_hash=:credential_hash AND c.status='active'
+               AND c.expires_at>CURRENT_TIMESTAMP AND g.status IN ('active','offline')
+               AND g.current_generation=c.generation
+        """), {"credential_hash": credential_hash}).mappings().one_or_none()
+
+    def commit(self) -> None:
+        self.session.commit()
+
+    def list_gateways(self, *, limit: int, offset: int) -> list[dict]:
+        return [dict(row) for row in self.session.execute(text("""
+            SELECT uid::text AS gateway_id,name,environment,network_zone,status,current_generation,
+                   policy_digest,allowed_control_hosts,allowed_proxy_hosts,last_heartbeat_at,runtime_version
+              FROM public.edge_gateways ORDER BY created_at,uid LIMIT :limit OFFSET :offset
+        """), {"limit": limit, "offset": offset}).mappings()]
+
+    def gateway_binding(self, gateway_id: str):
+        return self.session.execute(text("""
+            SELECT uid::text AS gateway_id,environment,network_zone,policy_digest,status
+              FROM public.edge_gateways WHERE uid=CAST(:gateway_id AS uuid)
+        """), {"gateway_id": gateway_id}).mappings().one_or_none()
+
+    def heartbeat(self, gateway_id: str, version: str, safe_summary: dict) -> dict:
+        row = self.session.execute(text("""
+            UPDATE public.edge_gateways SET status='active',runtime_version=:version,
+                   safe_health_summary=CAST(:summary AS jsonb),last_heartbeat_at=CURRENT_TIMESTAMP,
+                   updated_at=CURRENT_TIMESTAMP WHERE uid=CAST(:gateway_id AS uuid)
+             RETURNING uid::text AS gateway_id,status,last_heartbeat_at,runtime_version
+        """), {"gateway_id": gateway_id, "version": version, "summary": _json(safe_summary)}).mappings().one()
+        self._audit(gateway_id, "heartbeat_accepted", None, True, {"version": version})
+        self.session.commit()
+        return dict(row)
+
+    def rotate(self, gateway_id: str, credential_hash: str, certificate_sha256: str, actor_uid: str, request_id: str, request_digest: str) -> dict | None:
+        gateway = self.session.execute(text("""
+            SELECT current_generation,status FROM public.edge_gateways
+             WHERE uid=CAST(:gateway_id AS uuid) FOR UPDATE
+        """), {"gateway_id": gateway_id}).mappings().one_or_none()
+        if gateway is None or gateway["status"] not in {"active", "offline"}:
+            return None
+        prior = self.session.execute(text("""
+            SELECT request_digest,generation FROM edge_gateway_rotation_requests
+             WHERE gateway_id=CAST(:gateway_id AS uuid) AND request_id=:request_id
+        """), {"gateway_id": gateway_id, "request_id": request_id}).mappings().one_or_none()
+        if prior:
+            self.session.commit()
+            if prior["request_digest"] != request_digest:
+                return {"conflict": True}
+            return {"generation": prior["generation"], "replayed": True}
+        old = self.session.execute(text("""
+            UPDATE public.edge_gateway_credentials SET status='rotated',revoked_at=CURRENT_TIMESTAMP
+             WHERE gateway_id=CAST(:gateway_id AS uuid) AND generation=:generation AND status='active'
+             RETURNING uid::text
+        """), {"gateway_id": gateway_id, "generation": gateway["current_generation"]}).scalar_one()
+        generation = int(gateway["current_generation"]) + 1
+        uid = new_governance_uid()
+        self.session.execute(text("""
+            INSERT INTO public.edge_gateway_credentials
+              (uid,gateway_id,generation,credential_hash,certificate_sha256,status,expires_at,rotated_from_uid)
+            VALUES (CAST(:uid AS uuid),CAST(:gateway_id AS uuid),:generation,:credential_hash,
+                    :certificate,'active',CURRENT_TIMESTAMP + INTERVAL '365 days',CAST(:old AS uuid))
+        """), {"uid": uid, "gateway_id": gateway_id, "generation": generation, "credential_hash": credential_hash, "certificate": certificate_sha256, "old": old})
+        self.session.execute(text("UPDATE public.edge_gateways SET current_generation=:generation,status='active',updated_at=CURRENT_TIMESTAMP WHERE uid=CAST(:gateway_id AS uuid)"), {"gateway_id": gateway_id, "generation": generation})
+        self.session.execute(text("""
+            INSERT INTO edge_gateway_rotation_requests(gateway_id,request_id,request_digest,generation)
+            VALUES (CAST(:gateway_id AS uuid),:request_id,:request_digest,:generation)
+        """), {"gateway_id": gateway_id, "request_id": request_id, "request_digest": request_digest, "generation": generation})
+        self._audit(gateway_id, "credential_rotated", actor_uid, True, {"generation": generation})
+        self.session.commit()
+        return {"credential_uid": uid, "generation": generation}
+
+    def revoke(self, gateway_id: str, actor_uid: str) -> bool:
+        gateway = self.session.execute(text("""
+            UPDATE public.edge_gateways SET status='revoked',revoked_at=CURRENT_TIMESTAMP,
+                   updated_at=CURRENT_TIMESTAMP WHERE uid=CAST(:gateway_id AS uuid)
+                   AND status <> 'revoked' RETURNING uid::text
+        """), {"gateway_id": gateway_id}).scalar_one_or_none()
+        if gateway is None:
+            self.session.rollback()
+            return False
+        self.session.execute(text("UPDATE public.edge_gateway_credentials SET status='revoked',revoked_at=CURRENT_TIMESTAMP WHERE gateway_id=CAST(:gateway_id AS uuid) AND status='active'"), {"gateway_id": gateway_id})
+        self._audit(gateway_id, "gateway_revoked", actor_uid, True, {})
+        self.session.commit()
+        return True
+
+    def insert_task(
+        self, contract: dict, digest: str, authority: dict, actor_uid: str,
+    ) -> dict:
+        existing = self.session.execute(text("""
+            SELECT task_id,contract_digest,contract,authority FROM public.edge_gateway_tasks
+             WHERE gateway_id=CAST(:gateway_id AS uuid) AND idempotency_key=:idempotency_key FOR UPDATE
+        """), {"gateway_id": contract["gateway_id"], "idempotency_key": contract["idempotency_key"]}).mappings().one_or_none()
+        if existing:
+            result = dict(existing)
+            self.session.commit()
+            return result
+        inserted = self.session.execute(text("""
+            INSERT INTO public.edge_gateway_tasks
+              (task_id,gateway_id,idempotency_key,contract,contract_digest,authority,status,created_by)
+            SELECT :task_id,uid,:idempotency_key,CAST(:contract AS jsonb),:digest,
+                   CAST(:authority AS jsonb),'pending',CAST(:actor AS uuid)
+              FROM public.edge_gateways WHERE uid=CAST(:gateway_id AS uuid) AND status IN ('active','offline')
+            ON CONFLICT DO NOTHING
+            RETURNING task_id
+        """), {"task_id": contract["task_id"], "gateway_id": contract["gateway_id"], "idempotency_key": contract["idempotency_key"], "contract": _json(contract), "digest": digest, "authority": _json(authority), "actor": actor_uid})
+        if inserted.scalar_one_or_none() is None:
+            concurrent = self.session.execute(text("""
+                SELECT task_id,contract_digest,contract,authority FROM public.edge_gateway_tasks
+                 WHERE task_id=:task_id OR
+                   (gateway_id=CAST(:gateway_id AS uuid) AND idempotency_key=:idempotency_key)
+            """), {
+                "task_id": contract["task_id"], "gateway_id": contract["gateway_id"],
+                "idempotency_key": contract["idempotency_key"],
+            }).mappings().one_or_none()
+            self.session.commit()
+            return dict(concurrent) if concurrent else {}
+        self._audit(contract["gateway_id"], "task_issued", actor_uid, True, {"task_id": contract["task_id"]})
+        self.session.commit()
+        return {"task_id": contract["task_id"], "contract_digest": digest, "contract": contract, "authority": authority}
+
+    def pull_task(self, gateway_id: str, lease_hash: str, lease_seconds: int = 120):
+        row = self.session.execute(text("""
+            WITH expired AS (
+              UPDATE public.edge_gateway_tasks
+                 SET status='failed',lease_token_hash=NULL,lease_expires_at=NULL,
+                     result_summary='{"reason_code":"task_deadline_expired"}'::jsonb,
+                     updated_at=CURRENT_TIMESTAMP
+               WHERE gateway_id=CAST(:gateway_id AS uuid)
+                 AND status IN ('pending','leased')
+                 AND (contract->>'deadline_at')::timestamptz<=CURRENT_TIMESTAMP
+               RETURNING task_id
+            ), candidate AS (
+              SELECT task_id FROM public.edge_gateway_tasks
+               WHERE gateway_id=CAST(:gateway_id AS uuid)
+                 AND (status='pending' OR (status='leased' AND lease_expires_at<=CURRENT_TIMESTAMP))
+                 AND (contract->>'deadline_at')::timestamptz>CURRENT_TIMESTAMP
+               ORDER BY created_at,task_id FOR UPDATE SKIP LOCKED LIMIT 1
+            )
+            UPDATE public.edge_gateway_tasks t SET status='leased',lease_token_hash=:lease_hash,
+                   lease_expires_at=LEAST(
+                     (t.contract->>'deadline_at')::timestamptz,
+                     CURRENT_TIMESTAMP + (:lease_seconds * INTERVAL '1 second')
+                   ),
+                   updated_at=CURRENT_TIMESTAMP FROM candidate c WHERE t.task_id=c.task_id
+             RETURNING t.task_id,t.contract,t.authority,t.contract_digest,t.lease_expires_at
+        """), {"gateway_id": gateway_id, "lease_hash": lease_hash, "lease_seconds": lease_seconds}).mappings().one_or_none()
+        self.session.commit()
+        return dict(row) if row else None
+
+    def cancel_task(self, task_id: str, actor_uid: str) -> bool:
+        gateway_id = self.session.execute(text("""
+            SELECT g.uid::text
+              FROM public.edge_gateway_tasks t
+              JOIN public.edge_gateways g ON g.uid=t.gateway_id
+             WHERE t.task_id=:task_id
+             FOR UPDATE OF g
+        """), {"task_id": task_id}).scalar_one_or_none()
+        if gateway_id is None:
+            self.session.rollback()
+            return False
+        row = self.session.execute(text("""
+            UPDATE public.edge_gateway_tasks SET
+                   status=CASE WHEN status='pending' THEN 'cancelled' ELSE 'cancel_requested' END,
+                   cancel_requested_at=CURRENT_TIMESTAMP,updated_at=CURRENT_TIMESTAMP
+             WHERE task_id=:task_id AND status IN ('pending','leased') RETURNING gateway_id::text
+        """), {"task_id": task_id}).scalar_one_or_none()
+        if row:
+            self._audit(gateway_id, "task_cancel_requested", actor_uid, True, {"task_id": task_id})
+        self.session.commit()
+        return row is not None
+
+    def task_outcome(self, gateway_id: str, task_id: str, lease_hash: str, outcome: str, summary: dict):
+        row = self.session.execute(text("""
+            SELECT status,lease_token_hash,lease_expires_at,result_summary,
+                   lease_expires_at>CURRENT_TIMESTAMP AS lease_current
+              FROM edge_gateway_tasks WHERE gateway_id=CAST(:gateway_id AS uuid)
+               AND task_id=:task_id FOR UPDATE
+        """), {"gateway_id": gateway_id, "task_id": task_id}).mappings().one_or_none()
+        if row is None:
+            self.session.rollback()
+            return None
+        if row["status"] == outcome:
+            if dict(row["result_summary"] or {}) != summary:
+                self.session.rollback()
+                return None
+            self.session.commit()
+            return {"task_id": task_id, "status": outcome, "replayed": True}
+        allowed = (
+            outcome in {"completed", "failed"} and row["status"] == "leased"
+            and bool(row["lease_current"])
+        ) or (outcome == "cancelled" and row["status"] == "cancel_requested")
+        if not allowed or row["lease_token_hash"] != lease_hash:
+            self.session.rollback()
+            return None
+        try:
+            self.session.execute(text("""
+                UPDATE edge_gateway_tasks SET status=:outcome,result_summary=CAST(:summary AS jsonb),
+                       lease_token_hash=NULL,lease_expires_at=NULL,updated_at=CURRENT_TIMESTAMP
+                 WHERE gateway_id=CAST(:gateway_id AS uuid) AND task_id=:task_id
+            """), {"gateway_id": gateway_id, "task_id": task_id, "outcome": outcome,
+                     "summary": _json(summary)})
+            self._audit(
+                gateway_id, "task_outcome_recorded", None, True,
+                {"task_id": task_id, "outcome": outcome},
+            )
+            self.session.commit()
+        except Exception:
+            self.session.rollback()
+            raise
+        return {"task_id": task_id, "status": outcome, "replayed": False}
+
+    def cancelled_tasks(
+        self, gateway_id: str, *, limit: int, cursor: str | None = None
+    ) -> dict:
+        rows = list(self.session.execute(text("""
+            SELECT task_id FROM public.edge_gateway_tasks
+             WHERE gateway_id=CAST(:gateway_id AS uuid) AND status='cancel_requested'
+               AND (
+                 CAST(:cursor AS text) IS NULL OR (created_at,task_id) > (
+                   SELECT created_at,task_id FROM public.edge_gateway_tasks
+                    WHERE gateway_id=CAST(:gateway_id AS uuid) AND task_id=:cursor
+                 )
+               )
+             ORDER BY created_at,task_id LIMIT :page_limit
+        """), {
+            "gateway_id": gateway_id,
+            "cursor": cursor,
+            "page_limit": limit + 1,
+        }).scalars())
+        page = rows[:limit]
+        return {
+            "items": page,
+            "next_cursor": page[-1] if len(rows) > limit else None,
+        }
+
+    def event_by_id(self, gateway_id: str, event_id: str):
+        return self.session.execute(text("""
+            SELECT contract_digest,ack FROM public.edge_gateway_events
+             WHERE gateway_id=CAST(:gateway_id AS uuid) AND event_id=:event_id
+        """), {"gateway_id": gateway_id, "event_id": event_id}).mappings().one_or_none()
+
+    def task_lease_binding(self, task_id: str, gateway_id: str, lease_hash: str):
+        return self.session.execute(text("""
+              SELECT contract FROM public.edge_gateway_tasks
+               WHERE task_id=:task_id AND gateway_id=CAST(:gateway_id AS uuid)
+                 AND status='leased' AND lease_token_hash=:lease_hash
+                 AND lease_expires_at>CURRENT_TIMESTAMP
+               FOR UPDATE
+        """), {"task_id": task_id, "gateway_id": gateway_id, "lease_hash": lease_hash}).scalar_one_or_none()
+
+    def insert_event(self, contract: dict, digest: str, ack: dict) -> dict | None:
+        inserted = self.session.execute(text("""
+            INSERT INTO public.edge_gateway_events
+              (event_id,gateway_id,task_id,classification,contract,contract_digest,ack)
+            VALUES (:event_id,CAST(:gateway_id AS uuid),:task_id,:classification,
+                    CAST(:contract AS jsonb),:digest,CAST(:ack AS jsonb))
+            ON CONFLICT (event_id) DO NOTHING RETURNING event_id
+        """), {"event_id": contract["event_id"], "gateway_id": contract["gateway_id"], "task_id": contract["task_id"], "classification": contract["classification"], "contract": _json(contract), "digest": digest, "ack": _json(ack)})
+        if inserted.scalar_one_or_none() is None:
+            existing = self.event_by_id(contract["gateway_id"], contract["event_id"])
+            if existing is None or existing["contract_digest"] != digest:
+                self.session.rollback()
+                return None
+            self.session.commit()
+            return dict(existing["ack"])
+        self._audit(contract["gateway_id"], "event_accepted", None, True, {"event_id": contract["event_id"], "classification": contract["classification"]})
+        self.session.execute(text("""
+            UPDATE edge_gateway_tasks SET status='completed',lease_token_hash=NULL,
+                   lease_expires_at=NULL,result_summary=CAST(:summary AS jsonb),updated_at=CURRENT_TIMESTAMP
+             WHERE task_id=:task_id AND gateway_id=CAST(:gateway_id AS uuid) AND status='leased'
+        """), {"task_id": contract["task_id"], "gateway_id": contract["gateway_id"],
+                 "summary": _json({"event_id": contract["event_id"], "classification": contract["classification"]})})
+        self.session.commit()
+        return ack
+
+    def offer_release(
+        self, gateway_id: str, manifest: dict, actor_uid: str,
+        request_id: str, request_digest: str,
+    ) -> dict:
+        gateway = self.session.execute(text("""
+            SELECT status FROM public.edge_gateways
+             WHERE uid=CAST(:gateway_id AS uuid) FOR UPDATE
+        """), {"gateway_id": gateway_id}).mappings().one_or_none()
+        if gateway is None or gateway["status"] not in {"active", "offline"}:
+            self.session.rollback()
+            return {}
+        prior_request = self.session.execute(text("""
+            SELECT uid::text AS release_id,status,version,artifact_digest,rollback_version,
+                   request_id,request_digest,signed_manifest
+              FROM edge_gateway_releases WHERE gateway_id=CAST(:gateway_id AS uuid)
+               AND request_id=:request_id
+        """), {"gateway_id": gateway_id, "request_id": request_id}).mappings().one_or_none()
+        prior_version = self.session.execute(text("""
+            SELECT uid::text AS release_id,request_id,request_digest
+              FROM edge_gateway_releases WHERE gateway_id=CAST(:gateway_id AS uuid)
+               AND version=:version
+        """), {"gateway_id": gateway_id, "version": manifest["version"]}).mappings().one_or_none()
+        if prior_request:
+            self.session.commit()
+            if (
+                prior_request["request_digest"] != request_digest
+                or prior_request["version"] != manifest["version"]
+                or (prior_version is not None and prior_version["request_id"] != request_id)
+            ):
+                return {"conflict": True}
+            return {
+                **{key: prior_request[key] for key in (
+                    "release_id", "status", "version", "artifact_digest",
+                    "rollback_version",
+                )},
+                "manifest": dict(prior_request["signed_manifest"]),
+            }
+        if prior_version:
+            self.session.commit()
+            return {"conflict": True}
+        self.session.execute(text("""
+            INSERT INTO public.edge_gateway_releases
+              (uid,gateway_id,version,artifact_digest,rollback_version,status,request_id,
+               request_digest,created_by,artifact_name,deadline_at,signature_algorithm,
+               key_id,manifest_digest,signature,signed_manifest)
+            VALUES (CAST(:uid AS uuid),CAST(:gateway_id AS uuid),:version,:digest,:rollback,
+                    :status,:request_id,:request_digest,CAST(:actor AS uuid),:artifact_name,
+                    CAST(:deadline_at AS timestamptz),:signature_algorithm,:key_id,
+                    :manifest_digest,:signature,CAST(:signed_manifest AS jsonb))
+        """), {
+            "uid": manifest["release_id"], "gateway_id": gateway_id,
+            "version": manifest["version"], "digest": manifest["artifact_digest"],
+            "rollback": manifest["rollback_version"], "status": manifest["status"],
+            "request_id": request_id, "request_digest": request_digest,
+            "actor": actor_uid, "artifact_name": manifest["artifact_name"],
+            "deadline_at": manifest["deadline_at"],
+            "signature_algorithm": manifest["signature_algorithm"],
+            "key_id": manifest["key_id"], "manifest_digest": manifest["manifest_digest"],
+            "signature": manifest["signature"], "signed_manifest": _json(manifest),
+        })
+        self._audit(gateway_id, "release_offered", actor_uid, True, {
+            "release_id": manifest["release_id"], "version": manifest["version"],
+        })
+        self.session.commit()
+        return {
+            "release_id": manifest["release_id"], "status": "offered",
+            "version": manifest["version"], "artifact_digest": manifest["artifact_digest"],
+            "rollback_version": manifest["rollback_version"], "manifest": manifest,
+        }
+
+    def release_for_ack(self, gateway_id: str, release_id: str):
+        return self.session.execute(text("""
+            SELECT uid::text AS release_id,status,safe_summary,signed_manifest
+              FROM public.edge_gateway_releases WHERE uid=CAST(:uid AS uuid)
+               AND gateway_id=CAST(:gateway_id AS uuid) FOR UPDATE
+        """), {"uid": release_id, "gateway_id": gateway_id}).mappings().one_or_none()
+
+    def acknowledge_release(
+        self, gateway_id: str, release_id: str, status: str, safe_summary: dict,
+        signed_manifest: dict,
+    ) -> dict | None:
+        current = self.session.execute(text("""
+            SELECT uid::text AS release_id,status,version,rollback_version,safe_summary
+              FROM public.edge_gateway_releases WHERE uid=CAST(:uid AS uuid)
+               AND gateway_id=CAST(:gateway_id AS uuid) FOR UPDATE
+        """), {"uid": release_id, "gateway_id": gateway_id}).mappings().one_or_none()
+        if current is None:
+            self.session.rollback()
+            return None
+        if current["status"] == status:
+            if dict(current["safe_summary"] or {}) != safe_summary:
+                self.session.rollback()
+                return None
+            self.session.commit()
+            return {key: current[key] for key in ("release_id", "status", "version", "rollback_version")}
+        allowed = {
+            "offered": {"accepted"},
+            "accepted": {"installed", "failed"},
+            "failed": {"rolled_back"},
+            "installed": {"rolled_back"},
+            "rolled_back": set(),
+        }
+        if status not in allowed[current["status"]]:
+            self.session.rollback()
+            return None
+        row = self.session.execute(text("""
+            UPDATE public.edge_gateway_releases SET status=:status,safe_summary=CAST(:summary AS jsonb),
+                   acknowledged_at=CURRENT_TIMESTAMP,signature_algorithm=:signature_algorithm,
+                   key_id=:key_id,manifest_digest=:manifest_digest,signature=:signature,
+                   signed_manifest=CAST(:signed_manifest AS jsonb)
+             WHERE uid=CAST(:uid AS uuid)
+             RETURNING uid::text AS release_id,status,version,rollback_version
+        """), {
+            "uid": release_id, "status": status, "summary": _json(safe_summary),
+            "signature_algorithm": signed_manifest["signature_algorithm"],
+            "key_id": signed_manifest["key_id"],
+            "manifest_digest": signed_manifest["manifest_digest"],
+            "signature": signed_manifest["signature"],
+            "signed_manifest": _json(signed_manifest),
+        }).mappings().one()
+        if row:
+            self._audit(gateway_id, "release_acknowledged", None, True, {"release_id": release_id, "outcome": status})
+        self.session.commit()
+        return dict(row) if row else None
+
+    def unfinished_releases(
+        self, gateway_id: str, *, limit: int, cursor: str | None = None
+    ) -> dict:
+        rows = list(self.session.execute(text("""
+            SELECT uid::text AS release_id,signed_manifest
+              FROM public.edge_gateway_releases
+             WHERE gateway_id=CAST(:gateway_id AS uuid)
+               AND status IN ('offered','accepted','failed')
+               AND (
+                 CAST(:cursor AS text) IS NULL OR (created_at,uid) > (
+                   SELECT created_at,uid FROM public.edge_gateway_releases
+                    WHERE gateway_id=CAST(:gateway_id AS uuid)
+                      AND uid=CAST(:cursor AS uuid)
+                 )
+               )
+             ORDER BY created_at,uid LIMIT :page_limit
+        """), {
+            "gateway_id": gateway_id,
+            "cursor": cursor,
+            "page_limit": limit + 1,
+        }).mappings())
+        page = rows[:limit]
+        return {
+            "items": [dict(row["signed_manifest"]) for row in page],
+            "next_cursor": page[-1]["release_id"] if len(rows) > limit else None,
+        }
+
+    def latest_release_baseline(self, gateway_id: str) -> dict | None:
+        row = self.session.execute(text("""
+            SELECT signed_manifest FROM public.edge_gateway_releases
+             WHERE gateway_id=CAST(:gateway_id AS uuid)
+               AND status IN ('installed','rolled_back')
+             ORDER BY COALESCE(acknowledged_at,created_at) DESC,
+                      created_at DESC,uid DESC LIMIT 1
+        """), {"gateway_id": gateway_id}).scalar_one_or_none()
+        return dict(row) if row is not None else None
+
+    def audit_failure(
+        self, gateway_id: str | None, event_type: str, reason_code: str, *, trusted: bool,
+    ) -> int:
+        allowed = {
+            "registration_rejected", "authentication_rejected", "policy_rejected",
+            "binding_rejected", "idempotency_conflict", "release_rejected",
+        }
+        if event_type not in allowed or not reason_code.isidentifier() or len(reason_code) > 64:
+            raise ValueError("failure audit metadata is invalid")
+        if not trusted:
+            gateway_id = None
+            event_type = "untrusted_request_rejected"
+            reason_code = "untrusted_failure"
+        self.session.rollback()
+        engine = self.session.get_bind()
+        fingerprint = hashlib.sha256(f"{gateway_id or 'unknown'}:{event_type}:{reason_code}".encode()).hexdigest()
+        with engine.begin() as connection:
+            window = connection.execute(text("""
+                SELECT * FROM public.record_edge_gateway_failure(
+                  CAST(:audit_uid AS uuid),
+                  :fingerprint,
+                  CASE WHEN EXISTS (SELECT 1 FROM edge_gateways WHERE uid=CAST(:gateway AS uuid))
+                       THEN CAST(:gateway AS uuid) ELSE NULL END,
+                  :event_type,
+                  :reason_code
+                )
+            """), {
+                "audit_uid": new_governance_uid(), "fingerprint": fingerprint,
+                "gateway": gateway_id, "event_type": event_type,
+                "reason_code": reason_code,
+            }).mappings().one()
+            fingerprint = window["failure_fingerprint"]
+            gateway_id = window["gateway_id"]
+            event_type = window["event_type"]
+            if fingerprint == _OVERFLOW_FINGERPRINT:
+                reason_code = "capacity_overflow"
+        return int(window["occurrence_count"])
+
+    def _audit(self, gateway_id, event_type: str, actor_uid, success: bool, detail: dict) -> None:
+        self.session.execute(text("""
+            SELECT public.record_edge_gateway_audit(
+              CAST(:uid AS uuid),CAST(:gateway_id AS uuid),:event_type,
+              CAST(:actor_uid AS uuid),:success,CAST(:detail AS jsonb)
+            )
+        """), {"uid": new_governance_uid(), "gateway_id": gateway_id, "event_type": event_type, "actor_uid": actor_uid, "success": success, "detail": _json(detail)})

+ 99 - 0
deployment/app/core/edge_gateway/runtime_roles.py

@@ -0,0 +1,99 @@
+"""Provision the login used by long-running DataOps application processes."""
+
+from __future__ import annotations
+
+import os
+import re
+
+from sqlalchemy import create_engine, text
+from sqlalchemy.pool import NullPool
+
+from app.config.database_urls import validate_database_environment
+
+_ROLE_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_]{0,62}$")
+
+
+def provision_runtime_login() -> None:
+    role_init_url = os.environ.get("DB_ROLE_INIT_DATABASE_URL", "")
+    migration_url = os.environ.get("MIGRATION_DATABASE_URL", "")
+    runtime_url = os.environ.get("DATABASE_URL", "")
+    runtime_user = os.environ.get("DATAOPS_RUNTIME_USER", "")
+    runtime_password = os.environ.get("DATAOPS_RUNTIME_PASSWORD", "")
+    migrator_user = os.environ.get("DATAOPS_MIGRATOR_USER", "")
+    if (
+        not _ROLE_NAME.fullmatch(runtime_user)
+        or not _ROLE_NAME.fullmatch(migrator_user)
+    ):
+        raise RuntimeError("runtime database role provisioning is not configured")
+    validate_database_environment({
+        "DB_ROLE_INIT_DATABASE_URL": role_init_url,
+        "MIGRATION_DATABASE_URL": migration_url,
+        "DATABASE_URL": runtime_url,
+        "DATAOPS_RUNTIME_PASSWORD": runtime_password,
+    })
+    engine = create_engine(role_init_url, poolclass=NullPool)
+    quoted_user = engine.dialect.identifier_preparer.quote(runtime_user)
+    quoted_migrator = engine.dialect.identifier_preparer.quote(migrator_user)
+    try:
+        with engine.begin() as connection:
+            bootstrap_identity = connection.execute(text("""
+                SELECT rolsuper OR rolcreaterole
+                  FROM pg_roles WHERE rolname=current_user
+            """)).scalar_one()
+            if not bootstrap_identity:
+                raise RuntimeError("DB role-init identity requires CREATEROLE or superuser")
+            connection.execute(text("""
+                DO $$ BEGIN
+                  IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='dataops_app_runtime') THEN
+                    CREATE ROLE dataops_app_runtime NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION;
+                  END IF;
+                  IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='dataops_edge_evidence_owner') THEN
+                    CREATE ROLE dataops_edge_evidence_owner NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION;
+                  END IF;
+                END $$;
+                ALTER ROLE dataops_app_runtime NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION;
+                ALTER ROLE dataops_edge_evidence_owner NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION;
+            """))
+            migrator_exists = connection.execute(
+                text("SELECT EXISTS (SELECT 1 FROM pg_roles WHERE rolname=:role)"),
+                {"role": migrator_user},
+            ).scalar_one()
+            if not migrator_exists:
+                raise RuntimeError("DATAOPS_MIGRATOR_USER must already exist")
+            exists = connection.execute(
+                text("SELECT EXISTS (SELECT 1 FROM pg_roles WHERE rolname=:role)"),
+                {"role": runtime_user},
+            ).scalar_one()
+            action = "ALTER" if exists else "CREATE"
+            connection.execute(text(
+                f"{action} ROLE {quoted_user} LOGIN NOSUPERUSER NOCREATEDB "
+                "NOCREATEROLE NOREPLICATION PASSWORD :password"
+            ), {"password": runtime_password})
+            connection.execute(text(f"GRANT dataops_app_runtime TO {quoted_user}"))
+            connection.execute(text(
+                f"GRANT dataops_edge_evidence_owner TO {quoted_migrator}"
+            ))
+            connection.execute(text(
+                "GRANT USAGE,CREATE ON SCHEMA public TO dataops_edge_evidence_owner"
+            ))
+            connection.execute(text("GRANT USAGE ON SCHEMA public TO dataops_app_runtime"))
+            connection.execute(text(
+                "GRANT SELECT,INSERT,UPDATE,DELETE ON ALL TABLES IN SCHEMA public "
+                "TO dataops_app_runtime"
+            ))
+            connection.execute(text(
+                "GRANT USAGE,SELECT ON ALL SEQUENCES IN SCHEMA public TO dataops_app_runtime"
+            ))
+            is_evidence_member = connection.execute(text("""
+                SELECT pg_has_role(:role,'dataops_edge_evidence_owner','MEMBER')
+            """), {"role": runtime_user}).scalar_one()
+            if is_evidence_member:
+                connection.execute(text(
+                    f"REVOKE dataops_edge_evidence_owner FROM {quoted_user}"
+                ))
+    finally:
+        engine.dispose()
+
+
+if __name__ == "__main__":
+    provision_runtime_login()

+ 700 - 0
deployment/app/core/edge_gateway/service.py

@@ -0,0 +1,700 @@
+"""Fail-closed application service for enterprise edge gateways."""
+
+from __future__ import annotations
+
+import hashlib
+import hmac
+import re
+import secrets
+import stat
+import uuid
+from collections.abc import Mapping, Sequence
+from contextlib import suppress
+from datetime import UTC, datetime, timedelta
+from pathlib import Path
+
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
+
+from app.core.edge_gateway.contracts import (
+    EdgeContractError,
+    EdgeEventContract,
+    EdgeTaskContract,
+    SignedTaskEnvelope,
+    canonical_json_bytes,
+    canonical_sha256,
+    canonical_timestamp,
+)
+from app.core.edge_gateway.policy import EdgeEgressPolicy, EdgePolicyError
+
+_SHA256 = re.compile(r"^[0-9a-f]{64}$")
+_VERSION = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._+-]{0,79}$")
+_RELEASE_VERSION = re.compile(
+    r"^(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})$"
+)
+_HOST = re.compile(r"^(?=.{1,253}$)[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$")
+
+
+class EdgeGatewayError(Exception):
+    status_code = 400
+    code = "EDGE_GATEWAY_ERROR"
+
+
+class EdgeGatewayValidationError(EdgeGatewayError):
+    code = "EDGE_GATEWAY_INVALID"
+
+
+class EdgeGatewayAuthenticationError(EdgeGatewayError):
+    status_code = 401
+    code = "EDGE_GATEWAY_AUTHENTICATION_FAILED"
+
+
+class EdgeGatewayConflictError(EdgeGatewayError):
+    status_code = 409
+    code = "EDGE_GATEWAY_CONFLICT"
+
+
+class EdgeGatewayPayloadTooLargeError(EdgeGatewayValidationError):
+    status_code = 413
+    code = "EDGE_GATEWAY_PAYLOAD_TOO_LARGE"
+
+
+class EdgeGatewayRateLimitError(EdgeGatewayError):
+    status_code = 429
+    code = "EDGE_GATEWAY_RATE_LIMITED"
+
+
+class EdgeGatewayNotFoundError(EdgeGatewayError):
+    status_code = 404
+    code = "EDGE_GATEWAY_NOT_FOUND"
+
+
+class EdgeGatewayConfigurationError(EdgeGatewayError):
+    status_code = 503
+    code = "EDGE_GATEWAY_SIGNING_UNAVAILABLE"
+
+
+def _digest(secret: str) -> str:
+    return hashlib.sha256(secret.encode("utf-8")).hexdigest()
+
+
+def _uuid(value: object, label: str) -> str:
+    try:
+        return str(uuid.UUID(str(value)))
+    except (ValueError, TypeError, AttributeError) as exc:
+        raise EdgeGatewayValidationError(f"{label} is invalid") from exc
+
+
+def _sha(value: object, label: str) -> str:
+    if not isinstance(value, str) or not _SHA256.fullmatch(value):
+        raise EdgeGatewayValidationError(f"{label} is invalid")
+    return value
+
+
+def _text(value: object, label: str, maximum: int) -> str:
+    if not isinstance(value, str) or not value or value.strip() != value or len(value.encode()) > maximum or "\x00" in value:
+        raise EdgeGatewayValidationError(f"{label} is invalid")
+    return value
+
+
+def _version(value: object) -> str:
+    value = _text(value, "version", 80)
+    if not _VERSION.fullmatch(value):
+        raise EdgeGatewayValidationError("version is invalid")
+    return value
+
+
+def _release_version(value: object, label: str) -> str:
+    value = _text(value, label, 80)
+    if (
+        not _RELEASE_VERSION.fullmatch(value)
+        or any(int(part) > 2_147_483_647 for part in value.split("."))
+    ):
+        raise EdgeGatewayValidationError(f"{label} is invalid")
+    return value
+
+
+def _release_version_tuple(value: str) -> tuple[int, ...]:
+    return tuple(int(item) for item in value.split("."))
+
+
+def _artifact_name(value: object) -> str:
+    value = _text(value, "artifact_name", 255)
+    if (
+        value in {".", ".."}
+        or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._+-]{0,254}", value)
+        or "/" in value
+        or "\\" in value
+    ):
+        raise EdgeGatewayValidationError("artifact_name is invalid")
+    return value
+
+
+def _future_deadline(value: object, *, now: datetime) -> str:
+    try:
+        canonical = canonical_timestamp(value, "deadline_at")
+        deadline = datetime.fromisoformat(canonical.replace("Z", "+00:00"))
+    except (EdgeContractError, TypeError, ValueError) as exc:
+        raise EdgeGatewayValidationError("deadline_at is invalid") from exc
+    if deadline <= now or deadline > now + timedelta(days=30):
+        raise EdgeGatewayValidationError("deadline_at is invalid")
+    return canonical
+
+
+def _signing_private_key(value: object) -> Ed25519PrivateKey | None:
+    if value is None or value == "":
+        return None
+    if isinstance(value, Ed25519PrivateKey):
+        return value
+    if isinstance(value, str):
+        encoded = value.encode("utf-8")
+        if re.fullmatch(r"[0-9a-fA-F]{64}", value):
+            encoded = bytes.fromhex(value)
+    elif isinstance(value, bytes):
+        encoded = value
+    else:
+        raise EdgeGatewayConfigurationError("edge signing authority is invalid")
+    try:
+        if isinstance(encoded, bytes) and len(encoded) == 32:
+            return Ed25519PrivateKey.from_private_bytes(encoded)
+        key = serialization.load_pem_private_key(encoded, password=None)
+    except (TypeError, ValueError) as exc:
+        raise EdgeGatewayConfigurationError("edge signing authority is invalid") from exc
+    if not isinstance(key, Ed25519PrivateKey):
+        raise EdgeGatewayConfigurationError("edge signing authority is invalid")
+    return key
+
+
+def _hosts(values: object, label: str, *, required: bool) -> list[str]:
+    if not isinstance(values, Sequence) or isinstance(values, (str, bytes)):
+        raise EdgeGatewayValidationError(f"{label} is invalid")
+    result = []
+    for item in values:
+        if not isinstance(item, str) or item != item.lower() or not _HOST.fullmatch(item) or ".." in item:
+            raise EdgeGatewayValidationError(f"{label} is invalid")
+        result.append(item)
+    if required and not result:
+        raise EdgeGatewayValidationError(f"{label} is required")
+    if len(result) != len(set(result)) or len(result) > 32:
+        raise EdgeGatewayValidationError(f"{label} is invalid")
+    return sorted(result)
+
+
+def _environment(value: object) -> str:
+    if value not in {"development", "staging", "production"}:
+        raise EdgeGatewayValidationError("environment is invalid")
+    return str(value)
+
+
+def _page(limit: object = 50, offset: object = 0) -> tuple[int, int]:
+    try:
+        limit, offset = int(limit), int(offset)
+    except (TypeError, ValueError) as exc:
+        raise EdgeGatewayValidationError("pagination is invalid") from exc
+    if not 1 <= limit <= 100 or not 0 <= offset <= 10_000:
+        raise EdgeGatewayValidationError("pagination is invalid")
+    return limit, offset
+
+
+def _safe_summary(value: object, classification: str = "diagnostic_summary") -> dict:
+    if not isinstance(value, Mapping):
+        raise EdgeGatewayValidationError("safe_summary is invalid")
+    candidate = dict(value)
+    try:
+        EdgeEgressPolicy(allowed_control_hosts={"validation.invalid"}).validate_approved_payload(classification, candidate)
+    except (EdgePolicyError, ValueError) as exc:
+        raise EdgeGatewayValidationError("safe_summary contains unapproved content") from exc
+    return candidate
+
+
+class EdgeGatewayService:
+    def __init__(
+        self, repository, *, signing_private_key=None,
+        signing_private_key_file=None, signing_key_provider=None,
+        signing_public_key=None, signing_key_id=None, production=False, clock=None,
+    ):
+        self.repository = repository
+        sources = sum(
+            item not in {None, ""}
+            for item in (signing_private_key, signing_private_key_file)
+        ) + int(signing_key_provider is not None)
+        if sources > 1:
+            raise EdgeGatewayConfigurationError("edge signing authority source is ambiguous")
+        if production and signing_private_key not in {None, ""}:
+            raise EdgeGatewayConfigurationError(
+                "production inline edge signing private key is forbidden"
+            )
+        key_material = signing_private_key
+        if signing_private_key_file not in {None, ""}:
+            if not isinstance(signing_private_key_file, str):
+                raise EdgeGatewayConfigurationError("edge signing key file is invalid")
+            path = Path(signing_private_key_file)
+            try:
+                if (
+                    not path.is_absolute()
+                    or not path.is_file()
+                    or path.is_symlink()
+                    or stat.S_IMODE(path.stat().st_mode) != 0o600
+                ):
+                    raise EdgeGatewayConfigurationError(
+                        "edge signing key file permissions must be 0600"
+                    )
+                key_material = path.read_bytes()
+            except OSError as exc:
+                raise EdgeGatewayConfigurationError(
+                    "edge signing key file is unavailable"
+                ) from exc
+        elif signing_key_provider is not None:
+            if not callable(signing_key_provider):
+                raise EdgeGatewayConfigurationError("edge signing key provider is invalid")
+            try:
+                key_material = signing_key_provider()
+            except Exception as exc:
+                raise EdgeGatewayConfigurationError(
+                    "edge signing key provider is unavailable"
+                ) from exc
+        self._signing_private_key = _signing_private_key(key_material)
+        if self._signing_private_key is not None and signing_public_key not in {None, ""}:
+            if (
+                not isinstance(signing_public_key, str)
+                or not re.fullmatch(r"[0-9a-f]{64}", signing_public_key)
+            ):
+                raise EdgeGatewayConfigurationError("edge signing public key is invalid")
+            actual_public = self._signing_private_key.public_key().public_bytes(
+                serialization.Encoding.Raw,
+                serialization.PublicFormat.Raw,
+            ).hex()
+            if not hmac.compare_digest(actual_public, signing_public_key):
+                raise EdgeGatewayConfigurationError(
+                    "edge signing public key does not match private key"
+                )
+        elif production and self._signing_private_key is not None:
+            raise EdgeGatewayConfigurationError(
+                "production edge signing public key is required"
+            )
+        self._signing_key_id = signing_key_id
+        self._clock = clock or (lambda: datetime.now(UTC))
+
+    def _require_signer(self) -> tuple[Ed25519PrivateKey, str]:
+        key_id = self._signing_key_id
+        if (
+            self._signing_private_key is None
+            or not isinstance(key_id, str)
+            or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._:-]{0,127}", key_id)
+        ):
+            self.repository.rollback()
+            raise EdgeGatewayConfigurationError(
+                "edge signing authority is not configured"
+            )
+        return self._signing_private_key, key_id
+
+    def _signed_task_envelope(self, contract: EdgeTaskContract) -> dict:
+        private_key, key_id = self._require_signer()
+        issued_at = canonical_timestamp(
+            self._clock().astimezone(UTC).isoformat().replace("+00:00", "Z")
+        )
+        unsigned = {
+            "task": contract.to_mapping(),
+            "authority_key_id": key_id,
+            "signature_algorithm": "Ed25519",
+            "contract_digest": contract.digest,
+            "gateway_id": contract.gateway_id,
+            "environment": contract.environment,
+            "network_zone": contract.network_zone,
+            "policy_digest": contract.policy_digest,
+            "purpose": contract.purpose,
+            "issued_at": issued_at,
+            "expires_at": contract.deadline_at,
+        }
+        try:
+            signed_bytes = SignedTaskEnvelope.canonical_unsigned_bytes(unsigned)
+        except EdgeContractError as exc:
+            self.repository.rollback()
+            raise EdgeGatewayValidationError(
+                "edge task authority validity is invalid"
+            ) from exc
+        return {**unsigned, "signature": private_key.sign(signed_bytes).hex()}
+
+    def _signed_release_manifest(self, unsigned: Mapping[str, object]) -> dict:
+        private_key, key_id = self._require_signer()
+        values = dict(unsigned)
+        values["signature_algorithm"] = "Ed25519"
+        values["key_id"] = key_id
+        digest = canonical_sha256(values)
+        return {
+            **values,
+            "manifest_digest": digest,
+            "signature": private_key.sign(canonical_json_bytes(values)).hex(),
+        }
+
+    def _failure(
+        self, gateway_id, event_type: str, reason_code: str, *, trusted: bool = False,
+    ) -> int:
+        count = self.repository.audit_failure(
+            gateway_id, event_type, reason_code, trusted=trusted,
+        )
+        if count >= 10:
+            raise EdgeGatewayRateLimitError("edge failure rate limit exceeded")
+        return count
+
+    def create_enrollment(
+        self, *, gateway_name, environment, network_zone, policy_digest,
+        allowed_control_hosts, allowed_proxy_hosts, ttl_seconds, actor_uid,
+        expected_certificate_sha256,
+    ) -> dict:
+        ttl = int(ttl_seconds)
+        if ttl < 60 or ttl > 86_400:
+            raise EdgeGatewayValidationError("enrollment TTL is invalid")
+        gateway_id, uid = str(uuid.uuid4()), str(uuid.uuid4())
+        token = "dope_" + secrets.token_urlsafe(32)
+        values = {
+            "uid": uid, "gateway_id": gateway_id,
+            "gateway_name": _text(gateway_name, "gateway_name", 200),
+            "environment": _environment(environment),
+            "network_zone": _text(network_zone, "network_zone", 120),
+            "policy_digest": _sha(policy_digest, "policy_digest"),
+            "expected_certificate_sha256": _sha(expected_certificate_sha256, "expected_certificate_sha256"),
+            "control_hosts": _hosts(allowed_control_hosts, "allowed_control_hosts", required=True),
+            "proxy_hosts": _hosts(allowed_proxy_hosts, "allowed_proxy_hosts", required=False),
+            "token_hash": _digest(token), "ttl": ttl, "actor_uid": _uuid(actor_uid, "actor_uid"),
+        }
+        self.repository.create_enrollment(values)
+        return {"enrollment_id": uid, "gateway_id": gateway_id, "enrollment_token": token, "expires_in": ttl, "returned_once": True}
+
+    def register(
+        self, *, enrollment_token, certificate_sha256, gateway_id, environment,
+        network_zone, policy_digest, allowed_control_hosts, allowed_proxy_hosts, version,
+    ) -> dict:
+        gateway_id = _uuid(gateway_id, "gateway_id")
+        if not isinstance(enrollment_token, str) or not enrollment_token.startswith("dope_") or len(enrollment_token) > 128:
+            self._failure(gateway_id, "registration_rejected", "enrollment_invalid")
+            raise EdgeGatewayAuthenticationError("edge enrollment is invalid or expired")
+        try:
+            certificate = _sha(certificate_sha256, "certificate_sha256")
+            supplied = {
+                "gateway_id": gateway_id, "environment": _environment(environment),
+                "network_zone": _text(network_zone, "network_zone", 120),
+                "policy_digest": _sha(policy_digest, "policy_digest"),
+                "allowed_control_hosts": _hosts(allowed_control_hosts, "allowed_control_hosts", required=True),
+                "allowed_proxy_hosts": _hosts(allowed_proxy_hosts, "allowed_proxy_hosts", required=False),
+            }
+        except EdgeGatewayValidationError as exc:
+            self._failure(gateway_id, "registration_rejected", "binding_invalid")
+            raise EdgeGatewayAuthenticationError("edge enrollment binding was rejected") from exc
+        enrollment = self.repository.lock_enrollment(_digest(enrollment_token))
+        now = datetime.now(UTC)
+        if enrollment is None or enrollment["status"] != "pending" or enrollment["expires_at"] <= now:
+            self._failure(supplied["gateway_id"], "registration_rejected", "enrollment_invalid")
+            raise EdgeGatewayAuthenticationError("edge enrollment is invalid or expired")
+        expected = {key: enrollment[key] for key in supplied}
+        expected["allowed_control_hosts"] = sorted(expected["allowed_control_hosts"])
+        expected["allowed_proxy_hosts"] = sorted(expected["allowed_proxy_hosts"])
+        if any(not hmac.compare_digest(str(expected[key]), str(supplied[key])) for key in supplied) or not hmac.compare_digest(enrollment["expected_certificate_sha256"], certificate):
+            self._failure(supplied["gateway_id"], "registration_rejected", "binding_mismatch")
+            raise EdgeGatewayAuthenticationError("edge enrollment binding was rejected")
+        credential = "dopg_" + secrets.token_urlsafe(40)
+        try:
+            result = self.repository.complete_registration(enrollment, _digest(credential), certificate, _version(version))
+        except Exception:
+            self.repository.rollback()
+            raise
+        return {**result, "credential": credential, "credential_returned_once": True, "certificate_sha256": certificate}
+
+    def authenticate(self, *, credential, certificate_sha256, gateway_id, environment, network_zone, generation, lock=False) -> dict:
+        try:
+            gateway_id = _uuid(gateway_id, "gateway_id")
+            generation = int(generation)
+            supplied = (
+                gateway_id, _environment(environment),
+                _text(network_zone, "network_zone", 120), generation,
+                _sha(certificate_sha256, "certificate_sha256"),
+            )
+        except EdgeGatewayValidationError as exc:
+            with suppress(EdgeGatewayValidationError):
+                self._failure(_uuid(gateway_id, "gateway_id"), "authentication_rejected", "binding_invalid")
+            raise EdgeGatewayAuthenticationError("edge credential binding was rejected") from exc
+        except (TypeError, ValueError) as exc:
+            self._failure(gateway_id, "authentication_rejected", "binding_invalid")
+            raise EdgeGatewayAuthenticationError("edge credential binding was rejected") from exc
+        if not isinstance(credential, str) or not credential.startswith("dopg_") or len(credential) > 160:
+            self._failure(gateway_id, "authentication_rejected", "credential_invalid")
+            raise EdgeGatewayAuthenticationError("edge credential binding was rejected")
+        row = self.repository.authenticate(_digest(credential), lock=lock)
+        if row is None:
+            self._failure(supplied[0], "authentication_rejected", "credential_invalid")
+            raise EdgeGatewayAuthenticationError("edge credential binding was rejected")
+        expected = (row["gateway_id"], row["environment"], row["network_zone"], row["generation"], row["certificate_sha256"])
+        if any(not hmac.compare_digest(str(a), str(b)) for a, b in zip(supplied, expected, strict=True)):
+            self._failure(row["gateway_id"], "authentication_rejected", "binding_mismatch")
+            raise EdgeGatewayAuthenticationError("edge credential binding was rejected")
+        return dict(row)
+
+    def list_gateways(self, *, limit=50, offset=0) -> list[dict]:
+        limit, offset = _page(limit, offset)
+        return self.repository.list_gateways(limit=limit, offset=offset)
+
+    def heartbeat(self, *, version, safe_summary, **auth) -> dict:
+        version = _version(version)
+        safe_summary = _safe_summary(safe_summary, "health_summary")
+        identity = self.authenticate(**auth, lock=True)
+        result = self.repository.heartbeat(identity["gateway_id"], version, safe_summary)
+        result["status"] = "online"
+        return result
+
+    def rotate(self, gateway_id, *, certificate_sha256, request_id, actor_uid) -> dict:
+        request_id = _text(request_id, "request_id", 255)
+        certificate_sha256 = _sha(certificate_sha256, "certificate_sha256")
+        request_digest = canonical_sha256({"gateway_id": str(gateway_id), "certificate_sha256": certificate_sha256})
+        token = "dopg_" + secrets.token_urlsafe(40)
+        gateway_id = _uuid(gateway_id, "gateway_id")
+        result = self.repository.rotate(gateway_id, _digest(token), certificate_sha256, _uuid(actor_uid, "actor_uid"), request_id, request_digest)
+        if result is None:
+            raise EdgeGatewayNotFoundError("edge gateway was not found")
+        if result.get("conflict"):
+            raise EdgeGatewayConflictError("rotation request conflicts with an existing request")
+        if result.get("replayed"):
+            return {"gateway_id": gateway_id, "generation": result["generation"], "credential": None, "credential_returned_once": False, "replayed": True}
+        return {**result, "gateway_id": gateway_id, "credential": token, "credential_returned_once": True}
+
+    def revoke(self, gateway_id, *, actor_uid) -> bool:
+        return self.repository.revoke(_uuid(gateway_id, "gateway_id"), _uuid(actor_uid, "actor_uid"))
+
+    def issue_task(self, value: Mapping[str, object], *, actor_uid) -> dict:
+        try:
+            contract = EdgeTaskContract.from_mapping(value)
+        except EdgeContractError as exc:
+            raise EdgeGatewayValidationError("edge task contract is invalid") from exc
+        if _uuid(contract.gateway_id, "gateway_id") != contract.gateway_id:
+            raise EdgeGatewayValidationError("gateway_id is invalid")
+        gateway = self.repository.gateway_binding(contract.gateway_id)
+        if gateway is None or gateway["status"] not in {"active", "offline"}:
+            self.repository.rollback()
+            raise EdgeGatewayNotFoundError("edge gateway was not found")
+        if any(
+            getattr(contract, field) != gateway[field]
+            for field in ("environment", "network_zone", "policy_digest")
+        ):
+            self._failure(
+                contract.gateway_id, "binding_rejected", "task_binding_mismatch",
+                trusted=True,
+            )
+            raise EdgeGatewayValidationError("edge task gateway binding is invalid")
+        authority = self._signed_task_envelope(contract)
+        row = self.repository.insert_task(
+            contract.to_mapping(), contract.digest, authority,
+            _uuid(actor_uid, "actor_uid"),
+        )
+        if not row:
+            raise EdgeGatewayNotFoundError("edge gateway was not found")
+        if row["contract_digest"] != contract.digest:
+            self._failure(
+                contract.gateway_id, "idempotency_conflict", "task_contract_conflict",
+                trusted=True,
+            )
+            raise EdgeGatewayConflictError("task idempotency key conflicts with an existing contract")
+        return {"task_id": row["task_id"], "contract_digest": row["contract_digest"]}
+
+    def pull_task(self, **auth) -> dict:
+        identity = self.authenticate(**auth, lock=True)
+        token = "dopl_" + secrets.token_urlsafe(32)
+        row = self.repository.pull_task(identity["gateway_id"], _digest(token))
+        if row is None:
+            return {"task": None}
+        envelope = dict(row["authority"])
+        if envelope.get("task") != row["contract"]:
+            self.repository.rollback()
+            raise EdgeGatewayConfigurationError("stored task authority is invalid")
+        return {
+            "task": row["contract"],
+            "signed_task_envelope": envelope,
+            "lease_token": token,
+            "lease_expires_at": row["lease_expires_at"].isoformat(),
+        }
+
+    def cancel_task(self, task_id, *, actor_uid) -> bool:
+        return self.repository.cancel_task(_text(task_id, "task_id", 255), _uuid(actor_uid, "actor_uid"))
+
+    def task_outcome(self, task_id, *, outcome, lease_token, safe_summary, **auth) -> dict:
+        if outcome not in {"completed", "failed", "cancelled"}:
+            raise EdgeGatewayValidationError("task outcome is invalid")
+        if not isinstance(lease_token, str) or not lease_token.startswith("dopl_") or len(lease_token) > 128:
+            raise EdgeGatewayAuthenticationError("edge task lease binding was rejected")
+        summary = _safe_summary(safe_summary)
+        identity = self.authenticate(**auth, lock=True)
+        result = self.repository.task_outcome(
+            identity["gateway_id"], _text(task_id, "task_id", 255),
+            _digest(lease_token), outcome, summary,
+        )
+        if result is None:
+            self._failure(
+                identity["gateway_id"], "binding_rejected", "task_outcome_rejected",
+                trusted=True,
+            )
+            raise EdgeGatewayConflictError("task outcome was rejected")
+        return result
+
+    def reconcile(
+        self, *, limit=50, cancel_cursor=None, release_cursor=None, **auth
+    ) -> dict:
+        limit, _ = _page(limit, 0)
+        if cancel_cursor is not None:
+            cancel_cursor = _text(cancel_cursor, "cancel_cursor", 255)
+        if release_cursor is not None:
+            release_cursor = _uuid(release_cursor, "release_cursor")
+        identity = self.authenticate(**auth, lock=True)
+        cancellations = self.repository.cancelled_tasks(
+            identity["gateway_id"], limit=limit, cursor=cancel_cursor
+        )
+        releases = self.repository.unfinished_releases(
+            identity["gateway_id"], limit=limit, cursor=release_cursor
+        )
+        result = {
+            "cancelled_task_ids": cancellations["items"],
+            "cancel_next_cursor": cancellations["next_cursor"],
+            "release_offers": releases["items"],
+            "release_next_cursor": releases["next_cursor"],
+            "release_baseline": self.repository.latest_release_baseline(
+                identity["gateway_id"]
+            ),
+        }
+        self.repository.commit()
+        return result
+
+    def accept_event(self, *, event, lease_token=None, **auth) -> dict:
+        identity = self.authenticate(**auth, lock=True)
+        if not isinstance(event, Mapping):
+            self.repository.rollback()
+            raise EdgeGatewayValidationError("edge event contract is invalid")
+        policy = EdgeEgressPolicy(allowed_control_hosts=set(identity["allowed_control_hosts"]), allowed_proxy_hosts=set(identity["allowed_proxy_hosts"]))
+        try:
+            approved = policy.approve_event(event)
+        except EdgePolicyError as exc:
+            self._failure(identity["gateway_id"], "policy_rejected", "egress_policy_rejected", trusted=True)
+            raise EdgeGatewayValidationError("edge event is not approved for control-plane egress") from exc
+        try:
+            contract = EdgeEventContract.from_mapping(approved)
+        except EdgeContractError as exc:
+            self._failure(identity["gateway_id"], "binding_rejected", "event_contract_invalid", trusted=True)
+            raise EdgeGatewayValidationError("edge event contract is invalid") from exc
+        if contract.gateway_id != identity["gateway_id"] or contract.environment != identity["environment"] or contract.network_zone != identity["network_zone"] or contract.policy_digest != identity["policy_digest"]:
+            self._failure(identity["gateway_id"], "binding_rejected", "event_gateway_mismatch", trusted=True)
+            raise EdgeGatewayAuthenticationError("edge event binding was rejected")
+        supplied_digest = contract.digest
+        existing = self.repository.event_by_id(identity["gateway_id"], contract.event_id)
+        if existing is not None:
+            if not hmac.compare_digest(existing["contract_digest"], supplied_digest):
+                self._failure(identity["gateway_id"], "idempotency_conflict", "event_contract_conflict", trusted=True)
+                raise EdgeGatewayConflictError("event identifier conflicts with an existing contract")
+            self.repository.rollback()
+            return dict(existing["ack"])
+        if not isinstance(lease_token, str) or not lease_token.startswith("dopl_") or len(lease_token) > 128:
+            self._failure(identity["gateway_id"], "binding_rejected", "lease_invalid", trusted=True)
+            raise EdgeGatewayAuthenticationError("edge task lease binding was rejected")
+        remote_lease_digest = _digest(lease_token)
+        task_contract = self.repository.task_lease_binding(
+            contract.task_id, contract.gateway_id, remote_lease_digest
+        )
+        if task_contract is None:
+            existing = self.repository.event_by_id(identity["gateway_id"], contract.event_id)
+            if existing is not None and hmac.compare_digest(existing["contract_digest"], supplied_digest):
+                self.repository.rollback()
+                return dict(existing["ack"])
+            self._failure(identity["gateway_id"], "binding_rejected", "lease_mismatch", trusted=True)
+            raise EdgeGatewayAuthenticationError("edge task lease binding was rejected")
+        if any(contract.to_mapping()[field] != task_contract[field] for field in (
+            "gateway_id", "environment", "network_zone", "purpose", "policy_digest",
+            "attempt",
+        )):
+            self._failure(identity["gateway_id"], "binding_rejected", "event_task_mismatch", trusted=True)
+            raise EdgeGatewayValidationError("edge event task binding is invalid")
+        ack = {
+            "event_id": contract.event_id,
+            "event_digest": contract.digest,
+            "remote_lease_digest": remote_lease_digest,
+            "received_at": datetime.now(UTC).isoformat().replace("+00:00", "Z"),
+            "status": "accepted",
+        }
+        accepted = self.repository.insert_event(contract.to_mapping(), contract.digest, ack)
+        if accepted is None:
+            self._failure(identity["gateway_id"], "idempotency_conflict", "event_contract_conflict", trusted=True)
+            raise EdgeGatewayConflictError("event identifier conflicts with an existing contract")
+        return accepted
+
+    def offer_release(
+        self, gateway_id, *, version, artifact_digest, rollback_version,
+        request_id, actor_uid, artifact_name=None, deadline_at=None,
+    ) -> dict:
+        gateway_id = _uuid(gateway_id, "gateway_id")
+        version = _release_version(version, "version")
+        artifact_digest = _sha(artifact_digest, "artifact_digest")
+        rollback_version = _release_version(rollback_version, "rollback_version")
+        if _release_version_tuple(version) <= _release_version_tuple(rollback_version):
+            raise EdgeGatewayValidationError(
+                "release version must be newer than rollback_version"
+            )
+        request_id = _text(request_id, "request_id", 255)
+        now = self._clock().astimezone(UTC)
+        supplied_deadline = deadline_at
+        artifact_name = _artifact_name(
+            artifact_name or f"edge-agent-{version}.bin"
+        )
+        deadline_at = _future_deadline(
+            deadline_at
+            or (now + timedelta(hours=24)).isoformat().replace("+00:00", "Z"),
+            now=now,
+        )
+        unsigned = {
+            "release_id": str(uuid.uuid4()), "version": version,
+            "rollback_version": rollback_version,
+            "artifact_digest": artifact_digest, "artifact_name": artifact_name,
+            "deadline_at": deadline_at, "status": "offered",
+        }
+        manifest = self._signed_release_manifest(unsigned)
+        request_digest = canonical_sha256({
+            "gateway_id": gateway_id, "version": version,
+            "artifact_digest": artifact_digest, "rollback_version": rollback_version,
+            "artifact_name": artifact_name,
+            "deadline_at": deadline_at if supplied_deadline is not None else None,
+        })
+        result = self.repository.offer_release(
+            gateway_id, manifest, _uuid(actor_uid, "actor_uid"), request_id,
+            request_digest,
+        )
+        if result.get("conflict"):
+            raise EdgeGatewayConflictError("release request conflicts with an existing request")
+        if not result:
+            raise EdgeGatewayNotFoundError("edge gateway was not found")
+        return result
+
+    def acknowledge_release(self, *, release_id, outcome, safe_summary, **auth) -> dict:
+        statuses = {"accepted": "accepted", "installed": "installed", "failed": "failed", "rollback": "rolled_back"}
+        if outcome not in statuses:
+            raise EdgeGatewayValidationError("release outcome is invalid")
+        release_id = _uuid(release_id, "release_id")
+        safe_summary = _safe_summary(safe_summary)
+        identity = self.authenticate(**auth, lock=True)
+        current = self.repository.release_for_ack(identity["gateway_id"], release_id)
+        if current is None:
+            self.repository.rollback()
+            self._failure(
+                identity["gateway_id"], "release_rejected",
+                "release_transition_invalid", trusted=True,
+            )
+            raise EdgeGatewayConflictError("release acknowledgement was rejected")
+        stored = dict(current["signed_manifest"])
+        unsigned = {
+            key: value for key, value in stored.items()
+            if key not in {
+                "manifest_digest", "signature", "signature_algorithm", "key_id"
+            }
+        }
+        unsigned["status"] = statuses[outcome]
+        manifest = self._signed_release_manifest(unsigned)
+        row = self.repository.acknowledge_release(
+            identity["gateway_id"], release_id, statuses[outcome], safe_summary,
+            manifest,
+        )
+        if row is None:
+            self._failure(identity["gateway_id"], "release_rejected", "release_transition_invalid", trusted=True)
+            raise EdgeGatewayConflictError("release acknowledgement was rejected")
+        return row

+ 26 - 0
deployment/app/core/system/permissions.py

@@ -76,6 +76,9 @@ IDENTITY_OPERATE = "identity:operate"
 CONNECTORS_READ = "connectors:read"
 CONNECTORS_OPERATE = "connectors:operate"
 CONNECTORS_MANAGE = "connectors:manage"
+EDGE_GATEWAYS_READ = "edge-gateways:read"
+EDGE_GATEWAYS_OPERATE = "edge-gateways:operate"
+EDGE_GATEWAYS_MANAGE = "edge-gateways:manage"
 
 ROLE_PERMISSIONS = {
     "viewer": frozenset(
@@ -92,6 +95,7 @@ ROLE_PERMISSIONS = {
             SECURITY_GOVERNANCE_READ,
             IDENTITY_READ,
             CONNECTORS_READ,
+            EDGE_GATEWAYS_READ,
         }
     ),
     "editor": frozenset(
@@ -133,6 +137,8 @@ ROLE_PERMISSIONS = {
             IDENTITY_OPERATE,
             CONNECTORS_READ,
             CONNECTORS_OPERATE,
+            EDGE_GATEWAYS_READ,
+            EDGE_GATEWAYS_OPERATE,
         }
     ),
     "admin": frozenset(
@@ -206,6 +212,9 @@ ROLE_PERMISSIONS = {
             CONNECTORS_READ,
             CONNECTORS_OPERATE,
             CONNECTORS_MANAGE,
+            EDGE_GATEWAYS_READ,
+            EDGE_GATEWAYS_OPERATE,
+            EDGE_GATEWAYS_MANAGE,
         }
     ),
 }
@@ -225,6 +234,7 @@ def permission_for_request(path: str, method: str) -> tuple[str, ...]:
         ("/api/system/identity/exchange", "POST"),
         ("/api/system/identity/refresh", "POST"),
         ("/api/datasource/connectors/machine/runs", "POST"),
+        ("/api/datasource/edge/register", "POST"),
     }
     if (path, method) in public_methods:
         return (PUBLIC,)
@@ -234,6 +244,22 @@ def permission_for_request(path: str, method: str) -> tuple[str, ...]:
         and method == "POST"
     ):
         return (PUBLIC,)
+    if path.startswith("/api/datasource/edge/gateways/") and method == "POST":
+        suffix = path.rsplit("/", 1)[-1]
+        if suffix in {"heartbeat", "pull", "events", "ack", "reconcile", "outcome"}:
+            return (PUBLIC,)
+    if path.startswith("/api/datasource/edge"):
+        if path == "/api/datasource/edge/gateways" and method == "GET":
+            return (EDGE_GATEWAYS_READ,)
+        if path == "/api/datasource/edge/enrollments" and method == "POST":
+            return (EDGE_GATEWAYS_MANAGE,)
+        if path == "/api/datasource/edge/tasks" and method == "POST":
+            return (EDGE_GATEWAYS_OPERATE,)
+        if path.startswith("/api/datasource/edge/tasks/") and path.endswith("/cancel") and method == "POST":
+            return (EDGE_GATEWAYS_OPERATE,)
+        if method == "GET":
+            return (EDGE_GATEWAYS_READ,)
+        return (EDGE_GATEWAYS_MANAGE,)
     if path.startswith("/api/system/identity"):
         if path == "/api/system/identity/logout" and method == "POST":
             return (IDENTITY_READ,)

+ 19 - 0
deployment/app/edge_gateway/__init__.py

@@ -0,0 +1,19 @@
+"""Pull-only enterprise edge runtime.
+
+The package intentionally exposes no listener or inbound server.  All control
+plane communication is initiated by :class:`EdgeTransport`.
+"""
+
+from .agent import EdgeAgent, EdgeRunnerAdapter, SignedReleaseManager
+from .bootstrap import EdgeBootstrapConfig
+from .transport import EdgeAuthenticationStopped, EdgeTransport, EdgeTransportError
+
+__all__ = [
+    "EdgeAgent",
+    "EdgeAuthenticationStopped",
+    "EdgeBootstrapConfig",
+    "EdgeRunnerAdapter",
+    "EdgeTransport",
+    "EdgeTransportError",
+    "SignedReleaseManager",
+]

+ 5 - 0
deployment/app/edge_gateway/__main__.py

@@ -0,0 +1,5 @@
+"""Run the enterprise edge process."""
+
+from app.edge_gateway.runtime import run
+
+raise SystemExit(run())

+ 1273 - 0
deployment/app/edge_gateway/agent.py

@@ -0,0 +1,1273 @@
+"""Durable pull-only edge agent, closed runner adapter, and release verifier."""
+
+from __future__ import annotations
+
+import base64
+import hashlib
+import os
+import re
+import sqlite3
+import stat
+from collections.abc import Callable, Mapping
+from datetime import UTC, datetime, timedelta
+from pathlib import Path, PurePosixPath
+from types import MappingProxyType
+
+from cryptography.exceptions import InvalidSignature
+from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
+from cryptography.hazmat.primitives.ciphers.aead import AESGCM
+from cryptography.hazmat.primitives.kdf.hkdf import HKDF
+
+from app.core.edge_gateway.contracts import (
+    EdgeContractError,
+    EdgeEventContract,
+    EdgeTaskContract,
+    SignedTaskEnvelope,
+    canonical_json_bytes,
+    canonical_sha256,
+    canonical_timestamp,
+    stable_event_id,
+)
+from app.core.edge_gateway.policy import EdgeEgressPolicy, EdgePolicyError
+from app.core.edge_gateway.queue import (
+    EdgeQueueConflictError,
+    EdgeQueueLeaseError,
+    SqliteEdgeQueue,
+)
+from app.edge_gateway.bootstrap import EdgeBootstrapConfig
+from app.edge_gateway.transport import EdgeAuthenticationStopped, EdgeTransportError
+
+_SHA256 = re.compile(r"^[0-9a-f]{64}$")
+_VERSION = re.compile(r"^(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})$")
+_RELEASE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,254}$")
+
+EDGE_NODE_BY_OPERATION = MappingProxyType(
+    {
+        "collect": "edge.collect",
+        "profile": "edge.profile",
+        "quality": "quality.check",
+        "lineage": "edge.lineage",
+        "controlled_query": "edge.controlled_query",
+    }
+)
+EVENT_CLASSIFICATION_BY_OPERATION = MappingProxyType(
+    {
+        "collect": "desensitized_metadata",
+        "profile": "statistics",
+        "quality": "evidence",
+        "lineage": "lineage",
+        "controlled_query": "statistics",
+    }
+)
+DEFAULT_GOVERNED_PURPOSES = frozenset(
+    {
+        "inventory",
+        "governed-inventory",
+        "metadata-inventory",
+        "profile-statistics",
+        "quality-evaluation",
+        "lineage-capture",
+        "controlled-query",
+    }
+)
+_RUNNER_REQUEST_FIELDS = frozenset(
+    {
+        "task_id",
+        "operation",
+        "purpose",
+        "classification",
+        "environment",
+        "network_zone",
+        "idempotency_key",
+        "deadline_at",
+    }
+)
+_RUNNER_RESULT_FIELDS = frozenset(
+    {"classification", "payload", "local_artifact_digest", "local_artifact_ref"}
+)
+
+
+class EdgeAgentError(RuntimeError):
+    """Bounded edge runtime failure."""
+
+
+class EdgeTaskCancelled(EdgeAgentError):
+    """Cooperative cancellation was observed before a terminal event."""
+
+
+class EdgeReleaseError(EdgeAgentError):
+    """A candidate release failed a closed verification or activation gate."""
+
+
+class LocalArtifactStore:
+    """Validate and delete local artifacts without exposing their paths outbound."""
+
+    def __init__(self, root: str | Path, *, clock: Callable[[], object] | None = None):
+        candidate = Path(root)
+        if not candidate.is_absolute() or not candidate.is_dir() or candidate.is_symlink():
+            raise ValueError("artifact root must be an existing absolute directory")
+        self.root = candidate.resolve()
+        self.clock = clock or (lambda: datetime.now(UTC))
+
+    def inspect(self, reference: object, expected_digest: object) -> dict[str, str]:
+        if (
+            not isinstance(reference, str)
+            or not reference
+            or "\x00" in reference
+            or len(reference.encode("utf-8")) > 2_048
+            or not isinstance(expected_digest, str)
+            or not _SHA256.fullmatch(expected_digest)
+        ):
+            raise EdgePolicyError("local artifact reference is invalid")
+        raw = Path(reference)
+        path = raw if raw.is_absolute() else self.root / raw
+        try:
+            resolved = path.resolve(strict=True)
+            resolved.relative_to(self.root)
+            current = self.root
+            for part in resolved.relative_to(self.root).parts:
+                current = current / part
+                if current.is_symlink():
+                    raise EdgePolicyError("local artifact symlink is forbidden")
+            metadata = resolved.lstat()
+        except (OSError, ValueError) as exc:
+            raise EdgePolicyError("local artifact escapes its configured root") from exc
+        if not stat.S_ISREG(metadata.st_mode):
+            raise EdgePolicyError("local artifact must be a regular file")
+        digest = hashlib.sha256()
+        try:
+            with resolved.open("rb") as stream:
+                for chunk in iter(lambda: stream.read(1_048_576), b""):
+                    digest.update(chunk)
+        except OSError as exc:
+            raise EdgePolicyError("local artifact cannot be read") from exc
+        if digest.hexdigest() != expected_digest:
+            raise EdgePolicyError("local artifact digest does not match content")
+        canonical_ref = str(resolved)
+        return {
+            "artifact_digest": expected_digest,
+            "artifact_ref": canonical_ref,
+            "artifact_ref_hash": canonical_sha256(canonical_ref),
+        }
+
+    def delete(self, reference: object, expected_digest: object) -> dict[str, str]:
+        inspected = self.inspect(reference, expected_digest)
+        try:
+            Path(inspected["artifact_ref"]).unlink()
+        except OSError as exc:
+            raise EdgePolicyError("local artifact deletion failed") from exc
+        return {
+            "artifact_digest": inspected["artifact_digest"],
+            "artifact_ref_hash": inspected["artifact_ref_hash"],
+            "deleted_at": canonical_timestamp(
+                _now(self.clock).isoformat().replace("+00:00", "Z")
+            ),
+            "status": "deleted",
+        }
+
+    def receipt_for_missing(
+        self, reference: object, expected_digest: object, expected_ref_hash: object
+    ) -> dict[str, str]:
+        if (
+            not isinstance(reference, str)
+            or not isinstance(expected_digest, str)
+            or not _SHA256.fullmatch(expected_digest)
+            or not isinstance(expected_ref_hash, str)
+            or not _SHA256.fullmatch(expected_ref_hash)
+        ):
+            raise EdgePolicyError("missing artifact recovery metadata is invalid")
+        path = Path(reference)
+        try:
+            path.parent.resolve(strict=True).relative_to(self.root)
+            canonical_ref = str(path.resolve(strict=False))
+        except (OSError, ValueError) as exc:
+            raise EdgePolicyError("missing artifact escapes its configured root") from exc
+        if path.exists() or path.is_symlink() or canonical_sha256(canonical_ref) != expected_ref_hash:
+            raise EdgePolicyError("missing artifact recovery proof is invalid")
+        return {
+            "artifact_digest": expected_digest,
+            "artifact_ref_hash": expected_ref_hash,
+            "deleted_at": canonical_timestamp(
+                _now(self.clock).isoformat().replace("+00:00", "Z")
+            ),
+            "status": "deleted",
+        }
+
+
+def _now(clock: Callable[[], object]) -> datetime:
+    value = clock()
+    if not isinstance(value, datetime) or value.tzinfo is None or value.utcoffset() is None:
+        raise ValueError("edge clock must return a timezone-aware datetime")
+    return value.astimezone(UTC)
+
+
+def _timestamp(clock: Callable[[], object]) -> str:
+    return canonical_timestamp(_now(clock).isoformat().replace("+00:00", "Z"))
+
+
+def _remote_lease_codec(config: EdgeBootstrapConfig):
+    """Protect recoverable control leases without persisting the credential."""
+
+    key = HKDF(
+        algorithm=hashes.SHA256(),
+        length=32,
+        salt=bytes.fromhex(config.certificate_sha256),
+        info=(
+            f"dataops-edge-remote-lease-v1:{config.gateway_id}:"
+            f"{config.environment}:{config.network_zone}"
+        ).encode(),
+    ).derive(config.credential.encode())
+    cipher = AESGCM(key)
+    aad = f"{config.gateway_id}:{config.policy_digest}".encode()
+
+    def encode(token: str) -> str:
+        nonce = os.urandom(12)
+        protected = cipher.encrypt(nonce, token.encode("utf-8"), aad)
+        return base64.urlsafe_b64encode(nonce + protected).decode("ascii")
+
+    def decode(value: str) -> str:
+        raw = base64.b64decode(value.encode("ascii"), altchars=b"-_", validate=True)
+        if len(raw) < 29:
+            raise ValueError("protected remote lease is invalid")
+        return cipher.decrypt(raw[:12], raw[12:], aad).decode("utf-8")
+
+    return encode, decode
+
+
+class EdgeRunnerAdapter:
+    """Map governed operations to exact local runner handlers.
+
+    No control-plane supplied parameters, module names, commands, SQL, paths, or
+    network destinations are accepted by this adapter.  Local handlers resolve
+    their governed configuration using the immutable task identity.
+    """
+
+    def __init__(
+        self,
+        executor: Callable[[str, Mapping[str, object], Callable[[], bool]], object],
+        *,
+        policy: EdgeEgressPolicy | None = None,
+        allowed_purposes: set[str] | frozenset[str] = DEFAULT_GOVERNED_PURPOSES,
+    ) -> None:
+        if not callable(executor):
+            raise ValueError("an explicit edge runner executor is required")
+        self._executor = executor
+        self.policy = policy or EdgeEgressPolicy(allowed_control_hosts=set())
+        if not isinstance(allowed_purposes, (set, frozenset)) or not allowed_purposes:
+            raise ValueError("explicit governed purposes are required")
+        if any(
+            not isinstance(item, str)
+            or not item
+            or len(item.encode("utf-8")) > 255
+            for item in allowed_purposes
+        ):
+            raise ValueError("governed purpose allowlist is invalid")
+        self.allowed_purposes = frozenset(allowed_purposes)
+
+    def execute(
+        self, task: EdgeTaskContract, cancel_requested: Callable[[], bool]
+    ) -> dict[str, object]:
+        task = EdgeTaskContract.from_mapping(task.to_mapping())
+        node = EDGE_NODE_BY_OPERATION.get(task.operation)
+        if node is None:
+            raise EdgePolicyError("edge task operation is not runner-approved")
+        if task.purpose not in self.allowed_purposes:
+            raise EdgePolicyError("edge task purpose is not runner-approved")
+        if cancel_requested():
+            raise EdgeTaskCancelled("edge task was cancelled")
+        request = {
+            "task_id": task.task_id,
+            "operation": task.operation,
+            "purpose": task.purpose,
+            "classification": task.classification,
+            "environment": task.environment,
+            "network_zone": task.network_zone,
+            "idempotency_key": task.idempotency_key,
+            "deadline_at": task.deadline_at,
+        }
+        if set(request) != _RUNNER_REQUEST_FIELDS:  # pragma: no cover - invariant
+            raise EdgePolicyError("edge runner request schema is invalid")
+        try:
+            raw = self._executor(node, MappingProxyType(request), cancel_requested)
+        except EdgeTaskCancelled:
+            raise
+        except (MemoryError, RecursionError) as exc:
+            raise EdgePolicyError("edge runner result exceeds safe limits") from exc
+        if cancel_requested():
+            raise EdgeTaskCancelled("edge task was cancelled")
+        if not isinstance(raw, Mapping):
+            raise EdgePolicyError("edge runner result must be a mapping")
+        unknown = set(raw) - _RUNNER_RESULT_FIELDS
+        missing = {"classification", "payload"} - set(raw)
+        if unknown or missing:
+            raise EdgePolicyError("edge runner result schema is invalid")
+        classification = raw["classification"]
+        expected = EVENT_CLASSIFICATION_BY_OPERATION[task.operation]
+        if classification != expected:
+            raise EdgePolicyError("edge runner result classification is invalid")
+        payload = raw["payload"]
+        if not isinstance(payload, Mapping):
+            raise EdgePolicyError("edge runner result payload must be a mapping")
+        try:
+            self.policy.validate_approved_payload(expected, payload)
+        except (EdgePolicyError, EdgeContractError, MemoryError, RecursionError) as exc:
+            raise EdgePolicyError("edge runner result is not approved for egress") from exc
+        artifact_digest = raw.get("local_artifact_digest")
+        artifact_ref = raw.get("local_artifact_ref")
+        if (artifact_digest is None) != (artifact_ref is None):
+            raise EdgePolicyError("local artifact digest and reference must be paired")
+        if artifact_digest is not None:
+            if not isinstance(artifact_digest, str) or not _SHA256.fullmatch(artifact_digest):
+                raise EdgePolicyError("local artifact digest is invalid")
+            if (
+                not isinstance(artifact_ref, str)
+                or not artifact_ref
+                or "\x00" in artifact_ref
+                or len(artifact_ref.encode("utf-8")) > 2_048
+            ):
+                raise EdgePolicyError("local artifact reference is invalid")
+        return {
+            "classification": expected,
+            "payload": dict(payload),
+            "local_artifact_digest": artifact_digest,
+            "local_artifact_ref": artifact_ref,
+        }
+
+
+class SignedReleaseManager:
+    """Verify Ed25519 manifests and activate candidates through injected primitives."""
+
+    _MANIFEST_FIELDS = frozenset(
+        {
+            "release_id",
+            "version",
+            "rollback_version",
+            "artifact_digest",
+            "artifact_name",
+            "deadline_at",
+            "signature_algorithm",
+            "key_id",
+            "manifest_digest",
+            "signature",
+            "status",
+        }
+    )
+
+    def __init__(
+        self,
+        *,
+        current_version: str,
+        trusted_keys: Mapping[str, str],
+        artifact_loader: Callable[[Mapping[str, object]], bytes],
+        activator: Callable[[str, bytes], bool],
+        rollback: Callable[[str], bool],
+        clock: Callable[[], object] | None = None,
+    ) -> None:
+        if not _VERSION.fullmatch(current_version):
+            raise ValueError("current release version is invalid")
+        if any(int(part) > 2_147_483_647 for part in current_version.split(".")):
+            raise ValueError("current release version is invalid")
+        if not trusted_keys:
+            raise ValueError("trusted release keys are required")
+        self.current_version = current_version
+        self.trusted_keys = dict(trusted_keys)
+        self.artifact_loader = artifact_loader
+        self.activator = activator
+        self.rollback = rollback
+        self.clock = clock or (lambda: datetime.now(UTC))
+        self._seen: dict[str, str] = {}
+
+    @staticmethod
+    def _version_tuple(value: str) -> tuple[int, ...]:
+        return tuple(int(item) for item in value.split("."))
+
+    def verify(
+        self,
+        value: Mapping[str, object],
+        *,
+        allow_recovery: bool = False,
+    ) -> dict[str, object]:
+        if not isinstance(value, Mapping) or set(value) != self._MANIFEST_FIELDS:
+            raise EdgeReleaseError("release manifest schema is invalid")
+        manifest = dict(value)
+        release_id = manifest["release_id"]
+        version = manifest["version"]
+        rollback_version = manifest["rollback_version"]
+        if not isinstance(release_id, str) or not _RELEASE_ID.fullmatch(release_id):
+            raise EdgeReleaseError("release identifier is invalid")
+        status = manifest.get("status")
+        if status not in {"offered", "accepted", "failed", "installed", "rolled_back"}:
+            raise EdgeReleaseError("release state is invalid")
+        if (
+            not isinstance(version, str)
+            or not isinstance(rollback_version, str)
+            or not _VERSION.fullmatch(version)
+            or not _VERSION.fullmatch(rollback_version)
+            or any(int(part) > 2_147_483_647 for part in version.split("."))
+            or any(int(part) > 2_147_483_647 for part in rollback_version.split("."))
+        ):
+            raise EdgeReleaseError("release downgrade or rollback binding was rejected")
+        if not allow_recovery and status in {"offered", "accepted"} and (
+            rollback_version != self.current_version
+            or self._version_tuple(version) <= self._version_tuple(self.current_version)
+        ):
+            raise EdgeReleaseError("release downgrade or rollback binding was rejected")
+        artifact_name = manifest["artifact_name"]
+        if (
+            not isinstance(artifact_name, str)
+            or not artifact_name
+            or len(artifact_name.encode("utf-8")) > 255
+            or PurePosixPath(artifact_name).name != artifact_name
+            or artifact_name in {".", ".."}
+            or any(character in artifact_name for character in ("/", "\\", "\x00"))
+        ):
+            raise EdgeReleaseError("release artifact path was rejected")
+        if manifest["signature_algorithm"] != "Ed25519":
+            raise EdgeReleaseError("release signature algorithm is not approved")
+        key_id = manifest["key_id"]
+        key_hex = self.trusted_keys.get(key_id) if isinstance(key_id, str) else None
+        if key_hex is None:
+            raise EdgeReleaseError("release signing key is not trusted")
+        for field in ("artifact_digest", "manifest_digest"):
+            if not isinstance(manifest[field], str) or not _SHA256.fullmatch(manifest[field]):
+                raise EdgeReleaseError("release digest is invalid")
+        try:
+            deadline = datetime.fromisoformat(str(manifest["deadline_at"]).replace("Z", "+00:00"))
+        except ValueError as exc:
+            raise EdgeReleaseError("release deadline is invalid") from exc
+        if deadline.tzinfo is None or deadline.astimezone(UTC) <= _now(self.clock):
+            raise EdgeReleaseError("release deadline has expired")
+        try:
+            canonical_deadline = canonical_timestamp(manifest["deadline_at"], "deadline_at")
+        except EdgeContractError as exc:
+            raise EdgeReleaseError("release deadline is invalid") from exc
+        if canonical_deadline != manifest["deadline_at"]:
+            raise EdgeReleaseError("release deadline must use canonical UTC form")
+        unsigned = {
+            key: manifest[key]
+            for key in sorted(self._MANIFEST_FIELDS - {"manifest_digest", "signature"})
+        }
+        expected_digest = canonical_sha256(unsigned)
+        if expected_digest != manifest["manifest_digest"]:
+            raise EdgeReleaseError("release manifest digest does not match")
+        content_digest = canonical_sha256(
+            {
+                key: manifest[key]
+                for key in (
+                    "release_id",
+                    "version",
+                    "rollback_version",
+                    "artifact_digest",
+                    "artifact_name",
+                    "deadline_at",
+                )
+            }
+        )
+        prior = self._seen.get(release_id)
+        if prior is not None and prior != content_digest:
+            raise EdgeReleaseError("release identifier replay changed its digest")
+        try:
+            signature = bytes.fromhex(str(manifest["signature"]))
+            Ed25519PublicKey.from_public_bytes(bytes.fromhex(key_hex)).verify(
+                signature, canonical_json_bytes(unsigned)
+            )
+        except (ValueError, InvalidSignature) as exc:
+            raise EdgeReleaseError("release signature verification failed") from exc
+        self._seen[release_id] = content_digest
+        return manifest
+
+    def install(self, value: Mapping[str, object]) -> dict[str, object]:
+        manifest = self.verify(value)
+        release_id = str(manifest["release_id"])
+        previous = self.current_version
+        try:
+            artifact = self.artifact_loader(MappingProxyType(manifest))
+            if not isinstance(artifact, bytes) or hashlib.sha256(artifact).hexdigest() != manifest["artifact_digest"]:
+                raise EdgeReleaseError("release artifact digest does not match")
+            if not self.activator(str(manifest["version"]), artifact):
+                raise EdgeReleaseError("release candidate health confirmation failed")
+        except Exception:
+            rolled_back = bool(self.rollback(previous))
+            return {
+                "release_id": release_id,
+                "outcome": "rollback" if rolled_back else "failed",
+                "summary": {
+                    "release_status": "rolled_back" if rolled_back else "failed",
+                    "version": previous,
+                },
+            }
+        self.current_version = str(manifest["version"])
+        return {
+            "release_id": release_id,
+            "outcome": "installed",
+            "summary": {"release_status": "installed", "version": self.current_version},
+        }
+
+
+class EdgeAgent:
+    """One durable worker for pull, execute, persist, and acknowledge cycles."""
+
+    def __init__(
+        self,
+        config: EdgeBootstrapConfig,
+        transport,
+        runner: EdgeRunnerAdapter,
+        *,
+        worker_id: str = "edge-worker-1",
+        clock: Callable[[], object] | None = None,
+        random: Callable[[], float] | None = None,
+        release_manager: SignedReleaseManager | None = None,
+        artifact_store: LocalArtifactStore | None = None,
+    ) -> None:
+        if not isinstance(config, EdgeBootstrapConfig):
+            raise ValueError("validated edge bootstrap configuration is required")
+        if not isinstance(runner, EdgeRunnerAdapter):
+            raise ValueError("validated edge runner adapter is required")
+        if not isinstance(worker_id, str) or not worker_id or len(worker_id.encode()) > 255:
+            raise ValueError("worker_id is invalid")
+        self.config = config
+        self.transport = transport
+        self.runner = runner
+        self.worker_id = worker_id
+        self.clock = clock or (lambda: datetime.now(UTC))
+        self.random = random or __import__("random").random
+        self.policy = EdgeEgressPolicy(
+            allowed_control_hosts=set(config.allowed_control_hosts),
+            allowed_proxy_hosts=set(config.allowed_proxy_hosts),
+        )
+        lease_encoder, lease_decoder = _remote_lease_codec(config)
+        self.queue = SqliteEdgeQueue(
+            config.queue_path,
+            clock=self.clock,
+            egress_policy=self.policy,
+            random_source=self.random,
+            remote_lease_encoder=lease_encoder,
+            remote_lease_decoder=lease_decoder,
+        )
+        self.artifact_store = artifact_store or LocalArtifactStore(
+            config.artifact_root, clock=self.clock
+        )
+        self.release_manager = release_manager
+        self._cancelled: set[str] = set()
+        self._stopped = False
+        self._needs_remote_lease_task: str | None = None
+        self._trusted_task_keys = {
+            key_id: bytes.fromhex(public_key)
+            for key_id, public_key in config.trusted_task_keys.items()
+        }
+        self.gateway_id_digest = hashlib.sha256(config.gateway_id.encode()).hexdigest()
+
+    def retry_delay(self, attempt: int) -> float:
+        if isinstance(attempt, bool) or not isinstance(attempt, int) or attempt < 1:
+            raise ValueError("retry attempt is invalid")
+        base = min(300.0, float(2 ** min(attempt - 1, 8)))
+        jitter = self.random()
+        if not isinstance(jitter, (int, float)) or not 0 <= jitter <= 1:
+            raise ValueError("random source returned an invalid value")
+        return min(300.0, base * (0.75 + 0.5 * float(jitter)))
+
+    def _validate_binding(self, contract: EdgeTaskContract) -> None:
+        expected = (
+            self.config.gateway_id,
+            self.config.environment,
+            self.config.network_zone,
+            self.config.policy_digest,
+        )
+        actual = (
+            contract.gateway_id,
+            contract.environment,
+            contract.network_zone,
+            contract.policy_digest,
+        )
+        if actual != expected:
+            raise EdgePolicyError("pulled task binding does not match this edge gateway")
+
+    def accept_pulled_task(
+        self,
+        envelope_value: Mapping[str, object],
+        lease_token: str,
+        lease_expires_at: str,
+    ):
+        if not isinstance(lease_token, str) or not lease_token.startswith("dopl_") or len(lease_token) > 128:
+            raise EdgePolicyError("control task lease is invalid")
+        try:
+            envelope = SignedTaskEnvelope.verify_mapping(
+                envelope_value,
+                authority_keys=self._trusted_task_keys,
+                now=_now(self.clock),
+                allowed_future_skew_seconds=(
+                    self.config.task_authority_clock_skew_seconds
+                ),
+            )
+        except EdgeContractError as exc:
+            raise EdgePolicyError("signed task authority was rejected") from exc
+        self._validate_binding(envelope.task)
+        return self.queue.accept_signed_task(
+            envelope,
+            remote_lease_token=lease_token,
+            remote_lease_expires_at=lease_expires_at,
+            remote_attempt=envelope.task.attempt,
+        )
+
+    def _cancel_requested(self, task_id: str) -> bool:
+        if task_id in self._cancelled:
+            return True
+        probe = getattr(self.transport, "cancel_requested", None)
+        if callable(probe):
+            try:
+                if bool(probe(task_id)):
+                    self._cancelled.add(task_id)
+                    return True
+            except EdgeAuthenticationStopped:
+                self._stopped = True
+                raise
+            except EdgeTransportError:
+                return task_id in self._cancelled
+        return False
+
+    def _event(self, task: EdgeTaskContract, result: Mapping[str, object]) -> EdgeEventContract:
+        identity = {
+            "task_id": task.task_id,
+            "gateway_id": task.gateway_id,
+            "environment": task.environment,
+            "network_zone": task.network_zone,
+            "purpose": task.purpose,
+            "classification": result["classification"],
+            "contract_version": task.contract_version,
+            "occurred_at": _timestamp(self.clock),
+            "attempt": task.attempt,
+            "idempotency_key": task.idempotency_key,
+            "policy_digest": task.policy_digest,
+            "payload": result["payload"],
+        }
+        return EdgeEventContract(event_id=stable_event_id(identity), **identity)  # type: ignore[arg-type]
+
+    def _artifact_metadata(
+        self,
+        task: EdgeTaskContract,
+        result: Mapping[str, object],
+    ) -> tuple[dict[str, object], ...]:
+        digest = result.get("local_artifact_digest")
+        reference = result.get("local_artifact_ref")
+        if digest is None or reference is None:
+            return ()
+        inspected = self.artifact_store.inspect(reference, digest)
+        retention_class = (
+            task.classification
+            if task.classification in {"raw", "recent_detail", "evidence"}
+            else "raw"
+            if task.classification == "restricted"
+            else "metadata"
+        )
+        retention_until = _now(self.clock) + timedelta(
+            days=self.policy.retention_days(retention_class)
+        )
+        return (
+            {
+                "artifact_digest": inspected["artifact_digest"],
+                "artifact_ref": inspected["artifact_ref"],
+                "artifact_ref_hash": inspected["artifact_ref_hash"],
+                "classification": task.classification,
+                "retention_until": canonical_timestamp(
+                    retention_until.isoformat().replace("+00:00", "Z")
+                ),
+            },
+        )
+
+    def _drain_artifact_cleanup(self) -> str | None:
+        artifact = self.queue.claim_artifact_cleanup(self.worker_id)
+        if artifact is None:
+            return None
+        if artifact.artifact_ref is None:
+            self.queue.fail_artifact_cleanup(
+                artifact.artifact_digest,
+                lease_token=str(artifact.lease_token),
+                error_code="artifact_reference_missing",
+            )
+            return None
+        try:
+            receipt = self.artifact_store.delete(
+                artifact.artifact_ref, artifact.artifact_digest
+            )
+            self.queue.acknowledge_artifact_cleanup(
+                artifact.artifact_digest,
+                lease_token=str(artifact.lease_token),
+                receipt=receipt,
+            )
+            return artifact.artifact_digest
+        except EdgePolicyError:
+            try:
+                receipt = self.artifact_store.receipt_for_missing(
+                    artifact.artifact_ref,
+                    artifact.artifact_digest,
+                    artifact.artifact_ref_hash,
+                )
+                self.queue.acknowledge_artifact_cleanup(
+                    artifact.artifact_digest,
+                    lease_token=str(artifact.lease_token),
+                    receipt=receipt,
+                )
+                return artifact.artifact_digest
+            except EdgePolicyError:
+                pass
+            self.queue.fail_artifact_cleanup(
+                artifact.artifact_digest,
+                lease_token=str(artifact.lease_token),
+                error_code="artifact_cleanup_failed",
+            )
+            return None
+
+    def _drain_event(self) -> str | None:
+        event = self.queue.claim_event(self.worker_id)
+        if event is None:
+            return None
+        task_id = str(event.event["task_id"])
+        task = self.queue.get_task(task_id)
+        if task is None:
+            self.queue.fail_event(
+                event.event_id,
+                lease_token=str(event.lease_token),
+                error_code="control_lease_unavailable",
+            )
+            return None
+        expiry = task.remote_lease_expires_at
+        expired = (
+            expiry is None
+            or datetime.fromisoformat(expiry.replace("Z", "+00:00")) <= _now(self.clock)
+        )
+        if expired:
+            self._needs_remote_lease_task = task_id
+            self.queue.fail_event(
+                event.event_id,
+                lease_token=str(event.lease_token),
+                error_code="remote_lease_expired",
+            )
+            return None
+        try:
+            control_lease = self.queue.recover_remote_lease(task_id)
+        except EdgeQueueLeaseError:
+            self._needs_remote_lease_task = task_id
+            self.queue.fail_event(
+                event.event_id,
+                lease_token=str(event.lease_token),
+                error_code="control_lease_unavailable",
+            )
+            return None
+        try:
+            ack = self.transport.send_event(event.event, control_lease)
+            self.queue.acknowledge_event(
+                event.event_id,
+                lease_token=str(event.lease_token),
+                acknowledgement=ack,
+            )
+            return event.event_id
+        except EdgeAuthenticationStopped:
+            if not expired:
+                self._stopped = True
+            else:
+                self._needs_remote_lease_task = task_id
+            self.queue.fail_event(
+                event.event_id,
+                lease_token=str(event.lease_token),
+                error_code=(
+                    "credential_stopped" if not expired else "remote_lease_expired"
+                ),
+            )
+            if not expired:
+                raise
+            return None
+        except EdgeTransportError:
+            self.queue.fail_event(
+                event.event_id,
+                lease_token=str(event.lease_token),
+                error_code="control_unavailable",
+            )
+            return None
+
+    def _has_unresolved_events(self) -> bool:
+        connection = sqlite3.connect(f"file:{self.queue.db_path}?mode=ro", uri=True)
+        try:
+            count = connection.execute(
+                "SELECT COUNT(*) FROM edge_outbound_events WHERE status IN ('pending','sending')"
+            ).fetchone()[0]
+        finally:
+            connection.close()
+        return bool(count)
+
+    def _expired_pending_remote_task(self) -> str | None:
+        connection = sqlite3.connect(f"file:{self.queue.db_path}?mode=ro", uri=True)
+        try:
+            row = connection.execute(
+                """SELECT task_id FROM edge_tasks
+                WHERE status='pending' AND (
+                    remote_lease_expires_at IS NULL
+                    OR julianday(remote_lease_expires_at) <= julianday(?)
+                ) ORDER BY created_at,task_id LIMIT 1""",
+                (_timestamp(self.clock),),
+            ).fetchone()
+        finally:
+            connection.close()
+        return str(row[0]) if row is not None else None
+
+    def _apply_reconcile(self, value: Mapping[str, object]) -> tuple[list[str], list[dict]]:
+        legacy = {"cancelled_task_ids", "release_offers"}
+        paged = {
+            "cancelled_task_ids", "cancel_next_cursor", "release_offers",
+            "release_next_cursor", "release_baseline",
+        }
+        if (
+            not isinstance(value, Mapping)
+            or (set(value) != legacy and set(value) != paged)
+        ):
+            raise EdgeTransportError("reconcile response schema is invalid")
+        cancelled = value["cancelled_task_ids"]
+        releases = value["release_offers"]
+        if not isinstance(cancelled, list) or not isinstance(releases, list) or len(cancelled) > 100 or len(releases) > 100:
+            raise EdgeTransportError("reconcile response exceeds safe limits")
+        applied: list[str] = []
+        for task_id in cancelled:
+            if not isinstance(task_id, str) or len(task_id.encode()) > 255:
+                raise EdgeTransportError("cancel reconciliation is invalid")
+            self._cancelled.add(task_id)
+            queued = self.queue.get_task(task_id)
+            if queued is not None and queued.status in {"pending", "leased"}:
+                self.queue.cancel_with_outcome(task_id)
+                applied.append(task_id)
+        release_values: list[dict] = []
+        for release in releases:
+            if not isinstance(release, Mapping):
+                raise EdgeTransportError("release reconciliation is invalid")
+            release_values.append(dict(release))
+        if set(value) == paged and value["release_baseline"] is not None:
+            baseline = value["release_baseline"]
+            if not isinstance(baseline, Mapping):
+                raise EdgeTransportError("release baseline is invalid")
+            if all(
+                item.get("release_id") != baseline.get("release_id")
+                for item in release_values
+            ):
+                release_values.append(dict(baseline))
+        return applied, release_values
+
+    def _drain_outcome(self) -> str | None:
+        outcome = self.queue.claim_outcome(self.worker_id)
+        if outcome is None:
+            return None
+        try:
+            control_lease = self.queue.recover_remote_lease(outcome.task_id)
+            sender = getattr(self.transport, "task_outcome", None)
+            if not callable(sender):
+                raise EdgeTransportError("task outcome transport is unavailable")
+            acknowledgement = sender(
+                outcome.task_id,
+                outcome.outcome["outcome"],
+                control_lease,
+                outcome.outcome["safe_summary"],
+            )
+            self.queue.acknowledge_outcome(
+                outcome.task_id,
+                lease_token=str(outcome.lease_token),
+                acknowledgement=acknowledgement,
+            )
+            return outcome.task_id
+        except EdgeAuthenticationStopped:
+            self._stopped = True
+            self.queue.fail_outcome(
+                outcome.task_id,
+                lease_token=str(outcome.lease_token),
+                error_code="credential_stopped",
+            )
+            raise
+        except (EdgeTransportError, EdgeQueueLeaseError):
+            self.queue.fail_outcome(
+                outcome.task_id,
+                lease_token=str(outcome.lease_token),
+                error_code="control_unavailable",
+            )
+            return None
+
+    def _process_releases(self, releases: list[dict]) -> None:
+        if not releases:
+            return
+        if self.release_manager is None:
+            raise EdgeReleaseError("signed release manager is not configured")
+        for release in releases:
+            verified = self.release_manager.verify(release, allow_recovery=True)
+            release_id = str(verified["release_id"])
+            version = str(verified["version"])
+            rollback_version = str(verified["rollback_version"])
+            content_digest = canonical_sha256(
+                {
+                    key: verified[key]
+                    for key in (
+                        "release_id",
+                        "version",
+                        "rollback_version",
+                        "artifact_digest",
+                        "artifact_name",
+                        "deadline_at",
+                    )
+                }
+            )
+            existing = self.queue.get_release_state(release_id)
+            state = self.queue.put_release_state(
+                release_id=release_id,
+                manifest_digest=content_digest,
+                version=version,
+                rollback_version=rollback_version,
+            )
+            server_status = str(verified["status"])
+            if (
+                existing is None
+                and server_status == "offered"
+                and (
+                    rollback_version != self.release_manager.current_version
+                    or self.release_manager._version_tuple(version)
+                    <= self.release_manager._version_tuple(
+                        self.release_manager.current_version
+                    )
+                )
+            ):
+                raise EdgeReleaseError(
+                    "release downgrade or rollback binding was rejected"
+                )
+
+            if server_status == "rolled_back":
+                while state.status != "rolled_back":
+                    target = (
+                        "accepted"
+                        if state.status == "offered"
+                        else "candidate"
+                        if state.status == "accepted"
+                        else "failed"
+                        if state.status in {"candidate", "installed"}
+                        else "rolled_back"
+                    )
+                    state = self.queue.compare_and_set_release_state(
+                        release_id,
+                        expected_status=state.status,
+                        target_status=target,
+                        previous_version=rollback_version,
+                        current_version=rollback_version,
+                    )
+                self.release_manager.current_version = rollback_version
+                continue
+
+            if server_status == "installed":
+                if state.status in {"offered", "accepted"}:
+                    if state.status == "offered":
+                        state = self.queue.compare_and_set_release_state(
+                            release_id,
+                            expected_status="offered",
+                            target_status="accepted",
+                            previous_version=rollback_version,
+                        )
+                    state = self.queue.compare_and_set_release_state(
+                        release_id,
+                        expected_status="accepted",
+                        target_status="candidate",
+                        previous_version=rollback_version,
+                    )
+                if state.status == "candidate":
+                    state = self.queue.compare_and_set_release_state(
+                        release_id,
+                        expected_status="candidate",
+                        target_status="installed",
+                        previous_version=rollback_version,
+                        current_version=version,
+                    )
+                self.release_manager.current_version = version
+                continue
+
+            if server_status == "offered":
+                if state.status == "offered":
+                    state = self.queue.compare_and_set_release_state(
+                        release_id,
+                        expected_status="offered",
+                        target_status="accepted",
+                        previous_version=rollback_version,
+                    )
+                self.transport.acknowledge_release(
+                    release_id,
+                    "accepted",
+                    {"release_status": "accepted", "version": version},
+                )
+
+            if state.status == "installed":
+                self.transport.acknowledge_release(
+                    release_id,
+                    "installed",
+                    {"release_status": "installed", "version": version},
+                )
+                self.release_manager.current_version = version
+                continue
+            if state.status == "rolled_back":
+                if server_status == "accepted":
+                    self.transport.acknowledge_release(
+                        release_id,
+                        "failed",
+                        {"release_status": "failed", "version": version},
+                    )
+                self.transport.acknowledge_release(
+                    release_id,
+                    "rollback",
+                    {"release_status": "rolled_back", "version": rollback_version},
+                )
+                continue
+            if state.status == "failed" or server_status == "failed":
+                if state.status != "failed":
+                    state = self.queue.compare_and_set_release_state(
+                        release_id,
+                        expected_status=state.status,
+                        target_status="failed",
+                        previous_version=rollback_version,
+                    )
+                if not self.release_manager.rollback(rollback_version):
+                    raise EdgeReleaseError("release rollback health confirmation failed")
+                state = self.queue.compare_and_set_release_state(
+                    release_id,
+                    expected_status="failed",
+                    target_status="rolled_back",
+                    previous_version=rollback_version,
+                    current_version=rollback_version,
+                )
+                self.release_manager.current_version = rollback_version
+                if server_status != "failed":
+                    self.transport.acknowledge_release(
+                        release_id,
+                        "failed",
+                        {"release_status": "failed", "version": version},
+                    )
+                self.transport.acknowledge_release(
+                    release_id,
+                    "rollback",
+                    {"release_status": "rolled_back", "version": rollback_version},
+                )
+                continue
+
+            if state.status == "candidate":
+                if not self.release_manager.rollback(rollback_version):
+                    raise EdgeReleaseError("uncertain candidate rollback failed")
+                state = self.queue.compare_and_set_release_state(
+                    release_id,
+                    expected_status="candidate",
+                    target_status="failed",
+                    previous_version=rollback_version,
+                )
+                state = self.queue.compare_and_set_release_state(
+                    release_id,
+                    expected_status="failed",
+                    target_status="rolled_back",
+                    previous_version=rollback_version,
+                    current_version=rollback_version,
+                )
+                self.transport.acknowledge_release(
+                    release_id,
+                    "failed",
+                    {"release_status": "failed", "version": version},
+                )
+                self.transport.acknowledge_release(
+                    release_id,
+                    "rollback",
+                    {"release_status": "rolled_back", "version": rollback_version},
+                )
+                continue
+
+            state = self.queue.compare_and_set_release_state(
+                release_id,
+                expected_status="accepted",
+                target_status="candidate",
+                previous_version=rollback_version,
+            )
+            result = self.release_manager.install(release)
+            if result["outcome"] == "installed":
+                self.queue.compare_and_set_release_state(
+                    release_id,
+                    expected_status="candidate",
+                    target_status="installed",
+                    previous_version=rollback_version,
+                    current_version=version,
+                )
+                self.transport.acknowledge_release(
+                    release_id, "installed", result["summary"]
+                )
+            else:
+                self.queue.compare_and_set_release_state(
+                    release_id,
+                    expected_status="candidate",
+                    target_status="failed",
+                    previous_version=rollback_version,
+                )
+                self.queue.compare_and_set_release_state(
+                    release_id,
+                    expected_status="failed",
+                    target_status="rolled_back",
+                    previous_version=rollback_version,
+                    current_version=rollback_version,
+                )
+                self.transport.acknowledge_release(
+                    release_id,
+                    "failed",
+                    {"release_status": "failed", "version": version},
+                )
+                self.transport.acknowledge_release(
+                    release_id, "rollback", result["summary"]
+                )
+
+    def heartbeat_once(self) -> Mapping[str, object]:
+        """Send only the policy-approved bounded health summary."""
+
+        summary = self.safe_diagnostic()
+        self.policy.validate_approved_payload("health_summary", summary)
+        return self.transport.heartbeat(summary)
+
+    def run_once(self) -> dict[str, object]:
+        if self._stopped:
+            raise EdgeAuthenticationStopped("edge agent is stopped")
+        self._drain_artifact_cleanup()
+        try:
+            cancelled, releases = self._apply_reconcile(self.transport.reconcile())
+            self._drain_outcome()
+            self._process_releases(releases)
+            sent = self._drain_event()
+            if sent is not None:
+                return {"cancelled": cancelled, "sent": sent, "executed": None}
+            if self._has_unresolved_events() and self._needs_remote_lease_task is None:
+                return {"cancelled": cancelled, "sent": None, "executed": None}
+            pulled = self.transport.pull_task()
+            if pulled is not None:
+                if not isinstance(pulled, tuple) or len(pulled) != 3:
+                    raise EdgeTransportError("pulled task response is invalid")
+                accepted = self.accept_pulled_task(pulled[0], pulled[1], pulled[2])
+                if self._needs_remote_lease_task is not None:
+                    if accepted.task_id == self._needs_remote_lease_task:
+                        self._needs_remote_lease_task = None
+                    return {
+                        "cancelled": cancelled,
+                        "sent": None,
+                        "executed": None,
+                    }
+            elif self._needs_remote_lease_task is not None:
+                return {"cancelled": cancelled, "sent": None, "executed": None}
+            expired_pending = self._expired_pending_remote_task()
+            if expired_pending is not None:
+                self._needs_remote_lease_task = expired_pending
+                return {"cancelled": cancelled, "sent": None, "executed": None}
+        except EdgeAuthenticationStopped:
+            self._stopped = True
+            raise
+        task_row = self.queue.claim(self.worker_id)
+        if task_row is None:
+            return {"cancelled": cancelled, "sent": None, "executed": None}
+        contract = EdgeTaskContract.from_mapping(task_row.task)
+        now = _now(self.clock)
+        if (
+            datetime.fromisoformat(contract.deadline_at.replace("Z", "+00:00")) <= now
+            or task_row.lease_expires_at is None
+            or datetime.fromisoformat(
+                task_row.lease_expires_at.replace("Z", "+00:00")
+            ) <= now
+            or task_row.remote_lease_expires_at is None
+            or datetime.fromisoformat(
+                task_row.remote_lease_expires_at.replace("Z", "+00:00")
+            ) <= now
+        ):
+            self.queue.fail(
+                contract.task_id,
+                lease_token=str(task_row.lease_token),
+                error_code="execution_lease_expired",
+                retryable=False,
+            )
+            self._needs_remote_lease_task = contract.task_id
+            return {"cancelled": cancelled, "sent": None, "executed": None}
+        try:
+            control_lease = self.queue.recover_remote_lease(contract.task_id)
+        except EdgeQueueLeaseError:
+            self.queue.fail(
+                contract.task_id,
+                lease_token=str(task_row.lease_token),
+                error_code="control_lease_unavailable",
+            )
+            return {"cancelled": cancelled, "sent": None, "executed": None}
+        try:
+            result = self.runner.execute(
+                contract, lambda: self._cancel_requested(contract.task_id)
+            )
+            if self._cancel_requested(contract.task_id):
+                raise EdgeTaskCancelled("edge task was cancelled")
+            event = self._event(contract, result)
+            self.queue.complete_with_event(
+                contract.task_id,
+                lease_token=str(task_row.lease_token),
+                remote_lease_token=control_lease,
+                event=event,
+                artifacts=self._artifact_metadata(contract, result),
+            )
+            self._drain_artifact_cleanup()
+        except EdgeTaskCancelled:
+            self.queue.cancel_with_outcome(contract.task_id)
+            self._drain_outcome()
+            return {"cancelled": sorted(set(cancelled) | {contract.task_id}), "sent": None, "executed": None}
+        except (EdgePolicyError, EdgeContractError, EdgeQueueConflictError):
+            self.queue.fail(
+                contract.task_id,
+                lease_token=str(task_row.lease_token),
+                error_code="execution_boundary_rejected",
+                retryable=False,
+            )
+            raise
+        except EdgeQueueLeaseError:
+            current = self.queue.get_task(contract.task_id)
+            if current is not None and current.status == "cancelled":
+                return {
+                    "cancelled": sorted(set(cancelled) | {contract.task_id}),
+                    "sent": None,
+                    "executed": None,
+                }
+            if current is not None and current.status == "leased":
+                self.queue.fail(
+                    contract.task_id,
+                    lease_token=str(task_row.lease_token),
+                    error_code="execution_lease_expired",
+                    retryable=False,
+                )
+            self._needs_remote_lease_task = contract.task_id
+            return {"cancelled": cancelled, "sent": None, "executed": None}
+        except EdgeAuthenticationStopped:
+            self._stopped = True
+            self.queue.fail(
+                contract.task_id,
+                lease_token=str(task_row.lease_token),
+                error_code="credential_stopped",
+            )
+            raise
+        except Exception as exc:
+            self.queue.fail(
+                contract.task_id,
+                lease_token=str(task_row.lease_token),
+                error_code="runner_execution_failed",
+                retryable=False,
+            )
+            raise EdgeAgentError("edge runner execution failed") from exc
+        sent = self._drain_event()
+        return {"cancelled": cancelled, "sent": sent, "executed": contract.task_id}
+
+    def safe_diagnostic(self) -> dict[str, object]:
+        connection = sqlite3.connect(f"file:{self.queue.db_path}?mode=ro", uri=True)
+        try:
+            pending = connection.execute(
+                "SELECT COUNT(*) FROM edge_tasks WHERE status IN ('pending','leased')"
+            ).fetchone()[0]
+            events = connection.execute(
+                "SELECT COUNT(*) FROM edge_outbound_events WHERE status != 'acknowledged'"
+            ).fetchone()[0]
+        finally:
+            connection.close()
+        return {
+            "agent_status": "stopped" if self._stopped else "ready",
+            "gateway_id_digest": self.gateway_id_digest,
+            "queue_pending_count": int(pending),
+            "queue_unacknowledged_count": int(events),
+            "version": self.config.version,
+        }

+ 333 - 0
deployment/app/edge_gateway/bootstrap.py

@@ -0,0 +1,333 @@
+"""Fail-closed bootstrap configuration for an enterprise edge agent."""
+
+from __future__ import annotations
+
+import re
+import ssl
+import stat
+from collections.abc import Mapping, Sequence
+from dataclasses import dataclass, field
+from datetime import UTC, datetime
+from pathlib import Path
+from types import MappingProxyType
+
+from cryptography import x509
+from cryptography.hazmat.primitives import hashes, serialization
+from cryptography.hazmat.primitives.asymmetric import ec, ed448, ed25519, padding, rsa
+
+from app.core.edge_gateway.policy import EdgeEgressPolicy
+
+_SHA256 = re.compile(r"^[0-9a-f]{64}$")
+_IDENTIFIER = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,254}$")
+_VERSION = re.compile(r"^(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})$")
+_PLACEHOLDERS = {"change-me", "changeme", "example", "placeholder", "tbd", "todo"}
+_REQUIRED = frozenset(
+    {
+        "queue_path",
+        "artifact_root",
+        "gateway_id",
+        "credential",
+        "certificate_sha256",
+        "client_certificate_path",
+        "client_private_key_path",
+        "ca_bundle_path",
+        "generation",
+        "environment",
+        "network_zone",
+        "policy_digest",
+        "control_url",
+        "proxy_url",
+        "allowed_control_hosts",
+        "allowed_proxy_hosts",
+        "trusted_release_keys",
+        "trusted_task_keys",
+        "task_authority_clock_skew_seconds",
+        "version",
+    }
+)
+_OPTIONAL = frozenset({"server_crl_path", "allowed_control_origins", "allowed_proxy_origins"})
+
+
+def _text(value: object, label: str, maximum: int = 255) -> str:
+    if (
+        not isinstance(value, str)
+        or not value
+        or value.strip() != value
+        or "\x00" in value
+        or len(value.encode("utf-8")) > maximum
+        or value.casefold() in _PLACEHOLDERS
+    ):
+        raise ValueError(f"{label} is not explicitly configured")
+    return value
+
+
+def _identifier(value: object, label: str) -> str:
+    candidate = _text(value, label)
+    if not _IDENTIFIER.fullmatch(candidate):
+        raise ValueError(f"{label} is invalid")
+    return candidate
+
+
+def _hosts(value: object, label: str, *, required: bool) -> frozenset[str]:
+    if not isinstance(value, Sequence) or isinstance(value, (str, bytes)):
+        raise ValueError(f"{label} must be an explicit sequence")
+    items = [_text(item, label) for item in value]
+    if any(item != item.lower() for item in items):
+        raise ValueError(f"{label} must contain lowercase exact hosts")
+    result = frozenset(items)
+    if required and not result:
+        raise ValueError(f"{label} is required")
+    if len(result) != len(value):
+        raise ValueError(f"{label} contains duplicates")
+    return result
+
+
+@dataclass(frozen=True, slots=True)
+class EdgeBootstrapConfig:
+    queue_path: str
+    artifact_root: str
+    gateway_id: str
+    credential: str = field(repr=False)
+    certificate_sha256: str
+    client_certificate_path: str
+    client_private_key_path: str = field(repr=False)
+    ca_bundle_path: str
+    generation: int
+    environment: str
+    network_zone: str
+    policy_digest: str
+    control_url: str
+    proxy_url: str | None
+    allowed_control_hosts: frozenset[str]
+    allowed_proxy_hosts: frozenset[str]
+    trusted_release_keys: Mapping[str, str] = field(repr=False)
+    trusted_task_keys: Mapping[str, str] = field(repr=False)
+    version: str
+    task_authority_clock_skew_seconds: int = 0
+    server_crl_path: str | None = None
+    allowed_control_origins: frozenset[str] = frozenset()
+    allowed_proxy_origins: frozenset[str] = frozenset()
+
+    @classmethod
+    def from_mapping(cls, value: Mapping[str, object]) -> EdgeBootstrapConfig:
+        if (
+            not isinstance(value, Mapping)
+            or not set(value) >= _REQUIRED
+            or not set(value) <= _REQUIRED | _OPTIONAL
+        ):
+            raise ValueError("edge bootstrap requires the exact configuration fields")
+        queue = _text(value["queue_path"], "queue_path", 2_048)
+        if queue == ":memory:" or "mode=memory" in queue:
+            raise ValueError("edge queue must use an explicit disk path")
+        queue_path = Path(queue).expanduser()
+        if not queue_path.is_absolute():
+            raise ValueError("edge queue path must be absolute")
+        artifact_root = Path(
+            _text(value["artifact_root"], "artifact_root", 2_048)
+        ).expanduser()
+        if (
+            not artifact_root.is_absolute()
+            or not artifact_root.is_dir()
+            or artifact_root.is_symlink()
+        ):
+            raise ValueError("artifact_root must be an existing absolute directory")
+        credential = _text(value["credential"], "credential", 160)
+        if (
+            not credential.startswith("dopg_")
+            or len(credential) < 16
+            or any(marker in credential.casefold() for marker in _PLACEHOLDERS)
+        ):
+            raise ValueError("credential is invalid")
+        certificate = _text(value["certificate_sha256"], "certificate_sha256", 64)
+        policy_digest = _text(value["policy_digest"], "policy_digest", 64)
+        if not _SHA256.fullmatch(certificate) or not _SHA256.fullmatch(policy_digest):
+            raise ValueError("edge digest binding is invalid")
+        material: dict[str, Path] = {}
+        for field_name in (
+            "client_certificate_path",
+            "client_private_key_path",
+            "ca_bundle_path",
+        ):
+            candidate = Path(_text(value[field_name], field_name, 2_048)).expanduser()
+            if not candidate.is_absolute() or not candidate.is_file():
+                raise ValueError(f"{field_name} must be an existing absolute file")
+            material[field_name] = candidate.resolve()
+        key_mode = stat.S_IMODE(material["client_private_key_path"].stat().st_mode)
+        if key_mode & 0o077:
+            raise ValueError("client private key permissions are not secure")
+        try:
+            certificate_object = x509.load_pem_x509_certificate(
+                material["client_certificate_path"].read_bytes()
+            )
+            ca_object = x509.load_pem_x509_certificate(
+                material["ca_bundle_path"].read_bytes()
+            )
+            private_key = serialization.load_pem_private_key(
+                material["client_private_key_path"].read_bytes(), password=None
+            )
+        except (ValueError, OSError) as exc:
+            raise ValueError("mTLS material is invalid") from exc
+        actual_fingerprint = certificate_object.fingerprint(hashes.SHA256()).hex()
+        if actual_fingerprint != certificate:
+            raise ValueError("client certificate fingerprint does not match configuration")
+        public_encoding = {
+            "encoding": serialization.Encoding.DER,
+            "format": serialization.PublicFormat.SubjectPublicKeyInfo,
+        }
+        if certificate_object.public_key().public_bytes(**public_encoding) != private_key.public_key().public_bytes(**public_encoding):
+            raise ValueError("client private key does not match certificate")
+        now = datetime.now(UTC)
+        if certificate_object.not_valid_before_utc > now or certificate_object.not_valid_after_utc <= now:
+            raise ValueError("client certificate is not currently valid")
+        if ca_object.not_valid_after_utc <= now:
+            raise ValueError("CA bundle certificate is expired")
+        generation = value["generation"]
+        if isinstance(generation, bool) or not isinstance(generation, int) or generation < 1:
+            raise ValueError("generation is invalid")
+        control_hosts = _hosts(value["allowed_control_hosts"], "allowed_control_hosts", required=True)
+        proxy_hosts = _hosts(value["allowed_proxy_hosts"], "allowed_proxy_hosts", required=False)
+        proxy_url = value["proxy_url"]
+        if proxy_url is not None:
+            proxy_url = _text(proxy_url, "proxy_url", 2_048)
+        control_url = _text(value["control_url"], "control_url", 2_048)
+        def origins(field_name: str) -> frozenset[str]:
+            raw = value.get(field_name, [])
+            if not isinstance(raw, Sequence) or isinstance(raw, (str, bytes)):
+                raise ValueError(f"{field_name} must be an explicit sequence")
+            result = frozenset(_text(item, field_name, 2_048) for item in raw)
+            if len(result) != len(raw) or any(not item.startswith("https://") for item in result):
+                raise ValueError(f"{field_name} is invalid")
+            return result
+
+        control_origins = origins("allowed_control_origins")
+        proxy_origins = origins("allowed_proxy_origins")
+        EdgeEgressPolicy(
+            allowed_control_hosts=set(control_hosts),
+            allowed_proxy_hosts=set(proxy_hosts),
+            allowed_control_origins=set(control_origins) or None,
+            allowed_proxy_origins=set(proxy_origins) or None,
+        ).validate_destination(control_url, proxy_url=proxy_url)
+        def trusted_keys(field_name: str) -> Mapping[str, str]:
+            raw_keys = value[field_name]
+            if not isinstance(raw_keys, Mapping) or not raw_keys:
+                raise ValueError(f"{field_name} is required")
+            keys: dict[str, str] = {}
+            for raw_key_id, raw_public_key in raw_keys.items():
+                key_id = _identifier(raw_key_id, "signing key id")
+                public_key = _text(raw_public_key, "signing public key", 128)
+                try:
+                    decoded = bytes.fromhex(public_key)
+                except ValueError as exc:
+                    raise ValueError("signing public key must be lowercase hex") from exc
+                if (
+                    public_key != public_key.lower()
+                    or len(decoded) != 32
+                    or decoded == bytes(32)
+                ):
+                    raise ValueError("signing public key must be a 32-byte Ed25519 key")
+                keys[key_id] = public_key
+            return MappingProxyType(keys)
+
+        release_keys = trusted_keys("trusted_release_keys")
+        task_keys = trusted_keys("trusted_task_keys")
+        task_authority_clock_skew_seconds = value[
+            "task_authority_clock_skew_seconds"
+        ]
+        if (
+            isinstance(task_authority_clock_skew_seconds, bool)
+            or not isinstance(task_authority_clock_skew_seconds, int)
+            or not 0 <= task_authority_clock_skew_seconds <= 300
+        ):
+            raise ValueError("task authority clock skew is invalid")
+        version = _text(value["version"], "version", 80)
+        if (
+            not _VERSION.fullmatch(version)
+            or any(int(part) > 2_147_483_647 for part in version.split("."))
+        ):
+            raise ValueError("version is invalid")
+        server_crl_path: str | None = None
+        if "server_crl_path" in value:
+            candidate = Path(
+                _text(value["server_crl_path"], "server_crl_path", 2_048)
+            ).expanduser()
+            if not candidate.is_absolute() or not candidate.is_file():
+                raise ValueError("server_crl_path must be an existing absolute file")
+            server_crl_path = str(candidate.resolve())
+            validate_server_crl(str(material["ca_bundle_path"]), server_crl_path)
+        return cls(
+            queue_path=str(queue_path.resolve()),
+            artifact_root=str(artifact_root.resolve()),
+            gateway_id=_identifier(value["gateway_id"], "gateway_id"),
+            credential=credential,
+            certificate_sha256=certificate,
+            client_certificate_path=str(material["client_certificate_path"]),
+            client_private_key_path=str(material["client_private_key_path"]),
+            ca_bundle_path=str(material["ca_bundle_path"]),
+            generation=generation,
+            environment=_identifier(value["environment"], "environment"),
+            network_zone=_identifier(value["network_zone"], "network_zone"),
+            policy_digest=policy_digest,
+            control_url=control_url.rstrip("/"),
+            proxy_url=proxy_url,
+            allowed_control_hosts=control_hosts,
+            allowed_proxy_hosts=proxy_hosts,
+            trusted_release_keys=release_keys,
+            trusted_task_keys=task_keys,
+            version=version,
+            task_authority_clock_skew_seconds=task_authority_clock_skew_seconds,
+            server_crl_path=server_crl_path,
+            allowed_control_origins=control_origins,
+            allowed_proxy_origins=proxy_origins,
+        )
+
+
+def _verify_crl_signature(crl: x509.CertificateRevocationList, issuer) -> None:
+    public_key = issuer.public_key()
+    try:
+        if isinstance(public_key, rsa.RSAPublicKey):
+            public_key.verify(
+                crl.signature, crl.tbs_certlist_bytes, padding.PKCS1v15(),
+                crl.signature_hash_algorithm,
+            )
+        elif isinstance(public_key, ec.EllipticCurvePublicKey):
+            public_key.verify(
+                crl.signature, crl.tbs_certlist_bytes,
+                ec.ECDSA(crl.signature_hash_algorithm),
+            )
+        elif isinstance(public_key, (ed25519.Ed25519PublicKey, ed448.Ed448PublicKey)):
+            public_key.verify(crl.signature, crl.tbs_certlist_bytes)
+        else:
+            raise ValueError("server CRL issuer key is unsupported")
+    except Exception as exc:
+        raise ValueError("server CRL signature is invalid") from exc
+
+
+def validate_server_crl(ca_bundle_path: str, server_crl_path: str) -> datetime:
+    try:
+        authorities = x509.load_pem_x509_certificates(Path(ca_bundle_path).read_bytes())
+        crl = x509.load_pem_x509_crl(Path(server_crl_path).read_bytes())
+    except (OSError, ValueError) as exc:
+        raise ValueError("server CRL material is invalid") from exc
+    issuer = next((item for item in authorities if item.subject == crl.issuer), None)
+    if issuer is None:
+        raise ValueError("server CRL issuer is not in the CA bundle")
+    _verify_crl_signature(crl, issuer)
+    now = datetime.now(UTC)
+    if (
+        crl.last_update_utc > now
+        or crl.next_update_utc is None
+        or crl.next_update_utc <= now
+    ):
+        raise ValueError("server CRL is not fresh")
+    return crl.next_update_utc
+
+
+def build_server_crl_ssl_context(
+    ca_bundle_path: str, server_crl_path: str
+) -> ssl.SSLContext:
+    validate_server_crl(ca_bundle_path, server_crl_path)
+    context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=ca_bundle_path)
+    context.load_verify_locations(cafile=server_crl_path)
+    context.verify_flags |= ssl.VERIFY_CRL_CHECK_LEAF
+    context.check_hostname = True
+    return context

+ 32 - 0
deployment/app/edge_gateway/healthcheck.py

@@ -0,0 +1,32 @@
+"""Bounded container health probe for the edge runtime."""
+
+from __future__ import annotations
+
+import json
+import os
+from datetime import UTC, datetime, timedelta
+from pathlib import Path
+
+
+def main() -> int:
+    path = Path(os.environ.get("EDGE_HEALTH_PATH", "/run/edge/tmp/health.json"))
+    try:
+        value = json.loads(path.read_text(encoding="utf-8"))
+        checked = datetime.fromisoformat(str(value["checked_at"]).replace("Z", "+00:00"))
+        if (
+            set(value) - {
+                "checked_at", "crl_next_update", "gateway_id_digest", "last_cycle",
+                "queue", "status",
+            }
+            or value["status"] not in {"healthy", "degraded"}
+            or checked.tzinfo is None
+            or datetime.now(UTC) - checked.astimezone(UTC) > timedelta(seconds=90)
+        ):
+            return 1
+    except (OSError, ValueError, KeyError, TypeError, json.JSONDecodeError):
+        return 1
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())

+ 276 - 0
deployment/app/edge_gateway/runtime.py

@@ -0,0 +1,276 @@
+"""Runnable pull-only edge process with mTLS server-CRL fail-stop semantics."""
+
+from __future__ import annotations
+
+import hashlib
+import json
+import os
+import signal
+import stat
+import threading
+from datetime import UTC, datetime
+from pathlib import Path
+
+import requests
+from requests.adapters import HTTPAdapter
+
+from app.core.edge_gateway.policy import EdgePolicyError
+from app.edge_gateway.agent import EdgeAgent, EdgeRunnerAdapter
+from app.edge_gateway.bootstrap import (
+    EdgeBootstrapConfig,
+    build_server_crl_ssl_context,
+    validate_server_crl,
+)
+from app.edge_gateway.transport import (
+    EdgeAuthenticationStopped,
+    EdgeTransport,
+    EdgeTransportError,
+)
+
+MAX_CONFIG_BYTES = 262_144
+
+
+def _now() -> str:
+    return datetime.now(UTC).isoformat().replace("+00:00", "Z")
+
+
+def _read_regular(path: Path, *, maximum: int, private: bool = False) -> bytes:
+    if not path.is_absolute() or path.is_symlink():
+        raise ValueError("runtime material path is invalid")
+    flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0)
+    if hasattr(os, "O_NOFOLLOW"):
+        flags |= os.O_NOFOLLOW
+    descriptor = os.open(path, flags)
+    try:
+        metadata = os.fstat(descriptor)
+        if (
+            not stat.S_ISREG(metadata.st_mode)
+            or metadata.st_nlink != 1
+            or metadata.st_size > maximum
+            or (private and stat.S_IMODE(metadata.st_mode) & 0o077)
+        ):
+            raise ValueError("runtime material metadata is unsafe")
+        chunks = bytearray()
+        while len(chunks) <= maximum:
+            chunk = os.read(descriptor, min(65_536, maximum + 1 - len(chunks)))
+            if not chunk:
+                break
+            chunks.extend(chunk)
+        if len(chunks) > maximum or os.fstat(descriptor) != metadata:
+            raise ValueError("runtime material changed while reading")
+        return bytes(chunks)
+    finally:
+        os.close(descriptor)
+
+
+def _read_json(path: Path) -> dict[str, object]:
+    try:
+        value = json.loads(_read_regular(path, maximum=MAX_CONFIG_BYTES).decode("utf-8"))
+    except (UnicodeError, json.JSONDecodeError) as exc:
+        raise ValueError("runtime JSON is invalid") from exc
+    if not isinstance(value, dict):
+        raise ValueError("runtime JSON must be an object")
+    return value
+
+
+def _atomic_health(path: Path, value: dict[str, object]) -> None:
+    encoded = json.dumps(value, sort_keys=True, separators=(",", ":")).encode() + b"\n"
+    path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
+    temporary = path.parent / f".{path.name}.{os.getpid()}.tmp"
+    flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_CLOEXEC", 0)
+    if hasattr(os, "O_NOFOLLOW"):
+        flags |= os.O_NOFOLLOW
+    descriptor = os.open(temporary, flags, 0o600)
+    try:
+        os.write(descriptor, encoded)
+        os.fsync(descriptor)
+    finally:
+        os.close(descriptor)
+    os.replace(temporary, path)
+    directory = os.open(path.parent, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
+    try:
+        os.fsync(directory)
+    finally:
+        os.close(directory)
+
+
+class ServerCrlHttpAdapter(HTTPAdapter):
+    def __init__(self, ca_bundle_path: str, server_crl_path: str):
+        self._ssl_context = build_server_crl_ssl_context(
+            ca_bundle_path, server_crl_path
+        )
+        super().__init__()
+
+    def init_poolmanager(self, connections, maxsize, block=False, **pool_kwargs):
+        pool_kwargs["ssl_context"] = self._ssl_context
+        return super().init_poolmanager(connections, maxsize, block, **pool_kwargs)
+
+
+class ServerCrlSession(requests.Session):
+    """Reload the TLS pool whenever the operator atomically replaces the CRL."""
+
+    def __init__(self, ca_bundle_path: str, server_crl_path: str):
+        super().__init__()
+        self.trust_env = False
+        self._ca_bundle_path = ca_bundle_path
+        self._server_crl_path = server_crl_path
+        self._server_crl_digest = ""
+        self.refresh_server_crl()
+
+    @property
+    def server_crl_digest(self) -> str:
+        return self._server_crl_digest
+
+    def refresh_server_crl(self) -> bool:
+        validate_server_crl(self._ca_bundle_path, self._server_crl_path)
+        digest = hashlib.sha256(
+            _read_regular(Path(self._server_crl_path), maximum=1_048_576)
+        ).hexdigest()
+        if digest == self._server_crl_digest:
+            return False
+        replacement = ServerCrlHttpAdapter(
+            self._ca_bundle_path, self._server_crl_path
+        )
+        previous = self.adapters.get("https://")
+        self.mount("https://", replacement)
+        self._server_crl_digest = digest
+        if previous is not None:
+            previous.close()
+        return True
+
+    def request(self, *args, **kwargs):
+        self.refresh_server_crl()
+        return super().request(*args, **kwargs)
+
+
+def _runtime_self_check(node, request, cancel_requested):
+    if (
+        node != "quality.check"
+        or request.get("operation") != "quality"
+        or request.get("purpose") != "quality-evaluation"
+        or request.get("classification") != "evidence"
+    ):
+        raise EdgePolicyError("enterprise source runner handler is not provisioned")
+    if cancel_requested():
+        raise EdgePolicyError("runtime self-check was cancelled")
+    return {
+        "classification": "evidence",
+        "payload": {
+            "check_count": 1,
+            "passed_count": 1,
+            "scope": "edge_runtime",
+            "status": "passed",
+        },
+    }
+
+
+def build_runtime_runner() -> EdgeRunnerAdapter:
+    """Provide one closed engineering self-check; source handlers remain external."""
+
+    return EdgeRunnerAdapter(_runtime_self_check)
+
+
+def build_runtime_from_environment() -> tuple[EdgeAgent, Path, float]:
+    config_path = Path(os.environ.get("EDGE_CONFIG_PATH", ""))
+    credential_path = Path(os.environ.get("EDGE_CREDENTIAL_FILE", ""))
+    task_keys_path = Path(os.environ.get("EDGE_TASK_SIGNING_KEYS_PATH", ""))
+    release_keys_path = Path(os.environ.get("EDGE_RELEASE_SIGNING_KEYS_PATH", ""))
+    server_crl_path = Path(os.environ.get("EDGE_SERVER_CRL_PATH", ""))
+    health_path = Path(os.environ.get("EDGE_HEALTH_PATH", "/run/edge/tmp/health.json"))
+    if any(not path.is_absolute() for path in (
+        config_path, credential_path, task_keys_path, release_keys_path,
+        server_crl_path, health_path,
+    )):
+        raise ValueError("edge runtime paths must be absolute")
+    raw = _read_json(config_path)
+    credential = _read_regular(
+        credential_path, maximum=160, private=True
+    ).decode("utf-8").strip()
+    task_keys = _read_json(task_keys_path)
+    release_keys = _read_json(release_keys_path)
+    raw.update({
+        "credential": credential,
+        "trusted_task_keys": task_keys,
+        "trusted_release_keys": release_keys,
+        "server_crl_path": str(server_crl_path),
+    })
+    config = EdgeBootstrapConfig.from_mapping(raw)
+    if config.server_crl_path is None:
+        raise ValueError("server CRL is required by the runtime")
+    session = ServerCrlSession(config.ca_bundle_path, config.server_crl_path)
+    transport = EdgeTransport(
+        base_url=config.control_url,
+        gateway_id=config.gateway_id,
+        environment=config.environment,
+        network_zone=config.network_zone,
+        generation=config.generation,
+        credential=config.credential,
+        certificate_sha256=config.certificate_sha256,
+        allowed_control_hosts=config.allowed_control_hosts,
+        allowed_proxy_hosts=config.allowed_proxy_hosts,
+        allowed_control_origins=config.allowed_control_origins or None,
+        allowed_proxy_origins=config.allowed_proxy_origins or None,
+        proxy_url=config.proxy_url,
+        client_certificate_path=config.client_certificate_path,
+        client_private_key_path=config.client_private_key_path,
+        ca_bundle_path=config.ca_bundle_path,
+        client=session,
+        server_crl_path=config.server_crl_path,
+    )
+    runner = build_runtime_runner()
+    interval = float(os.environ.get("EDGE_POLL_INTERVAL_SECONDS", "5"))
+    if not 1 <= interval <= 60:
+        raise ValueError("EDGE_POLL_INTERVAL_SECONDS must be between 1 and 60")
+    return EdgeAgent(config, transport, runner), health_path, interval
+
+
+def run() -> int:
+    stop = threading.Event()
+    for event in (signal.SIGINT, signal.SIGTERM):
+        signal.signal(event, lambda _signum, _frame: stop.set())
+    try:
+        agent, health_path, interval = build_runtime_from_environment()
+    except Exception:
+        return 2
+    stopped = False
+    while not stop.is_set():
+        try:
+            if agent.config.server_crl_path is None:  # pragma: no cover - invariant
+                raise EdgeAuthenticationStopped("server CRL is missing")
+            crl_next_update = validate_server_crl(
+                agent.config.ca_bundle_path, agent.config.server_crl_path
+            )
+            agent.heartbeat_once()
+            cycle = agent.run_once()
+            _atomic_health(health_path, {
+                "checked_at": _now(),
+                "crl_next_update": crl_next_update.isoformat().replace("+00:00", "Z"),
+                "gateway_id_digest": agent.gateway_id_digest,
+                "last_cycle": cycle,
+                "queue": agent.safe_diagnostic(),
+                "status": "healthy",
+            })
+        except EdgeAuthenticationStopped:
+            stopped = True
+            _atomic_health(health_path, {
+                "checked_at": _now(), "gateway_id_digest": agent.gateway_id_digest,
+                "status": "stopped",
+            })
+        except (EdgeTransportError, OSError, ValueError):
+            _atomic_health(health_path, {
+                "checked_at": _now(), "gateway_id_digest": agent.gateway_id_digest,
+                "status": "degraded",
+            })
+        if stopped:
+            stop.wait(interval)
+        else:
+            stop.wait(interval)
+    _atomic_health(health_path, {
+        "checked_at": _now(), "gateway_id_digest": agent.gateway_id_digest,
+        "status": "stopped",
+    })
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(run())

+ 427 - 0
deployment/app/edge_gateway/transport.py

@@ -0,0 +1,427 @@
+"""Bounded pull-only HTTPS transport for the enterprise edge agent."""
+
+from __future__ import annotations
+
+import hashlib
+import json
+import logging
+import re
+from collections.abc import Mapping
+from urllib.parse import quote, urlsplit
+
+from app.core.edge_gateway.contracts import (
+    EdgeContractError,
+    EdgeEventContract,
+    canonical_sha256,
+    preflight_json,
+    strict_json_bytes,
+)
+from app.core.edge_gateway.policy import EdgeEgressPolicy, EdgePolicyError
+from app.edge_gateway.bootstrap import validate_server_crl
+
+LOGGER = logging.getLogger(__name__)
+
+_IDENTIFIER = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,254}$")
+_SHA256 = re.compile(r"^[0-9a-f]{64}$")
+
+
+class EdgeTransportError(RuntimeError):
+    """A bounded transport failure safe for logs and retry decisions."""
+
+
+class EdgeAuthenticationStopped(EdgeTransportError):
+    """Credential/certificate binding is no longer usable; stop the agent."""
+
+
+class EdgeTransportConflict(EdgeTransportError):
+    """The control plane rejected an idempotent replay with changed content."""
+
+
+class EdgeTransport:
+    MAX_REQUEST_BYTES = 262_144
+    MAX_RESPONSE_BYTES = 1_048_576
+
+    def __init__(
+        self,
+        *,
+        base_url: str,
+        gateway_id: str,
+        environment: str,
+        network_zone: str,
+        generation: int,
+        credential: str,
+        certificate_sha256: str,
+        allowed_control_hosts: set[str] | frozenset[str],
+        allowed_proxy_hosts: set[str] | frozenset[str] | None = None,
+        allowed_control_origins: set[str] | frozenset[str] | None = None,
+        allowed_proxy_origins: set[str] | frozenset[str] | None = None,
+        proxy_url: str | None = None,
+        client_certificate_path: str,
+        client_private_key_path: str,
+        ca_bundle_path: str,
+        client,
+        server_crl_path: str | None = None,
+        connect_timeout_seconds: int = 5,
+        read_timeout_seconds: int = 20,
+    ) -> None:
+        self.policy = EdgeEgressPolicy(
+            allowed_control_hosts=set(allowed_control_hosts),
+            allowed_proxy_hosts=set(allowed_proxy_hosts or set()),
+            allowed_control_origins=allowed_control_origins,
+            allowed_proxy_origins=allowed_proxy_origins,
+        )
+        self.policy.validate_destination(base_url, proxy_url=proxy_url)
+        parsed = urlsplit(base_url)
+        if parsed.query or parsed.fragment or parsed.path not in {"", "/"}:
+            raise EdgePolicyError("control base URL must not contain a path or query")
+        if not isinstance(gateway_id, str) or not _IDENTIFIER.fullmatch(gateway_id):
+            raise ValueError("gateway_id is invalid")
+        if not isinstance(certificate_sha256, str) or not _SHA256.fullmatch(certificate_sha256):
+            raise ValueError("certificate_sha256 is invalid")
+        if (
+            not isinstance(credential, str)
+            or not credential.startswith("dopg_")
+            or len(credential) > 160
+        ):
+            raise ValueError("credential is invalid")
+        if isinstance(generation, bool) or not isinstance(generation, int) or generation < 1:
+            raise ValueError("generation is invalid")
+        for label, timeout in {
+            "connect_timeout_seconds": connect_timeout_seconds,
+            "read_timeout_seconds": read_timeout_seconds,
+        }.items():
+            if isinstance(timeout, bool) or not isinstance(timeout, int) or not 1 <= timeout <= 120:
+                raise ValueError(f"{label} is invalid")
+        if client is None or not callable(getattr(client, "request", None)):
+            raise ValueError("an explicit bounded HTTP client is required")
+        if hasattr(client, "trust_env") and client.trust_env is not False:
+            raise ValueError("HTTP client must disable implicit environment proxies")
+        self.base_url = base_url.rstrip("/")
+        self.proxy_url = proxy_url
+        self.gateway_id = gateway_id
+        self.environment = environment
+        self.network_zone = network_zone
+        self.generation = generation
+        self._credential = credential
+        self.certificate_sha256 = certificate_sha256
+        self.client = client
+        for label, path in {
+            "client_certificate_path": client_certificate_path,
+            "client_private_key_path": client_private_key_path,
+            "ca_bundle_path": ca_bundle_path,
+        }.items():
+            if not isinstance(path, str) or not path.startswith("/") or "\x00" in path:
+                raise ValueError(f"{label} is invalid")
+        self.client_certificate_path = client_certificate_path
+        self._client_private_key_path = client_private_key_path
+        self.ca_bundle_path = ca_bundle_path
+        if server_crl_path is not None and (
+            not isinstance(server_crl_path, str)
+            or not server_crl_path.startswith("/")
+            or "\x00" in server_crl_path
+        ):
+            raise ValueError("server_crl_path is invalid")
+        self.server_crl_path = server_crl_path
+        if server_crl_path is not None:
+            validate_server_crl(ca_bundle_path, server_crl_path)
+        self.timeout = (connect_timeout_seconds, read_timeout_seconds)
+
+    def _binding(self) -> dict[str, object]:
+        return {
+            "environment": self.environment,
+            "network_zone": self.network_zone,
+            "generation": self.generation,
+        }
+
+    def _request(self, path: str, payload: Mapping[str, object]) -> dict[str, object]:
+        if self.server_crl_path is not None:
+            try:
+                validate_server_crl(self.ca_bundle_path, self.server_crl_path)
+            except ValueError as exc:
+                raise EdgeAuthenticationStopped("control-plane server CRL is not fresh") from exc
+        if not path.startswith("/") or ".." in path or "//" in path:
+            raise EdgeTransportError("control path is invalid")
+        url = f"{self.base_url}{path}"
+        self.policy.validate_destination(url, proxy_url=self.proxy_url)
+        try:
+            encoded = strict_json_bytes(payload)
+        except EdgeContractError as exc:
+            raise EdgeTransportError("control request is invalid") from exc
+        if len(encoded) > self.MAX_REQUEST_BYTES:
+            raise EdgeTransportError("control request exceeds the byte limit")
+        headers = {
+            "Accept": "application/json",
+            "Content-Type": "application/json",
+            "X-Edge-Credential": self._credential,
+            "X-Edge-Certificate-SHA256": self.certificate_sha256,
+        }
+        try:
+            response = self.client.request(
+                "POST",
+                url,
+                data=encoded,
+                headers=headers,
+                timeout=self.timeout,
+                allow_redirects=False,
+                proxies=(
+                    {"https": self.proxy_url}
+                    if self.proxy_url is not None
+                    else {"http": None, "https": None}
+                ),
+                verify=self.ca_bundle_path,
+                cert=(self.client_certificate_path, self._client_private_key_path),
+                stream=True,
+            )
+        except Exception as exc:
+            LOGGER.warning(
+                "edge control request failed category=%s", type(exc).__name__
+            )
+            raise EdgeTransportError("control request failed") from exc
+        try:
+            return self._read_response(response)
+        finally:
+            close = getattr(response, "close", None)
+            if callable(close):
+                close()
+
+    def _read_response(self, response) -> dict[str, object]:
+        status = getattr(response, "status_code", None)
+        if not isinstance(status, int):
+            raise EdgeTransportError("control response is invalid")
+        if 300 <= status < 400:
+            raise EdgeTransportError("control redirects are forbidden")
+        headers_value = getattr(response, "headers", {})
+        content_length = headers_value.get("Content-Length") if isinstance(headers_value, Mapping) else None
+        if content_length is not None:
+            try:
+                declared_length = int(content_length)
+            except (TypeError, ValueError) as exc:
+                raise EdgeTransportError("control response content length is invalid") from exc
+            if declared_length < 0 or declared_length > self.MAX_RESPONSE_BYTES:
+                raise EdgeTransportError("control response exceeds the byte limit")
+        iterator = getattr(response, "iter_content", None)
+        if not callable(iterator):
+            raise EdgeTransportError("control response streaming is unavailable")
+        chunks = bytearray()
+        try:
+            for chunk in iterator(chunk_size=65_536):
+                if not isinstance(chunk, bytes):
+                    raise EdgeTransportError("control response chunk is invalid")
+                chunks.extend(chunk)
+                if len(chunks) > self.MAX_RESPONSE_BYTES:
+                    raise EdgeTransportError("control response exceeds the byte limit")
+        except EdgeTransportError:
+            raise
+        except Exception as exc:
+            raise EdgeTransportError("control response stream failed") from exc
+        body = bytes(chunks)
+        if len(body) > self.MAX_RESPONSE_BYTES:
+            raise EdgeTransportError("control response exceeds the byte limit")
+        if status in {401, 403}:
+            raise EdgeAuthenticationStopped("edge credential or certificate binding was rejected")
+        if status == 409:
+            raise EdgeTransportConflict("control plane idempotency conflict")
+        if not 200 <= status < 300:
+            raise EdgeTransportError("control plane rejected the request")
+        try:
+            value = json.loads(body.decode("utf-8"))
+            preflight_json(
+                value,
+                max_depth=32,
+                max_nodes=5_000,
+                max_string_bytes=131_072,
+                max_total_bytes=self.MAX_RESPONSE_BYTES,
+            )
+        except (UnicodeDecodeError, json.JSONDecodeError, EdgeContractError) as exc:
+            raise EdgeTransportError("control response is invalid") from exc
+        if not isinstance(value, dict):
+            raise EdgeTransportError("control response is invalid")
+        if set(value) == {"code", "message", "data"}:
+            if value["code"] != 200:
+                raise EdgeTransportError("control response envelope is invalid")
+            value = value["data"]
+        if not isinstance(value, dict):
+            raise EdgeTransportError("control response data is invalid")
+        return value
+
+    def reconcile(self) -> dict[str, object]:
+        cancelled: list[str] = []
+        releases: list[dict[str, object]] = []
+        release_digests: dict[str, str] = {}
+        cancel_cursor: str | None = None
+        release_cursor: str | None = None
+        cancel_done = False
+        release_done = False
+        for _page in range(100):
+            value = self._request(
+                f"/api/datasource/edge/gateways/{quote(self.gateway_id, safe='')}/reconcile",
+                {
+                    **self._binding(),
+                    "limit": 50,
+                    "cancel_cursor": cancel_cursor,
+                    "release_cursor": release_cursor,
+                },
+            )
+            required = {
+                "cancelled_task_ids", "cancel_next_cursor", "release_offers",
+                "release_next_cursor", "release_baseline",
+            }
+            if set(value) != required:
+                raise EdgeTransportError("reconcile response schema is invalid")
+            page_cancelled = value["cancelled_task_ids"]
+            page_releases = value["release_offers"]
+            if (
+                not isinstance(page_cancelled, list)
+                or not isinstance(page_releases, list)
+                or len(page_cancelled) > 50
+                or len(page_releases) > 50
+            ):
+                raise EdgeTransportError("reconcile page exceeds safe limits")
+            for task_id in page_cancelled:
+                if not isinstance(task_id, str) or not _IDENTIFIER.fullmatch(task_id):
+                    raise EdgeTransportError("cancel reconciliation is invalid")
+                if task_id not in cancelled:
+                    cancelled.append(task_id)
+            baseline = value["release_baseline"]
+            candidates = list(page_releases)
+            if baseline is not None:
+                candidates.append(baseline)
+            for release in candidates:
+                if not isinstance(release, Mapping):
+                    raise EdgeTransportError("release reconciliation is invalid")
+                release_id = release.get("release_id")
+                if not isinstance(release_id, str) or not _IDENTIFIER.fullmatch(release_id):
+                    raise EdgeTransportError("release reconciliation is invalid")
+                digest = canonical_sha256(release)
+                prior = release_digests.get(release_id)
+                if prior is not None and prior != digest:
+                    raise EdgeTransportConflict("release page replay changed content")
+                if prior is None:
+                    release_digests[release_id] = digest
+                    releases.append(dict(release))
+            next_cancel = value["cancel_next_cursor"]
+            next_release = value["release_next_cursor"]
+            if next_cancel is not None and (
+                not isinstance(next_cancel, str) or not _IDENTIFIER.fullmatch(next_cancel)
+            ):
+                raise EdgeTransportError("cancel cursor is invalid")
+            if next_release is not None and (
+                not isinstance(next_release, str) or not _IDENTIFIER.fullmatch(next_release)
+            ):
+                raise EdgeTransportError("release cursor is invalid")
+            cancel_done = next_cancel is None
+            release_done = next_release is None
+            cancel_cursor = next_cancel or (
+                page_cancelled[-1] if page_cancelled else cancel_cursor
+            )
+            release_cursor = next_release or (
+                str(page_releases[-1].get("release_id"))
+                if page_releases and isinstance(page_releases[-1], Mapping)
+                else release_cursor
+            )
+            if len(cancelled) > 5_000 or len(releases) > 5_000:
+                raise EdgeTransportError("reconcile result exceeds safe limits")
+            if cancel_done and release_done:
+                return {
+                    "cancelled_task_ids": cancelled,
+                    "release_offers": releases,
+                }
+        raise EdgeTransportError("reconcile pagination did not converge")
+
+    def cancel_requested(self, task_id: str) -> bool:
+        if not isinstance(task_id, str) or not _IDENTIFIER.fullmatch(task_id):
+            raise EdgeTransportError("cancel task identity is invalid")
+        value = self.reconcile()
+        cancelled = value.get("cancelled_task_ids")
+        if not isinstance(cancelled, list) or len(cancelled) > 100:
+            raise EdgeTransportError("cancel reconciliation is invalid")
+        if any(not isinstance(item, str) or not _IDENTIFIER.fullmatch(item) for item in cancelled):
+            raise EdgeTransportError("cancel reconciliation is invalid")
+        return task_id in cancelled
+
+    def pull_task(self):
+        value = self._request(
+            f"/api/datasource/edge/gateways/{quote(self.gateway_id, safe='')}/tasks/pull",
+            self._binding(),
+        )
+        envelope = value.get("signed_task_envelope")
+        task = value.get("task")
+        if envelope is None and task is None:
+            return None
+        lease_token = value.get("lease_token")
+        lease_expires_at = value.get("lease_expires_at")
+        if (
+            not isinstance(envelope, Mapping)
+            or not isinstance(task, Mapping)
+            or envelope.get("task") != task
+            or not isinstance(lease_token, str)
+            or not isinstance(lease_expires_at, str)
+        ):
+            raise EdgeTransportError("pulled task response is invalid")
+        return dict(envelope), lease_token, lease_expires_at
+
+    def send_event(self, event: Mapping[str, object], lease_token: str) -> dict[str, object]:
+        try:
+            normalized_event = EdgeEventContract.from_mapping(event).to_mapping()
+        except EdgeContractError as exc:
+            raise EdgeTransportError("outbound event contract is invalid") from exc
+        value = self._request(
+            f"/api/datasource/edge/gateways/{quote(self.gateway_id, safe='')}/events",
+            {
+                **self._binding(),
+                "event": normalized_event,
+                "lease_token": lease_token,
+            },
+        )
+        expected_digest = hashlib.sha256(lease_token.encode("utf-8")).hexdigest()
+        if (
+            value.get("status") != "accepted"
+            or value.get("event_id") != normalized_event.get("event_id")
+            or value.get("event_digest") != canonical_sha256(normalized_event)
+            or value.get("remote_lease_digest") != expected_digest
+            or not isinstance(value.get("received_at"), str)
+        ):
+            raise EdgeTransportError("event acknowledgement is invalid")
+        return {
+            "event_id": value["event_id"],
+            "event_digest": value["event_digest"],
+            "remote_lease_digest": value["remote_lease_digest"],
+            "received_at": value["received_at"],
+            "status": "accepted",
+        }
+
+    def task_outcome(self, task_id: str, outcome: str, lease_token: str, summary: Mapping[str, object]):
+        value = self._request(
+            f"/api/datasource/edge/gateways/{quote(self.gateway_id, safe='')}/tasks/{quote(task_id, safe='')}/outcome",
+            {
+                **self._binding(),
+                "outcome": outcome,
+                "lease_token": lease_token,
+                "safe_summary": dict(summary),
+            },
+        )
+        if (
+            set(value) != {"task_id", "status", "replayed"}
+            or value.get("task_id") != task_id
+            or value.get("status") != outcome
+            or not isinstance(value.get("replayed"), bool)
+        ):
+            raise EdgeTransportError("task outcome acknowledgement is invalid")
+        return value
+
+    def heartbeat(self, summary: Mapping[str, object]):
+        return self._request(
+            f"/api/datasource/edge/gateways/{quote(self.gateway_id, safe='')}/heartbeat",
+            {**self._binding(), "version": summary.get("version"), "safe_summary": dict(summary)},
+        )
+
+    def acknowledge_release(self, release_id: str, outcome: str, summary: Mapping[str, object]):
+        return self._request(
+            f"/api/datasource/edge/gateways/{quote(self.gateway_id, safe='')}/releases/ack",
+            {
+                **self._binding(),
+                "release_id": release_id,
+                "outcome": outcome,
+                "safe_summary": dict(summary),
+            },
+        )

+ 20 - 0
deployment/app/models/__init__.py

@@ -33,6 +33,17 @@ from app.models.data_research import (
     OntologyVersionModel,
     SourceArtifact,
 )
+from app.models.edge_gateway import (
+    EdgeGateway,
+    EdgeGatewayAudit,
+    EdgeGatewayCredential,
+    EdgeGatewayEnrollment,
+    EdgeGatewayEvent,
+    EdgeGatewayFailureWindow,
+    EdgeGatewayRelease,
+    EdgeGatewayRotationRequest,
+    EdgeGatewayTask,
+)
 from app.models.governance_template import (
     GovernanceDomainTemplate,
     GovernanceDomainTemplateImport,
@@ -65,6 +76,15 @@ __all__ = [
     "ConnectorPrincipal",
     "ConnectorRun",
     "ConnectorSourceBinding",
+    "EdgeGatewayEnrollment",
+    "EdgeGateway",
+    "EdgeGatewayCredential",
+    "EdgeGatewayRotationRequest",
+    "EdgeGatewayTask",
+    "EdgeGatewayEvent",
+    "EdgeGatewayRelease",
+    "EdgeGatewayAudit",
+    "EdgeGatewayFailureWindow",
     "MetadataReviewRecord",
     "MetadataVersionHistory",
     "SemanticAsset",

+ 133 - 0
deployment/app/models/edge_gateway.py

@@ -0,0 +1,133 @@
+"""Read models for the enterprise edge-gateway control plane."""
+
+from sqlalchemy.dialects.postgresql import ARRAY, JSONB, UUID
+
+from app import db
+from app.core.common.identifiers import new_governance_uid
+
+
+class EdgeGatewayEnrollment(db.Model):
+    __tablename__ = "edge_gateway_enrollments"
+    __table_args__ = {"schema": "public"}
+    uid = db.Column(UUID(as_uuid=False), primary_key=True, default=new_governance_uid)
+    gateway_id = db.Column(UUID(as_uuid=False), nullable=False, unique=True)
+    gateway_name = db.Column(db.String(200), nullable=False)
+    environment = db.Column(db.String(20), nullable=False)
+    network_zone = db.Column(db.String(120), nullable=False)
+    policy_digest = db.Column(db.String(64), nullable=False)
+    expected_certificate_sha256 = db.Column(db.String(64), nullable=False)
+    allowed_control_hosts = db.Column(ARRAY(db.Text), nullable=False)
+    allowed_proxy_hosts = db.Column(ARRAY(db.Text), nullable=False)
+    token_hash = db.Column(db.String(64), nullable=False, unique=True)
+    status = db.Column(db.String(20), nullable=False)
+    expires_at = db.Column(db.DateTime(timezone=True), nullable=False)
+    consumed_at = db.Column(db.DateTime(timezone=True))
+
+
+class EdgeGateway(db.Model):
+    __tablename__ = "edge_gateways"
+    __table_args__ = {"schema": "public"}
+    uid = db.Column(UUID(as_uuid=False), primary_key=True, default=new_governance_uid)
+    name = db.Column(db.String(200), nullable=False)
+    environment = db.Column(db.String(20), nullable=False)
+    network_zone = db.Column(db.String(120), nullable=False)
+    status = db.Column(db.String(20), nullable=False)
+    current_generation = db.Column(db.Integer, nullable=False)
+    policy_digest = db.Column(db.String(64), nullable=False)
+    allowed_control_hosts = db.Column(ARRAY(db.Text), nullable=False)
+    allowed_proxy_hosts = db.Column(ARRAY(db.Text), nullable=False)
+    last_heartbeat_at = db.Column(db.DateTime(timezone=True))
+    runtime_version = db.Column(db.String(80))
+    safe_health_summary = db.Column(JSONB, nullable=False)
+
+
+class EdgeGatewayCredential(db.Model):
+    __tablename__ = "edge_gateway_credentials"
+    __table_args__ = {"schema": "public"}
+    uid = db.Column(UUID(as_uuid=False), primary_key=True, default=new_governance_uid)
+    gateway_id = db.Column(UUID(as_uuid=False), nullable=False)
+    generation = db.Column(db.Integer, nullable=False)
+    credential_hash = db.Column(db.String(64), nullable=False, unique=True)
+    certificate_sha256 = db.Column(db.String(64), nullable=False)
+    status = db.Column(db.String(20), nullable=False)
+    expires_at = db.Column(db.DateTime(timezone=True), nullable=False)
+
+
+class EdgeGatewayRotationRequest(db.Model):
+    __tablename__ = "edge_gateway_rotation_requests"
+    __table_args__ = {"schema": "public"}
+    gateway_id = db.Column(UUID(as_uuid=False), primary_key=True)
+    request_id = db.Column(db.String(255), primary_key=True)
+    request_digest = db.Column(db.String(64), nullable=False)
+    generation = db.Column(db.Integer, nullable=False)
+
+
+class EdgeGatewayTask(db.Model):
+    __tablename__ = "edge_gateway_tasks"
+    __table_args__ = {"schema": "public"}
+    task_id = db.Column(db.String(255), primary_key=True)
+    gateway_id = db.Column(UUID(as_uuid=False), nullable=False)
+    idempotency_key = db.Column(db.String(255), nullable=False)
+    contract = db.Column(JSONB, nullable=False)
+    contract_digest = db.Column(db.String(64), nullable=False)
+    authority = db.Column(JSONB, nullable=False)
+    status = db.Column(db.String(24), nullable=False)
+    result_summary = db.Column(JSONB)
+
+
+class EdgeGatewayEvent(db.Model):
+    __tablename__ = "edge_gateway_events"
+    __table_args__ = {"schema": "public"}
+    event_id = db.Column(db.String(80), primary_key=True)
+    gateway_id = db.Column(UUID(as_uuid=False), nullable=False)
+    task_id = db.Column(db.String(255), nullable=False)
+    classification = db.Column(db.String(40), nullable=False)
+    contract = db.Column(JSONB, nullable=False)
+    contract_digest = db.Column(db.String(64), nullable=False)
+    ack = db.Column(JSONB, nullable=False)
+
+
+class EdgeGatewayRelease(db.Model):
+    __tablename__ = "edge_gateway_releases"
+    __table_args__ = {"schema": "public"}
+    uid = db.Column(UUID(as_uuid=False), primary_key=True, default=new_governance_uid)
+    gateway_id = db.Column(UUID(as_uuid=False), nullable=False)
+    version = db.Column(db.String(80), nullable=False)
+    artifact_digest = db.Column(db.String(64), nullable=False)
+    rollback_version = db.Column(db.String(80), nullable=False)
+    request_id = db.Column(db.String(255), nullable=False)
+    request_digest = db.Column(db.String(64), nullable=False)
+    status = db.Column(db.String(24), nullable=False)
+    safe_summary = db.Column(JSONB, nullable=False)
+    artifact_name = db.Column(db.String(255), nullable=False)
+    deadline_at = db.Column(db.DateTime(timezone=True), nullable=False)
+    signature_algorithm = db.Column(db.String(20), nullable=False)
+    key_id = db.Column(db.String(128), nullable=False)
+    manifest_digest = db.Column(db.String(64), nullable=False)
+    signature = db.Column(db.String(128), nullable=False)
+    signed_manifest = db.Column(JSONB, nullable=False)
+
+
+class EdgeGatewayAudit(db.Model):
+    __tablename__ = "edge_gateway_audits"
+    __table_args__ = {"schema": "public"}
+    uid = db.Column(UUID(as_uuid=False), primary_key=True, default=new_governance_uid)
+    gateway_id = db.Column(UUID(as_uuid=False))
+    event_type = db.Column(db.String(80), nullable=False)
+    actor_uid = db.Column(UUID(as_uuid=False))
+    success = db.Column(db.Boolean, nullable=False)
+    safe_detail = db.Column(JSONB, nullable=False)
+    failure_fingerprint = db.Column(db.String(64))
+    window_started_at = db.Column(db.DateTime(timezone=True))
+
+
+class EdgeGatewayFailureWindow(db.Model):
+    __tablename__ = "edge_gateway_failure_windows"
+    __table_args__ = {"schema": "public"}
+    failure_fingerprint = db.Column(db.String(64), primary_key=True)
+    window_started_at = db.Column(db.DateTime(timezone=True), primary_key=True)
+    gateway_id = db.Column(UUID(as_uuid=False))
+    event_type = db.Column(db.String(80), nullable=False)
+    occurrence_count = db.Column(db.Integer, nullable=False)
+    first_seen_at = db.Column(db.DateTime(timezone=True), nullable=False)
+    last_seen_at = db.Column(db.DateTime(timezone=True), nullable=False)

+ 51 - 0
deployment/app/runner/api.py

@@ -9,6 +9,57 @@ from flask import Flask, jsonify, request
 from app.runner.auth import TaskTokenExpired, TaskTokenInvalid
 from app.runner.nodes import NodeExecutionError
 
+EDGE_RUNNER_NODES = frozenset(
+    {
+        "edge.collect",
+        "edge.profile",
+        "quality.check",
+        "edge.lineage",
+        "edge.controlled_query",
+    }
+)
+EDGE_RUNNER_REQUEST_FIELDS = frozenset(
+    {
+        "task_id",
+        "operation",
+        "purpose",
+        "classification",
+        "environment",
+        "network_zone",
+        "idempotency_key",
+        "deadline_at",
+    }
+)
+
+
+def execute_edge_adapter(handlers, node, edge_request, cancel_requested):
+    """Execute a closed local edge request without opening an HTTP surface.
+
+    Handlers are locally configured callables.  The control plane cannot name a
+    module, command, path, network target, script, or SQL statement through
+    this schema.
+    """
+
+    if node not in EDGE_RUNNER_NODES:
+        raise NodeExecutionError("edge runner node is not approved")
+    if not isinstance(edge_request, dict) or set(edge_request) != EDGE_RUNNER_REQUEST_FIELDS:
+        raise NodeExecutionError("edge runner request is invalid")
+    if not callable(cancel_requested):
+        raise NodeExecutionError("edge cancellation probe is invalid")
+    handler = handlers.get(node) if isinstance(handlers, dict) else None
+    if not callable(handler):
+        raise NodeExecutionError("edge runner node is unavailable")
+    if cancel_requested():
+        from app.edge_gateway.agent import EdgeTaskCancelled
+
+        raise EdgeTaskCancelled("edge task was cancelled")
+    result = handler(dict(edge_request), cancel_requested)
+    if cancel_requested():
+        from app.edge_gateway.agent import EdgeTaskCancelled
+
+        raise EdgeTaskCancelled("edge task was cancelled")
+    return result
+
 
 def create_runner_app(*, verifier, ledger, registry):
     app = Flask("dataops-runner")

+ 15 - 1
deployment/app/runner/bootstrap.py

@@ -12,7 +12,7 @@ from app.core.data_source.runtime import (
     DataSourceRuntimeConfig,
     build_standalone_data_source_runtime,
 )
-from app.runner.api import create_runner_app
+from app.runner.api import create_runner_app, execute_edge_adapter
 from app.runner.artifacts import ArtifactStore, PostgresArtifactResolver
 from app.runner.auth import TaskTokenVerifier
 from app.runner.ledger import PostgresTaskLedger
@@ -79,6 +79,20 @@ class RunnerSettings:
     max_query_rows: int = 1000
 
 
+def build_edge_runner_adapter(handlers):
+    """Build the local-only edge adapter around explicitly wired handlers."""
+
+    if not isinstance(handlers, dict) or not handlers:
+        raise ValueError("explicit edge runner handlers are required")
+    from app.edge_gateway.agent import EdgeRunnerAdapter
+
+    return EdgeRunnerAdapter(
+        lambda node, request, cancel: execute_edge_adapter(
+            handlers, node, dict(request), cancel
+        )
+    )
+
+
 def register_artifact_cleanup_cli(
     app, artifact_resolver, evidence_writer=None
 ):

+ 33 - 0
deployment/config/nginx-dataops-platform.conf

@@ -33,3 +33,36 @@ server {
         proxy_read_timeout 180s;
     }
 }
+
+# Dedicated machine edge endpoint. Keep this port separate from the human UI.
+server {
+    listen 18443 ssl;
+    server_name edge-control.company.citupro.com;
+
+    ssl_protocols TLSv1.2 TLSv1.3;
+    ssl_certificate /etc/nginx/ssl/edge-control-server.crt;
+    ssl_certificate_key /etc/nginx/ssl/edge-control-server.key;
+    ssl_client_certificate /etc/nginx/ssl/edge-client-ca.crt;
+    ssl_crl /etc/nginx/ssl/edge-client-ca.crl;
+    ssl_verify_client on;
+    ssl_verify_depth 3;
+    client_max_body_size 256k;
+
+    location /api/datasource/edge/ {
+        proxy_pass http://127.0.0.1:5500;
+        proxy_http_version 1.1;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto https;
+        proxy_set_header X-DataOps-Edge-Client-Cert $ssl_client_escaped_cert;
+        proxy_set_header X-DataOps-Edge-Client-Verify $ssl_client_verify;
+        proxy_connect_timeout 5s;
+        proxy_send_timeout 30s;
+        proxy_read_timeout 30s;
+    }
+
+    location / {
+        return 404;
+    }
+}

+ 13 - 2
deployment/dataops.env

@@ -13,9 +13,17 @@ AUDIT_EVIDENCE_SECRET=replace-with-dedicated-64-hex-random-value
 AUDIT_EVIDENCE_KEY_VERSION=v1
 # Comma-separated exact hostnames. Keep empty until enterprise SIEM is approved.
 SECURITY_SIEM_HOST_ALLOWLIST=
+# Exact local Nginx/reverse-proxy addresses authorized to supply edge mTLS proof.
+EDGE_MTLS_TRUSTED_PROXY_IPS=127.0.0.1
 
-# 平台 PostgreSQL(可与平台同机;部署前替换密码)
-DATABASE_URL=postgresql://dataops_user:replace-password@127.0.0.1:5432/dataops
+# 平台 PostgreSQL:DBA role-init、schema migrator、常驻 runtime 三种身份必须分离。
+# role-init 身份仅在一次性预置/轮换步骤使用,需 CREATEROLE;不得注入 Gunicorn。
+DB_ROLE_INIT_DATABASE_URL=postgresql://dataops_role_admin:replace-role-init-password@127.0.0.1:5432/dataops
+MIGRATION_DATABASE_URL=postgresql://dataops_migrator:replace-migration-password@127.0.0.1:5432/dataops
+DATABASE_URL=postgresql://dataops_app:replace-runtime-password@127.0.0.1:5432/dataops
+DATAOPS_MIGRATOR_USER=dataops_migrator
+DATAOPS_RUNTIME_USER=dataops_app
+DATAOPS_RUNTIME_PASSWORD=replace-runtime-password
 
 # Neo4j
 NEO4J_URI=bolt://127.0.0.1:7687
@@ -72,3 +80,6 @@ LISTEN_PORT=5500
 PORT=5500
 GUNICORN_WORKERS=4
 GUNICORN_TIMEOUT=120
+EDGE_GATEWAY_SIGNING_PRIVATE_KEY_FILE=/etc/dataops-platform/secrets/edge-gateway-signing.key
+EDGE_GATEWAY_SIGNING_PUBLIC_KEY=replace-edge-signing-public-key
+EDGE_GATEWAY_SIGNING_KEY_ID=replace-edge-signing-key-id

+ 7 - 0
deployment/deploy_dataops.sh

@@ -102,6 +102,13 @@ install_env_file() {
 
 install_env_file
 
+DATABASE_ENV_VALIDATOR="${SCRIPT_DIR}/app/config/database_urls.py"
+if [[ ! -f "${DATABASE_ENV_VALIDATOR}" ]] || \
+   ! python3 "${DATABASE_ENV_VALIDATOR}" --env-file "${ENV_FILE}"; then
+  warn "数据库连接或 runtime 密码仍为空、格式错误或包含模板占位值"
+  exit 2
+fi
+
 if grep -qE 'replace-with|replace-n8n|your-api-key|your-deepseek' "${ENV_FILE}" 2>/dev/null; then
   warn "环境变量仍含模板占位符,请编辑 ${ENV_FILE} 后重新部署"
   warn "必须配置: SECRET_KEY, DEEPSEEK_API_KEY, N8N_API_KEY 及数据库/MinIO 连接"

+ 5 - 4
deployment/migrations/env.py

@@ -6,17 +6,18 @@ from logging.config import fileConfig
 from alembic import context
 from sqlalchemy import engine_from_config, pool
 
-
 config = context.config
 if config.config_file_name is not None:
     fileConfig(config.config_file_name)
 
-database_url = os.environ.get("SQLALCHEMY_DATABASE_URI") or os.environ.get(
-    "DATABASE_URL"
+database_url = (
+    os.environ.get("MIGRATION_DATABASE_URL")
+    or os.environ.get("SQLALCHEMY_DATABASE_URI")
+    or os.environ.get("DATABASE_URL")
 )
 if not database_url:
     raise RuntimeError(
-        "SQLALCHEMY_DATABASE_URI or DATABASE_URL must be configured for migrations"
+        "MIGRATION_DATABASE_URL must be configured for migrations"
     )
 
 # ConfigParser treats percent signs as interpolation markers.

+ 532 - 0
deployment/migrations/versions/20260809_477_edge_gateway_control_plane.py

@@ -0,0 +1,532 @@
+"""Enterprise edge-gateway control-plane identity and durable ledgers."""
+
+from alembic import op
+
+revision = "20260809_477"
+down_revision = "20260802_476"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+    op.execute(r"""
+    DO $$
+    BEGIN
+      IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='dataops_app_runtime') OR
+         NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='dataops_edge_evidence_owner') THEN
+        RAISE EXCEPTION 'edge database roles are not pre-provisioned; run the privileged role-init step';
+      END IF;
+      IF EXISTS (
+        SELECT 1 FROM pg_roles
+         WHERE rolname IN ('dataops_app_runtime','dataops_edge_evidence_owner')
+           AND (rolcanlogin OR rolsuper OR rolcreatedb OR rolcreaterole OR rolreplication)
+      ) THEN
+        RAISE EXCEPTION 'edge database roles violate the NOLOGIN least-privilege contract';
+      END IF;
+    END $$;
+    CREATE OR REPLACE FUNCTION public.edge_hosts_valid(hosts TEXT[], require_nonempty BOOLEAN)
+    RETURNS BOOLEAN LANGUAGE plpgsql IMMUTABLE STRICT AS $$
+    DECLARE host TEXT;
+    BEGIN
+      IF cardinality(hosts)>32 OR (require_nonempty AND cardinality(hosts)=0) OR
+         cardinality(hosts)<>(SELECT count(DISTINCT item) FROM unnest(hosts) item) THEN
+        RETURN FALSE;
+      END IF;
+      FOREACH host IN ARRAY hosts LOOP
+        IF host<>lower(host) OR host !~ '^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$' OR
+           length(host)>253 OR host LIKE '%..%' OR host LIKE '%:%' OR
+           host LIKE '%/%' OR host LIKE '%@%' THEN
+          RETURN FALSE;
+        END IF;
+      END LOOP;
+      RETURN TRUE;
+    END $$;
+    CREATE TABLE public.edge_gateway_enrollments (
+      uid UUID PRIMARY KEY, gateway_id UUID NOT NULL UNIQUE,
+      gateway_name VARCHAR(200) NOT NULL, environment VARCHAR(20) NOT NULL,
+      network_zone VARCHAR(120) NOT NULL, policy_digest CHAR(64) NOT NULL,
+      expected_certificate_sha256 CHAR(64) NOT NULL,
+      allowed_control_hosts TEXT[] NOT NULL, allowed_proxy_hosts TEXT[] NOT NULL,
+      token_hash CHAR(64) NOT NULL UNIQUE, status VARCHAR(20) NOT NULL DEFAULT 'pending',
+      expires_at TIMESTAMPTZ NOT NULL, consumed_at TIMESTAMPTZ,
+      created_by UUID NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
+      CONSTRAINT ck_edge_enrollment_environment CHECK (environment IN ('development','staging','production')),
+      CONSTRAINT ck_edge_enrollment_status CHECK (status IN ('pending','consumed','revoked','expired')),
+      CONSTRAINT ck_edge_enrollment_digest CHECK (
+        policy_digest ~ '^[0-9a-f]{64}$' AND token_hash ~ '^[0-9a-f]{64}$' AND
+        expected_certificate_sha256 ~ '^[0-9a-f]{64}$'),
+      CONSTRAINT ck_edge_enrollment_ttl CHECK (expires_at > created_at AND expires_at <= created_at + INTERVAL '24 hours'),
+      CONSTRAINT ck_edge_enrollment_consumed CHECK ((status='consumed') = (consumed_at IS NOT NULL)),
+      CONSTRAINT ck_edge_enrollment_hosts CHECK (
+        public.edge_hosts_valid(allowed_control_hosts,TRUE) AND
+        public.edge_hosts_valid(allowed_proxy_hosts,FALSE))
+    );
+    CREATE INDEX ix_edge_enrollment_pending ON public.edge_gateway_enrollments(token_hash,expires_at) WHERE status='pending';
+
+    CREATE OR REPLACE FUNCTION public.protect_edge_enrollment_identity()
+    RETURNS trigger LANGUAGE plpgsql AS $$
+    BEGIN
+      IF NEW.uid<>OLD.uid OR NEW.gateway_id<>OLD.gateway_id OR
+         NEW.gateway_name<>OLD.gateway_name OR NEW.environment<>OLD.environment OR
+         NEW.network_zone<>OLD.network_zone OR NEW.policy_digest<>OLD.policy_digest OR
+         NEW.expected_certificate_sha256<>OLD.expected_certificate_sha256 OR
+         NEW.allowed_control_hosts<>OLD.allowed_control_hosts OR
+         NEW.allowed_proxy_hosts<>OLD.allowed_proxy_hosts OR NEW.token_hash<>OLD.token_hash OR
+         NEW.expires_at<>OLD.expires_at OR NEW.created_by<>OLD.created_by OR
+         NEW.created_at<>OLD.created_at THEN
+        RAISE EXCEPTION 'edge enrollment identity binding is immutable';
+      END IF;
+      IF NOT (
+        (OLD.status='pending' AND NEW.status IN ('pending','consumed','revoked','expired')) OR
+        (OLD.status<>'pending' AND NEW.status=OLD.status)
+      ) THEN
+        RAISE EXCEPTION 'edge enrollment status transition is invalid';
+      END IF;
+      RETURN NEW;
+    END $$;
+    CREATE TRIGGER trg_edge_enrollment_identity_immutable
+      BEFORE UPDATE ON public.edge_gateway_enrollments FOR EACH ROW
+      EXECUTE FUNCTION public.protect_edge_enrollment_identity();
+
+    CREATE TABLE public.edge_gateways (
+      uid UUID PRIMARY KEY, name VARCHAR(200) NOT NULL, environment VARCHAR(20) NOT NULL,
+      network_zone VARCHAR(120) NOT NULL, status VARCHAR(20) NOT NULL,
+      current_generation INTEGER NOT NULL, policy_digest CHAR(64) NOT NULL,
+      allowed_control_hosts TEXT[] NOT NULL, allowed_proxy_hosts TEXT[] NOT NULL,
+      last_heartbeat_at TIMESTAMPTZ, runtime_version VARCHAR(80),
+      safe_health_summary JSONB NOT NULL DEFAULT '{}'::jsonb,
+      created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
+      updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, revoked_at TIMESTAMPTZ,
+      CONSTRAINT ck_edge_gateway_environment CHECK (environment IN ('development','staging','production')),
+      CONSTRAINT ck_edge_gateway_status CHECK (status IN ('pending','active','offline','revoked')),
+      CONSTRAINT ck_edge_gateway_generation CHECK (current_generation > 0),
+      CONSTRAINT ck_edge_gateway_policy CHECK (policy_digest ~ '^[0-9a-f]{64}$'),
+      CONSTRAINT ck_edge_gateway_summary CHECK (jsonb_typeof(safe_health_summary)='object'),
+      CONSTRAINT ck_edge_gateway_hosts CHECK (
+        public.edge_hosts_valid(allowed_control_hosts,TRUE) AND
+        public.edge_hosts_valid(allowed_proxy_hosts,FALSE)),
+      CONSTRAINT fk_edge_gateway_enrollment FOREIGN KEY (uid)
+        REFERENCES public.edge_gateway_enrollments(gateway_id)
+    );
+    CREATE INDEX ix_edge_gateways_environment_status ON public.edge_gateways(environment,status);
+
+    CREATE TABLE public.edge_gateway_credentials (
+      uid UUID PRIMARY KEY, gateway_id UUID NOT NULL REFERENCES public.edge_gateways(uid),
+      generation INTEGER NOT NULL, credential_hash CHAR(64) NOT NULL UNIQUE,
+      certificate_sha256 CHAR(64) NOT NULL, status VARCHAR(20) NOT NULL,
+      expires_at TIMESTAMPTZ NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
+      revoked_at TIMESTAMPTZ, rotated_from_uid UUID REFERENCES public.edge_gateway_credentials(uid),
+      CONSTRAINT uq_edge_credential_generation UNIQUE(gateway_id,generation),
+      CONSTRAINT ck_edge_credential_status CHECK (status IN ('active','rotated','revoked','expired')),
+      CONSTRAINT ck_edge_credential_generation CHECK (generation > 0),
+      CONSTRAINT ck_edge_credential_hashes CHECK (credential_hash ~ '^[0-9a-f]{64}$' AND certificate_sha256 ~ '^[0-9a-f]{64}$'),
+      CONSTRAINT ck_edge_credential_expiry CHECK (expires_at > created_at AND expires_at <= created_at + INTERVAL '370 days')
+    );
+    CREATE INDEX ix_edge_credential_auth ON public.edge_gateway_credentials(credential_hash,status,expires_at);
+    CREATE UNIQUE INDEX uq_edge_one_active_credential
+      ON public.edge_gateway_credentials(gateway_id) WHERE status='active';
+    CREATE TABLE public.edge_gateway_rotation_requests (
+      gateway_id UUID NOT NULL REFERENCES public.edge_gateways(uid),
+      request_id VARCHAR(255) NOT NULL, request_digest CHAR(64) NOT NULL,
+      generation INTEGER NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
+      PRIMARY KEY(gateway_id,request_id),
+      CONSTRAINT ck_edge_rotation_digest CHECK (request_digest ~ '^[0-9a-f]{64}$')
+    );
+
+    CREATE OR REPLACE FUNCTION public.protect_edge_gateway_identity()
+    RETURNS trigger LANGUAGE plpgsql AS $$
+    BEGIN
+      IF NEW.uid<>OLD.uid OR NEW.name<>OLD.name OR NEW.environment<>OLD.environment OR
+         NEW.network_zone<>OLD.network_zone OR NEW.policy_digest<>OLD.policy_digest OR
+         NEW.allowed_control_hosts<>OLD.allowed_control_hosts OR
+         NEW.allowed_proxy_hosts<>OLD.allowed_proxy_hosts THEN
+        RAISE EXCEPTION 'registered edge gateway identity binding is immutable';
+      END IF;
+      IF OLD.status='revoked' AND NEW.status<>'revoked' THEN
+        RAISE EXCEPTION 'revoked edge gateway is terminal';
+      END IF;
+      IF NEW.current_generation<OLD.current_generation OR
+         NEW.current_generation>OLD.current_generation+1 THEN
+        RAISE EXCEPTION 'edge gateway generation transition is invalid';
+      END IF;
+      IF NOT (
+        (OLD.status='pending' AND NEW.status IN ('pending','active','revoked')) OR
+        (OLD.status='active' AND NEW.status IN ('active','offline','revoked')) OR
+        (OLD.status='offline' AND NEW.status IN ('active','offline','revoked')) OR
+        (OLD.status='revoked' AND NEW.status='revoked')
+      ) THEN
+        RAISE EXCEPTION 'edge gateway status transition is invalid';
+      END IF;
+      RETURN NEW;
+    END $$;
+    CREATE TRIGGER trg_edge_gateway_identity_immutable
+      BEFORE UPDATE ON public.edge_gateways FOR EACH ROW
+      EXECUTE FUNCTION public.protect_edge_gateway_identity();
+
+    CREATE OR REPLACE FUNCTION public.protect_edge_credential_identity()
+    RETURNS trigger LANGUAGE plpgsql AS $$
+    BEGIN
+      IF NEW.uid<>OLD.uid OR NEW.gateway_id<>OLD.gateway_id OR
+         NEW.generation<>OLD.generation OR NEW.credential_hash<>OLD.credential_hash OR
+         NEW.certificate_sha256<>OLD.certificate_sha256 OR
+         NEW.created_at<>OLD.created_at OR NEW.expires_at<>OLD.expires_at THEN
+        RAISE EXCEPTION 'edge credential identity binding is immutable';
+      END IF;
+      IF NOT (
+        (OLD.status='active' AND NEW.status IN ('active','rotated','revoked','expired')) OR
+        (OLD.status<>'active' AND NEW.status=OLD.status)
+      ) THEN
+        RAISE EXCEPTION 'edge credential status transition is invalid';
+      END IF;
+      RETURN NEW;
+    END $$;
+    CREATE TRIGGER trg_edge_credential_identity_immutable
+      BEFORE UPDATE ON public.edge_gateway_credentials FOR EACH ROW
+      EXECUTE FUNCTION public.protect_edge_credential_identity();
+
+    CREATE OR REPLACE FUNCTION public.validate_edge_gateway_binding(p_gateway UUID)
+    RETURNS void LANGUAGE plpgsql AS $$
+    DECLARE g public.edge_gateways%ROWTYPE;
+    BEGIN
+      SELECT * INTO g FROM public.edge_gateways WHERE uid=p_gateway;
+      IF NOT FOUND OR g.status='revoked' THEN RETURN; END IF;
+      IF g.status IN ('active','offline') AND NOT EXISTS (
+        SELECT 1 FROM public.edge_gateway_credentials c
+         WHERE c.gateway_id=g.uid AND c.generation=g.current_generation
+           AND c.status='active' AND c.expires_at>CURRENT_TIMESTAMP
+      ) THEN
+        RAISE EXCEPTION 'active edge gateway requires its exact active credential binding';
+      END IF;
+      IF g.status IN ('active','offline') AND NOT EXISTS (
+        SELECT 1 FROM public.edge_gateway_enrollments e
+        JOIN public.edge_gateway_credentials c ON c.gateway_id=g.uid AND c.generation=1
+         WHERE e.gateway_id=g.uid AND e.status='consumed'
+           AND e.gateway_name=g.name AND e.environment=g.environment
+           AND e.network_zone=g.network_zone AND e.policy_digest=g.policy_digest
+           AND e.allowed_control_hosts=g.allowed_control_hosts
+           AND e.allowed_proxy_hosts=g.allowed_proxy_hosts
+           AND e.expected_certificate_sha256=c.certificate_sha256
+      ) THEN
+        RAISE EXCEPTION 'active edge gateway does not match its consumed enrollment binding';
+      END IF;
+      IF EXISTS (
+        SELECT 1 FROM public.edge_gateway_credentials c
+        JOIN public.edge_gateway_enrollments e ON e.gateway_id=c.gateway_id
+         WHERE c.gateway_id=g.uid AND c.generation=1
+           AND c.certificate_sha256<>e.expected_certificate_sha256
+      ) THEN
+        RAISE EXCEPTION 'initial edge certificate does not match enrollment binding';
+      END IF;
+    END $$;
+    CREATE OR REPLACE FUNCTION public.enforce_edge_gateway_binding()
+    RETURNS trigger LANGUAGE plpgsql AS $$
+    BEGIN
+      IF TG_OP='UPDATE' AND OLD.status IN ('active','offline') AND NEW.status='pending' THEN
+        RAISE EXCEPTION 'registered edge gateway cannot return to pending status';
+      END IF;
+      PERFORM public.validate_edge_gateway_binding(COALESCE(NEW.uid,OLD.uid));
+      RETURN NULL;
+    END $$;
+    CREATE CONSTRAINT TRIGGER trg_edge_gateway_binding
+      AFTER INSERT OR UPDATE ON public.edge_gateways DEFERRABLE INITIALLY DEFERRED
+      FOR EACH ROW EXECUTE FUNCTION public.enforce_edge_gateway_binding();
+    CREATE OR REPLACE FUNCTION public.enforce_edge_credential_binding()
+    RETURNS trigger LANGUAGE plpgsql AS $$
+    DECLARE target UUID;
+    BEGIN
+      target := COALESCE(NEW.gateway_id,OLD.gateway_id);
+      PERFORM public.validate_edge_gateway_binding(target);
+      IF TG_OP <> 'DELETE' AND NEW.status='active' AND NOT EXISTS (
+        SELECT 1 FROM public.edge_gateways g WHERE g.uid=NEW.gateway_id
+         AND g.status IN ('active','offline') AND g.current_generation=NEW.generation
+      ) THEN
+        RAISE EXCEPTION 'active credential must be the exact current gateway binding';
+      END IF;
+      RETURN NULL;
+    END $$;
+    CREATE CONSTRAINT TRIGGER trg_edge_credential_binding
+      AFTER INSERT OR UPDATE OR DELETE ON public.edge_gateway_credentials
+      DEFERRABLE INITIALLY DEFERRED FOR EACH ROW
+      EXECUTE FUNCTION public.enforce_edge_credential_binding();
+
+    CREATE TABLE public.edge_gateway_tasks (
+      task_id VARCHAR(255) PRIMARY KEY, gateway_id UUID NOT NULL REFERENCES public.edge_gateways(uid),
+      idempotency_key VARCHAR(255) NOT NULL, contract JSONB NOT NULL, contract_digest CHAR(64) NOT NULL,
+      status VARCHAR(24) NOT NULL, lease_token_hash CHAR(64), lease_expires_at TIMESTAMPTZ,
+      cancel_requested_at TIMESTAMPTZ, result_summary JSONB,
+      created_by UUID NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
+      updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
+      CONSTRAINT uq_edge_task_idempotency UNIQUE(gateway_id,idempotency_key),
+      CONSTRAINT uq_edge_task_gateway UNIQUE(task_id,gateway_id),
+      CONSTRAINT ck_edge_task_status CHECK (status IN ('pending','leased','cancel_requested','completed','failed','cancelled')),
+      CONSTRAINT ck_edge_task_contract CHECK (
+        jsonb_typeof(contract)='object' AND contract_digest ~ '^[0-9a-f]{64}$' AND
+        contract->>'task_id'=task_id AND contract->>'gateway_id'=gateway_id::text AND
+        contract->>'idempotency_key'=idempotency_key),
+      CONSTRAINT ck_edge_task_lease CHECK (
+        (status='leased' AND lease_token_hash IS NOT NULL AND lease_expires_at IS NOT NULL) OR
+        (status='cancel_requested' AND ((lease_token_hash IS NULL AND lease_expires_at IS NULL) OR
+          (lease_token_hash IS NOT NULL AND lease_expires_at IS NOT NULL))) OR
+        (status NOT IN ('leased','cancel_requested') AND lease_token_hash IS NULL AND lease_expires_at IS NULL)),
+      CONSTRAINT ck_edge_task_result CHECK (result_summary IS NULL OR jsonb_typeof(result_summary)='object')
+    );
+    CREATE INDEX ix_edge_task_pull ON public.edge_gateway_tasks(gateway_id,status,created_at);
+
+    CREATE TABLE public.edge_gateway_events (
+      event_id VARCHAR(80) PRIMARY KEY, gateway_id UUID NOT NULL REFERENCES public.edge_gateways(uid),
+      task_id VARCHAR(255) NOT NULL,
+      classification VARCHAR(40) NOT NULL, contract JSONB NOT NULL, contract_digest CHAR(64) NOT NULL,
+      ack JSONB NOT NULL, received_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
+      CONSTRAINT ck_edge_event_classification CHECK (classification IN ('desensitized_metadata','statistics','lineage','evidence','health_summary','diagnostic_summary')),
+      CONSTRAINT fk_edge_event_task_gateway FOREIGN KEY (task_id,gateway_id)
+        REFERENCES public.edge_gateway_tasks(task_id,gateway_id),
+      CONSTRAINT ck_edge_event_contract CHECK (
+        jsonb_typeof(contract)='object' AND jsonb_typeof(ack)='object' AND
+        contract_digest ~ '^[0-9a-f]{64}$' AND contract->>'event_id'=event_id AND
+        contract->>'task_id'=task_id AND contract->>'gateway_id'=gateway_id::text AND
+        contract->>'classification'=classification)
+    );
+    CREATE INDEX ix_edge_event_gateway_received ON public.edge_gateway_events(gateway_id,received_at DESC);
+
+    CREATE TABLE public.edge_gateway_releases (
+      uid UUID PRIMARY KEY, gateway_id UUID NOT NULL REFERENCES public.edge_gateways(uid),
+      version VARCHAR(80) NOT NULL, artifact_digest CHAR(64) NOT NULL,
+      rollback_version VARCHAR(80) NOT NULL, status VARCHAR(24) NOT NULL,
+      request_id VARCHAR(255) NOT NULL, request_digest CHAR(64) NOT NULL,
+      safe_summary JSONB NOT NULL DEFAULT '{}'::jsonb, created_by UUID NOT NULL,
+      created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, acknowledged_at TIMESTAMPTZ,
+      CONSTRAINT uq_edge_release_version UNIQUE(gateway_id,version),
+      CONSTRAINT uq_edge_release_request UNIQUE(gateway_id,request_id),
+      CONSTRAINT ck_edge_release_status CHECK (status IN ('offered','accepted','installed','failed','rolled_back')),
+      CONSTRAINT ck_edge_release_digest CHECK (artifact_digest ~ '^[0-9a-f]{64}$' AND request_digest ~ '^[0-9a-f]{64}$'),
+      CONSTRAINT ck_edge_release_summary CHECK (jsonb_typeof(safe_summary)='object')
+    );
+
+    CREATE TABLE public.edge_gateway_audits (
+      uid UUID PRIMARY KEY, gateway_id UUID REFERENCES public.edge_gateways(uid),
+      event_type VARCHAR(80) NOT NULL, actor_uid UUID, success BOOLEAN NOT NULL,
+      safe_detail JSONB NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
+      CONSTRAINT ck_edge_audit_detail CHECK (jsonb_typeof(safe_detail)='object')
+    );
+    CREATE INDEX ix_edge_audit_gateway_created ON public.edge_gateway_audits(gateway_id,created_at DESC);
+    CREATE TABLE public.edge_gateway_failure_windows (
+      failure_fingerprint CHAR(64) NOT NULL, window_started_at TIMESTAMPTZ NOT NULL,
+      gateway_id UUID REFERENCES public.edge_gateways(uid), event_type VARCHAR(80) NOT NULL,
+      occurrence_count INTEGER NOT NULL, first_seen_at TIMESTAMPTZ NOT NULL,
+      last_seen_at TIMESTAMPTZ NOT NULL,
+      PRIMARY KEY(failure_fingerprint,window_started_at),
+      CONSTRAINT ck_edge_failure_count CHECK (occurrence_count BETWEEN 1 AND 1000000)
+    );
+    ALTER TABLE public.edge_gateway_audits ADD COLUMN failure_fingerprint CHAR(64);
+    ALTER TABLE public.edge_gateway_audits ADD COLUMN window_started_at TIMESTAMPTZ;
+    CREATE UNIQUE INDEX uq_edge_failure_audit_window
+      ON public.edge_gateway_audits(failure_fingerprint,window_started_at)
+      WHERE success=FALSE;
+    CREATE OR REPLACE FUNCTION public.protect_edge_audit_append_only()
+    RETURNS trigger LANGUAGE plpgsql AS $$
+    BEGIN
+      IF TG_OP='INSERT' AND current_user='dataops_edge_evidence_owner' THEN
+        RETURN NEW;
+      END IF;
+      RAISE EXCEPTION 'edge gateway audit ledger is owner-written and append-only';
+    END $$;
+    CREATE TRIGGER trg_edge_audit_append_only
+      BEFORE INSERT OR UPDATE OR DELETE ON public.edge_gateway_audits
+      FOR EACH ROW EXECUTE FUNCTION public.protect_edge_audit_append_only();
+    CREATE TRIGGER trg_edge_audit_no_truncate
+      BEFORE TRUNCATE ON public.edge_gateway_audits
+      FOR EACH STATEMENT EXECUTE FUNCTION public.protect_edge_audit_append_only();
+    CREATE OR REPLACE FUNCTION public.protect_edge_failure_window()
+    RETURNS trigger LANGUAGE plpgsql AS $$
+    BEGIN
+      IF TG_OP IN ('DELETE','TRUNCATE') THEN
+        RAISE EXCEPTION 'edge gateway failure evidence cannot be removed';
+      END IF;
+      IF current_user<>'dataops_edge_evidence_owner' THEN
+        RAISE EXCEPTION 'edge gateway failure evidence requires the controlled writer';
+      END IF;
+      IF TG_OP='INSERT' THEN RETURN NEW; END IF;
+      IF NEW.failure_fingerprint<>OLD.failure_fingerprint OR
+         NEW.window_started_at<>OLD.window_started_at OR
+         NEW.gateway_id IS DISTINCT FROM OLD.gateway_id OR
+         NEW.event_type<>OLD.event_type OR NEW.first_seen_at<>OLD.first_seen_at THEN
+        RAISE EXCEPTION 'edge gateway failure window identity is immutable';
+      END IF;
+      IF NEW.occurrence_count<>OLD.occurrence_count+1 OR
+         NEW.last_seen_at<OLD.last_seen_at OR NEW.last_seen_at<NEW.first_seen_at THEN
+        RAISE EXCEPTION 'edge gateway failure window update is invalid';
+      END IF;
+      RETURN NEW;
+    END $$;
+    CREATE TRIGGER trg_edge_failure_window_protected
+      BEFORE INSERT OR UPDATE OR DELETE ON public.edge_gateway_failure_windows
+      FOR EACH ROW EXECUTE FUNCTION public.protect_edge_failure_window();
+    CREATE TRIGGER trg_edge_failure_window_no_truncate
+      BEFORE TRUNCATE ON public.edge_gateway_failure_windows
+      FOR EACH STATEMENT EXECUTE FUNCTION public.protect_edge_failure_window();
+    CREATE OR REPLACE FUNCTION public.record_edge_gateway_audit(
+      p_uid UUID,p_gateway UUID,p_event_type VARCHAR(80),p_actor UUID,
+      p_success BOOLEAN,p_detail JSONB
+    ) RETURNS void LANGUAGE plpgsql SECURITY DEFINER
+      SET search_path=pg_catalog AS $$
+    BEGIN
+      IF p_uid IS NULL OR p_event_type NOT IN (
+        'enrollment_created','gateway_registered','credential_rotated','gateway_revoked',
+        'heartbeat_accepted','task_issued','task_cancel_requested','task_outcome_recorded',
+        'event_accepted','release_offered','release_acknowledged'
+      ) OR jsonb_typeof(p_detail)<>'object' OR octet_length(p_detail::text)>8192 OR
+         (p_gateway IS NOT NULL AND NOT EXISTS (
+           SELECT 1 FROM public.edge_gateways WHERE uid=p_gateway
+         )) THEN
+        RAISE EXCEPTION 'edge gateway audit evidence is invalid';
+      END IF;
+      INSERT INTO public.edge_gateway_audits
+        (uid,gateway_id,event_type,actor_uid,success,safe_detail)
+      VALUES (p_uid,p_gateway,p_event_type,p_actor,p_success,p_detail);
+    END $$;
+    CREATE OR REPLACE FUNCTION public.record_edge_gateway_failure(
+      p_audit_uid UUID,p_fingerprint CHAR(64),p_gateway UUID,
+      p_event_type VARCHAR(80),p_reason_code VARCHAR(64)
+    ) RETURNS TABLE(
+      failure_fingerprint CHAR(64), window_started_at TIMESTAMPTZ,
+      occurrence_count INTEGER, gateway_id UUID, event_type VARCHAR(80)
+    ) LANGUAGE plpgsql SECURITY DEFINER
+      SET search_path=pg_catalog AS $$
+    DECLARE
+      target_fingerprint CHAR(64) := p_fingerprint;
+      target_gateway UUID := p_gateway;
+      target_event_type VARCHAR(80) := p_event_type;
+      target_reason_code VARCHAR(64) := p_reason_code;
+      target_window TIMESTAMPTZ := date_trunc('minute',CURRENT_TIMESTAMP);
+      total_windows INTEGER;
+    BEGIN
+      IF p_audit_uid IS NULL OR p_fingerprint !~ '^[0-9a-f]{64}$' OR
+         p_event_type NOT IN (
+           'untrusted_request_rejected','policy_rejected','binding_rejected',
+           'idempotency_conflict','release_rejected'
+         ) OR p_reason_code !~ '^[a-z][a-z0-9_]{0,63}$' OR
+         (p_gateway IS NOT NULL AND NOT EXISTS (
+           SELECT 1 FROM public.edge_gateways WHERE uid=p_gateway
+         )) THEN
+        RAISE EXCEPTION 'edge gateway failure evidence is invalid';
+      END IF;
+      PERFORM pg_advisory_xact_lock(4774096);
+      SELECT count(*) INTO total_windows FROM public.edge_gateway_failure_windows;
+      IF NOT EXISTS (
+        SELECT 1 FROM public.edge_gateway_failure_windows AS existing
+         WHERE existing.failure_fingerprint=target_fingerprint
+           AND existing.window_started_at=target_window
+      ) AND total_windows>=4095 THEN
+        target_fingerprint := '16b5bae3613ee26975528e149d401a46f1d659405cfe0148262caec288dfb365';
+        target_gateway := NULL;
+        target_event_type := 'untrusted_request_rejected';
+        target_reason_code := 'capacity_overflow';
+        target_window := '1970-01-01 00:00:00+00'::timestamptz;
+      END IF;
+      RETURN QUERY
+        INSERT INTO public.edge_gateway_failure_windows AS windows
+          (failure_fingerprint,window_started_at,gateway_id,event_type,
+           occurrence_count,first_seen_at,last_seen_at)
+        VALUES (target_fingerprint,target_window,target_gateway,target_event_type,
+                1,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)
+        ON CONFLICT ON CONSTRAINT edge_gateway_failure_windows_pkey DO UPDATE
+          SET occurrence_count=windows.occurrence_count+1,
+              last_seen_at=CURRENT_TIMESTAMP
+        RETURNING windows.failure_fingerprint,windows.window_started_at,
+                  windows.occurrence_count,windows.gateway_id,windows.event_type;
+      IF NOT EXISTS (
+        SELECT 1 FROM public.edge_gateway_audits AS existing_audit
+         WHERE existing_audit.failure_fingerprint=target_fingerprint
+           AND existing_audit.window_started_at=target_window
+           AND existing_audit.success=FALSE
+      ) THEN
+        INSERT INTO public.edge_gateway_audits
+          (uid,gateway_id,event_type,actor_uid,success,safe_detail,
+           failure_fingerprint,window_started_at)
+        VALUES (p_audit_uid,target_gateway,target_event_type,NULL,FALSE,
+                jsonb_build_object('reason_code',target_reason_code),
+                target_fingerprint,target_window);
+      END IF;
+    END $$;
+    ALTER TABLE public.edge_gateway_audits OWNER TO dataops_edge_evidence_owner;
+    ALTER TABLE public.edge_gateway_failure_windows OWNER TO dataops_edge_evidence_owner;
+    ALTER FUNCTION public.protect_edge_audit_append_only() OWNER TO dataops_edge_evidence_owner;
+    ALTER FUNCTION public.protect_edge_failure_window() OWNER TO dataops_edge_evidence_owner;
+    ALTER FUNCTION public.record_edge_gateway_audit(UUID,UUID,VARCHAR(80),UUID,BOOLEAN,JSONB)
+      OWNER TO dataops_edge_evidence_owner;
+    ALTER FUNCTION public.record_edge_gateway_failure(UUID,CHAR(64),UUID,VARCHAR(80),VARCHAR(64))
+      OWNER TO dataops_edge_evidence_owner;
+    GRANT SELECT ON public.edge_gateways TO dataops_edge_evidence_owner;
+    REVOKE ALL ON public.edge_gateway_audits,public.edge_gateway_failure_windows FROM PUBLIC;
+    REVOKE ALL ON FUNCTION public.record_edge_gateway_audit(UUID,UUID,VARCHAR(80),UUID,BOOLEAN,JSONB) FROM PUBLIC;
+    REVOKE ALL ON FUNCTION public.record_edge_gateway_failure(UUID,CHAR(64),UUID,VARCHAR(80),VARCHAR(64)) FROM PUBLIC;
+    GRANT USAGE ON SCHEMA public TO dataops_app_runtime;
+    GRANT SELECT,INSERT,UPDATE,DELETE ON
+      public.edge_gateway_enrollments,public.edge_gateways,
+      public.edge_gateway_credentials,public.edge_gateway_rotation_requests,
+      public.edge_gateway_tasks,public.edge_gateway_events,public.edge_gateway_releases
+      TO dataops_app_runtime;
+    ALTER DEFAULT PRIVILEGES IN SCHEMA public
+      GRANT SELECT,INSERT,UPDATE,DELETE ON TABLES TO dataops_app_runtime;
+    ALTER DEFAULT PRIVILEGES IN SCHEMA public
+      GRANT USAGE,SELECT,UPDATE ON SEQUENCES TO dataops_app_runtime;
+    REVOKE INSERT,UPDATE,DELETE,TRUNCATE ON
+      public.edge_gateway_audits,public.edge_gateway_failure_windows
+      FROM dataops_app_runtime;
+    GRANT SELECT ON public.edge_gateway_audits,public.edge_gateway_failure_windows
+      TO dataops_app_runtime;
+    GRANT EXECUTE ON FUNCTION public.record_edge_gateway_audit(UUID,UUID,VARCHAR(80),UUID,BOOLEAN,JSONB)
+      TO dataops_app_runtime;
+    GRANT EXECUTE ON FUNCTION public.record_edge_gateway_failure(UUID,CHAR(64),UUID,VARCHAR(80),VARCHAR(64))
+      TO dataops_app_runtime;
+    """)
+
+
+def downgrade() -> None:
+    op.execute("""
+    DO $$
+    DECLARE table_name TEXT; has_data BOOLEAN;
+    BEGIN
+      FOREACH table_name IN ARRAY ARRAY[
+        'edge_gateway_enrollments','edge_gateways','edge_gateway_credentials',
+        'edge_gateway_tasks','edge_gateway_events','edge_gateway_releases',
+        'edge_gateway_audits','edge_gateway_failure_windows','edge_gateway_rotation_requests'
+      ] LOOP
+        IF to_regclass('public.' || table_name) IS NOT NULL THEN
+          EXECUTE format('SELECT EXISTS (SELECT 1 FROM public.%I)',table_name) INTO has_data;
+          IF has_data THEN
+            RAISE EXCEPTION 'edge gateway downgrade rejected: clear Task2 business and audit data first';
+          END IF;
+        END IF;
+      END LOOP;
+    END $$;
+    DROP TRIGGER IF EXISTS trg_edge_audit_append_only ON public.edge_gateway_audits;
+    DROP TRIGGER IF EXISTS trg_edge_audit_no_truncate ON public.edge_gateway_audits;
+    DROP TRIGGER IF EXISTS trg_edge_failure_window_protected ON public.edge_gateway_failure_windows;
+    DROP TRIGGER IF EXISTS trg_edge_failure_window_no_truncate ON public.edge_gateway_failure_windows;
+    DROP FUNCTION IF EXISTS public.record_edge_gateway_failure(UUID,CHAR(64),UUID,VARCHAR(80),VARCHAR(64));
+    DROP FUNCTION IF EXISTS public.record_edge_gateway_audit(UUID,UUID,VARCHAR(80),UUID,BOOLEAN,JSONB);
+    DROP FUNCTION IF EXISTS public.protect_edge_failure_window();
+    DROP FUNCTION IF EXISTS public.protect_edge_audit_append_only();
+    DROP TABLE IF EXISTS public.edge_gateway_failure_windows;
+    DROP TABLE IF EXISTS public.edge_gateway_audits;
+    DROP TABLE IF EXISTS public.edge_gateway_events;
+    DROP TABLE IF EXISTS public.edge_gateway_releases;
+    DROP TABLE IF EXISTS public.edge_gateway_tasks;
+    DROP TABLE IF EXISTS public.edge_gateway_rotation_requests;
+    DROP TRIGGER IF EXISTS trg_edge_credential_identity_immutable ON public.edge_gateway_credentials;
+    DROP TRIGGER IF EXISTS trg_edge_gateway_identity_immutable ON public.edge_gateways;
+    DROP TRIGGER IF EXISTS trg_edge_credential_binding ON public.edge_gateway_credentials;
+    DROP TRIGGER IF EXISTS trg_edge_gateway_binding ON public.edge_gateways;
+    DROP FUNCTION IF EXISTS public.enforce_edge_credential_binding();
+    DROP FUNCTION IF EXISTS public.enforce_edge_gateway_binding();
+    DROP FUNCTION IF EXISTS public.validate_edge_gateway_binding(UUID);
+    DROP FUNCTION IF EXISTS public.protect_edge_credential_identity();
+    DROP FUNCTION IF EXISTS public.protect_edge_gateway_identity();
+    DROP TABLE IF EXISTS public.edge_gateway_credentials;
+    DROP TABLE IF EXISTS public.edge_gateways;
+    DROP TRIGGER IF EXISTS trg_edge_enrollment_identity_immutable ON public.edge_gateway_enrollments;
+    DROP TABLE IF EXISTS public.edge_gateway_enrollments;
+    DROP FUNCTION IF EXISTS public.protect_edge_enrollment_identity();
+    DROP FUNCTION IF EXISTS public.edge_hosts_valid(TEXT[],BOOLEAN);
+    """)

+ 22 - 0
deployment/migrations/versions/20260809_478_edge_gateway_security_cleanup.py

@@ -0,0 +1,22 @@
+"""Remove legacy edge evidence function overloads without restoring them."""
+
+from alembic import op
+
+revision = "20260809_478"
+down_revision = "20260809_477"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+    op.execute(r"""
+    DROP FUNCTION IF EXISTS public.record_edge_gateway_failure(
+      CHARACTER,UUID,CHARACTER VARYING
+    );
+    """)
+
+
+def downgrade() -> None:
+    # Deliberately non-restorative: the removed SECURITY DEFINER overload had an
+    # unsafe search_path and must never be recreated by a rollback.
+    pass

+ 187 - 0
deployment/migrations/versions/20260809_479_edge_gateway_signed_authority.py

@@ -0,0 +1,187 @@
+"""Persist signed edge task authorities and complete release manifests."""
+
+from alembic import op
+
+revision = "20260809_479"
+down_revision = "20260809_478"
+branch_labels = None
+depends_on = None
+
+
+def _failure_window_function(*, lock_clock: bool) -> str:
+    observed_at = "NULL" if lock_clock else "CURRENT_TIMESTAMP"
+    refresh_clock = (
+        "observed_at := clock_timestamp();\n"
+        "      target_window := date_trunc('minute',observed_at);"
+        if lock_clock
+        else ""
+    )
+    return rf"""
+    CREATE OR REPLACE FUNCTION public.record_edge_gateway_failure(
+      p_audit_uid UUID,p_fingerprint CHAR(64),p_gateway UUID,
+      p_event_type VARCHAR(80),p_reason_code VARCHAR(64)
+    ) RETURNS TABLE(
+      failure_fingerprint CHAR(64), window_started_at TIMESTAMPTZ,
+      occurrence_count INTEGER, gateway_id UUID, event_type VARCHAR(80)
+    ) LANGUAGE plpgsql SECURITY DEFINER
+      SET search_path=pg_catalog AS $$
+    DECLARE
+      target_fingerprint CHAR(64) := p_fingerprint;
+      target_gateway UUID := p_gateway;
+      target_event_type VARCHAR(80) := p_event_type;
+      target_reason_code VARCHAR(64) := p_reason_code;
+      observed_at TIMESTAMPTZ := {observed_at};
+      target_window TIMESTAMPTZ := date_trunc('minute',observed_at);
+      total_windows INTEGER;
+    BEGIN
+      IF p_audit_uid IS NULL OR p_fingerprint !~ '^[0-9a-f]{{64}}$' OR
+         p_event_type NOT IN (
+           'untrusted_request_rejected','policy_rejected','binding_rejected',
+           'idempotency_conflict','release_rejected'
+         ) OR p_reason_code !~ '^[a-z][a-z0-9_]{{0,63}}$' OR
+         (p_gateway IS NOT NULL AND NOT EXISTS (
+           SELECT 1 FROM public.edge_gateways WHERE uid=p_gateway
+         )) THEN
+        RAISE EXCEPTION 'edge gateway failure evidence is invalid';
+      END IF;
+      PERFORM pg_advisory_xact_lock(4774096);
+      {refresh_clock}
+      SELECT count(*) INTO total_windows FROM public.edge_gateway_failure_windows;
+      IF NOT EXISTS (
+        SELECT 1 FROM public.edge_gateway_failure_windows AS existing
+         WHERE existing.failure_fingerprint=target_fingerprint
+           AND existing.window_started_at=target_window
+      ) AND total_windows>=4095 THEN
+        target_fingerprint := '16b5bae3613ee26975528e149d401a46f1d659405cfe0148262caec288dfb365';
+        target_gateway := NULL;
+        target_event_type := 'untrusted_request_rejected';
+        target_reason_code := 'capacity_overflow';
+        target_window := '1970-01-01 00:00:00+00'::timestamptz;
+      END IF;
+      RETURN QUERY
+        INSERT INTO public.edge_gateway_failure_windows AS windows
+          (failure_fingerprint,window_started_at,gateway_id,event_type,
+           occurrence_count,first_seen_at,last_seen_at)
+        VALUES (target_fingerprint,target_window,target_gateway,target_event_type,
+                1,observed_at,observed_at)
+        ON CONFLICT ON CONSTRAINT edge_gateway_failure_windows_pkey DO UPDATE
+          SET occurrence_count=windows.occurrence_count+1,
+              last_seen_at=observed_at
+        RETURNING windows.failure_fingerprint,windows.window_started_at,
+                  windows.occurrence_count,windows.gateway_id,windows.event_type;
+      IF NOT EXISTS (
+        SELECT 1 FROM public.edge_gateway_audits AS existing_audit
+         WHERE existing_audit.failure_fingerprint=target_fingerprint
+           AND existing_audit.window_started_at=target_window
+           AND existing_audit.success=FALSE
+      ) THEN
+        INSERT INTO public.edge_gateway_audits
+          (uid,gateway_id,event_type,actor_uid,success,safe_detail,
+           failure_fingerprint,window_started_at)
+        VALUES (p_audit_uid,target_gateway,target_event_type,NULL,FALSE,
+                jsonb_build_object('reason_code',target_reason_code),
+                target_fingerprint,target_window);
+      END IF;
+    END $$;
+    """
+
+
+def upgrade() -> None:
+    op.execute(r"""
+    DO $$
+    BEGIN
+      IF EXISTS (SELECT 1 FROM public.edge_gateway_tasks)
+         OR EXISTS (SELECT 1 FROM public.edge_gateway_releases) THEN
+        RAISE EXCEPTION 'cannot add signed edge authority to existing unsigned rows';
+      END IF;
+    END $$;
+
+    ALTER TABLE public.edge_gateway_tasks
+      ADD COLUMN authority JSONB NOT NULL;
+    ALTER TABLE public.edge_gateway_tasks
+      ADD CONSTRAINT ck_edge_task_authority CHECK (
+        jsonb_typeof(authority)='object' AND
+        authority ?& ARRAY[
+          'task','authority_key_id','signature_algorithm','contract_digest','gateway_id',
+          'environment','network_zone','policy_digest','purpose','issued_at','expires_at',
+          'signature'
+        ] AND
+        authority - ARRAY[
+          'task','authority_key_id','signature_algorithm','contract_digest','gateway_id',
+          'environment','network_zone','policy_digest','purpose','issued_at','expires_at',
+          'signature'
+        ]::text[] = '{}'::jsonb AND
+        authority->>'signature_algorithm'='Ed25519' AND
+        authority->'task'=contract AND
+        authority->>'contract_digest'=contract_digest AND
+        authority->>'gateway_id'=gateway_id::text AND
+        authority->>'environment'=contract->>'environment' AND
+        authority->>'network_zone'=contract->>'network_zone' AND
+        authority->>'policy_digest'=contract->>'policy_digest' AND
+        authority->>'purpose'=contract->>'purpose' AND
+        authority->>'authority_key_id' ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
+        authority->>'signature' ~ '^[0-9a-f]{128}$'
+      );
+
+    ALTER TABLE public.edge_gateway_releases
+      ADD COLUMN artifact_name VARCHAR(255) NOT NULL,
+      ADD COLUMN deadline_at TIMESTAMPTZ NOT NULL,
+      ADD COLUMN signature_algorithm VARCHAR(20) NOT NULL,
+      ADD COLUMN key_id VARCHAR(128) NOT NULL,
+      ADD COLUMN manifest_digest CHAR(64) NOT NULL,
+      ADD COLUMN signature CHAR(128) NOT NULL,
+      ADD COLUMN signed_manifest JSONB NOT NULL;
+    ALTER TABLE public.edge_gateway_releases
+      ADD CONSTRAINT ck_edge_release_signed_manifest CHECK (
+        signature_algorithm='Ed25519' AND
+        key_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
+        manifest_digest ~ '^[0-9a-f]{64}$' AND
+        signature ~ '^[0-9a-f]{128}$' AND
+        jsonb_typeof(signed_manifest)='object' AND
+        signed_manifest ?& ARRAY[
+          'release_id','version','rollback_version','artifact_digest','artifact_name',
+          'deadline_at','signature_algorithm','key_id','manifest_digest','signature','status'
+        ] AND
+        signed_manifest - ARRAY[
+          'release_id','version','rollback_version','artifact_digest','artifact_name',
+          'deadline_at','signature_algorithm','key_id','manifest_digest','signature','status'
+        ]::text[] = '{}'::jsonb AND
+        signed_manifest->>'release_id'=uid::text AND
+        signed_manifest->>'version'=version AND
+        signed_manifest->>'rollback_version'=rollback_version AND
+        signed_manifest->>'artifact_digest'=artifact_digest AND
+        signed_manifest->>'artifact_name'=artifact_name AND
+        (signed_manifest->>'deadline_at')::timestamptz=deadline_at AND
+        signed_manifest->>'signature_algorithm'=signature_algorithm AND
+        signed_manifest->>'key_id'=key_id AND
+        signed_manifest->>'manifest_digest'=manifest_digest AND
+        signed_manifest->>'signature'=signature AND
+        signed_manifest->>'status'=status
+      );
+    """)
+    op.execute(_failure_window_function(lock_clock=True))
+
+
+def downgrade() -> None:
+    op.execute(r"""
+    DO $$
+    BEGIN
+      IF EXISTS (SELECT 1 FROM public.edge_gateway_tasks)
+         OR EXISTS (SELECT 1 FROM public.edge_gateway_releases) THEN
+        RAISE EXCEPTION 'signed edge authority rows block metadata-dropping downgrade';
+      END IF;
+    END $$;
+    ALTER TABLE public.edge_gateway_releases
+      DROP CONSTRAINT ck_edge_release_signed_manifest,
+      DROP COLUMN signed_manifest,
+      DROP COLUMN signature,
+      DROP COLUMN manifest_digest,
+      DROP COLUMN key_id,
+      DROP COLUMN signature_algorithm,
+      DROP COLUMN deadline_at,
+      DROP COLUMN artifact_name;
+    ALTER TABLE public.edge_gateway_tasks
+      DROP CONSTRAINT ck_edge_task_authority,
+      DROP COLUMN authority;
+    """)
+    op.execute(_failure_window_function(lock_clock=False))

+ 34 - 2
deployment/package_release.sh

@@ -4,12 +4,13 @@
 # 用法: cd deployment && bash package_release.sh
 #
 set -euo pipefail
+export COPYFILE_DISABLE=1
 
 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
 ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
-VERSION="$(date +%Y%m%d)"
+VERSION="${DATAOPS_RELEASE_VERSION:-$(date +%Y%m%d)}"
 ARCHIVE_NAME="dataops-platform-release-${VERSION}.tar.gz"
-OUTPUT_DIR="${ROOT_DIR}/dist"
+OUTPUT_DIR="${DATAOPS_RELEASE_OUTPUT_DIR:-${ROOT_DIR}/dist}"
 
 required=(
   "${SCRIPT_DIR}/app"
@@ -21,6 +22,14 @@ required=(
   "${SCRIPT_DIR}/gunicorn_config.py"
   "${SCRIPT_DIR}/deploy_dataops.sh"
   "${SCRIPT_DIR}/scripts/run_dataops.sh"
+  "${ROOT_DIR}/scripts/edge_gateway_admin.py"
+  "${ROOT_DIR}/deploy/edge/docker-compose.edge.yml"
+  "${SCRIPT_DIR}/scripts/scan_release_secrets.py"
+  "${ROOT_DIR}/docs/runbooks/EDGE_GATEWAY_OPERATIONS.md"
+  "${ROOT_DIR}/docs/phase3/P3_WP04_EDGE_GATEWAY.md"
+  "${ROOT_DIR}/docs/validation/P3_WP04_EDGE_GATEWAY_EVIDENCE.md"
+  "${ROOT_DIR}/docs/validation/P3_WP04_FAILURE_INJECTION.json"
+  "${ROOT_DIR}/docs/architecture/OPENAPI.yaml"
   "${SCRIPT_DIR}/dataops.env"
 )
 
@@ -53,6 +62,13 @@ copy_into_release "${SCRIPT_DIR}/database" "database"
 copy_into_release "${SCRIPT_DIR}/migrations" "migrations"
 copy_into_release "${SCRIPT_DIR}/config" "config"
 copy_into_release "${SCRIPT_DIR}/scripts" "scripts"
+copy_into_release "${ROOT_DIR}/scripts/edge_gateway_admin.py" "scripts/edge_gateway_admin.py"
+copy_into_release "${ROOT_DIR}/deploy/edge" "deploy/edge"
+copy_into_release "${ROOT_DIR}/docs/runbooks/EDGE_GATEWAY_OPERATIONS.md" "docs/runbooks/EDGE_GATEWAY_OPERATIONS.md"
+copy_into_release "${ROOT_DIR}/docs/phase3/P3_WP04_EDGE_GATEWAY.md" "docs/phase3/P3_WP04_EDGE_GATEWAY.md"
+copy_into_release "${ROOT_DIR}/docs/validation/P3_WP04_EDGE_GATEWAY_EVIDENCE.md" "docs/validation/P3_WP04_EDGE_GATEWAY_EVIDENCE.md"
+copy_into_release "${ROOT_DIR}/docs/validation/P3_WP04_FAILURE_INJECTION.json" "docs/validation/P3_WP04_FAILURE_INJECTION.json"
+copy_into_release "${ROOT_DIR}/docs/architecture/OPENAPI.yaml" "docs/architecture/OPENAPI.yaml"
 copy_into_release "${SCRIPT_DIR}/requirements.txt" "requirements.txt"
 copy_into_release "${SCRIPT_DIR}/alembic.ini" "alembic.ini"
 copy_into_release "${SCRIPT_DIR}/wsgi.py" "wsgi.py"
@@ -66,6 +82,22 @@ copy_into_release "${SCRIPT_DIR}/DEPLOYMENT_GUIDE.md" "DEPLOYMENT_GUIDE.md"
 copy_into_release "${SCRIPT_DIR}/CHECKLIST.md" "CHECKLIST.md"
 copy_into_release "${SCRIPT_DIR}/MANIFEST.md" "MANIFEST.md"
 
+# Runtime bytecode and AppleDouble metadata are never release inputs.
+find "${RELEASE_ROOT}" -type f \( -name '*.pyc' -o -name '*.pyo' -o -name '._*' \) -delete
+find "${RELEASE_ROOT}" -type d -name '__pycache__' -empty -delete
+
+# Scan every remaining regular file as UTF-8 text.  This rejects encrypted or
+# unencrypted private-key blocks, binary/PKCS#12 stores, API-secret shapes and
+# literal credentials while permitting explicit replacement templates.
+"${PYTHON_BIN:-python3}" "${SCRIPT_DIR}/scripts/scan_release_secrets.py" "${RELEASE_ROOT}"
+
+(
+  cd "${RELEASE_ROOT}"
+  find . -type f ! -name SHA256SUMS -print | LC_ALL=C sort | while IFS= read -r item; do
+    shasum -a 256 "${item}"
+  done
+) > "${RELEASE_ROOT}/SHA256SUMS"
+
 tar -czf "${OUTPUT_DIR}/${ARCHIVE_NAME}" -C "${TMP_DIR}" dataops-platform
 
 echo "发布包已生成: ${OUTPUT_DIR}/${ARCHIVE_NAME}"

+ 5 - 0
deployment/scripts/deploy_dataops.sh

@@ -60,6 +60,11 @@ setup_env_file() {
   normalize_env_file
   load_env_file
   ensure_env_file_permissions
+  local validator="${SCRIPT_DIR}/../app/config/database_urls.py"
+  if [[ ! -f "${validator}" ]] || ! "${PYTHON_BIN}" "${validator}" --env-file "${ENV_FILE}"; then
+    echo_error "数据库环境变量仍为空、格式错误或包含模板占位值"
+    exit 2
+  fi
 }
 
 setup_venv() {

+ 1314 - 0
deployment/scripts/edge_gateway_admin.py

@@ -0,0 +1,1314 @@
+#!/usr/bin/env python3
+"""Fail-closed local administration for a DataOps enterprise edge gateway."""
+
+from __future__ import annotations
+
+import argparse
+import fcntl
+import hashlib
+import json
+import os
+import re
+import sqlite3
+import stat
+import sys
+import tempfile
+from contextlib import contextmanager, suppress
+from datetime import UTC, datetime
+from decimal import Decimal
+from pathlib import Path, PurePosixPath
+
+from cryptography import x509
+from cryptography.exceptions import InvalidSignature
+from cryptography.hazmat.primitives import hashes, serialization
+from cryptography.hazmat.primitives.asymmetric import (
+    ec,
+    ed448,
+    ed25519,
+    padding,
+    rsa,
+)
+from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID
+
+MAX_JSON_BYTES = 262_144
+MAX_BACKUPS = 3
+SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
+IDENTIFIER_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,254}$")
+VERSION_RE = re.compile(r"^(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})$")
+ORIGIN_RE = re.compile(r"^https://[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?(?::[0-9]{1,5})?$")
+MANIFEST_FIELDS = frozenset(
+    {
+        "release_id", "version", "rollback_version", "artifact_digest",
+        "artifact_name", "deadline_at", "signature_algorithm", "key_id",
+        "manifest_digest", "signature", "status",
+    }
+)
+
+
+class AdminError(RuntimeError):
+    def __init__(self, code: str, exit_code: int = 2):
+        super().__init__(code)
+        self.code = code
+        self.exit_code = exit_code
+
+
+def _json_bytes(value: object) -> bytes:
+    return json.dumps(
+        value, ensure_ascii=False, sort_keys=True, separators=(",", ":")
+    ).encode("utf-8")
+
+
+def _digest(value: object) -> str:
+    return hashlib.sha256(_json_bytes(value)).hexdigest()
+
+
+def _protocol_bytes(value: object) -> bytes:
+    def frame(tag: bytes, content: bytes) -> bytes:
+        return tag + str(len(content)).encode("ascii") + b":" + content
+
+    if value is None:
+        return b"n"
+    if isinstance(value, bool):
+        return b"b1" if value else b"b0"
+    if isinstance(value, (int, float)):
+        number = Decimal(value if isinstance(value, int) else str(value))
+        if not number.is_finite():
+            raise AdminError("INVALID_INPUT")
+        text = "0" if number == 0 else format(number.normalize(), "f")
+        if "." in text:
+            text = text.rstrip("0").rstrip(".")
+        return frame(b"d", text.encode("ascii"))
+    if isinstance(value, str):
+        return frame(b"s", value.encode("utf-8"))
+    if isinstance(value, dict):
+        if any(not isinstance(key, str) for key in value):
+            raise AdminError("INVALID_INPUT")
+        encoded = bytearray(b"m" + str(len(value)).encode("ascii") + b":")
+        for key in sorted(value, key=lambda item: item.encode("utf-8")):
+            encoded.extend(frame(b"k", key.encode("utf-8")))
+            encoded.extend(_protocol_bytes(value[key]))
+        return bytes(encoded)
+    if isinstance(value, (list, tuple)):
+        encoded = bytearray(b"l" + str(len(value)).encode("ascii") + b":")
+        for item in value:
+            encoded.extend(_protocol_bytes(item))
+        return bytes(encoded)
+    raise AdminError("INVALID_INPUT")
+
+
+def _protocol_digest(value: object) -> str:
+    return hashlib.sha256(_protocol_bytes(value)).hexdigest()
+
+
+def _output(value: dict[str, object]) -> None:
+    rendered = json.dumps(value, ensure_ascii=False, sort_keys=True)
+    if len(rendered.encode("utf-8")) > 32_768:
+        raise AdminError("OUTPUT_LIMIT")
+    print(rendered)
+
+
+def _root(raw: str, *, create: bool = False) -> Path:
+    candidate = Path(raw)
+    if not candidate.is_absolute() or "\x00" in raw:
+        raise AdminError("INVALID_INPUT")
+    protected = {
+        Path("/"), Path.home(), Path("/opt"), Path("/var"), Path("/usr"),
+        Path("/etc"), Path("/tmp"), Path("/private"), Path("/private/tmp"),
+        Path("/Users"), Path("/home"), Path("/root"), Path("/Volumes"),
+    }
+    normalized = Path(os.path.abspath(candidate))
+    if normalized in protected:
+        raise AdminError("PATH_POLICY")
+    if candidate.exists() and candidate.is_symlink():
+        raise AdminError("PATH_POLICY")
+    if create:
+        with suppress(FileExistsError):
+            candidate.mkdir(mode=0o700, parents=False, exist_ok=False)
+    if not candidate.is_dir():
+        raise AdminError("PATH_POLICY")
+    resolved = candidate.resolve(strict=True)
+    if resolved != candidate:
+        raise AdminError("PATH_POLICY")
+    return resolved
+
+
+def _runtime_identity(root: Path) -> tuple[int, int]:
+    record = _read_json(root / "state" / "runtime-identity.json")
+    if set(record) != {"runtime_gid", "runtime_uid", "schema_version"}:
+        raise AdminError("OWNER_POLICY")
+    uid, gid = record["runtime_uid"], record["runtime_gid"]
+    if (
+        isinstance(uid, bool)
+        or not isinstance(uid, int)
+        or isinstance(gid, bool)
+        or not isinstance(gid, int)
+        or uid < 1
+        or gid < 1
+    ):
+        raise AdminError("OWNER_POLICY")
+    metadata = root.stat()
+    if metadata.st_uid != uid or metadata.st_gid != gid:
+        raise AdminError("OWNER_POLICY")
+    return uid, gid
+
+
+def _validated_root(raw: str) -> Path:
+    root = _root(raw)
+    _runtime_identity(root)
+    return root
+
+
+def _chown_exact(path: Path, uid: int, gid: int) -> None:
+    metadata = path.lstat()
+    if stat.S_ISLNK(metadata.st_mode):
+        raise AdminError("PATH_POLICY")
+    if (metadata.st_uid, metadata.st_gid) == (uid, gid):
+        return
+    if os.geteuid() != 0:
+        raise AdminError("OWNER_POLICY")
+    os.chown(path, uid, gid, follow_symlinks=False)
+
+
+def _inside(root: Path, raw: str | Path, *, must_exist: bool = False) -> Path:
+    candidate = Path(raw)
+    if not candidate.is_absolute() or "\x00" in str(candidate):
+        raise AdminError("INVALID_INPUT")
+    if candidate.is_symlink():
+        raise AdminError("PATH_POLICY")
+    try:
+        parent = candidate.parent.resolve(strict=True)
+        parent.relative_to(root)
+    except (OSError, ValueError) as exc:
+        raise AdminError("PATH_POLICY") from exc
+    current = root
+    for part in parent.relative_to(root).parts:
+        current /= part
+        if current.is_symlink():
+            raise AdminError("PATH_POLICY")
+    if must_exist:
+        try:
+            resolved = candidate.resolve(strict=True)
+            resolved.relative_to(root)
+        except (OSError, ValueError) as exc:
+            raise AdminError("PATH_POLICY") from exc
+        metadata = candidate.stat()
+        if (
+            candidate.is_symlink()
+            or not stat.S_ISREG(metadata.st_mode)
+            or metadata.st_nlink != 1
+        ):
+            raise AdminError("PATH_POLICY")
+    return candidate
+
+
+def _mkdir(root: Path, name: str, mode: int = 0o700) -> Path:
+    path = root / name
+    if path.exists() and (path.is_symlink() or not path.is_dir()):
+        raise AdminError("PATH_POLICY")
+    created = not path.exists()
+    path.mkdir(mode=mode, exist_ok=True)
+    os.chmod(path, mode)
+    parent = path.parent.stat()
+    metadata = path.stat()
+    if created and os.geteuid() == 0:
+        os.chown(path, parent.st_uid, parent.st_gid, follow_symlinks=False)
+    elif (metadata.st_uid, metadata.st_gid) != (parent.st_uid, parent.st_gid):
+        raise AdminError("OWNER_POLICY")
+    return path
+
+
+def _inherit_parent_owner(descriptor: int, parent: Path) -> None:
+    parent_metadata = parent.stat()
+    metadata = os.fstat(descriptor)
+    expected = (parent_metadata.st_uid, parent_metadata.st_gid)
+    if (metadata.st_uid, metadata.st_gid) == expected:
+        return
+    if os.geteuid() != 0:
+        raise AdminError("OWNER_POLICY")
+    os.fchown(descriptor, *expected)
+
+
+def _atomic_write(path: Path, data: bytes, mode: int) -> None:
+    if path.exists() and path.is_symlink():
+        raise AdminError("PATH_POLICY")
+    descriptor, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
+    try:
+        os.fchmod(descriptor, mode)
+        _inherit_parent_owner(descriptor, path.parent)
+        with os.fdopen(descriptor, "wb", closefd=True) as stream:
+            stream.write(data)
+            stream.flush()
+            os.fsync(stream.fileno())
+        os.replace(temporary, path)
+        os.chmod(path, mode)
+        _fsync_directory(path.parent)
+    except Exception:
+        with suppress(FileNotFoundError):
+            os.unlink(temporary)
+        raise
+
+
+def _fsync_directory(path: Path) -> None:
+    directory = os.open(path, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
+    try:
+        os.fsync(directory)
+    finally:
+        os.close(directory)
+
+
+def _atomic_copy(source: Path, target: Path, mode: int) -> None:
+    if target.exists() or target.is_symlink():
+        raise AdminError("PATH_POLICY")
+    descriptor, temporary = tempfile.mkstemp(prefix=f".{target.name}.", dir=target.parent)
+    try:
+        os.fchmod(descriptor, mode)
+        _inherit_parent_owner(descriptor, target.parent)
+        with source.open("rb") as reader, os.fdopen(descriptor, "wb") as writer:
+            for chunk in iter(lambda: reader.read(1_048_576), b""):
+                writer.write(chunk)
+            writer.flush()
+            os.fsync(writer.fileno())
+        os.replace(temporary, target)
+        os.chmod(target, mode)
+        _fsync_directory(target.parent)
+    except Exception:
+        with suppress(FileNotFoundError):
+            os.unlink(temporary)
+        raise
+
+
+def _read_json(path: Path) -> dict[str, object]:
+    if not path.is_file() or path.is_symlink() or path.stat().st_size > MAX_JSON_BYTES:
+        raise AdminError("INVALID_INPUT")
+    try:
+        value = json.loads(path.read_text(encoding="utf-8"))
+    except (OSError, UnicodeError, json.JSONDecodeError) as exc:
+        raise AdminError("INVALID_INPUT") from exc
+    if not isinstance(value, dict):
+        raise AdminError("INVALID_INPUT")
+    return value
+
+
+def _sha256_file(path: Path) -> str:
+    digest = hashlib.sha256()
+    try:
+        with path.open("rb") as stream:
+            for chunk in iter(lambda: stream.read(1_048_576), b""):
+                digest.update(chunk)
+    except OSError as exc:
+        raise AdminError("INVALID_INPUT") from exc
+    return digest.hexdigest()
+
+
+def _open_root_file(root: Path, raw: str | Path) -> tuple[int, os.stat_result]:
+    path = _inside(root, raw)
+    flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0)
+    if hasattr(os, "O_NOFOLLOW"):
+        flags |= os.O_NOFOLLOW
+    try:
+        descriptor = os.open(path, flags)
+    except OSError as exc:
+        raise AdminError("PATH_POLICY") from exc
+    try:
+        metadata = os.fstat(descriptor)
+        uid, gid = _runtime_identity(root)
+        if (
+            not stat.S_ISREG(metadata.st_mode)
+            or metadata.st_nlink != 1
+            or (metadata.st_uid, metadata.st_gid) != (uid, gid)
+        ):
+            raise AdminError("PATH_POLICY")
+        return descriptor, metadata
+    except Exception:
+        os.close(descriptor)
+        raise
+
+
+def _read_json_descriptor(descriptor: int) -> dict[str, object]:
+    metadata = os.fstat(descriptor)
+    if metadata.st_size > MAX_JSON_BYTES:
+        raise AdminError("INVALID_INPUT")
+    os.lseek(descriptor, 0, os.SEEK_SET)
+    try:
+        raw = b""
+        while len(raw) <= MAX_JSON_BYTES:
+            chunk = os.read(descriptor, min(65_536, MAX_JSON_BYTES + 1 - len(raw)))
+            if not chunk:
+                break
+            raw += chunk
+        value = json.loads(raw.decode("utf-8"))
+    except (OSError, UnicodeError, json.JSONDecodeError) as exc:
+        raise AdminError("INVALID_INPUT") from exc
+    if not isinstance(value, dict):
+        raise AdminError("INVALID_INPUT")
+    return value
+
+
+def _copy_descriptor(
+    source_descriptor: int, source_before: os.stat_result, target: Path
+) -> str:
+    flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_CLOEXEC", 0)
+    if hasattr(os, "O_NOFOLLOW"):
+        flags |= os.O_NOFOLLOW
+    target_descriptor = os.open(target, flags, 0o600)
+    digest = hashlib.sha256()
+    try:
+        os.fchmod(target_descriptor, 0o600)
+        _inherit_parent_owner(target_descriptor, target.parent)
+        os.lseek(source_descriptor, 0, os.SEEK_SET)
+        while True:
+            chunk = os.read(source_descriptor, 1_048_576)
+            if not chunk:
+                break
+            digest.update(chunk)
+            view = memoryview(chunk)
+            while view:
+                written = os.write(target_descriptor, view)
+                view = view[written:]
+        os.fsync(target_descriptor)
+    finally:
+        os.close(target_descriptor)
+    source_after = os.fstat(source_descriptor)
+    snapshot_fields = (
+        "st_dev", "st_ino", "st_mode", "st_nlink", "st_uid", "st_gid",
+        "st_size", "st_mtime_ns", "st_ctime_ns",
+    )
+    if any(
+        getattr(source_before, field) != getattr(source_after, field)
+        for field in snapshot_fields
+    ):
+        raise AdminError("RELEASE_REJECTED", 3)
+    copied_digest = digest.hexdigest()
+    if _sha256_file(target) != copied_digest:
+        raise AdminError("RELEASE_REJECTED", 3)
+    return copied_digest
+
+
+def _layout(root: Path) -> None:
+    for name in (
+        "queue", "artifacts", "releases", "secrets", "certificates",
+        "registration", "state",
+    ):
+        _mkdir(root, name)
+
+
+@contextmanager
+def _process_lock(root: Path, uid: int | None = None, gid: int | None = None):
+    if uid is None or gid is None:
+        uid, gid = _runtime_identity(root)
+    lock_path = root / "state" / "admin.lock"
+    flags = os.O_RDWR | os.O_CREAT | getattr(os, "O_CLOEXEC", 0)
+    if hasattr(os, "O_NOFOLLOW"):
+        flags |= os.O_NOFOLLOW
+    try:
+        descriptor = os.open(lock_path, flags, 0o600)
+    except OSError as exc:
+        raise AdminError("LOCK_POLICY", 3) from exc
+    try:
+        if os.geteuid() == 0:
+            os.fchown(descriptor, uid, gid)
+        os.fchmod(descriptor, 0o600)
+        metadata = os.fstat(descriptor)
+        if (
+            not stat.S_ISREG(metadata.st_mode)
+            or stat.S_IMODE(metadata.st_mode) != 0o600
+            or metadata.st_nlink != 1
+            or (metadata.st_uid, metadata.st_gid) != (uid, gid)
+        ):
+            raise AdminError("LOCK_POLICY", 3)
+        try:
+            fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
+        except BlockingIOError as exc:
+            raise AdminError("CONCURRENT_OPERATION", 3) from exc
+        yield
+    finally:
+        os.close(descriptor)
+
+
+def _bounded_backups(directory: Path, prefix: str) -> None:
+    candidates = sorted(
+        (path for path in directory.glob(f"{prefix}*") if path.is_file() and not path.is_symlink()),
+        key=lambda path: path.stat().st_mtime_ns,
+        reverse=True,
+    )
+    for path in candidates[MAX_BACKUPS:]:
+        path.unlink()
+
+
+def command_init(args: argparse.Namespace) -> dict[str, object]:
+    if (
+        isinstance(args.runtime_uid, bool)
+        or not 1 <= args.runtime_uid <= 2_147_483_647
+        or isinstance(args.runtime_gid, bool)
+        or not 1 <= args.runtime_gid <= 2_147_483_647
+    ):
+        raise AdminError("INVALID_INPUT")
+    root = _root(args.root, create=True)
+    identity_path = root / "state" / "runtime-identity.json"
+    if identity_path.exists():
+        uid, gid = _runtime_identity(root)
+        if (uid, gid) != (args.runtime_uid, args.runtime_gid):
+            raise AdminError("OWNER_POLICY")
+        target = root / "edge-config.template.json"
+        if not target.is_file() or target.is_symlink():
+            raise AdminError("PATH_POLICY")
+        with _process_lock(root):
+            return {
+                "config_template_path": str(target),
+                "root": str(root),
+                "runtime_gid": gid,
+                "runtime_uid": uid,
+                "status": "initialized",
+            }
+    if any(root.iterdir()):
+        raise AdminError("PATH_POLICY")
+    os.chmod(root, 0o700)
+    current = root.stat()
+    if (current.st_uid, current.st_gid) != (args.runtime_uid, args.runtime_gid):
+        if not args.allow_chown:
+            raise AdminError("OWNER_POLICY")
+        _chown_exact(root, args.runtime_uid, args.runtime_gid)
+    _layout(root)
+    for name in (
+        "queue", "artifacts", "releases", "secrets", "certificates",
+        "registration", "state",
+    ):
+        _chown_exact(root / name, args.runtime_uid, args.runtime_gid)
+    identity = {
+        "runtime_gid": args.runtime_gid,
+        "runtime_uid": args.runtime_uid,
+        "schema_version": 1,
+    }
+    with _process_lock(root, args.runtime_uid, args.runtime_gid):
+        _atomic_write(identity_path, _json_bytes(identity) + b"\n", 0o600)
+        _chown_exact(identity_path, args.runtime_uid, args.runtime_gid)
+        template = {
+            "allowed_control_hosts": [],
+            "allowed_proxy_hosts": [],
+            "control_origin": None,
+            "environment": None,
+            "gateway_id": None,
+            "network_zone": None,
+            "policy_digest": None,
+            "proxy_origin": None,
+            "runtime_gid": args.runtime_gid,
+            "runtime_uid": args.runtime_uid,
+            "schema_version": 1,
+        }
+        target = root / "edge-config.template.json"
+        _atomic_write(target, _json_bytes(template) + b"\n", 0o600)
+        _chown_exact(target, args.runtime_uid, args.runtime_gid)
+    return {
+        "config_template_path": str(target),
+        "root": str(root),
+        "runtime_gid": args.runtime_gid,
+        "runtime_uid": args.runtime_uid,
+        "status": "initialized",
+    }
+
+
+def _new_csr(root: Path, common_name: str, prefix: str) -> dict[str, object]:
+    if not IDENTIFIER_RE.fullmatch(common_name):
+        raise AdminError("INVALID_INPUT")
+    key_path = root / "secrets" / f"{prefix}.key"
+    csr_path = root / "certificates" / f"{prefix}.csr"
+    if key_path.exists() or csr_path.exists():
+        raise AdminError("MATERIAL_EXISTS")
+    private = rsa.generate_private_key(public_exponent=65537, key_size=3072)
+    csr = (
+        x509.CertificateSigningRequestBuilder()
+        .subject_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, common_name)]))
+        .sign(private, hashes.SHA256())
+    )
+    key_bytes = private.private_bytes(
+        serialization.Encoding.PEM,
+        serialization.PrivateFormat.PKCS8,
+        serialization.NoEncryption(),
+    )
+    csr_bytes = csr.public_bytes(serialization.Encoding.PEM)
+    try:
+        _atomic_write(key_path, key_bytes, 0o600)
+        _atomic_write(csr_path, csr_bytes, 0o644)
+    except Exception:
+        if key_path.exists():
+            key_path.unlink()
+        if csr_path.exists():
+            csr_path.unlink()
+        raise
+    return {
+        "csr_path": str(csr_path),
+        "private_key_path": str(key_path),
+        "public_key_sha256": hashlib.sha256(
+            csr.public_key().public_bytes(
+                serialization.Encoding.DER,
+                serialization.PublicFormat.SubjectPublicKeyInfo,
+            )
+        ).hexdigest(),
+        "status": "csr_created",
+    }
+
+
+def command_csr(args: argparse.Namespace) -> dict[str, object]:
+    root = _validated_root(args.root)
+    _layout(root)
+    return _new_csr(root, args.common_name, "client")
+
+
+def _verify_x509_signature(value, issuer_public_key) -> None:
+    try:
+        if isinstance(issuer_public_key, rsa.RSAPublicKey):
+            issuer_public_key.verify(
+                value.signature, value.tbs_certificate_bytes
+                if isinstance(value, x509.Certificate)
+                else value.tbs_certlist_bytes,
+                padding.PKCS1v15(), value.signature_hash_algorithm,
+            )
+        elif isinstance(issuer_public_key, ec.EllipticCurvePublicKey):
+            issuer_public_key.verify(
+                value.signature, value.tbs_certificate_bytes
+                if isinstance(value, x509.Certificate)
+                else value.tbs_certlist_bytes,
+                ec.ECDSA(value.signature_hash_algorithm),
+            )
+        elif isinstance(issuer_public_key, (ed25519.Ed25519PublicKey, ed448.Ed448PublicKey)):
+            issuer_public_key.verify(
+                value.signature, value.tbs_certificate_bytes
+                if isinstance(value, x509.Certificate)
+                else value.tbs_certlist_bytes,
+            )
+        else:
+            raise AdminError("MATERIAL_INVALID")
+    except (InvalidSignature, ValueError, TypeError) as exc:
+        raise AdminError("MATERIAL_INVALID") from exc
+
+
+def _load_material(
+    root: Path, certificate_raw: str, ca_raw: str, crl_raw: str,
+    prefix: str = "client",
+):
+    certificate_path = _inside(root, certificate_raw, must_exist=True)
+    ca_path = _inside(root, ca_raw, must_exist=True)
+    crl_path = _inside(root, crl_raw, must_exist=True)
+    key_path = _inside(root, root / "secrets" / f"{prefix}.key", must_exist=True)
+    csr_path = _inside(root, root / "certificates" / f"{prefix}.csr", must_exist=True)
+    if stat.S_IMODE(key_path.stat().st_mode) & 0o077:
+        raise AdminError("KEY_PERMISSION")
+    try:
+        certificate = x509.load_pem_x509_certificate(certificate_path.read_bytes())
+        authorities = x509.load_pem_x509_certificates(ca_path.read_bytes())
+        crl = x509.load_pem_x509_crl(crl_path.read_bytes())
+        private = serialization.load_pem_private_key(key_path.read_bytes(), password=None)
+        csr = x509.load_pem_x509_csr(csr_path.read_bytes())
+    except (ValueError, TypeError, OSError) as exc:
+        raise AdminError("MATERIAL_INVALID") from exc
+    public = certificate.public_key().public_bytes(
+        serialization.Encoding.DER, serialization.PublicFormat.SubjectPublicKeyInfo
+    )
+    if public != private.public_key().public_bytes(
+        serialization.Encoding.DER, serialization.PublicFormat.SubjectPublicKeyInfo
+    ) or public != csr.public_key().public_bytes(
+        serialization.Encoding.DER, serialization.PublicFormat.SubjectPublicKeyInfo
+    ) or not csr.is_signature_valid:
+        raise AdminError("MATERIAL_MISMATCH")
+    now = datetime.now(UTC)
+    if certificate.not_valid_before_utc > now or certificate.not_valid_after_utc <= now:
+        raise AdminError("MATERIAL_INVALID")
+    try:
+        usages = certificate.extensions.get_extension_for_class(
+            x509.ExtendedKeyUsage
+        ).value
+    except x509.ExtensionNotFound as exc:
+        raise AdminError("MATERIAL_INVALID") from exc
+    if ExtendedKeyUsageOID.CLIENT_AUTH not in usages:
+        raise AdminError("MATERIAL_INVALID")
+    issuer = next(
+        (authority for authority in authorities if authority.subject == certificate.issuer),
+        None,
+    )
+    if issuer is None:
+        raise AdminError("MATERIAL_INVALID")
+    try:
+        basic = issuer.extensions.get_extension_for_class(x509.BasicConstraints).value
+    except x509.ExtensionNotFound as exc:
+        raise AdminError("MATERIAL_INVALID") from exc
+    if not basic.ca or issuer.not_valid_after_utc <= now:
+        raise AdminError("MATERIAL_INVALID")
+    _verify_x509_signature(certificate, issuer.public_key())
+    if crl.issuer != issuer.subject or crl.last_update_utc > now or crl.next_update_utc <= now:
+        raise AdminError("MATERIAL_INVALID")
+    _verify_x509_signature(crl, issuer.public_key())
+    if crl.get_revoked_certificate_by_serial_number(certificate.serial_number) is not None:
+        raise AdminError("MATERIAL_INVALID")
+    return certificate, csr, ca_path, crl, crl_path
+
+
+def command_register_manifest(args: argparse.Namespace) -> dict[str, object]:
+    root = _validated_root(args.root)
+    certificate, csr, ca_path, crl, crl_path = _load_material(
+        root, args.certificate, args.ca_bundle, args.crl
+    )
+    for value in (args.gateway_id, args.environment, args.network_zone):
+        if not IDENTIFIER_RE.fullmatch(value):
+            raise AdminError("INVALID_INPUT")
+    if args.environment not in {"development", "staging", "production"}:
+        raise AdminError("INVALID_INPUT")
+    if not SHA256_RE.fullmatch(args.policy_digest):
+        raise AdminError("INVALID_INPUT")
+    if not ORIGIN_RE.fullmatch(args.control_origin):
+        raise AdminError("INVALID_INPUT")
+    if args.proxy_origin and not ORIGIN_RE.fullmatch(args.proxy_origin):
+        raise AdminError("INVALID_INPUT")
+    output = root / "registration" / "registration.json"
+    if output.is_symlink():
+        raise AdminError("PATH_POLICY")
+    certificate_digest = certificate.fingerprint(hashes.SHA256()).hex()
+    public_digest = hashlib.sha256(
+        certificate.public_key().public_bytes(
+            serialization.Encoding.DER,
+            serialization.PublicFormat.SubjectPublicKeyInfo,
+        )
+    ).hexdigest()
+    material = {
+        "allowed_control_hosts": [args.control_origin.removeprefix("https://").split(":", 1)[0]],
+        "allowed_proxy_hosts": [] if not args.proxy_origin else [args.proxy_origin.removeprefix("https://").split(":", 1)[0]],
+        "certificate_sha256": certificate_digest,
+        "certificate_not_after": certificate.not_valid_after_utc.isoformat().replace("+00:00", "Z"),
+        "certificate_not_before": certificate.not_valid_before_utc.isoformat().replace("+00:00", "Z"),
+        "control_origin": args.control_origin,
+        "csr_sha256": hashlib.sha256(
+            csr.public_bytes(serialization.Encoding.DER)
+        ).hexdigest(),
+        "public_key_sha256": public_digest,
+        "environment": args.environment,
+        "gateway_id": args.gateway_id,
+        "network_zone": args.network_zone,
+        "policy_digest": args.policy_digest,
+        "proxy_origin": args.proxy_origin,
+        "ca_bundle_sha256": _sha256_file(ca_path),
+        "ca_chain_verified": True,
+        "client_auth_eku_verified": True,
+        "crl_sha256": _sha256_file(crl_path),
+        "crl_verified": True,
+        "crl_this_update": crl.last_update_utc.isoformat().replace("+00:00", "Z"),
+        "crl_next_update": crl.next_update_utc.isoformat().replace("+00:00", "Z"),
+        "schema_version": 1,
+    }
+    replayed = False
+    if output.exists():
+        if _read_json(output) != material:
+            raise AdminError("IDEMPOTENCY_CONFLICT", 3)
+        replayed = True
+    else:
+        _atomic_write(output, _json_bytes(material) + b"\n", 0o600)
+    return {
+        "certificate_sha256": certificate_digest,
+        "manifest_digest": _digest(material),
+        "manifest_path": str(output),
+        "replayed": replayed,
+        "status": "registration_material_ready",
+    }
+
+
+def command_rotate(args: argparse.Namespace) -> dict[str, object]:
+    root = _validated_root(args.root)
+    if not IDENTIFIER_RE.fullmatch(args.request_id) or not SHA256_RE.fullmatch(
+        args.expected_current_certificate_digest
+    ):
+        raise AdminError("INVALID_INPUT")
+    request = {
+        "common_name": args.common_name,
+        "expected_current_certificate_digest": args.expected_current_certificate_digest,
+        "request_id": args.request_id,
+    }
+    request_digest = _digest(request)
+    record_path = root / "state" / f"rotation-{args.request_id}.json"
+    if record_path.exists():
+        record = _read_json(record_path)
+        if record.get("request_digest") != request_digest:
+            raise AdminError("IDEMPOTENCY_CONFLICT", 3)
+        return record
+    pending_records = [
+        path
+        for path in (root / "state").glob("rotation-*.json")
+        if path.is_file()
+        and not path.is_symlink()
+        and _read_json(path).get("status") == "pending_server_approval"
+    ]
+    if len(pending_records) >= MAX_BACKUPS:
+        raise AdminError("ROTATION_LIMIT", 3)
+    prefix = f"pending-{request_digest[:16]}"
+    result = _new_csr(root, args.common_name, prefix)
+    record = {
+        **result,
+        "expected_current_certificate_digest": args.expected_current_certificate_digest,
+        "request_digest": request_digest,
+        "request_id": args.request_id,
+        "status": "pending_server_approval",
+    }
+    _atomic_write(record_path, _json_bytes(record) + b"\n", 0o600)
+    return record
+
+
+def _trusted_keys(bindings: list[str]) -> dict[str, bytes]:
+    keys: dict[str, bytes] = {}
+    for binding in bindings:
+        key_id, separator, key_hex = binding.partition("=")
+        if (
+            not separator
+            or not IDENTIFIER_RE.fullmatch(key_id)
+            or not re.fullmatch(r"[0-9a-f]{64}", key_hex)
+        ):
+            raise AdminError("INVALID_INPUT")
+        keys[key_id] = bytes.fromhex(key_hex)
+    return keys
+
+
+STATUS_FIELDS = frozenset(
+    {
+        "certificate_sha256", "expires_at", "gateway_id", "generation",
+        "issued_at", "key_id", "signature_algorithm", "status",
+        "envelope_digest", "signature",
+    }
+)
+
+
+def command_revoke_status(args: argparse.Namespace) -> dict[str, object]:
+    root = _validated_root(args.root)
+    local_marker = root / "state" / "REVOKED_STOP"
+    status = "revoked" if local_marker.exists() else "not_revoked_locally"
+    evidence_digest = None
+    if args.server_status:
+        evidence_path = _inside(root, args.server_status, must_exist=True)
+        evidence = _read_json(evidence_path)
+        if set(evidence) != STATUS_FIELDS:
+            raise AdminError("STATUS_REJECTED", 3)
+        unsigned = {
+            key: evidence[key]
+            for key in sorted(STATUS_FIELDS - {"envelope_digest", "signature"})
+        }
+        try:
+            issued = datetime.fromisoformat(str(evidence["issued_at"]).replace("Z", "+00:00"))
+            expires = datetime.fromisoformat(str(evidence["expires_at"]).replace("Z", "+00:00"))
+            generation = int(evidence["generation"])
+        except (TypeError, ValueError) as exc:
+            raise AdminError("STATUS_REJECTED", 3) from exc
+        now = datetime.now(UTC)
+        if (
+            not str(evidence["issued_at"]).endswith("Z")
+            or not str(evidence["expires_at"]).endswith("Z")
+            or issued.tzinfo is None
+            or expires.tzinfo is None
+            or issued.astimezone(UTC) > now
+            or expires.astimezone(UTC) <= now
+            or evidence["signature_algorithm"] != "Ed25519"
+            or evidence["status"] not in {"active", "revoked"}
+            or evidence["gateway_id"] != args.expected_gateway_id
+            or generation != args.expected_generation
+            or evidence["certificate_sha256"] != args.expected_certificate_digest
+            or _protocol_digest(unsigned) != evidence["envelope_digest"]
+        ):
+            raise AdminError("STATUS_REJECTED", 3)
+        public = _trusted_keys(args.trusted_key).get(str(evidence["key_id"]))
+        if public is None:
+            raise AdminError("STATUS_REJECTED", 3)
+        try:
+            ed25519.Ed25519PublicKey.from_public_bytes(public).verify(
+                bytes.fromhex(str(evidence["signature"])), _protocol_bytes(unsigned)
+            )
+        except (InvalidSignature, ValueError) as exc:
+            raise AdminError("STATUS_REJECTED", 3) from exc
+        if evidence["status"] == "revoked":
+            _atomic_write(local_marker, _json_bytes(evidence) + b"\n", 0o600)
+            status = "revoked"
+        evidence_digest = evidence["envelope_digest"]
+    return {
+        "evidence_digest": evidence_digest,
+        "local_stop_required": status == "revoked",
+        "status": status,
+    }
+
+
+def _version_tuple(value: str) -> tuple[int, int, int, int]:
+    if not VERSION_RE.fullmatch(value):
+        raise AdminError("RELEASE_REJECTED", 3)
+    values = tuple(int(item) for item in value.split("."))
+    if any(item > 2_147_483_647 for item in values):
+        raise AdminError("RELEASE_REJECTED", 3)
+    return (*values, 0)  # type: ignore[return-value]
+
+
+def _verify_release_manifest(
+    manifest: dict[str, object], artifact_name_actual: str,
+    artifact_digest_actual: str, trusted_keys: dict[str, bytes],
+    *, current_version: str | None = None,
+) -> dict[str, object]:
+    if set(manifest) != MANIFEST_FIELDS:
+        raise AdminError("RELEASE_REJECTED", 3)
+    if not IDENTIFIER_RE.fullmatch(str(manifest["release_id"])):
+        raise AdminError("RELEASE_REJECTED", 3)
+    artifact_name = manifest["artifact_name"]
+    if (
+        not isinstance(artifact_name, str)
+        or PurePosixPath(artifact_name).name != artifact_name
+        or artifact_name != artifact_name_actual
+        or artifact_name in {".", ".."}
+    ):
+        raise AdminError("RELEASE_REJECTED", 3)
+    if (
+        manifest["signature_algorithm"] != "Ed25519"
+        or manifest["status"] not in {
+            "offered", "accepted", "installed", "rolled_back"
+        }
+    ):
+        raise AdminError("RELEASE_REJECTED", 3)
+    version_tuple = _version_tuple(str(manifest["version"]))
+    rollback_tuple = _version_tuple(str(manifest["rollback_version"]))
+    if version_tuple <= rollback_tuple:
+        raise AdminError("RELEASE_REJECTED", 3)
+    if current_version is not None and (
+        manifest["rollback_version"] != current_version
+        or version_tuple <= _version_tuple(current_version)
+    ):
+        raise AdminError("RELEASE_REJECTED", 3)
+    try:
+        deadline = datetime.fromisoformat(str(manifest["deadline_at"]).replace("Z", "+00:00"))
+    except ValueError as exc:
+        raise AdminError("RELEASE_REJECTED", 3) from exc
+    if deadline.tzinfo is None or deadline.astimezone(UTC) <= datetime.now(UTC):
+        raise AdminError("RELEASE_REJECTED", 3)
+    if not str(manifest["deadline_at"]).endswith("Z"):
+        raise AdminError("RELEASE_REJECTED", 3)
+    unsigned = {
+        key: manifest[key]
+        for key in sorted(MANIFEST_FIELDS - {"manifest_digest", "signature"})
+    }
+    if not SHA256_RE.fullmatch(str(manifest["manifest_digest"])) or _protocol_digest(unsigned) != manifest["manifest_digest"]:
+        raise AdminError("RELEASE_REJECTED", 3)
+    if not SHA256_RE.fullmatch(str(manifest["artifact_digest"])):
+        raise AdminError("RELEASE_REJECTED", 3)
+    if artifact_digest_actual != manifest["artifact_digest"]:
+        raise AdminError("RELEASE_REJECTED", 3)
+    public = trusted_keys.get(str(manifest["key_id"]))
+    if public is None:
+        raise AdminError("RELEASE_REJECTED", 3)
+    try:
+        ed25519.Ed25519PublicKey.from_public_bytes(public).verify(
+            bytes.fromhex(str(manifest["signature"])), _protocol_bytes(unsigned)
+        )
+    except (ValueError, InvalidSignature) as exc:
+        raise AdminError("RELEASE_REJECTED", 3) from exc
+    return {
+        "artifact_digest": artifact_digest_actual,
+        "content_digest": manifest["manifest_digest"],
+        "manifest": manifest,
+    }
+
+
+def _bundle_record(manifest: dict[str, object]) -> dict[str, object]:
+    unsigned = {
+        "artifact_digest": manifest["artifact_digest"],
+        "artifact_name": manifest["artifact_name"],
+        "manifest_digest": manifest["manifest_digest"],
+        "schema_version": 1,
+    }
+    return {**unsigned, "bundle_digest": _protocol_digest(unsigned)}
+
+
+def _verified_bundle(root: Path, manifest_digest: str) -> Path:
+    if not SHA256_RE.fullmatch(manifest_digest):
+        raise AdminError("RELEASE_REJECTED", 3)
+    verified = root / "releases" / "verified"
+    if verified.is_symlink() or not verified.is_dir():
+        raise AdminError("PATH_POLICY")
+    bundle = verified / manifest_digest
+    if bundle.is_symlink() or not bundle.is_dir():
+        raise AdminError("RELEASE_REJECTED", 3)
+    return bundle
+
+
+def _verify_bundle(
+    root: Path, manifest_digest: str, trusted_keys: dict[str, bytes]
+) -> tuple[dict[str, object], dict[str, object], Path]:
+    bundle = _verified_bundle(root, manifest_digest)
+    manifest_path = _inside(root, bundle / "manifest.json", must_exist=True)
+    manifest = _read_json(manifest_path)
+    if manifest.get("manifest_digest") != manifest_digest:
+        raise AdminError("RELEASE_REJECTED", 3)
+    artifact_name = manifest.get("artifact_name")
+    if not isinstance(artifact_name, str):
+        raise AdminError("RELEASE_REJECTED", 3)
+    artifact_path = _inside(root, bundle / artifact_name, must_exist=True)
+    artifact_descriptor, artifact_metadata = _open_root_file(root, artifact_path)
+    try:
+        digest = hashlib.sha256()
+        for chunk in iter(lambda: os.read(artifact_descriptor, 1_048_576), b""):
+            digest.update(chunk)
+        if os.fstat(artifact_descriptor) != artifact_metadata:
+            raise AdminError("RELEASE_REJECTED", 3)
+    finally:
+        os.close(artifact_descriptor)
+    _verify_release_manifest(manifest, artifact_name, digest.hexdigest(), trusted_keys)
+    bundle_path = _inside(root, bundle / "bundle.json", must_exist=True)
+    record = _read_json(bundle_path)
+    expected = _bundle_record(manifest)
+    if record != expected or set(bundle.iterdir()) != {
+        manifest_path, artifact_path, bundle_path
+    }:
+        raise AdminError("RELEASE_REJECTED", 3)
+    return manifest, record, artifact_path
+
+
+def command_release_check(args: argparse.Namespace) -> dict[str, object]:
+    root = _validated_root(args.root)
+    manifest_path = _inside(root, args.manifest)
+    artifact_path = _inside(root, args.artifact)
+    manifest_descriptor = -1
+    artifact_descriptor = -1
+    temporary: Path | None = None
+    try:
+        manifest_descriptor, manifest_before = _open_root_file(root, manifest_path)
+        artifact_descriptor, artifact_before = _open_root_file(root, artifact_path)
+        manifest = _read_json_descriptor(manifest_descriptor)
+        if os.fstat(manifest_descriptor) != manifest_before:
+            raise AdminError("RELEASE_REJECTED", 3)
+        artifact_name = manifest.get("artifact_name")
+        if (
+            not isinstance(artifact_name, str)
+            or not artifact_name
+            or artifact_name in {".", ".."}
+            or len(artifact_name.encode("utf-8")) > 255
+            or PurePosixPath(artifact_name).name != artifact_name
+            or Path(artifact_name).name != artifact_name
+            or artifact_path.name != artifact_name
+        ):
+            raise AdminError("RELEASE_REJECTED", 3)
+        verified_dir = _mkdir(root / "releases", "verified")
+        temporary = Path(tempfile.mkdtemp(prefix=".bundle-", dir=verified_dir))
+        os.chmod(temporary, 0o700)
+        parent_owner = verified_dir.stat()
+        if os.geteuid() == 0:
+            os.chown(
+                temporary, parent_owner.st_uid, parent_owner.st_gid,
+                follow_symlinks=False,
+            )
+        elif (temporary.stat().st_uid, temporary.stat().st_gid) != (
+            parent_owner.st_uid, parent_owner.st_gid
+        ):
+            raise AdminError("OWNER_POLICY")
+        copied_artifact = temporary / artifact_name
+        copied_digest = _copy_descriptor(
+            artifact_descriptor, artifact_before, copied_artifact
+        )
+        keys = _trusted_keys(args.trusted_key)
+        verified = _verify_release_manifest(
+            manifest, artifact_path.name, copied_digest, keys,
+            current_version=args.current_version,
+        )
+        replay_path = root / "state" / "release-replay.json"
+        replay = _read_json(replay_path) if replay_path.exists() else {}
+        content_digest = str(verified["content_digest"])
+        prior = replay.get(str(manifest["release_id"]))
+        if prior is not None and prior != content_digest:
+            raise AdminError("RELEASE_REJECTED", 3)
+        final_bundle = verified_dir / str(manifest["manifest_digest"])
+        replayed = False
+        if final_bundle.exists() or final_bundle.is_symlink():
+            _verify_bundle(root, str(manifest["manifest_digest"]), keys)
+            copied_artifact.unlink()
+            temporary.rmdir()
+            replayed = True
+        else:
+            _atomic_write(
+                temporary / "manifest.json", _json_bytes(manifest) + b"\n", 0o600
+            )
+            _atomic_write(
+                temporary / "bundle.json",
+                _json_bytes(_bundle_record(manifest)) + b"\n", 0o600,
+            )
+            _fsync_directory(temporary)
+            os.replace(temporary, final_bundle)
+            _fsync_directory(verified_dir)
+        replay[str(manifest["release_id"])] = content_digest
+        _atomic_write(replay_path, _json_bytes(replay) + b"\n", 0o600)
+        return {
+            "artifact_digest": manifest["artifact_digest"],
+            "manifest_digest": manifest["manifest_digest"],
+            "replayed": replayed,
+            "release_id": manifest["release_id"],
+            "status": "verified",
+            "verified_bundle_path": str(final_bundle),
+            "version": manifest["version"],
+        }
+    finally:
+        if manifest_descriptor >= 0:
+            os.close(manifest_descriptor)
+        if artifact_descriptor >= 0:
+            os.close(artifact_descriptor)
+        if temporary is not None and temporary.exists():
+            if temporary.is_symlink() or not temporary.is_dir():
+                raise AdminError("PATH_POLICY")
+            for item in temporary.iterdir():
+                if item.is_symlink() or not item.is_file():
+                    raise AdminError("PATH_POLICY")
+                item.unlink()
+            temporary.rmdir()
+            _fsync_directory(temporary.parent)
+
+
+def command_rollback(args: argparse.Namespace) -> dict[str, object]:
+    root = _validated_root(args.root)
+    if not args.yes and (
+        not sys.stdin.isatty()
+        or input("Type ROLLBACK to continue: ").strip() != "ROLLBACK"
+    ):
+        raise AdminError("CONFIRMATION_REQUIRED", 4)
+    if not SHA256_RE.fullmatch(args.expected_current_digest) or not SHA256_RE.fullmatch(args.target_digest):
+        raise AdminError("INVALID_INPUT")
+    keys = _trusted_keys(args.trusted_key)
+    current_path = _inside(root, root / "releases" / "current.json", must_exist=True)
+    current_pointer = _read_json(current_path)
+    history_dir = _mkdir(root / "state", "rollback-history")
+    request_digest = _protocol_digest(
+        {
+            "expected_current_digest": args.expected_current_digest,
+            "target_digest": args.target_digest,
+        }
+    )
+    history_path = history_dir / f"{request_digest}.json"
+    intent_path = root / "state" / "rollback-intent.json"
+    current, current_bundle, _ = _verify_bundle(
+        root, args.expected_current_digest, keys
+    )
+    target, target_bundle, _ = _verify_bundle(root, args.target_digest, keys)
+    expected_pointer = {
+        "artifact_name": current["artifact_name"],
+        "bundle_digest": current_bundle["bundle_digest"],
+        "manifest_digest": current["manifest_digest"],
+        "schema_version": 1,
+        "version": current["version"],
+    }
+    pointer = {
+        "artifact_name": target["artifact_name"],
+        "bundle_digest": target_bundle["bundle_digest"],
+        "manifest_digest": target["manifest_digest"],
+        "schema_version": 1,
+        "version": target["version"],
+    }
+    if (
+        not isinstance(target.get("version"), str)
+        or not isinstance(current.get("version"), str)
+        or _version_tuple(target["version"]) >= _version_tuple(current["version"])
+        or current.get("rollback_version") != target.get("version")
+    ):
+        raise AdminError("RELEASE_REJECTED", 3)
+    if history_path.exists():
+        history = _read_json(history_path)
+        if current_pointer != pointer:
+            raise AdminError("STATE_CONFLICT", 3)
+        if intent_path.exists():
+            expected_intent = {
+                "current_pointer": expected_pointer,
+                "request_digest": request_digest,
+                "target_pointer": pointer,
+            }
+            if _read_json(intent_path) != expected_intent:
+                raise AdminError("STATE_CONFLICT", 3)
+            intent_path.unlink()
+            _fsync_directory(intent_path.parent)
+        return {**history, "replayed": True}
+    if intent_path.exists():
+        intent = _read_json(intent_path)
+        if (
+            intent.get("request_digest") != request_digest
+            or intent.get("current_pointer") != expected_pointer
+            or intent.get("target_pointer") != pointer
+        ):
+            raise AdminError("STATE_CONFLICT", 3)
+        if current_pointer == pointer:
+            recovered = {
+                "current_manifest_digest": args.target_digest,
+                "previous_manifest_digest": args.expected_current_digest,
+                "replayed": True,
+                "status": "rolled_back",
+                "version": target.get("version"),
+            }
+            _atomic_write(history_path, _json_bytes(recovered) + b"\n", 0o600)
+            intent_path.unlink()
+            _fsync_directory(intent_path.parent)
+            return recovered
+    if current_pointer != expected_pointer:
+        raise AdminError("STATE_CONFLICT", 3)
+    backup_dir = _mkdir(root / "releases", "backups")
+    backup_path = backup_dir / f"current-{args.expected_current_digest}.json"
+    if not backup_path.exists():
+        _atomic_write(backup_path, _json_bytes(current_pointer) + b"\n", 0o600)
+    _bounded_backups(backup_dir, "current-")
+    intent = {
+        "current_pointer": expected_pointer,
+        "request_digest": request_digest,
+        "target_pointer": pointer,
+    }
+    if intent_path.exists() and _read_json(intent_path) != intent:
+        raise AdminError("STATE_CONFLICT", 3)
+    if not intent_path.exists():
+        _atomic_write(intent_path, _json_bytes(intent) + b"\n", 0o600)
+    if _read_json(current_path) != current_pointer:
+        raise AdminError("STATE_CONFLICT", 3)
+    _atomic_write(current_path, _json_bytes(pointer) + b"\n", 0o600)
+    result = {
+        "current_manifest_digest": args.target_digest,
+        "previous_manifest_digest": args.expected_current_digest,
+        "replayed": False,
+        "status": "rolled_back",
+        "version": target.get("version"),
+    }
+    _atomic_write(history_path, _json_bytes(result) + b"\n", 0o600)
+    intent_path.unlink()
+    _fsync_directory(intent_path.parent)
+    return result
+
+
+def command_status(args: argparse.Namespace) -> dict[str, object]:
+    root = _validated_root(args.root)
+    queue_files = [
+        path for path in (root / "queue").glob("*.sqlite3")
+        if path.is_file() and not path.is_symlink()
+    ]
+    if len(queue_files) > 1:
+        raise AdminError("STATE_CONFLICT", 3)
+    queue_status: dict[str, object] = {
+        "artifact_cleanup": {}, "events": {}, "outcomes": {}, "releases": {},
+        "schema_version": None, "tasks": {},
+    }
+    active_leases = 0
+    if queue_files:
+        queue_path = _inside(root, queue_files[0], must_exist=True)
+        connection = sqlite3.connect(f"file:{queue_path}?mode=ro&immutable=1", uri=True)
+        try:
+            queue_status["schema_version"] = int(
+                connection.execute("PRAGMA user_version").fetchone()[0]
+            )
+            table_specs = {
+                "tasks": ("edge_tasks", "status"),
+                "events": ("edge_outbound_events", "status"),
+                "outcomes": ("edge_task_outcomes", "status"),
+                "artifact_cleanup": ("edge_local_artifacts", "cleanup_status"),
+                "releases": ("edge_release_state", "status"),
+            }
+            for label, (table, column) in table_specs.items():
+                rows = connection.execute(
+                    f"SELECT {column},COUNT(*) FROM {table} GROUP BY {column}"  # noqa: S608
+                ).fetchall()
+                queue_status[label] = {str(status): int(count) for status, count in rows}
+            active_leases = sum(
+                int(connection.execute(query).fetchone()[0])
+                for query in (
+                    "SELECT COUNT(*) FROM edge_tasks WHERE status='leased'",
+                    "SELECT COUNT(*) FROM edge_outbound_events WHERE status='sending'",
+                    "SELECT COUNT(*) FROM edge_task_outcomes WHERE status='sending'",
+                    "SELECT COUNT(*) FROM edge_local_artifacts WHERE cleanup_status='deleting'",
+                )
+            )
+        except sqlite3.DatabaseError as exc:
+            raise AdminError("QUEUE_STATUS_FAILED", 3) from exc
+        finally:
+            connection.close()
+    current = None
+    current_path = root / "releases" / "current.json"
+    if current_path.exists():
+        pointer = _read_json(current_path)
+        current = {
+            "manifest_digest": pointer.get("manifest_digest"),
+            "version": pointer.get("version"),
+        }
+    return {
+        "active_leases": active_leases,
+        "current_release": current,
+        "cursor_state": "reconciled_per_cycle_not_persisted",
+        "queue": queue_status,
+        "registration_present": (root / "registration" / "registration.json").is_file(),
+        "rollback_intent_present": (root / "state" / "rollback-intent.json").is_file(),
+        "status": "read_only",
+    }
+
+
+def parser() -> argparse.ArgumentParser:
+    main = argparse.ArgumentParser(description=__doc__)
+    subcommands = main.add_subparsers(dest="command", required=True)
+    init = subcommands.add_parser("init")
+    init.add_argument("--root", required=True)
+    init.add_argument("--runtime-uid", required=True, type=int)
+    init.add_argument("--runtime-gid", required=True, type=int)
+    init.add_argument("--allow-chown", action="store_true")
+    init.set_defaults(handler=command_init)
+    csr = subcommands.add_parser("csr")
+    csr.add_argument("--root", required=True)
+    csr.add_argument("--common-name", required=True)
+    csr.set_defaults(handler=command_csr)
+    registration = subcommands.add_parser("register-manifest")
+    registration.add_argument("--root", required=True)
+    registration.add_argument("--certificate", required=True)
+    registration.add_argument("--ca-bundle", required=True)
+    registration.add_argument("--crl", required=True)
+    registration.add_argument("--gateway-id", required=True)
+    registration.add_argument("--environment", required=True)
+    registration.add_argument("--network-zone", required=True)
+    registration.add_argument("--policy-digest", required=True)
+    registration.add_argument("--control-origin", required=True)
+    registration.add_argument("--proxy-origin")
+    registration.set_defaults(handler=command_register_manifest)
+    rotate = subcommands.add_parser("rotate")
+    rotate.add_argument("--root", required=True)
+    rotate.add_argument("--common-name", required=True)
+    rotate.add_argument("--request-id", required=True)
+    rotate.add_argument("--expected-current-certificate-digest", required=True)
+    rotate.set_defaults(handler=command_rotate)
+    revoke = subcommands.add_parser("revoke-status")
+    revoke.add_argument("--root", required=True)
+    revoke.add_argument("--server-status")
+    revoke.add_argument("--trusted-key", action="append", default=[])
+    revoke.add_argument("--expected-gateway-id")
+    revoke.add_argument("--expected-generation", type=int)
+    revoke.add_argument("--expected-certificate-digest")
+    revoke.set_defaults(handler=command_revoke_status)
+    check = subcommands.add_parser("release-check")
+    check.add_argument("--root", required=True)
+    check.add_argument("--manifest", required=True)
+    check.add_argument("--artifact", required=True)
+    check.add_argument("--trusted-key", action="append", required=True)
+    check.add_argument("--current-version", required=True)
+    check.set_defaults(handler=command_release_check)
+    rollback = subcommands.add_parser("rollback")
+    rollback.add_argument("--root", required=True)
+    rollback.add_argument("--expected-current-digest", required=True)
+    rollback.add_argument("--target-digest", required=True)
+    rollback.add_argument("--trusted-key", action="append", required=True)
+    rollback.add_argument("--yes", action="store_true")
+    rollback.set_defaults(handler=command_rollback)
+    status_command = subcommands.add_parser("status")
+    status_command.add_argument("--root", required=True)
+    status_command.set_defaults(handler=command_status)
+    return main
+
+
+def main() -> int:
+    try:
+        args = parser().parse_args()
+        if args.command in {"init", "status"}:
+            result = args.handler(args)
+        else:
+            root = _validated_root(args.root)
+            with _process_lock(root):
+                result = args.handler(args)
+        _output(result)
+        return 0
+    except AdminError as exc:
+        _output({"error": {"code": exc.code}, "status": "error"})
+        return exc.exit_code
+    except (OSError, ValueError, TypeError, MemoryError, RecursionError):
+        _output({"error": {"code": "OPERATION_FAILED"}, "status": "error"})
+        return 5
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())

+ 13 - 1
deployment/scripts/run_dataops.sh

@@ -59,7 +59,19 @@ if [[ ! -f "${APP_DIR}/alembic.ini" || ! -d "${APP_DIR}/migrations" ]]; then
 fi
 
 echo "[run_dataops] applying database migrations" >&2
-"${VENV_DIR}/bin/alembic" -c "${APP_DIR}/alembic.ini" upgrade head
+role_init_url="${DB_ROLE_INIT_DATABASE_URL:?DB_ROLE_INIT_DATABASE_URL is required for one-shot role bootstrap}"
+migration_url="${MIGRATION_DATABASE_URL:?MIGRATION_DATABASE_URL is required}"
+runtime_url="${DATABASE_URL:?DATABASE_URL runtime connection is required}"
+if [[ "${role_init_url}" == "${runtime_url}" || "${migration_url}" == "${runtime_url}" ]]; then
+  echo "role-init/migration and runtime URLs must use different database identities" >&2
+  exit 1
+fi
+DB_ROLE_INIT_DATABASE_URL="${role_init_url}" \
+  "${VENV_DIR}/bin/python" -m app.core.edge_gateway.runtime_roles
+MIGRATION_DATABASE_URL="${migration_url}" \
+  "${VENV_DIR}/bin/alembic" -c "${APP_DIR}/alembic.ini" upgrade head
+unset DB_ROLE_INIT_DATABASE_URL MIGRATION_DATABASE_URL SQLALCHEMY_DATABASE_URI
+unset role_init_url migration_url
 
 echo "[run_dataops] binding ${LISTEN_HOST}:${LISTEN_PORT} workers=${GUNICORN_WORKERS} log=${LOG_FILE}" >&2
 

+ 73 - 0
deployment/scripts/scan_release_secrets.py

@@ -0,0 +1,73 @@
+#!/usr/bin/env python3
+"""Fail closed when a release tree contains secret or binary payloads."""
+
+from __future__ import annotations
+
+import re
+import sys
+from pathlib import Path
+
+_FORBIDDEN_SUFFIXES = frozenset(
+    {".der", ".jks", ".key", ".keystore", ".p12", ".pfx", ".pkcs12", ".pyc", ".pyo"}
+)
+_CONFIG_SUFFIXES = frozenset({".conf", ".env", ".ini", ".json", ".toml", ".txt", ".yaml", ".yml"})
+_PRIVATE_BLOCK = re.compile(
+    "-----BEGIN " + r"(?:ENCRYPTED |RSA |EC |OPENSSH )?PRIVATE KEY-----",
+    re.IGNORECASE,
+)
+_API_SECRET = re.compile(r"\bsk-[A-Za-z0-9_-]{20,}\b")
+_ASSIGNMENT = re.compile(
+    r"(?im)^\s*(?:[A-Za-z0-9_]*_)?"
+    r"(?:credential|password|passwd|secret|token|api_key|private_key)"
+    r"\s*[:=]\s*['\"]?([^'\"\s#;,]{8,})"
+)
+_PLACEHOLDERS = ("${", "<", "change", "example", "placeholder", "replace-", "tbd")
+
+
+def _reject(message: str) -> int:
+    print(f"release secret scan rejected: {message}", file=sys.stderr)
+    return 1
+
+
+def scan(root: Path) -> int:
+    if not root.is_absolute() or not root.is_dir() or root.is_symlink():
+        return _reject("root must be an existing absolute directory")
+    for path in sorted(root.rglob("*")):
+        relative = path.relative_to(root)
+        if path.is_symlink():
+            return _reject(f"symbolic link {relative}")
+        if path.is_dir():
+            if path.name == "__pycache__":
+                return _reject(f"cache directory {relative}")
+            continue
+        if not path.is_file() or path.suffix.casefold() in _FORBIDDEN_SUFFIXES:
+            return _reject(f"forbidden file type {relative}")
+        try:
+            raw = path.read_bytes()
+            if b"\x00" in raw:
+                return _reject(f"binary file {relative}")
+            text = raw.decode("utf-8")
+        except (OSError, UnicodeDecodeError):
+            return _reject(f"unreadable or non-UTF-8 file {relative}")
+        if _PRIVATE_BLOCK.search(text) or _API_SECRET.search(text):
+            return _reject(f"private key or API secret shape in {relative}")
+        if path.suffix.casefold() in _CONFIG_SUFFIXES or path.name.startswith(".env"):
+            for match in _ASSIGNMENT.finditer(text):
+                value = match.group(1).casefold()
+                if value in {"false", "none", "null", "true"}:
+                    continue
+                if any(marker in value for marker in _PLACEHOLDERS):
+                    continue
+                return _reject(f"literal credential shape in {relative}")
+    return 0
+
+
+def main() -> int:
+    if len(sys.argv) != 2:
+        print("usage: scan_release_secrets.py ABSOLUTE_RELEASE_ROOT", file=sys.stderr)
+        return 2
+    return scan(Path(sys.argv[1]))
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())

+ 1 - 1
deployment/sync_release.sh

@@ -56,7 +56,7 @@ EOF
 fi
 
 mkdir -p "${DEPLOY_DIR}/scripts"
-for script in dataops-common.sh deploy_dataops.sh run_dataops.sh start_dataops.sh stop_dataops.sh restart_dataops.sh; do
+for script in dataops-common.sh deploy_dataops.sh run_dataops.sh start_dataops.sh stop_dataops.sh restart_dataops.sh edge_gateway_admin.py; do
   copy_file "${ROOT_DIR}/scripts/${script}" "${DEPLOY_DIR}/scripts/${script}"
 done
 

+ 13 - 8
docs/DATAOPS_PHASE3_6_MONTH_DEVELOPMENT_PLAN_20260802.md

@@ -198,7 +198,7 @@ P3-WP01 的企业验收和 P3-WP07 的交付工程是贯穿性工作。第 1~2
 | P3-WP01 | 第二业务域企业正式验收 | P0 | P2-WP13 外部门禁 | 第 1~4 周 | 工程完成待企业 UAT | 真实用户、来源、配置、预生产环境 |
 | P3-WP02 | 企业统一身份与 SSO | P0 | IAM-05~12、IAM-17 | 第 5~8 周 | 工程完成待企业 UAT | IdP 测试租户和映射规则仍待提供 |
 | P3-WP03 | 企业连接器与扩展契约 | P0 | CON-06~15、CON-21~22、IAM-13 | 第 5~8 周 | 外部条件待满足 | 两个目标来源和只读账号 |
-| P3-WP04 | 边缘网关与数据不出域 | P0 | CON-17~20、SEC-05~07 | 第 9~12 周 | 外部条件待满足 | 网络区、出站和脱敏策略 |
+| P3-WP04 | 边缘网关与数据不出域 | P0 | CON-17~20、SEC-05~07 | 第 9~12 周 | 工程完成待企业 UAT | 网络区、出站和脱敏策略 |
 | P3-WP05 | 生产可观测、值班与协同 | P0 | OBS-03~06、WFC-11~14、PLT-11~12、DFY-22 | 第 9~12 周 | 外部条件待满足 | 监控、SMTP/协作、ITSM 和排班接口 |
 | P3-WP06 | 细粒度访问与可信交付 | P0 | SEC-04~07、SEC-14~22、MKT-09~18 | 第 13~16 周 | 外部条件待满足 | 分类、KMS/DLP/SIEM 和网关方案 |
 | P3-WP07 | Kubernetes、HA、GitOps 与灾备 | P1 | PLT-06~10、PLT-13~15、PLT-25~26 | 第 2~17 周 | 外部条件待满足 | 目标基础设施、SLA、RTO/RPO |
@@ -361,15 +361,20 @@ AI、多租户、BI/AI、成本或插件输入。P0 企业、安全、恢复和
 
 **目标:** 在企业网络边界内执行数据密集和敏感操作,只向控制面传输批准内容。
 
+**当前状态(2026-08-09):** `ENGINEERING_COMPLETE_ENTERPRISE_EDGE_UAT_BLOCKED`。
+本地工程证据见 `docs/validation/P3_WP04_EDGE_GATEWAY_EVIDENCE.md`;
+`enterprise_source_1`、`enterprise_source_2`、`network`、`security_legal`
+保持 `TBD_EXTERNAL`,下列勾选只表示工程基线已实现,不表示完成企业验收或生产就绪。
+
 **主要工作:**
 
-- [ ] 定义控制面与边缘面的任务、身份、策略、证据、心跳和版本契约。
-- [ ] 支持企业内采集、画像、质量、血缘解析和受控查询执行。
-- [ ] 建设出站白名单、单向拉取/长轮询或批准消息通道、代理和网络区策略。
-- [ ] 对原始数据、近期明细、脱敏元数据、统计和证据定义分级存储与保留边界。
-- [ ] 建设断网续跑、队列幂等、重连、取消、升级、远程诊断摘要和安全日志。
-- [ ] 建设边缘安装、证书注册、轮换、撤销、升级和回滚工具。
-- [ ] 对数据不出域、高敏默认拒绝和恶意任务/重放执行安全测试。
+- [x] 定义控制面与边缘面的任务、身份、策略、证据、心跳和版本工程契约。
+- [x] 建立企业内采集、画像、质量、血缘解析和受控查询的封闭 Runner 工程执行路径;真实企业来源待 UAT。
+- [x] 建设 exact host 出站白名单、单向拉取、代理和网络区工程策略;企业网络实施与抓包待 UAT。
+- [x] 对原始数据、近期明细、脱敏元数据、统计和证据定义分级存储与保留边界。
+- [x] 建设断网续跑、队列幂等、重连、取消、签名升级、远程诊断摘要和安全日志。
+- [x] 建设边缘安装、CSR/证书注册、轮换、撤销状态、签名升级和原子回滚工具。
+- [x] 对数据不出域、高敏默认拒绝和恶意任务/重放执行本地安全测试;企业网络与来源测试待 UAT。
 
 **主要文件区域:** `app/core/orchestration/`、`app/core/data_source/`、Runner、
 `deployment/`、`deploy/`、`scripts/`、`docs/runbooks/`、`tests/integration/`、

+ 982 - 2
docs/architecture/OPENAPI.yaml

@@ -1,9 +1,9 @@
 openapi: 3.1.0
 info:
   title: "DataOps Platform API(当前代码基线)"
-  version: "2026-07-16"
+  version: "2026-08-09"
   description: "由 scripts/generate_openapi.py 从 app/api/*/routes.py 生成。请求体与响应细节仍以现有专项 API 文档和代码为准。"
-x-route-count: 430
+x-route-count: 444
 servers:
   - url: "http://localhost:15500"
     description: "全本地隔离测试后端"
@@ -2708,6 +2708,776 @@ paths:
             application/json:
               schema:
                 $ref: '#/components/schemas/ApiEnvelope'
+  "/api/datasource/edge/enrollments":
+    post:
+      tags: [data_source]
+      operationId: data_source_edge_create_enrollment_post
+      summary: "edge create enrollment"
+      x-source: "app/api/data_source/edge_routes.py"
+      x-max-request-bytes: 262144
+      x-max-json-depth: 32
+      x-max-json-nodes: 5000
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/EdgeEnrollmentRequest'
+      x-required-permission: "edge-gateways:manage"
+      security:
+        - bearerAuth: []
+      responses:
+        "201":
+          description: "请求已由当前实现处理"
+          headers:
+            Cache-Control:
+              required: true
+              schema: {type: string, const: no-store}
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/EdgeEnrollmentEnvelope'
+        default:
+          description: "错误响应"
+          headers:
+            Cache-Control:
+              required: true
+              schema: {type: string, const: no-store}
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/EdgeErrorEnvelope'
+  "/api/datasource/edge/gateways":
+    get:
+      tags: [data_source]
+      operationId: data_source_edge_list_gateways_get
+      summary: "edge list gateways"
+      x-source: "app/api/data_source/edge_routes.py"
+      x-max-request-bytes: 262144
+      x-max-json-depth: 32
+      x-max-json-nodes: 5000
+      parameters:
+        - name: limit
+          in: query
+          required: false
+          schema:
+            type: integer
+            minimum: 1
+            maximum: 100
+        - name: offset
+          in: query
+          required: false
+          schema:
+            type: integer
+            minimum: 0
+            maximum: 10000
+      x-required-permission: "edge-gateways:read"
+      security:
+        - bearerAuth: []
+      responses:
+        "200":
+          description: "请求已由当前实现处理"
+          headers:
+            Cache-Control:
+              required: true
+              schema: {type: string, const: no-store}
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/EdgeGatewayListEnvelope'
+        default:
+          description: "错误响应"
+          headers:
+            Cache-Control:
+              required: true
+              schema: {type: string, const: no-store}
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/EdgeErrorEnvelope'
+  "/api/datasource/edge/gateways/{gateway_id}/events":
+    post:
+      tags: [data_source]
+      operationId: data_source_edge_accept_event_post
+      summary: "edge accept event"
+      x-source: "app/api/data_source/edge_routes.py"
+      x-max-request-bytes: 262144
+      x-max-json-depth: 32
+      x-max-json-nodes: 5000
+      parameters:
+        - name: gateway_id
+          in: path
+          required: true
+          schema:
+            type: string
+        - name: X-Edge-Credential
+          in: header
+          required: true
+          schema:
+            type: string
+            format: password
+            writeOnly: true
+        - name: X-DataOps-Edge-Client-Verify
+          in: header
+          required: true
+          schema:
+            type: string
+        - name: X-DataOps-Edge-Client-Cert
+          in: header
+          required: true
+          schema:
+            type: string
+        - name: X-Edge-Certificate-SHA256
+          in: header
+          required: true
+          schema:
+            type: string
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/EdgeEventRequest'
+      x-required-permission: "edge-credential+mTLS"
+      security:
+        - mutualTLS: []
+          edgeCredential: []
+      responses:
+        "200":
+          description: "请求已由当前实现处理"
+          headers:
+            Cache-Control:
+              required: true
+              schema: {type: string, const: no-store}
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/EdgeEventAckEnvelope'
+        default:
+          description: "错误响应"
+          headers:
+            Cache-Control:
+              required: true
+              schema: {type: string, const: no-store}
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/EdgeErrorEnvelope'
+  "/api/datasource/edge/gateways/{gateway_id}/heartbeat":
+    post:
+      tags: [data_source]
+      operationId: data_source_edge_heartbeat_post
+      summary: "edge heartbeat"
+      x-source: "app/api/data_source/edge_routes.py"
+      x-max-request-bytes: 262144
+      x-max-json-depth: 32
+      x-max-json-nodes: 5000
+      parameters:
+        - name: gateway_id
+          in: path
+          required: true
+          schema:
+            type: string
+        - name: X-Edge-Credential
+          in: header
+          required: true
+          schema:
+            type: string
+            format: password
+            writeOnly: true
+        - name: X-DataOps-Edge-Client-Verify
+          in: header
+          required: true
+          schema:
+            type: string
+        - name: X-DataOps-Edge-Client-Cert
+          in: header
+          required: true
+          schema:
+            type: string
+        - name: X-Edge-Certificate-SHA256
+          in: header
+          required: true
+          schema:
+            type: string
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/EdgeHeartbeatRequest'
+      x-required-permission: "edge-credential+mTLS"
+      security:
+        - mutualTLS: []
+          edgeCredential: []
+      responses:
+        "200":
+          description: "请求已由当前实现处理"
+          headers:
+            Cache-Control:
+              required: true
+              schema: {type: string, const: no-store}
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/EdgeApiEnvelope'
+        default:
+          description: "错误响应"
+          headers:
+            Cache-Control:
+              required: true
+              schema: {type: string, const: no-store}
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/EdgeErrorEnvelope'
+  "/api/datasource/edge/gateways/{gateway_id}/reconcile":
+    post:
+      tags: [data_source]
+      operationId: data_source_edge_reconcile_post
+      summary: "edge reconcile"
+      x-source: "app/api/data_source/edge_routes.py"
+      x-max-request-bytes: 262144
+      x-max-json-depth: 32
+      x-max-json-nodes: 5000
+      parameters:
+        - name: gateway_id
+          in: path
+          required: true
+          schema:
+            type: string
+        - name: X-Edge-Credential
+          in: header
+          required: true
+          schema:
+            type: string
+            format: password
+            writeOnly: true
+        - name: X-DataOps-Edge-Client-Verify
+          in: header
+          required: true
+          schema:
+            type: string
+        - name: X-DataOps-Edge-Client-Cert
+          in: header
+          required: true
+          schema:
+            type: string
+        - name: X-Edge-Certificate-SHA256
+          in: header
+          required: true
+          schema:
+            type: string
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/EdgeReconcileRequest'
+      x-required-permission: "edge-credential+mTLS"
+      security:
+        - mutualTLS: []
+          edgeCredential: []
+      responses:
+        "200":
+          description: "请求已由当前实现处理"
+          headers:
+            Cache-Control:
+              required: true
+              schema: {type: string, const: no-store}
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/EdgeReconcileResponse'
+        default:
+          description: "错误响应"
+          headers:
+            Cache-Control:
+              required: true
+              schema: {type: string, const: no-store}
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/EdgeErrorEnvelope'
+  "/api/datasource/edge/gateways/{gateway_id}/releases":
+    post:
+      tags: [data_source]
+      operationId: data_source_edge_offer_release_post
+      summary: "edge offer release"
+      x-source: "app/api/data_source/edge_routes.py"
+      x-max-request-bytes: 262144
+      x-max-json-depth: 32
+      x-max-json-nodes: 5000
+      parameters:
+        - name: gateway_id
+          in: path
+          required: true
+          schema:
+            type: string
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/EdgeReleaseOfferRequest'
+      x-required-permission: "edge-gateways:operate"
+      security:
+        - bearerAuth: []
+      responses:
+        "201":
+          description: "请求已由当前实现处理"
+          headers:
+            Cache-Control:
+              required: true
+              schema: {type: string, const: no-store}
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/EdgeReleaseEnvelope'
+        default:
+          description: "错误响应"
+          headers:
+            Cache-Control:
+              required: true
+              schema: {type: string, const: no-store}
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/EdgeErrorEnvelope'
+  "/api/datasource/edge/gateways/{gateway_id}/releases/ack":
+    post:
+      tags: [data_source]
+      operationId: data_source_edge_ack_release_post
+      summary: "edge ack release"
+      x-source: "app/api/data_source/edge_routes.py"
+      x-max-request-bytes: 262144
+      x-max-json-depth: 32
+      x-max-json-nodes: 5000
+      parameters:
+        - name: gateway_id
+          in: path
+          required: true
+          schema:
+            type: string
+        - name: X-Edge-Credential
+          in: header
+          required: true
+          schema:
+            type: string
+            format: password
+            writeOnly: true
+        - name: X-DataOps-Edge-Client-Verify
+          in: header
+          required: true
+          schema:
+            type: string
+        - name: X-DataOps-Edge-Client-Cert
+          in: header
+          required: true
+          schema:
+            type: string
+        - name: X-Edge-Certificate-SHA256
+          in: header
+          required: true
+          schema:
+            type: string
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/EdgeReleaseAckRequest'
+      x-required-permission: "edge-credential+mTLS"
+      security:
+        - mutualTLS: []
+          edgeCredential: []
+      responses:
+        "200":
+          description: "请求已由当前实现处理"
+          headers:
+            Cache-Control:
+              required: true
+              schema: {type: string, const: no-store}
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/EdgeApiEnvelope'
+        default:
+          description: "错误响应"
+          headers:
+            Cache-Control:
+              required: true
+              schema: {type: string, const: no-store}
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/EdgeErrorEnvelope'
+  "/api/datasource/edge/gateways/{gateway_id}/revoke":
+    post:
+      tags: [data_source]
+      operationId: data_source_edge_revoke_post
+      summary: "edge revoke"
+      x-source: "app/api/data_source/edge_routes.py"
+      x-max-request-bytes: 262144
+      x-max-json-depth: 32
+      x-max-json-nodes: 5000
+      parameters:
+        - name: gateway_id
+          in: path
+          required: true
+          schema:
+            type: string
+      x-required-permission: "edge-gateways:operate"
+      security:
+        - bearerAuth: []
+      responses:
+        "200":
+          description: "请求已由当前实现处理"
+          headers:
+            Cache-Control:
+              required: true
+              schema: {type: string, const: no-store}
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/EdgeApiEnvelope'
+        default:
+          description: "错误响应"
+          headers:
+            Cache-Control:
+              required: true
+              schema: {type: string, const: no-store}
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/EdgeErrorEnvelope'
+  "/api/datasource/edge/gateways/{gateway_id}/rotate":
+    post:
+      tags: [data_source]
+      operationId: data_source_edge_rotate_post
+      summary: "edge rotate"
+      x-source: "app/api/data_source/edge_routes.py"
+      x-max-request-bytes: 262144
+      x-max-json-depth: 32
+      x-max-json-nodes: 5000
+      parameters:
+        - name: gateway_id
+          in: path
+          required: true
+          schema:
+            type: string
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/EdgeRotateRequest'
+      x-required-permission: "edge-gateways:operate"
+      security:
+        - bearerAuth: []
+      responses:
+        "200":
+          description: "请求已由当前实现处理"
+          headers:
+            Cache-Control:
+              required: true
+              schema: {type: string, const: no-store}
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/EdgeApiEnvelope'
+        default:
+          description: "错误响应"
+          headers:
+            Cache-Control:
+              required: true
+              schema: {type: string, const: no-store}
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/EdgeErrorEnvelope'
+  "/api/datasource/edge/gateways/{gateway_id}/tasks/pull":
+    post:
+      tags: [data_source]
+      operationId: data_source_edge_pull_task_post
+      summary: "edge pull task"
+      x-source: "app/api/data_source/edge_routes.py"
+      x-max-request-bytes: 262144
+      x-max-json-depth: 32
+      x-max-json-nodes: 5000
+      parameters:
+        - name: gateway_id
+          in: path
+          required: true
+          schema:
+            type: string
+        - name: X-Edge-Credential
+          in: header
+          required: true
+          schema:
+            type: string
+            format: password
+            writeOnly: true
+        - name: X-DataOps-Edge-Client-Verify
+          in: header
+          required: true
+          schema:
+            type: string
+        - name: X-DataOps-Edge-Client-Cert
+          in: header
+          required: true
+          schema:
+            type: string
+        - name: X-Edge-Certificate-SHA256
+          in: header
+          required: true
+          schema:
+            type: string
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/EdgeMachineBinding'
+      x-required-permission: "edge-credential+mTLS"
+      security:
+        - mutualTLS: []
+          edgeCredential: []
+      responses:
+        "200":
+          description: "请求已由当前实现处理"
+          headers:
+            Cache-Control:
+              required: true
+              schema: {type: string, const: no-store}
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/EdgeTaskPullEnvelope'
+        default:
+          description: "错误响应"
+          headers:
+            Cache-Control:
+              required: true
+              schema: {type: string, const: no-store}
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/EdgeErrorEnvelope'
+  "/api/datasource/edge/gateways/{gateway_id}/tasks/{task_id}/outcome":
+    post:
+      tags: [data_source]
+      operationId: data_source_edge_task_outcome_post
+      summary: "edge task outcome"
+      x-source: "app/api/data_source/edge_routes.py"
+      x-max-request-bytes: 262144
+      x-max-json-depth: 32
+      x-max-json-nodes: 5000
+      parameters:
+        - name: gateway_id
+          in: path
+          required: true
+          schema:
+            type: string
+        - name: task_id
+          in: path
+          required: true
+          schema:
+            type: string
+        - name: X-Edge-Credential
+          in: header
+          required: true
+          schema:
+            type: string
+            format: password
+            writeOnly: true
+        - name: X-DataOps-Edge-Client-Verify
+          in: header
+          required: true
+          schema:
+            type: string
+        - name: X-DataOps-Edge-Client-Cert
+          in: header
+          required: true
+          schema:
+            type: string
+        - name: X-Edge-Certificate-SHA256
+          in: header
+          required: true
+          schema:
+            type: string
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/EdgeTaskOutcomeRequest'
+      x-required-permission: "edge-credential+mTLS"
+      security:
+        - mutualTLS: []
+          edgeCredential: []
+      responses:
+        "200":
+          description: "请求已由当前实现处理"
+          headers:
+            Cache-Control:
+              required: true
+              schema: {type: string, const: no-store}
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/EdgeApiEnvelope'
+        default:
+          description: "错误响应"
+          headers:
+            Cache-Control:
+              required: true
+              schema: {type: string, const: no-store}
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/EdgeErrorEnvelope'
+  "/api/datasource/edge/register":
+    post:
+      tags: [data_source]
+      operationId: data_source_edge_register_post
+      summary: "edge register"
+      x-source: "app/api/data_source/edge_routes.py"
+      x-max-request-bytes: 262144
+      x-max-json-depth: 32
+      x-max-json-nodes: 5000
+      parameters:
+        - name: X-Edge-Enrollment
+          in: header
+          required: true
+          schema:
+            type: string
+            format: password
+            writeOnly: true
+        - name: X-DataOps-Edge-Client-Verify
+          in: header
+          required: true
+          schema:
+            type: string
+        - name: X-DataOps-Edge-Client-Cert
+          in: header
+          required: true
+          schema:
+            type: string
+        - name: X-Edge-Certificate-SHA256
+          in: header
+          required: true
+          schema:
+            type: string
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/EdgeRegisterRequest'
+      x-required-permission: "one-time-enrollment+mTLS"
+      security:
+        - mutualTLS: []
+          edgeEnrollment: []
+      responses:
+        "201":
+          description: "请求已由当前实现处理"
+          headers:
+            Cache-Control:
+              required: true
+              schema: {type: string, const: no-store}
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/EdgeRegistrationEnvelope'
+        default:
+          description: "错误响应"
+          headers:
+            Cache-Control:
+              required: true
+              schema: {type: string, const: no-store}
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/EdgeErrorEnvelope'
+  "/api/datasource/edge/tasks":
+    post:
+      tags: [data_source]
+      operationId: data_source_edge_issue_task_post
+      summary: "edge issue task"
+      x-source: "app/api/data_source/edge_routes.py"
+      x-max-request-bytes: 262144
+      x-max-json-depth: 32
+      x-max-json-nodes: 5000
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/EdgeTaskContract'
+      x-required-permission: "edge-gateways:operate"
+      security:
+        - bearerAuth: []
+      responses:
+        "201":
+          description: "请求已由当前实现处理"
+          headers:
+            Cache-Control:
+              required: true
+              schema: {type: string, const: no-store}
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/EdgeApiEnvelope'
+        default:
+          description: "错误响应"
+          headers:
+            Cache-Control:
+              required: true
+              schema: {type: string, const: no-store}
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/EdgeErrorEnvelope'
+  "/api/datasource/edge/tasks/{task_id}/cancel":
+    post:
+      tags: [data_source]
+      operationId: data_source_edge_cancel_task_post
+      summary: "edge cancel task"
+      x-source: "app/api/data_source/edge_routes.py"
+      x-max-request-bytes: 262144
+      x-max-json-depth: 32
+      x-max-json-nodes: 5000
+      parameters:
+        - name: task_id
+          in: path
+          required: true
+          schema:
+            type: string
+      x-required-permission: "edge-gateways:operate"
+      security:
+        - bearerAuth: []
+      responses:
+        "200":
+          description: "请求已由当前实现处理"
+          headers:
+            Cache-Control:
+              required: true
+              schema: {type: string, const: no-store}
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/EdgeApiEnvelope'
+        default:
+          description: "错误响应"
+          headers:
+            Cache-Control:
+              required: true
+              schema: {type: string, const: no-store}
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/EdgeErrorEnvelope'
   "/api/datasource/graph":
     post:
       tags: [data_source]
@@ -11251,6 +12021,204 @@ components:
           type: string
         data: {}
         error: {type: object}
+    EdgeApiEnvelope:
+      type: object
+      additionalProperties: false
+      required: [code, message, data]
+      properties: {code: {type: integer}, message: {type: string}, data: {type: object}}
+    EdgeErrorEnvelope:
+      type: object
+      additionalProperties: false
+      required: [code, message, data, error]
+      properties: {code: {type: integer}, message: {type: string}, data: {type: 'null'}, error: {type: object, additionalProperties: false, required: [code], properties: {code: {type: string, maxLength: 80}}}}
+    EdgeMachineBinding:
+      type: object
+      additionalProperties: false
+      required: [environment, network_zone, generation]
+      properties: {environment: {type: string, enum: [development, staging, production]}, network_zone: {type: string, minLength: 1, maxLength: 120}, generation: {type: integer, minimum: 1}}
+    EdgeEnrollmentRequest:
+      type: object
+      additionalProperties: false
+      required: [gateway_name, environment, network_zone, policy_digest, allowed_control_hosts, allowed_proxy_hosts, expected_certificate_sha256]
+      properties: {gateway_name: {type: string, minLength: 1, maxLength: 200}, environment: {type: string, enum: [development, staging, production]}, network_zone: {type: string, minLength: 1, maxLength: 120}, policy_digest: {$ref: '#/components/schemas/Sha256Digest'}, allowed_control_hosts: {$ref: '#/components/schemas/EdgeExactHosts'}, allowed_proxy_hosts: {$ref: '#/components/schemas/EdgeExactHosts'}, expected_certificate_sha256: {$ref: '#/components/schemas/Sha256Digest'}, ttl_seconds: {type: integer, minimum: 60, maximum: 86400}}
+    EdgeRegisterRequest:
+      type: object
+      additionalProperties: false
+      required: [gateway_id, environment, network_zone, policy_digest, allowed_control_hosts, allowed_proxy_hosts, version]
+      properties: {gateway_id: {type: string, format: uuid}, environment: {type: string, enum: [development, staging, production]}, network_zone: {type: string, minLength: 1, maxLength: 120}, policy_digest: {$ref: '#/components/schemas/Sha256Digest'}, allowed_control_hosts: {$ref: '#/components/schemas/EdgeExactHosts'}, allowed_proxy_hosts: {$ref: '#/components/schemas/EdgeExactHosts'}, version: {$ref: '#/components/schemas/EdgeAgentVersion'}}
+    EdgeHeartbeatRequest:
+      type: object
+      additionalProperties: false
+      required: [environment, network_zone, generation, version, safe_summary]
+      properties: {environment: {type: string, enum: [development, staging, production]}, network_zone: {type: string, minLength: 1, maxLength: 120}, generation: {type: integer, minimum: 1}, version: {$ref: '#/components/schemas/EdgeAgentVersion'}, safe_summary: {$ref: '#/components/schemas/EdgeSafeSummary'}}
+    EdgeRotateRequest:
+      type: object
+      additionalProperties: false
+      required: [certificate_sha256, request_id]
+      properties: {certificate_sha256: {$ref: '#/components/schemas/Sha256Digest'}, request_id: {type: string, minLength: 1, maxLength: 255}}
+    Sha256Digest: {type: string, pattern: '^[a-f0-9]{64}$'}
+    Ed25519Signature: {type: string, pattern: '^[a-f0-9]{128}$'}
+    EdgeExactHosts: {type: array, maxItems: 32, uniqueItems: true, items: {type: string, pattern: '^[a-z0-9][a-z0-9.-]*[a-z0-9]$'}}
+    EdgeSafeSummary:
+      type: object
+      x-max-policy-depth: 12
+      maxProperties: 64
+      propertyNames: {type: string, minLength: 1, maxLength: 80, pattern: '^(?!.*(?:raw|rows?|sql|credential|secret|private|password|token|local_path))[a-z][a-z0-9_]{0,79}$'}
+      additionalProperties: {$ref: '#/components/schemas/EdgeSafeValue12'}
+      description: 'Policy-approved bounded metadata/statistics/health/diagnostic fields only; raw rows, SQL, credentials, and local paths are forbidden.'
+    EdgeSafeScalar:
+      anyOf:
+        - {type: string, maxLength: 4096}
+        - {type: integer, minimum: -9007199254740991, maximum: 9007199254740991}
+        - {type: number, minimum: -1000000000000, maximum: 1000000000000}
+        - {type: boolean}
+        - {type: 'null'}
+    EdgeSafeValue0: {$ref: '#/components/schemas/EdgeSafeScalar'}
+    EdgeSafeValue1:
+      anyOf:
+        - {$ref: '#/components/schemas/EdgeSafeScalar'}
+        - {type: array, maxItems: 100, items: {$ref: '#/components/schemas/EdgeSafeValue0'}}
+        - {type: object, maxProperties: 32, propertyNames: {type: string, minLength: 1, maxLength: 80, pattern: '^(?!.*(?:raw|rows?|sql|credential|secret|private|password|token|local_path))[a-z][a-z0-9_]{0,79}$'}, additionalProperties: {$ref: '#/components/schemas/EdgeSafeValue0'}}
+    EdgeSafeValue2:
+      anyOf:
+        - {$ref: '#/components/schemas/EdgeSafeScalar'}
+        - {type: array, maxItems: 100, items: {$ref: '#/components/schemas/EdgeSafeValue1'}}
+        - {type: object, maxProperties: 32, propertyNames: {type: string, minLength: 1, maxLength: 80, pattern: '^(?!.*(?:raw|rows?|sql|credential|secret|private|password|token|local_path))[a-z][a-z0-9_]{0,79}$'}, additionalProperties: {$ref: '#/components/schemas/EdgeSafeValue1'}}
+    EdgeSafeValue3:
+      anyOf:
+        - {$ref: '#/components/schemas/EdgeSafeScalar'}
+        - {type: array, maxItems: 100, items: {$ref: '#/components/schemas/EdgeSafeValue2'}}
+        - {type: object, maxProperties: 32, propertyNames: {type: string, minLength: 1, maxLength: 80, pattern: '^(?!.*(?:raw|rows?|sql|credential|secret|private|password|token|local_path))[a-z][a-z0-9_]{0,79}$'}, additionalProperties: {$ref: '#/components/schemas/EdgeSafeValue2'}}
+    EdgeSafeValue4:
+      anyOf:
+        - {$ref: '#/components/schemas/EdgeSafeScalar'}
+        - {type: array, maxItems: 100, items: {$ref: '#/components/schemas/EdgeSafeValue3'}}
+        - {type: object, maxProperties: 32, propertyNames: {type: string, minLength: 1, maxLength: 80, pattern: '^(?!.*(?:raw|rows?|sql|credential|secret|private|password|token|local_path))[a-z][a-z0-9_]{0,79}$'}, additionalProperties: {$ref: '#/components/schemas/EdgeSafeValue3'}}
+    EdgeSafeValue5:
+      anyOf:
+        - {$ref: '#/components/schemas/EdgeSafeScalar'}
+        - {type: array, maxItems: 100, items: {$ref: '#/components/schemas/EdgeSafeValue4'}}
+        - {type: object, maxProperties: 32, propertyNames: {type: string, minLength: 1, maxLength: 80, pattern: '^(?!.*(?:raw|rows?|sql|credential|secret|private|password|token|local_path))[a-z][a-z0-9_]{0,79}$'}, additionalProperties: {$ref: '#/components/schemas/EdgeSafeValue4'}}
+    EdgeSafeValue6:
+      anyOf:
+        - {$ref: '#/components/schemas/EdgeSafeScalar'}
+        - {type: array, maxItems: 100, items: {$ref: '#/components/schemas/EdgeSafeValue5'}}
+        - {type: object, maxProperties: 32, propertyNames: {type: string, minLength: 1, maxLength: 80, pattern: '^(?!.*(?:raw|rows?|sql|credential|secret|private|password|token|local_path))[a-z][a-z0-9_]{0,79}$'}, additionalProperties: {$ref: '#/components/schemas/EdgeSafeValue5'}}
+    EdgeSafeValue7:
+      anyOf:
+        - {$ref: '#/components/schemas/EdgeSafeScalar'}
+        - {type: array, maxItems: 100, items: {$ref: '#/components/schemas/EdgeSafeValue6'}}
+        - {type: object, maxProperties: 32, propertyNames: {type: string, minLength: 1, maxLength: 80, pattern: '^(?!.*(?:raw|rows?|sql|credential|secret|private|password|token|local_path))[a-z][a-z0-9_]{0,79}$'}, additionalProperties: {$ref: '#/components/schemas/EdgeSafeValue6'}}
+    EdgeSafeValue8:
+      anyOf:
+        - {$ref: '#/components/schemas/EdgeSafeScalar'}
+        - {type: array, maxItems: 100, items: {$ref: '#/components/schemas/EdgeSafeValue7'}}
+        - {type: object, maxProperties: 32, propertyNames: {type: string, minLength: 1, maxLength: 80, pattern: '^(?!.*(?:raw|rows?|sql|credential|secret|private|password|token|local_path))[a-z][a-z0-9_]{0,79}$'}, additionalProperties: {$ref: '#/components/schemas/EdgeSafeValue7'}}
+    EdgeSafeValue9:
+      anyOf:
+        - {$ref: '#/components/schemas/EdgeSafeScalar'}
+        - {type: array, maxItems: 100, items: {$ref: '#/components/schemas/EdgeSafeValue8'}}
+        - {type: object, maxProperties: 32, propertyNames: {type: string, minLength: 1, maxLength: 80, pattern: '^(?!.*(?:raw|rows?|sql|credential|secret|private|password|token|local_path))[a-z][a-z0-9_]{0,79}$'}, additionalProperties: {$ref: '#/components/schemas/EdgeSafeValue8'}}
+    EdgeSafeValue10:
+      anyOf:
+        - {$ref: '#/components/schemas/EdgeSafeScalar'}
+        - {type: array, maxItems: 100, items: {$ref: '#/components/schemas/EdgeSafeValue9'}}
+        - {type: object, maxProperties: 32, propertyNames: {type: string, minLength: 1, maxLength: 80, pattern: '^(?!.*(?:raw|rows?|sql|credential|secret|private|password|token|local_path))[a-z][a-z0-9_]{0,79}$'}, additionalProperties: {$ref: '#/components/schemas/EdgeSafeValue9'}}
+    EdgeSafeValue11:
+      anyOf:
+        - {$ref: '#/components/schemas/EdgeSafeScalar'}
+        - {type: array, maxItems: 100, items: {$ref: '#/components/schemas/EdgeSafeValue10'}}
+        - {type: object, maxProperties: 32, propertyNames: {type: string, minLength: 1, maxLength: 80, pattern: '^(?!.*(?:raw|rows?|sql|credential|secret|private|password|token|local_path))[a-z][a-z0-9_]{0,79}$'}, additionalProperties: {$ref: '#/components/schemas/EdgeSafeValue10'}}
+    EdgeSafeValue12:
+      anyOf:
+        - {$ref: '#/components/schemas/EdgeSafeScalar'}
+        - {type: array, maxItems: 100, items: {$ref: '#/components/schemas/EdgeSafeValue11'}}
+        - {type: object, maxProperties: 32, propertyNames: {type: string, minLength: 1, maxLength: 80, pattern: '^(?!.*(?:raw|rows?|sql|credential|secret|private|password|token|local_path))[a-z][a-z0-9_]{0,79}$'}, additionalProperties: {$ref: '#/components/schemas/EdgeSafeValue11'}}
+    EdgeSafeValue: {$ref: '#/components/schemas/EdgeSafeValue12'}
+    EdgeIdentifier: {type: string, minLength: 1, maxLength: 255, pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{0,254}$'}
+    EdgeCursor: {type: [string, 'null'], minLength: 1, maxLength: 255, pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{0,254}$'}
+    EdgeAgentVersion: {type: string, minLength: 1, maxLength: 80, pattern: '^[A-Za-z0-9][A-Za-z0-9._+-]{0,79}$'}
+    EdgeVersion: {type: string, pattern: '^(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})$'}
+    EdgeTaskContract:
+      type: object
+      additionalProperties: false
+      required: [task_id, gateway_id, environment, network_zone, purpose, classification, task_type, contract_version, deadline_at, attempt, idempotency_key, policy_digest]
+      properties: {task_id: {$ref: '#/components/schemas/EdgeIdentifier'}, gateway_id: {type: string, format: uuid}, environment: {type: string, enum: [development, staging, production]}, network_zone: {type: string, minLength: 1, maxLength: 120}, purpose: {type: string, minLength: 1, maxLength: 255}, classification: {type: string, enum: [raw, recent_detail, restricted, desensitized_metadata, statistics, lineage, evidence]}, task_type: {type: string, enum: [collect, profile, quality, lineage, controlled_query]}, contract_version: {const: 1}, deadline_at: {type: string, format: date-time}, attempt: {type: integer, minimum: 1, maximum: 5}, idempotency_key: {$ref: '#/components/schemas/EdgeIdentifier'}, policy_digest: {$ref: '#/components/schemas/Sha256Digest'}}
+    EdgeSignedTaskEnvelope:
+      type: object
+      additionalProperties: false
+      required: [task, authority_key_id, signature_algorithm, contract_digest, gateway_id, environment, network_zone, policy_digest, purpose, issued_at, expires_at, signature]
+      properties: {task: {$ref: '#/components/schemas/EdgeTaskContract'}, authority_key_id: {type: string}, signature_algorithm: {const: Ed25519}, contract_digest: {$ref: '#/components/schemas/Sha256Digest'}, gateway_id: {type: string}, environment: {type: string}, network_zone: {type: string}, policy_digest: {$ref: '#/components/schemas/Sha256Digest'}, purpose: {type: string}, issued_at: {type: string, format: date-time}, expires_at: {type: string, format: date-time}, signature: {$ref: '#/components/schemas/Ed25519Signature'}}
+    EdgeEventContract:
+      type: object
+      additionalProperties: false
+      required: [event_id, task_id, gateway_id, environment, network_zone, purpose, classification, contract_version, occurred_at, attempt, idempotency_key, policy_digest, payload]
+      properties: {event_id: {type: string}, task_id: {type: string}, gateway_id: {type: string}, environment: {type: string}, network_zone: {type: string}, purpose: {type: string}, classification: {type: string, enum: [desensitized_metadata, statistics, lineage, evidence, health_summary, diagnostic_summary]}, contract_version: {const: 1}, occurred_at: {type: string, format: date-time}, attempt: {type: integer, minimum: 1, maximum: 5}, idempotency_key: {type: string}, policy_digest: {$ref: '#/components/schemas/Sha256Digest'}, payload: {$ref: '#/components/schemas/EdgeSafeSummary'}}
+    EdgeEventRequest:
+      type: object
+      additionalProperties: false
+      required: [environment, network_zone, generation, event, lease_token]
+      properties: {environment: {type: string}, network_zone: {type: string}, generation: {type: integer, minimum: 1}, event: {$ref: '#/components/schemas/EdgeEventContract'}, lease_token: {type: string, writeOnly: true}}
+    EdgeTaskOutcomeRequest:
+      type: object
+      additionalProperties: false
+      required: [environment, network_zone, generation, outcome, lease_token, safe_summary]
+      properties: {environment: {type: string}, network_zone: {type: string}, generation: {type: integer, minimum: 1}, outcome: {type: string, enum: [completed, failed, cancelled]}, lease_token: {type: string, writeOnly: true}, safe_summary: {$ref: '#/components/schemas/EdgeSafeSummary'}}
+    EdgeReconcileRequest:
+      type: object
+      additionalProperties: false
+      required: [environment, network_zone, generation]
+      properties: {environment: {type: string, enum: [development, staging, production]}, network_zone: {type: string, minLength: 1, maxLength: 120}, generation: {type: integer, minimum: 1}, limit: {type: integer, minimum: 1, maximum: 100}, cancel_cursor: {$ref: '#/components/schemas/EdgeCursor'}, release_cursor: {$ref: '#/components/schemas/EdgeCursor'}}
+    EdgeReconcileResponse:
+      type: object
+      additionalProperties: false
+      required: [code, message, data]
+      properties: {code: {type: integer}, message: {type: string, maxLength: 200}, data: {type: object, additionalProperties: false, required: [cancelled_task_ids, cancel_next_cursor, release_offers, release_next_cursor, release_baseline], properties: {cancelled_task_ids: {type: array, maxItems: 50, items: {$ref: '#/components/schemas/EdgeIdentifier'}}, cancel_next_cursor: {$ref: '#/components/schemas/EdgeCursor'}, release_offers: {type: array, maxItems: 50, items: {$ref: '#/components/schemas/EdgeReleaseManifest'}}, release_next_cursor: {$ref: '#/components/schemas/EdgeCursor'}, release_baseline: {oneOf: [{$ref: '#/components/schemas/EdgeReleaseManifest'}, {type: 'null'}]}}}}
+    EdgeReleaseOfferRequest:
+      type: object
+      additionalProperties: false
+      required: [version, artifact_digest, artifact_name, deadline_at, rollback_version, request_id]
+      properties: {version: {$ref: '#/components/schemas/EdgeVersion'}, artifact_digest: {$ref: '#/components/schemas/Sha256Digest'}, artifact_name: {type: string, minLength: 1, maxLength: 255, pattern: '^[^/\\]+$'}, deadline_at: {type: string, format: date-time}, rollback_version: {$ref: '#/components/schemas/EdgeVersion'}, request_id: {$ref: '#/components/schemas/EdgeIdentifier'}}
+    EdgeReleaseAckRequest:
+      type: object
+      additionalProperties: false
+      required: [environment, network_zone, generation, release_id, outcome, safe_summary]
+      properties: {environment: {type: string}, network_zone: {type: string}, generation: {type: integer, minimum: 1}, release_id: {type: string, format: uuid}, outcome: {type: string, enum: [accepted, installed, failed, rollback]}, safe_summary: {$ref: '#/components/schemas/EdgeSafeSummary'}}
+    EdgeReleaseManifest:
+      type: object
+      additionalProperties: false
+      required: [release_id, version, rollback_version, artifact_digest, artifact_name, deadline_at, status, signature_algorithm, key_id, manifest_digest, signature]
+      properties: {release_id: {$ref: '#/components/schemas/EdgeIdentifier'}, version: {$ref: '#/components/schemas/EdgeVersion'}, rollback_version: {$ref: '#/components/schemas/EdgeVersion'}, artifact_digest: {$ref: '#/components/schemas/Sha256Digest'}, artifact_name: {type: string, minLength: 1, maxLength: 255, pattern: '^[^/\\]+$'}, deadline_at: {type: string, format: date-time}, status: {type: string, enum: [offered, accepted, failed, installed, rolled_back]}, signature_algorithm: {const: Ed25519}, key_id: {$ref: '#/components/schemas/EdgeIdentifier'}, manifest_digest: {$ref: '#/components/schemas/Sha256Digest'}, signature: {$ref: '#/components/schemas/Ed25519Signature'}}
+    EdgeEnrollmentEnvelope:
+      type: object
+      additionalProperties: false
+      required: [code, message, data]
+      properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [enrollment_id, gateway_id, enrollment_token, expires_in, returned_once], properties: {enrollment_id: {type: string, format: uuid}, gateway_id: {type: string, format: uuid}, enrollment_token: {type: string, writeOnly: true}, expires_in: {type: integer}, returned_once: {const: true}}}}
+    EdgeRegistrationEnvelope:
+      type: object
+      additionalProperties: false
+      required: [code, message, data]
+      properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [gateway_id, generation, certificate_sha256, credential, credential_returned_once], properties: {gateway_id: {type: string, format: uuid}, generation: {type: integer}, certificate_sha256: {$ref: '#/components/schemas/Sha256Digest'}, credential: {type: string, writeOnly: true}, credential_returned_once: {const: true}}}}
+    EdgeGatewayListEnvelope:
+      type: object
+      additionalProperties: false
+      required: [code, message, data]
+      properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [gateways], properties: {gateways: {type: array, maxItems: 100, items: {type: object, description: 'Bounded gateway metadata; credential/token/private key are never included.'}}}}}
+    EdgeTaskPullEnvelope:
+      type: object
+      additionalProperties: false
+      required: [code, message, data]
+      properties: {code: {type: integer}, message: {type: string}, data: {oneOf: [{type: object, additionalProperties: false, required: [task], properties: {task: {type: 'null'}}}, {type: object, additionalProperties: false, required: [task, signed_task_envelope, lease_token, lease_expires_at], properties: {task: {$ref: '#/components/schemas/EdgeTaskContract'}, signed_task_envelope: {$ref: '#/components/schemas/EdgeSignedTaskEnvelope'}, lease_token: {type: string, writeOnly: true}, lease_expires_at: {type: string, format: date-time}}}]}}
+    EdgeEventAckEnvelope:
+      type: object
+      additionalProperties: false
+      required: [code, message, data]
+      properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [event_id, event_digest, remote_lease_digest, received_at, status], properties: {event_id: {type: string}, event_digest: {$ref: '#/components/schemas/Sha256Digest'}, remote_lease_digest: {$ref: '#/components/schemas/Sha256Digest'}, received_at: {type: string, format: date-time}, status: {const: accepted}}}}
+    EdgeReleaseEnvelope:
+      type: object
+      additionalProperties: false
+      required: [code, message, data]
+      properties: {code: {type: integer}, message: {type: string, maxLength: 200}, data: {type: object, additionalProperties: false, required: [release_id, status, version, artifact_digest, rollback_version, manifest], properties: {release_id: {type: string, format: uuid}, status: {type: string, enum: [offered, accepted, failed, installed, rolled_back]}, version: {$ref: '#/components/schemas/EdgeVersion'}, artifact_digest: {$ref: '#/components/schemas/Sha256Digest'}, rollback_version: {$ref: '#/components/schemas/EdgeVersion'}, manifest: {$ref: '#/components/schemas/EdgeReleaseManifest'}}}}
     ConnectorConfigValidationRequest:
       type: object
       additionalProperties: false
@@ -11422,6 +12390,18 @@ components:
       required: [code, message, data]
       properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [nodes, edges, summary], properties: {nodes: {type: array, items: {type: object, additionalProperties: false, required: [type, key], properties: {type: {type: string, enum: [source, asset, process, business_domain, run]}, key: {type: string}}}}, edges: {type: array, items: {type: object}}, summary: {type: object, additionalProperties: false, required: [node_count, edge_count], properties: {node_count: {type: integer}, edge_count: {type: integer}}}}}}
   securitySchemes:
+    mutualTLS:
+      type: mutualTLS
+    edgeCredential:
+      type: apiKey
+      in: header
+      name: X-Edge-Credential
+      description: "One-time-returned edge credential; always combined with mutualTLS certificate proof."
+    edgeEnrollment:
+      type: apiKey
+      in: header
+      name: X-Edge-Enrollment
+      description: "Single-use enrollment token; always combined with mutualTLS certificate proof."
     bearerAuth:
       type: http
       scheme: bearer

+ 15 - 2
docs/phase3/P3_WP00_REQUIREMENTS.json

@@ -548,14 +548,27 @@
         "边缘网关在批准网络区完成采集与受控查询",
         "未经批准的原始数据出域事件为 0 且有企业侧审计证据"
       ],
-      "engineering_baseline_inputs": [],
+      "engineering_baseline_inputs": [
+        "docs/phase3/P3_WP04_EDGE_GATEWAY.md",
+        "docs/runbooks/EDGE_GATEWAY_OPERATIONS.md",
+        "docs/validation/P3_WP04_EDGE_GATEWAY_EVIDENCE.md",
+        "docs/validation/P3_WP04_FAILURE_INJECTION.json",
+        "tests/test_phase3_wp04_edge_gateway.py",
+        "tests/security/test_phase3_wp04_data_boundary.py",
+        "tests/integration/test_phase3_wp04_edge_gateway_postgres.py",
+        "tests/integration/test_phase3_wp04_edge_agent.py",
+        "tests/integration/test_phase3_wp04_edge_runtime.py",
+        "tests/integration/test_phase3_wp04_nginx_mtls.py",
+        "tests/test_phase3_wp04_production_runtime.py",
+        "tests/test_phase3_wp04_delivery_contract.py"
+      ],
       "enterprise_gate_inputs": [
         "enterprise_source_1",
         "enterprise_source_2",
         "network",
         "security_legal"
       ],
-      "engineering_completion_status": "NOT_STARTED",
+      "engineering_completion_status": "ENGINEERING_COMPLETE_ENTERPRISE_EDGE_UAT_BLOCKED",
       "enterprise_confirmation_status": "TBD_EXTERNAL",
       "dynamic_required_inputs_by_started_wp": {}
     },

+ 72 - 0
docs/phase3/P3_WP04_EDGE_GATEWAY.md

@@ -0,0 +1,72 @@
+# P3-WP04 边缘网关与数据不出域
+
+## 结论
+
+P3-WP04 已形成可测试的企业边缘工程基线,状态为
+`ENGINEERING_COMPLETE_ENTERPRISE_EDGE_UAT_BLOCKED`。这不是生产就绪声明:
+`enterprise_source_1`、`enterprise_source_2`、`network`、`security_legal`
+仍为 `TBD_EXTERNAL`,真实来源、企业网络区、出站规则和安全法务签字尚未完成。
+
+## 架构与信任边界
+
+```text
+平台人员 --RBAC--> 控制面/API/PostgreSQL(head 479)
+                           | Ed25519签名任务/发布
+                           v
+企业边缘 --仅主动HTTPS+mTLS拉取--> 受信mTLS反向代理/控制面
+   | SQLite离线队列、Runner、artifact/release本地存储
+   +-- 企业来源采集/画像/质量/血缘/受控查询
+   +-- 仅批准事件摘要出域;raw/recent_detail/restricted不出域
+```
+
+身份由 gateway ID、environment、network zone、generation、证书 SHA-256、
+credential hash 和 policy digest 共同绑定。注册令牌一次性且只保存 hash;edge
+credential 只返回一次,控制面只保存 hash;mTLS 终止只信任配置的代理地址及其
+`SUCCESS` 证明,并从真实证书计算指纹。CRL、企业 CA 和出站防火墙属于外部网络门禁。
+
+任务与发布由控制面 Ed25519 权威签名。签名材料在 PostgreSQL 中受 append-only、
+generation、active-key 和数据库角色约束;生产禁止 inline private key。边缘验证
+key-id、完整绑定、有效期、digest 和签名后才入队。服务端 signing key、edge 私钥、
+credential 或 lease 均不得进入诊断、事件或 OpenAPI 普通响应正文。
+
+## 功能闭环
+
+1. 严格 Task/Event contract,未知字段拒绝;操作只允许 collect/profile/quality/lineage/controlled_query。
+2. 默认拒绝的出站策略和数据分级:原始数据、近期明细与 restricted 留在边缘;仅脱敏元数据、统计、血缘、证据、健康/诊断摘要进入控制面。
+3. SQLite durable queue 支持幂等、lease fencing、断网、重连、取消、重试、稳定 event ID 和 exact ACK replay。
+4. PostgreSQL 控制面覆盖 enrollment、gateway、credential、task、event、release 和审计;迁移 head 479,应用角色最小权限。
+5. pull-only agent 在企业内调用封闭 Runner adapter;任意 URL、SQL、命令、路径和未批准 node 均不能由控制面注入。
+6. Nginx mTLS 契约验证可信代理、证书链、CRL、证书指纹及未认证拒绝;Compose 不暴露入站端口。
+7. 安装/CSR/注册材料/轮换/撤销状态/签名发布验证/原子回滚工具实行绝对路径、符号链接拒绝、0600/0700、运行 UID/GID 绑定和有界备份;注册固定写入 `registration/registration.json`,且只含无秘密 digest/公钥/CA/CRL 绑定。
+8. 可运行 Edge 镜像实际消费闭合 `edge.json`、credential 文件、client cert/key、CA、server CRL 和 task/release 公钥,执行无入站监听的 heartbeat/reconcile/pull 循环;内置 Runner 只接受签名的 `quality-evaluation` 工程自检并形成 `evidence` 事件,其他来源任务默认拒绝。server CRL 原子替换后会重建 TLS 连接池,过期、验签失败或命中已撤销服务端证书即 fail-stop;断网进入有界 degraded,SIGTERM 原子写 stopped health 后退出。企业来源 Runner handler 仍为 `TBD_EXTERNAL`。
+
+## 数据和保留边界
+
+| 分类 | 位置 | 控制面允许 | 保留边界 |
+|---|---|---|---|
+| raw/restricted | 边缘 artifact | 否 | 企业批准周期;到期可验证删除 |
+| recent_detail | 边缘 artifact | 否 | 工程上限 365 天,企业可更短 |
+| desensitized_metadata/statistics/lineage/evidence | 边缘 queue 与控制面 | 是,策略校验后 | 按批准的数据合同和审计周期 |
+| health_summary/diagnostic_summary | 边缘 queue 与控制面 | 是,有界字段 | 按运维证据周期,禁止秘密/样本 |
+
+## 运维、安全和迁移约束
+
+- exact HTTPS control/proxy origins;企业侧以防火墙验证实际出站,应用策略不能替代网络控制。
+- queue、artifact、release、secret/cert 分离;客户端 key 固定为 `secrets/client.key`,整个 secrets 与 cert/signing key mounts 只读,queue/artifact/release 可写;初始化 UID/GID 与 Compose runtime UID/GID 必须一致。
+- 日志有界且脱敏;安全事故只记录 code、digest、计数、时间和版本。
+- 证书轮换先 pending CSR、服务端批准再切换;撤销状态必须验证绑定 gateway/generation/cert/time/key-id 的 canonical Ed25519 envelope;撤销后本地停机且为终态,不允许本地解除。
+- 发布检查将完整 Ed25519 manifest、artifact 和 bundle 记录原子归档到 `releases/verified/<manifest_digest>/`;回滚重新验签 current/target 及 artifact SHA-256,验证 deadline、版本、rollback binding、basename 后才原子提交 current 指针,使用 intent/history 识别 fsync 崩溃恢复。伪造 verified JSON、验后篡改、replay/降级/路径穿越均失败。
+- v1→v2 queue 必须 drain gate、备份、副本迁移、兼容与恢复验证,不能在线原地强迁。
+- 当前本地 queue schema 为 v2;只读 `status` 提供 task/event/outcome/artifact cleanup/release 水位、lease、schema 和 rollback intent 状态,不输出路径或秘密。retention 清理由 legal hold、delete receipt 和 ACK 约束。
+- 数据库迁移由 migration owner 执行;应用 role 不得操作签名 authority 表和密钥轮换函数。
+
+## 验收边界
+
+本地工程验收覆盖恶意输入、重放、高敏出域、断网/重连、取消、发布/回滚、证书链/CRL、
+真实 PostgreSQL、真实容器镜像配置和角色最小权限。未执行全量仓库回归,符合只验证受影响功能的约束。
+
+企业 UAT 仍需:两个批准来源完成采集/质量/受控查询;批准网络区真实部署;抓包与网关审计证明未经批准原始数据出域为 0;网络和安全法务负责人签字;备份/恢复、证书轮换/撤销与升级/回滚在企业拓扑演练。完成前不得把本工作包标为生产就绪。
+
+详细命令见 `docs/runbooks/EDGE_GATEWAY_OPERATIONS.md`,证据见
+`docs/validation/P3_WP04_EDGE_GATEWAY_EVIDENCE.md`,故障注入台账见
+`docs/validation/P3_WP04_FAILURE_INJECTION.json`。

+ 229 - 0
docs/phase3/P3_WP04_EDGE_GATEWAY_IMPLEMENTATION_PLAN.md

@@ -0,0 +1,229 @@
+# P3-WP04 Edge Gateway Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Build an enterprise-edge engineering baseline that executes approved data operations inside the enterprise boundary and sends only policy-approved, minimized events to the control plane.
+
+**Architecture:** Reuse the existing standalone Runner for governed execution and its single-use task-token and durable-ledger semantics. Add a focused `app/core/edge_gateway/` boundary for contracts, egress/retention policy, an offline SQLite queue, control-plane PostgreSQL state, and gateway lifecycle services; expose management and machine pull/event endpoints through the existing data-source blueprint. The application records certificate fingerprints and rotation state, while private keys remain at the edge and real mTLS termination, enterprise network topology, and security/legal sign-off remain external UAT gates.
+
+**Tech Stack:** Python 3.11, Flask, SQLAlchemy/PostgreSQL, SQLite WAL, PyJWT/cryptography, Alembic, Docker Compose, pytest, OpenAPI 3.1.
+
+---
+
+## Engineering and acceptance boundary
+
+- Engineering completion may reach `ENGINEERING_COMPLETE_ENTERPRISE_EDGE_UAT_BLOCKED`.
+- `enterprise_source_1`, `enterprise_source_2`, `network`, and `security_legal` remain `TBD_EXTERNAL` until enterprise evidence is supplied.
+- The control plane must never persist raw rows, recent device detail, credentials, private keys, SQL text, or unrestricted task parameters from edge outbound events.
+- The edge may execute `collect`, `profile`, `quality`, `lineage`, and `controlled_query`; the control plane may receive only `desensitized_metadata`, `statistics`, `lineage`, `evidence`, and bounded health/diagnostic summaries.
+- Existing user-owned untracked architecture assets and `canvas/` are outside this work package.
+- Use one final P3-WP04 commit after all tasks and reviews; do not create task-slice commits.
+
+### Task 1: Edge contracts, data boundary policy, and durable offline queue
+
+**Files:**
+- Create: `app/core/edge_gateway/__init__.py`
+- Create: `app/core/edge_gateway/contracts.py`
+- Create: `app/core/edge_gateway/policy.py`
+- Create: `app/core/edge_gateway/queue.py`
+- Mirror: matching paths under `deployment/app/core/edge_gateway/`
+- Test: `tests/test_phase3_wp04_edge_gateway.py`
+
+- [x] **Step 1: Write failing contract and policy tests**
+
+```python
+def test_raw_and_recent_detail_never_cross_the_control_plane_boundary():
+    policy = EdgeEgressPolicy(allowed_control_hosts={"control.example.com"})
+    for classification in ("raw", "recent_detail", "restricted"):
+        with pytest.raises(EdgePolicyError):
+            policy.approve_event({"classification": classification, "payload": {"rows": [{"secret": "x"}]}})
+
+def test_only_supported_task_types_and_bounded_approved_results_are_valid():
+    task = EdgeTaskContract.from_mapping(APPROVED_TASK)
+    assert task.task_type == "profile"
+    with pytest.raises(EdgeContractError):
+        EdgeTaskContract.from_mapping({**APPROVED_TASK, "task_type": "shell"})
+```
+
+- [x] **Step 2: Run the new unit tests and verify RED**
+
+Run: `PYTHONPATH=. .venv/bin/pytest -q tests/test_phase3_wp04_edge_gateway.py`
+
+Expected: collection/import failure because the edge gateway package does not exist.
+
+- [x] **Step 3: Implement immutable contracts and fail-closed policy**
+
+Implement canonical SHA-256 digests, stable event IDs, strict enums, recursive sensitive-key rejection, per-channel byte limits, HTTPS host allowlisting, high-sensitivity default deny, and explicit retention classes:
+
+```python
+EDGE_ONLY = {"raw", "recent_detail", "restricted"}
+CONTROL_PLANE_ALLOWED = {
+    "desensitized_metadata",
+    "statistics",
+    "lineage",
+    "evidence",
+    "health_summary",
+    "diagnostic_summary",
+}
+RETENTION_DAYS = {"raw": 0, "recent_detail": 365, "metadata": 1095, "evidence": 2190}
+```
+
+- [x] **Step 4: Implement `SqliteEdgeQueue` with WAL and idempotent event storage**
+
+The queue must atomically enqueue/claim/complete/cancel tasks, persist outbound events before delivery, retry with bounded exponential backoff, reject duplicate IDs with different digests, and return an existing terminal acknowledgement for exact replays.
+
+- [x] **Step 5: Verify GREEN and focused queue concurrency**
+
+Run: `PYTHONPATH=. .venv/bin/pytest -q tests/test_phase3_wp04_edge_gateway.py -k 'contract or policy or queue'`
+
+Expected: all selected tests pass, including two independent queue connections claiming one task only.
+
+### Task 2: Control-plane persistence, identity lifecycle, and machine pull/event API
+
+**Files:**
+- Create: `app/core/edge_gateway/repository.py`
+- Create: `app/core/edge_gateway/service.py`
+- Create: `app/api/data_source/edge_routes.py`
+- Modify: `app/api/data_source/__init__.py`
+- Modify: `app/core/system/permissions.py`
+- Create: `app/models/edge_gateway.py`
+- Modify: `app/models/__init__.py`
+- Create: `migrations/versions/20260809_477_edge_gateway_control_plane.py`
+- Create: `migrations/versions/20260809_478_edge_gateway_security_cleanup.py`
+- Create: `migrations/versions/20260809_479_edge_gateway_signed_authority.py`
+- Mirror: matching paths under `deployment/app/` and `deployment/migrations/`
+- Test: `tests/integration/test_phase3_wp04_edge_gateway_postgres.py`
+- Test: `tests/test_permission_matrix.py`
+
+- [x] **Step 1: Write failing PostgreSQL tests for registration and event idempotency**
+
+```python
+def test_gateway_registration_requires_one_time_enrollment_and_exact_certificate_binding(pg_session):
+    enrollment = service.issue_enrollment(APPROVED_GATEWAY)
+    gateway = service.register(enrollment.secret, certificate_sha256=CERT_SHA256)
+    assert gateway["status"] == "active"
+    with pytest.raises(EdgeAuthenticationError):
+        service.register(enrollment.secret, certificate_sha256=CERT_SHA256)
+
+def test_same_event_id_with_different_digest_is_rejected(pg_service):
+    pg_service.accept_event(GATEWAY_ID, APPROVED_EVENT)
+    with pytest.raises(EdgeConflictError):
+        pg_service.accept_event(GATEWAY_ID, {**APPROVED_EVENT, "payload": {"count": 2}})
+```
+
+- [x] **Step 2: Run the PostgreSQL file and verify RED**
+
+Run: `TEST_DATABASE_URL=postgresql://dataops:dataops-test-password@127.0.0.1:15432/dataops PYTHONPATH=. .venv/bin/pytest -q tests/integration/test_phase3_wp04_edge_gateway_postgres.py`
+
+Expected: failure because migration 477, repository, and service are absent.
+
+- [x] **Step 3: Add migrations 477-479 and repository CAS operations**
+
+Create governed tables for gateways, one-time enrollments, credentials/certificate generations, tasks, outbound events, releases, and audit events. Add exact unique keys, digest conflict checks, status constraints, lease/cancel fields, and database triggers that prevent active gateways without an active credential and certificate fingerprint.
+
+- [x] **Step 4: Implement service lifecycle**
+
+Support enrollment, register, heartbeat, credential/certificate rotate and revoke, task issue/pull/cancel, event accept/ack, reconnect reconciliation, safe diagnostic summary, release offer/ack/rollback, and stable audit evidence. Authenticate the complete gateway/environment/network-zone/certificate-generation binding before consuming one-time credentials.
+
+- [x] **Step 5: Add human and machine routes with deny-by-default permissions**
+
+Human management routes require `edge-gateways:read|operate|manage`; machine register, heartbeat, pull, event, and release acknowledgement routes are public only at the platform bearer layer and must require the dedicated one-time enrollment or edge credential plus exact certificate fingerprint.
+
+- [x] **Step 6: Verify GREEN, migration head, and app/deployment parity**
+
+Run the PostgreSQL file, permission matrix nodes, `alembic current`, and recursive parity checks. Actual expected head: `20260809_479`.
+
+### Task 3: Edge agent, local execution boundary, reconnect, and release rollback
+
+**Files:**
+- Create: `app/edge_gateway/__init__.py`
+- Create: `app/edge_gateway/agent.py`
+- Create: `app/edge_gateway/transport.py`
+- Create: `app/edge_gateway/bootstrap.py`
+- Modify: `app/runner/bootstrap.py`
+- Modify: `app/runner/api.py`
+- Mirror: matching paths under `deployment/app/`
+- Test: `tests/security/test_phase3_wp04_data_boundary.py`
+- Test: `tests/integration/test_phase3_wp04_edge_agent.py`
+
+- [x] **Step 1: Write failing offline/reconnect and malicious-task tests**
+
+```python
+def test_offline_execution_reconnect_delivers_each_approved_event_once(tmp_path):
+    agent = build_agent(tmp_path, transport=FlakyPullTransport(disconnect_after=1))
+    agent.run_once()
+    agent.run_once()
+    assert agent.pending_events == 1
+    agent.transport.reconnect()
+    agent.flush_events()
+    agent.flush_events()
+    assert agent.transport.accepted_event_ids == {STABLE_EVENT_ID}
+
+def test_malicious_task_cannot_request_raw_export_or_unapproved_host(agent):
+    with pytest.raises(EdgePolicyError):
+        agent.accept_task(MALICIOUS_RAW_EXPORT_TASK)
+```
+
+- [x] **Step 2: Verify RED**
+
+Run: `PYTHONPATH=. .venv/bin/pytest -q tests/security/test_phase3_wp04_data_boundary.py tests/integration/test_phase3_wp04_edge_agent.py`
+
+- [x] **Step 3: Implement pull-only agent and controlled Runner adapter**
+
+The edge initiates all control traffic, verifies signed task contracts, stages tasks before execution, dispatches only allowlisted Runner node types, and converts results to approved summaries before enqueueing outbound events. Raw artifacts and recent details remain referenced by local digest only.
+
+- [x] **Step 4: Implement reconnect/cancel/diagnostic and release state machine**
+
+Use bounded backoff with jitter, exact event replay, task cancellation probes, safe diagnostics without log bodies, signed release manifest verification, candidate activation, health confirmation, and rollback to the previous locally verified release.
+
+- [x] **Step 5: Verify GREEN and attack-path coverage**
+
+Expected tests include malicious task type, raw payload, secret-like fields, oversized summary, event replay with changed digest, revoked certificate, expired credential, unapproved host/proxy, disconnected control plane, and failed candidate rollback.
+
+### Task 4: Installation/rotation tools, OpenAPI, runbook, and delivery evidence
+
+**Files:**
+- Create: `scripts/edge_gateway_admin.py`
+- Create: `app/edge_gateway/runtime.py`
+- Create: `app/edge_gateway/__main__.py`
+- Create: `app/edge_gateway/healthcheck.py`
+- Create: `deploy/edge/Dockerfile.edge`
+- Create: `deploy/edge/docker-compose.edge.yml`
+- Create: `deploy/edge/edge.env.example`
+- Create: `deploy/edge/edge.config.example.json`
+- Create: `deploy/edge/edge.config.schema.json`
+- Create: `deploy/edge/README.md`
+- Create: `docs/runbooks/EDGE_GATEWAY_OPERATIONS.md`
+- Create: `docs/phase3/P3_WP04_EDGE_GATEWAY.md`
+- Create: `docs/validation/P3_WP04_EDGE_GATEWAY_EVIDENCE.md`
+- Create: `docs/validation/P3_WP04_FAILURE_INJECTION.json`
+- Modify: `scripts/generate_openapi.py`
+- Modify: `docs/architecture/OPENAPI.yaml`
+- Modify: `docs/DATAOPS_PHASE3_6_MONTH_DEVELOPMENT_PLAN_20260802.md`
+- Modify: `docs/phase3/P3_WP00_REQUIREMENTS.json`
+- Test: `tests/test_phase3_wp04_delivery_contract.py`
+- Test: `tests/integration/test_phase3_wp04_edge_runtime.py`
+
+- [x] **Step 1: Write failing delivery-contract tests**
+
+Assert that the Compose topology is pull-only, mounts key/certificate paths read-only, uses a non-root/read-only container, defines no inbound control-plane port, requires explicit host/proxy allowlists, and exposes health without sensitive fields. Assert that the admin tool creates a private key and CSR locally with restrictive permissions and never prints or uploads private-key material.
+
+- [x] **Step 2: Verify RED**
+
+Run: `PYTHONPATH=. .venv/bin/pytest -q tests/test_phase3_wp04_delivery_contract.py`
+
+- [x] **Step 3: Implement tools and deployment contract**
+
+Provide `init`, `csr`, `register-manifest`, `rotate`, `revoke-status`, `release-check`, `rollback`, and read-only `status` operations. Require a dedicated absolute root, O_NOFOLLOW/fcntl mutation locking, same-file-descriptor artifact verification/copy, atomic files, full Ed25519 release authority, bounded backups, and operator confirmation for rollback; never embed credentials in Compose or documentation. The Edge image runs the actual pull-only loop, verifies a fresh server CRL with `CRL_CHECK_LEAF`, publishes only a safe health file, degrades safely during disconnect, and exposes no inbound listener.
+
+- [x] **Step 4: Generate OpenAPI and write evidence-bound documentation**
+
+Document architecture, allowed data classes, default-deny decisions, retention/legal hold/delete receipts, queue v1-to-v2 copy migration, incident thresholds and diagnostics, database role order, client/server CRL updates, install/rotate/revoke/upgrade/rollback procedures, and external UAT inputs. Package the full OpenAPI, failure-injection ledger, edge runtime/config schema and full-file SHA-256 closure. Mark only locally verified engineering work complete.
+
+- [x] **Step 5: Run final P3-WP04 targeted gates**
+
+Run only P3-WP04 unit, security, PostgreSQL integration, edge-agent, Runner directly affected tests, permission/OpenAPI contracts, Ruff, `py_compile`, JSON/YAML parsing, Compose config validation, migration head, `app/deployment` parity, `git diff --check`, and secret scan. Do not run the repository-wide regression suite.
+
+- [x] **Step 6: Two-stage review and one final commit**
+
+Require specification PASS and quality/security approval. Then stage only P3-WP04 files, preserve unrelated untracked assets, and create one local commit without pushing or deploying.

+ 616 - 0
docs/runbooks/EDGE_GATEWAY_OPERATIONS.md

@@ -0,0 +1,616 @@
+# 边缘网关安装、证书与发布运行手册
+
+适用对象:平台运维、企业网络、安全、数据库和验收人员。本手册描述可在本地验证的工程基线;当前最高状态为
+`ENGINEERING_COMPLETE_ENTERPRISE_EDGE_UAT_BLOCKED`。`enterprise_source_1`、
+`enterprise_source_2`、`network`、`security_legal` 均为 `TBD_EXTERNAL`,不得把本手册的本地验证解释为真实企业验收。
+
+## 1. 安全边界与职责
+
+- 边缘主动以 HTTPS/mTLS 拉取,不开放入站控制端口。企业网络负责人批准精确控制面/代理 origin、DNS、出站防火墙、CA 与 CRL。
+- 平台控制面签发一次性注册令牌、任务和发布 Ed25519 签名;私钥由对应安全域保管。
+- 边缘私钥只在本地生成,权限 `0600`;根目录、queue、artifacts、releases、secrets 目录权限 `0700`。
+- 默认拒绝:任意命令、任意 SQL、任意 URL、路径穿越、符号链接、通配 host、HTTP、原始行、近期明细、凭据、私钥和本地路径均不可出域。
+- 数据分级:`raw`、`recent_detail`、`restricted` 仅边缘保存;控制面只接受 `desensitized_metadata`、`statistics`、`lineage`、`evidence`、`health_summary`、`diagnostic_summary`。原始数据保留周期由企业制度批准;近期明细工程上限 365 天;控制面事件按已批准最小保留期执行。
+- PostgreSQL 迁移 head 为 `20260809_479`。应用角色不能创建/轮换/撤销 signing key;仅 migration/security-owner 路径可写受控 signing state。
+
+## 2. 前置条件
+
+零主机依赖基线(低于该版本必须先由平台运维完成兼容性评估):Docker Engine 26、
+Docker Compose 2.27、Python 3.11、OpenSSL 3、SQLite 3.40、curl 8 和 cosign 2.2。
+主机不安装数据库驱动、企业来源 SDK 或编译器;这些只能进入经签名的 Edge 镜像。
+
+1. 在批准的 Linux 主机建立专用非 root 账号,记录其数值 UID/GID,确认磁盘加密、时间同步、审计和备份位置。`init` 参数、`edge.env` 和 Compose `user` 必须使用同一组 UID/GID;不一致时工具拒绝继续,只有 root 可显式使用 `--allow-chown` 安全调整既有空目录所有权。
+2. 网络负责人书面给出精确控制 origin、可选代理 origin、DNS/IP、证书链、CRL 更新频率和出站策略。
+3. 安全负责人交付任务/发布 Ed25519 公钥集;不得交付控制面签名私钥。
+4. 数据负责人批准可执行来源、用途、环境、网络区、分类、保留期和只读账号。
+5. 确认控制面数据库已由 migration owner 升到 479;应用数据库角色经过最小权限验证。
+
+以下示例固定使用绝对根目录 `/opt/dataops-edge`;如企业批准其他目录,逐条替换并重新留证,不得使用相对路径。
+
+发布包必须来自受控制品库。下载到空的 `/opt/dataops-release`,先核对制品库登记的
+tar SHA-256 和离线签名,再展开并验证包内每一个文件。下面的公钥、签名和摘要均是
+企业批准后注入的占位路径,不能用仓库示例代替:
+
+```bash
+sudo install -d -m 0700 /opt/dataops-release
+cd /opt/dataops-release
+sha256sum dataops-platform-release-<VERSION>.tar.gz
+cosign verify-blob --key /etc/dataops/trust/release-cosign.pub \
+  --signature dataops-platform-release-<VERSION>.tar.gz.sig \
+  dataops-platform-release-<VERSION>.tar.gz
+sudo tar -xzf dataops-platform-release-<VERSION>.tar.gz -C /opt
+cd /opt/dataops-platform
+sha256sum -c SHA256SUMS
+```
+
+任一步失败:隔离下载文件、记录制品 digest 和失败码、从受控制品库重新取件;禁止
+跳过验签、手工改包或继续启动。以上只证明交付完整性,不证明企业来源/网络 UAT。
+
+## 3. 初始化与 CSR
+
+```bash
+sudo python3 /opt/dataops-platform/scripts/edge_gateway_admin.py init \
+  --root /opt/dataops-edge \
+  --runtime-uid 65532 \
+  --runtime-gid 65532
+sudo python3 /opt/dataops-platform/scripts/edge_gateway_admin.py csr --root /opt/dataops-edge --common-name edge-prod-zone-a
+```
+
+验收:JSON 输出仅含路径、指纹和状态;`/opt/dataops-edge/secrets/client.key` 为 `0600`,CSR 签名有效,stdout/stderr/审计没有私钥内容。失败时停止注册,隔离并销毁不完整材料后重新初始化;不得降低权限或上传 key。
+
+由企业 CA 对 `/opt/dataops-edge/certificates/client.csr` 签发客户端证书。将证书、CA bundle、CRL 写入批准证书目录并设为只读挂载源。验证证书用途、有效期、链、CRL 和 CSR/key 公钥一致:
+
+```bash
+python3 /opt/dataops-platform/scripts/edge_gateway_admin.py register-manifest \
+  --root /opt/dataops-edge \
+  --certificate /opt/dataops-edge/certificates/client.pem \
+  --ca-bundle /opt/dataops-edge/certificates/ca.pem \
+  --crl /opt/dataops-edge/certificates/control-plane.crl.pem \
+  --gateway-id 11111111-1111-4111-8111-111111111111 \
+  --environment production \
+  --network-zone zone-a \
+  --policy-digest aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \
+  --control-origin https://control.example.invalid
+```
+
+工具只写固定目标 `/opt/dataops-edge/registration/registration.json`,不会覆盖 secrets、certificates、CSR、queue 或 releases。它校验证书有效期、clientAuth EKU、key/CSR/certificate 公钥一致、CA 签名、CRL 签名/有效期/撤销状态;相同内容重放幂等,已有不同内容则冲突。注册材料只含 digest、公钥绑定和 CA/CRL 证据字段,不含本地绝对路径、私钥或 credential。通过受批准的人工/平台通道提交它和一次性注册令牌;令牌只能使用一次且失败绑定不得消耗。控制面返回的 edge credential 立即写入 `/opt/dataops-edge/secrets/control-credential`(`0600`),不得进入 shell history、环境文件或工单正文。
+
+### 3.1 控制面注册、轮换、撤销与机器 ACK
+
+下列命令均使用占位符,执行前由平台运维把 origin、ID 和 digest 替换为批准值。
+Bearer、enrollment token 和 edge credential 只放在 `0600` header 文件;不得把值
+直接写进命令行。`human-auth.headers` 由企业统一认证流程生成,本手册不生成弱口令。
+
+先以 `umask 077` 创建以下闭合请求文件;字段外内容一律拒绝。示例 UUID、digest、
+deadline 和 lease 只是占位,必须替换为控制面返回或批准台账中的精确值:
+
+```json
+// /run/operator/enrollment-request.json
+{"gateway_name":"edge-prod-zone-a","environment":"production","network_zone":"zone-a","policy_digest":"<64-hex>","allowed_control_hosts":["control.example.invalid"],"allowed_proxy_hosts":[],"expected_certificate_sha256":"<64-hex>","ttl_seconds":3600}
+// /run/operator/rotate-request.json
+{"certificate_sha256":"<new-64-hex>","request_id":"rotate-2026-08-zone-a"}
+// /run/edge/outcome-request.json
+{"environment":"production","network_zone":"zone-a","generation":1,"outcome":"completed","lease_token":"<control-lease>","safe_summary":{"status":"completed"}}
+// /run/edge/reconcile-request.json
+{"environment":"production","network_zone":"zone-a","generation":1,"limit":50,"cancel_cursor":null,"release_cursor":null}
+// /run/edge/release-installed-ack.json
+{"environment":"production","network_zone":"zone-a","generation":1,"release_id":"<uuid>","outcome":"installed","safe_summary":{"release_status":"installed","version":"2.0.0"}}
+```
+
+注释行仅用于说明,真正保存时每个文件只保留对应的一行 JSON;运行前用 OpenAPI
+`EdgeEnrollmentRequest`、`EdgeRotateRequest`、`EdgeTaskOutcomeRequest`、
+`EdgeReconcileRequest`、`EdgeReleaseAckRequest` 校验。`lease_token` 只由 Agent 持久化加密
+并自动发送,人工 curl 仅限事故演练,演练文件用后立即销毁。
+
+平台人员创建 enrollment(HTTP 201;验收字段为 enrollment_id、gateway_id、
+expires_in、returned_once):
+
+```bash
+umask 077
+curl --fail-with-body --silent --show-error \
+  --header @/run/operator/human-auth.headers --header 'Content-Type: application/json' \
+  --output /run/operator/enrollment-response.json \
+  --data @/run/operator/enrollment-request.json \
+  https://control.example.invalid/api/datasource/edge/enrollments
+```
+
+`enrollment-request.json` 必须含 gateway_name、environment、network_zone、policy_digest、
+allowed_control_hosts、allowed_proxy_hosts、expected_certificate_sha256。把一次性 token
+从响应安全写入 header 文件,既不打印也不进入 shell history:
+
+```bash
+python3 - /run/operator/enrollment-response.json /run/operator/enrollment.headers <<'PY'
+import json, os, sys
+value = json.load(open(sys.argv[1], encoding="utf-8"))["data"]["enrollment_token"]
+fd = os.open(sys.argv[2], os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
+with os.fdopen(fd, "w", encoding="utf-8") as target:
+    target.write("X-Edge-Enrollment: " + value + "\n")
+PY
+```
+
+边缘以 mTLS 注册(HTTP 201;验收 generation=1、certificate_sha256 精确匹配、
+credential_returned_once=true):
+
+```bash
+curl --fail-with-body --silent --show-error \
+  --cert /opt/dataops-edge/certificates/client.pem \
+  --key /opt/dataops-edge/secrets/client.key \
+  --cacert /opt/dataops-edge/certificates/ca.pem \
+  --header @/run/operator/enrollment.headers --header 'Content-Type: application/json' \
+  --output /run/operator/registration-response.json \
+  --data @/opt/dataops-edge/registration/registration.json \
+  https://control.example.invalid/api/datasource/edge/register
+```
+
+安全落盘 credential,同时生成 curl 使用的 `X-Edge-Credential` header;两个文件均
+`0600`,脚本不打印值:
+
+```bash
+python3 - /run/operator/registration-response.json \
+  /opt/dataops-edge/secrets/control-credential /opt/dataops-edge/secrets/edge-auth.headers <<'PY'
+import json, os, sys
+value = json.load(open(sys.argv[1], encoding="utf-8"))["data"]["credential"]
+for path, prefix in ((sys.argv[2], ""), (sys.argv[3], "X-Edge-Credential: ")):
+    fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
+    with os.fdopen(fd, "w", encoding="utf-8") as target:
+        target.write(prefix + value + "\n")
+PY
+shred -u /run/operator/registration-response.json /run/operator/enrollment-response.json
+```
+
+平台人员轮换/撤销(均 HTTP 200;轮换验收 generation 增加且旧 credential 失效;
+撤销验收 revoked=true、旧身份随后返回 401/403):
+
+```bash
+curl --fail-with-body --header @/run/operator/human-auth.headers \
+  --header 'Content-Type: application/json' --data @/run/operator/rotate-request.json \
+  https://control.example.invalid/api/datasource/edge/gateways/<GATEWAY_ID>/rotate
+curl --fail-with-body --request POST --header @/run/operator/human-auth.headers \
+  https://control.example.invalid/api/datasource/edge/gateways/<GATEWAY_ID>/revoke
+```
+
+机器 outcome、reconcile 和 installed ACK 必须同时使用 mTLS 与 credential header。
+`/tasks/{task_id}/outcome` 的 `{task_id}` 是文档占位符,实际替换为已租赁任务 ID:
+
+```bash
+curl --fail-with-body --cert /opt/dataops-edge/certificates/client.pem \
+  --key /opt/dataops-edge/secrets/client.key --cacert /opt/dataops-edge/certificates/ca.pem \
+  --header @/opt/dataops-edge/secrets/edge-auth.headers --header 'Content-Type: application/json' \
+  --data @/run/edge/outcome-request.json \
+  https://control.example.invalid/api/datasource/edge/gateways/<GATEWAY_ID>/tasks/<TASK_ID>/outcome
+curl --fail-with-body --cert /opt/dataops-edge/certificates/client.pem \
+  --key /opt/dataops-edge/secrets/client.key --cacert /opt/dataops-edge/certificates/ca.pem \
+  --header @/opt/dataops-edge/secrets/edge-auth.headers --header 'Content-Type: application/json' \
+  --data @/run/edge/reconcile-request.json \
+  https://control.example.invalid/api/datasource/edge/gateways/<GATEWAY_ID>/reconcile
+curl --fail-with-body --cert /opt/dataops-edge/certificates/client.pem \
+  --key /opt/dataops-edge/secrets/client.key --cacert /opt/dataops-edge/certificates/ca.pem \
+  --header @/opt/dataops-edge/secrets/edge-auth.headers --header 'Content-Type: application/json' \
+  --data @/run/edge/release-installed-ack.json \
+  https://control.example.invalid/api/datasource/edge/gateways/<GATEWAY_ID>/releases/ack
+```
+
+outcome 验收为 completed/failed/cancelled 的幂等确认;reconcile 验收 cancel/release
+cursor 单调且数组不超过 50;installed ACK 验收 release ID、artifact digest 和当前
+verified pointer 一致。4xx 先检查绑定/重放,不重建 credential;5xx/断网保留本地
+outbox 并按阈值退避,不手工伪造 ACK。
+
+## 4. 部署与启动验收
+
+1. 复制 `/opt/dataops-platform/deploy/edge/edge.env.example` 到 `/opt/dataops-edge/edge.env`,设为 `0600`,将 `EDGE_RUNTIME_UID/GID` 填成初始化时批准的同一数值,只填写目录、不可变镜像 digest 和精确 origin;credential 不写入 env。仓库默认的 `dataops-edge:local` 只允许本地工程验证,生产必须把 `EDGE_IMAGE` 和 `EDGE_PULL_POLICY=never` 固定到已验签 digest。
+2. 本地工程镜像可在隔离构建区从归档根目录构建;该构建可能访问批准的 base image/
+Python 包镜像,因此不能在企业 Edge 运行主机临时构建。企业 Edge 主机只从已验签
+镜像仓库按 digest 拉取。两种路径都必须记录最终 digest,禁止只用可变 tag:
+
+```bash
+docker build --pull=false -f deploy/edge/Dockerfile.edge -t dataops-edge:<VERSION> .
+docker image inspect --format '{{.Id}}' dataops-edge:<VERSION>
+# 企业制品库路径:先由安全方验证签名,再拉 immutable digest
+cosign verify --key /etc/dataops/trust/image-cosign.pub \
+  registry.example.invalid/dataops-edge@sha256:<IMAGE_DIGEST>
+docker pull registry.example.invalid/dataops-edge@sha256:<IMAGE_DIGEST>
+```
+
+3. 将 `deploy/edge/edge.config.example.json` 复制为 `/opt/dataops-edge/config/edge.json`。
+`deploy/edge/edge.config.schema.json` 是该文件的完整闭合 schema:除 schema 所列字段外均拒绝。
+credential、task/release 公钥和 server CRL 路径不在 `edge.json`;它们分别从
+`secrets/control-credential`、`signing/*.json` 和 Compose 固定只读挂载注入。校验:
+
+```bash
+python3 -c 'import json,jsonschema; s=json.load(open("deploy/edge/edge.config.schema.json")); d=json.load(open("/opt/dataops-edge/config/edge.json")); jsonschema.Draft202012Validator(s,format_checker=jsonschema.FormatChecker()).validate(d)'
+sudo install -d -m 0700 -o 65532 -g 65532 /opt/dataops-edge/{config,queue,artifacts,releases,secrets,certificates,signing}
+chmod 0600 /opt/dataops-edge/config/edge.json /opt/dataops-edge/secrets/client.key
+find /opt/dataops-edge -xdev -type d -exec stat -f '%Su:%Sg %Lp %N' {} \;
+find /opt/dataops-edge -xdev -type f -exec stat -f '%Su:%Sg %Lp %N' {} \;
+```
+
+Linux 上将最后两条 `stat -f` 替换为 `stat -c '%U:%G %a %n'`。验收要求运行 UID/GID
+可读 config/cert/key/signing,只能写 queue/artifacts/releases;其他账号不可读 key/credential。
+3. 渲染而不启动:
+
+```bash
+docker compose --env-file /opt/dataops-edge/edge.env -f /opt/dataops-platform/deploy/edge/docker-compose.edge.yml config
+```
+
+4. 安全和网络批准渲染结果后再启动:
+
+```bash
+docker compose --env-file /opt/dataops-edge/edge.env -f /opt/dataops-platform/deploy/edge/docker-compose.edge.yml up -d
+```
+
+启动后最多等待 120 秒。首次 health 必须是 `healthy`;`degraded` 只允许在批准的
+断网演练中出现,`stopped` 表示身份/CRL fail-stop,不能强行复活:
+
+```bash
+healthy=0
+for n in $(seq 1 24); do
+  docker compose --env-file /opt/dataops-edge/edge.env \
+    -f /opt/dataops-platform/deploy/edge/docker-compose.edge.yml ps
+  docker compose --env-file /opt/dataops-edge/edge.env \
+    -f /opt/dataops-platform/deploy/edge/docker-compose.edge.yml exec -T edge-gateway \
+    python -m app.edge_gateway.healthcheck && { healthy=1; break; }
+  sleep 5
+done
+if [ "$healthy" -ne 1 ]; then
+  docker compose --env-file /opt/dataops-edge/edge.env \
+    -f /opt/dataops-platform/deploy/edge/docker-compose.edge.yml stop edge-gateway
+  exit 1
+fi
+docker compose --env-file /opt/dataops-edge/edge.env \
+  -f /opt/dataops-platform/deploy/edge/docker-compose.edge.yml exec -T edge-gateway \
+  python -c 'import json; print(json.load(open("/run/edge/tmp/health.json"))["status"])'
+```
+
+验收:无 `ports`/`expose`;进程 UID 非 0;rootfs 只读;capabilities 全部移除;证书、secret、signing 公钥只读;仅 queue/artifact/release 可写;`health.json` 输出只含有界状态,不含 credential、key、原始行、SQL 或路径。120 秒未健康则停止容器、保留 health/计数/digest,检查 CRL、证书、exact origin 和文件 owner;禁止扩大 host allowlist。
+
+## 5. 断网、重连、取消与诊断
+
+- 断网:边缘先持久化已签名任务和出站事件,按有界指数退避重试;不得绕过 allowlist 改用其他地址。达到安全上限进入人工处置。
+- 重连:先 reconcile 取消游标、发布游标和 release baseline,再拉取新任务;稳定 event ID 的同 digest 重放返回原 ACK,异 digest 冲突并告警。
+- 取消:控制面置 cancel-requested;边缘在执行前、执行中合作点和出站前检查。不能安全中断的本地执行应停止派发新工作并形成安全摘要。
+- 诊断:只允许计数、版本、队列水位、最后心跳、分类化错误码和 digest。禁止日志正文包含源数据、近期明细、credential、lease、证书、私钥、SQL、连接串或本地绝对数据路径。
+- CRL 命中或证书撤销:mTLS 代理拒绝连接;边缘写本地停机标记并停止控制流。不得通过本地命令解除撤销。
+
+只读诊断统一使用下列命令;输出只有 queue/release/lease/cursor/outcome/artifact
+cleanup 计数和 digest,不含本地路径或秘密:
+
+```bash
+python3 /opt/dataops-platform/scripts/edge_gateway_admin.py status --root /opt/dataops-edge
+```
+
+人工介入阈值由企业可观测负责人确认,未批准更宽阈值前使用以下上限:
+
+| 信号 | 告警 | 人工介入阈值 | 安全动作/恢复验收 |
+|---|---:|---:|---|
+| 控制面连续失败 | 3 次或 60 秒 | 10 次或 15 分钟 | 保持 pull-only,禁止换 host;恢复后先 reconcile |
+| queue 待发事件 | 1,000 | 10,000 | 冻结新任务;确认 ACK 后再降水位 |
+| queue 运行/lease | 最老 5 分钟 | 最老 15 分钟 | 停止新 lease,验证 fencing,不手工删行 |
+| queue/artifact 分区 | 剩余 20% | 剩余 10% | 冻结采集;先 legal hold/retention 决策再清理 |
+| server CRL | 距 nextUpdate 24 小时 | 距 nextUpdate 1 小时或已过期 | fail-stop;更新并验签后重启 |
+| release rollback intent | 任意存在 | 5 分钟未清理 | 禁止新发布;按 intent/history 恢复 |
+
+每次告警记录 gateway digest、时间、计数、版本和批准工单号;不得附带源记录、SQL、
+credential 或证书私钥。阈值恢复验收要求连续 3 个心跳 healthy、queue 水位下降、
+reconcile cursor 单调和无新签名/身份错误。
+
+### 5.1 冻结、取消、强制 reconcile 与 drain
+
+当前工程基线没有可绕过签名/租约的“强制执行”入口。冻结新 pull 的唯一受控方式是
+停止 Agent;控制面可继续登记 cancel-requested。恢复时启动 Agent,启动循环会先完整
+reconcile,再拉取新任务:
+
+```bash
+COMPOSE=/opt/dataops-platform/deploy/edge/docker-compose.edge.yml
+ENV=/opt/dataops-edge/edge.env
+docker compose --env-file "$ENV" -f "$COMPOSE" stop edge-gateway
+curl --fail-with-body --request POST --header @/run/operator/human-auth.headers \
+  https://control.example.invalid/api/datasource/edge/tasks/<TASK_ID>/cancel
+sqlite3 -readonly /opt/dataops-edge/queue/edge-queue-v2.sqlite3 \
+  "SELECT status,count(*) FROM edge_tasks GROUP BY status; SELECT status,count(*) FROM edge_outbound_events GROUP BY status;"
+docker compose --env-file "$ENV" -f "$COMPOSE" up -d edge-gateway
+```
+
+drain 完成条件:task 不含 `pending/leased`,event/outcome 不含 `pending/sending`,
+artifact cleanup 不含 `deleting`,`status` 显示 active lease=0。若不能 drain,保持容器
+停止,保留 queue 副本和 digest;不得直接改状态或删行。恢复后必须观察 reconcile
+cursor 前进、cancelled outcome ACK 和待发事件下降,再解除变更冻结。
+
+## 6. 证书轮换与撤销
+
+轮换先生成 pending key/CSR,不覆盖当前有效材料。相同 request-id 和相同 digest 幂等返回;相同 request-id 的不同输入失败。
+
+```bash
+python3 /opt/dataops-platform/scripts/edge_gateway_admin.py rotate \
+  --root /opt/dataops-edge \
+  --common-name edge-prod-zone-a \
+  --request-id rotate-2026-08-zone-a \
+  --expected-current-certificate-digest bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
+```
+
+CA 签发 pending CSR 后,先在控制面调用 rotate 并取得新 generation/credential 的服务端批准证据,再原子切换证书/credential;旧材料只保留最多 3 份加密备份并在观察期后销毁。失败时继续使用尚未过期且未撤销的当前 generation;不得半切换。
+
+切换前先把新证书、新私钥和新 credential 分别写入同一文件系统的 `.next` 文件并设
+`0600`,验证证书/私钥、公钥、generation 和服务端响应完全匹配。然后停止容器,逐个
+原子 rename;容器停止期间任何一步失败都从 `.previous` 恢复,不能以半套身份启动:
+
+```bash
+COMPOSE=/opt/dataops-platform/deploy/edge/docker-compose.edge.yml
+ENV=/opt/dataops-edge/edge.env
+docker compose --env-file "$ENV" -f "$COMPOSE" stop edge-gateway
+install -m 0600 /run/operator/client.pem.next /opt/dataops-edge/certificates/client.pem.next
+install -m 0600 /run/operator/client.key.next /opt/dataops-edge/secrets/client.key.next
+install -m 0600 /run/operator/control-credential.next /opt/dataops-edge/secrets/control-credential.next
+mv /opt/dataops-edge/certificates/client.pem /opt/dataops-edge/certificates/client.pem.previous
+mv /opt/dataops-edge/secrets/client.key /opt/dataops-edge/secrets/client.key.previous
+mv /opt/dataops-edge/secrets/control-credential /opt/dataops-edge/secrets/control-credential.previous
+mv /opt/dataops-edge/certificates/client.pem.next /opt/dataops-edge/certificates/client.pem
+mv /opt/dataops-edge/secrets/client.key.next /opt/dataops-edge/secrets/client.key
+mv /opt/dataops-edge/secrets/control-credential.next /opt/dataops-edge/secrets/control-credential
+docker compose --env-file "$ENV" -f "$COMPOSE" up -d edge-gateway
+```
+
+120 秒内未 healthy 或新 generation 绑定失败:停止容器、把三个 `.previous` 原子改回
+固定文件名并恢复旧 generation 配置;只有旧证书尚未过期且未撤销时才允许回退。成功
+连续 3 个心跳后再按企业加密介质制度销毁 response、`.previous` 和临时 header;普通
+文件系统上的 `shred` 不能替代加密磁盘/密钥销毁证据。
+
+撤销状态查询:
+
+```bash
+python3 /opt/dataops-platform/scripts/edge_gateway_admin.py revoke-status --root /opt/dataops-edge
+python3 /opt/dataops-platform/scripts/edge_gateway_admin.py revoke-status \
+  --root /opt/dataops-edge \
+  --server-status /opt/dataops-edge/state/server-revocation.json \
+  --trusted-key status-key-2026=ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff \
+  --expected-gateway-id 11111111-1111-4111-8111-111111111111 \
+  --expected-generation 1 \
+  --expected-certificate-digest bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
+```
+
+服务端文件必须是 canonical Ed25519 签名 envelope,并精确绑定 gateway、generation、证书 SHA-256、status、issued/expires 和 key-id;伪签名、过期、跨网关或绑定不符均拒绝。若可信证据为 revoked,工具写 `REVOKED_STOP`;本地 revoked 是终态,即使后续收到 active 也不会自行解除。停止容器、隔离本机、保留安全日志并通知控制面。恢复必须新建 enrollment、证书和 credential;禁止清除标记继续复用旧身份。
+
+## 7. 签名升级与回滚
+
+控制面签名任务创建只接受完整 `EdgeTaskContract`,签名由服务端产生,操作者不提交
+signature。当前镜像内置的唯一可执行任务是工程自检 `quality/quality-evaluation/evidence`;
+企业来源 collect/profile/lineage/controlled_query handler 仍为 `TBD_EXTERNAL`,在验收接入
+前不得下发。工程自检请求示例:
+
+```bash
+umask 077
+cat >/run/operator/edge-self-check-task.json <<'JSON'
+{"task_id":"runtime-self-check-20260809","gateway_id":"11111111-1111-4111-8111-111111111111","environment":"production","network_zone":"zone-a","purpose":"quality-evaluation","classification":"evidence","task_type":"quality","contract_version":1,"deadline_at":"2026-08-09T16:00:00Z","attempt":1,"idempotency_key":"runtime-self-check-20260809","policy_digest":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}
+JSON
+curl --fail-with-body --silent --show-error \
+  --header @/run/operator/human-auth.headers --header 'Content-Type: application/json' \
+  --data @/run/operator/edge-self-check-task.json \
+  https://control.example.invalid/api/datasource/edge/tasks
+```
+
+把 deadline、gateway/policy 绑定替换为当前批准值。验收为控制面返回 task_id/digest,
+Agent 取得签名任务后本地事件为 `evidence`、payload 仅含 check_count/passed_count/scope/
+status,event exact ACK 后 queue 待发数归零。任何其他操作在企业 handler 接入前应返回
+`execution_boundary_rejected`,不能为通过测试而改成空成功。
+
+先下载到 `/opt/dataops-edge/releases`,再验证完整 Ed25519 manifest、artifact SHA-256、deadline、artifact basename、当前/rollback version 和 key-id:
+
+```bash
+python3 /opt/dataops-platform/scripts/edge_gateway_admin.py release-check \
+  --root /opt/dataops-edge \
+  --manifest /opt/dataops-edge/releases/release-manifest.json \
+  --artifact /opt/dataops-edge/releases/edge-agent-2.0.0.bin \
+  --trusted-key release-key-2026=cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc \
+  --current-version 1.0.0
+```
+
+只有 status `verified` 的候选可进入安装。工具会把已验签 manifest、artifact 和 bundle 记录分别 fsync 后原子写入 `/opt/dataops-edge/releases/verified/<manifest_digest>/`;同 digest 仅在整个受控 bundle 再验证一致时幂等返回。该目录外的“verified JSON”、验签后被篡改 artifact、manifest replay 改 digest、降级、过期、路径穿越、符号链接和任意命令均失败。安装前备份 queue 数据库、执行健康检查,再由控制面签收 installed。
+
+正常容器升级由运维在 Agent 已停止且 queue 已 drain 后,把 `EDGE_IMAGE` 改成已验签
+不可变 digest,再启动候选。`current.json` 只记录已验证 bundle,不替代镜像 digest:
+
+```bash
+docker compose --env-file /opt/dataops-edge/edge.env \
+  -f /opt/dataops-platform/deploy/edge/docker-compose.edge.yml stop edge-gateway
+sed -i.bak 's#^EDGE_IMAGE=.*#EDGE_IMAGE=registry.example.invalid/dataops-edge@sha256:<IMAGE_DIGEST>#' \
+  /opt/dataops-edge/edge.env
+docker compose --env-file /opt/dataops-edge/edge.env \
+  -f /opt/dataops-platform/deploy/edge/docker-compose.edge.yml up -d edge-gateway
+python3 /opt/dataops-platform/scripts/edge_gateway_admin.py status --root /opt/dataops-edge
+```
+
+连续 3 次 healthy、queue schema v2、签名自检 event ACK 和 `current.json`/候选 manifest
+一致后才发送 installed ACK;失败立即恢复 `edge.env.bak` 并按下述回滚程序处理。不要用
+可变 tag,也不要把 `release-check` 的 verified 状态当成已经安装。
+
+发布状态机固定为 `downloaded -> verified -> candidate -> installed -> ACK`;任一步失败
+转入 failed。只有新进程无法启动、120 秒无 healthy、queue schema 不是 v2 或控制面
+身份绑定失败,且上个 verified bundle 仍在有效 deadline 内,才允许自动回滚。数据
+语义/来源质量异常不得自动回滚,必须由数据负责人和事故指挥批准人工回滚。
+
+安装前记录 `status`、备份 queue,停止容器,原子更新 current 指针后以相同 Compose
+启动;启动成功后才发送 installed ACK。固定恢复文件为
+`/opt/dataops-edge/state/rollback-intent.json`、
+`/opt/dataops-edge/state/rollback-history/<request_digest>.json` 和
+`/opt/dataops-edge/releases/current.json`。恢复检查命令:
+
+```bash
+python3 /opt/dataops-platform/scripts/edge_gateway_admin.py status --root /opt/dataops-edge
+sqlite3 /opt/dataops-edge/queue/edge-queue-v2.sqlite3 'PRAGMA quick_check; PRAGMA user_version;'
+docker compose --env-file /opt/dataops-edge/edge.env \
+  -f /opt/dataops-platform/deploy/edge/docker-compose.edge.yml up -d
+```
+
+若 status 显示 rollback intent:禁止发布新版本;按 intent 中 expected current/target
+重新执行完全相同的 rollback 请求。相同请求会完成/清理可恢复 intent;任何字段不同
+都会冲突。不可手删 intent/history/current。恢复后必须验证 current 指针、bundle
+manifest/artifact SHA-256、queue quick_check/user_version=2 和连续 3 个 healthy,再 ACK。
+
+回滚需要交互输入 `ROLLBACK`,或自动化同时提供 `--yes`、expected-current digest 和 target verified digest:
+
+```bash
+python3 /opt/dataops-platform/scripts/edge_gateway_admin.py rollback \
+  --root /opt/dataops-edge --yes \
+  --expected-current-digest dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd \
+  --target-digest eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee \
+  --trusted-key release-key-2026=cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
+```
+
+工具重新验证 current/target 两个受控 bundle 的完整 canonical Ed25519 manifest、可信 key-id、deadline、版本/rollback 绑定和 artifact SHA-256,随后写 rollback intent 并原子提交 `/opt/dataops-edge/releases/current.json` 指针,fsync 父目录后才返回成功;history 和 intent 使指针已提交但进程崩溃的场景可判定并幂等恢复。已有 history 回放仍须先确认 current pointer,再将遗留 intent 与本次 request digest、expected current、target 做严格等值校验;只清理完全匹配的 intent 并 fsync state 目录,异内容一律冲突。unlink/fsync 中断可由下次同请求继续恢复,且不会阻塞清理后的下一次合法回滚;备份有界为 3 份。回滚后执行健康、queue schema、control binding 和事件重放验证,再上报 outcome=rollback。若任一 bundle 被篡改、expected-current/target 不符或 fsync 失败,停止服务并按 intent/history 判定,不得强行覆盖。
+
+## 8. queue v1 到 v2 迁移 drain gate
+
+当前本地 queue schema 是 v2;以下仅用于遗留 v1 主机。迁移器会拒绝仍有
+pending/leased task 或未 ACK event 的 v1 数据库,因此先冻结拉取并 drain:
+
+```bash
+python3 /opt/dataops-platform/scripts/edge_gateway_admin.py status --root /opt/dataops-edge
+sqlite3 /opt/dataops-edge/queue/edge-queue-v1.sqlite3 \
+  "SELECT status,count(*) FROM edge_tasks GROUP BY status; SELECT status,count(*) FROM edge_outbound_events GROUP BY status;"
+sqlite3 /opt/dataops-edge/queue/edge-queue-v1.sqlite3 'PRAGMA wal_checkpoint(FULL); PRAGMA quick_check;'
+sqlite3 /opt/dataops-edge/queue/edge-queue-v1.sqlite3 \
+  "VACUUM INTO '/opt/dataops-edge/queue-backup/edge-queue-v1-drained.sqlite3';"
+sha256sum /opt/dataops-edge/queue-backup/edge-queue-v1-drained.sqlite3
+cp -p /opt/dataops-edge/queue-backup/edge-queue-v1-drained.sqlite3 \
+  /opt/dataops-edge/queue-migration/edge-queue-v2-candidate.sqlite3
+EDGE_QUEUE_CANDIDATE=/opt/dataops-edge/queue-migration/edge-queue-v2-candidate.sqlite3 \
+  /opt/dataops-platform/.venv/bin/python -c \
+  'import os; from app.core.edge_gateway.queue import SqliteEdgeQueue; SqliteEdgeQueue(os.environ["EDGE_QUEUE_CANDIDATE"])'
+sqlite3 /opt/dataops-edge/queue-migration/edge-queue-v2-candidate.sqlite3 \
+  'PRAGMA quick_check; PRAGMA foreign_key_check; PRAGMA user_version;'
+```
+
+验收必须依次返回 `ok`、无 foreign-key 行、`2`;随后在候选副本执行幂等 replay、
+cancel、outcome/event ACK 用例,停旧容器后才原子改名为
+`edge-queue-v2.sqlite3` 并启动新 verified release。复制迁移期间禁止写 v1;不得直接
+在唯一副本原地迁移。失败时停止候选进程、保留失败摘要,删除候选文件并从 SHA-256
+已核对的 v1 drained 备份恢复;旧 verified release 只读该恢复文件。
+
+## 9. 备份、恢复与事故
+
+- 备份:queue、verified releases、public signing keys、非秘密配置和证书链;credential/private key 使用企业秘密/密钥备份制度,不进入普通备份。
+- 恢复:新主机先重建 0700/0600 权限,验证证书/CRL/签名公钥,再恢复 queue;先 reconcile 后启动执行。
+- 事故:身份失败、CRL 命中、原始数据出域尝试、签名失败、replay conflict、queue 损坏或 rollback 失败均停止对应流,保存 digest/计数/时间/版本证据,联系网络、安全和平台负责人。
+- 验收关闭:工程证据不替代企业网络抓包、批准来源、数据边界审计和 security/legal 签字;这些仍为 `TBD_EXTERNAL`。
+
+### 9.1 retention、legal hold 与 artifact cleanup
+
+清理前由数据负责人提供 retention policy digest;存在 legal hold 的 task/artifact 不得
+进入 deleting。先用只读查询导出候选 digest(不导出 artifact_ref),审批后由 agent
+正常 cleanup worker 执行;禁止用 `rm` 绕过状态机:
+
+```bash
+sqlite3 -readonly /opt/dataops-edge/queue/edge-queue-v2.sqlite3 \
+  "SELECT artifact_digest,classification,retention_until,cleanup_status FROM edge_local_artifacts WHERE cleanup_status IN ('pending','failed') AND julianday(retention_until)<=julianday('now') ORDER BY retention_until LIMIT 100;"
+python3 /opt/dataops-platform/scripts/edge_gateway_admin.py status --root /opt/dataops-edge
+```
+
+注意:queue schema v2 没有可由操作者直接修改的 `legal_hold` 列,且不提供人工强制删除
+命令。企业 legal-hold 服务接入、批准记录签名和 cleanup worker 的 hold 消费仍属于
+`security_legal=TBD_EXTERNAL`;接入前一旦出现 hold,必须停止 Agent 以阻止自动 cleanup,
+由数据/法务负责人决定延长 retention 后重新生成批准任务,不能直接改 SQLite 或运行
+`rm`。因此本节是候选只读核对,不是“已完成 legal hold 产品能力”的声明。
+
+每个删除必须形成 canonical delete receipt,至少含 artifact_digest、policy_digest、
+approved_by、deleted_at、result 和 receipt_digest,并由 outbox 发送 ACK;本地表只有
+receipt ACK 后才能保持 deleted。cleanup 中断时,`deleting` lease 到期后由同一
+artifact digest 重试:文件已不存在则生成幂等 delete receipt,不得改写 digest;
+失败转 failed 并保留分类化错误码。恢复验收为磁盘水位下降、receipt 已 ACK、没有
+legal hold 对象被删。legal hold 解除必须有新批准记录,不能直接改 SQLite。
+
+### 9.2 queue 与 release 备份恢复
+
+在停止新 pull、active lease=0 后执行 SQLite 在线一致副本和完整性校验;verified
+release、config、public signing keys、CA/CRL 分别归档并计算摘要:
+
+```bash
+sqlite3 /opt/dataops-edge/queue/edge-queue-v2.sqlite3 \
+  "VACUUM INTO '/opt/dataops-edge/backup/edge-queue-v2.sqlite3';"
+sqlite3 /opt/dataops-edge/backup/edge-queue-v2.sqlite3 \
+  'PRAGMA quick_check; PRAGMA foreign_key_check; PRAGMA user_version;'
+sha256sum /opt/dataops-edge/backup/edge-queue-v2.sqlite3
+tar -czf /opt/dataops-edge/backup/public-state.tar.gz \
+  -C /opt/dataops-edge config signing certificates releases state/rollback-history
+sha256sum /opt/dataops-edge/backup/public-state.tar.gz
+```
+
+恢复到新建的空专用 leaf,先验证两份 SHA-256,再执行 quick_check/foreign_key_check/
+user_version=2、签名公钥和 CRL freshness;恢复 current 指针前重新执行 bundle 验签。
+启动时先 reconcile 后 pull。任一校验失败则保持停机、回退到上一份已验证备份;不得
+在损坏原件上修表。credential/private key 由企业密钥恢复流程单独注入,绝不包含在
+`public-state.tar.gz`。
+
+## 10. PostgreSQL 三身份执行顺序
+
+职责固定:role-init 仅一次性创建/授予角色,需要 DBA/CREATEROLE;migrator 是
+`dataops_edge_evidence_owner` 成员、负责 Alembic/DDL、无 CREATEROLE;runtime 只持
+`dataops_app_runtime` DML/EXECUTE,不是对象 owner。三个连接串写入 root-only `0600`
+环境文件,禁止命令行明文。按顺序执行:
+
+```bash
+set -a
+. /etc/dataops-platform/dataops.env
+set +a
+/opt/dataops-platform/.venv/bin/python -m app.core.edge_gateway.runtime_roles
+unset DB_ROLE_INIT_DATABASE_URL
+MIGRATION_DATABASE_URL="$MIGRATION_DATABASE_URL" \
+  /opt/dataops-platform/.venv/bin/alembic -c /opt/dataops-platform/alembic.ini upgrade head
+MIGRATION_DATABASE_URL="$MIGRATION_DATABASE_URL" \
+  /opt/dataops-platform/.venv/bin/alembic -c /opt/dataops-platform/alembic.ini current
+```
+
+`DB_ROLE_INIT_DATABASE_URL` 只交给一次性 role-init,`MIGRATION_DATABASE_URL` 只交给
+Alembic 子进程,常驻 backend/runner 只获得 `DATABASE_URL`。用于验收的
+`/etc/postgresql/pg_service.conf` 仅写 host/port/dbname/user,密码放 `0600` passfile;
+分别执行 `PGSERVICE=dataops_migrator psql -X` 和
+`PGSERVICE=dataops_runtime psql -X`,不能共用 owner 登录。
+
+`current` 必须为完整 revision `20260809_479 (head)`。随后分别用 migrator/runtime
+会话核验职责(以下 SQL 在各自 `psql` 会话执行,连接由 0600 service/passfile 提供):
+
+```sql
+SELECT current_user,
+       has_database_privilege(current_user,current_database(),'CREATE') AS db_create,
+       has_schema_privilege(current_user,'public','CREATE') AS schema_create,
+       pg_has_role(current_user,'dataops_edge_evidence_owner','member') AS evidence_owner,
+       pg_has_role(current_user,'dataops_app_runtime','member') AS app_runtime;
+SELECT rolcreaterole, rolsuper FROM pg_roles WHERE rolname=current_user;
+```
+
+验收:role-init 完成后 credential 回收;migrator evidence_owner=true、schema_create=true、
+rolcreaterole=false、rolsuper=false;runtime app_runtime=true、schema_create=false、
+rolcreaterole=false、rolsuper=false,且不能写 signing authority。任一步失败停止应用,
+由 DBA 修 GRANT 后从 role-init/migration gate 重跑;禁止给 runtime 临时 owner/superuser。
+
+## 11. 双向 TLS 与 CRL 更新/恢复
+
+控制面 Nginx 用 client CRL 撤销 Edge 客户端;Edge Agent 用 server CRL 验证控制面
+服务端,二者不能互换。安全人员先验证 issuer、签名、thisUpdate/nextUpdate,再在同一
+文件系统写 `.next`、fsync 并原子 rename 只读源。Agent 每次请求前核对 CRL digest,
+变化时关闭旧连接池并用新 CRL 重建 SSLContext;无需重启即可拒绝新撤销,CRL 过期即
+fail-stop,且 SSLContext 必须保持 `CRL_CHECK_LEAF`:
+
+```bash
+openssl crl -in /opt/dataops-edge/certificates/control-plane-server.crl.pem -noout -issuer -lastupdate -nextupdate
+openssl verify -CAfile /opt/dataops-edge/certificates/ca.pem -crl_check \
+  -CRLfile /opt/dataops-edge/certificates/control-plane-server.crl.pem \
+  /opt/dataops-edge/certificates/control-plane-server.pem
+openssl crl -in /etc/nginx/edge-client.crl.pem -noout -issuer -lastupdate -nextupdate
+install -m 0644 /run/operator/control-plane-server.crl.pem.next \
+  /opt/dataops-edge/certificates/control-plane-server.crl.pem.next
+sync /opt/dataops-edge/certificates/control-plane-server.crl.pem.next
+mv /opt/dataops-edge/certificates/control-plane-server.crl.pem.next \
+  /opt/dataops-edge/certificates/control-plane-server.crl.pem
+install -m 0644 /run/operator/edge-client.crl.pem.next /etc/nginx/edge-client.crl.pem.next
+sync /etc/nginx/edge-client.crl.pem.next
+mv /etc/nginx/edge-client.crl.pem.next /etc/nginx/edge-client.crl.pem
+nginx -t
+nginx -s reload
+```
+
+验收:新 client CRL 中撤销证书无法握手;有效证书仍通过;Edge health 中 CRL
+nextUpdate 新鲜且控制面握手成功;工程测试已证明撤销当前服务端证书后下一次请求立即
+失败,不依赖容器重启。更新前保留一份同样已验签且未过期的 `.previous`;更新失败时
+原子恢复该文件,Nginx 执行 `nginx -t && nginx -s reload`,Edge 在下一次请求自动重建
+连接池;
+若上一份已过期则保持 fail-stop,不能关闭 CRL 检查。企业 CA/CRL 实际轮换、Nginx/
+Agent reload 和抓包证据仍是 `TBD_EXTERNAL`。

+ 115 - 0
docs/validation/P3_WP04_EDGE_GATEWAY_EVIDENCE.md

@@ -0,0 +1,115 @@
+# P3-WP04 边缘网关工程验证证据
+
+验证日期:2026-08-09(Asia/Shanghai)
+
+## 1. 结论和边界
+
+当前结论为 `ENGINEERING_COMPLETE_ENTERPRISE_EDGE_UAT_BLOCKED`。本证据证明本地工程契约和受影响代码的定向测试,不证明生产就绪,不替代企业验收。未执行仓库全量回归;未连接真实企业来源;未使用企业 CA、企业网络区或企业代理;未取得网络/安全/法务签字。
+
+外部输入状态:
+
+| 输入 | 状态 | 未完成证据 |
+|---|---|---|
+| enterprise_source_1 | `TBD_EXTERNAL` | 未提供批准来源、版本、规模、只读账号和样本 |
+| enterprise_source_2 | `TBD_EXTERNAL` | 未提供批准来源、版本、规模、只读账号和样本 |
+| network | `TBD_EXTERNAL` | 未提供企业网络区、exact egress、代理、DNS、CA/CRL 和抓包审批 |
+| security_legal | `TBD_EXTERNAL` | 未完成数据分级/保留、数据不出域和证书/发布流程签字 |
+
+## 2. 已验证批次
+
+所有批次均设置 `PYTHONDONTWRITEBYTECODE=1` 和 `PYTHONPATH=.`,只覆盖 P3-WP04 和直接受影响的 Runner/权限/OpenAPI 契约。
+
+### 批次 A:WP04 工程基线
+
+```text
+TEST_DATABASE_URL=postgresql://dataops:***@127.0.0.1:15432/dataops \
+pytest -q tests/test_phase3_wp04_edge_gateway.py \
+  tests/security/test_phase3_wp04_data_boundary.py \
+  tests/integration/test_phase3_wp04_edge_gateway_postgres.py \
+  tests/integration/test_phase3_wp04_edge_agent.py \
+  tests/integration/test_phase3_wp04_nginx_mtls.py \
+  tests/test_phase3_wp04_production_runtime.py
+```
+
+结果:`241 passed in 14.42s`。数据库 URL 在证据中脱敏;实际目标是本地 Docker PostgreSQL `127.0.0.1:15432/dataops`。
+
+### 批次 B:直接受影响 Runner 与权限
+
+```text
+pytest -q tests/runner/test_api.py tests/runner/test_replay_store.py \
+  tests/runner/test_task_tokens.py tests/runner/test_bootstrap.py \
+  tests/test_permission_matrix.py
+```
+
+结果:`29 passed in 1.25s`。其中 Runner 19 项、权限矩阵 10 项;与批次 A 合计 270 项。Task4 规格整改后再次合并执行批次 A、Runner 和权限矩阵,结果为 `270 passed in 15.20s`,作为本轮最终 WP04 受影响工程基线;未执行仓库全量回归。
+
+### 批次 C:Task4 交付契约
+
+```text
+pytest -q tests/test_phase3_wp04_delivery_contract.py
+```
+
+RED:首次执行 `7 failed`,原因分别为 admin CLI、edge Compose、OpenAPI edge schemas、证据/运行手册尚不存在以及生成文件不一致。规格复核补充信任链要求后,新测试批次为 `9 failed, 2 passed`,准确暴露了 rollback 未绑定受控 bundle、runtime UID/GID/key 路径、签名撤销 envelope、OpenAPI mTLS/schema 和发布归档缺口。增加 tmpfs UID/GID 复用、过期/伪签名撤销和归档链接检查时,RED 为 `10 passed, 1 failed`,唯一失败是 tmpfs 仍硬编码 UID/GID;最终审计又用一个有效重签但 status 改变的同 release-id manifest 发现 replay digest 过窄,定向 RED 为 `1 failed, 10 deselected`,随后改为完整 manifest digest 绑定。第二轮复核用已有 history 加异内容 stale intent 得到 `1 failed, 10 deselected`,暴露 history 快速返回前未核验/清理 intent,随后补上严格等值验证、unlink 和 state 目录 fsync。
+
+质量复核继续增加宽根拒绝、跨进程 fcntl lock、same-fd copy/hash、hardlink/竞态拒绝、正式三段版本、失败临时 bundle 清理、runtime parent owner 继承、真实 Edge 镜像、闭合 config schema、读者命令和归档摘要闭包。CLI 安全组先得到 `8 passed, 11 deselected`,release/rollback 安全组得到 `9 passed, 10 deselected`;控制面版本策略在 RED 中发现 6 个过宽版本全部被接受,修复后 `6 passed`。
+
+最终交付契约与真实 Edge 运行时合跑:`33 passed in 17.12s`。已覆盖 init/CSR 的 runtime owner 与 0600/0700、真实容器 UID 读取只读 `secrets/client.key`、证书有效期/clientAuth/key-CSR-cert/CA/CRL 注册校验、固定注册材料 exact replay/conflict、幂等 pending 轮换、canonical Ed25519 撤销状态的过期/跨网关/伪签名拒绝与本地终态、发布受控 bundle、完整 manifest 重验签、同 release-id 任何完整 manifest 变化拒绝、artifact 验后篡改、错误 current/target、history 与 matching/mismatched stale intent、unlink/directory-fsync 故障恢复及下一次合法回滚、路径/符号链接/hardlink/宽根策略、正式三段版本和失败临时目录清理;同时覆盖 pull-only Compose、可构建发布归档、OpenAPI human bearer 与 machine mTLS+credential、大小写敏感键和最大递归深度的可执行 JSON Schema、闭合 edge config schema、可执行读者手册、源/发布 CLI 精确副本,以及归档内 OpenAPI/Failure JSON/SHA256/二进制与文本秘密扫描。
+
+### 批次 D:真实 Edge 镜像与运行时
+
+```text
+pytest -q tests/integration/test_phase3_wp04_edge_runtime.py
+```
+
+普通受限沙箱首次得到 `1 failed, 1 passed`,失败只发生在 Docker buildx 写
+`~/.docker/buildx/activity` 时被宿主沙箱拒绝,镜像代码尚未执行。允许 Docker 使用其
+本机活动目录后最终结果纳入批次 C 的 33 项:实际构建 `Dockerfile.edge`,临时本地
+CA/server/client/新鲜 server CRL 与 mTLS fake control 完成 heartbeat、reconcile,接收
+Ed25519 签名且精确绑定的工程自检任务,经封闭 Runner 形成有界 `evidence` 事件并取得
+exact ACK;在线替换 CRL 并撤销当前服务端证书后,下一次连接在不重启容器的情况下
+立即失败并进入 degraded。无入站 port、日志/health 无 credential,SIGTERM 干净退出;
+另验证 `CRL_CHECK_LEAF` 与 stale CRL fail-stop。
+未连接真实企业来源、企业 CA 或企业网络。
+
+### 批次 E:OpenAPI、状态台账和静态门禁
+
+- delivery 与真实 Edge runtime:`33 passed in 17.12s`;OpenAPI schema/安全组合定向已包含其中;生成器生成 444 operations 且与 `docs/architecture/OPENAPI.yaml` byte parity。
+- P3-WP00 requirements 受影响语义:`11 passed, 1 deselected in 1.99s`;未选择项是会比较被 Git 忽略的既有 `__pycache__` 字节码的全目录测试,WP04 源码 parity 已用排除 `__pycache__` 的递归比较独立通过。
+- 非 Docker 的 WP04、Agent、Runner、权限直接受影响批次:`229 passed in 5.30s`;真实 PostgreSQL 控制面批次:`36 passed in 8.32s`;与 delivery/runtime 33 项合计 `298 passed`。未执行仓库全量回归。
+- Ruff:CLI、OpenAPI generator、Edge runtime/bootstrap/transport/agent、control-plane service、delivery/runtime tests 及 deployment mirrors 全部通过。
+- `compileall`:使用 `/private/tmp/dataops-wp04-pyc` cache 前缀通过;没有向源码目录新增字节码。
+- JSON/YAML:requirements、failure injection、OpenAPI、edge Compose 均解析通过。
+- Compose:使用无秘密 `edge.env.example`、显式 runtime UID/GID 和 exact HTTPS origin 后,`docker compose config --quiet` 通过;真实 `nginx:alpine` 容器以配置 UID/GID 读取 CLI 生成的只读 client key 通过;真实 Edge image smoke 见批次 D,但未启动真实企业源。
+- Migration:真实本地 PostgreSQL `alembic current` 返回 `20260809_479 (head)`。
+- `app/`/`deployment/app/` 的 WP04 源文件与 migration 477/478/479 递归 parity 通过(排除运行时 `__pycache__`)。
+- `git diff --check`、发布脚本 shell syntax、OpenAPI byte parity、发布归档 SHA-256 closure 和二进制/文本 secret scan 通过。扫描器拒绝 encrypted private key、PKCS#12、二进制载荷和明文 credential,并排除 `__pycache__`/字节码。测试中的私钥标记仅为拒绝规则夹具,不是秘密材料。
+
+## 3. 关键真实/模拟层次
+
+- PostgreSQL:真实本地 Docker PostgreSQL,Alembic `20260809_479`,验证 migration 升降级、约束、并发、应用/证据 owner/runtime role 隔离和 signing authority 最小权限。不是企业数据库。
+- Nginx mTLS:使用本机已有 `nginx:alpine` 镜像、临时本地 CA/服务端/有效/错误/撤销客户端证书和真实 CRL,实际 TLS handshake 验证无证书、错 CA、伪造 header、有效证书及 revoked 拒绝。不是企业 CA/CRL。
+- 镜像能力:定向检查 backend migrator/runtime 与 runner 镜像构建契约、Compose target 和 migration/runtime credential 隔离;没有发布新生产镜像。
+- Runner:真实本地 Runner API、bootstrap、task token、replay store 受影响契约;企业来源 handler 尚未接入。
+- Edge agent:真实 disk SQLite、签名任务、策略、断网队列、reconcile、cancel、event ACK、artifact retention 与发布回滚的本地执行;网络 transport 为受控本地测试。
+- Edge runtime:真实构建的 Edge 镜像实际运行 pull-only heartbeat/reconcile/pull 循环,接收一个 Ed25519 签名且精确绑定的 `quality-evaluation` 工程自检任务,经封闭 Runner 形成有界 `evidence` 事件并取得 exact ACK;其他来源任务仍默认拒绝。使用临时 mTLS 控制面和 server CRL,在线替换 CRL 并撤销当前服务端证书后下一次连接立即失败,证明 TLS 连接池已热重建;断网降级与 SIGTERM 已验证。企业来源 Runner handler 仍未配置,属于 `TBD_EXTERNAL`,本地 smoke 不把工程自检伪装为已接企业来源。
+- Admin CLI:真实生成 3072-bit RSA private key/CSR,0600/0700、runtime UID/GID owner 校验、fsync+rename、证书/CSR/key/CA/CRL 匹配、Ed25519 revoke/release authority、受控 verified bundle 和 artifact SHA-256;故障注入覆盖 current 指针替换后的 directory-fsync 失败及幂等恢复,测试材料均为临时生成。
+
+## 4. 已证明的安全属性
+
+1. 原始行、近期明细、restricted、credential、private key、SQL、任意 URL/命令/路径在出站边界默认拒绝,拒绝内容不持久化到控制面。
+2. enrollment 失败绑定不消耗;token/credential 只以 hash 入库;generation/certificate/environment/zone/gateway 精确绑定;rotate/revoke 后旧身份失败。
+3. stable event ID 的 exact replay 返回同一 ACK;变更 digest 冲突;task/event/cancel/revoke 并发由数据库锁与 lease fencing 线性化。
+4. 任务和发布完整 Ed25519 authority 绑定 key-id、用途、策略、环境、网络区、deadline、artifact digest 和 rollback version;release-check 将 manifest/artifact 原子复制到受控 bundle,rollback 重新验证 current/target 签名与两个 artifact hash 后才提交指针;伪 verified JSON、验后篡改、过期、降级、replay change、路径穿越和符号链接失败。
+5. Compose 无入站 control port,non-root、read-only、cap-drop ALL、no-new-privileges、CPU/内存/PID/日志上限和最小 RO/RW mounts。
+6. 诊断、心跳、日志和 API 错误为有界安全摘要;响应 `Cache-Control: no-store`。
+
+## 5. 运行、故障和恢复证据索引
+
+- 安装、CSR、注册、证书轮换/撤销、CRL、升级、回滚、v1→v2 drain gate、诊断、备份和恢复:`docs/runbooks/EDGE_GATEWAY_OPERATIONS.md`。
+- 数据边界、身份、签名和 retention 架构:`docs/phase3/P3_WP04_EDGE_GATEWAY.md`。
+- 每个故障注入的前置、步骤、安全边界、预期、证据、rollback 和状态:`docs/validation/P3_WP04_FAILURE_INJECTION.json`。
+- OpenAPI:`docs/architecture/OPENAPI.yaml`,由 `scripts/generate_openapi.py` 再生。
+
+## 6. 尚未满足的企业完成门禁
+
+必须在批准企业环境完成:两个真实来源的采集/画像/质量/血缘/受控查询;断网续跑和取消;真实网络抓包和出站网关审计;原始数据未经批准出域事件为 0;企业 CA/CRL 的注册、轮换和撤销;queue 备份恢复与升级/回滚演练;网络负责人和安全法务负责人签字。完成前,本工作包保持 `ENGINEERING_COMPLETE_ENTERPRISE_EDGE_UAT_BLOCKED`。

+ 107 - 0
docs/validation/P3_WP04_FAILURE_INJECTION.json

@@ -0,0 +1,107 @@
+{
+  "work_package": "P3-WP04",
+  "engineering_status": "ENGINEERING_COMPLETE_ENTERPRISE_EDGE_UAT_BLOCKED",
+  "generated_at": "2026-08-09",
+  "scope_boundary": "Local engineering verification only; no approved enterprise source or enterprise network UAT was executed.",
+  "cases": [
+    {
+      "case_id": "WP04-FI-001",
+      "scope": "contract and egress policy",
+      "preconditions": ["Local Python test environment", "No enterprise source connection"],
+      "steps": ["Submit raw rows, recent detail, credentials, private key markers, obfuscated SQL and oversized/cyclic payloads to contract/policy boundaries"],
+      "safety_boundary": "Synthetic values only; no real secret or source record",
+      "expected": "Default deny before persistence or network egress",
+      "evidence": ["tests/test_phase3_wp04_edge_gateway.py", "tests/security/test_phase3_wp04_data_boundary.py"],
+      "rollback": "No state mutation; remove temporary SQLite directories",
+      "status": "VERIFIED_LOCAL"
+    },
+    {
+      "case_id": "WP04-FI-002",
+      "scope": "offline queue, lease and replay",
+      "preconditions": ["Temporary disk-backed SQLite queue"],
+      "steps": ["Interrupt delivery", "Recover stale lease", "Replay same event", "Replay changed digest", "Race cancellation and completion"],
+      "safety_boundary": "Temporary local queue; synthetic task and event contracts",
+      "expected": "No lost accepted event; same digest returns prior ACK; changed digest conflicts; stale worker is fenced",
+      "evidence": ["tests/test_phase3_wp04_edge_gateway.py", "tests/integration/test_phase3_wp04_edge_agent.py"],
+      "rollback": "Close queue and remove temporary files",
+      "status": "VERIFIED_LOCAL"
+    },
+    {
+      "case_id": "WP04-FI-003",
+      "scope": "PostgreSQL identity and signing authority",
+      "preconditions": ["Local Docker PostgreSQL on 127.0.0.1:15432", "Alembic head 20260809_479"],
+      "steps": ["Replay and alter enrollment binding", "Rotate and revoke credentials", "Attempt app-role signing-key mutation", "Race task pull/event ACK/release transitions"],
+      "safety_boundary": "Local test database only; test rows are deleted or transactionally isolated",
+      "expected": "Hashes only at rest; failed binding does not consume enrollment; old/revoked credential fails; application role cannot mutate signing authority",
+      "evidence": ["tests/integration/test_phase3_wp04_edge_gateway_postgres.py", "tests/test_phase3_wp04_production_runtime.py"],
+      "rollback": "Downgrade/upgrade migration test restores head 479; dispose test connections",
+      "status": "VERIFIED_LOCAL"
+    },
+    {
+      "case_id": "WP04-FI-004",
+      "scope": "mTLS proxy, CA and CRL",
+      "preconditions": ["Temporary local CA/server/client certificates", "Local Nginx container image"],
+      "steps": ["Connect without certificate", "Connect with untrusted certificate", "Connect with valid certificate", "Revoke certificate and reload CRL"],
+      "safety_boundary": "Ephemeral CA and localhost-only test topology; no enterprise CA",
+      "expected": "Only valid non-revoked client reaches upstream with trusted proof headers; CRL blocks revoked client",
+      "evidence": ["tests/integration/test_phase3_wp04_nginx_mtls.py"],
+      "rollback": "Stop temporary containers and delete ephemeral certificates/network",
+      "status": "VERIFIED_LOCAL"
+    },
+    {
+      "case_id": "WP04-FI-005",
+      "scope": "signed release and local administration",
+      "preconditions": ["Temporary edge root", "Ephemeral Ed25519 release key"],
+      "steps": ["Create local CSR under the configured runtime UID/GID", "Validate clientAuth certificate, CA chain and CRL before writing fixed registration material", "Reject expired, cross-gateway and forged signed revocation envelopes", "Copy a valid signed artifact into a controlled verified bundle", "Forge a bundle manifest and alter an artifact after verification", "Inject directory fsync failure after current pointer replacement and retry rollback", "Replay completed rollback with matching and mismatched stale intent", "Inject stale-intent unlink and state-directory fsync failures, retry, then execute the next valid rollback", "Attempt symlink escape, wrong current/target digest, downgrade and changed replay"],
+      "safety_boundary": "Temporary local files; generated test keys only",
+      "expected": "Private key remains 0600 and undisclosed/readable only by the configured runtime identity; registration is secret-free and idempotent; revocation authority, both rollback manifests and both artifact hashes are reverified; tamper, replay change, wrong binding, downgrade and path escape fail closed; a post-pointer fsync interruption is recovered deterministically without reporting an uncommitted state",
+      "evidence": ["tests/test_phase3_wp04_delivery_contract.py", "tests/integration/test_phase3_wp04_edge_agent.py"],
+      "rollback": "Remove temporary root; no installed release is changed",
+      "status": "VERIFIED_LOCAL"
+    },
+    {
+      "case_id": "WP04-FI-006",
+      "scope": "runnable pull-only edge image and server CRL",
+      "preconditions": ["Local Docker Engine", "Ephemeral local mTLS control plane", "Temporary CA, server/client certificates and fresh server CRL"],
+      "steps": ["Build the repository Edge Dockerfile", "Start the container without published ports", "Observe heartbeat, reconcile and empty task pull over mTLS", "Stop the local control plane and observe degraded health without process exit", "Send SIGTERM and verify clean stop", "Validate stale server CRL rejection and CRL_CHECK_LEAF configuration"],
+      "safety_boundary": "Temporary local certificates, credential and localhost/host.docker.internal test control only; no enterprise source, CA or network",
+      "expected": "The image runs a pull-only loop with no inbound port; valid mTLS reaches only the exact origin; disconnect degrades without leaking secrets; stale/invalid server CRL fails closed; SIGTERM stops cleanly",
+      "evidence": ["tests/integration/test_phase3_wp04_edge_runtime.py"],
+      "rollback": "Stop the temporary container/control server and remove the temporary certificate and queue directories",
+      "status": "VERIFIED_LOCAL"
+    },
+    {
+      "case_id": "WP04-FI-101",
+      "scope": "approved enterprise sources",
+      "preconditions": ["enterprise_source_1", "enterprise_source_2", "Approved read-only identities and samples"],
+      "steps": ["Run collect, profile, quality, lineage and controlled query on both approved sources", "Interrupt and resume"],
+      "safety_boundary": "Enterprise approval and maintenance window required",
+      "expected": "Source tasks complete without data leaving the approved boundary",
+      "evidence": [],
+      "rollback": "Stop edge tasks and revoke source sessions according to enterprise procedure",
+      "status": "TBD_EXTERNAL"
+    },
+    {
+      "case_id": "WP04-FI-102",
+      "scope": "enterprise network data boundary",
+      "preconditions": ["network", "Approved network zone, firewall, proxy, DNS and packet-capture plan"],
+      "steps": ["Deploy pull-only edge", "Capture normal, blocked and reconnect traffic", "Attempt non-allowlisted destination"],
+      "safety_boundary": "Network owner approval required; no production disruption",
+      "expected": "Exact approved destinations only; unapproved raw-data egress events equal zero",
+      "evidence": [],
+      "rollback": "Disable edge egress rule and stop container",
+      "status": "TBD_EXTERNAL"
+    },
+    {
+      "case_id": "WP04-FI-103",
+      "scope": "enterprise security and legal acceptance",
+      "preconditions": ["security_legal", "Approved classification, retention and evidence map"],
+      "steps": ["Review data classification and retention", "Witness certificate revoke/rotate", "Witness backup/restore and release rollback", "Sign boundary evidence"],
+      "safety_boundary": "Authorized reviewers and approved evidence handling only",
+      "expected": "Security, network and legal owners sign the enterprise data-boundary evidence",
+      "evidence": [],
+      "rollback": "Record rejection and remediation; do not promote to production",
+      "status": "TBD_EXTERNAL"
+    }
+  ]
+}

+ 11 - 1
env.example

@@ -10,6 +10,11 @@ PORT=8000
 
 # 数据库配置
 DATABASE_URL=postgresql://username:password@localhost:5432/database_name
+DB_ROLE_INIT_DATABASE_URL=postgresql://role_admin:replace-password@localhost:5432/database_name
+MIGRATION_DATABASE_URL=postgresql://migration_owner:replace-password@localhost:5432/database_name
+DATAOPS_MIGRATOR_USER=dataops_migrator
+DATAOPS_RUNTIME_USER=dataops_app
+DATAOPS_RUNTIME_PASSWORD=replace-with-runtime-password
 # 或者使用SQLite
 # DATABASE_URL=sqlite:///./app.db
 
@@ -49,6 +54,9 @@ KNOWLEDGE_LIGHTRAG_ENABLED=false
 KNOWLEDGE_LIGHTRAG_SHADOW_ONLY=true
 KNOWLEDGE_LIGHTRAG_BASE_URL=http://lightrag:9621
 KNOWLEDGE_LIGHTRAG_API_KEY=
+
+# Exact reverse-proxy IPs allowed to supply verified edge mTLS certificate data.
+EDGE_MTLS_TRUSTED_PROXY_IPS=127.0.0.1
 QWEN_EMBEDDING_BASE_URL=
 QWEN_EMBEDDING_API_KEY=
 QWEN_EMBEDDING_MODEL=text-embedding-v3
@@ -62,4 +70,6 @@ SMTP_HOST=smtp.gmail.com
 SMTP_PORT=587
 SMTP_USERNAME=your-email@gmail.com
 SMTP_PASSWORD=your-app-password
-
+EDGE_GATEWAY_SIGNING_PRIVATE_KEY_FILE=/run/secrets/edge_gateway_signing_private_key
+EDGE_GATEWAY_SIGNING_PUBLIC_KEY=
+EDGE_GATEWAY_SIGNING_KEY_ID=

+ 5 - 4
migrations/env.py

@@ -6,17 +6,18 @@ from logging.config import fileConfig
 from alembic import context
 from sqlalchemy import engine_from_config, pool
 
-
 config = context.config
 if config.config_file_name is not None:
     fileConfig(config.config_file_name)
 
-database_url = os.environ.get("SQLALCHEMY_DATABASE_URI") or os.environ.get(
-    "DATABASE_URL"
+database_url = (
+    os.environ.get("MIGRATION_DATABASE_URL")
+    or os.environ.get("SQLALCHEMY_DATABASE_URI")
+    or os.environ.get("DATABASE_URL")
 )
 if not database_url:
     raise RuntimeError(
-        "SQLALCHEMY_DATABASE_URI or DATABASE_URL must be configured for migrations"
+        "MIGRATION_DATABASE_URL must be configured for migrations"
     )
 
 # ConfigParser treats percent signs as interpolation markers.

+ 532 - 0
migrations/versions/20260809_477_edge_gateway_control_plane.py

@@ -0,0 +1,532 @@
+"""Enterprise edge-gateway control-plane identity and durable ledgers."""
+
+from alembic import op
+
+revision = "20260809_477"
+down_revision = "20260802_476"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+    op.execute(r"""
+    DO $$
+    BEGIN
+      IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='dataops_app_runtime') OR
+         NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='dataops_edge_evidence_owner') THEN
+        RAISE EXCEPTION 'edge database roles are not pre-provisioned; run the privileged role-init step';
+      END IF;
+      IF EXISTS (
+        SELECT 1 FROM pg_roles
+         WHERE rolname IN ('dataops_app_runtime','dataops_edge_evidence_owner')
+           AND (rolcanlogin OR rolsuper OR rolcreatedb OR rolcreaterole OR rolreplication)
+      ) THEN
+        RAISE EXCEPTION 'edge database roles violate the NOLOGIN least-privilege contract';
+      END IF;
+    END $$;
+    CREATE OR REPLACE FUNCTION public.edge_hosts_valid(hosts TEXT[], require_nonempty BOOLEAN)
+    RETURNS BOOLEAN LANGUAGE plpgsql IMMUTABLE STRICT AS $$
+    DECLARE host TEXT;
+    BEGIN
+      IF cardinality(hosts)>32 OR (require_nonempty AND cardinality(hosts)=0) OR
+         cardinality(hosts)<>(SELECT count(DISTINCT item) FROM unnest(hosts) item) THEN
+        RETURN FALSE;
+      END IF;
+      FOREACH host IN ARRAY hosts LOOP
+        IF host<>lower(host) OR host !~ '^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$' OR
+           length(host)>253 OR host LIKE '%..%' OR host LIKE '%:%' OR
+           host LIKE '%/%' OR host LIKE '%@%' THEN
+          RETURN FALSE;
+        END IF;
+      END LOOP;
+      RETURN TRUE;
+    END $$;
+    CREATE TABLE public.edge_gateway_enrollments (
+      uid UUID PRIMARY KEY, gateway_id UUID NOT NULL UNIQUE,
+      gateway_name VARCHAR(200) NOT NULL, environment VARCHAR(20) NOT NULL,
+      network_zone VARCHAR(120) NOT NULL, policy_digest CHAR(64) NOT NULL,
+      expected_certificate_sha256 CHAR(64) NOT NULL,
+      allowed_control_hosts TEXT[] NOT NULL, allowed_proxy_hosts TEXT[] NOT NULL,
+      token_hash CHAR(64) NOT NULL UNIQUE, status VARCHAR(20) NOT NULL DEFAULT 'pending',
+      expires_at TIMESTAMPTZ NOT NULL, consumed_at TIMESTAMPTZ,
+      created_by UUID NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
+      CONSTRAINT ck_edge_enrollment_environment CHECK (environment IN ('development','staging','production')),
+      CONSTRAINT ck_edge_enrollment_status CHECK (status IN ('pending','consumed','revoked','expired')),
+      CONSTRAINT ck_edge_enrollment_digest CHECK (
+        policy_digest ~ '^[0-9a-f]{64}$' AND token_hash ~ '^[0-9a-f]{64}$' AND
+        expected_certificate_sha256 ~ '^[0-9a-f]{64}$'),
+      CONSTRAINT ck_edge_enrollment_ttl CHECK (expires_at > created_at AND expires_at <= created_at + INTERVAL '24 hours'),
+      CONSTRAINT ck_edge_enrollment_consumed CHECK ((status='consumed') = (consumed_at IS NOT NULL)),
+      CONSTRAINT ck_edge_enrollment_hosts CHECK (
+        public.edge_hosts_valid(allowed_control_hosts,TRUE) AND
+        public.edge_hosts_valid(allowed_proxy_hosts,FALSE))
+    );
+    CREATE INDEX ix_edge_enrollment_pending ON public.edge_gateway_enrollments(token_hash,expires_at) WHERE status='pending';
+
+    CREATE OR REPLACE FUNCTION public.protect_edge_enrollment_identity()
+    RETURNS trigger LANGUAGE plpgsql AS $$
+    BEGIN
+      IF NEW.uid<>OLD.uid OR NEW.gateway_id<>OLD.gateway_id OR
+         NEW.gateway_name<>OLD.gateway_name OR NEW.environment<>OLD.environment OR
+         NEW.network_zone<>OLD.network_zone OR NEW.policy_digest<>OLD.policy_digest OR
+         NEW.expected_certificate_sha256<>OLD.expected_certificate_sha256 OR
+         NEW.allowed_control_hosts<>OLD.allowed_control_hosts OR
+         NEW.allowed_proxy_hosts<>OLD.allowed_proxy_hosts OR NEW.token_hash<>OLD.token_hash OR
+         NEW.expires_at<>OLD.expires_at OR NEW.created_by<>OLD.created_by OR
+         NEW.created_at<>OLD.created_at THEN
+        RAISE EXCEPTION 'edge enrollment identity binding is immutable';
+      END IF;
+      IF NOT (
+        (OLD.status='pending' AND NEW.status IN ('pending','consumed','revoked','expired')) OR
+        (OLD.status<>'pending' AND NEW.status=OLD.status)
+      ) THEN
+        RAISE EXCEPTION 'edge enrollment status transition is invalid';
+      END IF;
+      RETURN NEW;
+    END $$;
+    CREATE TRIGGER trg_edge_enrollment_identity_immutable
+      BEFORE UPDATE ON public.edge_gateway_enrollments FOR EACH ROW
+      EXECUTE FUNCTION public.protect_edge_enrollment_identity();
+
+    CREATE TABLE public.edge_gateways (
+      uid UUID PRIMARY KEY, name VARCHAR(200) NOT NULL, environment VARCHAR(20) NOT NULL,
+      network_zone VARCHAR(120) NOT NULL, status VARCHAR(20) NOT NULL,
+      current_generation INTEGER NOT NULL, policy_digest CHAR(64) NOT NULL,
+      allowed_control_hosts TEXT[] NOT NULL, allowed_proxy_hosts TEXT[] NOT NULL,
+      last_heartbeat_at TIMESTAMPTZ, runtime_version VARCHAR(80),
+      safe_health_summary JSONB NOT NULL DEFAULT '{}'::jsonb,
+      created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
+      updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, revoked_at TIMESTAMPTZ,
+      CONSTRAINT ck_edge_gateway_environment CHECK (environment IN ('development','staging','production')),
+      CONSTRAINT ck_edge_gateway_status CHECK (status IN ('pending','active','offline','revoked')),
+      CONSTRAINT ck_edge_gateway_generation CHECK (current_generation > 0),
+      CONSTRAINT ck_edge_gateway_policy CHECK (policy_digest ~ '^[0-9a-f]{64}$'),
+      CONSTRAINT ck_edge_gateway_summary CHECK (jsonb_typeof(safe_health_summary)='object'),
+      CONSTRAINT ck_edge_gateway_hosts CHECK (
+        public.edge_hosts_valid(allowed_control_hosts,TRUE) AND
+        public.edge_hosts_valid(allowed_proxy_hosts,FALSE)),
+      CONSTRAINT fk_edge_gateway_enrollment FOREIGN KEY (uid)
+        REFERENCES public.edge_gateway_enrollments(gateway_id)
+    );
+    CREATE INDEX ix_edge_gateways_environment_status ON public.edge_gateways(environment,status);
+
+    CREATE TABLE public.edge_gateway_credentials (
+      uid UUID PRIMARY KEY, gateway_id UUID NOT NULL REFERENCES public.edge_gateways(uid),
+      generation INTEGER NOT NULL, credential_hash CHAR(64) NOT NULL UNIQUE,
+      certificate_sha256 CHAR(64) NOT NULL, status VARCHAR(20) NOT NULL,
+      expires_at TIMESTAMPTZ NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
+      revoked_at TIMESTAMPTZ, rotated_from_uid UUID REFERENCES public.edge_gateway_credentials(uid),
+      CONSTRAINT uq_edge_credential_generation UNIQUE(gateway_id,generation),
+      CONSTRAINT ck_edge_credential_status CHECK (status IN ('active','rotated','revoked','expired')),
+      CONSTRAINT ck_edge_credential_generation CHECK (generation > 0),
+      CONSTRAINT ck_edge_credential_hashes CHECK (credential_hash ~ '^[0-9a-f]{64}$' AND certificate_sha256 ~ '^[0-9a-f]{64}$'),
+      CONSTRAINT ck_edge_credential_expiry CHECK (expires_at > created_at AND expires_at <= created_at + INTERVAL '370 days')
+    );
+    CREATE INDEX ix_edge_credential_auth ON public.edge_gateway_credentials(credential_hash,status,expires_at);
+    CREATE UNIQUE INDEX uq_edge_one_active_credential
+      ON public.edge_gateway_credentials(gateway_id) WHERE status='active';
+    CREATE TABLE public.edge_gateway_rotation_requests (
+      gateway_id UUID NOT NULL REFERENCES public.edge_gateways(uid),
+      request_id VARCHAR(255) NOT NULL, request_digest CHAR(64) NOT NULL,
+      generation INTEGER NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
+      PRIMARY KEY(gateway_id,request_id),
+      CONSTRAINT ck_edge_rotation_digest CHECK (request_digest ~ '^[0-9a-f]{64}$')
+    );
+
+    CREATE OR REPLACE FUNCTION public.protect_edge_gateway_identity()
+    RETURNS trigger LANGUAGE plpgsql AS $$
+    BEGIN
+      IF NEW.uid<>OLD.uid OR NEW.name<>OLD.name OR NEW.environment<>OLD.environment OR
+         NEW.network_zone<>OLD.network_zone OR NEW.policy_digest<>OLD.policy_digest OR
+         NEW.allowed_control_hosts<>OLD.allowed_control_hosts OR
+         NEW.allowed_proxy_hosts<>OLD.allowed_proxy_hosts THEN
+        RAISE EXCEPTION 'registered edge gateway identity binding is immutable';
+      END IF;
+      IF OLD.status='revoked' AND NEW.status<>'revoked' THEN
+        RAISE EXCEPTION 'revoked edge gateway is terminal';
+      END IF;
+      IF NEW.current_generation<OLD.current_generation OR
+         NEW.current_generation>OLD.current_generation+1 THEN
+        RAISE EXCEPTION 'edge gateway generation transition is invalid';
+      END IF;
+      IF NOT (
+        (OLD.status='pending' AND NEW.status IN ('pending','active','revoked')) OR
+        (OLD.status='active' AND NEW.status IN ('active','offline','revoked')) OR
+        (OLD.status='offline' AND NEW.status IN ('active','offline','revoked')) OR
+        (OLD.status='revoked' AND NEW.status='revoked')
+      ) THEN
+        RAISE EXCEPTION 'edge gateway status transition is invalid';
+      END IF;
+      RETURN NEW;
+    END $$;
+    CREATE TRIGGER trg_edge_gateway_identity_immutable
+      BEFORE UPDATE ON public.edge_gateways FOR EACH ROW
+      EXECUTE FUNCTION public.protect_edge_gateway_identity();
+
+    CREATE OR REPLACE FUNCTION public.protect_edge_credential_identity()
+    RETURNS trigger LANGUAGE plpgsql AS $$
+    BEGIN
+      IF NEW.uid<>OLD.uid OR NEW.gateway_id<>OLD.gateway_id OR
+         NEW.generation<>OLD.generation OR NEW.credential_hash<>OLD.credential_hash OR
+         NEW.certificate_sha256<>OLD.certificate_sha256 OR
+         NEW.created_at<>OLD.created_at OR NEW.expires_at<>OLD.expires_at THEN
+        RAISE EXCEPTION 'edge credential identity binding is immutable';
+      END IF;
+      IF NOT (
+        (OLD.status='active' AND NEW.status IN ('active','rotated','revoked','expired')) OR
+        (OLD.status<>'active' AND NEW.status=OLD.status)
+      ) THEN
+        RAISE EXCEPTION 'edge credential status transition is invalid';
+      END IF;
+      RETURN NEW;
+    END $$;
+    CREATE TRIGGER trg_edge_credential_identity_immutable
+      BEFORE UPDATE ON public.edge_gateway_credentials FOR EACH ROW
+      EXECUTE FUNCTION public.protect_edge_credential_identity();
+
+    CREATE OR REPLACE FUNCTION public.validate_edge_gateway_binding(p_gateway UUID)
+    RETURNS void LANGUAGE plpgsql AS $$
+    DECLARE g public.edge_gateways%ROWTYPE;
+    BEGIN
+      SELECT * INTO g FROM public.edge_gateways WHERE uid=p_gateway;
+      IF NOT FOUND OR g.status='revoked' THEN RETURN; END IF;
+      IF g.status IN ('active','offline') AND NOT EXISTS (
+        SELECT 1 FROM public.edge_gateway_credentials c
+         WHERE c.gateway_id=g.uid AND c.generation=g.current_generation
+           AND c.status='active' AND c.expires_at>CURRENT_TIMESTAMP
+      ) THEN
+        RAISE EXCEPTION 'active edge gateway requires its exact active credential binding';
+      END IF;
+      IF g.status IN ('active','offline') AND NOT EXISTS (
+        SELECT 1 FROM public.edge_gateway_enrollments e
+        JOIN public.edge_gateway_credentials c ON c.gateway_id=g.uid AND c.generation=1
+         WHERE e.gateway_id=g.uid AND e.status='consumed'
+           AND e.gateway_name=g.name AND e.environment=g.environment
+           AND e.network_zone=g.network_zone AND e.policy_digest=g.policy_digest
+           AND e.allowed_control_hosts=g.allowed_control_hosts
+           AND e.allowed_proxy_hosts=g.allowed_proxy_hosts
+           AND e.expected_certificate_sha256=c.certificate_sha256
+      ) THEN
+        RAISE EXCEPTION 'active edge gateway does not match its consumed enrollment binding';
+      END IF;
+      IF EXISTS (
+        SELECT 1 FROM public.edge_gateway_credentials c
+        JOIN public.edge_gateway_enrollments e ON e.gateway_id=c.gateway_id
+         WHERE c.gateway_id=g.uid AND c.generation=1
+           AND c.certificate_sha256<>e.expected_certificate_sha256
+      ) THEN
+        RAISE EXCEPTION 'initial edge certificate does not match enrollment binding';
+      END IF;
+    END $$;
+    CREATE OR REPLACE FUNCTION public.enforce_edge_gateway_binding()
+    RETURNS trigger LANGUAGE plpgsql AS $$
+    BEGIN
+      IF TG_OP='UPDATE' AND OLD.status IN ('active','offline') AND NEW.status='pending' THEN
+        RAISE EXCEPTION 'registered edge gateway cannot return to pending status';
+      END IF;
+      PERFORM public.validate_edge_gateway_binding(COALESCE(NEW.uid,OLD.uid));
+      RETURN NULL;
+    END $$;
+    CREATE CONSTRAINT TRIGGER trg_edge_gateway_binding
+      AFTER INSERT OR UPDATE ON public.edge_gateways DEFERRABLE INITIALLY DEFERRED
+      FOR EACH ROW EXECUTE FUNCTION public.enforce_edge_gateway_binding();
+    CREATE OR REPLACE FUNCTION public.enforce_edge_credential_binding()
+    RETURNS trigger LANGUAGE plpgsql AS $$
+    DECLARE target UUID;
+    BEGIN
+      target := COALESCE(NEW.gateway_id,OLD.gateway_id);
+      PERFORM public.validate_edge_gateway_binding(target);
+      IF TG_OP <> 'DELETE' AND NEW.status='active' AND NOT EXISTS (
+        SELECT 1 FROM public.edge_gateways g WHERE g.uid=NEW.gateway_id
+         AND g.status IN ('active','offline') AND g.current_generation=NEW.generation
+      ) THEN
+        RAISE EXCEPTION 'active credential must be the exact current gateway binding';
+      END IF;
+      RETURN NULL;
+    END $$;
+    CREATE CONSTRAINT TRIGGER trg_edge_credential_binding
+      AFTER INSERT OR UPDATE OR DELETE ON public.edge_gateway_credentials
+      DEFERRABLE INITIALLY DEFERRED FOR EACH ROW
+      EXECUTE FUNCTION public.enforce_edge_credential_binding();
+
+    CREATE TABLE public.edge_gateway_tasks (
+      task_id VARCHAR(255) PRIMARY KEY, gateway_id UUID NOT NULL REFERENCES public.edge_gateways(uid),
+      idempotency_key VARCHAR(255) NOT NULL, contract JSONB NOT NULL, contract_digest CHAR(64) NOT NULL,
+      status VARCHAR(24) NOT NULL, lease_token_hash CHAR(64), lease_expires_at TIMESTAMPTZ,
+      cancel_requested_at TIMESTAMPTZ, result_summary JSONB,
+      created_by UUID NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
+      updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
+      CONSTRAINT uq_edge_task_idempotency UNIQUE(gateway_id,idempotency_key),
+      CONSTRAINT uq_edge_task_gateway UNIQUE(task_id,gateway_id),
+      CONSTRAINT ck_edge_task_status CHECK (status IN ('pending','leased','cancel_requested','completed','failed','cancelled')),
+      CONSTRAINT ck_edge_task_contract CHECK (
+        jsonb_typeof(contract)='object' AND contract_digest ~ '^[0-9a-f]{64}$' AND
+        contract->>'task_id'=task_id AND contract->>'gateway_id'=gateway_id::text AND
+        contract->>'idempotency_key'=idempotency_key),
+      CONSTRAINT ck_edge_task_lease CHECK (
+        (status='leased' AND lease_token_hash IS NOT NULL AND lease_expires_at IS NOT NULL) OR
+        (status='cancel_requested' AND ((lease_token_hash IS NULL AND lease_expires_at IS NULL) OR
+          (lease_token_hash IS NOT NULL AND lease_expires_at IS NOT NULL))) OR
+        (status NOT IN ('leased','cancel_requested') AND lease_token_hash IS NULL AND lease_expires_at IS NULL)),
+      CONSTRAINT ck_edge_task_result CHECK (result_summary IS NULL OR jsonb_typeof(result_summary)='object')
+    );
+    CREATE INDEX ix_edge_task_pull ON public.edge_gateway_tasks(gateway_id,status,created_at);
+
+    CREATE TABLE public.edge_gateway_events (
+      event_id VARCHAR(80) PRIMARY KEY, gateway_id UUID NOT NULL REFERENCES public.edge_gateways(uid),
+      task_id VARCHAR(255) NOT NULL,
+      classification VARCHAR(40) NOT NULL, contract JSONB NOT NULL, contract_digest CHAR(64) NOT NULL,
+      ack JSONB NOT NULL, received_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
+      CONSTRAINT ck_edge_event_classification CHECK (classification IN ('desensitized_metadata','statistics','lineage','evidence','health_summary','diagnostic_summary')),
+      CONSTRAINT fk_edge_event_task_gateway FOREIGN KEY (task_id,gateway_id)
+        REFERENCES public.edge_gateway_tasks(task_id,gateway_id),
+      CONSTRAINT ck_edge_event_contract CHECK (
+        jsonb_typeof(contract)='object' AND jsonb_typeof(ack)='object' AND
+        contract_digest ~ '^[0-9a-f]{64}$' AND contract->>'event_id'=event_id AND
+        contract->>'task_id'=task_id AND contract->>'gateway_id'=gateway_id::text AND
+        contract->>'classification'=classification)
+    );
+    CREATE INDEX ix_edge_event_gateway_received ON public.edge_gateway_events(gateway_id,received_at DESC);
+
+    CREATE TABLE public.edge_gateway_releases (
+      uid UUID PRIMARY KEY, gateway_id UUID NOT NULL REFERENCES public.edge_gateways(uid),
+      version VARCHAR(80) NOT NULL, artifact_digest CHAR(64) NOT NULL,
+      rollback_version VARCHAR(80) NOT NULL, status VARCHAR(24) NOT NULL,
+      request_id VARCHAR(255) NOT NULL, request_digest CHAR(64) NOT NULL,
+      safe_summary JSONB NOT NULL DEFAULT '{}'::jsonb, created_by UUID NOT NULL,
+      created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, acknowledged_at TIMESTAMPTZ,
+      CONSTRAINT uq_edge_release_version UNIQUE(gateway_id,version),
+      CONSTRAINT uq_edge_release_request UNIQUE(gateway_id,request_id),
+      CONSTRAINT ck_edge_release_status CHECK (status IN ('offered','accepted','installed','failed','rolled_back')),
+      CONSTRAINT ck_edge_release_digest CHECK (artifact_digest ~ '^[0-9a-f]{64}$' AND request_digest ~ '^[0-9a-f]{64}$'),
+      CONSTRAINT ck_edge_release_summary CHECK (jsonb_typeof(safe_summary)='object')
+    );
+
+    CREATE TABLE public.edge_gateway_audits (
+      uid UUID PRIMARY KEY, gateway_id UUID REFERENCES public.edge_gateways(uid),
+      event_type VARCHAR(80) NOT NULL, actor_uid UUID, success BOOLEAN NOT NULL,
+      safe_detail JSONB NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
+      CONSTRAINT ck_edge_audit_detail CHECK (jsonb_typeof(safe_detail)='object')
+    );
+    CREATE INDEX ix_edge_audit_gateway_created ON public.edge_gateway_audits(gateway_id,created_at DESC);
+    CREATE TABLE public.edge_gateway_failure_windows (
+      failure_fingerprint CHAR(64) NOT NULL, window_started_at TIMESTAMPTZ NOT NULL,
+      gateway_id UUID REFERENCES public.edge_gateways(uid), event_type VARCHAR(80) NOT NULL,
+      occurrence_count INTEGER NOT NULL, first_seen_at TIMESTAMPTZ NOT NULL,
+      last_seen_at TIMESTAMPTZ NOT NULL,
+      PRIMARY KEY(failure_fingerprint,window_started_at),
+      CONSTRAINT ck_edge_failure_count CHECK (occurrence_count BETWEEN 1 AND 1000000)
+    );
+    ALTER TABLE public.edge_gateway_audits ADD COLUMN failure_fingerprint CHAR(64);
+    ALTER TABLE public.edge_gateway_audits ADD COLUMN window_started_at TIMESTAMPTZ;
+    CREATE UNIQUE INDEX uq_edge_failure_audit_window
+      ON public.edge_gateway_audits(failure_fingerprint,window_started_at)
+      WHERE success=FALSE;
+    CREATE OR REPLACE FUNCTION public.protect_edge_audit_append_only()
+    RETURNS trigger LANGUAGE plpgsql AS $$
+    BEGIN
+      IF TG_OP='INSERT' AND current_user='dataops_edge_evidence_owner' THEN
+        RETURN NEW;
+      END IF;
+      RAISE EXCEPTION 'edge gateway audit ledger is owner-written and append-only';
+    END $$;
+    CREATE TRIGGER trg_edge_audit_append_only
+      BEFORE INSERT OR UPDATE OR DELETE ON public.edge_gateway_audits
+      FOR EACH ROW EXECUTE FUNCTION public.protect_edge_audit_append_only();
+    CREATE TRIGGER trg_edge_audit_no_truncate
+      BEFORE TRUNCATE ON public.edge_gateway_audits
+      FOR EACH STATEMENT EXECUTE FUNCTION public.protect_edge_audit_append_only();
+    CREATE OR REPLACE FUNCTION public.protect_edge_failure_window()
+    RETURNS trigger LANGUAGE plpgsql AS $$
+    BEGIN
+      IF TG_OP IN ('DELETE','TRUNCATE') THEN
+        RAISE EXCEPTION 'edge gateway failure evidence cannot be removed';
+      END IF;
+      IF current_user<>'dataops_edge_evidence_owner' THEN
+        RAISE EXCEPTION 'edge gateway failure evidence requires the controlled writer';
+      END IF;
+      IF TG_OP='INSERT' THEN RETURN NEW; END IF;
+      IF NEW.failure_fingerprint<>OLD.failure_fingerprint OR
+         NEW.window_started_at<>OLD.window_started_at OR
+         NEW.gateway_id IS DISTINCT FROM OLD.gateway_id OR
+         NEW.event_type<>OLD.event_type OR NEW.first_seen_at<>OLD.first_seen_at THEN
+        RAISE EXCEPTION 'edge gateway failure window identity is immutable';
+      END IF;
+      IF NEW.occurrence_count<>OLD.occurrence_count+1 OR
+         NEW.last_seen_at<OLD.last_seen_at OR NEW.last_seen_at<NEW.first_seen_at THEN
+        RAISE EXCEPTION 'edge gateway failure window update is invalid';
+      END IF;
+      RETURN NEW;
+    END $$;
+    CREATE TRIGGER trg_edge_failure_window_protected
+      BEFORE INSERT OR UPDATE OR DELETE ON public.edge_gateway_failure_windows
+      FOR EACH ROW EXECUTE FUNCTION public.protect_edge_failure_window();
+    CREATE TRIGGER trg_edge_failure_window_no_truncate
+      BEFORE TRUNCATE ON public.edge_gateway_failure_windows
+      FOR EACH STATEMENT EXECUTE FUNCTION public.protect_edge_failure_window();
+    CREATE OR REPLACE FUNCTION public.record_edge_gateway_audit(
+      p_uid UUID,p_gateway UUID,p_event_type VARCHAR(80),p_actor UUID,
+      p_success BOOLEAN,p_detail JSONB
+    ) RETURNS void LANGUAGE plpgsql SECURITY DEFINER
+      SET search_path=pg_catalog AS $$
+    BEGIN
+      IF p_uid IS NULL OR p_event_type NOT IN (
+        'enrollment_created','gateway_registered','credential_rotated','gateway_revoked',
+        'heartbeat_accepted','task_issued','task_cancel_requested','task_outcome_recorded',
+        'event_accepted','release_offered','release_acknowledged'
+      ) OR jsonb_typeof(p_detail)<>'object' OR octet_length(p_detail::text)>8192 OR
+         (p_gateway IS NOT NULL AND NOT EXISTS (
+           SELECT 1 FROM public.edge_gateways WHERE uid=p_gateway
+         )) THEN
+        RAISE EXCEPTION 'edge gateway audit evidence is invalid';
+      END IF;
+      INSERT INTO public.edge_gateway_audits
+        (uid,gateway_id,event_type,actor_uid,success,safe_detail)
+      VALUES (p_uid,p_gateway,p_event_type,p_actor,p_success,p_detail);
+    END $$;
+    CREATE OR REPLACE FUNCTION public.record_edge_gateway_failure(
+      p_audit_uid UUID,p_fingerprint CHAR(64),p_gateway UUID,
+      p_event_type VARCHAR(80),p_reason_code VARCHAR(64)
+    ) RETURNS TABLE(
+      failure_fingerprint CHAR(64), window_started_at TIMESTAMPTZ,
+      occurrence_count INTEGER, gateway_id UUID, event_type VARCHAR(80)
+    ) LANGUAGE plpgsql SECURITY DEFINER
+      SET search_path=pg_catalog AS $$
+    DECLARE
+      target_fingerprint CHAR(64) := p_fingerprint;
+      target_gateway UUID := p_gateway;
+      target_event_type VARCHAR(80) := p_event_type;
+      target_reason_code VARCHAR(64) := p_reason_code;
+      target_window TIMESTAMPTZ := date_trunc('minute',CURRENT_TIMESTAMP);
+      total_windows INTEGER;
+    BEGIN
+      IF p_audit_uid IS NULL OR p_fingerprint !~ '^[0-9a-f]{64}$' OR
+         p_event_type NOT IN (
+           'untrusted_request_rejected','policy_rejected','binding_rejected',
+           'idempotency_conflict','release_rejected'
+         ) OR p_reason_code !~ '^[a-z][a-z0-9_]{0,63}$' OR
+         (p_gateway IS NOT NULL AND NOT EXISTS (
+           SELECT 1 FROM public.edge_gateways WHERE uid=p_gateway
+         )) THEN
+        RAISE EXCEPTION 'edge gateway failure evidence is invalid';
+      END IF;
+      PERFORM pg_advisory_xact_lock(4774096);
+      SELECT count(*) INTO total_windows FROM public.edge_gateway_failure_windows;
+      IF NOT EXISTS (
+        SELECT 1 FROM public.edge_gateway_failure_windows AS existing
+         WHERE existing.failure_fingerprint=target_fingerprint
+           AND existing.window_started_at=target_window
+      ) AND total_windows>=4095 THEN
+        target_fingerprint := '16b5bae3613ee26975528e149d401a46f1d659405cfe0148262caec288dfb365';
+        target_gateway := NULL;
+        target_event_type := 'untrusted_request_rejected';
+        target_reason_code := 'capacity_overflow';
+        target_window := '1970-01-01 00:00:00+00'::timestamptz;
+      END IF;
+      RETURN QUERY
+        INSERT INTO public.edge_gateway_failure_windows AS windows
+          (failure_fingerprint,window_started_at,gateway_id,event_type,
+           occurrence_count,first_seen_at,last_seen_at)
+        VALUES (target_fingerprint,target_window,target_gateway,target_event_type,
+                1,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)
+        ON CONFLICT ON CONSTRAINT edge_gateway_failure_windows_pkey DO UPDATE
+          SET occurrence_count=windows.occurrence_count+1,
+              last_seen_at=CURRENT_TIMESTAMP
+        RETURNING windows.failure_fingerprint,windows.window_started_at,
+                  windows.occurrence_count,windows.gateway_id,windows.event_type;
+      IF NOT EXISTS (
+        SELECT 1 FROM public.edge_gateway_audits AS existing_audit
+         WHERE existing_audit.failure_fingerprint=target_fingerprint
+           AND existing_audit.window_started_at=target_window
+           AND existing_audit.success=FALSE
+      ) THEN
+        INSERT INTO public.edge_gateway_audits
+          (uid,gateway_id,event_type,actor_uid,success,safe_detail,
+           failure_fingerprint,window_started_at)
+        VALUES (p_audit_uid,target_gateway,target_event_type,NULL,FALSE,
+                jsonb_build_object('reason_code',target_reason_code),
+                target_fingerprint,target_window);
+      END IF;
+    END $$;
+    ALTER TABLE public.edge_gateway_audits OWNER TO dataops_edge_evidence_owner;
+    ALTER TABLE public.edge_gateway_failure_windows OWNER TO dataops_edge_evidence_owner;
+    ALTER FUNCTION public.protect_edge_audit_append_only() OWNER TO dataops_edge_evidence_owner;
+    ALTER FUNCTION public.protect_edge_failure_window() OWNER TO dataops_edge_evidence_owner;
+    ALTER FUNCTION public.record_edge_gateway_audit(UUID,UUID,VARCHAR(80),UUID,BOOLEAN,JSONB)
+      OWNER TO dataops_edge_evidence_owner;
+    ALTER FUNCTION public.record_edge_gateway_failure(UUID,CHAR(64),UUID,VARCHAR(80),VARCHAR(64))
+      OWNER TO dataops_edge_evidence_owner;
+    GRANT SELECT ON public.edge_gateways TO dataops_edge_evidence_owner;
+    REVOKE ALL ON public.edge_gateway_audits,public.edge_gateway_failure_windows FROM PUBLIC;
+    REVOKE ALL ON FUNCTION public.record_edge_gateway_audit(UUID,UUID,VARCHAR(80),UUID,BOOLEAN,JSONB) FROM PUBLIC;
+    REVOKE ALL ON FUNCTION public.record_edge_gateway_failure(UUID,CHAR(64),UUID,VARCHAR(80),VARCHAR(64)) FROM PUBLIC;
+    GRANT USAGE ON SCHEMA public TO dataops_app_runtime;
+    GRANT SELECT,INSERT,UPDATE,DELETE ON
+      public.edge_gateway_enrollments,public.edge_gateways,
+      public.edge_gateway_credentials,public.edge_gateway_rotation_requests,
+      public.edge_gateway_tasks,public.edge_gateway_events,public.edge_gateway_releases
+      TO dataops_app_runtime;
+    ALTER DEFAULT PRIVILEGES IN SCHEMA public
+      GRANT SELECT,INSERT,UPDATE,DELETE ON TABLES TO dataops_app_runtime;
+    ALTER DEFAULT PRIVILEGES IN SCHEMA public
+      GRANT USAGE,SELECT,UPDATE ON SEQUENCES TO dataops_app_runtime;
+    REVOKE INSERT,UPDATE,DELETE,TRUNCATE ON
+      public.edge_gateway_audits,public.edge_gateway_failure_windows
+      FROM dataops_app_runtime;
+    GRANT SELECT ON public.edge_gateway_audits,public.edge_gateway_failure_windows
+      TO dataops_app_runtime;
+    GRANT EXECUTE ON FUNCTION public.record_edge_gateway_audit(UUID,UUID,VARCHAR(80),UUID,BOOLEAN,JSONB)
+      TO dataops_app_runtime;
+    GRANT EXECUTE ON FUNCTION public.record_edge_gateway_failure(UUID,CHAR(64),UUID,VARCHAR(80),VARCHAR(64))
+      TO dataops_app_runtime;
+    """)
+
+
+def downgrade() -> None:
+    op.execute("""
+    DO $$
+    DECLARE table_name TEXT; has_data BOOLEAN;
+    BEGIN
+      FOREACH table_name IN ARRAY ARRAY[
+        'edge_gateway_enrollments','edge_gateways','edge_gateway_credentials',
+        'edge_gateway_tasks','edge_gateway_events','edge_gateway_releases',
+        'edge_gateway_audits','edge_gateway_failure_windows','edge_gateway_rotation_requests'
+      ] LOOP
+        IF to_regclass('public.' || table_name) IS NOT NULL THEN
+          EXECUTE format('SELECT EXISTS (SELECT 1 FROM public.%I)',table_name) INTO has_data;
+          IF has_data THEN
+            RAISE EXCEPTION 'edge gateway downgrade rejected: clear Task2 business and audit data first';
+          END IF;
+        END IF;
+      END LOOP;
+    END $$;
+    DROP TRIGGER IF EXISTS trg_edge_audit_append_only ON public.edge_gateway_audits;
+    DROP TRIGGER IF EXISTS trg_edge_audit_no_truncate ON public.edge_gateway_audits;
+    DROP TRIGGER IF EXISTS trg_edge_failure_window_protected ON public.edge_gateway_failure_windows;
+    DROP TRIGGER IF EXISTS trg_edge_failure_window_no_truncate ON public.edge_gateway_failure_windows;
+    DROP FUNCTION IF EXISTS public.record_edge_gateway_failure(UUID,CHAR(64),UUID,VARCHAR(80),VARCHAR(64));
+    DROP FUNCTION IF EXISTS public.record_edge_gateway_audit(UUID,UUID,VARCHAR(80),UUID,BOOLEAN,JSONB);
+    DROP FUNCTION IF EXISTS public.protect_edge_failure_window();
+    DROP FUNCTION IF EXISTS public.protect_edge_audit_append_only();
+    DROP TABLE IF EXISTS public.edge_gateway_failure_windows;
+    DROP TABLE IF EXISTS public.edge_gateway_audits;
+    DROP TABLE IF EXISTS public.edge_gateway_events;
+    DROP TABLE IF EXISTS public.edge_gateway_releases;
+    DROP TABLE IF EXISTS public.edge_gateway_tasks;
+    DROP TABLE IF EXISTS public.edge_gateway_rotation_requests;
+    DROP TRIGGER IF EXISTS trg_edge_credential_identity_immutable ON public.edge_gateway_credentials;
+    DROP TRIGGER IF EXISTS trg_edge_gateway_identity_immutable ON public.edge_gateways;
+    DROP TRIGGER IF EXISTS trg_edge_credential_binding ON public.edge_gateway_credentials;
+    DROP TRIGGER IF EXISTS trg_edge_gateway_binding ON public.edge_gateways;
+    DROP FUNCTION IF EXISTS public.enforce_edge_credential_binding();
+    DROP FUNCTION IF EXISTS public.enforce_edge_gateway_binding();
+    DROP FUNCTION IF EXISTS public.validate_edge_gateway_binding(UUID);
+    DROP FUNCTION IF EXISTS public.protect_edge_credential_identity();
+    DROP FUNCTION IF EXISTS public.protect_edge_gateway_identity();
+    DROP TABLE IF EXISTS public.edge_gateway_credentials;
+    DROP TABLE IF EXISTS public.edge_gateways;
+    DROP TRIGGER IF EXISTS trg_edge_enrollment_identity_immutable ON public.edge_gateway_enrollments;
+    DROP TABLE IF EXISTS public.edge_gateway_enrollments;
+    DROP FUNCTION IF EXISTS public.protect_edge_enrollment_identity();
+    DROP FUNCTION IF EXISTS public.edge_hosts_valid(TEXT[],BOOLEAN);
+    """)

+ 22 - 0
migrations/versions/20260809_478_edge_gateway_security_cleanup.py

@@ -0,0 +1,22 @@
+"""Remove legacy edge evidence function overloads without restoring them."""
+
+from alembic import op
+
+revision = "20260809_478"
+down_revision = "20260809_477"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+    op.execute(r"""
+    DROP FUNCTION IF EXISTS public.record_edge_gateway_failure(
+      CHARACTER,UUID,CHARACTER VARYING
+    );
+    """)
+
+
+def downgrade() -> None:
+    # Deliberately non-restorative: the removed SECURITY DEFINER overload had an
+    # unsafe search_path and must never be recreated by a rollback.
+    pass

+ 187 - 0
migrations/versions/20260809_479_edge_gateway_signed_authority.py

@@ -0,0 +1,187 @@
+"""Persist signed edge task authorities and complete release manifests."""
+
+from alembic import op
+
+revision = "20260809_479"
+down_revision = "20260809_478"
+branch_labels = None
+depends_on = None
+
+
+def _failure_window_function(*, lock_clock: bool) -> str:
+    observed_at = "NULL" if lock_clock else "CURRENT_TIMESTAMP"
+    refresh_clock = (
+        "observed_at := clock_timestamp();\n"
+        "      target_window := date_trunc('minute',observed_at);"
+        if lock_clock
+        else ""
+    )
+    return rf"""
+    CREATE OR REPLACE FUNCTION public.record_edge_gateway_failure(
+      p_audit_uid UUID,p_fingerprint CHAR(64),p_gateway UUID,
+      p_event_type VARCHAR(80),p_reason_code VARCHAR(64)
+    ) RETURNS TABLE(
+      failure_fingerprint CHAR(64), window_started_at TIMESTAMPTZ,
+      occurrence_count INTEGER, gateway_id UUID, event_type VARCHAR(80)
+    ) LANGUAGE plpgsql SECURITY DEFINER
+      SET search_path=pg_catalog AS $$
+    DECLARE
+      target_fingerprint CHAR(64) := p_fingerprint;
+      target_gateway UUID := p_gateway;
+      target_event_type VARCHAR(80) := p_event_type;
+      target_reason_code VARCHAR(64) := p_reason_code;
+      observed_at TIMESTAMPTZ := {observed_at};
+      target_window TIMESTAMPTZ := date_trunc('minute',observed_at);
+      total_windows INTEGER;
+    BEGIN
+      IF p_audit_uid IS NULL OR p_fingerprint !~ '^[0-9a-f]{{64}}$' OR
+         p_event_type NOT IN (
+           'untrusted_request_rejected','policy_rejected','binding_rejected',
+           'idempotency_conflict','release_rejected'
+         ) OR p_reason_code !~ '^[a-z][a-z0-9_]{{0,63}}$' OR
+         (p_gateway IS NOT NULL AND NOT EXISTS (
+           SELECT 1 FROM public.edge_gateways WHERE uid=p_gateway
+         )) THEN
+        RAISE EXCEPTION 'edge gateway failure evidence is invalid';
+      END IF;
+      PERFORM pg_advisory_xact_lock(4774096);
+      {refresh_clock}
+      SELECT count(*) INTO total_windows FROM public.edge_gateway_failure_windows;
+      IF NOT EXISTS (
+        SELECT 1 FROM public.edge_gateway_failure_windows AS existing
+         WHERE existing.failure_fingerprint=target_fingerprint
+           AND existing.window_started_at=target_window
+      ) AND total_windows>=4095 THEN
+        target_fingerprint := '16b5bae3613ee26975528e149d401a46f1d659405cfe0148262caec288dfb365';
+        target_gateway := NULL;
+        target_event_type := 'untrusted_request_rejected';
+        target_reason_code := 'capacity_overflow';
+        target_window := '1970-01-01 00:00:00+00'::timestamptz;
+      END IF;
+      RETURN QUERY
+        INSERT INTO public.edge_gateway_failure_windows AS windows
+          (failure_fingerprint,window_started_at,gateway_id,event_type,
+           occurrence_count,first_seen_at,last_seen_at)
+        VALUES (target_fingerprint,target_window,target_gateway,target_event_type,
+                1,observed_at,observed_at)
+        ON CONFLICT ON CONSTRAINT edge_gateway_failure_windows_pkey DO UPDATE
+          SET occurrence_count=windows.occurrence_count+1,
+              last_seen_at=observed_at
+        RETURNING windows.failure_fingerprint,windows.window_started_at,
+                  windows.occurrence_count,windows.gateway_id,windows.event_type;
+      IF NOT EXISTS (
+        SELECT 1 FROM public.edge_gateway_audits AS existing_audit
+         WHERE existing_audit.failure_fingerprint=target_fingerprint
+           AND existing_audit.window_started_at=target_window
+           AND existing_audit.success=FALSE
+      ) THEN
+        INSERT INTO public.edge_gateway_audits
+          (uid,gateway_id,event_type,actor_uid,success,safe_detail,
+           failure_fingerprint,window_started_at)
+        VALUES (p_audit_uid,target_gateway,target_event_type,NULL,FALSE,
+                jsonb_build_object('reason_code',target_reason_code),
+                target_fingerprint,target_window);
+      END IF;
+    END $$;
+    """
+
+
+def upgrade() -> None:
+    op.execute(r"""
+    DO $$
+    BEGIN
+      IF EXISTS (SELECT 1 FROM public.edge_gateway_tasks)
+         OR EXISTS (SELECT 1 FROM public.edge_gateway_releases) THEN
+        RAISE EXCEPTION 'cannot add signed edge authority to existing unsigned rows';
+      END IF;
+    END $$;
+
+    ALTER TABLE public.edge_gateway_tasks
+      ADD COLUMN authority JSONB NOT NULL;
+    ALTER TABLE public.edge_gateway_tasks
+      ADD CONSTRAINT ck_edge_task_authority CHECK (
+        jsonb_typeof(authority)='object' AND
+        authority ?& ARRAY[
+          'task','authority_key_id','signature_algorithm','contract_digest','gateway_id',
+          'environment','network_zone','policy_digest','purpose','issued_at','expires_at',
+          'signature'
+        ] AND
+        authority - ARRAY[
+          'task','authority_key_id','signature_algorithm','contract_digest','gateway_id',
+          'environment','network_zone','policy_digest','purpose','issued_at','expires_at',
+          'signature'
+        ]::text[] = '{}'::jsonb AND
+        authority->>'signature_algorithm'='Ed25519' AND
+        authority->'task'=contract AND
+        authority->>'contract_digest'=contract_digest AND
+        authority->>'gateway_id'=gateway_id::text AND
+        authority->>'environment'=contract->>'environment' AND
+        authority->>'network_zone'=contract->>'network_zone' AND
+        authority->>'policy_digest'=contract->>'policy_digest' AND
+        authority->>'purpose'=contract->>'purpose' AND
+        authority->>'authority_key_id' ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
+        authority->>'signature' ~ '^[0-9a-f]{128}$'
+      );
+
+    ALTER TABLE public.edge_gateway_releases
+      ADD COLUMN artifact_name VARCHAR(255) NOT NULL,
+      ADD COLUMN deadline_at TIMESTAMPTZ NOT NULL,
+      ADD COLUMN signature_algorithm VARCHAR(20) NOT NULL,
+      ADD COLUMN key_id VARCHAR(128) NOT NULL,
+      ADD COLUMN manifest_digest CHAR(64) NOT NULL,
+      ADD COLUMN signature CHAR(128) NOT NULL,
+      ADD COLUMN signed_manifest JSONB NOT NULL;
+    ALTER TABLE public.edge_gateway_releases
+      ADD CONSTRAINT ck_edge_release_signed_manifest CHECK (
+        signature_algorithm='Ed25519' AND
+        key_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
+        manifest_digest ~ '^[0-9a-f]{64}$' AND
+        signature ~ '^[0-9a-f]{128}$' AND
+        jsonb_typeof(signed_manifest)='object' AND
+        signed_manifest ?& ARRAY[
+          'release_id','version','rollback_version','artifact_digest','artifact_name',
+          'deadline_at','signature_algorithm','key_id','manifest_digest','signature','status'
+        ] AND
+        signed_manifest - ARRAY[
+          'release_id','version','rollback_version','artifact_digest','artifact_name',
+          'deadline_at','signature_algorithm','key_id','manifest_digest','signature','status'
+        ]::text[] = '{}'::jsonb AND
+        signed_manifest->>'release_id'=uid::text AND
+        signed_manifest->>'version'=version AND
+        signed_manifest->>'rollback_version'=rollback_version AND
+        signed_manifest->>'artifact_digest'=artifact_digest AND
+        signed_manifest->>'artifact_name'=artifact_name AND
+        (signed_manifest->>'deadline_at')::timestamptz=deadline_at AND
+        signed_manifest->>'signature_algorithm'=signature_algorithm AND
+        signed_manifest->>'key_id'=key_id AND
+        signed_manifest->>'manifest_digest'=manifest_digest AND
+        signed_manifest->>'signature'=signature AND
+        signed_manifest->>'status'=status
+      );
+    """)
+    op.execute(_failure_window_function(lock_clock=True))
+
+
+def downgrade() -> None:
+    op.execute(r"""
+    DO $$
+    BEGIN
+      IF EXISTS (SELECT 1 FROM public.edge_gateway_tasks)
+         OR EXISTS (SELECT 1 FROM public.edge_gateway_releases) THEN
+        RAISE EXCEPTION 'signed edge authority rows block metadata-dropping downgrade';
+      END IF;
+    END $$;
+    ALTER TABLE public.edge_gateway_releases
+      DROP CONSTRAINT ck_edge_release_signed_manifest,
+      DROP COLUMN signed_manifest,
+      DROP COLUMN signature,
+      DROP COLUMN manifest_digest,
+      DROP COLUMN key_id,
+      DROP COLUMN signature_algorithm,
+      DROP COLUMN deadline_at,
+      DROP COLUMN artifact_name;
+    ALTER TABLE public.edge_gateway_tasks
+      DROP CONSTRAINT ck_edge_task_authority,
+      DROP COLUMN authority;
+    """)
+    op.execute(_failure_window_function(lock_clock=False))

+ 5 - 0
scripts/deploy_dataops.sh

@@ -60,6 +60,11 @@ setup_env_file() {
   normalize_env_file
   load_env_file
   ensure_env_file_permissions
+  local validator="${SCRIPT_DIR}/../app/config/database_urls.py"
+  if [[ ! -f "${validator}" ]] || ! "${PYTHON_BIN}" "${validator}" --env-file "${ENV_FILE}"; then
+    echo_error "数据库环境变量仍为空、格式错误或包含模板占位值"
+    exit 2
+  fi
 }
 
 setup_venv() {

+ 1314 - 0
scripts/edge_gateway_admin.py

@@ -0,0 +1,1314 @@
+#!/usr/bin/env python3
+"""Fail-closed local administration for a DataOps enterprise edge gateway."""
+
+from __future__ import annotations
+
+import argparse
+import fcntl
+import hashlib
+import json
+import os
+import re
+import sqlite3
+import stat
+import sys
+import tempfile
+from contextlib import contextmanager, suppress
+from datetime import UTC, datetime
+from decimal import Decimal
+from pathlib import Path, PurePosixPath
+
+from cryptography import x509
+from cryptography.exceptions import InvalidSignature
+from cryptography.hazmat.primitives import hashes, serialization
+from cryptography.hazmat.primitives.asymmetric import (
+    ec,
+    ed448,
+    ed25519,
+    padding,
+    rsa,
+)
+from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID
+
+MAX_JSON_BYTES = 262_144
+MAX_BACKUPS = 3
+SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
+IDENTIFIER_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,254}$")
+VERSION_RE = re.compile(r"^(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})$")
+ORIGIN_RE = re.compile(r"^https://[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?(?::[0-9]{1,5})?$")
+MANIFEST_FIELDS = frozenset(
+    {
+        "release_id", "version", "rollback_version", "artifact_digest",
+        "artifact_name", "deadline_at", "signature_algorithm", "key_id",
+        "manifest_digest", "signature", "status",
+    }
+)
+
+
+class AdminError(RuntimeError):
+    def __init__(self, code: str, exit_code: int = 2):
+        super().__init__(code)
+        self.code = code
+        self.exit_code = exit_code
+
+
+def _json_bytes(value: object) -> bytes:
+    return json.dumps(
+        value, ensure_ascii=False, sort_keys=True, separators=(",", ":")
+    ).encode("utf-8")
+
+
+def _digest(value: object) -> str:
+    return hashlib.sha256(_json_bytes(value)).hexdigest()
+
+
+def _protocol_bytes(value: object) -> bytes:
+    def frame(tag: bytes, content: bytes) -> bytes:
+        return tag + str(len(content)).encode("ascii") + b":" + content
+
+    if value is None:
+        return b"n"
+    if isinstance(value, bool):
+        return b"b1" if value else b"b0"
+    if isinstance(value, (int, float)):
+        number = Decimal(value if isinstance(value, int) else str(value))
+        if not number.is_finite():
+            raise AdminError("INVALID_INPUT")
+        text = "0" if number == 0 else format(number.normalize(), "f")
+        if "." in text:
+            text = text.rstrip("0").rstrip(".")
+        return frame(b"d", text.encode("ascii"))
+    if isinstance(value, str):
+        return frame(b"s", value.encode("utf-8"))
+    if isinstance(value, dict):
+        if any(not isinstance(key, str) for key in value):
+            raise AdminError("INVALID_INPUT")
+        encoded = bytearray(b"m" + str(len(value)).encode("ascii") + b":")
+        for key in sorted(value, key=lambda item: item.encode("utf-8")):
+            encoded.extend(frame(b"k", key.encode("utf-8")))
+            encoded.extend(_protocol_bytes(value[key]))
+        return bytes(encoded)
+    if isinstance(value, (list, tuple)):
+        encoded = bytearray(b"l" + str(len(value)).encode("ascii") + b":")
+        for item in value:
+            encoded.extend(_protocol_bytes(item))
+        return bytes(encoded)
+    raise AdminError("INVALID_INPUT")
+
+
+def _protocol_digest(value: object) -> str:
+    return hashlib.sha256(_protocol_bytes(value)).hexdigest()
+
+
+def _output(value: dict[str, object]) -> None:
+    rendered = json.dumps(value, ensure_ascii=False, sort_keys=True)
+    if len(rendered.encode("utf-8")) > 32_768:
+        raise AdminError("OUTPUT_LIMIT")
+    print(rendered)
+
+
+def _root(raw: str, *, create: bool = False) -> Path:
+    candidate = Path(raw)
+    if not candidate.is_absolute() or "\x00" in raw:
+        raise AdminError("INVALID_INPUT")
+    protected = {
+        Path("/"), Path.home(), Path("/opt"), Path("/var"), Path("/usr"),
+        Path("/etc"), Path("/tmp"), Path("/private"), Path("/private/tmp"),
+        Path("/Users"), Path("/home"), Path("/root"), Path("/Volumes"),
+    }
+    normalized = Path(os.path.abspath(candidate))
+    if normalized in protected:
+        raise AdminError("PATH_POLICY")
+    if candidate.exists() and candidate.is_symlink():
+        raise AdminError("PATH_POLICY")
+    if create:
+        with suppress(FileExistsError):
+            candidate.mkdir(mode=0o700, parents=False, exist_ok=False)
+    if not candidate.is_dir():
+        raise AdminError("PATH_POLICY")
+    resolved = candidate.resolve(strict=True)
+    if resolved != candidate:
+        raise AdminError("PATH_POLICY")
+    return resolved
+
+
+def _runtime_identity(root: Path) -> tuple[int, int]:
+    record = _read_json(root / "state" / "runtime-identity.json")
+    if set(record) != {"runtime_gid", "runtime_uid", "schema_version"}:
+        raise AdminError("OWNER_POLICY")
+    uid, gid = record["runtime_uid"], record["runtime_gid"]
+    if (
+        isinstance(uid, bool)
+        or not isinstance(uid, int)
+        or isinstance(gid, bool)
+        or not isinstance(gid, int)
+        or uid < 1
+        or gid < 1
+    ):
+        raise AdminError("OWNER_POLICY")
+    metadata = root.stat()
+    if metadata.st_uid != uid or metadata.st_gid != gid:
+        raise AdminError("OWNER_POLICY")
+    return uid, gid
+
+
+def _validated_root(raw: str) -> Path:
+    root = _root(raw)
+    _runtime_identity(root)
+    return root
+
+
+def _chown_exact(path: Path, uid: int, gid: int) -> None:
+    metadata = path.lstat()
+    if stat.S_ISLNK(metadata.st_mode):
+        raise AdminError("PATH_POLICY")
+    if (metadata.st_uid, metadata.st_gid) == (uid, gid):
+        return
+    if os.geteuid() != 0:
+        raise AdminError("OWNER_POLICY")
+    os.chown(path, uid, gid, follow_symlinks=False)
+
+
+def _inside(root: Path, raw: str | Path, *, must_exist: bool = False) -> Path:
+    candidate = Path(raw)
+    if not candidate.is_absolute() or "\x00" in str(candidate):
+        raise AdminError("INVALID_INPUT")
+    if candidate.is_symlink():
+        raise AdminError("PATH_POLICY")
+    try:
+        parent = candidate.parent.resolve(strict=True)
+        parent.relative_to(root)
+    except (OSError, ValueError) as exc:
+        raise AdminError("PATH_POLICY") from exc
+    current = root
+    for part in parent.relative_to(root).parts:
+        current /= part
+        if current.is_symlink():
+            raise AdminError("PATH_POLICY")
+    if must_exist:
+        try:
+            resolved = candidate.resolve(strict=True)
+            resolved.relative_to(root)
+        except (OSError, ValueError) as exc:
+            raise AdminError("PATH_POLICY") from exc
+        metadata = candidate.stat()
+        if (
+            candidate.is_symlink()
+            or not stat.S_ISREG(metadata.st_mode)
+            or metadata.st_nlink != 1
+        ):
+            raise AdminError("PATH_POLICY")
+    return candidate
+
+
+def _mkdir(root: Path, name: str, mode: int = 0o700) -> Path:
+    path = root / name
+    if path.exists() and (path.is_symlink() or not path.is_dir()):
+        raise AdminError("PATH_POLICY")
+    created = not path.exists()
+    path.mkdir(mode=mode, exist_ok=True)
+    os.chmod(path, mode)
+    parent = path.parent.stat()
+    metadata = path.stat()
+    if created and os.geteuid() == 0:
+        os.chown(path, parent.st_uid, parent.st_gid, follow_symlinks=False)
+    elif (metadata.st_uid, metadata.st_gid) != (parent.st_uid, parent.st_gid):
+        raise AdminError("OWNER_POLICY")
+    return path
+
+
+def _inherit_parent_owner(descriptor: int, parent: Path) -> None:
+    parent_metadata = parent.stat()
+    metadata = os.fstat(descriptor)
+    expected = (parent_metadata.st_uid, parent_metadata.st_gid)
+    if (metadata.st_uid, metadata.st_gid) == expected:
+        return
+    if os.geteuid() != 0:
+        raise AdminError("OWNER_POLICY")
+    os.fchown(descriptor, *expected)
+
+
+def _atomic_write(path: Path, data: bytes, mode: int) -> None:
+    if path.exists() and path.is_symlink():
+        raise AdminError("PATH_POLICY")
+    descriptor, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
+    try:
+        os.fchmod(descriptor, mode)
+        _inherit_parent_owner(descriptor, path.parent)
+        with os.fdopen(descriptor, "wb", closefd=True) as stream:
+            stream.write(data)
+            stream.flush()
+            os.fsync(stream.fileno())
+        os.replace(temporary, path)
+        os.chmod(path, mode)
+        _fsync_directory(path.parent)
+    except Exception:
+        with suppress(FileNotFoundError):
+            os.unlink(temporary)
+        raise
+
+
+def _fsync_directory(path: Path) -> None:
+    directory = os.open(path, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
+    try:
+        os.fsync(directory)
+    finally:
+        os.close(directory)
+
+
+def _atomic_copy(source: Path, target: Path, mode: int) -> None:
+    if target.exists() or target.is_symlink():
+        raise AdminError("PATH_POLICY")
+    descriptor, temporary = tempfile.mkstemp(prefix=f".{target.name}.", dir=target.parent)
+    try:
+        os.fchmod(descriptor, mode)
+        _inherit_parent_owner(descriptor, target.parent)
+        with source.open("rb") as reader, os.fdopen(descriptor, "wb") as writer:
+            for chunk in iter(lambda: reader.read(1_048_576), b""):
+                writer.write(chunk)
+            writer.flush()
+            os.fsync(writer.fileno())
+        os.replace(temporary, target)
+        os.chmod(target, mode)
+        _fsync_directory(target.parent)
+    except Exception:
+        with suppress(FileNotFoundError):
+            os.unlink(temporary)
+        raise
+
+
+def _read_json(path: Path) -> dict[str, object]:
+    if not path.is_file() or path.is_symlink() or path.stat().st_size > MAX_JSON_BYTES:
+        raise AdminError("INVALID_INPUT")
+    try:
+        value = json.loads(path.read_text(encoding="utf-8"))
+    except (OSError, UnicodeError, json.JSONDecodeError) as exc:
+        raise AdminError("INVALID_INPUT") from exc
+    if not isinstance(value, dict):
+        raise AdminError("INVALID_INPUT")
+    return value
+
+
+def _sha256_file(path: Path) -> str:
+    digest = hashlib.sha256()
+    try:
+        with path.open("rb") as stream:
+            for chunk in iter(lambda: stream.read(1_048_576), b""):
+                digest.update(chunk)
+    except OSError as exc:
+        raise AdminError("INVALID_INPUT") from exc
+    return digest.hexdigest()
+
+
+def _open_root_file(root: Path, raw: str | Path) -> tuple[int, os.stat_result]:
+    path = _inside(root, raw)
+    flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0)
+    if hasattr(os, "O_NOFOLLOW"):
+        flags |= os.O_NOFOLLOW
+    try:
+        descriptor = os.open(path, flags)
+    except OSError as exc:
+        raise AdminError("PATH_POLICY") from exc
+    try:
+        metadata = os.fstat(descriptor)
+        uid, gid = _runtime_identity(root)
+        if (
+            not stat.S_ISREG(metadata.st_mode)
+            or metadata.st_nlink != 1
+            or (metadata.st_uid, metadata.st_gid) != (uid, gid)
+        ):
+            raise AdminError("PATH_POLICY")
+        return descriptor, metadata
+    except Exception:
+        os.close(descriptor)
+        raise
+
+
+def _read_json_descriptor(descriptor: int) -> dict[str, object]:
+    metadata = os.fstat(descriptor)
+    if metadata.st_size > MAX_JSON_BYTES:
+        raise AdminError("INVALID_INPUT")
+    os.lseek(descriptor, 0, os.SEEK_SET)
+    try:
+        raw = b""
+        while len(raw) <= MAX_JSON_BYTES:
+            chunk = os.read(descriptor, min(65_536, MAX_JSON_BYTES + 1 - len(raw)))
+            if not chunk:
+                break
+            raw += chunk
+        value = json.loads(raw.decode("utf-8"))
+    except (OSError, UnicodeError, json.JSONDecodeError) as exc:
+        raise AdminError("INVALID_INPUT") from exc
+    if not isinstance(value, dict):
+        raise AdminError("INVALID_INPUT")
+    return value
+
+
+def _copy_descriptor(
+    source_descriptor: int, source_before: os.stat_result, target: Path
+) -> str:
+    flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_CLOEXEC", 0)
+    if hasattr(os, "O_NOFOLLOW"):
+        flags |= os.O_NOFOLLOW
+    target_descriptor = os.open(target, flags, 0o600)
+    digest = hashlib.sha256()
+    try:
+        os.fchmod(target_descriptor, 0o600)
+        _inherit_parent_owner(target_descriptor, target.parent)
+        os.lseek(source_descriptor, 0, os.SEEK_SET)
+        while True:
+            chunk = os.read(source_descriptor, 1_048_576)
+            if not chunk:
+                break
+            digest.update(chunk)
+            view = memoryview(chunk)
+            while view:
+                written = os.write(target_descriptor, view)
+                view = view[written:]
+        os.fsync(target_descriptor)
+    finally:
+        os.close(target_descriptor)
+    source_after = os.fstat(source_descriptor)
+    snapshot_fields = (
+        "st_dev", "st_ino", "st_mode", "st_nlink", "st_uid", "st_gid",
+        "st_size", "st_mtime_ns", "st_ctime_ns",
+    )
+    if any(
+        getattr(source_before, field) != getattr(source_after, field)
+        for field in snapshot_fields
+    ):
+        raise AdminError("RELEASE_REJECTED", 3)
+    copied_digest = digest.hexdigest()
+    if _sha256_file(target) != copied_digest:
+        raise AdminError("RELEASE_REJECTED", 3)
+    return copied_digest
+
+
+def _layout(root: Path) -> None:
+    for name in (
+        "queue", "artifacts", "releases", "secrets", "certificates",
+        "registration", "state",
+    ):
+        _mkdir(root, name)
+
+
+@contextmanager
+def _process_lock(root: Path, uid: int | None = None, gid: int | None = None):
+    if uid is None or gid is None:
+        uid, gid = _runtime_identity(root)
+    lock_path = root / "state" / "admin.lock"
+    flags = os.O_RDWR | os.O_CREAT | getattr(os, "O_CLOEXEC", 0)
+    if hasattr(os, "O_NOFOLLOW"):
+        flags |= os.O_NOFOLLOW
+    try:
+        descriptor = os.open(lock_path, flags, 0o600)
+    except OSError as exc:
+        raise AdminError("LOCK_POLICY", 3) from exc
+    try:
+        if os.geteuid() == 0:
+            os.fchown(descriptor, uid, gid)
+        os.fchmod(descriptor, 0o600)
+        metadata = os.fstat(descriptor)
+        if (
+            not stat.S_ISREG(metadata.st_mode)
+            or stat.S_IMODE(metadata.st_mode) != 0o600
+            or metadata.st_nlink != 1
+            or (metadata.st_uid, metadata.st_gid) != (uid, gid)
+        ):
+            raise AdminError("LOCK_POLICY", 3)
+        try:
+            fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
+        except BlockingIOError as exc:
+            raise AdminError("CONCURRENT_OPERATION", 3) from exc
+        yield
+    finally:
+        os.close(descriptor)
+
+
+def _bounded_backups(directory: Path, prefix: str) -> None:
+    candidates = sorted(
+        (path for path in directory.glob(f"{prefix}*") if path.is_file() and not path.is_symlink()),
+        key=lambda path: path.stat().st_mtime_ns,
+        reverse=True,
+    )
+    for path in candidates[MAX_BACKUPS:]:
+        path.unlink()
+
+
+def command_init(args: argparse.Namespace) -> dict[str, object]:
+    if (
+        isinstance(args.runtime_uid, bool)
+        or not 1 <= args.runtime_uid <= 2_147_483_647
+        or isinstance(args.runtime_gid, bool)
+        or not 1 <= args.runtime_gid <= 2_147_483_647
+    ):
+        raise AdminError("INVALID_INPUT")
+    root = _root(args.root, create=True)
+    identity_path = root / "state" / "runtime-identity.json"
+    if identity_path.exists():
+        uid, gid = _runtime_identity(root)
+        if (uid, gid) != (args.runtime_uid, args.runtime_gid):
+            raise AdminError("OWNER_POLICY")
+        target = root / "edge-config.template.json"
+        if not target.is_file() or target.is_symlink():
+            raise AdminError("PATH_POLICY")
+        with _process_lock(root):
+            return {
+                "config_template_path": str(target),
+                "root": str(root),
+                "runtime_gid": gid,
+                "runtime_uid": uid,
+                "status": "initialized",
+            }
+    if any(root.iterdir()):
+        raise AdminError("PATH_POLICY")
+    os.chmod(root, 0o700)
+    current = root.stat()
+    if (current.st_uid, current.st_gid) != (args.runtime_uid, args.runtime_gid):
+        if not args.allow_chown:
+            raise AdminError("OWNER_POLICY")
+        _chown_exact(root, args.runtime_uid, args.runtime_gid)
+    _layout(root)
+    for name in (
+        "queue", "artifacts", "releases", "secrets", "certificates",
+        "registration", "state",
+    ):
+        _chown_exact(root / name, args.runtime_uid, args.runtime_gid)
+    identity = {
+        "runtime_gid": args.runtime_gid,
+        "runtime_uid": args.runtime_uid,
+        "schema_version": 1,
+    }
+    with _process_lock(root, args.runtime_uid, args.runtime_gid):
+        _atomic_write(identity_path, _json_bytes(identity) + b"\n", 0o600)
+        _chown_exact(identity_path, args.runtime_uid, args.runtime_gid)
+        template = {
+            "allowed_control_hosts": [],
+            "allowed_proxy_hosts": [],
+            "control_origin": None,
+            "environment": None,
+            "gateway_id": None,
+            "network_zone": None,
+            "policy_digest": None,
+            "proxy_origin": None,
+            "runtime_gid": args.runtime_gid,
+            "runtime_uid": args.runtime_uid,
+            "schema_version": 1,
+        }
+        target = root / "edge-config.template.json"
+        _atomic_write(target, _json_bytes(template) + b"\n", 0o600)
+        _chown_exact(target, args.runtime_uid, args.runtime_gid)
+    return {
+        "config_template_path": str(target),
+        "root": str(root),
+        "runtime_gid": args.runtime_gid,
+        "runtime_uid": args.runtime_uid,
+        "status": "initialized",
+    }
+
+
+def _new_csr(root: Path, common_name: str, prefix: str) -> dict[str, object]:
+    if not IDENTIFIER_RE.fullmatch(common_name):
+        raise AdminError("INVALID_INPUT")
+    key_path = root / "secrets" / f"{prefix}.key"
+    csr_path = root / "certificates" / f"{prefix}.csr"
+    if key_path.exists() or csr_path.exists():
+        raise AdminError("MATERIAL_EXISTS")
+    private = rsa.generate_private_key(public_exponent=65537, key_size=3072)
+    csr = (
+        x509.CertificateSigningRequestBuilder()
+        .subject_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, common_name)]))
+        .sign(private, hashes.SHA256())
+    )
+    key_bytes = private.private_bytes(
+        serialization.Encoding.PEM,
+        serialization.PrivateFormat.PKCS8,
+        serialization.NoEncryption(),
+    )
+    csr_bytes = csr.public_bytes(serialization.Encoding.PEM)
+    try:
+        _atomic_write(key_path, key_bytes, 0o600)
+        _atomic_write(csr_path, csr_bytes, 0o644)
+    except Exception:
+        if key_path.exists():
+            key_path.unlink()
+        if csr_path.exists():
+            csr_path.unlink()
+        raise
+    return {
+        "csr_path": str(csr_path),
+        "private_key_path": str(key_path),
+        "public_key_sha256": hashlib.sha256(
+            csr.public_key().public_bytes(
+                serialization.Encoding.DER,
+                serialization.PublicFormat.SubjectPublicKeyInfo,
+            )
+        ).hexdigest(),
+        "status": "csr_created",
+    }
+
+
+def command_csr(args: argparse.Namespace) -> dict[str, object]:
+    root = _validated_root(args.root)
+    _layout(root)
+    return _new_csr(root, args.common_name, "client")
+
+
+def _verify_x509_signature(value, issuer_public_key) -> None:
+    try:
+        if isinstance(issuer_public_key, rsa.RSAPublicKey):
+            issuer_public_key.verify(
+                value.signature, value.tbs_certificate_bytes
+                if isinstance(value, x509.Certificate)
+                else value.tbs_certlist_bytes,
+                padding.PKCS1v15(), value.signature_hash_algorithm,
+            )
+        elif isinstance(issuer_public_key, ec.EllipticCurvePublicKey):
+            issuer_public_key.verify(
+                value.signature, value.tbs_certificate_bytes
+                if isinstance(value, x509.Certificate)
+                else value.tbs_certlist_bytes,
+                ec.ECDSA(value.signature_hash_algorithm),
+            )
+        elif isinstance(issuer_public_key, (ed25519.Ed25519PublicKey, ed448.Ed448PublicKey)):
+            issuer_public_key.verify(
+                value.signature, value.tbs_certificate_bytes
+                if isinstance(value, x509.Certificate)
+                else value.tbs_certlist_bytes,
+            )
+        else:
+            raise AdminError("MATERIAL_INVALID")
+    except (InvalidSignature, ValueError, TypeError) as exc:
+        raise AdminError("MATERIAL_INVALID") from exc
+
+
+def _load_material(
+    root: Path, certificate_raw: str, ca_raw: str, crl_raw: str,
+    prefix: str = "client",
+):
+    certificate_path = _inside(root, certificate_raw, must_exist=True)
+    ca_path = _inside(root, ca_raw, must_exist=True)
+    crl_path = _inside(root, crl_raw, must_exist=True)
+    key_path = _inside(root, root / "secrets" / f"{prefix}.key", must_exist=True)
+    csr_path = _inside(root, root / "certificates" / f"{prefix}.csr", must_exist=True)
+    if stat.S_IMODE(key_path.stat().st_mode) & 0o077:
+        raise AdminError("KEY_PERMISSION")
+    try:
+        certificate = x509.load_pem_x509_certificate(certificate_path.read_bytes())
+        authorities = x509.load_pem_x509_certificates(ca_path.read_bytes())
+        crl = x509.load_pem_x509_crl(crl_path.read_bytes())
+        private = serialization.load_pem_private_key(key_path.read_bytes(), password=None)
+        csr = x509.load_pem_x509_csr(csr_path.read_bytes())
+    except (ValueError, TypeError, OSError) as exc:
+        raise AdminError("MATERIAL_INVALID") from exc
+    public = certificate.public_key().public_bytes(
+        serialization.Encoding.DER, serialization.PublicFormat.SubjectPublicKeyInfo
+    )
+    if public != private.public_key().public_bytes(
+        serialization.Encoding.DER, serialization.PublicFormat.SubjectPublicKeyInfo
+    ) or public != csr.public_key().public_bytes(
+        serialization.Encoding.DER, serialization.PublicFormat.SubjectPublicKeyInfo
+    ) or not csr.is_signature_valid:
+        raise AdminError("MATERIAL_MISMATCH")
+    now = datetime.now(UTC)
+    if certificate.not_valid_before_utc > now or certificate.not_valid_after_utc <= now:
+        raise AdminError("MATERIAL_INVALID")
+    try:
+        usages = certificate.extensions.get_extension_for_class(
+            x509.ExtendedKeyUsage
+        ).value
+    except x509.ExtensionNotFound as exc:
+        raise AdminError("MATERIAL_INVALID") from exc
+    if ExtendedKeyUsageOID.CLIENT_AUTH not in usages:
+        raise AdminError("MATERIAL_INVALID")
+    issuer = next(
+        (authority for authority in authorities if authority.subject == certificate.issuer),
+        None,
+    )
+    if issuer is None:
+        raise AdminError("MATERIAL_INVALID")
+    try:
+        basic = issuer.extensions.get_extension_for_class(x509.BasicConstraints).value
+    except x509.ExtensionNotFound as exc:
+        raise AdminError("MATERIAL_INVALID") from exc
+    if not basic.ca or issuer.not_valid_after_utc <= now:
+        raise AdminError("MATERIAL_INVALID")
+    _verify_x509_signature(certificate, issuer.public_key())
+    if crl.issuer != issuer.subject or crl.last_update_utc > now or crl.next_update_utc <= now:
+        raise AdminError("MATERIAL_INVALID")
+    _verify_x509_signature(crl, issuer.public_key())
+    if crl.get_revoked_certificate_by_serial_number(certificate.serial_number) is not None:
+        raise AdminError("MATERIAL_INVALID")
+    return certificate, csr, ca_path, crl, crl_path
+
+
+def command_register_manifest(args: argparse.Namespace) -> dict[str, object]:
+    root = _validated_root(args.root)
+    certificate, csr, ca_path, crl, crl_path = _load_material(
+        root, args.certificate, args.ca_bundle, args.crl
+    )
+    for value in (args.gateway_id, args.environment, args.network_zone):
+        if not IDENTIFIER_RE.fullmatch(value):
+            raise AdminError("INVALID_INPUT")
+    if args.environment not in {"development", "staging", "production"}:
+        raise AdminError("INVALID_INPUT")
+    if not SHA256_RE.fullmatch(args.policy_digest):
+        raise AdminError("INVALID_INPUT")
+    if not ORIGIN_RE.fullmatch(args.control_origin):
+        raise AdminError("INVALID_INPUT")
+    if args.proxy_origin and not ORIGIN_RE.fullmatch(args.proxy_origin):
+        raise AdminError("INVALID_INPUT")
+    output = root / "registration" / "registration.json"
+    if output.is_symlink():
+        raise AdminError("PATH_POLICY")
+    certificate_digest = certificate.fingerprint(hashes.SHA256()).hex()
+    public_digest = hashlib.sha256(
+        certificate.public_key().public_bytes(
+            serialization.Encoding.DER,
+            serialization.PublicFormat.SubjectPublicKeyInfo,
+        )
+    ).hexdigest()
+    material = {
+        "allowed_control_hosts": [args.control_origin.removeprefix("https://").split(":", 1)[0]],
+        "allowed_proxy_hosts": [] if not args.proxy_origin else [args.proxy_origin.removeprefix("https://").split(":", 1)[0]],
+        "certificate_sha256": certificate_digest,
+        "certificate_not_after": certificate.not_valid_after_utc.isoformat().replace("+00:00", "Z"),
+        "certificate_not_before": certificate.not_valid_before_utc.isoformat().replace("+00:00", "Z"),
+        "control_origin": args.control_origin,
+        "csr_sha256": hashlib.sha256(
+            csr.public_bytes(serialization.Encoding.DER)
+        ).hexdigest(),
+        "public_key_sha256": public_digest,
+        "environment": args.environment,
+        "gateway_id": args.gateway_id,
+        "network_zone": args.network_zone,
+        "policy_digest": args.policy_digest,
+        "proxy_origin": args.proxy_origin,
+        "ca_bundle_sha256": _sha256_file(ca_path),
+        "ca_chain_verified": True,
+        "client_auth_eku_verified": True,
+        "crl_sha256": _sha256_file(crl_path),
+        "crl_verified": True,
+        "crl_this_update": crl.last_update_utc.isoformat().replace("+00:00", "Z"),
+        "crl_next_update": crl.next_update_utc.isoformat().replace("+00:00", "Z"),
+        "schema_version": 1,
+    }
+    replayed = False
+    if output.exists():
+        if _read_json(output) != material:
+            raise AdminError("IDEMPOTENCY_CONFLICT", 3)
+        replayed = True
+    else:
+        _atomic_write(output, _json_bytes(material) + b"\n", 0o600)
+    return {
+        "certificate_sha256": certificate_digest,
+        "manifest_digest": _digest(material),
+        "manifest_path": str(output),
+        "replayed": replayed,
+        "status": "registration_material_ready",
+    }
+
+
+def command_rotate(args: argparse.Namespace) -> dict[str, object]:
+    root = _validated_root(args.root)
+    if not IDENTIFIER_RE.fullmatch(args.request_id) or not SHA256_RE.fullmatch(
+        args.expected_current_certificate_digest
+    ):
+        raise AdminError("INVALID_INPUT")
+    request = {
+        "common_name": args.common_name,
+        "expected_current_certificate_digest": args.expected_current_certificate_digest,
+        "request_id": args.request_id,
+    }
+    request_digest = _digest(request)
+    record_path = root / "state" / f"rotation-{args.request_id}.json"
+    if record_path.exists():
+        record = _read_json(record_path)
+        if record.get("request_digest") != request_digest:
+            raise AdminError("IDEMPOTENCY_CONFLICT", 3)
+        return record
+    pending_records = [
+        path
+        for path in (root / "state").glob("rotation-*.json")
+        if path.is_file()
+        and not path.is_symlink()
+        and _read_json(path).get("status") == "pending_server_approval"
+    ]
+    if len(pending_records) >= MAX_BACKUPS:
+        raise AdminError("ROTATION_LIMIT", 3)
+    prefix = f"pending-{request_digest[:16]}"
+    result = _new_csr(root, args.common_name, prefix)
+    record = {
+        **result,
+        "expected_current_certificate_digest": args.expected_current_certificate_digest,
+        "request_digest": request_digest,
+        "request_id": args.request_id,
+        "status": "pending_server_approval",
+    }
+    _atomic_write(record_path, _json_bytes(record) + b"\n", 0o600)
+    return record
+
+
+def _trusted_keys(bindings: list[str]) -> dict[str, bytes]:
+    keys: dict[str, bytes] = {}
+    for binding in bindings:
+        key_id, separator, key_hex = binding.partition("=")
+        if (
+            not separator
+            or not IDENTIFIER_RE.fullmatch(key_id)
+            or not re.fullmatch(r"[0-9a-f]{64}", key_hex)
+        ):
+            raise AdminError("INVALID_INPUT")
+        keys[key_id] = bytes.fromhex(key_hex)
+    return keys
+
+
+STATUS_FIELDS = frozenset(
+    {
+        "certificate_sha256", "expires_at", "gateway_id", "generation",
+        "issued_at", "key_id", "signature_algorithm", "status",
+        "envelope_digest", "signature",
+    }
+)
+
+
+def command_revoke_status(args: argparse.Namespace) -> dict[str, object]:
+    root = _validated_root(args.root)
+    local_marker = root / "state" / "REVOKED_STOP"
+    status = "revoked" if local_marker.exists() else "not_revoked_locally"
+    evidence_digest = None
+    if args.server_status:
+        evidence_path = _inside(root, args.server_status, must_exist=True)
+        evidence = _read_json(evidence_path)
+        if set(evidence) != STATUS_FIELDS:
+            raise AdminError("STATUS_REJECTED", 3)
+        unsigned = {
+            key: evidence[key]
+            for key in sorted(STATUS_FIELDS - {"envelope_digest", "signature"})
+        }
+        try:
+            issued = datetime.fromisoformat(str(evidence["issued_at"]).replace("Z", "+00:00"))
+            expires = datetime.fromisoformat(str(evidence["expires_at"]).replace("Z", "+00:00"))
+            generation = int(evidence["generation"])
+        except (TypeError, ValueError) as exc:
+            raise AdminError("STATUS_REJECTED", 3) from exc
+        now = datetime.now(UTC)
+        if (
+            not str(evidence["issued_at"]).endswith("Z")
+            or not str(evidence["expires_at"]).endswith("Z")
+            or issued.tzinfo is None
+            or expires.tzinfo is None
+            or issued.astimezone(UTC) > now
+            or expires.astimezone(UTC) <= now
+            or evidence["signature_algorithm"] != "Ed25519"
+            or evidence["status"] not in {"active", "revoked"}
+            or evidence["gateway_id"] != args.expected_gateway_id
+            or generation != args.expected_generation
+            or evidence["certificate_sha256"] != args.expected_certificate_digest
+            or _protocol_digest(unsigned) != evidence["envelope_digest"]
+        ):
+            raise AdminError("STATUS_REJECTED", 3)
+        public = _trusted_keys(args.trusted_key).get(str(evidence["key_id"]))
+        if public is None:
+            raise AdminError("STATUS_REJECTED", 3)
+        try:
+            ed25519.Ed25519PublicKey.from_public_bytes(public).verify(
+                bytes.fromhex(str(evidence["signature"])), _protocol_bytes(unsigned)
+            )
+        except (InvalidSignature, ValueError) as exc:
+            raise AdminError("STATUS_REJECTED", 3) from exc
+        if evidence["status"] == "revoked":
+            _atomic_write(local_marker, _json_bytes(evidence) + b"\n", 0o600)
+            status = "revoked"
+        evidence_digest = evidence["envelope_digest"]
+    return {
+        "evidence_digest": evidence_digest,
+        "local_stop_required": status == "revoked",
+        "status": status,
+    }
+
+
+def _version_tuple(value: str) -> tuple[int, int, int, int]:
+    if not VERSION_RE.fullmatch(value):
+        raise AdminError("RELEASE_REJECTED", 3)
+    values = tuple(int(item) for item in value.split("."))
+    if any(item > 2_147_483_647 for item in values):
+        raise AdminError("RELEASE_REJECTED", 3)
+    return (*values, 0)  # type: ignore[return-value]
+
+
+def _verify_release_manifest(
+    manifest: dict[str, object], artifact_name_actual: str,
+    artifact_digest_actual: str, trusted_keys: dict[str, bytes],
+    *, current_version: str | None = None,
+) -> dict[str, object]:
+    if set(manifest) != MANIFEST_FIELDS:
+        raise AdminError("RELEASE_REJECTED", 3)
+    if not IDENTIFIER_RE.fullmatch(str(manifest["release_id"])):
+        raise AdminError("RELEASE_REJECTED", 3)
+    artifact_name = manifest["artifact_name"]
+    if (
+        not isinstance(artifact_name, str)
+        or PurePosixPath(artifact_name).name != artifact_name
+        or artifact_name != artifact_name_actual
+        or artifact_name in {".", ".."}
+    ):
+        raise AdminError("RELEASE_REJECTED", 3)
+    if (
+        manifest["signature_algorithm"] != "Ed25519"
+        or manifest["status"] not in {
+            "offered", "accepted", "installed", "rolled_back"
+        }
+    ):
+        raise AdminError("RELEASE_REJECTED", 3)
+    version_tuple = _version_tuple(str(manifest["version"]))
+    rollback_tuple = _version_tuple(str(manifest["rollback_version"]))
+    if version_tuple <= rollback_tuple:
+        raise AdminError("RELEASE_REJECTED", 3)
+    if current_version is not None and (
+        manifest["rollback_version"] != current_version
+        or version_tuple <= _version_tuple(current_version)
+    ):
+        raise AdminError("RELEASE_REJECTED", 3)
+    try:
+        deadline = datetime.fromisoformat(str(manifest["deadline_at"]).replace("Z", "+00:00"))
+    except ValueError as exc:
+        raise AdminError("RELEASE_REJECTED", 3) from exc
+    if deadline.tzinfo is None or deadline.astimezone(UTC) <= datetime.now(UTC):
+        raise AdminError("RELEASE_REJECTED", 3)
+    if not str(manifest["deadline_at"]).endswith("Z"):
+        raise AdminError("RELEASE_REJECTED", 3)
+    unsigned = {
+        key: manifest[key]
+        for key in sorted(MANIFEST_FIELDS - {"manifest_digest", "signature"})
+    }
+    if not SHA256_RE.fullmatch(str(manifest["manifest_digest"])) or _protocol_digest(unsigned) != manifest["manifest_digest"]:
+        raise AdminError("RELEASE_REJECTED", 3)
+    if not SHA256_RE.fullmatch(str(manifest["artifact_digest"])):
+        raise AdminError("RELEASE_REJECTED", 3)
+    if artifact_digest_actual != manifest["artifact_digest"]:
+        raise AdminError("RELEASE_REJECTED", 3)
+    public = trusted_keys.get(str(manifest["key_id"]))
+    if public is None:
+        raise AdminError("RELEASE_REJECTED", 3)
+    try:
+        ed25519.Ed25519PublicKey.from_public_bytes(public).verify(
+            bytes.fromhex(str(manifest["signature"])), _protocol_bytes(unsigned)
+        )
+    except (ValueError, InvalidSignature) as exc:
+        raise AdminError("RELEASE_REJECTED", 3) from exc
+    return {
+        "artifact_digest": artifact_digest_actual,
+        "content_digest": manifest["manifest_digest"],
+        "manifest": manifest,
+    }
+
+
+def _bundle_record(manifest: dict[str, object]) -> dict[str, object]:
+    unsigned = {
+        "artifact_digest": manifest["artifact_digest"],
+        "artifact_name": manifest["artifact_name"],
+        "manifest_digest": manifest["manifest_digest"],
+        "schema_version": 1,
+    }
+    return {**unsigned, "bundle_digest": _protocol_digest(unsigned)}
+
+
+def _verified_bundle(root: Path, manifest_digest: str) -> Path:
+    if not SHA256_RE.fullmatch(manifest_digest):
+        raise AdminError("RELEASE_REJECTED", 3)
+    verified = root / "releases" / "verified"
+    if verified.is_symlink() or not verified.is_dir():
+        raise AdminError("PATH_POLICY")
+    bundle = verified / manifest_digest
+    if bundle.is_symlink() or not bundle.is_dir():
+        raise AdminError("RELEASE_REJECTED", 3)
+    return bundle
+
+
+def _verify_bundle(
+    root: Path, manifest_digest: str, trusted_keys: dict[str, bytes]
+) -> tuple[dict[str, object], dict[str, object], Path]:
+    bundle = _verified_bundle(root, manifest_digest)
+    manifest_path = _inside(root, bundle / "manifest.json", must_exist=True)
+    manifest = _read_json(manifest_path)
+    if manifest.get("manifest_digest") != manifest_digest:
+        raise AdminError("RELEASE_REJECTED", 3)
+    artifact_name = manifest.get("artifact_name")
+    if not isinstance(artifact_name, str):
+        raise AdminError("RELEASE_REJECTED", 3)
+    artifact_path = _inside(root, bundle / artifact_name, must_exist=True)
+    artifact_descriptor, artifact_metadata = _open_root_file(root, artifact_path)
+    try:
+        digest = hashlib.sha256()
+        for chunk in iter(lambda: os.read(artifact_descriptor, 1_048_576), b""):
+            digest.update(chunk)
+        if os.fstat(artifact_descriptor) != artifact_metadata:
+            raise AdminError("RELEASE_REJECTED", 3)
+    finally:
+        os.close(artifact_descriptor)
+    _verify_release_manifest(manifest, artifact_name, digest.hexdigest(), trusted_keys)
+    bundle_path = _inside(root, bundle / "bundle.json", must_exist=True)
+    record = _read_json(bundle_path)
+    expected = _bundle_record(manifest)
+    if record != expected or set(bundle.iterdir()) != {
+        manifest_path, artifact_path, bundle_path
+    }:
+        raise AdminError("RELEASE_REJECTED", 3)
+    return manifest, record, artifact_path
+
+
+def command_release_check(args: argparse.Namespace) -> dict[str, object]:
+    root = _validated_root(args.root)
+    manifest_path = _inside(root, args.manifest)
+    artifact_path = _inside(root, args.artifact)
+    manifest_descriptor = -1
+    artifact_descriptor = -1
+    temporary: Path | None = None
+    try:
+        manifest_descriptor, manifest_before = _open_root_file(root, manifest_path)
+        artifact_descriptor, artifact_before = _open_root_file(root, artifact_path)
+        manifest = _read_json_descriptor(manifest_descriptor)
+        if os.fstat(manifest_descriptor) != manifest_before:
+            raise AdminError("RELEASE_REJECTED", 3)
+        artifact_name = manifest.get("artifact_name")
+        if (
+            not isinstance(artifact_name, str)
+            or not artifact_name
+            or artifact_name in {".", ".."}
+            or len(artifact_name.encode("utf-8")) > 255
+            or PurePosixPath(artifact_name).name != artifact_name
+            or Path(artifact_name).name != artifact_name
+            or artifact_path.name != artifact_name
+        ):
+            raise AdminError("RELEASE_REJECTED", 3)
+        verified_dir = _mkdir(root / "releases", "verified")
+        temporary = Path(tempfile.mkdtemp(prefix=".bundle-", dir=verified_dir))
+        os.chmod(temporary, 0o700)
+        parent_owner = verified_dir.stat()
+        if os.geteuid() == 0:
+            os.chown(
+                temporary, parent_owner.st_uid, parent_owner.st_gid,
+                follow_symlinks=False,
+            )
+        elif (temporary.stat().st_uid, temporary.stat().st_gid) != (
+            parent_owner.st_uid, parent_owner.st_gid
+        ):
+            raise AdminError("OWNER_POLICY")
+        copied_artifact = temporary / artifact_name
+        copied_digest = _copy_descriptor(
+            artifact_descriptor, artifact_before, copied_artifact
+        )
+        keys = _trusted_keys(args.trusted_key)
+        verified = _verify_release_manifest(
+            manifest, artifact_path.name, copied_digest, keys,
+            current_version=args.current_version,
+        )
+        replay_path = root / "state" / "release-replay.json"
+        replay = _read_json(replay_path) if replay_path.exists() else {}
+        content_digest = str(verified["content_digest"])
+        prior = replay.get(str(manifest["release_id"]))
+        if prior is not None and prior != content_digest:
+            raise AdminError("RELEASE_REJECTED", 3)
+        final_bundle = verified_dir / str(manifest["manifest_digest"])
+        replayed = False
+        if final_bundle.exists() or final_bundle.is_symlink():
+            _verify_bundle(root, str(manifest["manifest_digest"]), keys)
+            copied_artifact.unlink()
+            temporary.rmdir()
+            replayed = True
+        else:
+            _atomic_write(
+                temporary / "manifest.json", _json_bytes(manifest) + b"\n", 0o600
+            )
+            _atomic_write(
+                temporary / "bundle.json",
+                _json_bytes(_bundle_record(manifest)) + b"\n", 0o600,
+            )
+            _fsync_directory(temporary)
+            os.replace(temporary, final_bundle)
+            _fsync_directory(verified_dir)
+        replay[str(manifest["release_id"])] = content_digest
+        _atomic_write(replay_path, _json_bytes(replay) + b"\n", 0o600)
+        return {
+            "artifact_digest": manifest["artifact_digest"],
+            "manifest_digest": manifest["manifest_digest"],
+            "replayed": replayed,
+            "release_id": manifest["release_id"],
+            "status": "verified",
+            "verified_bundle_path": str(final_bundle),
+            "version": manifest["version"],
+        }
+    finally:
+        if manifest_descriptor >= 0:
+            os.close(manifest_descriptor)
+        if artifact_descriptor >= 0:
+            os.close(artifact_descriptor)
+        if temporary is not None and temporary.exists():
+            if temporary.is_symlink() or not temporary.is_dir():
+                raise AdminError("PATH_POLICY")
+            for item in temporary.iterdir():
+                if item.is_symlink() or not item.is_file():
+                    raise AdminError("PATH_POLICY")
+                item.unlink()
+            temporary.rmdir()
+            _fsync_directory(temporary.parent)
+
+
+def command_rollback(args: argparse.Namespace) -> dict[str, object]:
+    root = _validated_root(args.root)
+    if not args.yes and (
+        not sys.stdin.isatty()
+        or input("Type ROLLBACK to continue: ").strip() != "ROLLBACK"
+    ):
+        raise AdminError("CONFIRMATION_REQUIRED", 4)
+    if not SHA256_RE.fullmatch(args.expected_current_digest) or not SHA256_RE.fullmatch(args.target_digest):
+        raise AdminError("INVALID_INPUT")
+    keys = _trusted_keys(args.trusted_key)
+    current_path = _inside(root, root / "releases" / "current.json", must_exist=True)
+    current_pointer = _read_json(current_path)
+    history_dir = _mkdir(root / "state", "rollback-history")
+    request_digest = _protocol_digest(
+        {
+            "expected_current_digest": args.expected_current_digest,
+            "target_digest": args.target_digest,
+        }
+    )
+    history_path = history_dir / f"{request_digest}.json"
+    intent_path = root / "state" / "rollback-intent.json"
+    current, current_bundle, _ = _verify_bundle(
+        root, args.expected_current_digest, keys
+    )
+    target, target_bundle, _ = _verify_bundle(root, args.target_digest, keys)
+    expected_pointer = {
+        "artifact_name": current["artifact_name"],
+        "bundle_digest": current_bundle["bundle_digest"],
+        "manifest_digest": current["manifest_digest"],
+        "schema_version": 1,
+        "version": current["version"],
+    }
+    pointer = {
+        "artifact_name": target["artifact_name"],
+        "bundle_digest": target_bundle["bundle_digest"],
+        "manifest_digest": target["manifest_digest"],
+        "schema_version": 1,
+        "version": target["version"],
+    }
+    if (
+        not isinstance(target.get("version"), str)
+        or not isinstance(current.get("version"), str)
+        or _version_tuple(target["version"]) >= _version_tuple(current["version"])
+        or current.get("rollback_version") != target.get("version")
+    ):
+        raise AdminError("RELEASE_REJECTED", 3)
+    if history_path.exists():
+        history = _read_json(history_path)
+        if current_pointer != pointer:
+            raise AdminError("STATE_CONFLICT", 3)
+        if intent_path.exists():
+            expected_intent = {
+                "current_pointer": expected_pointer,
+                "request_digest": request_digest,
+                "target_pointer": pointer,
+            }
+            if _read_json(intent_path) != expected_intent:
+                raise AdminError("STATE_CONFLICT", 3)
+            intent_path.unlink()
+            _fsync_directory(intent_path.parent)
+        return {**history, "replayed": True}
+    if intent_path.exists():
+        intent = _read_json(intent_path)
+        if (
+            intent.get("request_digest") != request_digest
+            or intent.get("current_pointer") != expected_pointer
+            or intent.get("target_pointer") != pointer
+        ):
+            raise AdminError("STATE_CONFLICT", 3)
+        if current_pointer == pointer:
+            recovered = {
+                "current_manifest_digest": args.target_digest,
+                "previous_manifest_digest": args.expected_current_digest,
+                "replayed": True,
+                "status": "rolled_back",
+                "version": target.get("version"),
+            }
+            _atomic_write(history_path, _json_bytes(recovered) + b"\n", 0o600)
+            intent_path.unlink()
+            _fsync_directory(intent_path.parent)
+            return recovered
+    if current_pointer != expected_pointer:
+        raise AdminError("STATE_CONFLICT", 3)
+    backup_dir = _mkdir(root / "releases", "backups")
+    backup_path = backup_dir / f"current-{args.expected_current_digest}.json"
+    if not backup_path.exists():
+        _atomic_write(backup_path, _json_bytes(current_pointer) + b"\n", 0o600)
+    _bounded_backups(backup_dir, "current-")
+    intent = {
+        "current_pointer": expected_pointer,
+        "request_digest": request_digest,
+        "target_pointer": pointer,
+    }
+    if intent_path.exists() and _read_json(intent_path) != intent:
+        raise AdminError("STATE_CONFLICT", 3)
+    if not intent_path.exists():
+        _atomic_write(intent_path, _json_bytes(intent) + b"\n", 0o600)
+    if _read_json(current_path) != current_pointer:
+        raise AdminError("STATE_CONFLICT", 3)
+    _atomic_write(current_path, _json_bytes(pointer) + b"\n", 0o600)
+    result = {
+        "current_manifest_digest": args.target_digest,
+        "previous_manifest_digest": args.expected_current_digest,
+        "replayed": False,
+        "status": "rolled_back",
+        "version": target.get("version"),
+    }
+    _atomic_write(history_path, _json_bytes(result) + b"\n", 0o600)
+    intent_path.unlink()
+    _fsync_directory(intent_path.parent)
+    return result
+
+
+def command_status(args: argparse.Namespace) -> dict[str, object]:
+    root = _validated_root(args.root)
+    queue_files = [
+        path for path in (root / "queue").glob("*.sqlite3")
+        if path.is_file() and not path.is_symlink()
+    ]
+    if len(queue_files) > 1:
+        raise AdminError("STATE_CONFLICT", 3)
+    queue_status: dict[str, object] = {
+        "artifact_cleanup": {}, "events": {}, "outcomes": {}, "releases": {},
+        "schema_version": None, "tasks": {},
+    }
+    active_leases = 0
+    if queue_files:
+        queue_path = _inside(root, queue_files[0], must_exist=True)
+        connection = sqlite3.connect(f"file:{queue_path}?mode=ro&immutable=1", uri=True)
+        try:
+            queue_status["schema_version"] = int(
+                connection.execute("PRAGMA user_version").fetchone()[0]
+            )
+            table_specs = {
+                "tasks": ("edge_tasks", "status"),
+                "events": ("edge_outbound_events", "status"),
+                "outcomes": ("edge_task_outcomes", "status"),
+                "artifact_cleanup": ("edge_local_artifacts", "cleanup_status"),
+                "releases": ("edge_release_state", "status"),
+            }
+            for label, (table, column) in table_specs.items():
+                rows = connection.execute(
+                    f"SELECT {column},COUNT(*) FROM {table} GROUP BY {column}"  # noqa: S608
+                ).fetchall()
+                queue_status[label] = {str(status): int(count) for status, count in rows}
+            active_leases = sum(
+                int(connection.execute(query).fetchone()[0])
+                for query in (
+                    "SELECT COUNT(*) FROM edge_tasks WHERE status='leased'",
+                    "SELECT COUNT(*) FROM edge_outbound_events WHERE status='sending'",
+                    "SELECT COUNT(*) FROM edge_task_outcomes WHERE status='sending'",
+                    "SELECT COUNT(*) FROM edge_local_artifacts WHERE cleanup_status='deleting'",
+                )
+            )
+        except sqlite3.DatabaseError as exc:
+            raise AdminError("QUEUE_STATUS_FAILED", 3) from exc
+        finally:
+            connection.close()
+    current = None
+    current_path = root / "releases" / "current.json"
+    if current_path.exists():
+        pointer = _read_json(current_path)
+        current = {
+            "manifest_digest": pointer.get("manifest_digest"),
+            "version": pointer.get("version"),
+        }
+    return {
+        "active_leases": active_leases,
+        "current_release": current,
+        "cursor_state": "reconciled_per_cycle_not_persisted",
+        "queue": queue_status,
+        "registration_present": (root / "registration" / "registration.json").is_file(),
+        "rollback_intent_present": (root / "state" / "rollback-intent.json").is_file(),
+        "status": "read_only",
+    }
+
+
+def parser() -> argparse.ArgumentParser:
+    main = argparse.ArgumentParser(description=__doc__)
+    subcommands = main.add_subparsers(dest="command", required=True)
+    init = subcommands.add_parser("init")
+    init.add_argument("--root", required=True)
+    init.add_argument("--runtime-uid", required=True, type=int)
+    init.add_argument("--runtime-gid", required=True, type=int)
+    init.add_argument("--allow-chown", action="store_true")
+    init.set_defaults(handler=command_init)
+    csr = subcommands.add_parser("csr")
+    csr.add_argument("--root", required=True)
+    csr.add_argument("--common-name", required=True)
+    csr.set_defaults(handler=command_csr)
+    registration = subcommands.add_parser("register-manifest")
+    registration.add_argument("--root", required=True)
+    registration.add_argument("--certificate", required=True)
+    registration.add_argument("--ca-bundle", required=True)
+    registration.add_argument("--crl", required=True)
+    registration.add_argument("--gateway-id", required=True)
+    registration.add_argument("--environment", required=True)
+    registration.add_argument("--network-zone", required=True)
+    registration.add_argument("--policy-digest", required=True)
+    registration.add_argument("--control-origin", required=True)
+    registration.add_argument("--proxy-origin")
+    registration.set_defaults(handler=command_register_manifest)
+    rotate = subcommands.add_parser("rotate")
+    rotate.add_argument("--root", required=True)
+    rotate.add_argument("--common-name", required=True)
+    rotate.add_argument("--request-id", required=True)
+    rotate.add_argument("--expected-current-certificate-digest", required=True)
+    rotate.set_defaults(handler=command_rotate)
+    revoke = subcommands.add_parser("revoke-status")
+    revoke.add_argument("--root", required=True)
+    revoke.add_argument("--server-status")
+    revoke.add_argument("--trusted-key", action="append", default=[])
+    revoke.add_argument("--expected-gateway-id")
+    revoke.add_argument("--expected-generation", type=int)
+    revoke.add_argument("--expected-certificate-digest")
+    revoke.set_defaults(handler=command_revoke_status)
+    check = subcommands.add_parser("release-check")
+    check.add_argument("--root", required=True)
+    check.add_argument("--manifest", required=True)
+    check.add_argument("--artifact", required=True)
+    check.add_argument("--trusted-key", action="append", required=True)
+    check.add_argument("--current-version", required=True)
+    check.set_defaults(handler=command_release_check)
+    rollback = subcommands.add_parser("rollback")
+    rollback.add_argument("--root", required=True)
+    rollback.add_argument("--expected-current-digest", required=True)
+    rollback.add_argument("--target-digest", required=True)
+    rollback.add_argument("--trusted-key", action="append", required=True)
+    rollback.add_argument("--yes", action="store_true")
+    rollback.set_defaults(handler=command_rollback)
+    status_command = subcommands.add_parser("status")
+    status_command.add_argument("--root", required=True)
+    status_command.set_defaults(handler=command_status)
+    return main
+
+
+def main() -> int:
+    try:
+        args = parser().parse_args()
+        if args.command in {"init", "status"}:
+            result = args.handler(args)
+        else:
+            root = _validated_root(args.root)
+            with _process_lock(root):
+                result = args.handler(args)
+        _output(result)
+        return 0
+    except AdminError as exc:
+        _output({"error": {"code": exc.code}, "status": "error"})
+        return exc.exit_code
+    except (OSError, ValueError, TypeError, MemoryError, RecursionError):
+        _output({"error": {"code": "OPERATION_FAILED"}, "status": "error"})
+        return 5
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())

+ 307 - 12
scripts/generate_openapi.py

@@ -28,6 +28,45 @@ PREFIXES = {
 }
 
 SHORTHAND_METHODS = {"get", "post", "put", "patch", "delete"}
+EDGE_SAFE_KEY_PATTERN = (
+    "^(?!.*(?:raw|rows?|sql|credential|secret|private|password|token|local_path))"
+    "[a-z][a-z0-9_]{0,79}$"
+)
+
+
+def _edge_safe_schema_lines() -> list[str]:
+    lines = [
+        "    EdgeSafeSummary:",
+        "      type: object",
+        "      x-max-policy-depth: 12",
+        "      maxProperties: 64",
+        f"      propertyNames: {{type: string, minLength: 1, maxLength: 80, pattern: '{EDGE_SAFE_KEY_PATTERN}'}}",
+        "      additionalProperties: {$ref: '#/components/schemas/EdgeSafeValue12'}",
+        "      description: 'Policy-approved bounded metadata/statistics/health/diagnostic fields only; raw rows, SQL, credentials, and local paths are forbidden.'",
+        "    EdgeSafeScalar:",
+        "      anyOf:",
+        "        - {type: string, maxLength: 4096}",
+        "        - {type: integer, minimum: -9007199254740991, maximum: 9007199254740991}",
+        "        - {type: number, minimum: -1000000000000, maximum: 1000000000000}",
+        "        - {type: boolean}",
+        "        - {type: 'null'}",
+        "    EdgeSafeValue0: {$ref: '#/components/schemas/EdgeSafeScalar'}",
+    ]
+    for depth in range(1, 13):
+        previous = depth - 1
+        lines.extend(
+            [
+                f"    EdgeSafeValue{depth}:",
+                "      anyOf:",
+                "        - {$ref: '#/components/schemas/EdgeSafeScalar'}",
+                f"        - {{type: array, maxItems: 100, items: {{$ref: '#/components/schemas/EdgeSafeValue{previous}'}}}}",
+                f"        - {{type: object, maxProperties: 32, propertyNames: {{type: string, minLength: 1, maxLength: 80, pattern: '{EDGE_SAFE_KEY_PATTERN}'}}, additionalProperties: {{$ref: '#/components/schemas/EdgeSafeValue{previous}'}}}}",
+            ]
+        )
+    lines.append(
+        "    EdgeSafeValue: {$ref: '#/components/schemas/EdgeSafeValue12'}"
+    )
+    return lines
 RESPONSE_FIELDS = {
     ("data_development", "summary"): [
         "scope",
@@ -109,11 +148,49 @@ NO_REQUEST_BODY_PATHS = {
     "/api/datasource/connectors/machine/runs/{idempotency_key}/cancel",
     "/api/datasource/connectors/machine/runs/{idempotency_key}/resume",
     "/api/datasource/connectors/source-bindings/{binding_uid}/revoke",
+    "/api/datasource/edge/tasks/{task_id}/cancel",
+    "/api/datasource/edge/gateways/{gateway_id}/revoke",
 }
 OPTIONAL_REQUEST_BODY_PATHS = {
     "/api/datasource/connectors/principals/{principal_uid}/credentials",
     "/api/datasource/connectors/credentials/{credential_uid}/{action}",
 }
+EDGE_REQUEST_SCHEMAS = {
+    "/api/datasource/edge/enrollments": "EdgeEnrollmentRequest",
+    "/api/datasource/edge/register": "EdgeRegisterRequest",
+    "/api/datasource/edge/gateways/{gateway_id}/heartbeat": "EdgeHeartbeatRequest",
+    "/api/datasource/edge/gateways/{gateway_id}/rotate": "EdgeRotateRequest",
+    "/api/datasource/edge/tasks": "EdgeTaskContract",
+    "/api/datasource/edge/gateways/{gateway_id}/tasks/pull": "EdgeMachineBinding",
+    "/api/datasource/edge/gateways/{gateway_id}/tasks/{task_id}/outcome": "EdgeTaskOutcomeRequest",
+    "/api/datasource/edge/gateways/{gateway_id}/events": "EdgeEventRequest",
+    "/api/datasource/edge/gateways/{gateway_id}/reconcile": "EdgeReconcileRequest",
+    "/api/datasource/edge/gateways/{gateway_id}/releases": "EdgeReleaseOfferRequest",
+    "/api/datasource/edge/gateways/{gateway_id}/releases/ack": "EdgeReleaseAckRequest",
+}
+EDGE_RESPONSE_SCHEMAS = {
+    ("/api/datasource/edge/enrollments", "post"): "EdgeEnrollmentEnvelope",
+    ("/api/datasource/edge/register", "post"): "EdgeRegistrationEnvelope",
+    ("/api/datasource/edge/gateways", "get"): "EdgeGatewayListEnvelope",
+    ("/api/datasource/edge/gateways/{gateway_id}/tasks/pull", "post"): "EdgeTaskPullEnvelope",
+    ("/api/datasource/edge/gateways/{gateway_id}/events", "post"): "EdgeEventAckEnvelope",
+    ("/api/datasource/edge/gateways/{gateway_id}/reconcile", "post"): "EdgeReconcileResponse",
+    ("/api/datasource/edge/gateways/{gateway_id}/releases", "post"): "EdgeReleaseEnvelope",
+}
+EDGE_MACHINE_PATHS = {
+    "/api/datasource/edge/gateways/{gateway_id}/heartbeat",
+    "/api/datasource/edge/gateways/{gateway_id}/tasks/pull",
+    "/api/datasource/edge/gateways/{gateway_id}/tasks/{task_id}/outcome",
+    "/api/datasource/edge/gateways/{gateway_id}/events",
+    "/api/datasource/edge/gateways/{gateway_id}/reconcile",
+    "/api/datasource/edge/gateways/{gateway_id}/releases/ack",
+}
+EDGE_CREATED_PATHS = {
+    "/api/datasource/edge/enrollments",
+    "/api/datasource/edge/register",
+    "/api/datasource/edge/tasks",
+    "/api/datasource/edge/gateways/{gateway_id}/releases",
+}
 
 
 def quoted(value: str) -> str:
@@ -194,7 +271,7 @@ def render(routes: list[dict[str, object]]) -> str:
         "openapi: 3.1.0",
         "info:",
         '  title: "DataOps Platform API(当前代码基线)"',
-        '  version: "2026-07-16"',
+        '  version: "2026-08-09"',
         '  description: "由 scripts/generate_openapi.py 从 app/api/*/routes.py 生成。请求体与响应细节仍以现有专项 API 文档和代码为准。"',
         f"x-route-count: {len(routes)}",
         "servers:",
@@ -218,6 +295,14 @@ def render(routes: list[dict[str, object]]) -> str:
                     f"      x-source: {quoted(str(operation['source']))}",
                 ]
             )
+            if path.startswith("/api/datasource/edge/"):
+                lines.extend(
+                    [
+                        "      x-max-request-bytes: 262144",
+                        "      x-max-json-depth: 32",
+                        "      x-max-json-nodes: 5000",
+                    ]
+                )
             parameters = operation["parameters"]
             if path.startswith("/api/datasource/connectors/machine/runs"):
                 parameters = [
@@ -228,18 +313,48 @@ def render(routes: list[dict[str, object]]) -> str:
                         "header": True,
                     },
                 ]
+            if path == "/api/datasource/edge/register":
+                parameters = [
+                    *parameters,
+                    {"name": "X-Edge-Enrollment", "type": "string", "header": True},
+                    {"name": "X-DataOps-Edge-Client-Verify", "type": "string", "header": True},
+                    {"name": "X-DataOps-Edge-Client-Cert", "type": "string", "header": True},
+                    {"name": "X-Edge-Certificate-SHA256", "type": "string", "header": True},
+                ]
+            elif path in EDGE_MACHINE_PATHS:
+                parameters = [
+                    *parameters,
+                    {"name": "X-Edge-Credential", "type": "string", "header": True},
+                    {"name": "X-DataOps-Edge-Client-Verify", "type": "string", "header": True},
+                    {"name": "X-DataOps-Edge-Client-Cert", "type": "string", "header": True},
+                    {"name": "X-Edge-Certificate-SHA256", "type": "string", "header": True},
+                ]
+            if path == "/api/datasource/edge/gateways" and operation["method"] == "get":
+                parameters = [
+                    *parameters,
+                    {"name": "limit", "type": "integer", "query": True},
+                    {"name": "offset", "type": "integer", "query": True},
+                ]
             if parameters:
                 lines.append("      parameters:")
                 for parameter in parameters:
                     lines.extend(
                         [
                             f"        - name: {parameter['name']}",
-                            f"          in: {'header' if parameter.get('header') else 'path'}",
-                            "          required: true",
+                            f"          in: {'header' if parameter.get('header') else 'query' if parameter.get('query') else 'path'}",
+                            f"          required: {'false' if parameter.get('query') else 'true'}",
                             "          schema:",
                             f"            type: {parameter['type']}",
                         ]
                     )
+                    if parameter["name"] in {"X-Edge-Credential", "X-Edge-Enrollment"}:
+                        lines.extend(
+                            ["            format: password", "            writeOnly: true"]
+                        )
+                    if parameter["name"] == "limit":
+                        lines.extend(["            minimum: 1", "            maximum: 100"])
+                    if parameter["name"] == "offset":
+                        lines.extend(["            minimum: 0", "            maximum: 10000"])
                     if (
                         path
                         == "/api/datasource/connectors/credentials/{credential_uid}/{action}"
@@ -251,6 +366,7 @@ def render(routes: list[dict[str, object]]) -> str:
                 and path not in NO_REQUEST_BODY_PATHS
             ):
                 connector_schema = CONNECTOR_REQUEST_SCHEMAS.get(path)
+                request_schema = connector_schema or EDGE_REQUEST_SCHEMAS.get(path)
                 lines.extend(
                     [
                         "      requestBody:",
@@ -258,17 +374,17 @@ def render(routes: list[dict[str, object]]) -> str:
                         + (
                             "false"
                             if path in OPTIONAL_REQUEST_BODY_PATHS
-                            else "true" if connector_schema else "false"
+                            else "true" if request_schema else "false"
                         ),
                         "        content:",
                         "          application/json:",
                         "            schema:",
-                        f"              $ref: '#/components/schemas/{connector_schema}'"
-                        if connector_schema
+                        f"              $ref: '#/components/schemas/{request_schema}'"
+                        if request_schema
                         else "              type: object",
                     ]
                 )
-                if not connector_schema:
+                if not request_schema:
                     lines.append("              additionalProperties: true")
             response_fields = RESPONSE_FIELDS.get(
                 (str(operation["tag"]), str(operation["operation_id"]).split("_")[-2])
@@ -280,17 +396,19 @@ def render(routes: list[dict[str, object]]) -> str:
             connector_response = CONNECTOR_RESPONSE_SCHEMAS.get(
                 (path, str(operation["method"]))
             )
+            edge_response = EDGE_RESPONSE_SCHEMAS.get(
+                (path, str(operation["method"]))
+            )
             response_status = (
                 "201"
                 if operation["method"] == "post"
-                and path
-                in {
+                and (path in EDGE_CREATED_PATHS or path in {
                     "/api/datasource/connectors/runs",
                     "/api/datasource/connectors/machine/runs",
                     "/api/datasource/connectors/principals",
                     "/api/datasource/connectors/principals/{principal_uid}/credentials",
                     "/api/datasource/connectors/source-bindings",
-                }
+                })
                 else "200"
             )
             if path.startswith("/api/datasource/connectors"):
@@ -316,21 +434,63 @@ def render(routes: list[dict[str, object]]) -> str:
                 )
             elif path == "/api/datasource/graph":
                 lines.append('      x-required-permission: "connectors:read"')
+            elif path.startswith("/api/datasource/edge"):
+                if path == "/api/datasource/edge/register":
+                    permission = "one-time-enrollment+mTLS"
+                elif path in EDGE_MACHINE_PATHS:
+                    permission = "edge-credential+mTLS"
+                elif path == "/api/datasource/edge/gateways" and operation["method"] == "get":
+                    permission = "edge-gateways:read"
+                elif path == "/api/datasource/edge/enrollments":
+                    permission = "edge-gateways:manage"
+                else:
+                    permission = "edge-gateways:operate"
+                lines.append(f"      x-required-permission: {quoted(permission)}")
+                if path == "/api/datasource/edge/register":
+                    lines.extend(
+                        [
+                            "      security:",
+                            "        - mutualTLS: []",
+                            "          edgeEnrollment: []",
+                        ]
+                    )
+                elif path in EDGE_MACHINE_PATHS:
+                    lines.extend(
+                        [
+                            "      security:",
+                            "        - mutualTLS: []",
+                            "          edgeCredential: []",
+                        ]
+                    )
+                else:
+                    lines.extend(["      security:", "        - bearerAuth: []"])
+            success_headers = (
+                [
+                    "          headers:",
+                    "            Cache-Control:",
+                    "              required: true",
+                    "              schema: {type: string, const: no-store}",
+                ]
+                if path.startswith("/api/datasource/edge")
+                else []
+            )
             lines.extend(
                 [
                     "      responses:",
                     f'        "{response_status}":',
                     '          description: "请求已由当前实现处理"',
+                    *success_headers,
                     "          content:",
                     "            application/json:",
                     "              schema:",
-                    f"                $ref: '#/components/schemas/{connector_response or 'ApiEnvelope'}'",
+                    f"                $ref: '#/components/schemas/{connector_response or edge_response or ('EdgeApiEnvelope' if path.startswith('/api/datasource/edge') else 'ApiEnvelope')}'",
                     "        default:",
                     '          description: "错误响应"',
+                    *success_headers,
                     "          content:",
                     "            application/json:",
                     "              schema:",
-                    f"                $ref: '#/components/schemas/{'ConnectorErrorEnvelope' if path.startswith('/api/datasource/connectors') or path == '/api/datasource/graph' else 'ApiEnvelope'}'",
+                    f"                $ref: '#/components/schemas/{'ConnectorErrorEnvelope' if path.startswith('/api/datasource/connectors') or path == '/api/datasource/graph' else 'EdgeErrorEnvelope' if path.startswith('/api/datasource/edge') else 'ApiEnvelope'}'",
                 ]
             )
 
@@ -349,6 +509,129 @@ def render(routes: list[dict[str, object]]) -> str:
             "          type: string",
             "        data: {}",
             "        error: {type: object}",
+            "    EdgeApiEnvelope:",
+            "      type: object",
+            "      additionalProperties: false",
+            "      required: [code, message, data]",
+            "      properties: {code: {type: integer}, message: {type: string}, data: {type: object}}",
+            "    EdgeErrorEnvelope:",
+            "      type: object",
+            "      additionalProperties: false",
+            "      required: [code, message, data, error]",
+            "      properties: {code: {type: integer}, message: {type: string}, data: {type: 'null'}, error: {type: object, additionalProperties: false, required: [code], properties: {code: {type: string, maxLength: 80}}}}",
+            "    EdgeMachineBinding:",
+            "      type: object",
+            "      additionalProperties: false",
+            "      required: [environment, network_zone, generation]",
+            "      properties: {environment: {type: string, enum: [development, staging, production]}, network_zone: {type: string, minLength: 1, maxLength: 120}, generation: {type: integer, minimum: 1}}",
+            "    EdgeEnrollmentRequest:",
+            "      type: object",
+            "      additionalProperties: false",
+            "      required: [gateway_name, environment, network_zone, policy_digest, allowed_control_hosts, allowed_proxy_hosts, expected_certificate_sha256]",
+            "      properties: {gateway_name: {type: string, minLength: 1, maxLength: 200}, environment: {type: string, enum: [development, staging, production]}, network_zone: {type: string, minLength: 1, maxLength: 120}, policy_digest: {$ref: '#/components/schemas/Sha256Digest'}, allowed_control_hosts: {$ref: '#/components/schemas/EdgeExactHosts'}, allowed_proxy_hosts: {$ref: '#/components/schemas/EdgeExactHosts'}, expected_certificate_sha256: {$ref: '#/components/schemas/Sha256Digest'}, ttl_seconds: {type: integer, minimum: 60, maximum: 86400}}",
+            "    EdgeRegisterRequest:",
+            "      type: object",
+            "      additionalProperties: false",
+            "      required: [gateway_id, environment, network_zone, policy_digest, allowed_control_hosts, allowed_proxy_hosts, version]",
+            "      properties: {gateway_id: {type: string, format: uuid}, environment: {type: string, enum: [development, staging, production]}, network_zone: {type: string, minLength: 1, maxLength: 120}, policy_digest: {$ref: '#/components/schemas/Sha256Digest'}, allowed_control_hosts: {$ref: '#/components/schemas/EdgeExactHosts'}, allowed_proxy_hosts: {$ref: '#/components/schemas/EdgeExactHosts'}, version: {$ref: '#/components/schemas/EdgeAgentVersion'}}",
+            "    EdgeHeartbeatRequest:",
+            "      type: object",
+            "      additionalProperties: false",
+            "      required: [environment, network_zone, generation, version, safe_summary]",
+            "      properties: {environment: {type: string, enum: [development, staging, production]}, network_zone: {type: string, minLength: 1, maxLength: 120}, generation: {type: integer, minimum: 1}, version: {$ref: '#/components/schemas/EdgeAgentVersion'}, safe_summary: {$ref: '#/components/schemas/EdgeSafeSummary'}}",
+            "    EdgeRotateRequest:",
+            "      type: object",
+            "      additionalProperties: false",
+            "      required: [certificate_sha256, request_id]",
+            "      properties: {certificate_sha256: {$ref: '#/components/schemas/Sha256Digest'}, request_id: {type: string, minLength: 1, maxLength: 255}}",
+            "    Sha256Digest: {type: string, pattern: '^[a-f0-9]{64}$'}",
+            "    Ed25519Signature: {type: string, pattern: '^[a-f0-9]{128}$'}",
+            "    EdgeExactHosts: {type: array, maxItems: 32, uniqueItems: true, items: {type: string, pattern: '^[a-z0-9][a-z0-9.-]*[a-z0-9]$'}}",
+            *_edge_safe_schema_lines(),
+            "    EdgeIdentifier: {type: string, minLength: 1, maxLength: 255, pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{0,254}$'}",
+            "    EdgeCursor: {type: [string, 'null'], minLength: 1, maxLength: 255, pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{0,254}$'}",
+            "    EdgeAgentVersion: {type: string, minLength: 1, maxLength: 80, pattern: '^[A-Za-z0-9][A-Za-z0-9._+-]{0,79}$'}",
+            "    EdgeVersion: {type: string, pattern: '^(0|[1-9][0-9]{0,9})\\.(0|[1-9][0-9]{0,9})\\.(0|[1-9][0-9]{0,9})$'}",
+            "    EdgeTaskContract:",
+            "      type: object",
+            "      additionalProperties: false",
+            "      required: [task_id, gateway_id, environment, network_zone, purpose, classification, task_type, contract_version, deadline_at, attempt, idempotency_key, policy_digest]",
+            "      properties: {task_id: {$ref: '#/components/schemas/EdgeIdentifier'}, gateway_id: {type: string, format: uuid}, environment: {type: string, enum: [development, staging, production]}, network_zone: {type: string, minLength: 1, maxLength: 120}, purpose: {type: string, minLength: 1, maxLength: 255}, classification: {type: string, enum: [raw, recent_detail, restricted, desensitized_metadata, statistics, lineage, evidence]}, task_type: {type: string, enum: [collect, profile, quality, lineage, controlled_query]}, contract_version: {const: 1}, deadline_at: {type: string, format: date-time}, attempt: {type: integer, minimum: 1, maximum: 5}, idempotency_key: {$ref: '#/components/schemas/EdgeIdentifier'}, policy_digest: {$ref: '#/components/schemas/Sha256Digest'}}",
+            "    EdgeSignedTaskEnvelope:",
+            "      type: object",
+            "      additionalProperties: false",
+            "      required: [task, authority_key_id, signature_algorithm, contract_digest, gateway_id, environment, network_zone, policy_digest, purpose, issued_at, expires_at, signature]",
+            "      properties: {task: {$ref: '#/components/schemas/EdgeTaskContract'}, authority_key_id: {type: string}, signature_algorithm: {const: Ed25519}, contract_digest: {$ref: '#/components/schemas/Sha256Digest'}, gateway_id: {type: string}, environment: {type: string}, network_zone: {type: string}, policy_digest: {$ref: '#/components/schemas/Sha256Digest'}, purpose: {type: string}, issued_at: {type: string, format: date-time}, expires_at: {type: string, format: date-time}, signature: {$ref: '#/components/schemas/Ed25519Signature'}}",
+            "    EdgeEventContract:",
+            "      type: object",
+            "      additionalProperties: false",
+            "      required: [event_id, task_id, gateway_id, environment, network_zone, purpose, classification, contract_version, occurred_at, attempt, idempotency_key, policy_digest, payload]",
+            "      properties: {event_id: {type: string}, task_id: {type: string}, gateway_id: {type: string}, environment: {type: string}, network_zone: {type: string}, purpose: {type: string}, classification: {type: string, enum: [desensitized_metadata, statistics, lineage, evidence, health_summary, diagnostic_summary]}, contract_version: {const: 1}, occurred_at: {type: string, format: date-time}, attempt: {type: integer, minimum: 1, maximum: 5}, idempotency_key: {type: string}, policy_digest: {$ref: '#/components/schemas/Sha256Digest'}, payload: {$ref: '#/components/schemas/EdgeSafeSummary'}}",
+            "    EdgeEventRequest:",
+            "      type: object",
+            "      additionalProperties: false",
+            "      required: [environment, network_zone, generation, event, lease_token]",
+            "      properties: {environment: {type: string}, network_zone: {type: string}, generation: {type: integer, minimum: 1}, event: {$ref: '#/components/schemas/EdgeEventContract'}, lease_token: {type: string, writeOnly: true}}",
+            "    EdgeTaskOutcomeRequest:",
+            "      type: object",
+            "      additionalProperties: false",
+            "      required: [environment, network_zone, generation, outcome, lease_token, safe_summary]",
+            "      properties: {environment: {type: string}, network_zone: {type: string}, generation: {type: integer, minimum: 1}, outcome: {type: string, enum: [completed, failed, cancelled]}, lease_token: {type: string, writeOnly: true}, safe_summary: {$ref: '#/components/schemas/EdgeSafeSummary'}}",
+            "    EdgeReconcileRequest:",
+            "      type: object",
+            "      additionalProperties: false",
+            "      required: [environment, network_zone, generation]",
+            "      properties: {environment: {type: string, enum: [development, staging, production]}, network_zone: {type: string, minLength: 1, maxLength: 120}, generation: {type: integer, minimum: 1}, limit: {type: integer, minimum: 1, maximum: 100}, cancel_cursor: {$ref: '#/components/schemas/EdgeCursor'}, release_cursor: {$ref: '#/components/schemas/EdgeCursor'}}",
+            "    EdgeReconcileResponse:",
+            "      type: object",
+            "      additionalProperties: false",
+            "      required: [code, message, data]",
+            "      properties: {code: {type: integer}, message: {type: string, maxLength: 200}, data: {type: object, additionalProperties: false, required: [cancelled_task_ids, cancel_next_cursor, release_offers, release_next_cursor, release_baseline], properties: {cancelled_task_ids: {type: array, maxItems: 50, items: {$ref: '#/components/schemas/EdgeIdentifier'}}, cancel_next_cursor: {$ref: '#/components/schemas/EdgeCursor'}, release_offers: {type: array, maxItems: 50, items: {$ref: '#/components/schemas/EdgeReleaseManifest'}}, release_next_cursor: {$ref: '#/components/schemas/EdgeCursor'}, release_baseline: {oneOf: [{$ref: '#/components/schemas/EdgeReleaseManifest'}, {type: 'null'}]}}}}",
+            "    EdgeReleaseOfferRequest:",
+            "      type: object",
+            "      additionalProperties: false",
+            "      required: [version, artifact_digest, artifact_name, deadline_at, rollback_version, request_id]",
+            "      properties: {version: {$ref: '#/components/schemas/EdgeVersion'}, artifact_digest: {$ref: '#/components/schemas/Sha256Digest'}, artifact_name: {type: string, minLength: 1, maxLength: 255, pattern: '^[^/\\\\]+$'}, deadline_at: {type: string, format: date-time}, rollback_version: {$ref: '#/components/schemas/EdgeVersion'}, request_id: {$ref: '#/components/schemas/EdgeIdentifier'}}",
+            "    EdgeReleaseAckRequest:",
+            "      type: object",
+            "      additionalProperties: false",
+            "      required: [environment, network_zone, generation, release_id, outcome, safe_summary]",
+            "      properties: {environment: {type: string}, network_zone: {type: string}, generation: {type: integer, minimum: 1}, release_id: {type: string, format: uuid}, outcome: {type: string, enum: [accepted, installed, failed, rollback]}, safe_summary: {$ref: '#/components/schemas/EdgeSafeSummary'}}",
+            "    EdgeReleaseManifest:",
+            "      type: object",
+            "      additionalProperties: false",
+            "      required: [release_id, version, rollback_version, artifact_digest, artifact_name, deadline_at, status, signature_algorithm, key_id, manifest_digest, signature]",
+            "      properties: {release_id: {$ref: '#/components/schemas/EdgeIdentifier'}, version: {$ref: '#/components/schemas/EdgeVersion'}, rollback_version: {$ref: '#/components/schemas/EdgeVersion'}, artifact_digest: {$ref: '#/components/schemas/Sha256Digest'}, artifact_name: {type: string, minLength: 1, maxLength: 255, pattern: '^[^/\\\\]+$'}, deadline_at: {type: string, format: date-time}, status: {type: string, enum: [offered, accepted, failed, installed, rolled_back]}, signature_algorithm: {const: Ed25519}, key_id: {$ref: '#/components/schemas/EdgeIdentifier'}, manifest_digest: {$ref: '#/components/schemas/Sha256Digest'}, signature: {$ref: '#/components/schemas/Ed25519Signature'}}",
+            "    EdgeEnrollmentEnvelope:",
+            "      type: object",
+            "      additionalProperties: false",
+            "      required: [code, message, data]",
+            "      properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [enrollment_id, gateway_id, enrollment_token, expires_in, returned_once], properties: {enrollment_id: {type: string, format: uuid}, gateway_id: {type: string, format: uuid}, enrollment_token: {type: string, writeOnly: true}, expires_in: {type: integer}, returned_once: {const: true}}}}",
+            "    EdgeRegistrationEnvelope:",
+            "      type: object",
+            "      additionalProperties: false",
+            "      required: [code, message, data]",
+            "      properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [gateway_id, generation, certificate_sha256, credential, credential_returned_once], properties: {gateway_id: {type: string, format: uuid}, generation: {type: integer}, certificate_sha256: {$ref: '#/components/schemas/Sha256Digest'}, credential: {type: string, writeOnly: true}, credential_returned_once: {const: true}}}}",
+            "    EdgeGatewayListEnvelope:",
+            "      type: object",
+            "      additionalProperties: false",
+            "      required: [code, message, data]",
+            "      properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [gateways], properties: {gateways: {type: array, maxItems: 100, items: {type: object, description: 'Bounded gateway metadata; credential/token/private key are never included.'}}}}}",
+            "    EdgeTaskPullEnvelope:",
+            "      type: object",
+            "      additionalProperties: false",
+            "      required: [code, message, data]",
+            "      properties: {code: {type: integer}, message: {type: string}, data: {oneOf: [{type: object, additionalProperties: false, required: [task], properties: {task: {type: 'null'}}}, {type: object, additionalProperties: false, required: [task, signed_task_envelope, lease_token, lease_expires_at], properties: {task: {$ref: '#/components/schemas/EdgeTaskContract'}, signed_task_envelope: {$ref: '#/components/schemas/EdgeSignedTaskEnvelope'}, lease_token: {type: string, writeOnly: true}, lease_expires_at: {type: string, format: date-time}}}]}}",
+            "    EdgeEventAckEnvelope:",
+            "      type: object",
+            "      additionalProperties: false",
+            "      required: [code, message, data]",
+            "      properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [event_id, event_digest, remote_lease_digest, received_at, status], properties: {event_id: {type: string}, event_digest: {$ref: '#/components/schemas/Sha256Digest'}, remote_lease_digest: {$ref: '#/components/schemas/Sha256Digest'}, received_at: {type: string, format: date-time}, status: {const: accepted}}}}",
+            "    EdgeReleaseEnvelope:",
+            "      type: object",
+            "      additionalProperties: false",
+            "      required: [code, message, data]",
+            "      properties: {code: {type: integer}, message: {type: string, maxLength: 200}, data: {type: object, additionalProperties: false, required: [release_id, status, version, artifact_digest, rollback_version, manifest], properties: {release_id: {type: string, format: uuid}, status: {type: string, enum: [offered, accepted, failed, installed, rolled_back]}, version: {$ref: '#/components/schemas/EdgeVersion'}, artifact_digest: {$ref: '#/components/schemas/Sha256Digest'}, rollback_version: {$ref: '#/components/schemas/EdgeVersion'}, manifest: {$ref: '#/components/schemas/EdgeReleaseManifest'}}}}",
             "    ConnectorConfigValidationRequest:",
             "      type: object",
             "      additionalProperties: false",
@@ -520,6 +803,18 @@ def render(routes: list[dict[str, object]]) -> str:
             "      required: [code, message, data]",
             "      properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [nodes, edges, summary], properties: {nodes: {type: array, items: {type: object, additionalProperties: false, required: [type, key], properties: {type: {type: string, enum: [source, asset, process, business_domain, run]}, key: {type: string}}}}, edges: {type: array, items: {type: object}}, summary: {type: object, additionalProperties: false, required: [node_count, edge_count], properties: {node_count: {type: integer}, edge_count: {type: integer}}}}}}",
             "  securitySchemes:",
+            "    mutualTLS:",
+            "      type: mutualTLS",
+            "    edgeCredential:",
+            "      type: apiKey",
+            "      in: header",
+            "      name: X-Edge-Credential",
+            '      description: "One-time-returned edge credential; always combined with mutualTLS certificate proof."',
+            "    edgeEnrollment:",
+            "      type: apiKey",
+            "      in: header",
+            "      name: X-Edge-Enrollment",
+            '      description: "Single-use enrollment token; always combined with mutualTLS certificate proof."',
             "    bearerAuth:",
             "      type: http",
             "      scheme: bearer",

+ 13 - 1
scripts/run_dataops.sh

@@ -59,7 +59,19 @@ if [[ ! -f "${APP_DIR}/alembic.ini" || ! -d "${APP_DIR}/migrations" ]]; then
 fi
 
 echo "[run_dataops] applying database migrations" >&2
-"${VENV_DIR}/bin/alembic" -c "${APP_DIR}/alembic.ini" upgrade head
+role_init_url="${DB_ROLE_INIT_DATABASE_URL:?DB_ROLE_INIT_DATABASE_URL is required for one-shot role bootstrap}"
+migration_url="${MIGRATION_DATABASE_URL:?MIGRATION_DATABASE_URL is required}"
+runtime_url="${DATABASE_URL:?DATABASE_URL runtime connection is required}"
+if [[ "${role_init_url}" == "${runtime_url}" || "${migration_url}" == "${runtime_url}" ]]; then
+  echo "role-init/migration and runtime URLs must use different database identities" >&2
+  exit 1
+fi
+DB_ROLE_INIT_DATABASE_URL="${role_init_url}" \
+  "${VENV_DIR}/bin/python" -m app.core.edge_gateway.runtime_roles
+MIGRATION_DATABASE_URL="${migration_url}" \
+  "${VENV_DIR}/bin/alembic" -c "${APP_DIR}/alembic.ini" upgrade head
+unset DB_ROLE_INIT_DATABASE_URL MIGRATION_DATABASE_URL SQLALCHEMY_DATABASE_URI
+unset role_init_url migration_url
 
 echo "[run_dataops] binding ${LISTEN_HOST}:${LISTEN_PORT} workers=${GUNICORN_WORKERS} log=${LOG_FILE}" >&2
 

+ 1186 - 0
tests/integration/test_phase3_wp04_edge_agent.py

@@ -0,0 +1,1186 @@
+"""Targeted durable edge-agent tests without enterprise data access."""
+
+from __future__ import annotations
+
+from datetime import UTC, datetime, timedelta
+
+import pytest
+from cryptography import x509
+from cryptography.hazmat.primitives import hashes, serialization
+from cryptography.hazmat.primitives.asymmetric import rsa
+from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
+from cryptography.x509.oid import NameOID
+
+from app.core.edge_gateway.contracts import (
+    SignedTaskEnvelope,
+    canonical_json_bytes,
+    canonical_sha256,
+    canonical_timestamp,
+)
+from app.core.edge_gateway.policy import EdgePolicyError
+from app.core.edge_gateway.queue import EdgeQueueConflictError
+from app.edge_gateway.agent import (
+    EdgeAgent,
+    EdgeReleaseError,
+    EdgeRunnerAdapter,
+    LocalArtifactStore,
+    SignedReleaseManager,
+)
+from app.edge_gateway.bootstrap import EdgeBootstrapConfig
+from app.edge_gateway.transport import EdgeTransportError
+
+TASK_SIGNING_PRIVATE_KEY = Ed25519PrivateKey.generate()
+TASK_SIGNING_PUBLIC_KEY = TASK_SIGNING_PRIVATE_KEY.public_key().public_bytes(
+    encoding=serialization.Encoding.Raw,
+    format=serialization.PublicFormat.Raw,
+).hex()
+
+
+def config(tmp_path, **changes):
+    cert_path = tmp_path / "edge-cert.pem"
+    key_path = tmp_path / "edge-key.pem"
+    ca_path = tmp_path / "ca.pem"
+    artifact_root = tmp_path / "artifacts"
+    artifact_root.mkdir(exist_ok=True)
+    if not cert_path.exists():
+        key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
+        name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "gateway-1")])
+        now = datetime.now(UTC)
+        cert = (
+            x509.CertificateBuilder()
+            .subject_name(name)
+            .issuer_name(name)
+            .public_key(key.public_key())
+            .serial_number(x509.random_serial_number())
+            .not_valid_before(now - timedelta(minutes=1))
+            .not_valid_after(now + timedelta(days=1))
+            .sign(key, hashes.SHA256())
+        )
+        cert_pem = cert.public_bytes(serialization.Encoding.PEM)
+        cert_path.write_bytes(cert_pem)
+        ca_path.write_bytes(cert_pem)
+        key_path.write_bytes(
+            key.private_bytes(
+                serialization.Encoding.PEM,
+                serialization.PrivateFormat.PKCS8,
+                serialization.NoEncryption(),
+            )
+        )
+        key_path.chmod(0o600)
+    fingerprint = x509.load_pem_x509_certificate(cert_path.read_bytes()).fingerprint(
+        hashes.SHA256()
+    ).hex()
+    value = {
+        "queue_path": str(tmp_path / "edge.sqlite3"),
+        "artifact_root": str(artifact_root),
+        "gateway_id": "gateway-1",
+        "credential": "dopg_local-test-credential",
+        "certificate_sha256": fingerprint,
+        "client_certificate_path": str(cert_path),
+        "client_private_key_path": str(key_path),
+        "ca_bundle_path": str(ca_path),
+        "generation": 1,
+        "environment": "production",
+        "network_zone": "zone-a",
+        "policy_digest": "a" * 64,
+        "control_url": "https://control.enterprise.test",
+        "proxy_url": None,
+        "allowed_control_hosts": ["control.enterprise.test"],
+        "allowed_proxy_hosts": [],
+        "trusted_release_keys": {"release-key-1": "01" * 32},
+        "trusted_task_keys": {"task-key-1": TASK_SIGNING_PUBLIC_KEY},
+        "task_authority_clock_skew_seconds": 60,
+        "version": "3.0.0",
+    }
+    value.update(changes)
+    return EdgeBootstrapConfig.from_mapping(value)
+
+
+@pytest.mark.parametrize(
+    "changes",
+    [
+        {"queue_path": ":memory:"},
+        {"credential": "changeme"},
+        {"trusted_release_keys": {}},
+        {"control_url": "http://control.enterprise.test"},
+        {"gateway_id": "TBD"},
+    ],
+)
+def test_bootstrap_fails_closed_for_missing_placeholder_or_memory_configuration(tmp_path, changes):
+    with pytest.raises(ValueError):
+        config(tmp_path, **changes)
+
+
+def _task():
+    return {
+        "task_id": "task-1",
+        "gateway_id": "gateway-1",
+        "environment": "production",
+        "network_zone": "zone-a",
+        "purpose": "governed-inventory",
+        "classification": "statistics",
+        "task_type": "profile",
+        "contract_version": 1,
+        "deadline_at": canonical_timestamp(
+            (datetime.now(UTC) + timedelta(minutes=5)).isoformat().replace("+00:00", "Z")
+        ),
+        "attempt": 1,
+        "idempotency_key": "idem-1",
+        "policy_digest": "a" * 64,
+    }
+
+
+def _signed_task_envelope(
+    task=None,
+    *,
+    key_id="task-key-1",
+    private_key=TASK_SIGNING_PRIVATE_KEY,
+    issued_at=None,
+):
+    task = task or _task()
+    issued_at = issued_at or canonical_timestamp(
+        (datetime.now(UTC) - timedelta(seconds=1)).isoformat().replace("+00:00", "Z")
+    )
+    unsigned = {
+        "task": task,
+        "authority_key_id": key_id,
+        "signature_algorithm": "Ed25519",
+        "contract_digest": canonical_sha256(task),
+        "gateway_id": task["gateway_id"],
+        "environment": task["environment"],
+        "network_zone": task["network_zone"],
+        "policy_digest": task["policy_digest"],
+        "purpose": task["purpose"],
+        "issued_at": issued_at,
+        "expires_at": task["deadline_at"],
+    }
+    return {
+        **unsigned,
+        "signature": private_key.sign(
+            SignedTaskEnvelope.canonical_unsigned_bytes(unsigned)
+        ).hex(),
+    }
+
+
+def test_agent_applies_configured_task_authority_future_clock_skew(tmp_path):
+    now = datetime(2026, 8, 9, 8, 0, tzinfo=UTC)
+    envelope = _signed_task_envelope(
+        task={
+            **_task(),
+            "deadline_at": "2026-08-09T08:05:00Z",
+        },
+        issued_at="2026-08-09T08:01:00Z",
+    )
+    agent = EdgeAgent(
+        config(tmp_path),
+        SignedTransport(),
+        EdgeRunnerAdapter(
+            lambda _node, _request, _cancel: {
+                "classification": "statistics",
+                "payload": {"metric_count": 1},
+            }
+        ),
+        clock=lambda: now,
+    )
+    accepted = agent.accept_pulled_task(
+        envelope,
+        "dopl_clock-skew-lease",
+        "2026-08-09T08:02:00Z",
+    )
+    assert accepted.task_id == "task-1"
+
+
+class SignedTransport:
+    def __init__(self, *, remote_expiry=None):
+        self.events = []
+        self.cancelled = []
+        self.heartbeats = []
+        self.tasks = [(
+            _signed_task_envelope(),
+            "dopl_remote-lease-1",
+            remote_expiry or canonical_timestamp(
+                (datetime.now(UTC) + timedelta(minutes=2)).isoformat().replace("+00:00", "Z")
+            ),
+        )]
+
+    def reconcile(self):
+        return {"cancelled_task_ids": list(self.cancelled), "release_offers": []}
+
+    def pull_task(self):
+        return self.tasks.pop(0) if self.tasks else None
+
+    def heartbeat(self, summary):
+        self.heartbeats.append(dict(summary))
+        return {"status": "online"}
+
+    def send_event(self, event, lease_token):
+        self.events.append((dict(event), lease_token))
+        return {
+            "event_id": event["event_id"],
+            "event_digest": canonical_sha256(event),
+            "remote_lease_digest": __import__("hashlib").sha256(
+                lease_token.encode("utf-8")
+            ).hexdigest(),
+            "received_at": canonical_timestamp(datetime.now(UTC).isoformat().replace("+00:00", "Z")),
+            "status": "accepted",
+        }
+
+
+def test_agent_requires_valid_signed_task_authority_before_atomic_accept(tmp_path):
+    transport = SignedTransport()
+    envelope, token, expiry = transport.tasks[0]
+    agent = EdgeAgent(
+        config(tmp_path),
+        transport,
+        EdgeRunnerAdapter(
+            lambda _n, _r, _c: {
+                "classification": "statistics",
+                "payload": {"metric_count": 1},
+            }
+        ),
+    )
+    accepted = agent.accept_pulled_task(envelope, token, expiry)
+    assert accepted.authority_key_id == "task-key-1"
+    tampered = {**envelope, "task": {**envelope["task"], "purpose": "controlled-query"}}
+    with pytest.raises(EdgePolicyError):
+        agent.accept_pulled_task(tampered, token, expiry)
+    unknown = _signed_task_envelope(key_id="retired-task-key")
+    with pytest.raises(EdgePolicyError):
+        agent.accept_pulled_task(unknown, token, expiry)
+
+
+def test_second_agent_recovers_encrypted_remote_lease_after_restart(tmp_path):
+    transport = SignedTransport()
+    first = EdgeAgent(
+        config(tmp_path),
+        transport,
+        EdgeRunnerAdapter(lambda _n, _r, _c: {"classification": "statistics", "payload": {"metric_count": 1}}),
+        worker_id="first",
+    )
+    first.accept_pulled_task(*transport.tasks.pop(0))
+    executions = []
+    restarted = EdgeAgent(
+        config(tmp_path),
+        transport,
+        EdgeRunnerAdapter(
+            lambda _n, request, _c: executions.append(request["task_id"])
+            or {"classification": "statistics", "payload": {"metric_count": 1}}
+        ),
+        worker_id="restarted",
+    )
+    result = restarted.run_once()
+    assert result["executed"] == "task-1"
+    assert executions == ["task-1"]
+    assert transport.events[0][1] == "dopl_remote-lease-1"
+    assert restarted.queue.get_event(transport.events[0][0]["event_id"]).status == "acknowledged"
+
+
+def test_second_agent_reclaims_crashed_local_attempt_with_new_remote_lease(tmp_path):
+    clock = MutableClock()
+    first_expiry = canonical_timestamp(
+        (clock.value + timedelta(seconds=20)).isoformat().replace("+00:00", "Z")
+    )
+    transport = SignedTransport(remote_expiry=first_expiry)
+    envelope = transport.tasks[0][0]
+    first = EdgeAgent(
+        config(tmp_path),
+        transport,
+        EdgeRunnerAdapter(
+            lambda _node, _request, _cancel: (_ for _ in ()).throw(
+                AssertionError("crashed agent must not execute")
+            )
+        ),
+        worker_id="crashed-agent",
+        clock=clock,
+    )
+    first.accept_pulled_task(*transport.tasks.pop(0))
+    crashed_claim = first.queue.claim("crashed-agent")
+    assert crashed_claim.attempt_count == 1
+
+    clock.advance(61)
+    transport.tasks.append(
+        (
+            envelope,
+            "dopl_remote-lease-2",
+            canonical_timestamp(
+                (clock.value + timedelta(minutes=2))
+                .isoformat()
+                .replace("+00:00", "Z")
+            ),
+        )
+    )
+    executions = []
+    replacement = EdgeAgent(
+        config(tmp_path),
+        transport,
+        EdgeRunnerAdapter(
+            lambda _node, request, _cancel: executions.append(request["task_id"])
+            or {"classification": "statistics", "payload": {"metric_count": 1}}
+        ),
+        worker_id="replacement-agent",
+        clock=clock,
+    )
+    result = replacement.run_once()
+    assert result["executed"] == "task-1"
+    assert executions == ["task-1"]
+    assert transport.events == [(transport.events[0][0], "dopl_remote-lease-2")]
+    assert replacement.queue.get_task("task-1").attempt_count == 2
+    with __import__("sqlite3").connect(replacement.queue.db_path) as connection:
+        assert connection.execute(
+            "SELECT count(*) FROM edge_outbound_events"
+        ).fetchone()[0] == 1
+
+
+class LostAckThenRenewedLeaseTransport(SignedTransport):
+    def __init__(self, *, remote_expiry, clock):
+        super().__init__(remote_expiry=remote_expiry)
+        self.clock = clock
+        self.send_tokens = []
+        self.lost = False
+
+    def send_event(self, event, lease_token):
+        self.send_tokens.append(lease_token)
+        if not self.lost:
+            self.lost = True
+            self.events.append((dict(event), lease_token))
+            raise EdgeTransportError("event acknowledgement was lost")
+        if lease_token != "dopl_remote-lease-2":
+            raise AssertionError("expired remote lease must not be retried")
+        return super().send_event(event, lease_token)
+
+
+def test_ack_loss_rebinds_completed_event_after_remote_lease_expiry(tmp_path):
+    clock = MutableClock()
+    expiry = canonical_timestamp(
+        (clock.value + timedelta(seconds=1)).isoformat().replace("+00:00", "Z")
+    )
+    transport = LostAckThenRenewedLeaseTransport(
+        remote_expiry=expiry,
+        clock=clock,
+    )
+    envelope = transport.tasks[0][0]
+    agent = EdgeAgent(
+        config(tmp_path),
+        transport,
+        EdgeRunnerAdapter(
+            lambda _node, _request, _cancel: {
+                "classification": "statistics",
+                "payload": {"metric_count": 1},
+            }
+        ),
+        clock=clock,
+        random=lambda: 0.0,
+    )
+    first = agent.run_once()
+    assert first["executed"] == "task-1"
+    event_id = transport.events[0][0]["event_id"]
+
+    clock.advance(2)
+    transport.tasks.append(
+        (
+            envelope,
+            "dopl_remote-lease-2",
+            canonical_timestamp(
+                (clock.value + timedelta(minutes=2))
+                .isoformat()
+                .replace("+00:00", "Z")
+            ),
+        )
+    )
+    agent.run_once()
+    clock.advance(5)
+    agent.run_once()
+    assert transport.send_tokens == ["dopl_remote-lease-1", "dopl_remote-lease-2"]
+    assert agent.queue.get_event(event_id).status == "acknowledged"
+
+
+def test_expired_remote_lease_is_reacquired_before_first_execution(tmp_path):
+    clock = MutableClock()
+    expiry = canonical_timestamp(
+        (clock.value + timedelta(seconds=1)).isoformat().replace("+00:00", "Z")
+    )
+    transport = SignedTransport(remote_expiry=expiry)
+    agent = EdgeAgent(
+        config(tmp_path),
+        transport,
+        EdgeRunnerAdapter(
+            lambda _n, _r, _c: {
+                "classification": "statistics",
+                "payload": {"metric_count": 1},
+            }
+        ),
+        clock=clock,
+    )
+    envelope, token, remote_expiry = transport.tasks.pop(0)
+    agent.accept_pulled_task(envelope, token, remote_expiry)
+    clock.advance(2)
+    transport.tasks.append((
+        envelope,
+        "dopl_remote-lease-2",
+        canonical_timestamp(
+            (clock.value + timedelta(minutes=2)).isoformat().replace("+00:00", "Z")
+        ),
+    ))
+    result = agent.run_once()
+    assert result["executed"] == "task-1"
+    assert transport.events[0][1] == "dopl_remote-lease-2"
+
+
+class WrongAckTransport(SignedTransport):
+    def send_event(self, event, lease_token):
+        self.events.append((dict(event), lease_token))
+        return {
+            "event_id": "evt_wrong",
+            "event_digest": "f" * 64,
+            "remote_lease_digest": __import__("hashlib").sha256(
+                lease_token.encode("utf-8")
+            ).hexdigest(),
+            "received_at": canonical_timestamp(
+                datetime.now(UTC).isoformat().replace("+00:00", "Z")
+            ),
+            "status": "accepted",
+        }
+
+
+def test_wrong_event_or_digest_ack_never_marks_event_acknowledged(tmp_path):
+    transport = WrongAckTransport()
+    agent = EdgeAgent(
+        config(tmp_path),
+        transport,
+        EdgeRunnerAdapter(
+            lambda _n, _r, _c: {
+                "classification": "statistics",
+                "payload": {"metric_count": 1},
+            }
+        ),
+    )
+    with pytest.raises(EdgePolicyError):
+        agent.run_once()
+    event_id = transport.events[0][0]["event_id"]
+    assert agent.queue.get_event(event_id).status != "acknowledged"
+
+
+class FakeTransport:
+    def __init__(self):
+        self.tasks = [(
+            _signed_task_envelope(),
+            "dopl_lease-1",
+            canonical_timestamp(
+                (datetime.now(UTC) + timedelta(minutes=2)).isoformat().replace("+00:00", "Z")
+            ),
+        )]
+        self.events = []
+        self.outcomes = []
+        self.cancelled = []
+        self.heartbeats = []
+
+    def reconcile(self):
+        return {"cancelled_task_ids": list(self.cancelled), "release_offers": []}
+
+    def pull_task(self):
+        return self.tasks.pop(0) if self.tasks else None
+
+    def send_event(self, event, lease_token):
+        self.events.append((dict(event), lease_token))
+        return {
+            "event_id": event["event_id"],
+            "event_digest": canonical_sha256(event),
+            "remote_lease_digest": __import__("hashlib").sha256(
+                lease_token.encode("utf-8")
+            ).hexdigest(),
+            "received_at": canonical_timestamp(
+                datetime.now(UTC).isoformat().replace("+00:00", "Z")
+            ),
+            "status": "accepted",
+        }
+
+    def task_outcome(self, task_id, outcome, lease_token, summary):
+        self.outcomes.append((task_id, outcome, lease_token, dict(summary)))
+        return {"task_id": task_id, "status": outcome, "replayed": False}
+
+    def heartbeat(self, summary):
+        self.heartbeats.append(dict(summary))
+        return {"status": "online"}
+
+
+def test_agent_persists_before_execute_and_send_and_reconnects_exactly_once(tmp_path):
+    transport = FakeTransport()
+    observations = []
+
+    def execute(_node, request, _cancel):
+        observations.append((request["task_id"], agent.queue.get_task(request["task_id"]).status))
+        return {"classification": "statistics", "payload": {"metric_count": 3}}
+
+    agent = EdgeAgent(config(tmp_path), transport, EdgeRunnerAdapter(execute))
+    first = agent.run_once()
+    assert first["executed"] == "task-1"
+    assert observations == [("task-1", "leased")]
+    assert transport.events and agent.queue.get_event(transport.events[0][0]["event_id"]).status == "acknowledged"
+
+    second = agent.run_once()
+    assert second["executed"] is None
+    assert len(transport.events) == 1
+    assert agent.safe_diagnostic() == {
+        "agent_status": "ready",
+        "gateway_id_digest": agent.gateway_id_digest,
+        "queue_pending_count": 0,
+        "queue_unacknowledged_count": 0,
+        "version": "3.0.0",
+    }
+
+
+def test_two_agents_do_not_execute_same_sqlite_task(tmp_path):
+    transport = FakeTransport()
+    count = 0
+
+    def execute(_node, _request, _cancel):
+        nonlocal count
+        count += 1
+        return {"classification": "statistics", "payload": {"metric_count": 1}}
+
+    first = EdgeAgent(config(tmp_path), transport, EdgeRunnerAdapter(execute), worker_id="one")
+    second = EdgeAgent(config(tmp_path), transport, EdgeRunnerAdapter(execute), worker_id="two")
+    first.run_once()
+    second.run_once()
+    assert count == 1
+
+
+def test_offline_cancel_converges_before_execution(tmp_path):
+    transport = FakeTransport()
+    called = False
+
+    def execute(_node, _request, _cancel):
+        nonlocal called
+        called = True
+        return {"classification": "statistics", "payload": {"metric_count": 1}}
+
+    agent = EdgeAgent(config(tmp_path), transport, EdgeRunnerAdapter(execute))
+    agent.accept_pulled_task(*transport.pull_task())
+    transport.cancelled = ["task-1"]
+    result = agent.run_once()
+    assert result["cancelled"] == ["task-1"]
+    assert not called
+    assert agent.queue.get_task("task-1").status == "cancelled"
+    assert transport.outcomes == [
+        ("task-1", "cancelled", "dopl_lease-1", {"task_status": "cancelled"})
+    ]
+    assert agent.queue.get_outcome("task-1").status == "acknowledged"
+
+
+class CancelOutcomeAckLossTransport(FakeTransport):
+    def __init__(self):
+        super().__init__()
+        self.outcome_attempts = 0
+
+    def task_outcome(self, task_id, outcome, lease_token, summary):
+        self.outcome_attempts += 1
+        self.outcomes.append((task_id, outcome, lease_token, dict(summary)))
+        if self.outcome_attempts == 1:
+            raise EdgeTransportError("cancel acknowledgement lost")
+        return {"task_id": task_id, "status": outcome, "replayed": True}
+
+
+def test_cancel_outcome_survives_ack_loss_disconnect_restart_and_replays_exactly(tmp_path):
+    clock = MutableClock()
+    transport = CancelOutcomeAckLossTransport()
+    first = EdgeAgent(
+        config(tmp_path),
+        transport,
+        EdgeRunnerAdapter(
+            lambda _node, _request, _cancel: {
+                "classification": "statistics",
+                "payload": {"metric_count": 1},
+            }
+        ),
+        worker_id="first",
+        clock=clock,
+    )
+    first.accept_pulled_task(*transport.pull_task())
+    transport.cancelled = ["task-1"]
+    assert first.run_once()["cancelled"] == ["task-1"]
+    pending = first.queue.get_outcome("task-1")
+    assert pending.status == "pending"
+    stable_digest = pending.digest
+
+    clock.advance(3)
+    restarted = EdgeAgent(
+        config(tmp_path),
+        transport,
+        EdgeRunnerAdapter(
+            lambda *_args: (_ for _ in ()).throw(
+                AssertionError("cancelled task must never execute")
+            )
+        ),
+        worker_id="restarted",
+        clock=clock,
+    )
+    restarted.run_once()
+    acknowledged = restarted.queue.get_outcome("task-1")
+    assert acknowledged.status == "acknowledged"
+    assert acknowledged.digest == stable_digest
+    assert [attempt[:2] for attempt in transport.outcomes] == [
+        ("task-1", "cancelled"),
+        ("task-1", "cancelled"),
+    ]
+
+
+def test_cancel_outcome_sqlite_claim_is_single_sender_across_agents(tmp_path):
+    transport = FakeTransport()
+    first = EdgeAgent(
+        config(tmp_path), transport, EdgeRunnerAdapter(lambda *_args: {}), worker_id="one"
+    )
+    first.accept_pulled_task(*transport.pull_task())
+    first.queue.cancel_with_outcome("task-1")
+    second = EdgeAgent(
+        config(tmp_path), transport, EdgeRunnerAdapter(lambda *_args: {}), worker_id="two"
+    )
+    claimed = first.queue.claim_outcome("one")
+    assert claimed is not None
+    assert second.queue.claim_outcome("two") is None
+
+
+class MutableClock:
+    def __init__(self):
+        self.value = datetime.now(UTC)
+
+    def __call__(self):
+        return self.value
+
+    def advance(self, seconds):
+        self.value += timedelta(seconds=seconds)
+
+
+class AckLossTransport(FakeTransport):
+    def __init__(self):
+        super().__init__()
+        self.attempts = 0
+
+    def send_event(self, event, lease_token):
+        self.attempts += 1
+        self.events.append((dict(event), lease_token))
+        if self.attempts == 1:
+            raise EdgeTransportError("ack lost")
+        return {
+            "event_id": event["event_id"],
+            "event_digest": canonical_sha256(event),
+            "remote_lease_digest": __import__("hashlib").sha256(
+                lease_token.encode("utf-8")
+            ).hexdigest(),
+            "received_at": canonical_timestamp(
+                datetime.now(UTC).isoformat().replace("+00:00", "Z")
+            ),
+            "status": "accepted",
+        }
+
+
+def test_ack_loss_keeps_oldest_event_durable_and_replays_same_identity(tmp_path):
+    clock = MutableClock()
+    transport = AckLossTransport()
+    agent = EdgeAgent(
+        config(tmp_path),
+        transport,
+        EdgeRunnerAdapter(
+            lambda _node, _request, _cancel: {
+                "classification": "statistics",
+                "payload": {"metric_count": 1},
+            }
+        ),
+        clock=clock,
+    )
+    first = agent.run_once()
+    assert first["executed"] == "task-1" and first["sent"] is None
+    event_id = transport.events[0][0]["event_id"]
+    assert agent.queue.get_event(event_id).status == "pending"
+
+    # Backoff has not elapsed: unresolved oldest event blocks all new work.
+    task_two = {**_task(), "task_id": "task-2", "idempotency_key": "idem-2"}
+    transport.tasks.append((
+        _signed_task_envelope(task_two),
+        "dopl_lease-2",
+        canonical_timestamp(
+            (clock.value + timedelta(minutes=2)).isoformat().replace("+00:00", "Z")
+        ),
+    ))
+    assert agent.run_once()["executed"] is None
+    assert len(transport.tasks) == 1
+
+    clock.advance(3)
+    assert agent.run_once()["sent"] == event_id
+    assert [event[0]["event_id"] for event in transport.events] == [event_id, event_id]
+    assert agent.queue.get_event(event_id).status == "acknowledged"
+
+
+def test_changed_event_digest_is_rejected_without_replacing_durable_event(tmp_path):
+    transport = FakeTransport()
+    agent = EdgeAgent(
+        config(tmp_path),
+        transport,
+        EdgeRunnerAdapter(
+            lambda _node, _request, _cancel: {
+                "classification": "statistics",
+                "payload": {"metric_count": 1},
+            }
+        ),
+    )
+    agent.run_once()
+    original = transport.events[0][0]
+    changed = {**original, "payload": {"metric_count": 2}}
+    with pytest.raises(EdgeQueueConflictError):
+        agent.queue.persist_event(changed)
+    assert agent.queue.get_event(original["event_id"]).event["payload"]["metric_count"] == 1
+
+
+class CancelRaceTransport(FakeTransport):
+    def __init__(self):
+        super().__init__()
+        self.probes = 0
+
+    def cancel_requested(self, _task_id):
+        self.probes += 1
+        return self.probes >= 2
+
+
+def test_cooperative_cancel_race_never_emits_success_event(tmp_path):
+    transport = CancelRaceTransport()
+    called = 0
+
+    def execute(_node, _request, cancel):
+        nonlocal called
+        called += 1
+        assert cancel()
+        return {"classification": "statistics", "payload": {"metric_count": 1}}
+
+    agent = EdgeAgent(config(tmp_path), transport, EdgeRunnerAdapter(execute))
+    result = agent.run_once()
+    assert result["executed"] is None
+    assert result["cancelled"] == ["task-1"]
+    assert called == 1 and transport.events == []
+    assert agent.queue.get_task("task-1").status == "cancelled"
+
+
+def _signed_manifest(private_key, **changes):
+    unsigned = {
+        "release_id": "release-1",
+        "version": "3.1.0",
+        "rollback_version": "3.0.0",
+        "artifact_digest": "0" * 64,
+        "artifact_name": "edge-agent-3.1.0.bin",
+        "deadline_at": canonical_timestamp(
+            (datetime.now(UTC) + timedelta(minutes=5))
+            .isoformat()
+            .replace("+00:00", "Z")
+        ),
+        "signature_algorithm": "Ed25519",
+        "key_id": "release-key-1",
+        "status": "offered",
+    }
+    unsigned.update(changes)
+    return {
+        **unsigned,
+        "manifest_digest": canonical_sha256(unsigned),
+        "signature": private_key.sign(canonical_json_bytes(unsigned)).hex(),
+    }
+
+
+def _release_manager(private_key, artifact, activator, rollback):
+    public = private_key.public_key().public_bytes(
+        encoding=serialization.Encoding.Raw,
+        format=serialization.PublicFormat.Raw,
+    ).hex()
+    return SignedReleaseManager(
+        current_version="3.0.0",
+        trusted_keys={"release-key-1": public},
+        artifact_loader=lambda _manifest: artifact,
+        activator=activator,
+        rollback=rollback,
+    )
+
+
+def test_signed_candidate_activates_only_after_digest_and_health_confirmation():
+    private = Ed25519PrivateKey.generate()
+    artifact = b"verified-edge-agent"
+    activated = []
+    manager = _release_manager(
+        private,
+        artifact,
+        lambda version, body: activated.append((version, body)) or True,
+        lambda _version: False,
+    )
+    manifest = _signed_manifest(private, artifact_digest=__import__("hashlib").sha256(artifact).hexdigest())
+    result = manager.install(manifest)
+    assert result["outcome"] == "installed"
+    assert activated == [("3.1.0", artifact)]
+    assert manager.current_version == "3.1.0"
+
+
+def test_candidate_health_failure_rolls_back_previous_verified_version():
+    private = Ed25519PrivateKey.generate()
+    artifact = b"verified-edge-agent"
+    rolled_back = []
+    manager = _release_manager(
+        private,
+        artifact,
+        lambda _version, _body: False,
+        lambda version: rolled_back.append(version) or True,
+    )
+    result = manager.install(
+        _signed_manifest(private, artifact_digest=__import__("hashlib").sha256(artifact).hexdigest())
+    )
+    assert result["outcome"] == "rollback"
+    assert rolled_back == ["3.0.0"]
+    assert manager.current_version == "3.0.0"
+
+
+@pytest.mark.parametrize(
+    "change",
+    [
+        {"artifact_name": "../agent.bin"},
+        {"artifact_name": "/tmp/agent.bin"},
+        {"version": "2.9.0"},
+        {"rollback_version": "2.8.0"},
+        {"signature_algorithm": "RSA"},
+    ],
+)
+def test_release_manifest_rejects_path_traversal_downgrade_and_unapproved_algorithm(change):
+    private = Ed25519PrivateKey.generate()
+    manager = _release_manager(private, b"body", lambda _v, _b: True, lambda _v: True)
+    with pytest.raises(EdgeReleaseError):
+        manager.verify(_signed_manifest(private, **change))
+
+
+def test_release_id_changed_digest_replay_fails_closed():
+    private = Ed25519PrivateKey.generate()
+    manager = _release_manager(private, b"body", lambda _v, _b: True, lambda _v: True)
+    first = _signed_manifest(private)
+    manager.verify(first)
+    with pytest.raises(EdgeReleaseError, match="replay"):
+        manager.verify(_signed_manifest(private, artifact_digest="1" * 64))
+
+
+def test_safe_diagnostic_is_approved_and_contains_no_runtime_secrets(tmp_path):
+    agent = EdgeAgent(
+        config(tmp_path),
+        FakeTransport(),
+        EdgeRunnerAdapter(lambda _n, _r, _c: {"classification": "statistics", "payload": {"metric_count": 1}}),
+    )
+    diagnostic = agent.safe_diagnostic()
+    agent.policy.validate_approved_payload("diagnostic_summary", diagnostic)
+    encoded = str(diagnostic)
+    assert agent.config.credential not in encoded
+    assert agent.config.certificate_sha256 not in encoded
+    assert agent.heartbeat_once() == {"status": "online"}
+    assert agent.transport.heartbeats == [diagnostic]
+
+
+def test_local_raw_artifact_reference_never_enters_outbound_event(tmp_path):
+    transport = FakeTransport()
+    envelope, lease, expiry = transport.tasks[0]
+    raw_task = {**envelope["task"], "classification": "raw"}
+    transport.tasks[0] = (_signed_task_envelope(raw_task), lease, expiry)
+    body = b"local raw rows never leave the edge"
+    digest = __import__("hashlib").sha256(body).hexdigest()
+    artifact_root = tmp_path / "artifacts"
+    artifact_root.mkdir()
+    artifact_path = artifact_root / digest
+    artifact_path.write_bytes(body)
+    agent = EdgeAgent(
+        config(tmp_path),
+        transport,
+        EdgeRunnerAdapter(
+            lambda _n, _r, _c: {
+                "classification": "statistics",
+                "payload": {"metric_count": 1},
+                "local_artifact_digest": digest,
+                "local_artifact_ref": str(artifact_path),
+            }
+        ),
+    )
+    agent.run_once()
+    event = transport.events[0][0]
+    assert event["payload"] == {"metric_count": 1}
+    assert "artifact" not in str(event).lower()
+    assert not artifact_path.exists()
+    artifact = agent.queue.list_local_artifacts(task_id="task-1")[0]
+    assert artifact.artifact_ref is None
+    assert artifact.artifact_ref_hash == canonical_sha256(str(artifact_path.resolve()))
+    assert artifact.cleanup_status == "deleted"
+    assert artifact.deletion_receipt["status"] == "deleted"
+
+
+@pytest.mark.parametrize("case", ["escape", "symlink", "digest_mismatch"])
+def test_local_artifact_store_rejects_escape_symlink_and_digest_mismatch(
+    tmp_path, case
+):
+    root = tmp_path / "artifacts"
+    root.mkdir()
+    outside = tmp_path / "outside.bin"
+    outside.write_bytes(b"enterprise raw data")
+    expected = __import__("hashlib").sha256(outside.read_bytes()).hexdigest()
+    reference = outside
+    digest = expected
+    if case == "symlink":
+        reference = root / "linked.bin"
+        reference.symlink_to(outside)
+    elif case == "digest_mismatch":
+        reference = root / "artifact.bin"
+        reference.write_bytes(outside.read_bytes())
+        digest = "0" * 64
+    store = LocalArtifactStore(root)
+    with pytest.raises(EdgePolicyError):
+        store.inspect(str(reference), digest)
+    assert outside.exists()
+
+
+class SimulatedProcessCrash(BaseException):
+    pass
+
+
+class CrashAfterArtifactDeleteStore(LocalArtifactStore):
+    def delete(self, reference, expected_digest):
+        super().delete(reference, expected_digest)
+        raise SimulatedProcessCrash("simulated crash after unlink before sqlite receipt")
+
+
+def test_artifact_cleanup_recovers_when_crash_happens_after_unlink_before_receipt(
+    tmp_path,
+):
+    clock = MutableClock()
+    transport = FakeTransport()
+    envelope, lease, expiry = transport.tasks[0]
+    raw_task = {**envelope["task"], "classification": "raw"}
+    transport.tasks[0] = (_signed_task_envelope(raw_task), lease, expiry)
+    root = tmp_path / "artifacts"
+    root.mkdir()
+    body = b"raw artifact crash recovery"
+    digest = __import__("hashlib").sha256(body).hexdigest()
+    artifact_path = root / digest
+    artifact_path.write_bytes(body)
+    first = EdgeAgent(
+        config(tmp_path),
+        transport,
+        EdgeRunnerAdapter(
+            lambda *_args: {
+                "classification": "statistics",
+                "payload": {"metric_count": 1},
+                "local_artifact_digest": digest,
+                "local_artifact_ref": str(artifact_path),
+            }
+        ),
+        clock=clock,
+        artifact_store=CrashAfterArtifactDeleteStore(root, clock=clock),
+    )
+    with pytest.raises(SimulatedProcessCrash):
+        first.run_once()
+    assert not artifact_path.exists()
+    assert first.queue.list_local_artifacts(task_id="task-1")[0].cleanup_status == "deleting"
+
+    clock.advance(61)
+    restarted = EdgeAgent(
+        config(tmp_path),
+        transport,
+        EdgeRunnerAdapter(lambda *_args: {}),
+        clock=clock,
+    )
+    restarted.run_once()
+    recovered = restarted.queue.list_local_artifacts(task_id="task-1")[0]
+    assert recovered.cleanup_status == "deleted"
+    assert recovered.artifact_ref is None
+
+
+def test_artifact_cleanup_sqlite_claim_is_single_deleter_across_agents(tmp_path):
+    clock = MutableClock()
+    transport = FakeTransport()
+    root = tmp_path / "artifacts"
+    root.mkdir()
+    body = b"retained metadata artifact"
+    digest = __import__("hashlib").sha256(body).hexdigest()
+    path = root / digest
+    path.write_bytes(body)
+    first = EdgeAgent(
+        config(tmp_path),
+        transport,
+        EdgeRunnerAdapter(
+            lambda *_args: {
+                "classification": "statistics",
+                "payload": {"metric_count": 1},
+                "local_artifact_digest": digest,
+                "local_artifact_ref": str(path),
+            }
+        ),
+        worker_id="one",
+        clock=clock,
+    )
+    first.run_once()
+    assert path.exists()
+    clock.advance(1_096 * 86_400)
+    second = EdgeAgent(
+        config(tmp_path), transport, EdgeRunnerAdapter(lambda *_args: {}),
+        worker_id="two", clock=clock,
+    )
+    claimed = first.queue.claim_artifact_cleanup("one")
+    assert claimed is not None
+    assert second.queue.claim_artifact_cleanup("two") is None
+
+
+class ReleaseTransport(FakeTransport):
+    def __init__(self, manifest):
+        super().__init__()
+        self.tasks = []
+        self.manifest = manifest
+        self.acks = []
+
+    def reconcile(self):
+        manifest, self.manifest = self.manifest, None
+        return {
+            "cancelled_task_ids": [],
+            "release_offers": [manifest] if manifest else [],
+        }
+
+    def acknowledge_release(self, release_id, outcome, summary):
+        self.acks.append((release_id, outcome, dict(summary)))
+        return {"release_id": release_id, "status": outcome}
+
+
+def test_agent_release_state_sequence_matches_control_plane_transitions(tmp_path):
+    private = Ed25519PrivateKey.generate()
+    artifact = b"verified-edge-agent"
+    manifest = _signed_manifest(
+        private, artifact_digest=__import__("hashlib").sha256(artifact).hexdigest()
+    )
+    manager = _release_manager(
+        private,
+        artifact,
+        lambda _version, _body: False,
+        lambda _version: True,
+    )
+    transport = ReleaseTransport(manifest)
+    agent = EdgeAgent(
+        config(tmp_path),
+        transport,
+        EdgeRunnerAdapter(
+            lambda _n, _r, _c: {
+                "classification": "statistics",
+                "payload": {"metric_count": 1},
+            }
+        ),
+        release_manager=manager,
+    )
+    agent.run_once()
+    assert [ack[1] for ack in transport.acks] == ["accepted", "failed", "rollback"]
+
+
+def _manifest_state(private_key, manifest, status):
+    unsigned = {
+        key: value
+        for key, value in manifest.items()
+        if key not in {"manifest_digest", "signature", "signature_algorithm", "key_id"}
+    }
+    unsigned["status"] = status
+    unsigned["signature_algorithm"] = "Ed25519"
+    unsigned["key_id"] = "release-key-1"
+    return {
+        **unsigned,
+        "manifest_digest": canonical_sha256(unsigned),
+        "signature": private_key.sign(canonical_json_bytes(unsigned)).hex(),
+    }
+
+
+class StatefulReleaseTransport(FakeTransport):
+    def __init__(self, private_key, manifest, *, fail_once=None):
+        super().__init__()
+        self.tasks = []
+        self.private_key = private_key
+        self.manifest = manifest
+        self.fail_once = fail_once
+        self.acks = []
+
+    def reconcile(self):
+        return {"cancelled_task_ids": [], "release_offers": [self.manifest]}
+
+    def acknowledge_release(self, release_id, outcome, summary):
+        self.acks.append((release_id, outcome, dict(summary)))
+        if self.fail_once == outcome:
+            self.fail_once = None
+            raise EdgeTransportError("release acknowledgement lost")
+        status = "rolled_back" if outcome == "rollback" else outcome
+        self.manifest = _manifest_state(self.private_key, self.manifest, status)
+        return {"release_id": release_id, "status": status}
+
+
+@pytest.mark.parametrize("lost_ack", ["accepted", "installed"])
+def test_release_ack_loss_and_agent_restart_converge_without_reactivation(tmp_path, lost_ack):
+    private = Ed25519PrivateKey.generate()
+    artifact = b"verified-edge-agent"
+    manifest = _signed_manifest(
+        private, artifact_digest=__import__("hashlib").sha256(artifact).hexdigest()
+    )
+    activations = []
+    transport = StatefulReleaseTransport(private, manifest, fail_once=lost_ack)
+    manager = _release_manager(
+        private,
+        artifact,
+        lambda version, _body: activations.append(version) or True,
+        lambda _version: True,
+    )
+    first = EdgeAgent(
+        config(tmp_path), transport,
+        EdgeRunnerAdapter(lambda _n, _r, _c: {"classification": "statistics", "payload": {"metric_count": 1}}),
+        release_manager=manager,
+    )
+    with pytest.raises(EdgeTransportError):
+        first.run_once()
+
+    restarted_activations = []
+    restarted_manager = _release_manager(
+        private,
+        artifact,
+        lambda version, _body: restarted_activations.append(version) or True,
+        lambda _version: True,
+    )
+    restarted = EdgeAgent(
+        config(tmp_path), transport,
+        EdgeRunnerAdapter(lambda _n, _r, _c: {"classification": "statistics", "payload": {"metric_count": 1}}),
+        release_manager=restarted_manager,
+    )
+    restarted.run_once()
+    assert transport.manifest["status"] == "installed"
+    assert activations + restarted_activations == ["3.1.0"]
+    assert restarted_activations == (["3.1.0"] if lost_ack == "accepted" else [])
+    assert restarted.queue.get_release_state("release-1").status == "installed"
+
+
+def test_failed_ack_loss_restart_resends_state_without_duplicate_rollback(tmp_path):
+    private = Ed25519PrivateKey.generate()
+    artifact = b"verified-edge-agent"
+    manifest = _signed_manifest(
+        private, artifact_digest=__import__("hashlib").sha256(artifact).hexdigest()
+    )
+    rollbacks = []
+    transport = StatefulReleaseTransport(private, manifest, fail_once="failed")
+    manager = _release_manager(
+        private,
+        artifact,
+        lambda _version, _body: False,
+        lambda version: rollbacks.append(version) or True,
+    )
+    first = EdgeAgent(
+        config(tmp_path), transport,
+        EdgeRunnerAdapter(lambda _n, _r, _c: {"classification": "statistics", "payload": {"metric_count": 1}}),
+        release_manager=manager,
+    )
+    with pytest.raises(EdgeTransportError):
+        first.run_once()
+    assert first.queue.get_release_state("release-1").status == "rolled_back"
+
+    restarted = EdgeAgent(
+        config(tmp_path), transport,
+        EdgeRunnerAdapter(lambda _n, _r, _c: {"classification": "statistics", "payload": {"metric_count": 1}}),
+        release_manager=_release_manager(
+            private, artifact,
+            lambda _version, _body: False,
+            lambda _version: (_ for _ in ()).throw(AssertionError("must not rollback twice")),
+        ),
+    )
+    restarted.run_once()
+    assert transport.manifest["status"] == "rolled_back"
+    assert rollbacks == ["3.0.0"]

+ 1945 - 0
tests/integration/test_phase3_wp04_edge_gateway_postgres.py

@@ -0,0 +1,1945 @@
+from __future__ import annotations
+
+import hashlib
+import os
+import subprocess
+import uuid
+from concurrent.futures import ThreadPoolExecutor
+from datetime import UTC, datetime, timedelta
+from pathlib import Path
+from threading import Barrier, Event
+from types import SimpleNamespace
+from urllib.parse import quote
+
+import pytest
+from cryptography import x509
+from cryptography.hazmat.primitives import hashes, serialization
+from cryptography.hazmat.primitives.asymmetric import rsa
+from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
+from cryptography.x509.oid import NameOID
+from sqlalchemy import create_engine, inspect, text
+from sqlalchemy.engine import make_url
+from sqlalchemy.exc import DBAPIError
+from sqlalchemy.orm import Session
+
+from app.config.config import validate_production_database_identity
+from app.core.edge_gateway.contracts import (
+    EdgeEventContract,
+    SignedTaskEnvelope,
+    canonical_json_bytes,
+    canonical_sha256,
+    stable_event_id,
+)
+from app.core.edge_gateway.repository import EdgeGatewayRepository
+from app.core.edge_gateway.service import (
+    EdgeGatewayAuthenticationError,
+    EdgeGatewayConfigurationError,
+    EdgeGatewayConflictError,
+    EdgeGatewayError,
+    EdgeGatewayService,
+    EdgeGatewayValidationError,
+)
+from app.edge_gateway.agent import EdgeAgent, EdgeRunnerAdapter, SignedReleaseManager
+from app.edge_gateway.bootstrap import EdgeBootstrapConfig
+
+ROOT = Path(__file__).resolve().parents[2]
+DATABASE_URL = os.environ.get("TEST_DATABASE_URL")
+pytestmark = pytest.mark.integration
+
+_EVIDENCE_TRIGGERS = {
+    "edge_gateway_audits": (
+        "trg_edge_audit_append_only",
+        "trg_edge_audit_no_truncate",
+    ),
+    "edge_gateway_failure_windows": (
+        "trg_edge_failure_window_protected",
+        "trg_edge_failure_window_no_truncate",
+    ),
+}
+
+_SIGNING_PRIVATE_KEY = Ed25519PrivateKey.generate()
+_SIGNING_KEY_ID = "edge-control-test-key-1"
+
+
+def _service(repository):
+    return EdgeGatewayService(
+        repository,
+        signing_private_key=_SIGNING_PRIVATE_KEY,
+        signing_key_id=_SIGNING_KEY_ID,
+    )
+
+
+def _uid() -> str:
+    return str(uuid.uuid4())
+
+
+def _alembic(command: str, target: str, database_url: str = None) -> None:
+    assert DATABASE_URL
+    subprocess.run(
+        [str(ROOT / ".venv/bin/alembic"), "-c", str(ROOT / "alembic.ini"), command, target],
+        cwd=ROOT,
+        env={
+            **os.environ,
+            "MIGRATION_DATABASE_URL": database_url or DATABASE_URL,
+            "SQLALCHEMY_DATABASE_URI": "",
+            "DATABASE_URL": "",
+        },
+        check=True,
+        capture_output=True,
+        text=True,
+    )
+
+
+def _clear_edge_rows_for_test(engine) -> None:
+    tables = set(inspect(engine).get_table_names(schema="public"))
+    targets = []
+    for table in (
+        "edge_gateway_failure_windows", "edge_gateway_audits", "edge_gateway_events",
+        "edge_gateway_releases", "edge_gateway_tasks", "edge_gateway_rotation_requests",
+        "edge_gateway_credentials", "edge_gateways", "edge_gateway_enrollments",
+    ):
+        if table in tables:
+            targets.append(f"public.{table}")
+    if not targets:
+        return
+    with engine.begin() as connection:
+        existing_triggers = set(connection.execute(text("""
+            SELECT tgname FROM pg_trigger
+             WHERE tgrelid IN (
+               'public.edge_gateway_audits'::regclass,
+               to_regclass('public.edge_gateway_failure_windows')
+             ) AND NOT tgisinternal
+        """)).scalars()) if "edge_gateway_audits" in tables else set()
+        disabled = []
+        for table, triggers in _EVIDENCE_TRIGGERS.items():
+            if table not in tables:
+                continue
+            for trigger in triggers:
+                if trigger in existing_triggers:
+                    connection.execute(text(f"ALTER TABLE public.{table} DISABLE TRIGGER {trigger}"))
+                    disabled.append((table, trigger))
+        connection.execute(text(f"TRUNCATE TABLE {','.join(targets)} CASCADE"))
+        for table, trigger in disabled:
+            connection.execute(text(f"ALTER TABLE public.{table} ENABLE TRIGGER {trigger}"))
+
+
+@pytest.fixture(scope="module")
+def engine():
+    if not DATABASE_URL:
+        pytest.skip("TEST_DATABASE_URL is required")
+    bootstrap = create_engine(DATABASE_URL, pool_pre_ping=True)
+    _clear_edge_rows_for_test(bootstrap)
+    bootstrap.dispose()
+    _alembic("downgrade", "20260802_476")
+    _alembic("upgrade", "head")
+    value = create_engine(DATABASE_URL, pool_pre_ping=True)
+    yield value
+    value.dispose()
+
+
+@pytest.fixture(autouse=True)
+def clear_edge_rows(engine):
+    _clear_edge_rows_for_test(engine)
+    yield
+    _clear_edge_rows_for_test(engine)
+
+
+@pytest.fixture
+def service(engine):
+    session = Session(engine)
+    yield _service(EdgeGatewayRepository(session))
+    session.close()
+
+
+def _enrollment(service, **overrides):
+    values = {
+        "gateway_name": "plant-a-edge",
+        "environment": "staging",
+        "network_zone": "manufacturing-zone-a",
+        "policy_digest": "a" * 64,
+        "allowed_control_hosts": ["control.example.test"],
+        "allowed_proxy_hosts": ["proxy.example.test"],
+        "expected_certificate_sha256": "b" * 64,
+        "ttl_seconds": 600,
+        "actor_uid": _uid(),
+    }
+    values.update(overrides)
+    return service.create_enrollment(**values)
+
+
+def _register(service, enrollment, certificate_sha256="b" * 64):
+    return service.register(
+        enrollment_token=enrollment["enrollment_token"],
+        certificate_sha256=certificate_sha256,
+        gateway_id=enrollment["gateway_id"],
+        environment="staging",
+        network_zone="manufacturing-zone-a",
+        policy_digest="a" * 64,
+        allowed_control_hosts=["control.example.test"],
+        allowed_proxy_hosts=["proxy.example.test"],
+        version="3.0.0",
+    )
+
+
+def _event(task, gateway, classification="statistics", payload=None):
+    identity = {
+        "task_id": task["task_id"], "gateway_id": gateway["gateway_id"],
+        "environment": "staging", "network_zone": "manufacturing-zone-a",
+        "purpose": "inventory", "classification": classification,
+        "contract_version": 1,
+        "occurred_at": datetime.now(UTC).isoformat().replace("+00:00", "Z"),
+        "attempt": 1, "idempotency_key": "evt-key-1", "policy_digest": "a" * 64,
+        "payload": payload or {"asset_count": 12},
+    }
+    return {"event_id": stable_event_id(identity), **identity}
+
+
+def test_migration_is_479_head_and_schema_is_constrained(engine):
+    with engine.connect() as connection:
+        assert connection.execute(text("SELECT version_num FROM alembic_version")).scalar_one() == "20260809_479"
+        functions = connection.execute(text("""
+            SELECT pg_get_function_identity_arguments(p.oid),r.rolname,p.prosecdef,p.proconfig
+              FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace
+              JOIN pg_roles r ON r.oid=p.proowner
+             WHERE n.nspname='public' AND p.proname='record_edge_gateway_failure'
+        """)).all()
+        constraints = connection.execute(text("""
+            SELECT conname FROM pg_constraint
+             WHERE conrelid IN ('public.edge_gateways'::regclass,
+                                'public.edge_gateway_credentials'::regclass)
+        """)).scalars().all()
+    assert "ck_edge_gateway_status" in constraints
+    assert "ck_edge_credential_status" in constraints
+    assert "trg_edge_gateway_binding" in constraints
+    assert "trg_edge_credential_binding" in constraints
+    assert functions == [(
+        "p_audit_uid uuid, p_fingerprint character, p_gateway uuid, p_event_type character varying, p_reason_code character varying",
+        "dataops_edge_evidence_owner", True, ["search_path=pg_catalog"],
+    )]
+    enrollment_columns = {column["name"] for column in inspect(engine).get_columns("edge_gateway_enrollments", schema="public")}
+    assert "expected_certificate_sha256" in enrollment_columns
+    task_columns = {
+        column["name"]
+        for column in inspect(engine).get_columns("edge_gateway_tasks", schema="public")
+    }
+    release_columns = {
+        column["name"]
+        for column in inspect(engine).get_columns("edge_gateway_releases", schema="public")
+    }
+    assert "authority" in task_columns
+    assert {
+        "artifact_name", "deadline_at", "signature_algorithm", "key_id",
+        "manifest_digest", "signature", "signed_manifest",
+    } <= release_columns
+    with pytest.raises(DBAPIError), engine.begin() as connection:
+        connection.execute(text("""
+            INSERT INTO edge_gateway_enrollments
+              (uid,gateway_id,gateway_name,environment,network_zone,policy_digest,
+               allowed_control_hosts,allowed_proxy_hosts,token_hash,status,expires_at,created_by)
+            VALUES (CAST(:uid AS uuid),CAST(:gateway AS uuid),'invalid-host','staging','zone-a',
+                    :policy,ARRAY['control.example.test','CONTROL.example.test'],ARRAY[]::text[],
+                    :token,'pending',CURRENT_TIMESTAMP+INTERVAL '10 minutes',CAST(:actor AS uuid))
+        """), {"uid": _uid(), "gateway": _uid(), "actor": _uid(), "policy": "1" * 64, "token": "2" * 64})
+
+
+def test_migration_controlled_477_downgrade_and_476_upgrade(engine):
+    try:
+        _alembic("downgrade", "20260802_476")
+        with engine.connect() as connection:
+            assert connection.execute(text("SELECT version_num FROM alembic_version")).scalar_one() == "20260802_476"
+            assert not inspect(connection).has_table("edge_gateways", schema="public")
+    finally:
+        _alembic("upgrade", "head")
+    with engine.connect() as connection:
+        assert connection.execute(text("SELECT version_num FROM alembic_version")).scalar_one() == "20260809_479"
+
+
+def test_479_downgrade_and_upgrade_are_reversible_without_signed_rows(engine):
+    try:
+        _alembic("downgrade", "20260809_478")
+        with engine.connect() as connection:
+            assert connection.execute(text("SELECT version_num FROM alembic_version")).scalar_one() == "20260809_478"
+            assert "authority" not in {
+                column["name"]
+                for column in inspect(connection).get_columns("edge_gateway_tasks", schema="public")
+            }
+    finally:
+        _alembic("upgrade", "head")
+    with engine.connect() as connection:
+        assert connection.execute(text("SELECT version_num FROM alembic_version")).scalar_one() == "20260809_479"
+
+
+def test_478_downgrade_never_restores_unsafe_function(engine):
+    try:
+        _alembic("downgrade", "20260809_477")
+        with engine.connect() as connection:
+            signatures = connection.execute(text("""
+                SELECT pg_get_function_identity_arguments(p.oid)
+                  FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace
+                 WHERE n.nspname='public' AND p.proname='record_edge_gateway_failure'
+                 ORDER BY 1
+            """)).scalars().all()
+        assert signatures == [
+            "p_audit_uid uuid, p_fingerprint character, p_gateway uuid, p_event_type character varying, p_reason_code character varying"
+        ]
+    finally:
+        _alembic("upgrade", "head")
+
+
+def test_migration_rejects_downgrade_with_task2_data(service, engine):
+    _enrollment(service)
+    with pytest.raises(subprocess.CalledProcessError):
+        _alembic("downgrade", "20260802_476")
+    with engine.connect() as connection:
+        assert connection.execute(text("SELECT version_num FROM alembic_version")).scalar_one() == "20260809_479"
+    _clear_edge_rows_for_test(engine)
+    with engine.begin() as connection:
+        connection.execute(text("""
+            SELECT public.record_edge_gateway_audit(
+              CAST(:uid AS uuid),NULL,'enrollment_created',NULL,FALSE,
+              '{"reason_code":"controlled_downgrade_probe"}'::jsonb
+            )
+        """), {"uid": _uid()})
+    with pytest.raises(subprocess.CalledProcessError):
+        _alembic("downgrade", "20260802_476")
+    with engine.connect() as connection:
+        assert connection.execute(text("SELECT version_num FROM alembic_version")).scalar_one() == "20260809_479"
+
+
+def test_enrollment_registration_hash_only_replay_and_failed_binding(service, engine):
+    enrollment = _enrollment(service)
+    assert enrollment["returned_once"] is True
+    token = enrollment["enrollment_token"]
+    with engine.connect() as connection:
+        row = connection.execute(text("SELECT token_hash, consumed_at FROM edge_gateway_enrollments")).mappings().one()
+    assert row["token_hash"] == hashlib.sha256(token.encode()).hexdigest()
+    assert token not in str(row)
+
+    with pytest.raises(DBAPIError), engine.begin() as connection:
+        connection.execute(text("""
+            UPDATE edge_gateway_enrollments SET expected_certificate_sha256=:certificate
+             WHERE gateway_id=CAST(:gateway AS uuid)
+        """), {"certificate": "c" * 64, "gateway": enrollment["gateway_id"]})
+
+    with pytest.raises(EdgeGatewayAuthenticationError):
+        _register(service, enrollment, certificate_sha256="c" * 64)
+    with engine.connect() as connection:
+        assert connection.execute(text("SELECT consumed_at FROM edge_gateway_enrollments")).scalar_one() is None
+        failures = connection.execute(text("SELECT safe_detail FROM edge_gateway_audits WHERE success=false")).scalars().all()
+    assert failures and all(token not in str(detail) for detail in failures)
+
+    with pytest.raises(EdgeGatewayAuthenticationError):
+        service.register(
+            enrollment_token=token, certificate_sha256="b" * 64,
+            gateway_id=enrollment["gateway_id"], environment="production",
+            network_zone="manufacturing-zone-a", policy_digest="a" * 64,
+            allowed_control_hosts=["control.example.test"],
+            allowed_proxy_hosts=["proxy.example.test"], version="3.0.0",
+        )
+    with engine.connect() as connection:
+        assert connection.execute(text("SELECT consumed_at FROM edge_gateway_enrollments")).scalar_one() is None
+
+    gateway = _register(service, enrollment)
+    assert gateway["credential_returned_once"] is True
+    with engine.connect() as connection:
+        credential = connection.execute(text("SELECT credential_hash, certificate_sha256, generation FROM edge_gateway_credentials")).mappings().one()
+    assert gateway["credential"] not in str(credential)
+    assert credential["certificate_sha256"] == "b" * 64
+    assert credential["generation"] == 1
+    with pytest.raises(EdgeGatewayAuthenticationError):
+        _register(service, enrollment)
+
+
+def test_registration_failures_are_windowed_rate_limited_and_do_not_consume(service, engine):
+    enrollment = _enrollment(service)
+    for _ in range(9):
+        with pytest.raises(EdgeGatewayAuthenticationError):
+            _register(service, enrollment, certificate_sha256="c" * 64)
+    with pytest.raises(EdgeGatewayError, match="rate limit") as limited:
+        _register(service, enrollment, certificate_sha256="c" * 64)
+    assert limited.value.status_code == 429
+    with engine.connect() as connection:
+        failure = connection.execute(text("""
+            SELECT occurrence_count FROM edge_gateway_failure_windows
+             WHERE event_type='untrusted_request_rejected'
+        """)).scalar_one()
+        audit = connection.execute(text("""
+            SELECT count(*),string_agg(safe_detail::text,'')
+              FROM edge_gateway_audits WHERE event_type='untrusted_request_rejected'
+        """)).one()
+        consumed_at = connection.execute(text("""
+            SELECT consumed_at FROM edge_gateway_enrollments
+             WHERE gateway_id=CAST(:gateway_id AS uuid)
+        """), {"gateway_id": enrollment["gateway_id"]}).scalar_one()
+    assert failure == 10
+    assert audit[0] == 1
+    assert enrollment["enrollment_token"] not in (audit[1] or "")
+    assert consumed_at is None
+    assert _register(service, enrollment)["generation"] == 1
+
+
+def test_mixed_unauthenticated_failures_share_one_bounded_unknown_bucket(service, engine):
+    limited = None
+    for attempt in range(10):
+        gateway_id = _uid()
+        try:
+            if attempt % 2:
+                service.register(
+                    enrollment_token=f"dope_invalid_{attempt}",
+                    certificate_sha256="b" * 64, gateway_id=gateway_id,
+                    environment="staging", network_zone=f"random-zone-{attempt}",
+                    policy_digest="a" * 64,
+                    allowed_control_hosts=["control.example.test"],
+                    allowed_proxy_hosts=[], version="3.0.0",
+                )
+            else:
+                service.authenticate(
+                    credential=f"dopg_invalid_{attempt}", certificate_sha256="b" * 64,
+                    gateway_id=gateway_id, environment="staging",
+                    network_zone=f"random-zone-{attempt}", generation=1,
+                )
+        except EdgeGatewayError as exc:
+            limited = exc
+    assert limited is not None and limited.status_code == 429
+    with engine.connect() as connection:
+        windows = connection.execute(text("""
+            SELECT failure_fingerprint,gateway_id,event_type,occurrence_count
+              FROM edge_gateway_failure_windows
+        """)).mappings().all()
+        audits = connection.execute(text("""
+            SELECT gateway_id,event_type,safe_detail FROM edge_gateway_audits WHERE success=false
+        """)).mappings().all()
+    assert len(windows) == 1 and windows[0]["occurrence_count"] == 10
+    assert windows[0]["gateway_id"] is None
+    assert windows[0]["event_type"] == "untrusted_request_rejected"
+    assert len(audits) == 1 and audits[0]["gateway_id"] is None
+    assert audits[0]["safe_detail"] == {"reason_code": "untrusted_failure"}
+
+
+def test_database_rejects_pending_enrollment_and_status_only_gateway_activation(service, engine):
+    enrollment = _enrollment(service)
+    values = {
+        "gateway_id": enrollment["gateway_id"], "policy": "a" * 64,
+    }
+    with pytest.raises(DBAPIError), engine.begin() as connection:
+        connection.execute(text("""
+            INSERT INTO edge_gateways
+              (uid,name,environment,network_zone,status,current_generation,policy_digest,
+               allowed_control_hosts,allowed_proxy_hosts,runtime_version)
+            VALUES (CAST(:gateway_id AS uuid),'plant-a-edge','staging','manufacturing-zone-a',
+                    'active',1,:policy,ARRAY['control.example.test'],
+                    ARRAY['proxy.example.test'],'3.0.0')
+        """), values)
+    with engine.begin() as connection:
+        connection.execute(text("""
+            INSERT INTO edge_gateways
+              (uid,name,environment,network_zone,status,current_generation,policy_digest,
+               allowed_control_hosts,allowed_proxy_hosts,runtime_version)
+            VALUES (CAST(:gateway_id AS uuid),'plant-a-edge','staging','manufacturing-zone-a',
+                    'pending',1,:policy,ARRAY['control.example.test'],
+                    ARRAY['proxy.example.test'],'3.0.0')
+        """), values)
+    with pytest.raises(DBAPIError), engine.begin() as connection:
+        connection.execute(text("""
+            UPDATE edge_gateways SET status='active'
+             WHERE uid=CAST(:gateway_id AS uuid)
+        """), values)
+
+
+def test_authentication_rotation_revocation_heartbeat_and_active_binding(service, engine):
+    gateway = _register(service, _enrollment(service))
+    identity = service.authenticate(
+        credential=gateway["credential"], certificate_sha256="b" * 64,
+        gateway_id=gateway["gateway_id"], environment="staging",
+        network_zone="manufacturing-zone-a", generation=1,
+    )
+    assert identity["gateway_id"] == gateway["gateway_id"]
+    for statement, parameters in (
+        ("UPDATE edge_gateway_credentials SET certificate_sha256=:value WHERE gateway_id=CAST(:gateway AS uuid)", {"value": "c" * 64}),
+        ("UPDATE edge_gateway_credentials SET credential_hash=:value WHERE gateway_id=CAST(:gateway AS uuid)", {"value": "d" * 64}),
+        ("UPDATE edge_gateways SET name='attacker' WHERE uid=CAST(:gateway AS uuid)", {}),
+        ("UPDATE edge_gateways SET environment='production' WHERE uid=CAST(:gateway AS uuid)", {}),
+        ("UPDATE edge_gateways SET network_zone='attacker-zone' WHERE uid=CAST(:gateway AS uuid)", {}),
+        ("UPDATE edge_gateways SET policy_digest=:value WHERE uid=CAST(:gateway AS uuid)", {"value": "c" * 64}),
+        ("UPDATE edge_gateways SET allowed_control_hosts=ARRAY['attacker.test'] WHERE uid=CAST(:gateway AS uuid)", {}),
+        ("UPDATE edge_gateways SET allowed_proxy_hosts=ARRAY['attacker.test'] WHERE uid=CAST(:gateway AS uuid)", {}),
+        ("""UPDATE edge_gateway_credentials SET generation=2,certificate_sha256=:certificate,
+              credential_hash=:credential WHERE gateway_id=CAST(:gateway AS uuid);
+              UPDATE edge_gateways SET current_generation=2 WHERE uid=CAST(:gateway AS uuid)""",
+         {"certificate": "c" * 64, "credential": "d" * 64}),
+    ):
+        with pytest.raises(DBAPIError), engine.begin() as connection:
+            connection.execute(text(statement), {"gateway": gateway["gateway_id"], **parameters})
+    with pytest.raises(DBAPIError), engine.begin() as connection:
+        connection.execute(text("""
+            UPDATE edge_gateway_credentials SET status='rotated',revoked_at=CURRENT_TIMESTAMP
+             WHERE gateway_id=CAST(:gateway_id AS uuid) AND generation=1
+        """), {"gateway_id": gateway["gateway_id"]})
+    with pytest.raises(EdgeGatewayAuthenticationError):
+        service.authenticate(
+            credential=gateway["credential"], certificate_sha256="c" * 64,
+            gateway_id=gateway["gateway_id"], environment="staging",
+            network_zone="manufacturing-zone-a", generation=1,
+        )
+    with pytest.raises(EdgeGatewayValidationError):
+        service.heartbeat(
+            credential=gateway["credential"], certificate_sha256="b" * 64,
+            gateway_id=gateway["gateway_id"], environment="staging",
+            network_zone="manufacturing-zone-a", generation=1, version="3.0.0",
+            safe_summary={"password": "must-not-persist"},
+        )
+    with engine.connect() as connection:
+        assert connection.execute(text("SELECT last_heartbeat_at FROM edge_gateways")).scalar_one() is None
+    rotation_actor = _uid()
+    rotated = service.rotate(
+        gateway["gateway_id"], certificate_sha256="c" * 64,
+        request_id="rotation-request-1", actor_uid=rotation_actor,
+    )
+    rotation_replay = service.rotate(
+        gateway["gateway_id"], certificate_sha256="c" * 64,
+        request_id="rotation-request-1", actor_uid=rotation_actor,
+    )
+    assert rotation_replay == {
+        "gateway_id": gateway["gateway_id"], "generation": 2,
+        "credential": None, "credential_returned_once": False, "replayed": True,
+    }
+    with pytest.raises(EdgeGatewayConflictError):
+        service.rotate(
+            gateway["gateway_id"], certificate_sha256="d" * 64,
+            request_id="rotation-request-1", actor_uid=rotation_actor,
+        )
+    with pytest.raises(EdgeGatewayAuthenticationError):
+        service.authenticate(
+            credential=gateway["credential"], certificate_sha256="b" * 64,
+            gateway_id=gateway["gateway_id"], environment="staging",
+            network_zone="manufacturing-zone-a", generation=1,
+        )
+    assert service.heartbeat(
+        credential=rotated["credential"], certificate_sha256="c" * 64,
+        gateway_id=gateway["gateway_id"], environment="staging",
+        network_zone="manufacturing-zone-a", generation=2, version="3.0.1",
+        safe_summary={"queue_depth": 1},
+    )["status"] == "online"
+    assert service.revoke(gateway["gateway_id"], actor_uid=_uid()) is True
+    with pytest.raises(DBAPIError), engine.begin() as connection:
+        connection.execute(text("UPDATE edge_gateways SET status='active' WHERE uid=CAST(:gateway AS uuid)"), {"gateway": gateway["gateway_id"]})
+    with pytest.raises(EdgeGatewayAuthenticationError):
+        service.authenticate(
+            credential=rotated["credential"], certificate_sha256="c" * 64,
+            gateway_id=gateway["gateway_id"], environment="staging",
+            network_zone="manufacturing-zone-a", generation=2,
+        )
+    with engine.connect() as connection:
+        assert "untrusted_request_rejected" in set(connection.execute(text(
+            "SELECT event_type FROM edge_gateway_audits WHERE success=false"
+        )).scalars())
+
+
+def test_database_rejects_second_active_credential_combination_injection(service, engine):
+    gateway = _register(service, _enrollment(service))
+    with pytest.raises(DBAPIError), engine.begin() as connection:
+        connection.execute(text("""
+            INSERT INTO edge_gateway_credentials
+              (uid,gateway_id,generation,credential_hash,certificate_sha256,status,expires_at)
+            VALUES (CAST(:uid AS uuid),CAST(:gateway AS uuid),2,:credential,:certificate,
+                    'active',CURRENT_TIMESTAMP+INTERVAL '365 days');
+            UPDATE edge_gateways SET current_generation=2 WHERE uid=CAST(:gateway AS uuid)
+        """), {
+            "uid": _uid(), "gateway": gateway["gateway_id"],
+            "credential": "9" * 64, "certificate": "8" * 64,
+        })
+    with engine.connect() as connection:
+        row = connection.execute(text("""
+            SELECT g.current_generation,count(*) FILTER (WHERE c.status='active') AS active_count
+              FROM edge_gateways g JOIN edge_gateway_credentials c ON c.gateway_id=g.uid
+             GROUP BY g.current_generation
+        """)).mappings().one()
+    assert row == {"current_generation": 1, "active_count": 1}
+
+
+def test_failure_evidence_tables_reject_direct_mutation_and_truncation(service, engine):
+    with pytest.raises(EdgeGatewayAuthenticationError):
+        service.authenticate(
+            credential="dopg_invalid", certificate_sha256="b" * 64,
+            gateway_id=_uid(), environment="staging",
+            network_zone="zone-a", generation=1,
+        )
+    with pytest.raises(DBAPIError), engine.begin() as connection:
+        connection.execute(text("TRUNCATE TABLE edge_gateway_audits"))
+    for statement in (
+        "UPDATE edge_gateway_failure_windows SET occurrence_count=occurrence_count+1,last_seen_at=CURRENT_TIMESTAMP",
+        "UPDATE edge_gateway_failure_windows SET occurrence_count=occurrence_count+5",
+        "UPDATE edge_gateway_failure_windows SET last_seen_at=last_seen_at-INTERVAL '1 second'",
+        "UPDATE edge_gateway_failure_windows SET first_seen_at=first_seen_at+INTERVAL '1 second'",
+        "UPDATE edge_gateway_failure_windows SET failure_fingerprint=:fingerprint",
+        "DELETE FROM edge_gateway_failure_windows",
+        "TRUNCATE TABLE edge_gateway_failure_windows",
+    ):
+        with pytest.raises(DBAPIError), engine.begin() as connection:
+            connection.execute(text(statement), {"fingerprint": "f" * 64})
+    with engine.connect() as connection:
+        evidence = connection.execute(text("""
+            SELECT occurrence_count,first_seen_at<=last_seen_at AS monotonic
+              FROM edge_gateway_failure_windows
+        """)).one()
+        assert evidence == (1, True)
+        assert connection.execute(text("SELECT count(*) FROM edge_gateway_audits")).scalar_one() == 1
+
+
+def test_task_pull_cancel_event_idempotency_policy_and_release(service, engine):
+    gateway = _register(service, _enrollment(service))
+    auth = {
+        "credential": gateway["credential"], "certificate_sha256": "b" * 64,
+        "gateway_id": gateway["gateway_id"], "environment": "staging",
+        "network_zone": "manufacturing-zone-a", "generation": 1,
+    }
+    task = service.issue_task({
+        "task_id": "task-1", "gateway_id": gateway["gateway_id"],
+        "environment": "staging", "network_zone": "manufacturing-zone-a",
+        "purpose": "inventory", "classification": "statistics", "task_type": "profile",
+        "contract_version": 1,
+        "deadline_at": (datetime.now(UTC) + timedelta(minutes=10)).isoformat().replace("+00:00", "Z"),
+        "attempt": 1, "idempotency_key": "task-key-1", "policy_digest": "a" * 64,
+    }, actor_uid=_uid())
+    conflicting_task = {
+        "task_id": "task-conflicting", "gateway_id": gateway["gateway_id"],
+        "environment": "staging", "network_zone": "manufacturing-zone-a",
+        "purpose": "different-purpose", "classification": "statistics", "task_type": "profile",
+        "contract_version": 1,
+        "deadline_at": (datetime.now(UTC) + timedelta(minutes=10)).isoformat().replace("+00:00", "Z"),
+        "attempt": 1, "idempotency_key": "task-key-1", "policy_digest": "a" * 64,
+    }
+    with pytest.raises(EdgeGatewayConflictError):
+        service.issue_task(conflicting_task, actor_uid=_uid())
+    with pytest.raises(EdgeGatewayValidationError):
+        service.issue_task({
+            "task_id": "task-wrong-binding", "gateway_id": gateway["gateway_id"],
+            "environment": "production", "network_zone": "manufacturing-zone-a",
+            "purpose": "inventory", "classification": "statistics", "task_type": "profile",
+            "contract_version": 1,
+            "deadline_at": (datetime.now(UTC) + timedelta(minutes=10)).isoformat().replace("+00:00", "Z"),
+            "attempt": 1, "idempotency_key": "task-wrong-binding-key", "policy_digest": "a" * 64,
+        }, actor_uid=_uid())
+    pulled = service.pull_task(**auth)
+    assert pulled["task"]["task_id"] == task["task_id"]
+
+    approved = _event(task, gateway)
+    with pytest.raises(EdgeGatewayAuthenticationError):
+        service.accept_event(event=approved, lease_token="dopl_wrong", **auth)
+    ack1 = service.accept_event(event=approved, lease_token=pulled["lease_token"], **auth)
+    ack2 = service.accept_event(event=approved, lease_token=pulled["lease_token"], **auth)
+    assert ack1 == ack2
+    assert set(ack1) == {
+        "event_id", "event_digest", "remote_lease_digest", "received_at", "status",
+    }
+    assert ack1["event_digest"] == EdgeEventContract.from_mapping(approved).digest
+    assert ack1["remote_lease_digest"] == hashlib.sha256(
+        pulled["lease_token"].encode("utf-8")
+    ).hexdigest()
+    assert pulled["lease_token"] not in str(ack1)
+    changed = dict(approved)
+    changed["payload"] = {"asset_count": 13}
+    with pytest.raises((EdgeGatewayConflictError, EdgeGatewayValidationError)):
+        service.accept_event(event=changed, **auth)
+    raw = dict(approved)
+    raw.update(classification="raw", payload={"password": "super-secret-value"}, event_id="evt_" + "0" * 64)
+    with pytest.raises(EdgeGatewayValidationError):
+        service.accept_event(event=raw, **auth)
+    wrong_purpose = dict(approved)
+    wrong_purpose["purpose"] = "different-purpose"
+    identity = {key: value for key, value in wrong_purpose.items() if key != "event_id"}
+    wrong_purpose["event_id"] = stable_event_id(identity)
+    with pytest.raises((EdgeGatewayValidationError, EdgeGatewayAuthenticationError)):
+        service.accept_event(event=wrong_purpose, lease_token=pulled["lease_token"], **auth)
+    assert service.cancel_task(task["task_id"], actor_uid=_uid()) is False
+    assert service.reconcile(**auth)["cancelled_task_ids"] == []
+    assert service.accept_event(
+        event=approved, lease_token=pulled["lease_token"], **auth,
+    ) == ack1
+    with engine.connect() as connection:
+        assert connection.execute(text("SELECT count(*) FROM edge_gateway_events")).scalar_one() == 1
+
+    release = service.offer_release(
+        gateway["gateway_id"], version="3.1.0", artifact_digest="d" * 64,
+        rollback_version="3.0.0", request_id="release-request-1", actor_uid=_uid(),
+    )
+    assert service.offer_release(
+        gateway["gateway_id"], version="3.1.0", artifact_digest="d" * 64,
+        rollback_version="3.0.0", request_id="release-request-1", actor_uid=_uid(),
+    ) == release
+    with pytest.raises(EdgeGatewayConflictError):
+        service.offer_release(
+            gateway["gateway_id"], version="3.1.1", artifact_digest="d" * 64,
+            rollback_version="3.0.0", request_id="release-request-1", actor_uid=_uid(),
+        )
+    with pytest.raises(EdgeGatewayConflictError):
+        service.acknowledge_release(
+            release_id=release["release_id"], outcome="rollback",
+            safe_summary={"reason_code": "health_check_failed"}, **auth,
+        )
+    accepted = service.acknowledge_release(
+        release_id=release["release_id"], outcome="accepted",
+        safe_summary={"reason_code": "accepted"}, **auth,
+    )
+    pending = service.reconcile(**auth)["release_offers"]
+    assert [(item["release_id"], item["status"]) for item in pending] == [(release["release_id"], "accepted")]
+    assert service.acknowledge_release(
+        release_id=release["release_id"], outcome="accepted",
+        safe_summary={"reason_code": "accepted"}, **auth,
+    ) == accepted
+    service.acknowledge_release(
+        release_id=release["release_id"], outcome="failed",
+        safe_summary={"reason_code": "health_check_failed"}, **auth,
+    )
+    acked = service.acknowledge_release(
+        release_id=release["release_id"], outcome="rollback",
+        safe_summary={"reason_code": "health_check_failed"}, **auth,
+    )
+    assert acked["status"] == "rolled_back"
+
+    installed_release = service.offer_release(
+        gateway["gateway_id"], version="3.2.0", artifact_digest="e" * 64,
+        rollback_version="3.1.0", request_id="release-request-2", actor_uid=_uid(),
+    )
+    service.acknowledge_release(
+        release_id=installed_release["release_id"], outcome="accepted",
+        safe_summary={"reason_code": "accepted"}, **auth,
+    )
+    service.acknowledge_release(
+        release_id=installed_release["release_id"], outcome="installed",
+        safe_summary={"reason_code": "installed"}, **auth,
+    )
+    installed_rollback = service.acknowledge_release(
+        release_id=installed_release["release_id"], outcome="rollback",
+        safe_summary={"reason_code": "post_install_health_failed"}, **auth,
+    )
+    assert installed_rollback["status"] == "rolled_back"
+    assert service.acknowledge_release(
+        release_id=installed_release["release_id"], outcome="rollback",
+        safe_summary={"reason_code": "post_install_health_failed"}, **auth,
+    ) == installed_rollback
+    with engine.connect() as connection:
+        audit_rows = connection.execute(text("SELECT event_type,safe_detail FROM edge_gateway_audits")).mappings().all()
+    details = [row["safe_detail"] for row in audit_rows]
+    assert all("credential" not in str(detail).lower() and "token" not in str(detail).lower() for detail in details)
+    assert all("super-secret-value" not in str(detail) for detail in details)
+    assert {"idempotency_conflict", "binding_rejected", "policy_rejected", "release_rejected"} <= {
+        row["event_type"] for row in audit_rows
+    }
+    with pytest.raises(DBAPIError), engine.begin() as connection:
+        connection.execute(text("UPDATE edge_gateway_audits SET success=true"))
+    with pytest.raises(DBAPIError), engine.begin() as connection:
+        connection.execute(text("DELETE FROM edge_gateway_audits"))
+
+    task_failed = service.issue_task({
+        "task_id": "task-failed", "gateway_id": gateway["gateway_id"],
+        "environment": "staging", "network_zone": "manufacturing-zone-a",
+        "purpose": "inventory", "classification": "statistics", "task_type": "quality",
+        "contract_version": 1,
+        "deadline_at": (datetime.now(UTC) + timedelta(minutes=10)).isoformat().replace("+00:00", "Z"),
+        "attempt": 1, "idempotency_key": "task-failed-key", "policy_digest": "a" * 64,
+    }, actor_uid=_uid())
+    failed_lease = service.pull_task(**auth)
+    failed = service.task_outcome(
+        task_failed["task_id"], outcome="failed", lease_token=failed_lease["lease_token"],
+        safe_summary={"reason_code": "controlled_failure"}, **auth,
+    )
+    assert failed["status"] == "failed"
+    assert service.task_outcome(
+        task_failed["task_id"], outcome="failed", lease_token=failed_lease["lease_token"],
+        safe_summary={"reason_code": "controlled_failure"}, **auth,
+    )["replayed"] is True
+    task_cancel = service.issue_task({
+        "task_id": "task-cancelled", "gateway_id": gateway["gateway_id"],
+        "environment": "staging", "network_zone": "manufacturing-zone-a",
+        "purpose": "inventory", "classification": "statistics", "task_type": "profile",
+        "contract_version": 1,
+        "deadline_at": (datetime.now(UTC) + timedelta(minutes=10)).isoformat().replace("+00:00", "Z"),
+        "attempt": 1, "idempotency_key": "task-cancelled-key", "policy_digest": "a" * 64,
+    }, actor_uid=_uid())
+    cancel_lease = service.pull_task(**auth)
+    assert service.cancel_task(task_cancel["task_id"], actor_uid=_uid()) is True
+    assert service.task_outcome(
+        task_cancel["task_id"], outcome="cancelled", lease_token=cancel_lease["lease_token"],
+        safe_summary={"reason_code": "cancel_acknowledged"}, **auth,
+    )["status"] == "cancelled"
+
+
+def test_control_plane_signs_task_authority_and_complete_release_state(service, engine):
+    gateway = _register(service, _enrollment(service))
+    auth = {
+        "credential": gateway["credential"],
+        "certificate_sha256": "b" * 64,
+        "gateway_id": gateway["gateway_id"],
+        "environment": "staging",
+        "network_zone": "manufacturing-zone-a",
+        "generation": 1,
+    }
+    deadline = (datetime.now(UTC) + timedelta(minutes=10)).isoformat().replace("+00:00", "Z")
+    task = service.issue_task({
+        "task_id": "task-signed-authority", "gateway_id": gateway["gateway_id"],
+        "environment": "staging", "network_zone": "manufacturing-zone-a",
+        "purpose": "inventory", "classification": "statistics", "task_type": "profile",
+        "contract_version": 1, "deadline_at": deadline, "attempt": 1,
+        "idempotency_key": "task-signed-authority-key", "policy_digest": "a" * 64,
+    }, actor_uid=_uid())
+    pulled = service.pull_task(**auth)
+    envelope = pulled["signed_task_envelope"]
+    assert pulled["task"] == envelope["task"]
+    assert envelope["task"]["task_id"] == task["task_id"]
+    assert set(envelope) == {
+        "task", "authority_key_id", "signature_algorithm", "contract_digest",
+        "gateway_id", "environment",
+        "network_zone", "policy_digest", "purpose", "issued_at", "expires_at",
+        "signature",
+    }
+    assert envelope["signature_algorithm"] == "Ed25519"
+    assert envelope["authority_key_id"] == _SIGNING_KEY_ID
+    assert envelope["contract_digest"] == canonical_sha256(envelope["task"])
+    unsigned_authority = {key: value for key, value in envelope.items() if key != "signature"}
+    _SIGNING_PRIVATE_KEY.public_key().verify(
+        bytes.fromhex(envelope["signature"]),
+        SignedTaskEnvelope.canonical_unsigned_bytes(unsigned_authority),
+    )
+    release_deadline = (datetime.now(UTC) + timedelta(hours=1)).isoformat().replace("+00:00", "Z")
+    release = service.offer_release(
+        gateway["gateway_id"], version="6.0.0", artifact_digest="6" * 64,
+        artifact_name="edge-agent-6.0.0.bin", deadline_at=release_deadline,
+        rollback_version="3.0.0", request_id="signed-release-1", actor_uid=_uid(),
+    )
+    manifest = release["manifest"]
+    with pytest.raises(EdgeGatewayValidationError):
+        service.offer_release(
+            gateway["gateway_id"], version="release-six", artifact_digest="6" * 64,
+            rollback_version="3.0.0", request_id="invalid-release-version",
+            actor_uid=_uid(),
+        )
+    with pytest.raises(EdgeGatewayValidationError):
+        service.offer_release(
+            gateway["gateway_id"], version="2.9.0", artifact_digest="6" * 64,
+            rollback_version="3.0.0", request_id="release-downgrade",
+            actor_uid=_uid(),
+        )
+    assert set(manifest) == {
+        "release_id", "version", "rollback_version", "artifact_digest", "artifact_name",
+        "deadline_at", "signature_algorithm", "key_id", "manifest_digest", "signature",
+        "status",
+    }
+    assert manifest["status"] == "offered"
+    unsigned_manifest = {
+        key: value for key, value in manifest.items()
+        if key not in {"manifest_digest", "signature"}
+    }
+    assert manifest["manifest_digest"] == canonical_sha256(unsigned_manifest)
+    _SIGNING_PRIVATE_KEY.public_key().verify(
+        bytes.fromhex(manifest["signature"]), canonical_json_bytes(unsigned_manifest),
+    )
+    for outcome, expected in (("accepted", "accepted"), ("failed", "failed"), ("rollback", "rolled_back")):
+        service.acknowledge_release(
+            release_id=release["release_id"], outcome=outcome,
+            safe_summary={"release_status": expected, "version": "3.0.0"}, **auth,
+        )
+        response = service.reconcile(**auth)
+        candidates = list(response["release_offers"])
+        if response["release_baseline"] is not None:
+            candidates.append(response["release_baseline"])
+        reconciled = {
+            item["release_id"]: item for item in candidates
+        }[release["release_id"]]
+        assert reconciled["status"] == expected
+        unsigned = {
+            key: value for key, value in reconciled.items()
+            if key not in {"manifest_digest", "signature"}
+        }
+        assert reconciled["manifest_digest"] == canonical_sha256(unsigned)
+        _SIGNING_PRIVATE_KEY.public_key().verify(
+            bytes.fromhex(reconciled["signature"]), canonical_json_bytes(unsigned),
+        )
+    with engine.connect() as connection:
+        task_row = connection.execute(text("""
+            SELECT authority,contract FROM edge_gateway_tasks WHERE task_id='task-signed-authority'
+        """)).mappings().one()
+        release_row = connection.execute(text("""
+            SELECT signed_manifest FROM edge_gateway_releases WHERE uid=CAST(:uid AS uuid)
+        """), {"uid": release["release_id"]}).scalar_one()
+    assert dict(task_row["authority"]) == envelope
+    assert dict(release_row)["status"] == "rolled_back"
+    persisted = f"{task_row['authority']}{release_row}"
+    assert "PRIVATE KEY" not in persisted and "signing_private_key" not in persisted
+
+
+@pytest.mark.parametrize("seconds", [30, 119, 120])
+def test_pull_lease_never_extends_beyond_task_deadline(service, seconds):
+    gateway = _register(service, _enrollment(service))
+    auth = {
+        "credential": gateway["credential"],
+        "certificate_sha256": "b" * 64,
+        "gateway_id": gateway["gateway_id"],
+        "environment": "staging",
+        "network_zone": "manufacturing-zone-a",
+        "generation": 1,
+    }
+    deadline = (datetime.now(UTC) + timedelta(seconds=seconds)).isoformat().replace(
+        "+00:00", "Z"
+    )
+    service.issue_task(
+        {
+            "task_id": f"deadline-boundary-{seconds}",
+            "gateway_id": gateway["gateway_id"],
+            "environment": "staging",
+            "network_zone": "manufacturing-zone-a",
+            "purpose": "inventory",
+            "classification": "statistics",
+            "task_type": "profile",
+            "contract_version": 1,
+            "deadline_at": deadline,
+            "attempt": 1,
+            "idempotency_key": f"deadline-boundary-{seconds}-key",
+            "policy_digest": "a" * 64,
+        },
+        actor_uid=_uid(),
+    )
+    pulled = service.pull_task(**auth)
+    assert datetime.fromisoformat(pulled["lease_expires_at"]) <= datetime.fromisoformat(
+        deadline.replace("Z", "+00:00")
+    )
+
+
+def test_cancel_reconcile_uses_stable_cursor_so_more_than_fifty_never_starve(
+    service,
+):
+    gateway = _register(service, _enrollment(service))
+    auth = {
+        "credential": gateway["credential"],
+        "certificate_sha256": "b" * 64,
+        "gateway_id": gateway["gateway_id"],
+        "environment": "staging",
+        "network_zone": "manufacturing-zone-a",
+        "generation": 1,
+    }
+    expected = []
+    deadline = (datetime.now(UTC) + timedelta(minutes=10)).isoformat().replace(
+        "+00:00", "Z"
+    )
+    for index in range(52):
+        task_id = f"cancel-page-{index:02d}"
+        service.issue_task(
+            {
+                "task_id": task_id,
+                "gateway_id": gateway["gateway_id"],
+                "environment": "staging",
+                "network_zone": "manufacturing-zone-a",
+                "purpose": "inventory",
+                "classification": "statistics",
+                "task_type": "profile",
+                "contract_version": 1,
+                "deadline_at": deadline,
+                "attempt": 1,
+                "idempotency_key": f"cancel-page-key-{index:02d}",
+                "policy_digest": "a" * 64,
+            },
+            actor_uid=_uid(),
+        )
+        assert service.pull_task(**auth)["task"]["task_id"] == task_id
+        assert service.cancel_task(task_id, actor_uid=_uid()) is True
+        expected.append(task_id)
+
+    first = service.reconcile(limit=50, cancel_cursor=None, release_cursor=None, **auth)
+    assert first["cancelled_task_ids"] == expected[:50]
+    assert first["cancel_next_cursor"] == expected[49]
+    second = service.reconcile(
+        limit=50,
+        cancel_cursor=first["cancel_next_cursor"],
+        release_cursor=None,
+        **auth,
+    )
+    assert second["cancelled_task_ids"] == expected[50:]
+    assert second["cancel_next_cursor"] is None
+
+
+def test_pull_terminalizes_expired_task_without_issuing_lease(service, engine):
+    gateway = _register(service, _enrollment(service))
+    auth = {
+        "credential": gateway["credential"],
+        "certificate_sha256": "b" * 64,
+        "gateway_id": gateway["gateway_id"],
+        "environment": "staging",
+        "network_zone": "manufacturing-zone-a",
+        "generation": 1,
+    }
+    task_id = "already-expired-task"
+    service.issue_task(
+        {
+            "task_id": task_id,
+            "gateway_id": gateway["gateway_id"],
+            "environment": "staging",
+            "network_zone": "manufacturing-zone-a",
+            "purpose": "inventory",
+            "classification": "statistics",
+            "task_type": "profile",
+            "contract_version": 1,
+            "deadline_at": (datetime.now(UTC) + timedelta(milliseconds=200))
+            .isoformat()
+            .replace("+00:00", "Z"),
+            "attempt": 1,
+            "idempotency_key": "already-expired-task-key",
+            "policy_digest": "a" * 64,
+        },
+        actor_uid=_uid(),
+    )
+    with engine.connect() as connection:
+        connection.execute(text("SELECT pg_sleep(0.3)"))
+    assert service.pull_task(**auth) == {"task": None}
+    with engine.connect() as connection:
+        row = connection.execute(
+            text(
+                "SELECT status,lease_token_hash,lease_expires_at,result_summary "
+                "FROM edge_gateway_tasks WHERE task_id=:task_id"
+            ),
+            {"task_id": task_id},
+        ).mappings().one()
+    assert row["status"] == "failed"
+    assert row["lease_token_hash"] is None and row["lease_expires_at"] is None
+    assert dict(row["result_summary"]) == {"reason_code": "task_deadline_expired"}
+
+
+def test_reconcile_prioritizes_new_actionable_release_over_terminal_history(service):
+    gateway = _register(service, _enrollment(service))
+    auth = {
+        "credential": gateway["credential"],
+        "certificate_sha256": "b" * 64,
+        "gateway_id": gateway["gateway_id"],
+        "environment": "staging",
+        "network_zone": "manufacturing-zone-a",
+        "generation": 1,
+    }
+    for index in range(51):
+        release = service.offer_release(
+            gateway["gateway_id"],
+            version=f"4.{index}.0",
+            rollback_version="3.0.0",
+            artifact_digest=f"{index:064x}",
+            request_id=f"terminal-release-{index}",
+            actor_uid=_uid(),
+        )
+        service.acknowledge_release(
+            release_id=release["release_id"],
+            outcome="accepted",
+            safe_summary={"release_status": "accepted", "version": f"4.{index}.0"},
+            **auth,
+        )
+        service.acknowledge_release(
+            release_id=release["release_id"],
+            outcome="installed",
+            safe_summary={"release_status": "installed", "version": f"4.{index}.0"},
+            **auth,
+        )
+    actionable = service.offer_release(
+        gateway["gateway_id"],
+        version="9.0.0",
+        rollback_version="3.0.0",
+        artifact_digest="f" * 64,
+        request_id="new-actionable-release",
+        actor_uid=_uid(),
+    )
+    reconciled = service.reconcile(limit=50, **auth)
+    releases = reconciled["release_offers"]
+    assert len(releases) == 1
+    assert releases[0]["release_id"] == actionable["release_id"]
+    assert releases[0]["status"] == "offered"
+    assert reconciled["release_next_cursor"] is None
+    assert reconciled["release_baseline"]["status"] == "installed"
+    assert reconciled["release_baseline"]["version"] == "4.50.0"
+
+
+def test_signed_release_offer_installs_through_real_postgres_control_plane(
+    service, engine, tmp_path,
+):
+    gateway = _register(service, _enrollment(service))
+    auth = {
+        "credential": gateway["credential"],
+        "certificate_sha256": "b" * 64,
+        "gateway_id": gateway["gateway_id"],
+        "environment": "staging",
+        "network_zone": "manufacturing-zone-a",
+        "generation": 1,
+    }
+    artifact = b"postgres-backed-signed-edge-release"
+    offered = service.offer_release(
+        gateway["gateway_id"],
+        version="3.1.0",
+        rollback_version="3.0.0",
+        artifact_digest=hashlib.sha256(artifact).hexdigest(),
+        artifact_name="edge-agent-3.1.0.bin",
+        deadline_at=(datetime.now(UTC) + timedelta(hours=1))
+        .isoformat()
+        .replace("+00:00", "Z"),
+        request_id="postgres-agent-release-chain",
+        actor_uid=_uid(),
+    )
+
+    class PostgresControlTransport:
+        def reconcile(self):
+            return service.reconcile(**auth)
+
+        def acknowledge_release(self, release_id, outcome, summary):
+            return service.acknowledge_release(
+                release_id=release_id,
+                outcome=outcome,
+                safe_summary=summary,
+                **auth,
+            )
+
+        def pull_task(self):
+            return None
+
+    public_key = _SIGNING_PRIVATE_KEY.public_key().public_bytes(
+        encoding=serialization.Encoding.Raw,
+        format=serialization.PublicFormat.Raw,
+    ).hex()
+    activated = []
+    agent = EdgeAgent(
+        EdgeBootstrapConfig(
+            queue_path=str(tmp_path / "postgres-release-agent.sqlite3"),
+            artifact_root=str(tmp_path),
+            gateway_id=gateway["gateway_id"],
+            credential=gateway["credential"],
+            certificate_sha256="b" * 64,
+            client_certificate_path=str(tmp_path / "client.pem"),
+            client_private_key_path=str(tmp_path / "client.key"),
+            ca_bundle_path=str(tmp_path / "ca.pem"),
+            generation=1,
+            environment="staging",
+            network_zone="manufacturing-zone-a",
+            policy_digest="a" * 64,
+            control_url="https://control.example.test",
+            proxy_url=None,
+            allowed_control_hosts=frozenset({"control.example.test"}),
+            allowed_proxy_hosts=frozenset(),
+            trusted_release_keys={_SIGNING_KEY_ID: public_key},
+            trusted_task_keys={_SIGNING_KEY_ID: public_key},
+            version="3.0.0",
+        ),
+        PostgresControlTransport(),
+        EdgeRunnerAdapter(
+            lambda _node, _request, _cancel: {
+                "classification": "statistics",
+                "payload": {"metric_count": 1},
+            }
+        ),
+        release_manager=SignedReleaseManager(
+            current_version="3.0.0",
+            trusted_keys={_SIGNING_KEY_ID: public_key},
+            artifact_loader=lambda _manifest: artifact,
+            activator=lambda version, body: activated.append((version, body)) or True,
+            rollback=lambda _version: False,
+        ),
+    )
+    agent.run_once()
+
+    with engine.connect() as connection:
+        persisted = connection.execute(
+            text("SELECT status,signed_manifest FROM edge_gateway_releases WHERE uid=CAST(:uid AS uuid)"),
+            {"uid": offered["release_id"]},
+        ).mappings().one()
+    assert persisted["status"] == "installed"
+    assert dict(persisted["signed_manifest"])["status"] == "installed"
+    assert activated == [("3.1.0", artifact)]
+    assert agent.queue.get_release_state(offered["release_id"]).status == "installed"
+
+
+def test_signing_operations_fail_closed_without_configured_private_key(service, engine):
+    gateway = _register(service, _enrollment(service))
+    session = Session(engine)
+    unsigned_service = EdgeGatewayService(EdgeGatewayRepository(session))
+    try:
+        with pytest.raises(EdgeGatewayConfigurationError):
+            unsigned_service.issue_task({
+                "task_id": "task-no-signer", "gateway_id": gateway["gateway_id"],
+                "environment": "staging", "network_zone": "manufacturing-zone-a",
+                "purpose": "inventory", "classification": "statistics", "task_type": "profile",
+                "contract_version": 1,
+                "deadline_at": (datetime.now(UTC) + timedelta(minutes=10)).isoformat().replace("+00:00", "Z"),
+                "attempt": 1, "idempotency_key": "task-no-signer-key", "policy_digest": "a" * 64,
+            }, actor_uid=_uid())
+    finally:
+        session.close()
+
+    with pytest.raises(EdgeGatewayValidationError):
+        service.issue_task({
+            "task_id": "task-expired-authority", "gateway_id": gateway["gateway_id"],
+            "environment": "staging", "network_zone": "manufacturing-zone-a",
+            "purpose": "inventory", "classification": "statistics", "task_type": "profile",
+            "contract_version": 1,
+            "deadline_at": (datetime.now(UTC) - timedelta(seconds=1)).isoformat().replace("+00:00", "Z"),
+            "attempt": 1, "idempotency_key": "task-expired-authority-key", "policy_digest": "a" * 64,
+        }, actor_uid=_uid())
+
+
+def test_task_outcome_audit_is_atomic_bounded_and_replay_idempotent(service, engine):
+    gateway = _register(service, _enrollment(service))
+    auth = {"credential": gateway["credential"], "certificate_sha256": "b" * 64,
+            "gateway_id": gateway["gateway_id"], "environment": "staging",
+            "network_zone": "manufacturing-zone-a", "generation": 1}
+    task = service.issue_task({
+        "task_id": "task-outcome-audit", "gateway_id": gateway["gateway_id"],
+        "environment": "staging", "network_zone": "manufacturing-zone-a",
+        "purpose": "inventory", "classification": "statistics", "task_type": "quality",
+        "contract_version": 1,
+        "deadline_at": (datetime.now(UTC) + timedelta(minutes=10)).isoformat().replace("+00:00", "Z"),
+        "attempt": 1, "idempotency_key": "task-outcome-audit-key", "policy_digest": "a" * 64,
+    }, actor_uid=_uid())
+    lease = service.pull_task(**auth)
+    with pytest.raises(EdgeGatewayValidationError):
+        service.task_outcome(
+            task["task_id"], outcome="failed", lease_token=lease["lease_token"],
+            safe_summary={"reason_code": "x" * 40_000}, **auth,
+        )
+    with engine.begin() as connection:
+        connection.execute(text("""
+            CREATE OR REPLACE FUNCTION test_reject_task_outcome_audit()
+            RETURNS trigger LANGUAGE plpgsql AS $$
+            BEGIN
+              IF NEW.event_type='task_outcome_recorded' THEN
+                RAISE EXCEPTION 'test audit failure';
+              END IF;
+              RETURN NEW;
+            END $$;
+            CREATE TRIGGER test_reject_task_outcome_audit
+              BEFORE INSERT ON edge_gateway_audits FOR EACH ROW
+              EXECUTE FUNCTION test_reject_task_outcome_audit();
+        """))
+    session_usable = False
+    try:
+        with pytest.raises(DBAPIError):
+            service.task_outcome(
+                task["task_id"], outcome="failed", lease_token=lease["lease_token"],
+                safe_summary={"reason_code": "controlled_failure"}, **auth,
+            )
+        service.repository.session.execute(text("SELECT 1")).scalar_one()
+        session_usable = True
+    finally:
+        service.repository.rollback()
+        with engine.begin() as connection:
+            connection.execute(text("DROP TRIGGER test_reject_task_outcome_audit ON edge_gateway_audits"))
+            connection.execute(text("DROP FUNCTION test_reject_task_outcome_audit()"))
+    assert session_usable is True
+    with engine.connect() as connection:
+        assert connection.execute(text("""
+            SELECT status FROM edge_gateway_tasks WHERE task_id=:task_id
+        """), {"task_id": task["task_id"]}).scalar_one() == "leased"
+        assert connection.execute(text("""
+            SELECT count(*) FROM edge_gateway_audits WHERE event_type='task_outcome_recorded'
+        """)).scalar_one() == 0
+    result = service.task_outcome(
+        task["task_id"], outcome="failed", lease_token=lease["lease_token"],
+        safe_summary={"reason_code": "controlled_failure"}, **auth,
+    )
+    assert result["status"] == "failed" and result["replayed"] is False
+    replay = service.task_outcome(
+        task["task_id"], outcome="failed", lease_token=lease["lease_token"],
+        safe_summary={"reason_code": "controlled_failure"}, **auth,
+    )
+    assert replay["status"] == "failed" and replay["replayed"] is True
+    with engine.connect() as connection:
+        audits = connection.execute(text("""
+            SELECT safe_detail FROM edge_gateway_audits WHERE event_type='task_outcome_recorded'
+        """)).scalars().all()
+    assert audits == [{"task_id": task["task_id"], "outcome": "failed"}]
+
+
+def test_cross_gateway_event_replay_never_discloses_foreign_ack(service):
+    gateway_a = _register(service, _enrollment(service, gateway_name="gateway-a"))
+    gateway_b = _register(service, _enrollment(service, gateway_name="gateway-b"), certificate_sha256="b" * 64)
+    auth_a = {"credential": gateway_a["credential"], "certificate_sha256": "b" * 64, "gateway_id": gateway_a["gateway_id"], "environment": "staging", "network_zone": "manufacturing-zone-a", "generation": 1}
+    auth_b = {"credential": gateway_b["credential"], "certificate_sha256": "b" * 64, "gateway_id": gateway_b["gateway_id"], "environment": "staging", "network_zone": "manufacturing-zone-a", "generation": 1}
+    task = service.issue_task({
+        "task_id": "task-owner-a", "gateway_id": gateway_a["gateway_id"], "environment": "staging",
+        "network_zone": "manufacturing-zone-a", "purpose": "inventory", "classification": "statistics",
+        "task_type": "profile", "contract_version": 1,
+        "deadline_at": (datetime.now(UTC) + timedelta(minutes=10)).isoformat().replace("+00:00", "Z"),
+        "attempt": 1, "idempotency_key": "task-owner-a-key", "policy_digest": "a" * 64,
+    }, actor_uid=_uid())
+    pulled = service.pull_task(**auth_a)
+    event = _event(task, gateway_a)
+    ack = service.accept_event(event=event, lease_token=pulled["lease_token"], **auth_a)
+    with pytest.raises(EdgeGatewayAuthenticationError):
+        service.accept_event(event=event, lease_token=pulled["lease_token"], **auth_b)
+    assert ack["event_id"] == event["event_id"]
+
+
+def test_release_offer_is_concurrently_idempotent_and_conflicts_are_controlled(service, engine):
+    gateway = _register(service, _enrollment(service))
+    barrier = Barrier(2)
+
+    def offer_once():
+        session = Session(engine)
+        try:
+            barrier.wait(timeout=5)
+            return _service(EdgeGatewayRepository(session)).offer_release(
+                gateway["gateway_id"], version="5.0.0", artifact_digest="5" * 64,
+                rollback_version="4.9.0", request_id="concurrent-release", actor_uid=_uid(),
+            )
+        finally:
+            session.close()
+
+    with ThreadPoolExecutor(max_workers=2) as pool:
+        results = [future.result() for future in (pool.submit(offer_once), pool.submit(offer_once))]
+    assert results[0] == results[1]
+    with pytest.raises(EdgeGatewayConflictError):
+        service.offer_release(
+            gateway["gateway_id"], version="5.0.1", artifact_digest="6" * 64,
+            rollback_version="4.9.0", request_id="concurrent-release", actor_uid=_uid(),
+        )
+    with pytest.raises(EdgeGatewayConflictError):
+        service.offer_release(
+            gateway["gateway_id"], version="5.0.0", artifact_digest="5" * 64,
+            rollback_version="4.9.0", request_id="different-request", actor_uid=_uid(),
+        )
+    service.offer_release(
+        gateway["gateway_id"], version="5.1.0", artifact_digest="7" * 64,
+        rollback_version="5.0.0", request_id="release-b", actor_uid=_uid(),
+    )
+    with pytest.raises(EdgeGatewayConflictError):
+        service.offer_release(
+            gateway["gateway_id"], version="5.1.0", artifact_digest="7" * 64,
+            rollback_version="5.0.0", request_id="concurrent-release", actor_uid=_uid(),
+        )
+    assert service.list_gateways()[0]["gateway_id"] == gateway["gateway_id"]
+
+
+def test_evidence_owner_and_runtime_role_are_really_isolated(engine):
+    runtime_login = f"edge_test_runtime_{uuid.uuid4().hex[:12]}"
+    runtime_password = f"runtime-{uuid.uuid4().hex}"
+    runtime_engine = None
+    runtime_session = None
+    with engine.begin() as connection:
+        connection.execute(text(f'CREATE ROLE "{runtime_login}" LOGIN PASSWORD :password'), {
+            "password": runtime_password,
+        })
+        connection.execute(text(f'GRANT dataops_app_runtime TO "{runtime_login}"'))
+    try:
+        runtime_url = make_url(DATABASE_URL).set(
+            username=runtime_login, password=runtime_password,
+        )
+        runtime_engine = create_engine(runtime_url, pool_pre_ping=True)
+        validate_production_database_identity(
+            SimpleNamespace(config={"FLASK_ENV": "production"}), runtime_engine,
+        )
+        runtime_session = Session(runtime_engine)
+        runtime_service = _service(EdgeGatewayRepository(runtime_session))
+        enrollment = _enrollment(runtime_service)
+        gateway = _register(runtime_service, enrollment)
+        assert gateway["generation"] == 1
+        auth = {
+            "credential": gateway["credential"], "certificate_sha256": "b" * 64,
+            "gateway_id": gateway["gateway_id"], "environment": "staging",
+            "network_zone": "manufacturing-zone-a", "generation": 1,
+        }
+        assert runtime_service.heartbeat(
+            version="3.0.1", safe_summary={"queue_depth": 0}, **auth,
+        )["status"] == "online"
+        task = runtime_service.issue_task({
+            "task_id": "runtime-role-task", "gateway_id": gateway["gateway_id"],
+            "environment": "staging", "network_zone": "manufacturing-zone-a",
+            "purpose": "inventory", "classification": "statistics", "task_type": "profile",
+            "contract_version": 1,
+            "deadline_at": (datetime.now(UTC) + timedelta(minutes=10)).isoformat().replace("+00:00", "Z"),
+            "attempt": 1, "idempotency_key": "runtime-role-task-key", "policy_digest": "a" * 64,
+        }, actor_uid=_uid())
+        assert runtime_service.pull_task(**auth)["task"]["task_id"] == task["task_id"]
+        with runtime_engine.connect() as connection:
+            identity = connection.execute(text("SELECT current_user,session_user")).one()
+            assert identity == (runtime_login, runtime_login)
+            owners = dict(connection.execute(text("""
+                SELECT c.relname,r.rolname FROM pg_class c
+                JOIN pg_roles r ON r.oid=c.relowner
+                WHERE c.relname IN ('edge_gateway_audits','edge_gateway_failure_windows')
+            """)).all())
+            assert owners == {
+                "edge_gateway_audits": "dataops_edge_evidence_owner",
+                "edge_gateway_failure_windows": "dataops_edge_evidence_owner",
+            }
+            assert connection.execute(text("""
+                SELECT count(*)=0 FROM information_schema.routine_privileges
+                 WHERE routine_schema='public'
+                   AND routine_name IN ('record_edge_gateway_failure','record_edge_gateway_audit')
+                   AND grantee='PUBLIC' AND privilege_type='EXECUTE'
+            """)).scalar_one() is True
+        attacks = (
+            "INSERT INTO edge_gateway_audits(uid,event_type,success,safe_detail) VALUES (gen_random_uuid(),'authentication_rejected',false,'{}')",
+            "UPDATE edge_gateway_audits SET success=true",
+            "DELETE FROM edge_gateway_failure_windows",
+            "TRUNCATE TABLE edge_gateway_audits,edge_gateway_failure_windows",
+            "ALTER TABLE edge_gateway_audits DISABLE TRIGGER ALL",
+            "SET ROLE dataops_edge_evidence_owner",
+        )
+        for statement in attacks:
+            with pytest.raises(DBAPIError), runtime_engine.begin() as connection:
+                connection.execute(text(statement))
+        with runtime_engine.begin() as connection:
+            connection.execute(text("SELECT set_config('dataops.edge_failure_writer','477-controlled',false)"))
+            with pytest.raises(DBAPIError):
+                connection.execute(text("""
+                    INSERT INTO edge_gateway_failure_windows
+                      (failure_fingerprint,window_started_at,event_type,occurrence_count,first_seen_at,last_seen_at)
+                    VALUES (:fingerprint,CURRENT_TIMESTAMP,'authentication_rejected',1,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)
+                """), {"fingerprint": "f" * 64})
+    finally:
+        if runtime_session is not None:
+            runtime_session.close()
+        if runtime_engine is not None:
+            runtime_engine.dispose()
+        with engine.begin() as connection:
+            connection.execute(text(f'DROP ROLE IF EXISTS "{runtime_login}"'))
+
+
+def test_production_owner_database_identity_and_direct_entry_fail_closed(engine):
+    with pytest.raises(RuntimeError, match="least-privilege runtime identity"):
+        validate_production_database_identity(
+            SimpleNamespace(config={"FLASK_ENV": "production"}), engine,
+        )
+    result = subprocess.run(
+        [str(ROOT / ".venv/bin/python"), "-c", "from app import create_app; create_app()"],
+        cwd=ROOT,
+        env={
+            **os.environ,
+            "PYTHONPATH": str(ROOT),
+            "FLASK_ENV": "production",
+            "APP_ENV_FILE": "/nonexistent/dataops-test.env",
+            "DATABASE_URL": DATABASE_URL,
+            "NEO4J_URI": "bolt://neo4j.example.test:7687",
+            "NEO4J_HTTP_URI": "http://neo4j.example.test:7474",
+            "NEO4J_USER": "dataops_graph",
+            "NEO4J_PASSWORD": "graph-test-secret",
+            "MINIO_HOST": "minio.example.test:9000",
+            "MINIO_USER": "dataops_objects",
+            "MINIO_PASSWORD": "object-test-secret",
+            "MINIO_BUCKET": "dataops-test",
+        },
+        capture_output=True,
+        text=True,
+    )
+    assert result.returncode != 0
+    assert "least-privilege runtime identity" in result.stderr
+    assert "dataops-test-password" not in result.stderr
+
+
+def test_preprovisioned_no_createrole_migrator_can_apply_477_and_478(engine):
+    migrator = f"edge_test_migrator_{uuid.uuid4().hex[:12]}"
+    password = f"migration-{uuid.uuid4().hex}"
+    _clear_edge_rows_for_test(engine)
+    _alembic("downgrade", "20260802_476")
+    try:
+        with engine.begin() as connection:
+            connection.execute(text(f'CREATE ROLE "{migrator}" LOGIN NOSUPERUSER NOCREATEROLE NOCREATEDB PASSWORD :password'), {"password": password})
+            connection.execute(text(f'GRANT dataops_edge_evidence_owner TO "{migrator}"'))
+            connection.execute(text('GRANT USAGE,CREATE ON SCHEMA public TO dataops_edge_evidence_owner'))
+            connection.execute(text(f'GRANT USAGE,CREATE ON SCHEMA public TO "{migrator}"'))
+            connection.execute(text(f'GRANT SELECT,INSERT,UPDATE,DELETE ON alembic_version TO "{migrator}"'))
+        migrator_url = make_url(DATABASE_URL).set(
+            username=migrator, password=password,
+        ).render_as_string(hide_password=False)
+        try:
+            _alembic("upgrade", "head", migrator_url)
+        except subprocess.CalledProcessError as exc:
+            pytest.fail(exc.stderr)
+        with engine.connect() as connection:
+            assert connection.execute(text("SELECT version_num FROM alembic_version")).scalar_one() == "20260809_479"
+            assert connection.execute(text("SELECT NOT rolcreaterole AND NOT rolsuper FROM pg_roles WHERE rolname=:role"), {"role": migrator}).scalar_one() is True
+    finally:
+        _clear_edge_rows_for_test(engine)
+        _alembic("downgrade", "20260802_476")
+        with engine.begin() as connection:
+            connection.execute(text(f'REVOKE dataops_edge_evidence_owner FROM "{migrator}"'))
+            connection.execute(text(f'DROP OWNED BY "{migrator}"'))
+            connection.execute(text(f'DROP ROLE "{migrator}"'))
+        _alembic("upgrade", "head")
+
+
+def test_backend_runtime_configuration_never_contains_migrator_credentials():
+    compose = (ROOT / "deploy/docker/docker-compose.yml").read_text()
+    backend_dockerfile = (ROOT / "deploy/docker/backend.Dockerfile").read_text()
+    runner_dockerfile = (ROOT / "deploy/docker/runner.Dockerfile").read_text()
+    run_script = (ROOT / "scripts/run_dataops.sh").read_text()
+    assert "db-role-init:" in compose and "db-migrate:" in compose
+    assert "DATAOPS_RUNTIME_PASSWORD" in compose
+    assert "postgresql://dataops_app:" in compose
+    backend = compose.split("  backend:", 1)[1].split("  frontend:", 1)[0]
+    assert "postgresql://dataops:dataops-test-password" not in backend
+    assert "MIGRATION_DATABASE_URL" not in backend
+    assert "db-migrate:\n        condition: service_completed_successfully" in backend
+    backend_runtime = backend_dockerfile.split("FROM dependencies AS runtime", 1)[1]
+    assert "alembic.ini" not in backend_runtime and "COPY migrations/" not in backend_runtime
+    assert "pip uninstall -y alembic" in backend_runtime
+    assert "alembic.ini" not in runner_dockerfile and "COPY migrations/" not in runner_dockerfile
+    assert "pip uninstall -y alembic" in runner_dockerfile
+    assert 'role_init_url="${DB_ROLE_INIT_DATABASE_URL:' in run_script
+    assert 'migration_url="${MIGRATION_DATABASE_URL:' in run_script
+    assert "unset DB_ROLE_INIT_DATABASE_URL MIGRATION_DATABASE_URL" in run_script
+    assert "MIGRATION_DATABASE_URL=" in (ROOT / "deployment/dataops.env").read_text()
+
+
+def test_task_pull_and_exact_event_replay_are_concurrency_safe(service, engine):
+    gateway = _register(service, _enrollment(service))
+    auth = {
+        "credential": gateway["credential"], "certificate_sha256": "b" * 64,
+        "gateway_id": gateway["gateway_id"], "environment": "staging",
+        "network_zone": "manufacturing-zone-a", "generation": 1,
+    }
+    task = service.issue_task({
+        "task_id": "task-concurrent", "gateway_id": gateway["gateway_id"],
+        "environment": "staging", "network_zone": "manufacturing-zone-a",
+        "purpose": "inventory", "classification": "statistics", "task_type": "profile",
+        "contract_version": 1,
+        "deadline_at": (datetime.now(UTC) + timedelta(minutes=10)).isoformat().replace("+00:00", "Z"),
+        "attempt": 1, "idempotency_key": "task-concurrent-key", "policy_digest": "a" * 64,
+    }, actor_uid=_uid())
+    pull_barrier = Barrier(2)
+
+    def pull_once():
+        session = Session(engine)
+        try:
+            worker = _service(EdgeGatewayRepository(session))
+            pull_barrier.wait()
+            return worker.pull_task(**auth)
+        finally:
+            session.close()
+
+    with ThreadPoolExecutor(max_workers=2) as pool:
+        pulls = [future.result() for future in (pool.submit(pull_once), pool.submit(pull_once))]
+    assert sorted(result["task"] is not None for result in pulls) == [False, True]
+    lease_token = next(result["lease_token"] for result in pulls if result["task"] is not None)
+
+    event = _event(task, gateway)
+    event_barrier = Barrier(2)
+
+    def accept_once():
+        session = Session(engine)
+        try:
+            worker = _service(EdgeGatewayRepository(session))
+            event_barrier.wait()
+            return worker.accept_event(event=event, lease_token=lease_token, **auth)
+        finally:
+            session.close()
+
+    with ThreadPoolExecutor(max_workers=2) as pool:
+        acks = [future.result() for future in (pool.submit(accept_once), pool.submit(accept_once))]
+    assert acks[0] == acks[1]
+    with engine.connect() as connection:
+        assert connection.execute(text("SELECT count(*) FROM edge_gateway_events WHERE event_id=:event_id"), {"event_id": event["event_id"]}).scalar_one() == 1
+
+    assert service.accept_event(event=event, lease_token=lease_token, **auth) == acks[0]
+
+
+def test_cancel_and_event_barrier_has_single_linearized_terminal_state(service, engine):
+    gateway = _register(service, _enrollment(service))
+    auth = {"credential": gateway["credential"], "certificate_sha256": "b" * 64,
+            "gateway_id": gateway["gateway_id"], "environment": "staging",
+            "network_zone": "manufacturing-zone-a", "generation": 1}
+    task = service.issue_task({
+        "task_id": "task-race", "gateway_id": gateway["gateway_id"], "environment": "staging",
+        "network_zone": "manufacturing-zone-a", "purpose": "inventory", "classification": "statistics",
+        "task_type": "profile", "contract_version": 1,
+        "deadline_at": (datetime.now(UTC) + timedelta(minutes=10)).isoformat().replace("+00:00", "Z"),
+        "attempt": 1, "idempotency_key": "task-race-key", "policy_digest": "a" * 64,
+    }, actor_uid=_uid())
+    lease = service.pull_task(**auth)
+    event = _event(task, gateway)
+    barrier = Barrier(2)
+
+    def accept():
+        session = Session(engine)
+        try:
+            barrier.wait()
+            return _service(EdgeGatewayRepository(session)).accept_event(
+                event=event, lease_token=lease["lease_token"], **auth,
+            )
+        except EdgeGatewayError:
+            return None
+        finally:
+            session.close()
+
+    def cancel():
+        session = Session(engine)
+        try:
+            barrier.wait()
+            return _service(EdgeGatewayRepository(session)).cancel_task(task["task_id"], actor_uid=_uid())
+        finally:
+            session.close()
+
+    with ThreadPoolExecutor(max_workers=2) as pool:
+        accepted, cancelled = pool.submit(accept), pool.submit(cancel)
+        accepted, cancelled = accepted.result(), cancelled.result()
+    with engine.connect() as connection:
+        row = connection.execute(text("""
+            SELECT status,(SELECT count(*) FROM edge_gateway_events WHERE task_id=:task_id) AS events
+              FROM edge_gateway_tasks WHERE task_id=:task_id
+        """), {"task_id": task["task_id"]}).mappings().one()
+    assert (row["status"], row["events"]) in {("completed", 1), ("cancel_requested", 0)}
+    assert bool(accepted) != bool(cancelled)
+
+
+def test_cancel_event_barriers_prove_both_lock_orders(service, engine):
+    gateway = _register(service, _enrollment(service))
+    auth = {"credential": gateway["credential"], "certificate_sha256": "b" * 64,
+            "gateway_id": gateway["gateway_id"], "environment": "staging",
+            "network_zone": "manufacturing-zone-a", "generation": 1}
+
+    def issue_and_pull(task_id):
+        task = service.issue_task({
+            "task_id": task_id, "gateway_id": gateway["gateway_id"], "environment": "staging",
+            "network_zone": "manufacturing-zone-a", "purpose": "inventory", "classification": "statistics",
+            "task_type": "profile", "contract_version": 1,
+            "deadline_at": (datetime.now(UTC) + timedelta(minutes=10)).isoformat().replace("+00:00", "Z"),
+            "attempt": 1, "idempotency_key": f"{task_id}-key", "policy_digest": "a" * 64,
+        }, actor_uid=_uid())
+        return task, service.pull_task(**auth)
+
+    cancel_task, cancel_lease = issue_and_pull("task-cancel-wins")
+    cancel_has_gateway = Event()
+    cancel_barrier = Barrier(2)
+
+    class CancelFirstRepository(EdgeGatewayRepository):
+        def cancel_task(self, task_id, actor_uid):
+            self.session.execute(text("""
+                SELECT g.uid FROM edge_gateway_tasks t JOIN edge_gateways g ON g.uid=t.gateway_id
+                 WHERE t.task_id=:task_id FOR UPDATE OF g
+            """), {"task_id": task_id}).scalar_one()
+            cancel_has_gateway.set()
+            cancel_barrier.wait(timeout=5)
+            return super().cancel_task(task_id, actor_uid)
+
+    def cancel_first():
+        session = Session(engine)
+        try:
+            return _service(CancelFirstRepository(session)).cancel_task(
+                cancel_task["task_id"], actor_uid=_uid(),
+            )
+        finally:
+            session.close()
+
+    def event_after_cancel_lock():
+        assert cancel_has_gateway.wait(timeout=5)
+        cancel_barrier.wait(timeout=5)
+        session = Session(engine)
+        try:
+            return _service(EdgeGatewayRepository(session)).accept_event(
+                event=_event(cancel_task, gateway), lease_token=cancel_lease["lease_token"], **auth,
+            )
+        except EdgeGatewayError:
+            return None
+        finally:
+            session.close()
+
+    with ThreadPoolExecutor(max_workers=2) as pool:
+        cancel_result = pool.submit(cancel_first)
+        event_result = pool.submit(event_after_cancel_lock)
+        assert cancel_result.result() is True
+        assert event_result.result() is None
+
+    event_task, event_lease = issue_and_pull("task-event-wins")
+    event_has_gateway = Event()
+    event_barrier = Barrier(2)
+
+    class EventFirstRepository(EdgeGatewayRepository):
+        def authenticate(self, credential_hash, *, lock=False):
+            identity = super().authenticate(credential_hash, lock=lock)
+            if lock:
+                event_has_gateway.set()
+                event_barrier.wait(timeout=5)
+            return identity
+
+    def event_first():
+        session = Session(engine)
+        try:
+            return _service(EventFirstRepository(session)).accept_event(
+                event=_event(event_task, gateway), lease_token=event_lease["lease_token"], **auth,
+            )
+        finally:
+            session.close()
+
+    def cancel_after_event_lock():
+        assert event_has_gateway.wait(timeout=5)
+        event_barrier.wait(timeout=5)
+        session = Session(engine)
+        try:
+            return _service(EdgeGatewayRepository(session)).cancel_task(
+                event_task["task_id"], actor_uid=_uid(),
+            )
+        finally:
+            session.close()
+
+    with ThreadPoolExecutor(max_workers=2) as pool:
+        event_result = pool.submit(event_first)
+        cancel_result = pool.submit(cancel_after_event_lock)
+        assert event_result.result()["status"] == "accepted"
+        assert cancel_result.result() is False
+
+    with engine.connect() as connection:
+        rows = dict(connection.execute(text("""
+            SELECT task_id,status FROM edge_gateway_tasks
+             WHERE task_id IN ('task-cancel-wins','task-event-wins')
+        """)).all())
+    assert rows == {"task-cancel-wins": "cancel_requested", "task-event-wins": "completed"}
+
+
+def test_machine_operations_and_rotate_are_gateway_fenced_with_barrier(service, engine):
+    gateway = _register(service, _enrollment(service))
+    auth = {"credential": gateway["credential"], "certificate_sha256": "b" * 64,
+            "gateway_id": gateway["gateway_id"], "environment": "staging",
+            "network_zone": "manufacturing-zone-a", "generation": 1}
+    release = service.offer_release(
+        gateway["gateway_id"], version="4.0.0", artifact_digest="7" * 64,
+        rollback_version="3.0.0", request_id="barrier-release", actor_uid=_uid(),
+    )
+    barrier = Barrier(3)
+
+    def run_machine(kind):
+        session = Session(engine)
+        worker = _service(EdgeGatewayRepository(session))
+        try:
+            barrier.wait()
+            if kind == "reconcile":
+                return worker.reconcile(**auth)
+            return worker.acknowledge_release(
+                release_id=release["release_id"], outcome="accepted",
+                safe_summary={"reason_code": "accepted"}, **auth,
+            )
+        except EdgeGatewayError:
+            return None
+        finally:
+            session.close()
+
+    def run_rotate():
+        session = Session(engine)
+        try:
+            barrier.wait()
+            return _service(EdgeGatewayRepository(session)).rotate(
+                gateway["gateway_id"], certificate_sha256="c" * 64,
+                request_id="barrier-rotate", actor_uid=_uid(),
+            )
+        finally:
+            session.close()
+
+    with ThreadPoolExecutor(max_workers=3) as pool:
+        results = [future.result() for future in (
+            pool.submit(run_machine, "reconcile"), pool.submit(run_machine, "ack"), pool.submit(run_rotate),
+        )]
+    assert results[2]["generation"] == 2
+    with pytest.raises(EdgeGatewayAuthenticationError):
+        service.reconcile(**auth)
+
+
+def test_pull_event_and_revoke_are_gateway_fenced_with_barrier(service, engine):
+    gateway = _register(service, _enrollment(service))
+    auth = {"credential": gateway["credential"], "certificate_sha256": "b" * 64,
+            "gateway_id": gateway["gateway_id"], "environment": "staging",
+            "network_zone": "manufacturing-zone-a", "generation": 1}
+    def issue(task_id):
+        return service.issue_task({
+            "task_id": task_id, "gateway_id": gateway["gateway_id"], "environment": "staging",
+            "network_zone": "manufacturing-zone-a", "purpose": "inventory", "classification": "statistics",
+            "task_type": "profile", "contract_version": 1,
+            "deadline_at": (datetime.now(UTC) + timedelta(minutes=10)).isoformat().replace("+00:00", "Z"),
+            "attempt": 1, "idempotency_key": f"{task_id}-key", "policy_digest": "a" * 64,
+        }, actor_uid=_uid())
+    event_task = issue("task-revoke-event")
+    event_lease = service.pull_task(**auth)
+    issue("task-revoke-pull")
+    event = _event(event_task, gateway)
+    barrier = Barrier(3)
+
+    def machine(kind):
+        session = Session(engine)
+        worker = _service(EdgeGatewayRepository(session))
+        try:
+            barrier.wait()
+            if kind == "event":
+                return worker.accept_event(event=event, lease_token=event_lease["lease_token"], **auth)
+            return worker.pull_task(**auth)
+        except EdgeGatewayError:
+            return None
+        finally:
+            session.close()
+
+    def revoke():
+        session = Session(engine)
+        try:
+            barrier.wait()
+            return _service(EdgeGatewayRepository(session)).revoke(gateway["gateway_id"], actor_uid=_uid())
+        finally:
+            session.close()
+
+    with ThreadPoolExecutor(max_workers=3) as pool:
+        results = [future.result() for future in (
+            pool.submit(machine, "event"), pool.submit(machine, "pull"), pool.submit(revoke),
+        )]
+    assert results[2] is True
+    with pytest.raises(EdgeGatewayAuthenticationError):
+        service.pull_task(**auth)
+
+
+def test_failure_window_uses_lock_time_not_stale_transaction_time(engine):
+    fingerprint = hashlib.sha256(f"stale-clock:{_uid()}".encode()).hexdigest()
+    statement = text("""
+        SELECT occurrence_count
+          FROM public.record_edge_gateway_failure(
+            CAST(:audit_uid AS uuid), :fingerprint, NULL,
+            'untrusted_request_rejected', 'untrusted_failure'
+          )
+    """)
+    older = engine.connect()
+    transaction = older.begin()
+    try:
+        older.execute(text("SELECT CURRENT_TIMESTAMP,pg_sleep(0.02)"))
+        with engine.begin() as newer:
+            first = newer.execute(statement, {
+                "audit_uid": _uid(), "fingerprint": fingerprint,
+            }).scalar_one()
+        second = older.execute(statement, {
+            "audit_uid": _uid(), "fingerprint": fingerprint,
+        }).scalar_one()
+        transaction.commit()
+    finally:
+        if transaction.is_active:
+            transaction.rollback()
+        older.close()
+    assert (first, second) == (1, 2)
+
+
+def test_edge_flask_postgres_machine_binding_and_human_permissions(monkeypatch, engine):
+    monkeypatch.setenv("DATABASE_URL", DATABASE_URL)
+    actor = _uid()
+    monkeypatch.setattr(
+        "app.core.system.permissions.authenticate_request",
+        lambda: {"id": actor, "sub": actor, "roles": ["admin"]},
+    )
+    from app import create_app
+
+    app = create_app()
+    app.config.update(
+        TESTING=True,
+        EDGE_GATEWAY_SIGNING_PRIVATE_KEY=_SIGNING_PRIVATE_KEY,
+        EDGE_GATEWAY_SIGNING_KEY_ID=_SIGNING_KEY_ID,
+        EDGE_MTLS_TRUSTED_PROXY_IPS=("127.0.0.1",),
+    )
+    certificate_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
+    certificate_name = x509.Name(
+        [x509.NameAttribute(NameOID.COMMON_NAME, "plant-route-edge")]
+    )
+    now = datetime.now(UTC)
+    certificate = (
+        x509.CertificateBuilder()
+        .subject_name(certificate_name)
+        .issuer_name(certificate_name)
+        .public_key(certificate_key.public_key())
+        .serial_number(x509.random_serial_number())
+        .not_valid_before(now - timedelta(minutes=1))
+        .not_valid_after(now + timedelta(minutes=10))
+        .sign(certificate_key, hashes.SHA256())
+    )
+    certificate_pem = certificate.public_bytes(serialization.Encoding.PEM).decode()
+    certificate_sha256 = certificate.fingerprint(hashes.SHA256()).hex()
+
+    def mtls_headers(**values):
+        return {
+            "X-DataOps-Edge-Client-Cert": quote(certificate_pem, safe=""),
+            "X-DataOps-Edge-Client-Verify": "SUCCESS",
+            "X-Edge-Certificate-SHA256": certificate_sha256,
+            **values,
+        }
+
+    client = app.test_client()
+    oversized = client.post("/api/datasource/edge/enrollments", json={"padding": "x" * 270_000})
+    assert oversized.status_code == 413
+    enrollment_response = client.post("/api/datasource/edge/enrollments", json={
+        "gateway_name": "plant-route", "environment": "staging",
+        "network_zone": "zone-route", "policy_digest": "e" * 64,
+        "allowed_control_hosts": ["control.example.test"], "allowed_proxy_hosts": [],
+        "expected_certificate_sha256": certificate_sha256,
+        "ttl_seconds": 600,
+    })
+    assert enrollment_response.status_code == 201
+    assert enrollment_response.headers["Cache-Control"] == "no-store"
+    enrollment = enrollment_response.get_json()["data"]
+    missing_tls_proof = client.post("/api/datasource/edge/register", headers={
+        "X-Edge-Enrollment": enrollment["enrollment_token"],
+        "X-Edge-Certificate-SHA256": certificate_sha256,
+    }, json={
+        "gateway_id": enrollment["gateway_id"], "environment": "staging",
+        "network_zone": "zone-route", "policy_digest": "e" * 64,
+        "allowed_control_hosts": ["control.example.test"], "allowed_proxy_hosts": [],
+        "version": "3.0.0",
+    })
+    assert missing_tls_proof.status_code == 401
+    spoofed_tls_proof = client.post(
+        "/api/datasource/edge/register",
+        headers=mtls_headers(**{
+            "X-Edge-Enrollment": enrollment["enrollment_token"],
+        }),
+        environ_overrides={"REMOTE_ADDR": "203.0.113.7"},
+        json={
+            "gateway_id": enrollment["gateway_id"], "environment": "staging",
+            "network_zone": "zone-route", "policy_digest": "e" * 64,
+            "allowed_control_hosts": ["control.example.test"], "allowed_proxy_hosts": [],
+            "version": "3.0.0",
+        },
+    )
+    assert spoofed_tls_proof.status_code == 401
+    register = client.post("/api/datasource/edge/register", headers=mtls_headers(**{
+        "X-Edge-Enrollment": enrollment["enrollment_token"],
+    }), environ_overrides={"REMOTE_ADDR": "127.0.0.1"}, json={
+        "gateway_id": enrollment["gateway_id"], "environment": "staging",
+        "network_zone": "zone-route", "policy_digest": "e" * 64,
+        "allowed_control_hosts": ["control.example.test"], "allowed_proxy_hosts": [],
+        "version": "3.0.0",
+    })
+    assert register.status_code == 201
+    assert register.headers["Cache-Control"] == "no-store"
+    registered = register.get_json()["data"]
+    bad = client.post(
+        f"/api/datasource/edge/gateways/{registered['gateway_id']}/heartbeat",
+        headers=mtls_headers(**{"X-Edge-Credential": registered["credential"] + "wrong"}),
+        environ_overrides={"REMOTE_ADDR": "127.0.0.1"},
+        json={"environment": "staging", "network_zone": "zone-route", "generation": 1, "version": "3.0.0", "safe_summary": {}},
+    )
+    assert bad.status_code == 401
+    assert bad.headers["Cache-Control"] == "no-store"
+    assert registered["credential"] not in bad.get_data(as_text=True)
+    limited = None
+    for _ in range(9):
+        limited = client.post(
+            f"/api/datasource/edge/gateways/{registered['gateway_id']}/heartbeat",
+            headers=mtls_headers(**{"X-Edge-Credential": registered["credential"] + "wrong"}),
+            environ_overrides={"REMOTE_ADDR": "127.0.0.1"},
+            json={"environment": "staging", "network_zone": "zone-route", "generation": 1, "version": "3.0.0", "safe_summary": {}},
+        )
+    assert limited is not None and limited.status_code == 429
+    assert limited.headers["Cache-Control"] == "no-store"
+    with engine.connect() as connection:
+        window = connection.execute(text("SELECT occurrence_count FROM edge_gateway_failure_windows")).scalar_one()
+        failure_rows = connection.execute(text("SELECT count(*) FROM edge_gateway_audits WHERE success=false")).scalar_one()
+    assert window == 10
+    assert failure_rows == 1
+    listed = client.get("/api/datasource/edge/gateways")
+    assert listed.status_code == 200
+    assert listed.get_json()["data"]["gateways"][0]["gateway_id"] == registered["gateway_id"]
+    assert client.get("/api/datasource/edge/gateways?limit=101").status_code == 400
+    oversized_reconcile = client.post(
+        f"/api/datasource/edge/gateways/{registered['gateway_id']}/reconcile",
+        headers=mtls_headers(**{"X-Edge-Credential": registered["credential"]}),
+        environ_overrides={"REMOTE_ADDR": "127.0.0.1"},
+        json={"environment": "staging", "network_zone": "zone-route", "generation": 1, "limit": 101},
+    )
+    assert oversized_reconcile.status_code == 400
+    app.config["EDGE_GATEWAY_SIGNING_PRIVATE_KEY"] = ""
+    unavailable_signer = client.post(
+        f"/api/datasource/edge/gateways/{registered['gateway_id']}/releases",
+        json={
+            "version": "7.0.0", "artifact_digest": "7" * 64,
+            "rollback_version": "6.0.0", "request_id": "no-production-signer",
+        },
+    )
+    assert unavailable_signer.status_code == 503
+    assert unavailable_signer.get_json()["error"]["code"] == "EDGE_GATEWAY_SIGNING_UNAVAILABLE"

+ 497 - 0
tests/integration/test_phase3_wp04_edge_runtime.py

@@ -0,0 +1,497 @@
+"""Real image/container smoke for the pull-only P3-WP04 edge runtime."""
+
+from __future__ import annotations
+
+import hashlib
+import json
+import os
+import shutil
+import sqlite3
+import ssl
+import subprocess
+import threading
+import time
+from datetime import UTC, datetime, timedelta
+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+from pathlib import Path
+
+import pytest
+from cryptography import x509
+from cryptography.hazmat.primitives import hashes, serialization
+from cryptography.hazmat.primitives.asymmetric import ed25519, rsa
+from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID
+
+import app.edge_gateway.runtime as edge_runtime
+from app.core.edge_gateway.contracts import (
+    EdgeTaskContract,
+    SignedTaskEnvelope,
+    canonical_sha256,
+    canonical_timestamp,
+)
+from app.core.edge_gateway.policy import EdgePolicyError
+from app.edge_gateway.bootstrap import (
+    build_server_crl_ssl_context,
+    validate_server_crl,
+)
+
+ROOT = Path(__file__).resolve().parents[2]
+IMAGE = "dataops-edge:wp04-runtime-smoke"
+
+
+def test_runtime_runner_executes_closed_engineering_self_check_only():
+    deadline = canonical_timestamp(
+        (datetime.now(UTC) + timedelta(minutes=2)).isoformat().replace("+00:00", "Z")
+    )
+    task = EdgeTaskContract.from_mapping({
+        "task_id": "runtime-self-check-1",
+        "gateway_id": "11111111-1111-4111-8111-111111111111",
+        "environment": "production",
+        "network_zone": "zone-a",
+        "purpose": "quality-evaluation",
+        "classification": "evidence",
+        "task_type": "quality",
+        "contract_version": 1,
+        "deadline_at": deadline,
+        "attempt": 1,
+        "idempotency_key": "runtime-self-check-1",
+        "policy_digest": "b" * 64,
+    })
+    result = edge_runtime.build_runtime_runner().execute(task, lambda: False)
+    assert result == {
+        "classification": "evidence",
+        "payload": {
+            "check_count": 1,
+            "passed_count": 1,
+            "scope": "edge_runtime",
+            "status": "passed",
+        },
+        "local_artifact_digest": None,
+        "local_artifact_ref": None,
+    }
+    with pytest.raises(EdgePolicyError):
+        edge_runtime.build_runtime_runner().execute(
+            EdgeTaskContract.from_mapping({
+                **task.to_mapping(),
+                "task_type": "collect",
+                "purpose": "governed-inventory",
+                "classification": "desensitized_metadata",
+            }),
+            lambda: False,
+        )
+
+
+def test_runtime_session_rebuilds_tls_context_when_server_crl_changes(tmp_path):
+    _fingerprint, _context, ca_key, ca, _server = _materials(tmp_path)
+    ca_path = tmp_path / "certificates" / "ca.pem"
+    crl_path = tmp_path / "certificates" / "control-plane-server.crl.pem"
+    session = edge_runtime.ServerCrlSession(str(ca_path), str(crl_path))
+    initial = session.server_crl_digest
+    now = datetime.now(UTC)
+    replacement = (
+        x509.CertificateRevocationListBuilder().issuer_name(ca.subject)
+        .last_update(now - timedelta(seconds=1))
+        .next_update(now + timedelta(hours=1))
+        .sign(ca_key, hashes.SHA256())
+    )
+    _write(crl_path, replacement.public_bytes(serialization.Encoding.PEM), 0o644)
+    assert session.refresh_server_crl() is True
+    assert session.server_crl_digest != initial
+    assert session.refresh_server_crl() is False
+
+
+def _write(path: Path, value: bytes, mode: int = 0o600) -> None:
+    path.write_bytes(value)
+    path.chmod(mode)
+
+
+def _materials(root: Path):
+    now = datetime.now(UTC)
+    ca_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
+    ca_name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "wp04-runtime-ca")])
+    ca = (
+        x509.CertificateBuilder()
+        .subject_name(ca_name).issuer_name(ca_name).public_key(ca_key.public_key())
+        .serial_number(x509.random_serial_number())
+        .not_valid_before(now - timedelta(minutes=1))
+        .not_valid_after(now + timedelta(days=7))
+        .add_extension(x509.BasicConstraints(ca=True, path_length=0), critical=True)
+        .sign(ca_key, hashes.SHA256())
+    )
+
+    def issue(name: str, *, server: bool):
+        key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
+        builder = (
+            x509.CertificateBuilder()
+            .subject_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, name)]))
+            .issuer_name(ca.subject).public_key(key.public_key())
+            .serial_number(x509.random_serial_number())
+            .not_valid_before(now - timedelta(minutes=1))
+            .not_valid_after(now + timedelta(days=1))
+            .add_extension(
+                x509.ExtendedKeyUsage([
+                    ExtendedKeyUsageOID.SERVER_AUTH if server
+                    else ExtendedKeyUsageOID.CLIENT_AUTH
+                ]), critical=True,
+            )
+        )
+        if server:
+            builder = builder.add_extension(
+                x509.SubjectAlternativeName([
+                    x509.DNSName("host.docker.internal"), x509.DNSName("localhost")
+                ]), critical=False,
+            )
+        return key, builder.sign(ca_key, hashes.SHA256())
+
+    server_key, server = issue("host.docker.internal", server=True)
+    client_key, client = issue("wp04-edge-client", server=False)
+    crl = (
+        x509.CertificateRevocationListBuilder().issuer_name(ca.subject)
+        .last_update(now - timedelta(minutes=1)).next_update(now + timedelta(hours=2))
+        .sign(ca_key, hashes.SHA256())
+    )
+    certificates = root / "certificates"
+    secrets = root / "secrets"
+    certificates.mkdir()
+    secrets.mkdir()
+    _write(certificates / "ca.pem", ca.public_bytes(serialization.Encoding.PEM), 0o644)
+    _write(certificates / "client.pem", client.public_bytes(serialization.Encoding.PEM), 0o644)
+    _write(
+        certificates / "control-plane-server.crl.pem",
+        crl.public_bytes(serialization.Encoding.PEM), 0o644,
+    )
+    _write(
+        secrets / "client.key",
+        client_key.private_bytes(
+            serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8,
+            serialization.NoEncryption(),
+        ),
+    )
+    _write(
+        root / "server.pem", server.public_bytes(serialization.Encoding.PEM), 0o644
+    )
+    _write(
+        root / "server.key",
+        server_key.private_bytes(
+            serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8,
+            serialization.NoEncryption(),
+        ),
+    )
+    context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
+    context.load_cert_chain(str(root / "server.pem"), str(root / "server.key"))
+    context.load_verify_locations(str(certificates / "ca.pem"))
+    context.verify_mode = ssl.CERT_REQUIRED
+    return client.fingerprint(hashes.SHA256()).hex(), context, ca_key, ca, server
+
+
+class _ControlHandler(BaseHTTPRequestHandler):
+    paths: list[str] = []
+    accepted_events: list[dict[str, object]] = []
+    task_private_key: ed25519.Ed25519PrivateKey | None = None
+    task_sent = False
+
+    @classmethod
+    def reset(cls, private_key: ed25519.Ed25519PrivateKey) -> None:
+        cls.paths = []
+        cls.accepted_events = []
+        cls.task_private_key = private_key
+        cls.task_sent = False
+
+    def do_POST(self):  # noqa: N802
+        length = int(self.headers.get("Content-Length", "0"))
+        self.paths.append(self.path)
+        request_body = json.loads(self.rfile.read(length) or b"{}")
+        if self.path.endswith("/reconcile"):
+            data = {
+                "cancelled_task_ids": [], "cancel_next_cursor": None,
+                "release_offers": [], "release_next_cursor": None,
+                "release_baseline": None,
+            }
+        elif self.path.endswith("/tasks/pull"):
+            if self.task_sent:
+                data = {"task": None}
+            else:
+                now = datetime.now(UTC)
+                task = {
+                    "task_id": "runtime-self-check-1",
+                    "gateway_id": "11111111-1111-4111-8111-111111111111",
+                    "environment": "production",
+                    "network_zone": "zone-a",
+                    "purpose": "quality-evaluation",
+                    "classification": "evidence",
+                    "task_type": "quality",
+                    "contract_version": 1,
+                    "deadline_at": canonical_timestamp(
+                        (now + timedelta(minutes=2)).isoformat().replace("+00:00", "Z")
+                    ),
+                    "attempt": 1,
+                    "idempotency_key": "runtime-self-check-1",
+                    "policy_digest": "b" * 64,
+                }
+                unsigned = {
+                    "task": task,
+                    "authority_key_id": "task-key-1",
+                    "signature_algorithm": "Ed25519",
+                    "contract_digest": canonical_sha256(task),
+                    "gateway_id": task["gateway_id"],
+                    "environment": task["environment"],
+                    "network_zone": task["network_zone"],
+                    "policy_digest": task["policy_digest"],
+                    "purpose": task["purpose"],
+                    "issued_at": canonical_timestamp(
+                        (now - timedelta(seconds=1)).isoformat().replace("+00:00", "Z")
+                    ),
+                    "expires_at": task["deadline_at"],
+                }
+                assert self.task_private_key is not None
+                envelope = {
+                    **unsigned,
+                    "signature": self.task_private_key.sign(
+                        SignedTaskEnvelope.canonical_unsigned_bytes(unsigned)
+                    ).hex(),
+                }
+                data = {
+                    "task": task,
+                    "signed_task_envelope": envelope,
+                    "lease_token": "dopl_runtime-smoke-lease",
+                    "lease_expires_at": canonical_timestamp(
+                        (now + timedelta(seconds=90)).isoformat().replace("+00:00", "Z")
+                    ),
+                }
+                self.task_sent = True
+        elif self.path.endswith("/events"):
+            event = request_body["event"]
+            lease_token = request_body["lease_token"]
+            self.accepted_events.append(event)
+            data = {
+                "event_id": event["event_id"],
+                "event_digest": canonical_sha256(event),
+                "remote_lease_digest": hashlib.sha256(
+                    lease_token.encode("utf-8")
+                ).hexdigest(),
+                "received_at": canonical_timestamp(
+                    datetime.now(UTC).isoformat().replace("+00:00", "Z")
+                ),
+                "status": "accepted",
+            }
+        else:
+            data = {"status": "active"}
+        encoded = json.dumps({"code": 200, "message": "ok", "data": data}).encode()
+        self.send_response(200)
+        self.send_header("Content-Type", "application/json")
+        self.send_header("Content-Length", str(len(encoded)))
+        self.end_headers()
+        self.wfile.write(encoded)
+
+    def log_message(self, _format, *_args):
+        return
+
+
+def _wait_for(path: Path, status: str, timeout: float = 30) -> dict:
+    deadline = time.monotonic() + timeout
+    while time.monotonic() < deadline:
+        try:
+            value = json.loads(path.read_text())
+            if value.get("status") == status:
+                return value
+        except (OSError, json.JSONDecodeError):
+            pass
+        time.sleep(0.25)
+    raise AssertionError(f"health did not reach {status}")
+
+
+@pytest.mark.skipif(shutil.which("docker") is None, reason="Docker is required")
+def test_real_edge_image_mtls_pull_degraded_and_clean_sigterm(tmp_path):
+    fingerprint, ssl_context, ca_key, ca, server_certificate = _materials(tmp_path)
+    task_private_key = ed25519.Ed25519PrivateKey.generate()
+    _ControlHandler.reset(task_private_key)
+    server = ThreadingHTTPServer(("0.0.0.0", 0), _ControlHandler)
+    server.socket = ssl_context.wrap_socket(server.socket, server_side=True)
+    thread = threading.Thread(target=server.serve_forever, daemon=True)
+    thread.start()
+    port = server.server_address[1]
+    for name in ("config", "queue", "artifacts", "releases", "signing", "health"):
+        (tmp_path / name).mkdir()
+    credential = "dopg_runtime_smoke_credential"
+    _write(tmp_path / "secrets" / "control-credential", credential.encode())
+    signing_key = task_private_key.public_key().public_bytes(
+        serialization.Encoding.Raw, serialization.PublicFormat.Raw
+    ).hex()
+    _write(
+        tmp_path / "signing" / "task-public-keys.json",
+        json.dumps({"task-key-1": signing_key}).encode(), 0o644,
+    )
+    _write(
+        tmp_path / "signing" / "release-public-keys.json",
+        json.dumps({"release-key-1": signing_key}).encode(), 0o644,
+    )
+    config = {
+        "allowed_control_hosts": ["host.docker.internal"],
+        "allowed_control_origins": [f"https://host.docker.internal:{port}"],
+        "allowed_proxy_hosts": [],
+        "allowed_proxy_origins": [],
+        "artifact_root": "/var/lib/dataops-edge/artifacts",
+        "ca_bundle_path": "/run/edge/certificates/ca.pem",
+        "certificate_sha256": fingerprint,
+        "client_certificate_path": "/run/edge/certificates/client.pem",
+        "client_private_key_path": "/run/edge/secrets/client.key",
+        "control_url": f"https://host.docker.internal:{port}",
+        "environment": "production",
+        "gateway_id": "11111111-1111-4111-8111-111111111111",
+        "generation": 1, "network_zone": "zone-a",
+        "policy_digest": "b" * 64, "proxy_url": None,
+        "queue_path": "/var/lib/dataops-edge/queue/edge-queue-v2.sqlite3",
+        "task_authority_clock_skew_seconds": 30, "version": "1.0.0",
+    }
+    _write(tmp_path / "config" / "edge.json", json.dumps(config).encode(), 0o644)
+    build = subprocess.run(
+        ["docker", "build", "-f", "deploy/edge/Dockerfile.edge", "-t", IMAGE, "."],
+        cwd=ROOT, capture_output=True, text=True, check=False,
+    )
+    assert build.returncode == 0, build.stderr[-4000:]
+    command = [
+        "docker", "run", "-d", "--add-host", "host.docker.internal:host-gateway",
+        "--user", f"{os.getuid()}:{os.getgid()}",
+    ]
+    for host, container, mode in (
+        ("config", "/run/edge/config", "ro"), ("queue", "/var/lib/dataops-edge/queue", "rw"),
+        ("artifacts", "/var/lib/dataops-edge/artifacts", "rw"),
+        ("releases", "/var/lib/dataops-edge/releases", "rw"),
+        ("secrets", "/run/edge/secrets", "ro"),
+        ("certificates", "/run/edge/certificates", "ro"),
+        ("signing", "/run/edge/signing", "ro"), ("health", "/run/edge/tmp", "rw"),
+    ):
+        command.extend(["-v", f"{tmp_path / host}:{container}:{mode}"])
+    command.extend([
+        "-e", "EDGE_CONFIG_PATH=/run/edge/config/edge.json",
+        "-e", "EDGE_CREDENTIAL_FILE=/run/edge/secrets/control-credential",
+        "-e", "EDGE_CLIENT_CERTIFICATE_PATH=/run/edge/certificates/client.pem",
+        "-e", "EDGE_CLIENT_PRIVATE_KEY_PATH=/run/edge/secrets/client.key",
+        "-e", "EDGE_CA_BUNDLE_PATH=/run/edge/certificates/ca.pem",
+        "-e", "EDGE_SERVER_CRL_PATH=/run/edge/certificates/control-plane-server.crl.pem",
+        "-e", "EDGE_TASK_SIGNING_KEYS_PATH=/run/edge/signing/task-public-keys.json",
+        "-e", "EDGE_RELEASE_SIGNING_KEYS_PATH=/run/edge/signing/release-public-keys.json",
+        "-e", "EDGE_HEALTH_PATH=/run/edge/tmp/health.json",
+        "-e", "EDGE_POLL_INTERVAL_SECONDS=1", IMAGE,
+    ])
+    started = subprocess.run(command, capture_output=True, text=True, check=False)
+    assert started.returncode == 0, started.stderr
+    container_id = started.stdout.strip()
+    health_path = tmp_path / "health" / "health.json"
+    try:
+        healthy = _wait_for(health_path, "healthy", 40)
+        assert credential not in json.dumps(healthy)
+        assert any(path.endswith("/heartbeat") for path in _ControlHandler.paths)
+        assert any(path.endswith("/reconcile") for path in _ControlHandler.paths)
+        assert any(path.endswith("/tasks/pull") for path in _ControlHandler.paths)
+        deadline = time.monotonic() + 15
+        while not _ControlHandler.accepted_events and time.monotonic() < deadline:
+            time.sleep(0.25)
+        if len(_ControlHandler.accepted_events) != 1:
+            queue_path = tmp_path / "queue" / "edge-queue-v2.sqlite3"
+            with sqlite3.connect(queue_path) as connection:
+                event_rows = connection.execute(
+                    "SELECT status,attempt_count,error_code,available_at,lease_expires_at,event_json "
+                    "FROM edge_outbound_events"
+                ).fetchall()
+            diagnostic = subprocess.run(
+                ["docker", "logs", container_id],
+                capture_output=True,
+                text=True,
+                check=False,
+            )
+            probe = subprocess.run(
+                [
+                    "docker", "exec", container_id, "python", "-c",
+                    "import json,sqlite3;"
+                    "from app.edge_gateway.runtime import build_runtime_from_environment;"
+                    "a,_,_=build_runtime_from_environment();"
+                    "c=sqlite3.connect(a.queue.db_path);"
+                    "e=json.loads(c.execute('select event_json from edge_outbound_events limit 1').fetchone()[0]);"
+                    "t=a.queue.recover_remote_lease('runtime-self-check-1');"
+                    "print('lease',len(t),__import__('hashlib').sha256(t.encode()).hexdigest() == __import__('hashlib').sha256(b'dopl_runtime-smoke-lease').hexdigest());"
+                    "\ntry: a.transport.send_event(e,t)"
+                    "\nexcept Exception as x: print(type(x).__name__,str(x),type(x.__cause__).__name__ if x.__cause__ else '',repr(x.__cause__))",
+                ],
+                capture_output=True,
+                text=True,
+                check=False,
+            )
+            raise AssertionError(
+                f"event chain incomplete paths={_ControlHandler.paths!r} "
+                f"health={health_path.read_text()!r} events={event_rows!r} logs="
+                f"{(diagnostic.stdout + diagnostic.stderr)[-4000:]!r} "
+                f"probe={(probe.stdout + probe.stderr)[-4000:]!r}"
+            )
+        assert _ControlHandler.accepted_events[0]["classification"] == "evidence"
+        assert _ControlHandler.accepted_events[0]["payload"] == {
+            "check_count": 1,
+            "passed_count": 1,
+            "scope": "edge_runtime",
+            "status": "passed",
+        }
+        inspect = subprocess.run(
+            ["docker", "inspect", container_id], capture_output=True, text=True, check=True
+        )
+        record = json.loads(inspect.stdout)[0]
+        assert not record["HostConfig"]["PortBindings"]
+        now = datetime.now(UTC)
+        revoked = (
+            x509.RevokedCertificateBuilder()
+            .serial_number(server_certificate.serial_number)
+            .revocation_date(now - timedelta(seconds=1))
+            .build()
+        )
+        replacement_crl = (
+            x509.CertificateRevocationListBuilder().issuer_name(ca.subject)
+            .last_update(now - timedelta(seconds=1))
+            .next_update(now + timedelta(hours=1))
+            .add_revoked_certificate(revoked)
+            .sign(ca_key, hashes.SHA256())
+        )
+        _write(
+            tmp_path / "certificates" / "control-plane-server.crl.pem",
+            replacement_crl.public_bytes(serialization.Encoding.PEM),
+            0o644,
+        )
+        degraded = _wait_for(health_path, "degraded", 15)
+        assert credential not in json.dumps(degraded)
+        running = subprocess.run(
+            ["docker", "inspect", "-f", "{{.State.Running}}", container_id],
+            capture_output=True, text=True, check=True,
+        )
+        assert running.stdout.strip() == "true"
+        logs = subprocess.run(
+            ["docker", "logs", container_id], capture_output=True, text=True, check=True
+        )
+        assert credential not in logs.stdout + logs.stderr
+        stopped = subprocess.run(
+            ["docker", "stop", "--time", "5", container_id],
+            capture_output=True, text=True, check=False,
+        )
+        assert stopped.returncode == 0, stopped.stderr
+        exit_code = subprocess.run(
+            ["docker", "inspect", "-f", "{{.State.ExitCode}}", container_id],
+            capture_output=True, text=True, check=True,
+        )
+        assert exit_code.stdout.strip() == "0"
+    finally:
+        server.shutdown()
+        server.server_close()
+        subprocess.run(["docker", "rm", "-f", container_id], capture_output=True)
+
+
+def test_server_crl_context_checks_leaf_and_rejects_stale_crl(tmp_path):
+    _fingerprint, _context, ca_key, ca, _server = _materials(tmp_path)
+    ca_path = tmp_path / "certificates" / "ca.pem"
+    crl_path = tmp_path / "certificates" / "control-plane-server.crl.pem"
+    context = build_server_crl_ssl_context(str(ca_path), str(crl_path))
+    assert context.verify_flags & ssl.VERIFY_CRL_CHECK_LEAF
+    now = datetime.now(UTC)
+    stale = (
+        x509.CertificateRevocationListBuilder().issuer_name(ca.subject)
+        .last_update(now - timedelta(days=2)).next_update(now - timedelta(days=1))
+        .sign(ca_key, hashes.SHA256())
+    )
+    _write(crl_path, stale.public_bytes(serialization.Encoding.PEM), 0o644)
+    with pytest.raises(ValueError, match="not fresh"):
+        validate_server_crl(str(ca_path), str(crl_path))

+ 291 - 0
tests/integration/test_phase3_wp04_nginx_mtls.py

@@ -0,0 +1,291 @@
+"""Actual Nginx mTLS handshake test using only temporary certificates."""
+
+from __future__ import annotations
+
+import ipaddress
+import shutil
+import socket
+import subprocess
+import time
+import uuid
+from datetime import UTC, datetime, timedelta
+from pathlib import Path
+
+import pytest
+from cryptography import x509
+from cryptography.hazmat.primitives import hashes, serialization
+from cryptography.hazmat.primitives.asymmetric import rsa
+from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID
+
+ROOT = Path(__file__).resolve().parents[2]
+
+
+def _certificate(
+    common_name: str,
+    *,
+    issuer_certificate=None,
+    issuer_key=None,
+    client: bool = False,
+):
+    key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
+    name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, common_name)])
+    issuer_certificate = issuer_certificate or None
+    issuer_key = issuer_key or key
+    now = datetime.now(UTC)
+    builder = (
+        x509.CertificateBuilder()
+        .subject_name(name)
+        .issuer_name(issuer_certificate.subject if issuer_certificate else name)
+        .public_key(key.public_key())
+        .serial_number(x509.random_serial_number())
+        .not_valid_before(now - timedelta(minutes=1))
+        .not_valid_after(now + timedelta(minutes=10))
+        .add_extension(x509.BasicConstraints(ca=issuer_certificate is None, path_length=None), critical=True)
+    )
+    if issuer_certificate is not None:
+        usage = ExtendedKeyUsageOID.CLIENT_AUTH if client else ExtendedKeyUsageOID.SERVER_AUTH
+        builder = builder.add_extension(x509.ExtendedKeyUsage([usage]), critical=False)
+        if not client:
+            builder = builder.add_extension(
+                x509.SubjectAlternativeName(
+                    [
+                        x509.DNSName("localhost"),
+                        x509.IPAddress(ipaddress.ip_address("127.0.0.1")),
+                    ]
+                ),
+                critical=False,
+            )
+    return key, builder.sign(issuer_key, hashes.SHA256())
+
+
+def _write_material(path: Path, name: str, key, certificate):
+    key_path = path / f"{name}.key"
+    certificate_path = path / f"{name}.crt"
+    key_path.write_bytes(
+        key.private_bytes(
+            serialization.Encoding.PEM,
+            serialization.PrivateFormat.PKCS8,
+            serialization.NoEncryption(),
+        )
+    )
+    certificate_path.write_bytes(certificate.public_bytes(serialization.Encoding.PEM))
+    return certificate_path, key_path
+
+
+@pytest.mark.integration
+def test_actual_nginx_rejects_missing_wrong_and_forged_client_certificate(tmp_path):
+    if shutil.which("docker") is None or shutil.which("curl") is None:
+        pytest.skip("Docker and curl are required for the actual Nginx mTLS test")
+    image = subprocess.run(
+        ["docker", "image", "inspect", "nginx:alpine"],
+        capture_output=True,
+        check=False,
+    )
+    if image.returncode != 0:
+        pytest.skip("the local nginx:alpine image is required; this test never pulls")
+
+    ca_key, ca_certificate = _certificate("DataOps test client CA")
+    server_key, server_certificate = _certificate(
+        "localhost", issuer_certificate=ca_certificate, issuer_key=ca_key
+    )
+    client_key, client_certificate = _certificate(
+        "gateway-valid",
+        issuer_certificate=ca_certificate,
+        issuer_key=ca_key,
+        client=True,
+    )
+    revoked_key, revoked_certificate = _certificate(
+        "gateway-revoked",
+        issuer_certificate=ca_certificate,
+        issuer_key=ca_key,
+        client=True,
+    )
+    wrong_ca_key, wrong_ca_certificate = _certificate("Wrong client CA")
+    wrong_key, wrong_certificate = _certificate(
+        "gateway-wrong",
+        issuer_certificate=wrong_ca_certificate,
+        issuer_key=wrong_ca_key,
+        client=True,
+    )
+    ca_path, _ = _write_material(tmp_path, "client-ca", ca_key, ca_certificate)
+    server_path, server_key_path = _write_material(
+        tmp_path, "server", server_key, server_certificate
+    )
+    client_path, client_key_path = _write_material(
+        tmp_path, "client", client_key, client_certificate
+    )
+    wrong_path, wrong_key_path = _write_material(
+        tmp_path, "wrong", wrong_key, wrong_certificate
+    )
+    revoked_path, revoked_key_path = _write_material(
+        tmp_path, "revoked", revoked_key, revoked_certificate
+    )
+    now = datetime.now(UTC)
+    revoked_entry = (
+        x509.RevokedCertificateBuilder()
+        .serial_number(revoked_certificate.serial_number)
+        .revocation_date(now - timedelta(seconds=30))
+        .build()
+    )
+    crl = (
+        x509.CertificateRevocationListBuilder()
+        .issuer_name(ca_certificate.subject)
+        .last_update(now - timedelta(minutes=1))
+        .next_update(now + timedelta(minutes=10))
+        .add_revoked_certificate(revoked_entry)
+        .sign(private_key=ca_key, algorithm=hashes.SHA256())
+    )
+
+    production = (ROOT / "deploy/docker/edge-mtls-nginx.conf").read_text()
+    production = production.replace("http://backend:5500", "http://127.0.0.1:8080")
+    upstream = """
+server {
+    listen 8080;
+    location / {
+        add_header X-Seen-Verify $http_x_dataops_edge_client_verify always;
+        add_header X-Seen-Cert $http_x_dataops_edge_client_cert always;
+        return 204;
+    }
+}
+"""
+    (tmp_path / "nginx.conf").write_text(production + upstream)
+    (tmp_path / "server.crt").write_bytes(server_path.read_bytes())
+    (tmp_path / "server.key").write_bytes(server_key_path.read_bytes())
+    (tmp_path / "client-ca.pem").write_bytes(ca_path.read_bytes())
+    (tmp_path / "client-ca.crl").write_bytes(
+        crl.public_bytes(serialization.Encoding.PEM)
+    )
+
+    valid_crl = (tmp_path / "client-ca.crl").read_bytes()
+    for invalid_crl in (None, b"not-a-crl"):
+        if invalid_crl is None:
+            (tmp_path / "client-ca.crl").unlink()
+        else:
+            (tmp_path / "client-ca.crl").write_bytes(invalid_crl)
+        invalid = subprocess.run(
+            [
+                "docker", "run", "--rm",
+                "--volume", f"{tmp_path / 'nginx.conf'}:/etc/nginx/conf.d/default.conf:ro",
+                "--volume", f"{tmp_path}:/etc/nginx/edge-mtls:ro",
+                "nginx:alpine", "nginx", "-t",
+            ],
+            capture_output=True,
+            check=False,
+        )
+        assert invalid.returncode != 0
+        (tmp_path / "client-ca.crl").write_bytes(valid_crl)
+
+    expired_crl = (
+        x509.CertificateRevocationListBuilder()
+        .issuer_name(ca_certificate.subject)
+        .last_update(now - timedelta(minutes=10))
+        .next_update(now - timedelta(minutes=1))
+        .sign(private_key=ca_key, algorithm=hashes.SHA256())
+    )
+    (tmp_path / "client-ca.crl").write_bytes(
+        expired_crl.public_bytes(serialization.Encoding.PEM)
+    )
+    with socket.socket() as expired_probe:
+        expired_probe.bind(("127.0.0.1", 0))
+        expired_port = expired_probe.getsockname()[1]
+    expired_name = f"dataops-edge-expired-crl-{uuid.uuid4().hex[:10]}"
+    subprocess.run(
+        [
+            "docker", "run", "--detach", "--rm", "--name", expired_name,
+            "--publish", f"127.0.0.1:{expired_port}:8443",
+            "--volume", f"{tmp_path / 'nginx.conf'}:/etc/nginx/conf.d/default.conf:ro",
+            "--volume", f"{tmp_path}:/etc/nginx/edge-mtls:ro",
+            "nginx:alpine",
+        ],
+        check=True,
+        capture_output=True,
+    )
+    try:
+        time.sleep(0.2)
+        expired = subprocess.run(
+            [
+                "curl", "--silent", "--show-error", "--max-time", "3",
+                "--cacert", str(ca_path), "--cert", str(client_path),
+                "--key", str(client_key_path), "--output", "/dev/null",
+                "--write-out", "%{http_code}",
+                f"https://127.0.0.1:{expired_port}/api/datasource/edge/test",
+            ],
+            capture_output=True,
+            text=True,
+            check=False,
+        )
+        assert expired.returncode != 0 or expired.stdout in {"400", "495", "496"}
+    finally:
+        subprocess.run(
+            ["docker", "stop", expired_name], check=False, capture_output=True
+        )
+    (tmp_path / "client-ca.crl").write_bytes(valid_crl)
+
+    with socket.socket() as probe:
+        probe.bind(("127.0.0.1", 0))
+        port = probe.getsockname()[1]
+    name = f"dataops-edge-mtls-{uuid.uuid4().hex[:12]}"
+    subprocess.run(
+        [
+            "docker", "run", "--detach", "--rm", "--name", name,
+            "--publish", f"127.0.0.1:{port}:8443",
+            "--volume", f"{tmp_path / 'nginx.conf'}:/etc/nginx/conf.d/default.conf:ro",
+            "--volume", f"{tmp_path}:/etc/nginx/edge-mtls:ro",
+            "nginx:alpine",
+        ],
+        check=True,
+        capture_output=True,
+    )
+    try:
+        for _ in range(40):
+            try:
+                with socket.create_connection(("127.0.0.1", port), timeout=0.2):
+                    break
+            except OSError:
+                time.sleep(0.05)
+        else:
+            raise AssertionError("temporary Nginx did not start")
+
+        endpoint = f"https://127.0.0.1:{port}/api/datasource/edge/test"
+        for certificate_path, key_path in (
+            (None, None),
+            (wrong_path, wrong_key_path),
+            (revoked_path, revoked_key_path),
+        ):
+            command = [
+                "curl", "--silent", "--show-error", "--max-time", "3",
+                "--cacert", str(ca_path), "--output", "/dev/null",
+                "--write-out", "%{http_code}", endpoint,
+            ]
+            if certificate_path is not None:
+                command[1:1] = [
+                    "--cert", str(certificate_path), "--key", str(key_path)
+                ]
+            rejected = subprocess.run(command, capture_output=True, text=True, check=False)
+            assert rejected.returncode != 0 or rejected.stdout in {"400", "495", "496"}
+
+        valid = subprocess.run(
+            [
+                "curl", "--silent", "--show-error", "--max-time", "3",
+                "--cacert", str(ca_path), "--cert", str(client_path),
+                "--key", str(client_key_path), "--dump-header", "-",
+                "--output", "/dev/null",
+                "--header", "X-DataOps-Edge-Client-Verify: FORGED",
+                "--header", "X-DataOps-Edge-Client-Cert: forged",
+                endpoint,
+            ],
+            capture_output=True,
+            text=True,
+            check=False,
+        )
+        assert valid.returncode == 0, valid.stderr
+        assert "HTTP/1.1 204" in valid.stdout
+        assert "X-Seen-Verify: SUCCESS" in valid.stdout
+        assert "X-Seen-Cert: -----BEGIN%20CERTIFICATE-----" in valid.stdout
+        assert "X-Seen-Cert: forged" not in valid.stdout
+    finally:
+        subprocess.run(
+            ["docker", "stop", name],
+            check=False,
+            capture_output=True,
+        )

+ 561 - 0
tests/security/test_phase3_wp04_data_boundary.py

@@ -0,0 +1,561 @@
+"""Security boundary tests for the P3-WP04 pull-only edge runtime."""
+
+from __future__ import annotations
+
+import json
+from datetime import UTC, datetime, timedelta
+from pathlib import Path
+from urllib.parse import quote
+
+import pytest
+from cryptography import x509
+from cryptography.hazmat.primitives import hashes, serialization
+from cryptography.hazmat.primitives.asymmetric import rsa
+from cryptography.x509.oid import NameOID
+from flask import Flask
+
+from app.api.data_source.edge_routes import _tls_certificate_sha256
+from app.core.edge_gateway.contracts import EdgeContractError, EdgeTaskContract
+from app.core.edge_gateway.policy import EdgePolicyError
+from app.core.edge_gateway.service import EdgeGatewayAuthenticationError
+from app.edge_gateway.agent import EdgeRunnerAdapter
+from app.edge_gateway.bootstrap import EdgeBootstrapConfig
+from app.edge_gateway.transport import (
+    EdgeAuthenticationStopped,
+    EdgeTransport,
+    EdgeTransportError,
+)
+from app.runner.api import execute_edge_adapter
+from app.runner.nodes import NodeExecutionError
+
+POLICY_DIGEST = "a" * 64
+
+
+def _escaped_client_certificate():
+    key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
+    name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "edge-gateway-test")])
+    now = datetime.now(UTC)
+    certificate = (
+        x509.CertificateBuilder()
+        .subject_name(name)
+        .issuer_name(name)
+        .public_key(key.public_key())
+        .serial_number(x509.random_serial_number())
+        .not_valid_before(now - timedelta(minutes=1))
+        .not_valid_after(now + timedelta(minutes=5))
+        .sign(key, hashes.SHA256())
+    )
+    pem = certificate.public_bytes(serialization.Encoding.PEM).decode()
+    return quote(pem, safe=""), certificate.fingerprint(hashes.SHA256()).hex()
+
+
+def test_backend_accepts_certificate_only_from_explicit_trusted_mtls_proxy():
+    app = Flask(__name__)
+    app.config["EDGE_MTLS_TRUSTED_PROXY_IPS"] = ("172.31.0.10",)
+    escaped, fingerprint = _escaped_client_certificate()
+    headers = {
+        "X-DataOps-Edge-Client-Cert": escaped,
+        "X-DataOps-Edge-Client-Verify": "SUCCESS",
+        "X-Edge-Certificate-SHA256": fingerprint,
+    }
+    with app.test_request_context(
+        "/", headers=headers, environ_overrides={"REMOTE_ADDR": "172.31.0.10"}
+    ):
+        assert _tls_certificate_sha256() == fingerprint
+    with app.test_request_context(
+        "/", headers=headers, environ_overrides={"REMOTE_ADDR": "203.0.113.7"}
+    ), pytest.raises(EdgeGatewayAuthenticationError, match="trusted mTLS proxy"):
+        _tls_certificate_sha256()
+    with app.test_request_context(
+        "/",
+        headers={**headers, "X-DataOps-Edge-Client-Cert": "forged"},
+        environ_overrides={"REMOTE_ADDR": "172.31.0.10"},
+    ), pytest.raises(EdgeGatewayAuthenticationError, match="certificate"):
+        _tls_certificate_sha256()
+
+
+def task(**changes):
+    value = {
+        "task_id": "task-1",
+        "gateway_id": "gateway-1",
+        "environment": "production",
+        "network_zone": "zone-a",
+        "purpose": "governed-inventory",
+        "classification": "statistics",
+        "task_type": "profile",
+        "contract_version": 1,
+        "deadline_at": (datetime.now(UTC) + timedelta(minutes=5)).isoformat().replace("+00:00", "Z"),
+        "attempt": 1,
+        "idempotency_key": "idem-1",
+        "policy_digest": POLICY_DIGEST,
+    }
+    value.update(changes)
+    return value
+
+
+class NeverNetwork:
+    def request(self, *args, **kwargs):  # pragma: no cover - must not run
+        raise AssertionError("network request must not be attempted")
+
+
+@pytest.mark.parametrize(
+    "url,proxy",
+    [
+        ("http://control.enterprise.test", None),
+        ("https://user@control.enterprise.test", None),
+        ("https://control.enterprise.test.evil.test", None),
+        ("https://control.enterprise.test", "https://evil.test"),
+    ],
+)
+def test_transport_rejects_unapproved_destinations_before_network(url, proxy):
+    with pytest.raises(EdgePolicyError):
+        EdgeTransport(
+            base_url=url,
+            proxy_url=proxy,
+            allowed_control_hosts={"control.enterprise.test"},
+            allowed_proxy_hosts={"proxy.enterprise.test"},
+            gateway_id="gateway-1",
+            environment="production",
+            network_zone="zone-a",
+            generation=1,
+            credential="dopg_example-credential",
+            certificate_sha256="b" * 64,
+            client=NeverNetwork(),
+            client_certificate_path="/secure/edge-cert.pem",
+            client_private_key_path="/secure/edge-key.pem",
+            ca_bundle_path="/secure/ca.pem",
+        )
+
+
+@pytest.mark.parametrize(
+    "operation",
+    ["shell", "python", "sql", "raw_export", "http", "../../bin/sh"],
+)
+def test_runner_adapter_rejects_unknown_or_malicious_task_types(operation):
+    adapter = EdgeRunnerAdapter(lambda _node, _request, _cancel: {})
+    value = task(task_type=operation)
+    with pytest.raises((EdgeContractError, EdgePolicyError)):
+        adapter.execute(EdgeTaskContract.from_mapping(value), lambda: False)
+
+
+@pytest.mark.parametrize(
+    "result",
+    [
+        {"classification": "raw", "payload": {"count": 1}},
+        {"classification": "statistics", "payload": {"sql": "SELECT * FROM secret"}},
+        {"classification": "statistics", "payload": {"password": "hidden"}},
+        {"classification": "statistics", "payload": {"rows": [{"id": 1}]}},
+        {"classification": "statistics", "payload": {"count": 1}, "command": "sh"},
+    ],
+)
+def test_runner_adapter_rejects_raw_secret_script_and_unknown_result_fields(result):
+    adapter = EdgeRunnerAdapter(lambda _node, _request, _cancel: result)
+    with pytest.raises(EdgePolicyError):
+        adapter.execute(EdgeTaskContract.from_mapping(task()), lambda: False)
+
+
+def test_runner_adapter_rejects_recursive_and_cyclic_results():
+    cyclic = {}
+    cyclic["child"] = cyclic
+    adapter = EdgeRunnerAdapter(
+        lambda _node, _request, _cancel: {
+            "classification": "statistics",
+            "payload": cyclic,
+        }
+    )
+    with pytest.raises(EdgePolicyError):
+        adapter.execute(EdgeTaskContract.from_mapping(task()), lambda: False)
+
+
+def test_runner_adapter_rejects_independently_oversized_result():
+    adapter = EdgeRunnerAdapter(
+        lambda _node, _request, _cancel: {
+            "classification": "statistics",
+            "payload": {"summary": "x" * 33_000},
+        }
+    )
+    with pytest.raises(EdgePolicyError):
+        adapter.execute(EdgeTaskContract.from_mapping(task()), lambda: False)
+
+
+def test_runner_adapter_rejects_unallowlisted_purpose_before_local_handler():
+    called = False
+
+    def execute(_node, _request, _cancel):
+        nonlocal called
+        called = True
+        return {"classification": "statistics", "payload": {"metric_count": 1}}
+
+    adapter = EdgeRunnerAdapter(execute)
+    with pytest.raises(EdgePolicyError, match="purpose"):
+        adapter.execute(
+            EdgeTaskContract.from_mapping(task(purpose="arbitrary-action")),
+            lambda: False,
+        )
+    assert not called
+
+
+class Response:
+    def __init__(self, status_code, content):
+        self.status_code = status_code
+        self.headers = {"Content-Length": str(len(content))}
+        self._content = content
+
+    def iter_content(self, chunk_size):
+        yield self._content
+
+
+class RecordingClient:
+    def __init__(self, response):
+        self.response = response
+        self.calls = []
+
+    def request(self, *args, **kwargs):
+        self.calls.append((args, kwargs))
+        return self.response
+
+
+class SequencedClient:
+    def __init__(self, responses):
+        self.responses = list(responses)
+        self.calls = []
+
+    def request(self, *args, **kwargs):
+        self.calls.append((args, kwargs))
+        return self.responses.pop(0)
+
+
+def _transport(client):
+    return EdgeTransport(
+        base_url="https://control.enterprise.test",
+        allowed_control_hosts={"control.enterprise.test"},
+        gateway_id="gateway-1",
+        environment="production",
+        network_zone="zone-a",
+        generation=1,
+        credential="dopg_example-credential",
+        certificate_sha256="b" * 64,
+        client=client,
+        client_certificate_path="/secure/edge-cert.pem",
+        client_private_key_path="/secure/edge-key.pem",
+        ca_bundle_path="/secure/ca.pem",
+    )
+
+
+def test_transport_uses_exact_bound_headers_and_forbids_redirect_following():
+    client = RecordingClient(Response(200, b'{"code":200,"message":"ok","data":{"task":null}}'))
+    assert _transport(client).pull_task() is None
+    args, kwargs = client.calls[0]
+    assert args == (
+        "POST",
+        "https://control.enterprise.test/api/datasource/edge/gateways/gateway-1/tasks/pull",
+    )
+    assert kwargs["allow_redirects"] is False
+    assert kwargs["headers"] == {
+        "Accept": "application/json",
+        "Content-Type": "application/json",
+        "X-Edge-Credential": "dopg_example-credential",
+        "X-Edge-Certificate-SHA256": "b" * 64,
+    }
+
+
+@pytest.mark.parametrize("status", [301, 302, 307, 308])
+def test_transport_rejects_all_redirects(status):
+    with pytest.raises(EdgeTransportError, match="redirects"):
+        _transport(RecordingClient(Response(status, b""))).pull_task()
+
+
+@pytest.mark.parametrize("status", [401, 403])
+def test_transport_stops_on_revoked_or_expired_binding(status):
+    with pytest.raises(EdgeAuthenticationStopped):
+        _transport(RecordingClient(Response(status, b'{}'))).reconcile()
+
+
+def test_transport_rejects_oversized_response_before_json_decode():
+    with pytest.raises(EdgeTransportError, match="byte limit"):
+        _transport(
+            RecordingClient(Response(200, b"x" * (EdgeTransport.MAX_RESPONSE_BYTES + 1)))
+        ).reconcile()
+
+
+def test_transport_cancel_probe_uses_bounded_reconcile_contract():
+    client = RecordingClient(
+            Response(
+                200,
+                b'{"code":200,"message":"ok","data":{"cancelled_task_ids":["task-1"],"cancel_next_cursor":null,"release_offers":[],"release_next_cursor":null,"release_baseline":null}}',
+            )
+    )
+    assert _transport(client).cancel_requested("task-1") is True
+
+
+def test_transport_reconcile_drains_all_stable_cursor_pages():
+    first_ids = [f"task-{index:02d}" for index in range(50)]
+    first = {
+        "code": 200,
+        "message": "ok",
+        "data": {
+            "cancelled_task_ids": first_ids,
+            "cancel_next_cursor": first_ids[-1],
+            "release_offers": [],
+            "release_next_cursor": None,
+            "release_baseline": None,
+        },
+    }
+    second = {
+        "code": 200,
+        "message": "ok",
+        "data": {
+            "cancelled_task_ids": ["task-50"],
+            "cancel_next_cursor": None,
+            "release_offers": [],
+            "release_next_cursor": None,
+            "release_baseline": None,
+        },
+    }
+    client = SequencedClient(
+        [
+            Response(200, json.dumps(first, separators=(",", ":")).encode()),
+            Response(200, json.dumps(second, separators=(",", ":")).encode()),
+        ]
+    )
+    reconciled = _transport(client).reconcile()
+    assert reconciled == {
+        "cancelled_task_ids": [*first_ids, "task-50"],
+        "release_offers": [],
+    }
+    second_payload = json.loads(client.calls[1][1]["data"])
+    assert second_payload["cancel_cursor"] == first_ids[-1]
+
+
+def _mtls_files(tmp_path: Path):
+    key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
+    name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "edge-gateway-1")])
+    now = datetime.now(UTC)
+    cert = (
+        x509.CertificateBuilder()
+        .subject_name(name)
+        .issuer_name(name)
+        .public_key(key.public_key())
+        .serial_number(x509.random_serial_number())
+        .not_valid_before(now - timedelta(minutes=1))
+        .not_valid_after(now + timedelta(days=1))
+        .sign(key, hashes.SHA256())
+    )
+    cert_path = tmp_path / "edge-cert.pem"
+    key_path = tmp_path / "edge-key.pem"
+    ca_path = tmp_path / "enterprise-ca.pem"
+    cert_pem = cert.public_bytes(serialization.Encoding.PEM)
+    cert_path.write_bytes(cert_pem)
+    ca_path.write_bytes(cert_pem)
+    key_path.write_bytes(
+        key.private_bytes(
+            serialization.Encoding.PEM,
+            serialization.PrivateFormat.PKCS8,
+            serialization.NoEncryption(),
+        )
+    )
+    key_path.chmod(0o600)
+    return cert_path, key_path, ca_path, cert.fingerprint(hashes.SHA256()).hex()
+
+
+def test_bootstrap_requires_real_mtls_material_and_matching_fingerprint(tmp_path):
+    cert, key, ca, fingerprint = _mtls_files(tmp_path)
+    value = {
+        "queue_path": str(tmp_path / "edge.sqlite3"),
+        "artifact_root": str(tmp_path),
+        "gateway_id": "gateway-1",
+        "credential": "dopg_local-test-credential",
+        "certificate_sha256": fingerprint,
+        "client_certificate_path": str(cert),
+        "client_private_key_path": str(key),
+        "ca_bundle_path": str(ca),
+        "generation": 1,
+        "environment": "production",
+        "network_zone": "zone-a",
+        "policy_digest": "a" * 64,
+        "control_url": "https://control.enterprise.test",
+        "proxy_url": None,
+        "allowed_control_hosts": ["control.enterprise.test"],
+        "allowed_proxy_hosts": [],
+        "trusted_release_keys": {"release-key-1": "01" * 32},
+        "trusted_task_keys": {"task-key-1": "02" * 32},
+        "task_authority_clock_skew_seconds": 60,
+        "version": "3.0.0",
+    }
+    config = EdgeBootstrapConfig.from_mapping(value)
+    assert config.certificate_sha256 == fingerprint
+    with pytest.raises(ValueError, match="fingerprint"):
+        EdgeBootstrapConfig.from_mapping({**value, "certificate_sha256": "f" * 64})
+
+
+@pytest.mark.parametrize(
+    "url,host",
+    [
+        ("https://control.enterprise.test:444", "control.enterprise.test"),
+        ("https://127.0.0.1", "127.0.0.1"),
+        ("https://éxample.test", "éxample.test"),
+    ],
+)
+def test_transport_rejects_unapproved_port_ip_literal_and_unicode_host(url, host):
+    with pytest.raises((ValueError, EdgePolicyError)):
+        EdgeTransport(
+            base_url=url,
+            allowed_control_hosts={host},
+            gateway_id="gateway-1",
+            environment="production",
+            network_zone="zone-a",
+            generation=1,
+            credential="dopg_example-credential",
+            certificate_sha256="b" * 64,
+            client=NeverNetwork(),
+            client_certificate_path="/secure/edge-cert.pem",
+            client_private_key_path="/secure/edge-key.pem",
+            ca_bundle_path="/secure/ca.pem",
+        )
+
+
+class StreamingResponse:
+    def __init__(self, chunks, *, content_length=None):
+        self.status_code = 200
+        self.headers = {}
+        if content_length is not None:
+            self.headers["Content-Length"] = str(content_length)
+        self._chunks = chunks
+
+    @property
+    def content(self):  # pragma: no cover - accessing this is the bug
+        raise AssertionError("streaming transport must not buffer response.content")
+
+    def iter_content(self, chunk_size):
+        assert chunk_size <= 65_536
+        yield from self._chunks
+
+
+class CloseTrackingResponse(StreamingResponse):
+    def __init__(self, chunks, *, status_code=200, content_length=None, read_error=None):
+        super().__init__(chunks, content_length=content_length)
+        self.status_code = status_code
+        self.read_error = read_error
+        self.close_count = 0
+
+    def iter_content(self, chunk_size):
+        if self.read_error is not None:
+            raise self.read_error
+        yield from super().iter_content(chunk_size)
+
+    def close(self):
+        self.close_count += 1
+
+
+@pytest.mark.parametrize(
+    "response,expected_exception",
+    [
+        (
+            CloseTrackingResponse(
+                [b'{"code":200,"message":"ok","data":{"task":null}}']
+            ),
+            None,
+        ),
+        (CloseTrackingResponse([], status_code=302), EdgeTransportError),
+        (CloseTrackingResponse([b"{}"], status_code=401), EdgeAuthenticationStopped),
+        (CloseTrackingResponse([b"{}"], status_code=500), EdgeTransportError),
+        (
+            CloseTrackingResponse([], content_length=EdgeTransport.MAX_RESPONSE_BYTES + 1),
+            EdgeTransportError,
+        ),
+        (
+            CloseTrackingResponse(
+                [b"x" * (EdgeTransport.MAX_RESPONSE_BYTES + 1)], content_length=1
+            ),
+            EdgeTransportError,
+        ),
+        (
+            CloseTrackingResponse([], read_error=TimeoutError("slow stream")),
+            EdgeTransportError,
+        ),
+    ],
+)
+def test_transport_closes_streamed_response_exactly_once_on_every_exit(
+    response, expected_exception
+):
+    transport = _transport(RecordingClient(response))
+    if expected_exception is None:
+        assert transport.pull_task() is None
+    else:
+        with pytest.raises(expected_exception):
+            transport.pull_task()
+    assert response.close_count == 1
+
+
+def test_transport_streams_with_mtls_and_rejects_deceptive_chunk_overflow():
+    body = b'{"code":200,"message":"ok","data":{"task":null}}'
+    client = RecordingClient(StreamingResponse([body], content_length=len(body)))
+    transport = EdgeTransport(
+        base_url="https://control.enterprise.test",
+        allowed_control_hosts={"control.enterprise.test"},
+        gateway_id="gateway-1",
+        environment="production",
+        network_zone="zone-a",
+        generation=1,
+        credential="dopg_example-credential",
+        certificate_sha256="b" * 64,
+        client=client,
+        client_certificate_path="/secure/edge-cert.pem",
+        client_private_key_path="/secure/edge-key.pem",
+        ca_bundle_path="/secure/ca.pem",
+    )
+    assert transport.pull_task() is None
+    _, kwargs = client.calls[0]
+    assert kwargs["stream"] is True
+    assert kwargs["cert"] == ("/secure/edge-cert.pem", "/secure/edge-key.pem")
+    assert kwargs["verify"] == "/secure/ca.pem"
+    assert kwargs["timeout"] == (5, 20)
+
+    overflow = RecordingClient(
+        StreamingResponse(
+            [b"x" * 700_000, b"y" * 700_000],
+            content_length=1,
+        )
+    )
+    bad = EdgeTransport(
+        base_url="https://control.enterprise.test",
+        allowed_control_hosts={"control.enterprise.test"},
+        gateway_id="gateway-1",
+        environment="production",
+        network_zone="zone-a",
+        generation=1,
+        credential="dopg_example-credential",
+        certificate_sha256="b" * 64,
+        client=overflow,
+        client_certificate_path="/secure/edge-cert.pem",
+        client_private_key_path="/secure/edge-key.pem",
+        ca_bundle_path="/secure/ca.pem",
+    )
+    with pytest.raises(EdgeTransportError, match="byte limit"):
+        bad.pull_task()
+
+
+def test_runner_bridge_has_exact_local_only_schema():
+    request = {
+        "task_id": "task-1",
+        "operation": "profile",
+        "purpose": "inventory",
+        "classification": "statistics",
+        "environment": "production",
+        "network_zone": "zone-a",
+        "idempotency_key": "idem-1",
+        "deadline_at": "2030-01-01T00:00:00Z",
+    }
+    handlers = {"edge.profile": lambda value, _cancel: {"seen": sorted(value)}}
+    assert execute_edge_adapter(handlers, "edge.profile", request, lambda: False) == {
+        "seen": sorted(request)
+    }
+    with pytest.raises(NodeExecutionError):
+        execute_edge_adapter(handlers, "python", request, lambda: False)
+    with pytest.raises(NodeExecutionError):
+        execute_edge_adapter(
+            handlers,
+            "edge.profile",
+            {**request, "sql": "SELECT 1"},
+            lambda: False,
+        )

+ 18 - 0
tests/test_permission_matrix.py

@@ -41,6 +41,24 @@ def test_fixed_role_permission_matrix_is_monotonic():
     assert "governance-audit:read" not in editor
     assert "governance-audit:read" in admin
     assert "governance-audit:seal" in admin
+    assert "edge-gateways:read" in viewer
+    assert "edge-gateways:operate" in editor
+    assert "edge-gateways:manage" not in editor
+    assert "edge-gateways:manage" in admin
+
+
+def test_edge_gateway_machine_routes_are_public_but_human_routes_are_governed():
+    from app.core.system.permissions import PUBLIC, permission_for_request
+
+    gateway = "11111111-1111-1111-1111-111111111111"
+    assert permission_for_request("/api/datasource/edge/register", "POST") == (PUBLIC,)
+    for suffix in ("heartbeat", "tasks/pull", "events", "releases/ack", "reconcile", "tasks/task-1/outcome"):
+        assert permission_for_request(f"/api/datasource/edge/gateways/{gateway}/{suffix}", "POST") == (PUBLIC,)
+    assert permission_for_request("/api/datasource/edge/enrollments", "POST") == ("edge-gateways:manage",)
+    assert permission_for_request("/api/datasource/edge/gateways", "GET") == ("edge-gateways:read",)
+    assert permission_for_request("/api/datasource/edge/tasks", "POST") == ("edge-gateways:operate",)
+    assert permission_for_request("/api/datasource/edge/tasks/task-1/cancel", "POST") == ("edge-gateways:operate",)
+    assert permission_for_request(f"/api/datasource/edge/gateways/{gateway}/rotate", "POST") == ("edge-gateways:manage",)
 
 
 def test_data_development_paths_have_specific_write_policies():

+ 1165 - 0
tests/test_phase3_wp04_delivery_contract.py

@@ -0,0 +1,1165 @@
+"""Delivery contracts for the P3-WP04 enterprise edge engineering baseline."""
+
+from __future__ import annotations
+
+import fcntl
+import hashlib
+import importlib.util
+import json
+import os
+import shutil
+import stat
+import subprocess
+import sys
+import tarfile
+from datetime import UTC, datetime, timedelta
+from pathlib import Path
+from types import SimpleNamespace
+
+import jsonschema
+import pytest
+import yaml
+from cryptography import x509
+from cryptography.hazmat.primitives import hashes, serialization
+from cryptography.hazmat.primitives.asymmetric import ed25519, rsa
+from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID
+
+from app.core.edge_gateway.contracts import canonical_json_bytes, canonical_sha256
+
+ROOT = Path(__file__).resolve().parents[1]
+CLI = ROOT / "scripts" / "edge_gateway_admin.py"
+COMPOSE = ROOT / "deploy" / "edge" / "docker-compose.edge.yml"
+ENV_EXAMPLE = ROOT / "deploy" / "edge" / "edge.env.example"
+OPENAPI = ROOT / "docs" / "architecture" / "OPENAPI.yaml"
+FAILURES = ROOT / "docs" / "validation" / "P3_WP04_FAILURE_INJECTION.json"
+EDGE_CONFIG_EXAMPLE = ROOT / "deploy" / "edge" / "edge.config.example.json"
+EDGE_CONFIG_SCHEMA = ROOT / "deploy" / "edge" / "edge.config.schema.json"
+RELEASE_SECRET_SCANNER = ROOT / "deployment" / "scripts" / "scan_release_secrets.py"
+
+
+def _load_cli_module():
+    spec = importlib.util.spec_from_file_location(
+        f"edge_gateway_admin_test_{os.getpid()}_{id(object())}", CLI
+    )
+    assert spec is not None and spec.loader is not None
+    module = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(module)
+    return module
+
+
+def _run(*args: str, expect: int = 0) -> tuple[dict[str, object], str]:
+    completed = subprocess.run(
+        [sys.executable, str(CLI), *args],
+        cwd=ROOT,
+        capture_output=True,
+        text=True,
+        check=False,
+        env={**os.environ, "PYTHONPATH": str(ROOT)},
+    )
+    assert completed.returncode == expect, completed.stderr
+    payload = json.loads(completed.stdout)
+    return payload, completed.stderr
+
+
+def _init(root: Path) -> dict[str, object]:
+    return _run(
+        "init", "--root", str(root), "--runtime-uid", str(os.getuid()),
+        "--runtime-gid", str(os.getgid()),
+    )[0]
+
+
+def _certificate_for(
+    key_path: Path, certificate_path: Path, ca_path: Path, crl_path: Path
+) -> str:
+    key = serialization.load_pem_private_key(key_path.read_bytes(), password=None)
+    ca_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
+    ca_name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "edge-test-ca")])
+    now = datetime.now(UTC)
+    ca = (
+        x509.CertificateBuilder()
+        .subject_name(ca_name)
+        .issuer_name(ca_name)
+        .public_key(ca_key.public_key())
+        .serial_number(x509.random_serial_number())
+        .not_valid_before(now - timedelta(minutes=1))
+        .not_valid_after(now + timedelta(days=365))
+        .add_extension(x509.BasicConstraints(ca=True, path_length=0), critical=True)
+        .sign(ca_key, hashes.SHA256())
+    )
+    name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "edge-test")])
+    certificate = (
+        x509.CertificateBuilder()
+        .subject_name(name)
+        .issuer_name(ca.subject)
+        .public_key(key.public_key())
+        .serial_number(x509.random_serial_number())
+        .not_valid_before(now - timedelta(minutes=1))
+        .not_valid_after(now + timedelta(days=30))
+        .add_extension(
+            x509.ExtendedKeyUsage([ExtendedKeyUsageOID.CLIENT_AUTH]), critical=True
+        )
+        .sign(ca_key, hashes.SHA256())
+    )
+    certificate_path.write_bytes(certificate.public_bytes(serialization.Encoding.PEM))
+    ca_path.write_bytes(ca.public_bytes(serialization.Encoding.PEM))
+    crl = (
+        x509.CertificateRevocationListBuilder()
+        .issuer_name(ca.subject)
+        .last_update(now - timedelta(minutes=1))
+        .next_update(now + timedelta(days=7))
+        .sign(ca_key, hashes.SHA256())
+    )
+    crl_path.write_bytes(crl.public_bytes(serialization.Encoding.PEM))
+    return certificate.fingerprint(hashes.SHA256()).hex()
+
+
+def _release_manifest(
+    private: ed25519.Ed25519PrivateKey,
+    *,
+    release_id: str,
+    version: str,
+    rollback_version: str,
+    artifact: Path,
+    status: str = "installed",
+) -> dict[str, object]:
+    unsigned = {
+        "artifact_digest": hashlib.sha256(artifact.read_bytes()).hexdigest(),
+        "artifact_name": artifact.name,
+        "deadline_at": (datetime.now(UTC) + timedelta(hours=1)).isoformat().replace("+00:00", "Z"),
+        "key_id": "release-key-1",
+        "release_id": release_id,
+        "rollback_version": rollback_version,
+        "signature_algorithm": "Ed25519",
+        "status": status,
+        "version": version,
+    }
+    return {
+        **unsigned,
+        "manifest_digest": canonical_sha256(unsigned),
+        "signature": private.sign(canonical_json_bytes(unsigned)).hex(),
+    }
+
+
+def _verify_release(
+    root: Path, manifest: dict[str, object], artifact: Path, public_hex: str
+) -> dict[str, object]:
+    source = root / "releases" / f"source-{manifest['release_id']}.json"
+    source.write_text(json.dumps(manifest))
+    return _run(
+        "release-check", "--root", str(root), "--manifest", str(source),
+        "--artifact", str(artifact), "--trusted-key", f"release-key-1={public_hex}",
+        "--current-version", str(manifest["rollback_version"]),
+    )[0]
+
+
+def _signed_status(
+    private: ed25519.Ed25519PrivateKey,
+    *,
+    gateway_id: str = "gw-test",
+    generation: int = 1,
+    certificate_sha256: str = "b" * 64,
+    status: str = "revoked",
+    expires_delta: timedelta = timedelta(hours=1),
+) -> dict[str, object]:
+    unsigned = {
+        "certificate_sha256": certificate_sha256,
+        "expires_at": (datetime.now(UTC) + expires_delta).isoformat().replace("+00:00", "Z"),
+        "gateway_id": gateway_id,
+        "generation": generation,
+        "issued_at": (datetime.now(UTC) - timedelta(seconds=1)).isoformat().replace("+00:00", "Z"),
+        "key_id": "status-key-1",
+        "signature_algorithm": "Ed25519",
+        "status": status,
+    }
+    return {
+        **unsigned,
+        "envelope_digest": canonical_sha256(unsigned),
+        "signature": private.sign(canonical_json_bytes(unsigned)).hex(),
+    }
+
+
+def test_admin_init_csr_and_registration_material_never_expose_private_key(tmp_path):
+    root = tmp_path / "edge-root"
+    initialized = _init(root)
+    init_stderr = ""
+    assert initialized["status"] == "initialized"
+    assert stat.S_IMODE(root.stat().st_mode) == 0o700
+    assert {"queue", "artifacts", "releases", "secrets", "certificates"} <= {
+        path.name for path in root.iterdir()
+    }
+
+    created, csr_stderr = _run(
+        "csr", "--root", str(root), "--common-name", "edge-test"
+    )
+    key_path = Path(str(created["private_key_path"]))
+    csr_path = Path(str(created["csr_path"]))
+    assert key_path.is_file() and csr_path.is_file()
+    assert stat.S_IMODE(key_path.stat().st_mode) == 0o600
+    assert x509.load_pem_x509_csr(csr_path.read_bytes()).is_signature_valid
+    certificate_path = root / "certificates" / "client.pem"
+    ca_path = root / "certificates" / "client-ca.pem"
+    crl_path = root / "certificates" / "client-ca.crl.pem"
+    digest = _certificate_for(key_path, certificate_path, ca_path, crl_path)
+
+    registered, register_stderr = _run(
+        "register-manifest",
+        "--root", str(root),
+        "--certificate", str(certificate_path),
+        "--ca-bundle", str(ca_path),
+        "--crl", str(crl_path),
+        "--gateway-id", "gw-test",
+        "--environment", "production",
+        "--network-zone", "zone-a",
+        "--policy-digest", "a" * 64,
+        "--control-origin", "https://control.example.test",
+    )
+    manifest_path = root / "registration" / "registration.json"
+    assert registered["certificate_sha256"] == digest
+    registration = json.loads(manifest_path.read_text())
+    assert registration["certificate_sha256"] == digest
+    combined = "\n".join(
+        [json.dumps(initialized), json.dumps(created), json.dumps(registered),
+         init_stderr, csr_stderr, register_stderr, manifest_path.read_text()]
+    )
+    assert "BEGIN PRIVATE KEY" not in combined
+    assert key_path.read_text() not in combined
+    assert "credential" not in registration
+    assert not any("path" in key for key in registration)
+    assert registration["ca_chain_verified"] is True
+    assert registration["crl_verified"] is True
+    assert registered["replayed"] is False
+    replayed, _ = _run(
+        "register-manifest", "--root", str(root),
+        "--certificate", str(certificate_path), "--ca-bundle", str(ca_path),
+        "--crl", str(crl_path), "--gateway-id", "gw-test",
+        "--environment", "production", "--network-zone", "zone-a",
+        "--policy-digest", "a" * 64,
+        "--control-origin", "https://control.example.test",
+    )
+    assert replayed["replayed"] is True
+    conflict, _ = _run(
+        "register-manifest", "--root", str(root),
+        "--certificate", str(certificate_path), "--ca-bundle", str(ca_path),
+        "--crl", str(crl_path), "--gateway-id", "gw-other",
+        "--environment", "production", "--network-zone", "zone-a",
+        "--policy-digest", "a" * 64,
+        "--control-origin", "https://control.example.test", expect=3,
+    )
+    assert conflict["error"]["code"] == "IDEMPOTENCY_CONFLICT"
+
+
+def test_init_rejects_broad_or_nonempty_roots_without_mutation(tmp_path):
+    spec = importlib.util.spec_from_file_location("edge_gateway_admin_root_test", CLI)
+    assert spec is not None and spec.loader is not None
+    admin = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(admin)
+    protected = [Path("/"), Path.home(), Path("/opt"), Path("/var"), Path("/usr"),
+                 Path("/etc"), Path("/tmp")]
+    snapshots = {
+        path: (path.stat().st_mode, path.stat().st_uid, path.stat().st_gid)
+        for path in protected if path.exists()
+    }
+    for path in snapshots:
+        with pytest.raises(admin.AdminError, match="PATH_POLICY"):
+            admin._root(str(path), create=False)
+    assert snapshots == {
+        path: (path.stat().st_mode, path.stat().st_uid, path.stat().st_gid)
+        for path in snapshots
+    }
+
+    nonempty = tmp_path / "existing-nonempty"
+    nonempty.mkdir()
+    sentinel = nonempty / "do-not-touch.txt"
+    sentinel.write_text("preserve")
+    rejected, _ = _run(
+        "init", "--root", str(nonempty), "--runtime-uid", str(os.getuid()),
+        "--runtime-gid", str(os.getgid()), expect=2,
+    )
+    assert rejected["error"]["code"] == "PATH_POLICY"
+    assert sentinel.read_text() == "preserve"
+    assert set(nonempty.iterdir()) == {sentinel}
+
+
+def test_mutating_commands_share_fail_closed_process_lock(tmp_path):
+    root = tmp_path / "edge-root"
+    _init(root)
+    lock_path = root / "state" / "admin.lock"
+    descriptor = os.open(
+        lock_path,
+        os.O_RDWR | os.O_CREAT | getattr(os, "O_NOFOLLOW", 0),
+        0o600,
+    )
+    try:
+        fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
+        blocked, _ = _run(
+            "csr", "--root", str(root), "--common-name", "edge-test", expect=3,
+        )
+        assert blocked["error"]["code"] == "CONCURRENT_OPERATION"
+        assert not (root / "secrets" / "client.key").exists()
+    finally:
+        os.close(descriptor)
+    status_payload, _ = _run("status", "--root", str(root))
+    assert status_payload["status"] == "read_only"
+    assert status_payload["active_leases"] == 0
+    rendered = json.dumps(status_payload)
+    assert str(root) not in rendered
+    assert "credential" not in rendered.casefold()
+
+
+@pytest.mark.parametrize(
+    "version",
+    ["1.0", "1.0.0-alpha", "1.0.0+build", "01.0.0", "2147483648.0.0"],
+)
+def test_release_rejects_noncanonical_or_unbounded_versions(tmp_path, version):
+    root = tmp_path / "edge-root"
+    _init(root)
+    artifact = root / "releases" / "edge.bin"
+    artifact.write_bytes(b"version policy")
+    key = ed25519.Ed25519PrivateKey.generate()
+    public_hex = key.public_key().public_bytes(
+        serialization.Encoding.Raw, serialization.PublicFormat.Raw
+    ).hex()
+    manifest = _release_manifest(
+        key, release_id="version-policy", version=version,
+        rollback_version="0.9.0", artifact=artifact, status="offered",
+    )
+    source = root / "releases" / "version-policy.json"
+    source.write_text(json.dumps(manifest))
+    rejected, _ = _run(
+        "release-check", "--root", str(root), "--manifest", str(source),
+        "--artifact", str(artifact), "--trusted-key", f"release-key-1={public_hex}",
+        "--current-version", "0.9.0", expect=3,
+    )
+    assert rejected["error"]["code"] == "RELEASE_REJECTED"
+    assert not list((root / "releases" / "verified").glob(".bundle-*"))
+
+
+@pytest.mark.parametrize(
+    "version", ["1.0", "1.0.0-alpha", "1.0.0+build", "1.2.3.4", "01.0.0", "2147483648.0.0"],
+)
+def test_control_plane_release_version_matches_edge_cli_policy(version):
+    from app.core.edge_gateway.service import (
+        EdgeGatewayValidationError,
+        _release_version,
+    )
+
+    with pytest.raises(EdgeGatewayValidationError):
+        _release_version(version, "version")
+
+
+def test_admin_atomic_outputs_inherit_runtime_parent_owner(monkeypatch, tmp_path):
+    module = _load_cli_module()
+    parent = tmp_path / "runtime-owned"
+    parent.mkdir(mode=0o700)
+    directory_chowns: list[tuple[Path, int, int]] = []
+    descriptor_chowns: list[tuple[int, int, int]] = []
+    monkeypatch.setattr(module.os, "geteuid", lambda: 0)
+    monkeypatch.setattr(
+        module.os, "chown",
+        lambda path, uid, gid, **_kwargs: directory_chowns.append((Path(path), uid, gid)),
+    )
+    monkeypatch.setattr(
+        module.os, "fchown",
+        lambda fd, uid, gid: descriptor_chowns.append((fd, uid, gid)),
+    )
+    child = module._mkdir(parent, "state")
+    expected = (parent.stat().st_uid, parent.stat().st_gid)
+    assert directory_chowns == [(child, *expected)]
+    monkeypatch.setattr(
+        module.os, "fstat", lambda _fd: SimpleNamespace(st_uid=-1, st_gid=-1)
+    )
+    module._inherit_parent_owner(99, parent)
+    assert any((uid, gid) == expected for _fd, uid, gid in descriptor_chowns)
+
+
+def test_release_check_rejects_hardlinked_artifact(tmp_path):
+    root = tmp_path / "edge-root"
+    _init(root)
+    original = root / "releases" / "original.bin"
+    original.write_bytes(b"hardlink must fail")
+    artifact = root / "releases" / "edge.bin"
+    os.link(original, artifact)
+    key = ed25519.Ed25519PrivateKey.generate()
+    public_hex = key.public_key().public_bytes(
+        serialization.Encoding.Raw, serialization.PublicFormat.Raw
+    ).hex()
+    manifest = _release_manifest(
+        key, release_id="hardlink-release", version="1.0.0",
+        rollback_version="0.9.0", artifact=artifact, status="offered",
+    )
+    source = root / "releases" / "hardlink.json"
+    source.write_text(json.dumps(manifest))
+    rejected, _ = _run(
+        "release-check", "--root", str(root), "--manifest", str(source),
+        "--artifact", str(artifact), "--trusted-key", f"release-key-1={public_hex}",
+        "--current-version", "0.9.0", expect=2,
+    )
+    assert rejected["error"]["code"] == "PATH_POLICY"
+
+
+def test_admin_requires_absolute_root_and_rejects_symlink_escape(tmp_path):
+    payload, _ = _run(
+        "init", "--root", "relative-edge-root", "--runtime-uid", str(os.getuid()),
+        "--runtime-gid", str(os.getgid()), expect=2,
+    )
+    assert payload == {"error": {"code": "INVALID_INPUT"}, "status": "error"}
+    root = tmp_path / "root"
+    outside = tmp_path / "outside"
+    outside.mkdir()
+    root.mkdir()
+    (root / "secrets").symlink_to(outside, target_is_directory=True)
+    payload, _ = _run(
+        "init", "--root", str(root), "--runtime-uid", str(os.getuid()),
+        "--runtime-gid", str(os.getgid()), expect=2,
+    )
+    assert payload["error"]["code"] == "PATH_POLICY"
+    mismatched = tmp_path / "owner-mismatch"
+    owner_error, _ = _run(
+        "init", "--root", str(mismatched),
+        "--runtime-uid", str(os.getuid() + 1),
+        "--runtime-gid", str(os.getgid()), expect=2,
+    )
+    assert owner_error["error"]["code"] == "OWNER_POLICY"
+
+
+def test_rotate_revoke_and_rollback_are_idempotent_and_fail_closed(
+    tmp_path, monkeypatch
+):
+    root = tmp_path / "edge-root"
+    _init(root)
+    rotated, _ = _run(
+        "rotate", "--root", str(root), "--common-name", "edge-test",
+        "--request-id", "rotate-1",
+        "--expected-current-certificate-digest", "b" * 64,
+    )
+    assert rotated["status"] == "pending_server_approval"
+    assert Path(str(rotated["private_key_path"])).stat().st_mode & 0o077 == 0
+    replayed, _ = _run(
+        "rotate", "--root", str(root), "--common-name", "edge-test",
+        "--request-id", "rotate-1",
+        "--expected-current-certificate-digest", "b" * 64,
+    )
+    assert replayed == rotated
+    conflict, _ = _run(
+        "rotate", "--root", str(root), "--common-name", "edge-other",
+        "--request-id", "rotate-1",
+        "--expected-current-certificate-digest", "b" * 64, expect=3,
+    )
+    assert conflict["error"]["code"] == "IDEMPOTENCY_CONFLICT"
+
+    status_key = ed25519.Ed25519PrivateKey.generate()
+    status_public = status_key.public_key().public_bytes(
+        serialization.Encoding.Raw, serialization.PublicFormat.Raw
+    ).hex()
+    signed_status = _signed_status(status_key)
+    status_path = root / "state" / "server-revocation.json"
+    status_path.write_text(json.dumps(signed_status))
+    revoked, _ = _run(
+        "revoke-status", "--root", str(root), "--server-status", str(status_path),
+        "--trusted-key", f"status-key-1={status_public}",
+        "--expected-gateway-id", "gw-test", "--expected-generation", "1",
+        "--expected-certificate-digest", "b" * 64,
+    )
+    assert revoked["local_stop_required"] is True
+    assert (root / "state" / "REVOKED_STOP").is_file()
+
+    forged = _signed_status(status_key, gateway_id="gw-other")
+    status_path.write_text(json.dumps(forged))
+    rejected, _ = _run(
+        "revoke-status", "--root", str(root), "--server-status", str(status_path),
+        "--trusted-key", f"status-key-1={status_public}",
+        "--expected-gateway-id", "gw-test", "--expected-generation", "1",
+        "--expected-certificate-digest", "b" * 64, expect=3,
+    )
+    assert rejected["error"]["code"] == "STATUS_REJECTED"
+
+    expired = _signed_status(status_key, expires_delta=timedelta(seconds=-1))
+    status_path.write_text(json.dumps(expired))
+    expired_result, _ = _run(
+        "revoke-status", "--root", str(root), "--server-status", str(status_path),
+        "--trusted-key", f"status-key-1={status_public}",
+        "--expected-gateway-id", "gw-test", "--expected-generation", "1",
+        "--expected-certificate-digest", "b" * 64, expect=3,
+    )
+    assert expired_result["error"]["code"] == "STATUS_REJECTED"
+
+    bad_signature = _signed_status(status_key)
+    bad_signature["signature"] = "0" * 128
+    status_path.write_text(json.dumps(bad_signature))
+    forged_result, _ = _run(
+        "revoke-status", "--root", str(root), "--server-status", str(status_path),
+        "--trusted-key", f"status-key-1={status_public}",
+        "--expected-gateway-id", "gw-test", "--expected-generation", "1",
+        "--expected-certificate-digest", "b" * 64, expect=3,
+    )
+    assert forged_result["error"]["code"] == "STATUS_REJECTED"
+
+    active = _signed_status(status_key, status="active")
+    status_path.write_text(json.dumps(active))
+    terminal, _ = _run(
+        "revoke-status", "--root", str(root), "--server-status", str(status_path),
+        "--trusted-key", f"status-key-1={status_public}",
+        "--expected-gateway-id", "gw-test", "--expected-generation", "1",
+        "--expected-certificate-digest", "b" * 64,
+    )
+    assert terminal["status"] == "revoked"
+    assert terminal["local_stop_required"] is True
+
+    release_key = ed25519.Ed25519PrivateKey.generate()
+    public_hex = release_key.public_key().public_bytes(
+        serialization.Encoding.Raw, serialization.PublicFormat.Raw
+    ).hex()
+    target_artifact = root / "releases" / "edge-1.0.0.tar"
+    target_artifact.write_bytes(b"prior verified release")
+    target = _release_manifest(
+        release_key, release_id="prior-release", version="1.0.0",
+        rollback_version="0.9.0", artifact=target_artifact,
+    )
+    target_checked = _verify_release(root, target, target_artifact, public_hex)
+    current_artifact = root / "releases" / "edge-2.0.0.tar"
+    current_artifact.write_bytes(b"current verified release")
+    current = _release_manifest(
+        release_key, release_id="current-release", version="2.0.0",
+        rollback_version="1.0.0", artifact=current_artifact,
+    )
+    current_checked = _verify_release(root, current, current_artifact, public_hex)
+    current_bundle = json.loads(
+        Path(str(current_checked["verified_bundle_path"]), "bundle.json").read_text()
+    )
+    pointer = {
+        "artifact_name": current["artifact_name"],
+        "bundle_digest": current_bundle["bundle_digest"],
+        "manifest_digest": current["manifest_digest"],
+        "schema_version": 1,
+        "version": current["version"],
+    }
+    (root / "releases" / "current.json").write_text(json.dumps(pointer))
+    spec = importlib.util.spec_from_file_location("edge_gateway_admin_fsync_test", CLI)
+    assert spec is not None and spec.loader is not None
+    admin = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(admin)
+    real_fsync_directory = admin._fsync_directory
+    failed = False
+
+    def fail_after_pointer_replace(path):
+        nonlocal failed
+        if Path(path) == root / "releases" and not failed:
+            failed = True
+            raise OSError("injected directory fsync failure")
+        return real_fsync_directory(path)
+
+    monkeypatch.setattr(admin, "_fsync_directory", fail_after_pointer_replace)
+    with pytest.raises(OSError, match="injected"):
+        admin.command_rollback(SimpleNamespace(
+            root=str(root), yes=True,
+            expected_current_digest=str(current["manifest_digest"]),
+            target_digest=str(target_checked["manifest_digest"]),
+            trusted_key=[f"release-key-1={public_hex}"],
+        ))
+    assert (root / "state" / "rollback-intent.json").is_file()
+    rolled_back, _ = _run(
+        "rollback", "--root", str(root), "--yes",
+        "--expected-current-digest", str(current["manifest_digest"]),
+        "--target-digest", str(target_checked["manifest_digest"]),
+        "--trusted-key", f"release-key-1={public_hex}",
+    )
+    assert rolled_back["status"] == "rolled_back"
+    assert rolled_back["replayed"] is True
+    assert rolled_back["version"] == "1.0.0"
+    committed = json.loads((root / "releases" / "current.json").read_text())
+    assert committed["manifest_digest"] == target["manifest_digest"]
+    replayed_rollback, _ = _run(
+        "rollback", "--root", str(root), "--yes",
+        "--expected-current-digest", str(current["manifest_digest"]),
+        "--target-digest", str(target_checked["manifest_digest"]),
+        "--trusted-key", f"release-key-1={public_hex}",
+    )
+    assert replayed_rollback["replayed"] is True
+
+    target_bundle = json.loads(
+        Path(str(target_checked["verified_bundle_path"]), "bundle.json").read_text()
+    )
+    target_pointer = {
+        "artifact_name": target["artifact_name"],
+        "bundle_digest": target_bundle["bundle_digest"],
+        "manifest_digest": target["manifest_digest"],
+        "schema_version": 1,
+        "version": target["version"],
+    }
+    rollback_request_digest = admin._protocol_digest({
+        "expected_current_digest": str(current["manifest_digest"]),
+        "target_digest": str(target["manifest_digest"]),
+    })
+    matching_intent = {
+        "current_pointer": pointer,
+        "request_digest": rollback_request_digest,
+        "target_pointer": target_pointer,
+    }
+    intent_path = root / "state" / "rollback-intent.json"
+    intent_path.write_text(json.dumps({**matching_intent, "request_digest": "f" * 64}))
+    mismatched, _ = _run(
+        "rollback", "--root", str(root), "--yes",
+        "--expected-current-digest", str(current["manifest_digest"]),
+        "--target-digest", str(target["manifest_digest"]),
+        "--trusted-key", f"release-key-1={public_hex}", expect=3,
+    )
+    assert mismatched["error"]["code"] == "STATE_CONFLICT"
+    assert intent_path.is_file()
+
+    intent_path.write_text(json.dumps(matching_intent))
+    real_unlink = Path.unlink
+    unlink_failed = False
+
+    def fail_intent_unlink(path, *args, **kwargs):
+        nonlocal unlink_failed
+        if path == intent_path and not unlink_failed:
+            unlink_failed = True
+            raise OSError("injected intent unlink failure")
+        return real_unlink(path, *args, **kwargs)
+
+    monkeypatch.setattr(Path, "unlink", fail_intent_unlink)
+    with pytest.raises(OSError, match="unlink"):
+        admin.command_rollback(SimpleNamespace(
+            root=str(root), yes=True,
+            expected_current_digest=str(current["manifest_digest"]),
+            target_digest=str(target["manifest_digest"]),
+            trusted_key=[f"release-key-1={public_hex}"],
+        ))
+    assert intent_path.is_file()
+    monkeypatch.setattr(Path, "unlink", real_unlink)
+
+    state_fsync_failed = False
+
+    def fail_state_fsync_once(path):
+        nonlocal state_fsync_failed
+        if Path(path) == root / "state" and not state_fsync_failed:
+            state_fsync_failed = True
+            raise OSError("injected state fsync failure")
+        return real_fsync_directory(path)
+
+    monkeypatch.setattr(admin, "_fsync_directory", fail_state_fsync_once)
+    with pytest.raises(OSError, match="state fsync"):
+        admin.command_rollback(SimpleNamespace(
+            root=str(root), yes=True,
+            expected_current_digest=str(current["manifest_digest"]),
+            target_digest=str(target["manifest_digest"]),
+            trusted_key=[f"release-key-1={public_hex}"],
+        ))
+    assert not intent_path.exists()
+    monkeypatch.setattr(admin, "_fsync_directory", real_fsync_directory)
+    recovered_replay = admin.command_rollback(SimpleNamespace(
+        root=str(root), yes=True,
+        expected_current_digest=str(current["manifest_digest"]),
+        target_digest=str(target["manifest_digest"]),
+        trusted_key=[f"release-key-1={public_hex}"],
+    ))
+    assert recovered_replay["replayed"] is True
+
+    older_artifact = root / "releases" / "edge-0.9.0.tar"
+    older_artifact.write_bytes(b"older verified release")
+    older = _release_manifest(
+        release_key, release_id="older-release", version="0.9.0",
+        rollback_version="0.8.0", artifact=older_artifact,
+    )
+    older_checked = _verify_release(root, older, older_artifact, public_hex)
+    next_rollback, _ = _run(
+        "rollback", "--root", str(root), "--yes",
+        "--expected-current-digest", str(target["manifest_digest"]),
+        "--target-digest", str(older_checked["manifest_digest"]),
+        "--trusted-key", f"release-key-1={public_hex}",
+    )
+    assert next_rollback["status"] == "rolled_back"
+    assert next_rollback["version"] == "0.9.0"
+
+
+def test_release_check_verifies_ed25519_artifact_digest_and_replay(tmp_path):
+    root = tmp_path / "edge-root"
+    _init(root)
+    artifact = root / "releases" / "edge-2.0.0.tar"
+    artifact.write_bytes(b"verified edge release")
+    private = ed25519.Ed25519PrivateKey.generate()
+    public_hex = private.public_key().public_bytes(
+        serialization.Encoding.Raw, serialization.PublicFormat.Raw
+    ).hex()
+    unsigned = {
+        "artifact_digest": hashlib.sha256(artifact.read_bytes()).hexdigest(),
+        "artifact_name": artifact.name,
+        "deadline_at": (datetime.now(UTC) + timedelta(hours=1)).isoformat().replace("+00:00", "Z"),
+        "key_id": "release-key-1",
+        "release_id": "rel-1",
+        "rollback_version": "1.0.0",
+        "signature_algorithm": "Ed25519",
+        "status": "offered",
+        "version": "2.0.0",
+    }
+    manifest = {
+        **unsigned,
+        "manifest_digest": canonical_sha256(unsigned),
+        "signature": private.sign(canonical_json_bytes(unsigned)).hex(),
+    }
+    manifest_path = root / "releases" / "manifest.json"
+    manifest_path.write_text(json.dumps(manifest))
+    checked, _ = _run(
+        "release-check", "--root", str(root), "--manifest", str(manifest_path),
+        "--artifact", str(artifact), "--trusted-key", f"release-key-1={public_hex}",
+        "--current-version", "1.0.0",
+    )
+    assert checked["status"] == "verified"
+    assert checked["manifest_digest"] == manifest["manifest_digest"]
+    bundle = Path(str(checked["verified_bundle_path"]))
+    assert (bundle / "manifest.json").is_file()
+    assert (bundle / artifact.name).read_bytes() == artifact.read_bytes()
+    assert (bundle / "bundle.json").is_file()
+    exact, _ = _run(
+        "release-check", "--root", str(root), "--manifest", str(manifest_path),
+        "--artifact", str(artifact), "--trusted-key", f"release-key-1={public_hex}",
+        "--current-version", "1.0.0",
+    )
+    assert exact["replayed"] is True
+    changed_unsigned = {**unsigned, "status": "accepted"}
+    changed_replay = {
+        **changed_unsigned,
+        "manifest_digest": canonical_sha256(changed_unsigned),
+        "signature": private.sign(canonical_json_bytes(changed_unsigned)).hex(),
+    }
+    manifest_path.write_text(json.dumps(changed_replay))
+    replay_rejected, _ = _run(
+        "release-check", "--root", str(root), "--manifest", str(manifest_path),
+        "--artifact", str(artifact), "--trusted-key", f"release-key-1={public_hex}",
+        "--current-version", "1.0.0", expect=3,
+    )
+    assert replay_rejected["error"]["code"] == "RELEASE_REJECTED"
+    tampered = dict(manifest)
+    tampered["version"] = "2.0.1"
+    manifest_path.write_text(json.dumps(tampered))
+    rejected, _ = _run(
+        "release-check", "--root", str(root), "--manifest", str(manifest_path),
+        "--artifact", str(artifact), "--trusted-key", f"release-key-1={public_hex}",
+        "--current-version", "1.0.0", expect=3,
+    )
+    assert rejected["error"]["code"] == "RELEASE_REJECTED"
+
+
+def test_rollback_rejects_forged_verified_manifest_and_tampered_artifact(tmp_path):
+    root = tmp_path / "edge-root"
+    _init(root)
+    key = ed25519.Ed25519PrivateKey.generate()
+    public_hex = key.public_key().public_bytes(
+        serialization.Encoding.Raw, serialization.PublicFormat.Raw
+    ).hex()
+    target_artifact = root / "releases" / "target.bin"
+    target_artifact.write_bytes(b"target")
+    target = _release_manifest(
+        key, release_id="target", version="1.0.0", rollback_version="0.9.0",
+        artifact=target_artifact,
+    )
+    target_checked = _verify_release(root, target, target_artifact, public_hex)
+    current_artifact = root / "releases" / "current.bin"
+    current_artifact.write_bytes(b"current")
+    current = _release_manifest(
+        key, release_id="current", version="2.0.0", rollback_version="1.0.0",
+        artifact=current_artifact,
+    )
+    current_checked = _verify_release(root, current, current_artifact, public_hex)
+    current_bundle = Path(str(current_checked["verified_bundle_path"]))
+    bundle_record = json.loads((current_bundle / "bundle.json").read_text())
+    (root / "releases" / "current.json").write_text(json.dumps({
+        "artifact_name": current["artifact_name"],
+        "bundle_digest": bundle_record["bundle_digest"],
+        "manifest_digest": current["manifest_digest"],
+        "schema_version": 1,
+        "version": current["version"],
+    }))
+    for expected_digest, target_digest in (
+        ("c" * 64, str(target["manifest_digest"])),
+        (str(current["manifest_digest"]), "d" * 64),
+    ):
+        wrong, _ = _run(
+            "rollback", "--root", str(root), "--yes",
+            "--expected-current-digest", expected_digest,
+            "--target-digest", target_digest,
+            "--trusted-key", f"release-key-1={public_hex}", expect=3,
+        )
+        assert wrong["error"]["code"] in {"RELEASE_REJECTED", "STATE_CONFLICT"}
+    target_bundle = Path(str(target_checked["verified_bundle_path"]))
+    manifest_path = target_bundle / "manifest.json"
+    manifest_record = json.loads(manifest_path.read_text())
+    manifest_path.write_text(json.dumps({**manifest_record, "signature": "0" * 128}))
+    forged, _ = _run(
+        "rollback", "--root", str(root), "--yes",
+        "--expected-current-digest", str(current["manifest_digest"]),
+        "--target-digest", str(target["manifest_digest"]),
+        "--trusted-key", f"release-key-1={public_hex}", expect=3,
+    )
+    assert forged["error"]["code"] == "RELEASE_REJECTED"
+    manifest_path.write_text(json.dumps(manifest_record))
+    (target_bundle / str(target["artifact_name"])).write_bytes(b"tampered")
+    rejected, _ = _run(
+        "rollback", "--root", str(root), "--yes",
+        "--expected-current-digest", str(current["manifest_digest"]),
+        "--target-digest", str(target["manifest_digest"]),
+        "--trusted-key", f"release-key-1={public_hex}", expect=3,
+    )
+    assert rejected["error"]["code"] == "RELEASE_REJECTED"
+    assert json.loads((root / "releases" / "current.json").read_text())["manifest_digest"] == current["manifest_digest"]
+
+
+def test_edge_compose_is_pull_only_and_hardened():
+    model = yaml.safe_load(COMPOSE.read_text())
+    assert set(model["services"]) == {"edge-gateway"}
+    service = model["services"]["edge-gateway"]
+    assert service["build"] == {
+        "context": "../..", "dockerfile": "deploy/edge/Dockerfile.edge"
+    }
+    assert service["pull_policy"] == "${EDGE_PULL_POLICY:-build}"
+    assert service["command"] == ["python", "-m", "app.edge_gateway"]
+    assert "ports" not in service and "expose" not in service
+    assert service["user"] not in {"0", 0, "root"}
+    assert service["read_only"] is True
+    assert service["cap_drop"] == ["ALL"]
+    assert "no-new-privileges:true" in service["security_opt"]
+    assert service["restart"] == "unless-stopped"
+    assert service["pids_limit"] <= 256
+    assert service["mem_limit"]
+    assert service["cpus"]
+    assert service["healthcheck"]["test"][0] == "CMD"
+    assert service["user"] == "${EDGE_RUNTIME_UID:?}:${EDGE_RUNTIME_GID:?}"
+    assert any(
+        "uid=${EDGE_RUNTIME_UID:?},gid=${EDGE_RUNTIME_GID:?}" in item
+        for item in service["tmpfs"]
+    )
+    volumes = "\n".join(service["volumes"])
+    for marker in ("queue", "artifacts", "releases", "secrets", "certificates"):
+        assert marker in volumes
+    assert "/certificates:ro" in volumes and "/secrets:ro" in volumes
+    env_text = ENV_EXAMPLE.read_text()
+    assert "EDGE_CONTROL_ORIGIN=https://" in env_text
+    assert "EDGE_ALLOWED_CONTROL_HOSTS=" in env_text
+    assert "EDGE_RUNTIME_UID=" in env_text and "EDGE_RUNTIME_GID=" in env_text
+    assert "EDGE_CLIENT_PRIVATE_KEY_PATH=/run/edge/secrets/client.key" in env_text
+    assert service["environment"]["EDGE_POLL_INTERVAL_SECONDS"] == (
+        "${EDGE_POLL_INTERVAL_SECONDS:-5}"
+    )
+    assert "EDGE_CREDENTIAL=" not in env_text
+    assert "BEGIN PRIVATE KEY" not in env_text
+
+
+def test_real_cli_private_material_is_readable_by_configured_container_uid(tmp_path):
+    image = subprocess.run(
+        ["docker", "image", "inspect", "nginx:alpine"], capture_output=True
+    )
+    if image.returncode != 0:
+        pytest.skip("local nginx:alpine image is required; this test never pulls")
+    root = tmp_path / "edge-root"
+    _init(root)
+    _run("csr", "--root", str(root), "--common-name", "edge-test")
+    completed = subprocess.run(
+        [
+            "docker", "run", "--rm", "--user", f"{os.getuid()}:{os.getgid()}",
+            "-v", f"{root / 'secrets'}:/run/edge/secrets:ro",
+            "nginx:alpine", "sh", "-c", "test -r /run/edge/secrets/client.key",
+        ],
+        capture_output=True,
+        text=True,
+        check=False,
+    )
+    assert completed.returncode == 0, completed.stderr
+
+
+def test_openapi_contains_closed_edge_contract_without_secret_response_fields():
+    document = yaml.safe_load(OPENAPI.read_text())
+    paths = document["paths"]
+    expected = {
+        "/api/datasource/edge/enrollments",
+        "/api/datasource/edge/register",
+        "/api/datasource/edge/gateways",
+        "/api/datasource/edge/gateways/{gateway_id}/heartbeat",
+        "/api/datasource/edge/gateways/{gateway_id}/tasks/pull",
+        "/api/datasource/edge/gateways/{gateway_id}/tasks/{task_id}/outcome",
+        "/api/datasource/edge/gateways/{gateway_id}/events",
+        "/api/datasource/edge/gateways/{gateway_id}/reconcile",
+        "/api/datasource/edge/gateways/{gateway_id}/releases",
+        "/api/datasource/edge/gateways/{gateway_id}/releases/ack",
+    }
+    assert expected <= set(paths)
+    machine = paths["/api/datasource/edge/gateways/{gateway_id}/heartbeat"]["post"]
+    assert machine["x-max-request-bytes"] == 262_144
+    assert machine["x-max-json-depth"] == 32
+    assert machine["x-max-json-nodes"] == 5_000
+    assert {parameter["name"] for parameter in machine["parameters"]} >= {
+        "gateway_id", "X-Edge-Credential", "X-Edge-Certificate-SHA256",
+        "X-DataOps-Edge-Client-Verify", "X-DataOps-Edge-Client-Cert",
+    }
+    assert machine["responses"]["200"]["headers"]["Cache-Control"]["schema"]["const"] == "no-store"
+    assert machine["security"] == [{"mutualTLS": [], "edgeCredential": []}]
+    assert paths["/api/datasource/edge/enrollments"]["post"]["security"] == [
+        {"bearerAuth": []}
+    ]
+    assert paths["/api/datasource/edge/gateways/{gateway_id}/rotate"]["post"][
+        "security"
+    ] == [{"bearerAuth": []}]
+    assert "requestBody" not in paths["/api/datasource/edge/tasks/{task_id}/cancel"][
+        "post"
+    ]
+    schemas = document["components"]["schemas"]
+    for name in (
+        "EdgeEnrollmentRequest", "EdgeRegisterRequest", "EdgeSignedTaskEnvelope",
+        "EdgeEventRequest", "EdgeReconcileResponse", "EdgeReleaseManifest",
+        "EdgeTaskOutcomeRequest", "EdgeErrorEnvelope",
+    ):
+        assert name in schemas
+    rendered = json.dumps(schemas).casefold()
+    assert "private_key" not in rendered
+    assert "credential_secret" not in rendered
+    assert "lineage" in schemas["EdgeTaskContract"]["properties"]["classification"]["enum"]
+    assert schemas["EdgeSafeSummary"]["x-max-policy-depth"] == 12
+    assert schemas["EdgeRegisterRequest"]["properties"]["version"] == {
+        "$ref": "#/components/schemas/EdgeAgentVersion"
+    }
+    assert schemas["EdgeHeartbeatRequest"]["properties"]["version"] == {
+        "$ref": "#/components/schemas/EdgeAgentVersion"
+    }
+    assert schemas["EdgeReleaseEnvelope"]["properties"]["data"]["properties"][
+        "version"
+    ] == {"$ref": "#/components/schemas/EdgeVersion"}
+    assert schemas["EdgeReleaseEnvelope"]["properties"]["data"]["properties"][
+        "rollback_version"
+    ] == {"$ref": "#/components/schemas/EdgeVersion"}
+    list_parameters = {
+        item["name"]: item["schema"]
+        for item in paths["/api/datasource/edge/gateways"]["get"]["parameters"]
+    }
+    assert list_parameters["limit"] == {
+        "type": "integer", "minimum": 1, "maximum": 100
+    }
+    assert list_parameters["offset"] == {
+        "type": "integer", "minimum": 0, "maximum": 10_000
+    }
+    heartbeat_sample = {
+        "environment": "production", "network_zone": "zone-a", "generation": 1,
+        "version": "1.0.0", "safe_summary": {"queue_depth": 0},
+    }
+    release_data = schemas["EdgeReleaseEnvelope"]["properties"]["data"]
+    assert release_data["properties"]["manifest"] == {
+        "$ref": "#/components/schemas/EdgeReleaseManifest"
+    }
+    security = document["components"]["securitySchemes"]
+    assert security["mutualTLS"] == {"type": "mutualTLS"}
+    assert security["edgeCredential"]["name"] == "X-Edge-Credential"
+
+    def local_refs(value):
+        if isinstance(value, dict):
+            return {
+                key: (
+                    f"#/$defs/{item.rsplit('/', 1)[-1]}"
+                    if key == "$ref"
+                    and isinstance(item, str)
+                    and item.startswith("#/components/schemas/")
+                    else local_refs(item)
+                )
+                for key, item in value.items()
+            }
+        if isinstance(value, list):
+            return [local_refs(item) for item in value]
+        return value
+
+    definitions = local_refs(schemas)
+    heartbeat_validator = jsonschema.Draft202012Validator({
+        "$defs": definitions, "$ref": "#/$defs/EdgeHeartbeatRequest"
+    })
+    heartbeat_validator.validate(heartbeat_sample)
+    safe_validator = jsonschema.Draft202012Validator({
+        "$defs": definitions, "$ref": "#/$defs/EdgeSafeSummary"
+    })
+    safe_validator.validate({"queue_depth": 0, "status": "healthy"})
+    with pytest.raises(jsonschema.ValidationError):
+        safe_validator.validate({"raw_rows": [{"secret": "x"}]})
+    with pytest.raises(jsonschema.ValidationError):
+        safe_validator.validate({"RawRows": [{"value": 1}]})
+    with pytest.raises(jsonschema.ValidationError):
+        safe_validator.validate({"PASSWORD": "masked"})
+    too_deep: object = "leaf"
+    for _ in range(13):
+        too_deep = {"level": too_deep}
+    with pytest.raises(jsonschema.ValidationError):
+        safe_validator.validate({"summary": too_deep})
+    with pytest.raises(jsonschema.ValidationError):
+        safe_validator.validate({"message": "x" * 4097})
+    cursor_validator = jsonschema.Draft202012Validator({
+        "$defs": definitions, "$ref": "#/$defs/EdgeCursor"
+    })
+    with pytest.raises(jsonschema.ValidationError):
+        cursor_validator.validate("../invalid cursor")
+
+
+def test_docs_and_machine_evidence_preserve_enterprise_uat_boundary():
+    required = [
+        ROOT / "deploy" / "edge" / "README.md",
+        ROOT / "docs" / "runbooks" / "EDGE_GATEWAY_OPERATIONS.md",
+        ROOT / "docs" / "phase3" / "P3_WP04_EDGE_GATEWAY.md",
+        ROOT / "docs" / "validation" / "P3_WP04_EDGE_GATEWAY_EVIDENCE.md",
+    ]
+    combined = "\n".join(path.read_text() for path in required)
+    assert "ENGINEERING_COMPLETE_ENTERPRISE_EDGE_UAT_BLOCKED" in combined
+    assert "TBD_EXTERNAL" in combined
+    for topic in (
+        "默认拒绝", "数据分级", "证书轮换", "证书撤销", "断网", "重连",
+        "取消", "回滚", "诊断", "备份", "恢复", "CRL", "v1", "v2",
+    ):
+        assert topic in combined
+    assert "真实企业来源已通过" not in combined
+    failures = json.loads(FAILURES.read_text())
+    assert failures["work_package"] == "P3-WP04"
+    assert failures["engineering_status"] == "ENGINEERING_COMPLETE_ENTERPRISE_EDGE_UAT_BLOCKED"
+    assert failures["cases"]
+    required_keys = {
+        "case_id", "scope", "preconditions", "steps", "safety_boundary",
+        "expected", "evidence", "rollback", "status",
+    }
+    assert all(required_keys <= set(case) for case in failures["cases"])
+    assert any(case["status"] == "TBD_EXTERNAL" for case in failures["cases"])
+
+
+def test_operator_runbook_contains_executable_install_recovery_and_uat_gates():
+    runbook = (ROOT / "docs/runbooks/EDGE_GATEWAY_OPERATIONS.md").read_text()
+    for marker in (
+        "Docker Engine 26", "Docker Compose 2.27", "Python 3.11",
+        "sha256sum -c SHA256SUMS", "cosign verify-blob",
+        "edge.config.schema.json", "docker image inspect",
+        "health.json", "120 秒", "X-Edge-Enrollment", "X-Edge-Credential",
+        "/tasks/{task_id}/outcome", "/reconcile", "/releases/ack",
+        "edge_gateway_admin.py status", "人工介入阈值", "legal hold",
+        "delete receipt", "sqlite3", "VACUUM INTO", "quick_check",
+        "rollback-intent.json", "state/rollback-history/", "DB_ROLE_INIT_DATABASE_URL",
+        "MIGRATION_DATABASE_URL", "has_schema_privilege", "CRL_CHECK_LEAF",
+        "nginx -s reload", "TBD_EXTERNAL", "runtime-self-check",
+        "format_checker=jsonschema.FormatChecker()", "healthy=0",
+        "julianday(retention_until)<=julianday('now')",
+        "PGSERVICE=dataops_migrator",
+    ):
+        assert marker in runbook
+    assert "当前 queue schema 是 v1" not in runbook
+    assert "20260809_477 (head)" not in runbook
+
+
+def test_edge_config_example_matches_closed_runtime_file_schema():
+    schema = json.loads(EDGE_CONFIG_SCHEMA.read_text())
+    example = json.loads(EDGE_CONFIG_EXAMPLE.read_text())
+    jsonschema.Draft202012Validator.check_schema(schema)
+    jsonschema.Draft202012Validator(schema).validate(example)
+    assert schema["additionalProperties"] is False
+    assert set(schema["required"]) == set(schema["properties"])
+
+
+def test_generator_is_reproducible_for_edge_contract(tmp_path):
+    output = tmp_path / "openapi.yaml"
+    subprocess.run(
+        [sys.executable, str(ROOT / "scripts" / "generate_openapi.py"), "--output", str(output)],
+        cwd=ROOT, check=True, capture_output=True, text=True,
+    )
+    assert output.read_bytes() == OPENAPI.read_bytes()
+
+
+def test_release_archive_contains_exact_edge_cli_compose_and_runbook(tmp_path):
+    assert (ROOT / "deployment/scripts/edge_gateway_admin.py").read_bytes() == CLI.read_bytes()
+    completed = subprocess.run(
+        ["bash", str(ROOT / "deployment/package_release.sh")],
+        cwd=ROOT,
+        capture_output=True,
+        text=True,
+        check=False,
+        env={
+            **os.environ,
+            "DATAOPS_RELEASE_OUTPUT_DIR": str(tmp_path),
+            "DATAOPS_RELEASE_VERSION": "wp04-test",
+        },
+    )
+    assert completed.returncode == 0, completed.stderr
+    archive = tmp_path / "dataops-platform-release-wp04-test.tar.gz"
+    with tarfile.open(archive) as bundle:
+        names = set(bundle.getnames())
+        required = {
+            "dataops-platform/scripts/edge_gateway_admin.py",
+            "dataops-platform/deploy/edge/docker-compose.edge.yml",
+            "dataops-platform/deploy/edge/Dockerfile.edge",
+            "dataops-platform/deploy/edge/edge.config.example.json",
+            "dataops-platform/deploy/edge/edge.config.schema.json",
+            "dataops-platform/deploy/edge/edge.env.example",
+            "dataops-platform/deploy/edge/README.md",
+            "dataops-platform/docs/runbooks/EDGE_GATEWAY_OPERATIONS.md",
+            "dataops-platform/docs/architecture/OPENAPI.yaml",
+            "dataops-platform/docs/validation/P3_WP04_FAILURE_INJECTION.json",
+            "dataops-platform/SHA256SUMS",
+        }
+        assert required <= names
+        cli_member = bundle.extractfile("dataops-platform/scripts/edge_gateway_admin.py")
+        assert cli_member is not None and cli_member.read() == CLI.read_bytes()
+        assert not any("__pycache__" in name or name.endswith(".pyc") for name in names)
+        readme_member = bundle.extractfile("dataops-platform/deploy/edge/README.md")
+        assert readme_member is not None
+        archive_readme = readme_member.read().decode("utf-8")
+        assert "../../docs/runbooks/EDGE_GATEWAY_OPERATIONS.md" in archive_readme
+        checksum_member = bundle.extractfile("dataops-platform/SHA256SUMS")
+        assert checksum_member is not None
+        checksum_lines = checksum_member.read().decode("utf-8").splitlines()
+        assert checksum_lines
+        for line in checksum_lines:
+            digest, relative = line.split("  ", 1)
+            member = bundle.extractfile(f"dataops-platform/{relative.removeprefix('./')}")
+            assert member is not None
+            assert hashlib.sha256(member.read()).hexdigest() == digest
+        for name in names:
+            member = bundle.extractfile(name)
+            if member is None:
+                continue
+            content = member.read()
+            assert b"-----BEGIN PRIVATE KEY-----" not in content
+            assert b"-----BEGIN RSA PRIVATE KEY-----" not in content
+
+    if shutil.which("docker") is not None:
+        extracted = tmp_path / "extracted"
+        extracted.mkdir()
+        with tarfile.open(archive) as bundle:
+            bundle.extractall(extracted, filter="data")
+        release_root = extracted / "dataops-platform"
+        build = subprocess.run(
+            [
+                "docker", "build", "-f", "deploy/edge/Dockerfile.edge",
+                "-t", "dataops-edge:wp04-archive-smoke", ".",
+            ],
+            cwd=release_root,
+            capture_output=True,
+            text=True,
+            check=False,
+        )
+        assert build.returncode == 0, build.stderr[-4000:]
+
+
+def test_release_secret_scanner_rejects_binary_and_extended_secret_shapes(tmp_path):
+    clean = tmp_path / "clean"
+    clean.mkdir()
+    (clean / "README.md").write_text("credential is mounted from a secret file\n")
+    accepted = subprocess.run(
+        [sys.executable, str(RELEASE_SECRET_SCANNER), str(clean)],
+        capture_output=True,
+        text=True,
+        check=False,
+    )
+    assert accepted.returncode == 0, accepted.stderr
+
+    cases = {
+        "encrypted.pem": b"-----BEGIN ENCRYPTED PRIVATE KEY-----\nAAAA\n",
+        "identity.p12": b"0\x82\x01\x00binary-pkcs12",
+        "config.txt": b"credential = actual-production-value-123456\n",
+    }
+    for name, content in cases.items():
+        candidate = tmp_path / name
+        candidate.mkdir()
+        (candidate / name).write_bytes(content)
+        rejected = subprocess.run(
+            [sys.executable, str(RELEASE_SECRET_SCANNER), str(candidate)],
+            capture_output=True,
+            text=True,
+            check=False,
+        )
+        assert rejected.returncode != 0, name

Some files were not shown because too many files changed in this diff