{
  "schema_version": "1.0.0",
  "bot_id": "5.5",
  "bot_name": "KeyRotationReminder",
  "slug": "keyrotationreminder",
  "layer": "Security",
  "layer_key": "sec",
  "bot_class": "Guardrail",
  "authority": [
    "Reject",
    "Pause"
  ],
  "status": "planned",
  "readiness": "Spec started",
  "flagship": false,
  "is_reference": false,
  "public_export": false,
  "identity": {
    "layer": "Security",
    "bot_class": "Guardrail",
    "authority": "Reject, Pause",
    "runs_before": "Any signing call using a key past its rotation schedule",
    "runs_after": "Session key validation",
    "applies_to": "All active signing keys per user across environments",
    "default_mode": "shadow_only",
    "user_visible": "Advanced details only",
    "developer_owner": "Polytraders core"
  },
  "purpose": "Nag the user to rotate signing keys on a schedule; prevent key reuse across environments.",
  "why_it_matters": [
    {
      "failure": "Signing key never rotated",
      "consequence": "A long-lived key that is compromised silently provides unlimited signing authority over time."
    },
    {
      "failure": "Same key used across prod and staging",
      "consequence": "A staging environment compromise exposes production signing capability."
    },
    {
      "failure": "No block on overdue rotation",
      "consequence": "Users ignore rotation reminders indefinitely, leaving stale keys in production."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "ClobAuth API key registration timestamp",
      "source": "clob_auth",
      "required": true,
      "use": "Determine when the current signing key was registered to calculate age."
    }
  ],
  "internal_inputs": [
    {
      "input": "Configured rotate_every_days and block_on_overdue_h",
      "source": "Admin UI",
      "required": true,
      "use": "Schedule and enforcement thresholds for key rotation."
    },
    {
      "input": "KillSwitch active flag",
      "source": "KillSwitch",
      "required": true,
      "use": "Block all signing during global pause."
    }
  ],
  "raw_params": [
    "rotate_every_days \u00b7 int",
    "block_on_overdue_h \u00b7 int",
    "require_unique_per_env \u00b7 bool",
    "publish_to_user \u00b7 bool"
  ],
  "parameters": [
    {
      "name": "rotate_every_days",
      "default": 30,
      "warning": "Key age > 0.9 * rotate_every_days",
      "hard": "Key age > rotate_every_days + (block_on_overdue_h / 24)",
      "controls": "Number of days before a signing key should be rotated.",
      "why_default_matters": "30-day default aligns with industry key management best practices.",
      "threshold_logic": [
        {
          "condition": "key_age_d < rotate_every_days",
          "action": "APPROVE"
        },
        {
          "condition": "rotate_every_days <= key_age_d <= rotate_every_days + block_on_overdue_h/24",
          "action": "WARN \u2014 rotation overdue"
        },
        {
          "condition": "key_age_d > rotate_every_days + block_on_overdue_h/24",
          "action": "REJECT \u2014 KEY_ROTATION_OVERDUE"
        }
      ],
      "dev_check": "if (key_age_d > p.rotate_every_days + p.block_on_overdue_h/24) return reject('KEY_ROTATION_OVERDUE');",
      "user_facing": "Your signing key is overdue for rotation. Trading is blocked until you rotate it."
    },
    {
      "name": "require_unique_per_env",
      "default": true,
      "warning": "Same key fingerprint detected in multiple environments",
      "hard": "Same key fingerprint used in prod and non-prod simultaneously",
      "controls": "Reject signing if the key fingerprint is shared across environments.",
      "why_default_matters": "Unique keys per environment limit blast radius of a staging compromise.",
      "threshold_logic": [
        {
          "condition": "key fingerprint unique per env",
          "action": "APPROVE"
        },
        {
          "condition": "key fingerprint shared across envs AND require_unique_per_env=true",
          "action": "REJECT \u2014 KEY_REUSE_ACROSS_ENV"
        }
      ],
      "dev_check": "if (p.require_unique_per_env && isKeySharedAcrossEnvs(key)) return reject('KEY_REUSE_ACROSS_ENV');",
      "user_facing": "Your signing key is shared across environments. Please rotate to a unique key."
    }
  ],
  "default_config": {
    "bot_id": "sec.key_rotation_reminder",
    "version": "0.1.0",
    "mode": "hard_guard",
    "defaults": {
      "rotate_every_days": 30,
      "block_on_overdue_h": 24,
      "require_unique_per_env": true,
      "publish_to_user": true
    }
  },
  "implementation_flow": [
    "Receive signing call.",
    "Check KillSwitch; if active, REJECT(KILL_SWITCH_ACTIVE).",
    "FETCH key registration timestamp from ClobAuth.",
    "Compute key_age_d = (now - registered_at) / 86400.",
    "If key_age_d in warning range: emit WARN and continue.",
    "If key_age_d > rotate_every_days + block_on_overdue_h/24: REJECT(KEY_ROTATION_OVERDUE).",
    "If require_unique_per_env: check fingerprint across env registry; if shared, REJECT(KEY_REUSE_ACROSS_ENV).",
    "Emit RiskVote(APPROVE) if all checks pass."
  ],
  "decision_logic": {
    "approve": "Key is within rotation schedule, unique per environment, and KillSwitch inactive.",
    "reshape_required": "Not applicable.",
    "reject": "Key overdue for rotation or shared across environments.",
    "warning_only": "Warn when key age is within 10% of rotate_every_days."
  },
  "decision_output_schema": "RiskVote",
  "decision_output_example": {
    "vote_id": "sec.key_rotation_reminder.20260509T160000Z",
    "decision": "APPROVE",
    "reason_code": null,
    "evidence": {
      "key_fingerprint": "ab12cd34",
      "key_age_d": 12,
      "rotate_every_days": 30,
      "days_until_required_rotation": 18
    },
    "checked_at": "2026-05-09T16:00:00Z"
  },
  "developer_log": {
    "bot_id": "sec.key_rotation_reminder",
    "decision": "APPROVE",
    "inputs_used": [
      "clob_auth.key_registered_at",
      "config.rotate_every_days"
    ],
    "checked_at": "2026-05-09T16:00:00Z"
  },
  "user_explanations": [
    {
      "situation": "Rotation reminder",
      "message": "Your signing key is approaching its rotation deadline. Please rotate it before it expires."
    },
    {
      "situation": "Signing blocked \u2014 rotation overdue",
      "message": "Your signing key is overdue for rotation. Trading is blocked until you complete the rotation."
    },
    {
      "situation": "Signing blocked \u2014 key shared across environments",
      "message": "Your signing key is shared across environments. Please rotate to a unique key for each environment."
    }
  ],
  "failure_modes": {
    "main_failure_mode": "ClobAuth API unreachable so key registration timestamp cannot be retrieved, causing all rotation checks to fail.",
    "false_positive_risk": "Clock skew between ClobAuth and the bot causes a key to appear older than it is, triggering premature block.",
    "false_negative_risk": "Key registration timestamp cached stale, allowing an overdue key to continue signing.",
    "safe_fallback": "If ClobAuth is unreachable, fail-closed after block_on_overdue_h: block all signing until connectivity restored.",
    "required_dependencies": [
      "ClobAuth API",
      "Admin UI config",
      "KillSwitch"
    ]
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "Approve key within rotation window",
        "setup": "key_age_d=12, rotate_every_days=30",
        "expected": "APPROVE"
      },
      {
        "test": "Warn when key in 90% of rotation window",
        "setup": "key_age_d=28, rotate_every_days=30",
        "expected": "APPROVE with WARN"
      },
      {
        "test": "Reject when key overdue",
        "setup": "key_age_d=32, rotate_every_days=30, block_on_overdue_h=24",
        "expected": "DENY(KEY_ROTATION_OVERDUE)"
      }
    ],
    "integration": [
      {
        "test": "Key shared across envs triggers reject",
        "expected": "DENY(KEY_REUSE_ACROSS_ENV) when same fingerprint in prod and staging"
      },
      {
        "test": "ClobAuth unreachable fails closed after grace period",
        "expected": "DENY after block_on_overdue_h elapses without connectivity"
      }
    ],
    "property": [
      {
        "property": "key_age_d > rotate_every_days + block_on_overdue_h/24 always DENY",
        "required": "Always true"
      },
      {
        "property": "Shared key fingerprint across envs always DENY when require_unique_per_env=true",
        "required": "Always true"
      }
    ]
  },
  "checklist_overrides": {},
  "legacy_goal": "Nag the user to rotate signing keys on a schedule; prevent key reuse across environments.",
  "legacy_pm_signals": [
    "Time since last rotation per signing key",
    "Cross-environment key fingerprints",
    "Outstanding rotation tickets"
  ],
  "legacy_external_feeds": [],
  "reporting_groups": [
    "risk_compliance",
    "governance_audit"
  ],
  "network": [
    "polygon"
  ],
  "api_surface": [
    "clob_auth",
    "internal"
  ],
  "version": {
    "spec": "2.0.0",
    "implementation": "0.1.0",
    "schema": "2",
    "released": null,
    "planned_release": "Q4-2026"
  },
  "migration_history": [
    {
      "date": "2026-04-28",
      "from": "n/a",
      "to": "v2-spec",
      "reason": "Spec drafted post-CLOB-V2 cutover; bot not yet implemented",
      "action_taken": "Designed against V2 schema (pUSD, builder codes, V2 EIP-712 domain)"
    }
  ],
  "polymarket_v2_compat": {
    "clob_version": "v2",
    "collateral": "pUSD",
    "eip712_domain_version": "2",
    "builder_code_aware": false,
    "negrisk_aware": false,
    "multichain_ready": false,
    "sdk_used": "py-clob-client-v2",
    "settlement_contract": "CTFExchangeV2",
    "notes": "Tracks V2 ClobAuth API key registration timestamps; rotation policy applies to keys used for V2 order signing."
  },
  "reference_implementation": {
    "pseudocode": "// KeyRotationReminder\nFUNCTION checkKeyRotation(signingKey, envRegistry):\n  IF FETCH(internal.killswitch).active:\n    EMIT RiskVote(DENY, KILL_SWITCH_ACTIVE); RETURN\n  keyInfo = FETCH(clob_auth.key_info(signingKey.fingerprint))\n  IF keyInfo == null:\n    EMIT RiskVote(DENY, STALE_DATA); RETURN\n  key_age_d = (NOW() - keyInfo.registered_at) / 86400\n  grace_d = params.block_on_overdue_h / 24\n  // Overdue block\n  IF key_age_d > params.rotate_every_days + grace_d:\n    EMIT RiskVote(DENY, KEY_ROTATION_OVERDUE); RETURN\n  // Warning band\n  IF key_age_d > params.rotate_every_days * 0.9:\n    EMIT warn(KEY_ROTATION_DUE_SOON)\n  // Cross-env check\n  IF params.require_unique_per_env:\n    IF envRegistry.count(signingKey.fingerprint) > 1:\n      EMIT RiskVote(DENY, KEY_REUSE_ACROSS_ENV); RETURN\n  EMIT RiskVote(APPROVE)\n  LOG(governance.audit, {fingerprint, key_age_d})",
    "sdk_calls": [
      "clob_auth.get_key_info(fingerprint)",
      "internal.killswitch.status()"
    ],
    "complexity": "O(e) where e = number of environments (small constant)"
  },
  "wire_examples": {
    "input": [
      {
        "label": "Signing call with healthy key",
        "source": "internal",
        "payload": {
          "intent_id": "int_5e6f7a8b9c0d1e2f",
          "key_fingerprint": "ab12cd34",
          "env": "prod",
          "timestamp_ms": 1746768672000
        }
      }
    ],
    "output": [
      {
        "label": "RiskVote \u2014 APPROVE",
        "payload": {
          "vote_id": "sec.key_rotation_reminder.20260509T160000Z",
          "decision": "APPROVE",
          "reason_code": null,
          "evidence": {
            "key_age_d": 12,
            "days_until_block": 19
          },
          "checked_at": "2026-05-09T16:00:00Z"
        }
      }
    ]
  },
  "reason_codes": [
    {
      "code": "KILL_SWITCH_ACTIVE",
      "severity": "HARD_REJECT",
      "meaning": "Global kill switch is active.",
      "action": "Immediately return DENY.",
      "user_message": "Trading is currently paused."
    },
    {
      "code": "KEY_ROTATION_OVERDUE",
      "severity": "HARD_REJECT",
      "meaning": "Signing key age exceeds rotate_every_days plus grace period.",
      "action": "Return DENY; prompt key rotation.",
      "user_message": "Your signing key is overdue for rotation. Please rotate it to resume trading."
    },
    {
      "code": "KEY_REUSE_ACROSS_ENV",
      "severity": "HARD_REJECT",
      "meaning": "Key fingerprint detected in multiple environments.",
      "action": "Return DENY; require unique key per environment.",
      "user_message": "Your signing key is shared across environments. Please use a unique key."
    },
    {
      "code": "KEY_ROTATION_DUE_SOON",
      "severity": "WARN",
      "meaning": "Key age exceeds 90% of rotate_every_days.",
      "action": "Warn user; allow signing to continue.",
      "user_message": "Your signing key will require rotation soon."
    },
    {
      "code": "STALE_DATA",
      "severity": "INFO",
      "meaning": "ClobAuth API unavailable; key age could not be verified.",
      "action": "Log; continue with cached data if available.",
      "user_message": "Could not verify key rotation status."
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "polytraders_sec_keyrotationreminder_decisions_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "decision"
        ],
        "meaning": "Total rotation check decisions."
      },
      {
        "name": "polytraders_sec_keyrotationreminder_overdue_total",
        "type": "counter",
        "unit": "count",
        "labels": [],
        "meaning": "Times signing was blocked for overdue rotation."
      },
      {
        "name": "polytraders_sec_keyrotationreminder_key_age_d",
        "type": "gauge",
        "unit": "days",
        "labels": [
          "env"
        ],
        "meaning": "Current age of active signing key per environment."
      }
    ],
    "alerts": [
      {
        "name": "KeyRotationOverdue",
        "condition": "polytraders_sec_keyrotationreminder_key_age_d > rotate_every_days",
        "severity": "P1",
        "runbook": "#runbook-keyrotation-overdue"
      },
      {
        "name": "KeyRotationBlock",
        "condition": "rate(polytraders_sec_keyrotationreminder_overdue_total[5m]) > 0",
        "severity": "P0",
        "runbook": "#runbook-keyrotation-block"
      }
    ]
  },
  "state": {
    "store": "in-process + ClobAuth API",
    "shape": "{fingerprint -> {registered_at, env, last_checked_at}}",
    "ttl": "refreshed hourly or on each signing call",
    "recovery": "Re-read from ClobAuth on restart; fail-closed if unavailable after grace period.",
    "size_estimate": "< 1 KB"
  },
  "concurrency": {
    "execution_model": "single-threaded event loop",
    "max_in_flight": 200,
    "idempotency_key": "intent_id",
    "timeout_ms": 200,
    "backpressure": "drop newest",
    "locking": "none"
  },
  "dependencies": {
    "depends_on": [
      {
        "bot_id": "risk.kill_switch",
        "why": "KillSwitch gate.",
        "contract": "DENY(KILL_SWITCH_ACTIVE) short-circuits."
      },
      {
        "bot_id": "sec.session_key_manager",
        "why": "Session key provides fingerprint for rotation check.",
        "contract": "Session must be active before rotation check runs."
      }
    ],
    "emits_to": [
      {
        "bot_id": "gov.builder_attribution",
        "why": "Audit log rotation events.",
        "contract": "GovernanceLog entry on each check."
      }
    ],
    "sibling": [
      "sec.session_key_manager"
    ],
    "external": [
      {
        "service": "ClobAuth API",
        "endpoint": "https://clob.polymarket.com",
        "sla": "99.9%",
        "failure_mode": "Fail-closed after block_on_overdue_h."
      }
    ]
  },
  "security_surfaces": {
    "signs_orders": false,
    "private_key_access": "none",
    "abuse_vectors": [
      "Backdating key registration timestamp to appear younger than actual age",
      "Using same key fingerprint across environments to avoid key generation overhead"
    ],
    "mitigations": [
      "Registration timestamp read from authoritative ClobAuth API, not from client",
      "Cross-environment fingerprint check is server-side"
    ]
  },
  "failure_injection": [
    {
      "scenario": "KEY_OVERDUE",
      "how_to_inject": "Set key registered_at to now - (rotate_every_days + 2) days",
      "expected_behaviour": "DENY(KEY_ROTATION_OVERDUE); signing blocked",
      "recovery": "User rotates key."
    },
    {
      "scenario": "KEY_REUSE",
      "how_to_inject": "Register same fingerprint in both prod and staging env registry",
      "expected_behaviour": "DENY(KEY_REUSE_ACROSS_ENV)",
      "recovery": "User generates separate key for each environment."
    },
    {
      "scenario": "CLOB_AUTH_DOWN",
      "how_to_inject": "Block ClobAuth API endpoint",
      "expected_behaviour": "STALE_DATA warn; fail-closed after block_on_overdue_h",
      "recovery": "Automatic when ClobAuth recovers."
    }
  ],
  "runbook": {
    "summary": "KeyRotationOverdue is a P1 security event; trading is blocked until rotation completes.",
    "oncall_actions": [
      {
        "alert": "KeyRotationBlock",
        "first_action": "Identify the affected user and key fingerprint from the alert metadata.",
        "escalate_to": "Security pod lead and affected user for emergency rotation."
      },
      {
        "alert": "KeyRotationOverdue",
        "first_action": "Notify user to complete key rotation immediately.",
        "escalate_to": "N/A \u2014 user action required."
      }
    ],
    "manual_overrides": [
      {
        "name": "Emergency key rotation bypass",
        "how": "polytraders admin rotate-key sec.key_rotation_reminder --user <user_id> --force",
        "when": "User is unable to complete rotation via normal flow; security review required."
      }
    ],
    "healthcheck": "GET /internal/health/keyrotationreminder \u2192 green if All active keys within rotate_every_days; no overdue alerts.; red if Any key older than rotate_every_days + block_on_overdue_h/24."
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "Unit tests pass for overdue block and cross-env reuse",
        "how_measured": "CI test run",
        "threshold": "100% pass"
      }
    ],
    "to_limited_live": [
      {
        "gate": "Overdue injection test fires DENY correctly",
        "how_measured": "Failure injection test",
        "threshold": "Pass"
      }
    ],
    "to_general_live": [
      {
        "gate": "Zero false-positive rotation blocks in 48h shadow",
        "how_measured": "Grafana KeyRotationBlock alert history",
        "threshold": "0 false positives"
      }
    ]
  },
  "reporting": {
    "emits_kinds": [
      "RiskVote"
    ],
    "topics": [
      "polytraders.reports.risk"
    ],
    "retention_class": "2y",
    "cadence": "every-event",
    "sampling_rule": "emit-every",
    "bus_failure_action": "fail-closed",
    "user_visible": "summary-only",
    "consumes_kinds": [
      "ObservationReport"
    ]
  },
  "capital_impact": "Direct",
  "mode_support": [
    "quarantine"
  ],
  "v3_status": {
    "phase": 5,
    "phase_name": "Execution rails",
    "docs": {
      "done": 27,
      "total": 27,
      "state": "done"
    },
    "impl": {
      "done": 0,
      "total": 15,
      "state": "pending"
    },
    "runtime": {
      "done": 0,
      "total": 8,
      "state": "pending"
    },
    "overall": "pending"
  }
}