{
  "agent_source": {},
  "assessment": {
    "backend_event_trigger_existing": false,
    "event_trigger_design_selected": true,
    "manual_agent_start_used": false,
    "private_material_published": false,
    "readonly_inventory_completed": true,
    "reconcile_design_required": true,
    "safe_to_implement_042c_after_review": true
  },
  "backend_source": {
    "candidate_files": [
      "/opt/wg-access/STATUS-STEP-014_AGENT_RECONCILE_SYNC_OK.txt",
      "/opt/wg-access/agent/wg_access_agent.py",
      "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532",
      "/opt/wg-access/backend/alembic/env.py",
      "/opt/wg-access/backend/alembic/versions/0001_initial_schema.py",
      "/opt/wg-access/backend/app/api/admin.py",
      "/opt/wg-access/backend/app/api/agent.py",
      "/opt/wg-access/backend/app/api/dev.py",
      "/opt/wg-access/backend/app/config.py",
      "/opt/wg-access/backend/app/db/base.py",
      "/opt/wg-access/backend/app/db/session.py",
      "/opt/wg-access/backend/app/main.py",
      "/opt/wg-access/backend/app/models/__init__.py",
      "/opt/wg-access/backend/app/models/core.py",
      "/opt/wg-access/backend/app/services/__init__.py",
      "/opt/wg-access/backend/app/services/wireguard.py",
      "/usr/local/sbin/wg-access-agent-job-selector-plan.py",
      "/usr/local/sbin/wg-access-agent-selector-apply.py",
      "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py",
      "/usr/local/sbin/wg-access-agent-selector-remote-plan.py",
      "/usr/local/sbin/wg-access-expire-subscriptions.sh",
      "/usr/local/sbin/wg-access-slot-allocator-dry-run.py"
    ],
    "grep_results": {
      "agent_apply_flow": [
        "/opt/wg-access/STATUS-STEP-014_AGENT_RECONCILE_SYNC_OK.txt:9:  VM100 wg_paid peers added with runtime wg set are not persistent across reboot,",
        "/opt/wg-access/STATUS-STEP-014_AGENT_RECONCILE_SYNC_OK.txt:17:    - public_key",
        "/opt/wg-access/STATUS-STEP-014_AGENT_RECONCILE_SYNC_OK.txt:18:    - preshared_key",
        "/opt/wg-access/STATUS-STEP-014_AGENT_RECONCILE_SYNC_OK.txt:26:         wg show wg_paid allowed-ips",
        "/opt/wg-access/STATUS-STEP-014_AGENT_RECONCILE_SYNC_OK.txt:28:         wg set wg_paid peer ... preshared-key ... allowed-ips ...",
        "/opt/wg-access/STATUS-STEP-014_AGENT_RECONCILE_SYNC_OK.txt:29:    4. Remove extra runtime peers that are not enabled in backend.",
        "/opt/wg-access/STATUS-STEP-014_AGENT_RECONCILE_SYNC_OK.txt:33:  1. Manually removed real peer from VM100 wg_paid.",
        "/opt/wg-access/STATUS-STEP-014_AGENT_RECONCILE_SYNC_OK.txt:38:  public_key: TdYIwRgGjStiQW1gyZjgsaPP+/6WE1U0NcmK0eVEVXM=",
        "/opt/wg-access/STATUS-STEP-014_AGENT_RECONCILE_SYNC_OK.txt:42:  sync desired enabled peers: 1",
        "/opt/wg-access/STATUS-STEP-014_AGENT_RECONCILE_SYNC_OK.txt:43:  sync current runtime peers: 0",
        "/opt/wg-access/STATUS-STEP-014_AGENT_RECONCILE_SYNC_OK.txt:50:  - VM100 wg_paid contains the enabled real peer.",
        "/opt/wg-access/agent/wg_access_agent.py:46:REMOTE_SSH_KEY = env[\"REMOTE_SSH_KEY\"]",
        "/opt/wg-access/agent/wg_access_agent.py:118:    public_key = require_wg_key(\"public_key\", peer[\"public_key\"])",
        "/opt/wg-access/agent/wg_access_agent.py:119:    preshared_key = require_wg_key(\"preshared_key\", peer[\"preshared_key\"])",
        "/opt/wg-access/agent/wg_access_agent.py:125:        \"public_key\": public_key,",
        "/opt/wg-access/agent/wg_access_agent.py:126:        \"preshared_key\": preshared_key,",
        "/opt/wg-access/agent/wg_access_agent.py:136:        \"ssh\",",
        "/opt/wg-access/agent/wg_access_agent.py:137:        \"-i\", REMOTE_SSH_KEY,",
        "/opt/wg-access/agent/wg_access_agent.py:169:    out = remote_wg(\"wg show \" + shlex.quote(REMOTE_WG_INTERFACE) + \" allowed-ips\")",
        "/opt/wg-access/agent/wg_access_agent.py:189:        \"wg set \"",
        "/opt/wg-access/agent/wg_access_agent.py:192:        + shlex.quote(peer[\"public_key\"])",
        "/opt/wg-access/agent/wg_access_agent.py:193:        + ' preshared-key \"$tmp\" allowed-ips '",
        "/opt/wg-access/agent/wg_access_agent.py:196:    remote_wg(script, peer[\"preshared_key\"] + \"\\n\")",
        "/opt/wg-access/agent/wg_access_agent.py:199:def remote_remove_peer(public_key):",
        "/opt/wg-access/agent/wg_access_agent.py:202:        + \"wg set \"",
        "/opt/wg-access/agent/wg_access_agent.py:205:        + shlex.quote(public_key)",
        "/opt/wg-access/agent/wg_access_agent.py:218:        desired[peer[\"public_key\"]] = peer",
        "/opt/wg-access/agent/wg_access_agent.py:220:    log(f\"sync desired enabled peers: {len(desired)}\")",
        "/opt/wg-access/agent/wg_access_agent.py:223:    log(f\"sync current runtime peers: {len(current)}\")",
        "/opt/wg-access/agent/wg_access_agent.py:225:    for public_key, peer in desired.items():",
        "/opt/wg-access/agent/wg_access_agent.py:226:        current_allowed = current.get(public_key)",
        "/opt/wg-access/agent/wg_access_agent.py:234:    for public_key in sorted(set(current.keys()) - set(desired.keys())):",
        "/opt/wg-access/agent/wg_access_agent.py:235:        remote_remove_peer(public_key)",
        "/opt/wg-access/agent/wg_access_agent.py:236:        log(f\"sync removed extra runtime peer public_key={public_key}\")",
        "/opt/wg-access/agent/wg_access_agent.py:243:    for public_key, peer in desired.items():",
        "/opt/wg-access/agent/wg_access_agent.py:244:        state[\"peers\"][public_key] = {",
        "/opt/wg-access/agent/wg_access_agent.py:246:            \"public_key\": public_key,",
        "/opt/wg-access/agent/wg_access_agent.py:247:            \"preshared_key\": peer[\"preshared_key\"],",
        "/opt/wg-access/agent/wg_access_agent.py:266:            \"public_key\": payload[\"public_key\"],",
        "/opt/wg-access/agent/wg_access_agent.py:267:            \"preshared_key\": payload[\"preshared_key\"],",
        "/opt/wg-access/agent/wg_access_agent.py:277:        state[\"peers\"][peer[\"public_key\"]] = {",
        "/opt/wg-access/agent/wg_access_agent.py:279:            \"public_key\": peer[\"public_key\"],",
        "/opt/wg-access/agent/wg_access_agent.py:280:            \"preshared_key\": peer[\"preshared_key\"],",
        "/opt/wg-access/agent/wg_access_agent.py:293:        public_key = require_wg_key(\"public_key\", payload[\"public_key\"])",
        "/opt/wg-access/agent/wg_access_agent.py:295:        remote_remove_peer(public_key)",
        "/opt/wg-access/agent/wg_access_agent.py:299:        if public_key not in state[\"peers\"]:",
        "/opt/wg-access/agent/wg_access_agent.py:300:            state[\"peers\"][public_key] = {",
        "/opt/wg-access/agent/wg_access_agent.py:302:                \"public_key\": public_key,",
        "/opt/wg-access/agent/wg_access_agent.py:305:        state[\"peers\"][public_key][\"enabled\"] = False",
        "/opt/wg-access/agent/wg_access_agent.py:306:        state[\"peers\"][public_key][\"last_action\"] = action",
        "/opt/wg-access/agent/wg_access_agent.py:307:        state[\"peers\"][public_key][\"last_job_id\"] = job[\"id\"]",
        "/opt/wg-access/agent/wg_access_agent.py:308:        state[\"peers\"][public_key][\"updated_at\"] = utc_now()",
        "/opt/wg-access/agent/wg_access_agent.py:318:    # Reconciliation first: VM100 wg_paid runtime state must match backend enabled peers.",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:46:REMOTE_SSH_KEY = env[\"REMOTE_SSH_KEY\"]",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:119:        \"ssh\",",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:120:        \"-i\", REMOTE_SSH_KEY,",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:159:        public_key = require_wg_key(\"public_key\", payload[\"public_key\"])",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:160:        preshared_key = require_wg_key(\"preshared_key\", payload[\"preshared_key\"])",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:169:            \"wg set \"",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:172:            + shlex.quote(public_key)",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:173:            + ' preshared-key \"$tmp\" allowed-ips '",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:178:        remote_wg(script, preshared_key + \"\\n\")",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:180:        state[\"peers\"][public_key] = {",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:182:            \"public_key\": public_key,",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:183:            \"preshared_key\": preshared_key,",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:196:        public_key = require_wg_key(\"public_key\", payload[\"public_key\"])",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:200:            \"wg set \"",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:203:            + shlex.quote(public_key)",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:210:        if public_key not in state[\"peers\"]:",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:211:            state[\"peers\"][public_key] = {",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:213:                \"public_key\": public_key,",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:216:        state[\"peers\"][public_key][\"enabled\"] = False",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:217:        state[\"peers\"][public_key][\"last_action\"] = action",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:218:        state[\"peers\"][public_key][\"last_job_id\"] = job[\"id\"]",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:219:        state[\"peers\"][public_key][\"updated_at\"] = utc_now()",
        "/opt/wg-access/backend/alembic/versions/0001_initial_schema.py:79:        sa.Column(\"public_key\", sa.String(length=128), nullable=False),",
        "/opt/wg-access/backend/alembic/versions/0001_initial_schema.py:80:        sa.Column(\"preshared_key\", sa.String(length=128), nullable=False),",
        "/opt/wg-access/backend/alembic/versions/0001_initial_schema.py:91:    op.create_unique_constraint(\"uq_peers_public_key\", \"peers\", [\"public_key\"])",
        "/opt/wg-access/backend/app/api/admin.py:67:    public_key: str",
        "/opt/wg-access/backend/app/api/admin.py:100:    private_key: str",
        "/opt/wg-access/backend/app/api/admin.py:101:    public_key: str",
        "/opt/wg-access/backend/app/api/admin.py:102:    preshared_key: str",
        "/opt/wg-access/backend/app/api/admin.py:128:    private_key, public_key = generate_wg_keypair()",
        "/opt/wg-access/backend/app/api/admin.py:129:    preshared_key = generate_wg_psk()",
        "/opt/wg-access/backend/app/api/admin.py:130:    client_config = build_client_config(private_key, tunnel_ip, preshared_key)",
        "/opt/wg-access/backend/app/api/admin.py:135:        public_key=public_key,",
        "/opt/wg-access/backend/app/api/admin.py:136:        preshared_key=preshared_key,",
        "/opt/wg-access/backend/app/api/admin.py:149:            \"public_key\": peer.public_key,",
        "/opt/wg-access/backend/app/api/admin.py:150:            \"preshared_key\": peer.preshared_key,",
        "/opt/wg-access/backend/app/api/admin.py:172:        private_key=private_key,",
        "/opt/wg-access/backend/app/api/admin.py:173:        public_key=peer.public_key,",
        "/opt/wg-access/backend/app/api/admin.py:174:        preshared_key=peer.preshared_key,",
        "/opt/wg-access/backend/app/api/admin.py:223:                    \"public_key\": peer.public_key,",
        "/opt/wg-access/backend/app/api/admin.py:286:                \"public_key\": peer.public_key,",
        "/opt/wg-access/backend/app/api/admin.py:314:    public_key: str",
        "/opt/wg-access/backend/app/api/admin.py:334:            \"public_key\": peer.public_key,",
        "/opt/wg-access/backend/app/api/admin.py:349:        public_key=peer.public_key,",
        "/opt/wg-access/backend/app/api/admin.py:393:    public_key: str",
        "/opt/wg-access/backend/app/api/agent.py:24:    public_key: str",
        "/opt/wg-access/backend/app/api/agent.py:25:    preshared_key: str",
        "/opt/wg-access/backend/app/api/dev.py:38:    private_key: str",
        "/opt/wg-access/backend/app/api/dev.py:39:    public_key: str",
        "/opt/wg-access/backend/app/api/dev.py:40:    preshared_key: str",
        "/opt/wg-access/backend/app/api/dev.py:60:    private_key, public_key = generate_wg_keypair()",
        "/opt/wg-access/backend/app/api/dev.py:61:    preshared_key = generate_wg_psk()",
        "/opt/wg-access/backend/app/api/dev.py:62:    client_config = build_client_config(private_key, tunnel_ip, preshared_key)",
        "/opt/wg-access/backend/app/api/dev.py:67:        public_key=public_key,",
        "/opt/wg-access/backend/app/api/dev.py:68:        preshared_key=preshared_key,",
        "/opt/wg-access/backend/app/api/dev.py:81:            \"public_key\": peer.public_key,",
        "/opt/wg-access/backend/app/api/dev.py:82:            \"preshared_key\": peer.preshared_key,",
        "/opt/wg-access/backend/app/api/dev.py:101:        private_key=private_key,",
        "/opt/wg-access/backend/app/api/dev.py:102:        public_key=peer.public_key,",
        "/opt/wg-access/backend/app/api/dev.py:103:        preshared_key=peer.preshared_key,",
        "/opt/wg-access/backend/app/models/core.py:92:    public_key: Mapped[str] = mapped_column(String(128), unique=True, nullable=False)",
        "/opt/wg-access/backend/app/models/core.py:93:    preshared_key: Mapped[str] = mapped_column(String(128), nullable=False)",
        "/opt/wg-access/backend/app/services/wireguard.py:26:    private_key = x25519.X25519PrivateKey.from_private_bytes(private_raw)",
        "/opt/wg-access/backend/app/services/wireguard.py:27:    public_key = private_key.public_key()",
        "/opt/wg-access/backend/app/services/wireguard.py:29:    public_raw = public_key.public_bytes(",
        "/opt/wg-access/backend/app/services/wireguard.py:77:def build_client_config(private_key: str, tunnel_ip: str, preshared_key: str) -> str:",
        "/opt/wg-access/backend/app/services/wireguard.py:78:    server_public_key = os.environ.get(\"WG_SERVER_PUBLIC_KEY\", \"\").strip()",
        "/opt/wg-access/backend/app/services/wireguard.py:83:    if not server_public_key:",
        "/opt/wg-access/backend/app/services/wireguard.py:84:        raise RuntimeError(\"WG_SERVER_PUBLIC_KEY is not configured\")",
        "/opt/wg-access/backend/app/services/wireguard.py:90:        f\"PrivateKey = {private_key}\",",
        "/opt/wg-access/backend/app/services/wireguard.py:100:        f\"PublicKey = {server_public_key}\",",
        "/opt/wg-access/backend/app/services/wireguard.py:101:        f\"PresharedKey = {preshared_key}\","
      ],
      "job_processing_flow": [
        "/opt/wg-access/STATUS-STEP-014_AGENT_RECONCILE_SYNC_OK.txt:6:Agent reconciliation / sync enabled peers completed.",
        "/opt/wg-access/STATUS-STEP-014_AGENT_RECONCILE_SYNC_OK.txt:30:    5. Then process pending provisioning jobs.",
        "/opt/wg-access/STATUS-STEP-014_AGENT_RECONCILE_SYNC_OK.txt:45:  no pending jobs",
        "/opt/wg-access/agent/wg_access_agent.py:163:        raise RuntimeError(f\"remote command failed rc={res.returncode}: {stderr or stdout}\")",
        "/opt/wg-access/agent/wg_access_agent.py:325:        log(\"no pending jobs\")",
        "/opt/wg-access/agent/wg_access_agent.py:328:    log(f\"fetched pending jobs: {len(jobs)}\")",
        "/opt/wg-access/agent/wg_access_agent.py:337:            completed = http_json(\"POST\", f\"/agent/jobs/{job_id}/complete\")",
        "/opt/wg-access/agent/wg_access_agent.py:338:            log(f\"completed job_id={completed['id']} status={completed['status']} attempts={completed['attempts']}\")",
        "/opt/wg-access/agent/wg_access_agent.py:341:            log(f\"failed job_id={job_id}: {err}\")",
        "/opt/wg-access/agent/wg_access_agent.py:343:                http_json(\"POST\", f\"/agent/jobs/{job_id}/fail\", {\"error\": err})",
        "/opt/wg-access/agent/wg_access_agent.py:344:            except Exception as fail_exc:",
        "/opt/wg-access/agent/wg_access_agent.py:345:                log(f\"failed to report failure job_id={job_id}: {fail_exc}\")",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:146:        raise RuntimeError(f\"remote command failed rc={res.returncode}: {stderr or stdout}\")",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:233:        log(\"no pending jobs\")",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:236:    log(f\"fetched pending jobs: {len(jobs)}\")",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:245:            completed = http_json(\"POST\", f\"/agent/jobs/{job_id}/complete\")",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:246:            log(f\"completed job_id={completed['id']} status={completed['status']} attempts={completed['attempts']}\")",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:249:            log(f\"failed job_id={job_id}: {err}\")",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:251:                http_json(\"POST\", f\"/agent/jobs/{job_id}/fail\", {\"error\": err})",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:252:            except Exception as fail_exc:",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:253:                log(f\"failed to report failure job_id={job_id}: {fail_exc}\")",
        "/opt/wg-access/backend/alembic/versions/0001_initial_schema.py:94:        \"provisioning_jobs\",",
        "/opt/wg-access/backend/alembic/versions/0001_initial_schema.py:104:        sa.Column(\"completed_at\", sa.DateTime(timezone=True), nullable=True),",
        "/opt/wg-access/backend/alembic/versions/0001_initial_schema.py:105:        sa.Column(\"last_error\", sa.Text(), nullable=True),",
        "/opt/wg-access/backend/alembic/versions/0001_initial_schema.py:107:    op.create_index(\"ix_provisioning_jobs_node_id\", \"provisioning_jobs\", [\"node_id\"])",
        "/opt/wg-access/backend/alembic/versions/0001_initial_schema.py:108:    op.create_index(\"ix_provisioning_jobs_status\", \"provisioning_jobs\", [\"status\"])",
        "/opt/wg-access/backend/alembic/versions/0001_initial_schema.py:109:    op.create_index(\"ix_provisioning_jobs_peer_id\", \"provisioning_jobs\", [\"peer_id\"])",
        "/opt/wg-access/backend/alembic/versions/0001_initial_schema.py:113:    op.drop_table(\"provisioning_jobs\")",
        "/opt/wg-access/backend/app/api/admin.py:109:    # It creates a user, an active subscription, a WG peer and a pending enable_peer job.",
        "/opt/wg-access/backend/app/api/admin.py:154:        status=\"pending\",",
        "/opt/wg-access/backend/app/api/admin.py:226:                status=\"pending\",",
        "/opt/wg-access/backend/app/api/admin.py:289:            status=\"pending\",",
        "/opt/wg-access/backend/app/api/admin.py:337:        status=\"pending\",",
        "/opt/wg-access/backend/app/api/admin.py:363:    last_error: str | None",
        "/opt/wg-access/backend/app/api/admin.py:412:    last_error: str | None",
        "/opt/wg-access/backend/app/api/agent.py:64:class FailRequest(BaseModel):",
        "/opt/wg-access/backend/app/api/agent.py:69:def get_pending_jobs(",
        "/opt/wg-access/backend/app/api/agent.py:78:        .where(ProvisioningJob.status == \"pending\")",
        "/opt/wg-access/backend/app/api/agent.py:86:def start_job(",
        "/opt/wg-access/backend/app/api/agent.py:94:    if job.status != \"pending\":",
        "/opt/wg-access/backend/app/api/agent.py:97:    job.status = \"running\"",
        "/opt/wg-access/backend/app/api/agent.py:106:@router.post(\"/jobs/{job_id}/complete\", response_model=AgentJobResponse)",
        "/opt/wg-access/backend/app/api/agent.py:107:def complete_job(",
        "/opt/wg-access/backend/app/api/agent.py:115:    if job.status not in (\"pending\", \"running\"):",
        "/opt/wg-access/backend/app/api/agent.py:118:    job.status = \"completed\"",
        "/opt/wg-access/backend/app/api/agent.py:119:    job.completed_at = datetime.now(timezone.utc)",
        "/opt/wg-access/backend/app/api/agent.py:120:    job.last_error = None",
        "/opt/wg-access/backend/app/api/agent.py:127:@router.post(\"/jobs/{job_id}/fail\", response_model=AgentJobResponse)",
        "/opt/wg-access/backend/app/api/agent.py:128:def fail_job(",
        "/opt/wg-access/backend/app/api/agent.py:130:    payload: FailRequest,",
        "/opt/wg-access/backend/app/api/agent.py:137:    if job.status not in (\"pending\", \"running\"):",
        "/opt/wg-access/backend/app/api/agent.py:140:    job.status = \"failed\"",
        "/opt/wg-access/backend/app/api/agent.py:141:    job.completed_at = datetime.now(timezone.utc)",
        "/opt/wg-access/backend/app/api/agent.py:142:    job.last_error = payload.error",
        "/opt/wg-access/backend/app/api/dev.py:86:        status=\"pending\",",
        "/opt/wg-access/backend/app/models/core.py:106:    __tablename__ = \"provisioning_jobs\"",
        "/opt/wg-access/backend/app/models/core.py:115:    status: Mapped[str] = mapped_column(String(32), default=\"pending\", nullable=False, index=True)",
        "/opt/wg-access/backend/app/models/core.py:120:    completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)",
        "/opt/wg-access/backend/app/models/core.py:121:    last_error: Mapped[str | None] = mapped_column(Text, nullable=True)",
        "/usr/local/sbin/wg-access-expire-subscriptions.sh:2:set -euo pipefail",
        "/usr/local/sbin/wg-access-expire-subscriptions.sh:48:    *) echo \"ERROR: expire-subscriptions failed\"; exit 1 ;;"
      ],
      "reconcile_flow": [
        "/opt/wg-access/STATUS-STEP-014_AGENT_RECONCILE_SYNC_OK.txt:29:    4. Remove extra runtime peers that are not enabled in backend.",
        "/opt/wg-access/STATUS-STEP-014_AGENT_RECONCILE_SYNC_OK.txt:42:  sync desired enabled peers: 1",
        "/opt/wg-access/STATUS-STEP-014_AGENT_RECONCILE_SYNC_OK.txt:43:  sync current runtime peers: 0",
        "/opt/wg-access/agent/wg_access_agent.py:220:    log(f\"sync desired enabled peers: {len(desired)}\")",
        "/opt/wg-access/agent/wg_access_agent.py:223:    log(f\"sync current runtime peers: {len(current)}\")",
        "/usr/local/sbin/wg-access-agent-job-selector-plan.py:14:    selector = payload.get(\"selector\", {})",
        "/usr/local/sbin/wg-access-agent-job-selector-plan.py:23:        \"slot_code\": selector.get(\"slot_code\", payload.get(\"slot_code\", \"\")),",
        "/usr/local/sbin/wg-access-agent-job-selector-plan.py:24:        \"selector_class\": selector.get(\"selector_class\", payload.get(\"selector_class\", \"\")),",
        "/usr/local/sbin/wg-access-agent-job-selector-plan.py:25:        \"selector_label\": selector.get(\"selector_label\", payload.get(\"selector_label\", \"\")),",
        "/usr/local/sbin/wg-access-agent-job-selector-plan.py:26:        \"fwmark\": selector.get(\"fwmark\", payload.get(\"fwmark\", \"\")),",
        "/usr/local/sbin/wg-access-agent-job-selector-plan.py:27:        \"table_id\": selector.get(\"table_id\", payload.get(\"table_id\", \"\")),",
        "/usr/local/sbin/wg-access-agent-job-selector-plan.py:28:        \"interface_name\": selector.get(\"interface_name\", payload.get(\"interface_name\", \"\")),",
        "/usr/local/sbin/wg-access-agent-job-selector-plan.py:36:    parser.add_argument(\"--selector-file\", required=True)",
        "/usr/local/sbin/wg-access-agent-job-selector-plan.py:38:    parser.add_argument(\"--selector-helper\", default=\"/usr/local/sbin/wg-access-agent-selector-apply.py\")",
        "/usr/local/sbin/wg-access-agent-job-selector-plan.py:47:        \"schema\": \"wg-access-agent-job-selector-plan-v1\",",
        "/usr/local/sbin/wg-access-agent-job-selector-plan.py:56:        \"selector_helper\": args.selector_helper,",
        "/usr/local/sbin/wg-access-agent-job-selector-plan.py:57:        \"selector_plan_rc\": None,",
        "/usr/local/sbin/wg-access-agent-job-selector-plan.py:58:        \"selector_plan\": None,",
        "/usr/local/sbin/wg-access-agent-job-selector-plan.py:67:        args.selector_helper,",
        "/usr/local/sbin/wg-access-agent-job-selector-plan.py:69:        \"--selector-file\", args.selector_file,",
        "/usr/local/sbin/wg-access-agent-job-selector-plan.py:76:    result[\"selector_plan_rc\"] = proc.returncode",
        "/usr/local/sbin/wg-access-agent-job-selector-plan.py:77:    result[\"selector_plan_stdout\"] = proc.stdout",
        "/usr/local/sbin/wg-access-agent-job-selector-plan.py:78:    result[\"selector_plan_stderr\"] = proc.stderr",
        "/usr/local/sbin/wg-access-agent-job-selector-plan.py:86:            \"errors\": [\"selector_plan_json_parse_error:\" + type(e).__name__],",
        "/usr/local/sbin/wg-access-agent-job-selector-plan.py:89:    result[\"selector_plan\"] = plan",
        "/usr/local/sbin/wg-access-agent-job-selector-plan.py:94:    result[\"selector_entry_count_before\"] = plan.get(\"selector_entry_count_before\")",
        "/usr/local/sbin/wg-access-agent-job-selector-plan.py:95:    result[\"selector_entry_count_after_planned\"] = plan.get(\"selector_entry_count_after_planned\")",
        "/usr/local/sbin/wg-access-agent-job-selector-plan.py:105:        print(\"selector_plan_rc=\" + str(result[\"selector_plan_rc\"]))",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:9:    \"vpn1\": {\"selector_class\": \"cs4\", \"selector_label\": \"canary_vpn1\", \"fwmark\": \"0x201\", \"table_id\": 201, \"interface_name\": \"vpn1\"},",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:10:    \"vpn2\": {\"selector_class\": \"cs5\", \"selector_label\": \"canary_vpn2\", \"fwmark\": \"0x202\", \"table_id\": 202, \"interface_name\": \"vpn2\"},",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:11:    \"vpn3\": {\"selector_class\": \"cs1\", \"selector_label\": \"canary_vpn3\", \"fwmark\": \"0x203\", \"table_id\": 203, \"interface_name\": \"vpn3\"},",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:12:    \"vpn4\": {\"selector_class\": \"cs2\", \"selector_label\": \"canary_vpn4\", \"fwmark\": \"0x204\", \"table_id\": 204, \"interface_name\": \"vpn4\"},",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:13:    \"vpn5\": {\"selector_class\": \"cs3\", \"selector_label\": \"canary_vpn5\", \"fwmark\": \"0x205\", \"table_id\": 205, \"interface_name\": \"vpn5\"},",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:19:def parse_selector(path):",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:35:            \"selector_class\": parts[1],",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:36:            \"selector_label\": parts[2],",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:44:def validate_and_plan(payload, selector_entries, slots, require_peer_visible=False, peer_visible=False):",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:50:    selector_class = str(payload.get(\"selector_class\", \"\"))",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:51:    selector_label = str(payload.get(\"selector_label\", \"\"))",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:65:        for key in (\"selector_class\", \"selector_label\", \"fwmark\", \"table_id\", \"interface_name\"):",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:75:        for key in (\"selector_class\", \"fwmark\", \"table_id\", \"interface_name\"):",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:82:    exact_line = f\"{tunnel_ip} {selector_class} {selector_label}\"",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:84:    valid_entries = [entry for entry in selector_entries if entry.get(\"valid\")]",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:93:        if entry.get(\"selector_class\") == selector_class and entry[\"raw\"] != exact_line",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:97:        if entry.get(\"selector_label\") == selector_label and entry[\"raw\"] != exact_line",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:103:        errors.append(\"same_selector_class_conflict\")",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:105:        errors.append(\"same_selector_label_conflict\")",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:115:        \"schema\": \"wg-access-agent-selector-apply-plan-v1\",",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:121:        \"selector_entry_count_before\": len(valid_entries),",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:122:        \"selector_entry_count_after_planned\": len(valid_entries) + (1 if action == \"append\" else 0),",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:127:        \"same_selector_class_conflict_count\": len(same_class_conflicts),",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:128:        \"same_selector_label_conflict_count\": len(same_label_conflicts),",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:135:            \"selector_class\": selector_class,",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:136:            \"selector_label\": selector_label,",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:148:    parser.add_argument(\"--selector-file\", required=True)",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:155:    selector_entries = parse_selector(Path(args.selector_file))",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:158:    plan = validate_and_plan(payload, selector_entries, slots)",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:167:            selector_path = Path(args.selector_file)",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:168:            text = selector_path.read_text(encoding=\"utf-8\", errors=\"replace\")",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:172:            selector_path.write_text(text, encoding=\"utf-8\")",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:9:    \"vpn1\": {\"selector_class\": \"cs4\", \"selector_label\": \"canary_vpn1\", \"fwmark\": \"0x201\", \"table_id\": 201, \"interface_name\": \"vpn1\"},",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:10:    \"vpn2\": {\"selector_class\": \"cs5\", \"selector_label\": \"canary_vpn2\", \"fwmark\": \"0x202\", \"table_id\": 202, \"interface_name\": \"vpn2\"},",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:11:    \"vpn3\": {\"selector_class\": \"cs1\", \"selector_label\": \"canary_vpn3\", \"fwmark\": \"0x203\", \"table_id\": 203, \"interface_name\": \"vpn3\"},",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:12:    \"vpn4\": {\"selector_class\": \"cs2\", \"selector_label\": \"canary_vpn4\", \"fwmark\": \"0x204\", \"table_id\": 204, \"interface_name\": \"vpn4\"},",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:13:    \"vpn5\": {\"selector_class\": \"cs3\", \"selector_label\": \"canary_vpn5\", \"fwmark\": \"0x205\", \"table_id\": 205, \"interface_name\": \"vpn5\"},",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:16:def load_selector(path: Path):",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:30:            \"selector_class\": parts[1],",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:31:            \"selector_label\": parts[2],",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:45:    ap.add_argument(\"--selector-file\", required=True)",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:51:    entries = load_selector(Path(args.selector_file))",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:59:    selector_class = str(payload.get(\"selector_class\", \"\"))",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:60:    selector_label = str(payload.get(\"selector_label\", \"\"))",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:75:        for key in (\"selector_class\", \"selector_label\", \"fwmark\", \"table_id\", \"interface_name\"):",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:85:        for key in (\"selector_class\", \"fwmark\", \"table_id\", \"interface_name\"):",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:89:    exact_line = f\"{tunnel_ip} {selector_class} {selector_label}\"",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:100:        if e.get(\"selector_class\") == selector_class and e[\"raw\"] != exact_line",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:104:        if e.get(\"selector_label\") == selector_label and e[\"raw\"] != exact_line",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:110:        errors.append(\"same_selector_class_conflict\")",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:112:        errors.append(\"same_selector_label_conflict\")",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:120:        \"schema\": \"wg-access-agent-selector-contract-dry-run-v1\",",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:126:        \"selector_entry_count_before\": len(valid_entries),",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:127:        \"selector_entry_count_after_simulated\": simulated_entries,",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:134:        \"same_selector_class_conflict_count\": len(same_class_conflicts),",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:135:        \"same_selector_label_conflict_count\": len(same_label_conflicts),",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:15:def build_remote_script(payload, selector_plan, selector_conf, selector_service):",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:16:    exact_line = selector_plan.get(\"exact_line\", \"\")",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:17:    action = selector_plan.get(\"action\", \"\")",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:19:    selector_class = str(payload.get(\"selector_class\", \"\"))",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:24:  echo \"selector_apply_action=noop_already_present\"",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:27:  echo \"selector_apply_action=append\"",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:33:echo \"selector_apply_action=noop_verified_present\"",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:37:echo \"selector_apply_action=error_invalid_plan\"",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:44:CONF={q(selector_conf)}",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:45:SERVICE={q(selector_service)}",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:48:SELECTOR_CLASS={q(selector_class)}",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:49:BACKUP=\"/root/peers.conf.before-agent-selector-apply-$(date +%Y%m%d-%H%M%S)\"",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:50:ROLLBACK=\"/root/rollback-agent-selector-apply.sh\"",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:52:echo \"remote_script_schema=wg-access-agent-selector-remote-script-v3\"",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:58:echo \"selector_backup_path=$BACKUP\"",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:75:nft -a list ruleset 2>/dev/null | grep -q \"$TUNNEL_IP.*$SELECTOR_CLASS\"",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:77:echo \"postcheck_selector_line_present=YES\"",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:78:echo \"postcheck_selector_service_enabled=YES\"",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:86:    parser.add_argument(\"--selector-file\", required=True)",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:88:    parser.add_argument(\"--selector-helper\", default=\"/usr/local/sbin/wg-access-agent-selector-apply.py\")",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:91:    parser.add_argument(\"--selector-conf\", default=\"/etc/router-wgpay-selector.d/peers.conf\")",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:92:    parser.add_argument(\"--selector-service\", default=\"router-wgpay-canary\")",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:100:        args.selector_helper,",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:102:        \"--selector-file\", args.selector_file,",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:111:        selector_plan = json.loads(proc.stdout)",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:113:        selector_plan = {",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:116:            \"errors\": [\"selector_plan_json_parse_error:\" + type(e).__name__],",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:119:    contract_valid = bool(selector_plan.get(\"contract_valid\", False))",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:120:    action = selector_plan.get(\"action\", \"\")",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:121:    remote_script = build_remote_script(payload, selector_plan, args.selector_conf, args.selector_service)",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:124:        \"schema\": \"wg-access-agent-selector-remote-plan-v3\",",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:130:            \"selector_conf\": args.selector_conf,",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:131:            \"selector_service\": args.selector_service",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:136:            \"selector_class\": payload.get(\"selector_class\", \"\"),",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:137:            \"selector_label\": payload.get(\"selector_label\", \"\"),",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:144:        \"selector_plan_rc\": proc.returncode,",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:145:        \"selector_plan\": selector_plan,",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:148:        \"would_append\": bool(selector_plan.get(\"would_append\", False)),",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:149:        \"idempotent_noop\": bool(selector_plan.get(\"idempotent_noop\", False)),",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:150:        \"exact_line\": selector_plan.get(\"exact_line\", \"\"),",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:163:        \"errors\": selector_plan.get(\"errors\", []),",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:164:        \"error_count\": len(selector_plan.get(\"errors\", [])),",
        "/usr/local/sbin/wg-access-slot-allocator-dry-run.py:10:def parse_selector(path: Path):",
        "/usr/local/sbin/wg-access-slot-allocator-dry-run.py:28:        selector_class = parts[1]",
        "/usr/local/sbin/wg-access-slot-allocator-dry-run.py:31:        used_classes.add(selector_class)",
        "/usr/local/sbin/wg-access-slot-allocator-dry-run.py:39:            \"selector_class\": selector_class,",
        "/usr/local/sbin/wg-access-slot-allocator-dry-run.py:45:def choose_slot(status_json: Path, selector_file: Path, strategy: str):",
        "/usr/local/sbin/wg-access-slot-allocator-dry-run.py:49:    used_classes, used_slots_by_label, selector_entries = parse_selector(selector_file)",
        "/usr/local/sbin/wg-access-slot-allocator-dry-run.py:56:        cls = slot.get(\"selector_class\", \"\")",
        "/usr/local/sbin/wg-access-slot-allocator-dry-run.py:67:            reason.append(\"selector_class_already_assigned\")",
        "/usr/local/sbin/wg-access-slot-allocator-dry-run.py:74:                \"selector_class\": cls,",
        "/usr/local/sbin/wg-access-slot-allocator-dry-run.py:99:        \"selector_entry_count\": len(selector_entries),",
        "/usr/local/sbin/wg-access-slot-allocator-dry-run.py:100:        \"used_selector_classes\": sorted(used_classes),",
        "/usr/local/sbin/wg-access-slot-allocator-dry-run.py:111:    ap.add_argument(\"--selector-file\", required=True)",
        "/usr/local/sbin/wg-access-slot-allocator-dry-run.py:117:        Path(args.selector_file),"
      ],
      "selector_slot_flow": [
        "/usr/local/sbin/wg-access-agent-job-selector-plan.py:23:        \"slot_code\": selector.get(\"slot_code\", payload.get(\"slot_code\", \"\")),",
        "/usr/local/sbin/wg-access-agent-job-selector-plan.py:24:        \"selector_class\": selector.get(\"selector_class\", payload.get(\"selector_class\", \"\")),",
        "/usr/local/sbin/wg-access-agent-job-selector-plan.py:25:        \"selector_label\": selector.get(\"selector_label\", payload.get(\"selector_label\", \"\")),",
        "/usr/local/sbin/wg-access-agent-job-selector-plan.py:26:        \"fwmark\": selector.get(\"fwmark\", payload.get(\"fwmark\", \"\")),",
        "/usr/local/sbin/wg-access-agent-job-selector-plan.py:27:        \"table_id\": selector.get(\"table_id\", payload.get(\"table_id\", \"\")),",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:9:    \"vpn1\": {\"selector_class\": \"cs4\", \"selector_label\": \"canary_vpn1\", \"fwmark\": \"0x201\", \"table_id\": 201, \"interface_name\": \"vpn1\"},",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:10:    \"vpn2\": {\"selector_class\": \"cs5\", \"selector_label\": \"canary_vpn2\", \"fwmark\": \"0x202\", \"table_id\": 202, \"interface_name\": \"vpn2\"},",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:11:    \"vpn3\": {\"selector_class\": \"cs1\", \"selector_label\": \"canary_vpn3\", \"fwmark\": \"0x203\", \"table_id\": 203, \"interface_name\": \"vpn3\"},",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:12:    \"vpn4\": {\"selector_class\": \"cs2\", \"selector_label\": \"canary_vpn4\", \"fwmark\": \"0x204\", \"table_id\": 204, \"interface_name\": \"vpn4\"},",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:13:    \"vpn5\": {\"selector_class\": \"cs3\", \"selector_label\": \"canary_vpn5\", \"fwmark\": \"0x205\", \"table_id\": 205, \"interface_name\": \"vpn5\"},",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:35:            \"selector_class\": parts[1],",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:36:            \"selector_label\": parts[2],",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:42:    return {slot.get(\"slot_code\"): slot for slot in data.get(\"slots\", [])}",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:49:    slot_code = str(payload.get(\"slot_code\", \"\"))",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:50:    selector_class = str(payload.get(\"selector_class\", \"\"))",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:51:    selector_label = str(payload.get(\"selector_label\", \"\"))",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:52:    fwmark = str(payload.get(\"fwmark\", \"\"))",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:53:    table_id = payload.get(\"table_id\")",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:61:    expected = EXPECTED.get(slot_code)",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:63:        errors.append(\"unknown_slot_code\")",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:65:        for key in (\"selector_class\", \"selector_label\", \"fwmark\", \"table_id\", \"interface_name\"):",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:69:    slot = slots.get(slot_code)",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:75:        for key in (\"selector_class\", \"fwmark\", \"table_id\", \"interface_name\"):",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:82:    exact_line = f\"{tunnel_ip} {selector_class} {selector_label}\"",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:93:        if entry.get(\"selector_class\") == selector_class and entry[\"raw\"] != exact_line",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:97:        if entry.get(\"selector_label\") == selector_label and entry[\"raw\"] != exact_line",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:103:        errors.append(\"same_selector_class_conflict\")",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:105:        errors.append(\"same_selector_label_conflict\")",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:127:        \"same_selector_class_conflict_count\": len(same_class_conflicts),",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:128:        \"same_selector_label_conflict_count\": len(same_label_conflicts),",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:134:            \"slot_code\": slot_code,",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:135:            \"selector_class\": selector_class,",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:136:            \"selector_label\": selector_label,",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:137:            \"fwmark\": fwmark,",
        "/usr/local/sbin/wg-access-agent-selector-apply.py:138:            \"table_id\": table_id,",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:9:    \"vpn1\": {\"selector_class\": \"cs4\", \"selector_label\": \"canary_vpn1\", \"fwmark\": \"0x201\", \"table_id\": 201, \"interface_name\": \"vpn1\"},",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:10:    \"vpn2\": {\"selector_class\": \"cs5\", \"selector_label\": \"canary_vpn2\", \"fwmark\": \"0x202\", \"table_id\": 202, \"interface_name\": \"vpn2\"},",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:11:    \"vpn3\": {\"selector_class\": \"cs1\", \"selector_label\": \"canary_vpn3\", \"fwmark\": \"0x203\", \"table_id\": 203, \"interface_name\": \"vpn3\"},",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:12:    \"vpn4\": {\"selector_class\": \"cs2\", \"selector_label\": \"canary_vpn4\", \"fwmark\": \"0x204\", \"table_id\": 204, \"interface_name\": \"vpn4\"},",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:13:    \"vpn5\": {\"selector_class\": \"cs3\", \"selector_label\": \"canary_vpn5\", \"fwmark\": \"0x205\", \"table_id\": 205, \"interface_name\": \"vpn5\"},",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:30:            \"selector_class\": parts[1],",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:31:            \"selector_label\": parts[2],",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:37:    return {s.get(\"slot_code\"): s for s in data.get(\"slots\", [])}",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:58:    slot_code = str(payload.get(\"slot_code\", \"\"))",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:59:    selector_class = str(payload.get(\"selector_class\", \"\"))",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:60:    selector_label = str(payload.get(\"selector_label\", \"\"))",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:61:    fwmark = str(payload.get(\"fwmark\", \"\"))",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:62:    table_id = payload.get(\"table_id\")",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:71:    expected = EXPECTED.get(slot_code)",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:73:        errors.append(\"unknown_slot_code\")",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:75:        for key in (\"selector_class\", \"selector_label\", \"fwmark\", \"table_id\", \"interface_name\"):",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:79:    slot_status = slots.get(slot_code)",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:85:        for key in (\"selector_class\", \"fwmark\", \"table_id\", \"interface_name\"):",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:89:    exact_line = f\"{tunnel_ip} {selector_class} {selector_label}\"",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:100:        if e.get(\"selector_class\") == selector_class and e[\"raw\"] != exact_line",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:104:        if e.get(\"selector_label\") == selector_label and e[\"raw\"] != exact_line",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:110:        errors.append(\"same_selector_class_conflict\")",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:112:        errors.append(\"same_selector_label_conflict\")",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:134:        \"same_selector_class_conflict_count\": len(same_class_conflicts),",
        "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py:135:        \"same_selector_label_conflict_count\": len(same_label_conflicts),",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:19:    selector_class = str(payload.get(\"selector_class\", \"\"))",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:48:SELECTOR_CLASS={q(selector_class)}",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:75:nft -a list ruleset 2>/dev/null | grep -q \"$TUNNEL_IP.*$SELECTOR_CLASS\"",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:135:            \"slot_code\": payload.get(\"slot_code\", \"\"),",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:136:            \"selector_class\": payload.get(\"selector_class\", \"\"),",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:137:            \"selector_label\": payload.get(\"selector_label\", \"\"),",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:138:            \"fwmark\": payload.get(\"fwmark\", \"\"),",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:139:            \"table_id\": payload.get(\"table_id\", \"\"),",
        "/usr/local/sbin/wg-access-slot-allocator-dry-run.py:28:        selector_class = parts[1]",
        "/usr/local/sbin/wg-access-slot-allocator-dry-run.py:31:        used_classes.add(selector_class)",
        "/usr/local/sbin/wg-access-slot-allocator-dry-run.py:39:            \"selector_class\": selector_class,",
        "/usr/local/sbin/wg-access-slot-allocator-dry-run.py:55:        code = slot.get(\"slot_code\", \"\")",
        "/usr/local/sbin/wg-access-slot-allocator-dry-run.py:56:        cls = slot.get(\"selector_class\", \"\")",
        "/usr/local/sbin/wg-access-slot-allocator-dry-run.py:67:            reason.append(\"selector_class_already_assigned\")",
        "/usr/local/sbin/wg-access-slot-allocator-dry-run.py:73:                \"slot_code\": code,",
        "/usr/local/sbin/wg-access-slot-allocator-dry-run.py:74:                \"selector_class\": cls,",
        "/usr/local/sbin/wg-access-slot-allocator-dry-run.py:82:        candidates.sort(key=lambda s: ORDER.index(s.get(\"slot_code\")) if s.get(\"slot_code\") in ORDER else 999)",
        "/usr/local/sbin/wg-access-slot-allocator-dry-run.py:87:                ORDER.index(s.get(\"slot_code\")) if s.get(\"slot_code\") in ORDER else 999,",
        "/usr/local/sbin/wg-access-slot-allocator-dry-run.py:100:        \"used_selector_classes\": sorted(used_classes),"
      ],
      "subscription_create_flow": [
        "/opt/wg-access/agent/wg_access_agent.py:262:    if action == \"enable_peer\":",
        "/opt/wg-access/agent/wg_access_agent.py:289:        log(f\"REAL enable_peer peer_id={peer_id} tunnel_ip={peer['tunnel_ip']}\")",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:158:    if action == \"enable_peer\":",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:192:        log(f\"REAL enable_peer peer_id={peer_id} tunnel_ip={tunnel_ip}\")",
        "/opt/wg-access/backend/alembic/versions/0001_initial_schema.py:94:        \"provisioning_jobs\",",
        "/opt/wg-access/backend/alembic/versions/0001_initial_schema.py:107:    op.create_index(\"ix_provisioning_jobs_node_id\", \"provisioning_jobs\", [\"node_id\"])",
        "/opt/wg-access/backend/alembic/versions/0001_initial_schema.py:108:    op.create_index(\"ix_provisioning_jobs_status\", \"provisioning_jobs\", [\"status\"])",
        "/opt/wg-access/backend/alembic/versions/0001_initial_schema.py:109:    op.create_index(\"ix_provisioning_jobs_peer_id\", \"provisioning_jobs\", [\"peer_id\"])",
        "/opt/wg-access/backend/alembic/versions/0001_initial_schema.py:113:    op.drop_table(\"provisioning_jobs\")",
        "/opt/wg-access/backend/app/api/admin.py:12:from app.models import InviteCode, User, Subscription, Peer, ProvisioningJob",
        "/opt/wg-access/backend/app/api/admin.py:106:@router.post(\"/subscriptions\", response_model=SubscriptionCreateResponse)",
        "/opt/wg-access/backend/app/api/admin.py:107:def create_subscription(payload: SubscriptionCreateRequest, db: Session = Depends(get_db)):",
        "/opt/wg-access/backend/app/api/admin.py:109:    # It creates a user, an active subscription, a WG peer and a pending enable_peer job.",
        "/opt/wg-access/backend/app/api/admin.py:144:    job = ProvisioningJob(",
        "/opt/wg-access/backend/app/api/admin.py:146:        action=\"enable_peer\",",
        "/opt/wg-access/backend/app/api/admin.py:188:@router.post(\"/maintenance/expire-subscriptions\", response_model=ExpireSubscriptionsResponse)",
        "/opt/wg-access/backend/app/api/admin.py:189:def expire_subscriptions(db: Session = Depends(get_db)):",
        "/opt/wg-access/backend/app/api/admin.py:218:            job = ProvisioningJob(",
        "/opt/wg-access/backend/app/api/admin.py:254:@router.post(\"/subscriptions/{subscription_id}/cancel\", response_model=CancelSubscriptionResponse)",
        "/opt/wg-access/backend/app/api/admin.py:255:def cancel_subscription(subscription_id: UUID, db: Session = Depends(get_db)):",
        "/opt/wg-access/backend/app/api/admin.py:281:        job = ProvisioningJob(",
        "/opt/wg-access/backend/app/api/admin.py:329:    job = ProvisioningJob(",
        "/opt/wg-access/backend/app/api/admin.py:371:    rows = db.execute(select(ProvisioningJob).order_by(ProvisioningJob.created_at.desc())).scalars().all()",
        "/opt/wg-access/backend/app/api/admin.py:481:            select(ProvisioningJob)",
        "/opt/wg-access/backend/app/api/admin.py:482:            .where(ProvisioningJob.peer_id.in_(peer_ids))",
        "/opt/wg-access/backend/app/api/admin.py:483:            .order_by(ProvisioningJob.created_at.desc())",
        "/opt/wg-access/backend/app/api/agent.py:11:from app.models import Peer, ProvisioningJob",
        "/opt/wg-access/backend/app/api/agent.py:76:        select(ProvisioningJob)",
        "/opt/wg-access/backend/app/api/agent.py:77:        .where(ProvisioningJob.node_id == node_id)",
        "/opt/wg-access/backend/app/api/agent.py:78:        .where(ProvisioningJob.status == \"pending\")",
        "/opt/wg-access/backend/app/api/agent.py:79:        .order_by(ProvisioningJob.created_at.asc())",
        "/opt/wg-access/backend/app/api/agent.py:91:    job = db.get(ProvisioningJob, job_id)",
        "/opt/wg-access/backend/app/api/agent.py:112:    job = db.get(ProvisioningJob, job_id)",
        "/opt/wg-access/backend/app/api/agent.py:134:    job = db.get(ProvisioningJob, job_id)",
        "/opt/wg-access/backend/app/api/dev.py:10:from app.models import User, Peer, ProvisioningJob",
        "/opt/wg-access/backend/app/api/dev.py:76:    job = ProvisioningJob(",
        "/opt/wg-access/backend/app/api/dev.py:78:        action=\"enable_peer\",",
        "/opt/wg-access/backend/app/models/__init__.py:7:    ProvisioningJob,",
        "/opt/wg-access/backend/app/models/__init__.py:16:    \"ProvisioningJob\",",
        "/opt/wg-access/backend/app/models/core.py:105:class ProvisioningJob(Base):",
        "/opt/wg-access/backend/app/models/core.py:106:    __tablename__ = \"provisioning_jobs\""
      ],
      "trigger_possible_existing": [
        "/opt/wg-access/STATUS-STEP-014_AGENT_RECONCILE_SYNC_OK.txt:51:  - wg-access-agent.timer remains disabled.",
        "/opt/wg-access/STATUS-STEP-014_AGENT_RECONCILE_SYNC_OK.txt:56:  - then re-enable timer",
        "/opt/wg-access/agent/wg_access_agent.py:8:import subprocess",
        "/opt/wg-access/agent/wg_access_agent.py:145:    res = subprocess.run(",
        "/opt/wg-access/agent/wg_access_agent.py:148:        stdout=subprocess.PIPE,",
        "/opt/wg-access/agent/wg_access_agent.py:149:        stderr=subprocess.PIPE,",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:8:import subprocess",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:128:    res = subprocess.run(",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:131:        stdout=subprocess.PIPE,",
        "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532:132:        stderr=subprocess.PIPE,",
        "/opt/wg-access/backend/app/api/admin.py:13:from app.services.wireguard import (",
        "/opt/wg-access/backend/app/api/dev.py:11:from app.services.wireguard import (",
        "/opt/wg-access/backend/app/main.py:21:        \"service\": \"wg-access-backend\",",
        "/opt/wg-access/backend/app/services/__init__.py:1:# service package",
        "/usr/local/sbin/wg-access-agent-job-selector-plan.py:4:import subprocess",
        "/usr/local/sbin/wg-access-agent-job-selector-plan.py:75:    proc = subprocess.run(cmd, text=True, capture_output=True)",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:6:import subprocess",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:15:def build_remote_script(payload, selector_plan, selector_conf, selector_service):",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:45:SERVICE={q(selector_service)}",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:64:/etc/init.d/$SERVICE start",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:71:/etc/init.d/$SERVICE start",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:74:/etc/init.d/$SERVICE enabled >/dev/null 2>&1",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:78:echo \"postcheck_selector_service_enabled=YES\"",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:92:    parser.add_argument(\"--selector-service\", default=\"router-wgpay-canary\")",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:108:    proc = subprocess.run(helper_cmd, text=True, capture_output=True)",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:121:    remote_script = build_remote_script(payload, selector_plan, args.selector_conf, args.selector_service)",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:131:            \"selector_service\": args.selector_service",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:160:        \"remote_script_contains_bad_etc_initd_quoted_service\": '/etc/init.d \"$SERVICE\"' in remote_script,",
        "/usr/local/sbin/wg-access-agent-selector-remote-plan.py:161:        \"remote_script_contains_clean_service_start\": \"/etc/init.d/$SERVICE start\" in remote_script,"
      ]
    },
    "selected_file_texts": {
      "/opt/wg-access/STATUS-STEP-014_AGENT_RECONCILE_SYNC_OK.txt": {
        "exists": true,
        "size": 1548,
        "text": "WG Access Dev checkpoint\n\nSTEP: 014\nSTATUS: OK\n\nAgent reconciliation / sync enabled peers completed.\n\nProblem solved:\n  VM100 wg_paid peers added with runtime wg set are not persistent across reboot,\n  network reload, or manual removal.\n\nImplemented:\n  Backend agent endpoint:\n    GET /agent/peers?node_id=ddn-test\n\n  It returns enabled peers for the node, including:\n    - public_key\n    - preshared_key\n    - tunnel_ip\n    - paid_until\n    - enabled\n\n  Agent behavior:\n    1. Fetch enabled peers from backend.\n    2. Read current VM100 runtime peers using:\n         wg show wg_paid allowed-ips\n    3. Add/update missing or changed peers using:\n         wg set wg_paid peer ... preshared-key ... allowed-ips ...\n    4. Remove extra runtime peers that are not enabled in backend.\n    5. Then process pending provisioning jobs.\n\nTest performed:\n  1. Manually removed real peer from VM100 wg_paid.\n  2. Ran agent once manually.\n  3. Agent restored the peer from backend enabled peers.\n\nVerified restored peer:\n  public_key: TdYIwRgGjStiQW1gyZjgsaPP+/6WE1U0NcmK0eVEVXM=\n  tunnel_ip: 10.253.1.14/32\n\nLog evidence:\n  sync desired enabled peers: 1\n  sync current runtime peers: 0\n  sync set peer tunnel_ip=10.253.1.14\n  no pending jobs\n\nCurrent state:\n  - Backend has one enabled real peer.\n  - Old fake peers are disabled.\n  - VM100 wg_paid contains the enabled real peer.\n  - wg-access-agent.timer remains disabled.\n\nNext steps:\n  - add disable/deprovision API/job and test sync removal\n  - generate client config from backend\n  - then re-enable timer\n"
      },
      "/opt/wg-access/agent/wg_access_agent.py": {
        "exists": true,
        "size": 10349,
        "text": "#!/usr/bin/env python3\n\nimport ipaddress\nimport json\nimport os\nimport re\nimport shlex\nimport subprocess\nimport sys\nimport traceback\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom urllib import request, parse, error\n\n\nWG_KEY_RE = re.compile(r\"^[A-Za-z0-9+/]{43}=$\")\n\n\ndef utc_now():\n    return datetime.now(timezone.utc).isoformat()\n\n\ndef load_env(path):\n    env = {}\n    with open(path, \"r\", encoding=\"utf-8\") as f:\n        for line in f:\n            line = line.strip()\n            if not line or line.startswith(\"#\") or \"=\" not in line:\n                continue\n            k, v = line.split(\"=\", 1)\n            env[k.strip()] = v.strip()\n    return env\n\n\nENV_PATH = os.environ.get(\"WG_ACCESS_AGENT_ENV\", \"/opt/wg-access/agent/agent.env\")\nenv = load_env(ENV_PATH)\n\nBACKEND_URL = env[\"BACKEND_URL\"].rstrip(\"/\")\nNODE_ID = env[\"NODE_ID\"]\nAGENT_TOKEN = env[\"AGENT_TOKEN\"]\nSTATE_FILE = Path(env[\"STATE_FILE\"])\nLOG_FILE = Path(env[\"LOG_FILE\"])\n\nREMOTE_HOST = env[\"REMOTE_HOST\"]\nREMOTE_USER = env.get(\"REMOTE_USER\", \"root\")\nREMOTE_SSH_KEY = env[\"REMOTE_SSH_KEY\"]\nREMOTE_WG_INTERFACE = env[\"REMOTE_WG_INTERFACE\"]\n\n\ndef log(msg):\n    LOG_FILE.parent.mkdir(parents=True, exist_ok=True)\n    line = f\"[{utc_now()}] {msg}\"\n    print(line)\n    with LOG_FILE.open(\"a\", encoding=\"utf-8\") as f:\n        f.write(line + \"\\n\")\n\n\ndef http_json(method, path, payload=None):\n    url = BACKEND_URL + path\n    data = None\n    headers = {\n        \"X-Agent-Token\": AGENT_TOKEN,\n        \"Accept\": \"application/json\",\n    }\n\n    if payload is not None:\n        data = json.dumps(payload).encode(\"utf-8\")\n        headers[\"Content-Type\"] = \"application/json\"\n\n    req = request.Request(url, data=data, headers=headers, method=method)\n\n    try:\n        with request.urlopen(req, timeout=20) as resp:\n            body = resp.read().decode(\"utf-8\")\n            return json.loads(body) if body else None\n    except error.HTTPError as e:\n        body = e.read().decode(\"utf-8\", errors=\"replace\")\n        raise RuntimeError(f\"HTTP {e.code} {url}: {body}\") from e\n\n\ndef load_state():\n    if not STATE_FILE.exists():\n        return {\n            \"node_id\": NODE_ID,\n            \"updated_at\": None,\n            \"peers\": {},\n        }\n    with STATE_FILE.open(\"r\", encoding=\"utf-8\") as f:\n        return json.load(f)\n\n\ndef save_state(state):\n    STATE_FILE.parent.mkdir(parents=True, exist_ok=True)\n    state[\"node_id\"] = NODE_ID\n    state[\"updated_at\"] = utc_now()\n\n    tmp = STATE_FILE.with_suffix(\".tmp\")\n    with tmp.open(\"w\", encoding=\"utf-8\") as f:\n        json.dump(state, f, ensure_ascii=False, indent=2, sort_keys=True)\n        f.write(\"\\n\")\n    tmp.replace(STATE_FILE)\n\n\ndef require_wg_key(name, value):\n    if not isinstance(value, str) or not WG_KEY_RE.match(value):\n        raise RuntimeError(f\"invalid WireGuard key in {name}\")\n    return value\n\n\ndef require_tunnel_ip(value):\n    ip = ipaddress.ip_address(str(value))\n    if ip.version != 4:\n        raise RuntimeError(\"only IPv4 tunnel_ip is supported\")\n    return str(ip)\n\n\ndef normalize_peer_payload(peer):\n    public_key = require_wg_key(\"public_key\", peer[\"public_key\"])\n    preshared_key = require_wg_key(\"preshared_key\", peer[\"preshared_key\"])\n    tunnel_ip = require_tunnel_ip(peer[\"tunnel_ip\"])\n\n    return {\n        \"id\": str(peer.get(\"id\") or peer.get(\"peer_id\") or \"\"),\n        \"node_id\": peer.get(\"node_id\", NODE_ID),\n        \"public_key\": public_key,\n        \"preshared_key\": preshared_key,\n        \"tunnel_ip\": tunnel_ip,\n        \"allowed_ips\": f\"{tunnel_ip}/32\",\n        \"paid_until\": peer.get(\"paid_until\"),\n        \"enabled\": bool(peer.get(\"enabled\", True)),\n    }\n\n\ndef remote_wg(script, stdin_text=None):\n    cmd = [\n        \"ssh\",\n        \"-i\", REMOTE_SSH_KEY,\n        \"-o\", \"IdentitiesOnly=yes\",\n        \"-o\", \"BatchMode=yes\",\n        \"-o\", \"StrictHostKeyChecking=yes\",\n        f\"{REMOTE_USER}@{REMOTE_HOST}\",\n        script,\n    ]\n\n    res = subprocess.run(\n        cmd,\n        input=(stdin_text.encode(\"utf-8\") if stdin_text is not None else None),\n        stdout=subprocess.PIPE,\n        stderr=subprocess.PIPE,\n        timeout=30,\n        check=False,\n    )\n\n    stdout = res.stdout.decode(\"utf-8\", errors=\"replace\").strip()\n    stderr = res.stderr.decode(\"utf-8\", errors=\"replace\").strip()\n\n    if stdout:\n        log(f\"remote stdout: {stdout}\")\n    if stderr:\n        log(f\"remote stderr: {stderr}\")\n\n    if res.returncode != 0:\n        raise RuntimeError(f\"remote command failed rc={res.returncode}: {stderr or stdout}\")\n\n    return stdout\n\n\ndef remote_current_allowed_ips():\n    out = remote_wg(\"wg show \" + shlex.quote(REMOTE_WG_INTERFACE) + \" allowed-ips\")\n    current = {}\n\n    for line in out.splitlines():\n        line = line.strip()\n        if not line:\n            continue\n        parts = line.split()\n        if len(parts) >= 2:\n            current[parts[0]] = parts[1:]\n\n    return current\n\n\ndef remote_set_peer(peer):\n    script = \"\\n\".join([\n        \"set -eu\",\n        'tmp=\"$(mktemp /tmp/wg_access_psk.XXXXXX)\"',\n        'trap \\'rm -f \"$tmp\"\\' EXIT',\n        'cat > \"$tmp\"',\n        \"wg set \"\n        + shlex.quote(REMOTE_WG_INTERFACE)\n        + \" peer \"\n        + shlex.quote(peer[\"public_key\"])\n        + ' preshared-key \"$tmp\" allowed-ips '\n        + shlex.quote(peer[\"allowed_ips\"]),\n    ])\n    remote_wg(script, peer[\"preshared_key\"] + \"\\n\")\n\n\ndef remote_remove_peer(public_key):\n    script = (\n        \"set -eu\\n\"\n        + \"wg set \"\n        + shlex.quote(REMOTE_WG_INTERFACE)\n        + \" peer \"\n        + shlex.quote(public_key)\n        + \" remove\"\n    )\n    remote_wg(script)\n\n\ndef sync_enabled_peers():\n    query = parse.urlencode({\"node_id\": NODE_ID})\n    rows = http_json(\"GET\", f\"/agent/peers?{query}\") or []\n    desired = {}\n\n    for row in rows:\n        peer = normalize_peer_payload(row)\n        desired[peer[\"public_key\"]] = peer\n\n    log(f\"sync desired enabled peers: {len(desired)}\")\n\n    current = remote_current_allowed_ips()\n    log(f\"sync current runtime peers: {len(current)}\")\n\n    for public_key, peer in desired.items():\n        current_allowed = current.get(public_key)\n        if current_allowed == [peer[\"allowed_ips\"]]:\n            log(f\"sync keep peer tunnel_ip={peer['tunnel_ip']}\")\n            continue\n\n        remote_set_peer(peer)\n        log(f\"sync set peer tunnel_ip={peer['tunnel_ip']}\")\n\n    for public_key in sorted(set(current.keys()) - set(desired.keys())):\n        remote_remove_peer(public_key)\n        log(f\"sync removed extra runtime peer public_key={public_key}\")\n\n    # Финальная проверка для лога.\n    remote_wg(\"wg show \" + shlex.quote(REMOTE_WG_INTERFACE))\n\n    state = load_state()\n    state[\"peers\"] = {}\n    for public_key, peer in desired.items():\n        state[\"peers\"][public_key] = {\n            \"peer_id\": peer[\"id\"],\n            \"public_key\": public_key,\n            \"preshared_key\": peer[\"preshared_key\"],\n            \"tunnel_ip\": peer[\"tunnel_ip\"],\n            \"paid_until\": peer[\"paid_until\"],\n            \"enabled\": True,\n            \"last_action\": \"sync_enabled_peers\",\n            \"updated_at\": utc_now(),\n        }\n    save_state(state)\n\n\ndef apply_job_real(job):\n    action = job[\"action\"]\n    payload = job[\"payload_json\"]\n    peer_id = str(job.get(\"peer_id\") or \"\")\n\n    if action == \"enable_peer\":\n        peer = normalize_peer_payload({\n            \"id\": peer_id,\n            \"node_id\": NODE_ID,\n            \"public_key\": payload[\"public_key\"],\n            \"preshared_key\": payload[\"preshared_key\"],\n            \"tunnel_ip\": payload[\"tunnel_ip\"],\n            \"paid_until\": payload.get(\"paid_until\"),\n            \"enabled\": True,\n        })\n\n        remote_set_peer(peer)\n        remote_wg(\"wg show \" + shlex.quote(REMOTE_WG_INTERFACE))\n\n        state = load_state()\n        state[\"peers\"][peer[\"public_key\"]] = {\n            \"peer_id\": peer_id,\n            \"public_key\": peer[\"public_key\"],\n            \"preshared_key\": peer[\"preshared_key\"],\n            \"tunnel_ip\": peer[\"tunnel_ip\"],\n            \"paid_until\": peer[\"paid_until\"],\n            \"enabled\": True,\n            \"last_action\": action,\n            \"last_job_id\": job[\"id\"],\n            \"updated_at\": utc_now(),\n        }\n        save_state(state)\n        log(f\"REAL enable_peer peer_id={peer_id} tunnel_ip={peer['tunnel_ip']}\")\n        return\n\n    if action == \"disable_peer\":\n        public_key = require_wg_key(\"public_key\", payload[\"public_key\"])\n\n        remote_remove_peer(public_key)\n        remote_wg(\"wg show \" + shlex.quote(REMOTE_WG_INTERFACE))\n\n        state = load_state()\n        if public_key not in state[\"peers\"]:\n            state[\"peers\"][public_key] = {\n                \"peer_id\": peer_id,\n                \"public_key\": public_key,\n            }\n\n        state[\"peers\"][public_key][\"enabled\"] = False\n        state[\"peers\"][public_key][\"last_action\"] = action\n        state[\"peers\"][public_key][\"last_job_id\"] = job[\"id\"]\n        state[\"peers\"][public_key][\"updated_at\"] = utc_now()\n\n        save_state(state)\n        log(f\"REAL disable_peer peer_id={peer_id}\")\n        return\n\n    raise RuntimeError(f\"unsupported action: {action}\")\n\n\ndef run_once():\n    # Reconciliation first: VM100 wg_paid runtime state must match backend enabled peers.\n    sync_enabled_peers()\n\n    query = parse.urlencode({\"node_id\": NODE_ID, \"limit\": 20})\n    jobs = http_json(\"GET\", f\"/agent/jobs?{query}\") or []\n\n    if not jobs:\n        log(\"no pending jobs\")\n        return 0\n\n    log(f\"fetched pending jobs: {len(jobs)}\")\n\n    for job in jobs:\n        job_id = job[\"id\"]\n        log(f\"starting job_id={job_id} action={job['action']}\")\n\n        try:\n            started = http_json(\"POST\", f\"/agent/jobs/{job_id}/start\")\n            apply_job_real(started)\n            completed = http_json(\"POST\", f\"/agent/jobs/{job_id}/complete\")\n            log(f\"completed job_id={completed['id']} status={completed['status']} attempts={completed['attempts']}\")\n        except Exception as exc:\n            err = \"\".join(traceback.format_exception_only(type(exc), exc)).strip()\n            log(f\"failed job_id={job_id}: {err}\")\n            try:\n                http_json(\"POST\", f\"/agent/jobs/{job_id}/fail\", {\"error\": err})\n            except Exception as fail_exc:\n                log(f\"failed to report failure job_id={job_id}: {fail_exc}\")\n\n    return 0\n\n\nif __name__ == \"__main__\":\n    sys.exit(run_once())\n"
      },
      "/opt/wg-access/agent/wg_access_agent.py.BEFORE_RECONCILE_SYNC.20260704-134532": {
        "exists": true,
        "size": 7492,
        "text": "#!/usr/bin/env python3\n\nimport ipaddress\nimport json\nimport os\nimport re\nimport shlex\nimport subprocess\nimport sys\nimport traceback\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom urllib import request, parse, error\n\n\nWG_KEY_RE = re.compile(r\"^[A-Za-z0-9+/]{43}=$\")\n\n\ndef utc_now():\n    return datetime.now(timezone.utc).isoformat()\n\n\ndef load_env(path):\n    env = {}\n    with open(path, \"r\", encoding=\"utf-8\") as f:\n        for line in f:\n            line = line.strip()\n            if not line or line.startswith(\"#\") or \"=\" not in line:\n                continue\n            k, v = line.split(\"=\", 1)\n            env[k.strip()] = v.strip()\n    return env\n\n\nENV_PATH = os.environ.get(\"WG_ACCESS_AGENT_ENV\", \"/opt/wg-access/agent/agent.env\")\nenv = load_env(ENV_PATH)\n\nBACKEND_URL = env[\"BACKEND_URL\"].rstrip(\"/\")\nNODE_ID = env[\"NODE_ID\"]\nAGENT_TOKEN = env[\"AGENT_TOKEN\"]\nSTATE_FILE = Path(env[\"STATE_FILE\"])\nLOG_FILE = Path(env[\"LOG_FILE\"])\n\nREMOTE_HOST = env[\"REMOTE_HOST\"]\nREMOTE_USER = env.get(\"REMOTE_USER\", \"root\")\nREMOTE_SSH_KEY = env[\"REMOTE_SSH_KEY\"]\nREMOTE_WG_INTERFACE = env[\"REMOTE_WG_INTERFACE\"]\n\n\ndef log(msg):\n    LOG_FILE.parent.mkdir(parents=True, exist_ok=True)\n    line = f\"[{utc_now()}] {msg}\"\n    print(line)\n    with LOG_FILE.open(\"a\", encoding=\"utf-8\") as f:\n        f.write(line + \"\\n\")\n\n\ndef http_json(method, path, payload=None):\n    url = BACKEND_URL + path\n    data = None\n    headers = {\n        \"X-Agent-Token\": AGENT_TOKEN,\n        \"Accept\": \"application/json\",\n    }\n\n    if payload is not None:\n        data = json.dumps(payload).encode(\"utf-8\")\n        headers[\"Content-Type\"] = \"application/json\"\n\n    req = request.Request(url, data=data, headers=headers, method=method)\n\n    try:\n        with request.urlopen(req, timeout=20) as resp:\n            body = resp.read().decode(\"utf-8\")\n            return json.loads(body) if body else None\n    except error.HTTPError as e:\n        body = e.read().decode(\"utf-8\", errors=\"replace\")\n        raise RuntimeError(f\"HTTP {e.code} {url}: {body}\") from e\n\n\ndef load_state():\n    if not STATE_FILE.exists():\n        return {\n            \"node_id\": NODE_ID,\n            \"updated_at\": None,\n            \"peers\": {},\n        }\n    with STATE_FILE.open(\"r\", encoding=\"utf-8\") as f:\n        return json.load(f)\n\n\ndef save_state(state):\n    STATE_FILE.parent.mkdir(parents=True, exist_ok=True)\n    state[\"node_id\"] = NODE_ID\n    state[\"updated_at\"] = utc_now()\n\n    tmp = STATE_FILE.with_suffix(\".tmp\")\n    with tmp.open(\"w\", encoding=\"utf-8\") as f:\n        json.dump(state, f, ensure_ascii=False, indent=2, sort_keys=True)\n        f.write(\"\\n\")\n    tmp.replace(STATE_FILE)\n\n\ndef require_wg_key(name, value):\n    if not isinstance(value, str) or not WG_KEY_RE.match(value):\n        raise RuntimeError(f\"invalid WireGuard key in {name}\")\n    return value\n\n\ndef require_tunnel_ip(value):\n    ip = ipaddress.ip_address(str(value))\n    if ip.version != 4:\n        raise RuntimeError(\"only IPv4 tunnel_ip is supported\")\n    return str(ip)\n\n\ndef remote_wg(script, stdin_text=None):\n    cmd = [\n        \"ssh\",\n        \"-i\", REMOTE_SSH_KEY,\n        \"-o\", \"IdentitiesOnly=yes\",\n        \"-o\", \"BatchMode=yes\",\n        \"-o\", \"StrictHostKeyChecking=yes\",\n        f\"{REMOTE_USER}@{REMOTE_HOST}\",\n        script,\n    ]\n\n    res = subprocess.run(\n        cmd,\n        input=(stdin_text.encode(\"utf-8\") if stdin_text is not None else None),\n        stdout=subprocess.PIPE,\n        stderr=subprocess.PIPE,\n        timeout=30,\n        check=False,\n    )\n\n    stdout = res.stdout.decode(\"utf-8\", errors=\"replace\").strip()\n    stderr = res.stderr.decode(\"utf-8\", errors=\"replace\").strip()\n\n    if stdout:\n        log(f\"remote stdout: {stdout}\")\n    if stderr:\n        log(f\"remote stderr: {stderr}\")\n\n    if res.returncode != 0:\n        raise RuntimeError(f\"remote command failed rc={res.returncode}: {stderr or stdout}\")\n\n    return stdout\n\n\ndef apply_job_real(job):\n    state = load_state()\n\n    action = job[\"action\"]\n    payload = job[\"payload_json\"]\n    peer_id = str(job.get(\"peer_id\") or \"\")\n\n    if action == \"enable_peer\":\n        public_key = require_wg_key(\"public_key\", payload[\"public_key\"])\n        preshared_key = require_wg_key(\"preshared_key\", payload[\"preshared_key\"])\n        tunnel_ip = require_tunnel_ip(payload[\"tunnel_ip\"])\n        allowed_ips = f\"{tunnel_ip}/32\"\n\n        script = \"\\n\".join([\n            \"set -eu\",\n            'tmp=\"$(mktemp /tmp/wg_access_psk.XXXXXX)\"',\n            'trap \\'rm -f \"$tmp\"\\' EXIT',\n            'cat > \"$tmp\"',\n            \"wg set \"\n            + shlex.quote(REMOTE_WG_INTERFACE)\n            + \" peer \"\n            + shlex.quote(public_key)\n            + ' preshared-key \"$tmp\" allowed-ips '\n            + shlex.quote(allowed_ips),\n            \"wg show \" + shlex.quote(REMOTE_WG_INTERFACE),\n        ])\n\n        remote_wg(script, preshared_key + \"\\n\")\n\n        state[\"peers\"][public_key] = {\n            \"peer_id\": peer_id,\n            \"public_key\": public_key,\n            \"preshared_key\": preshared_key,\n            \"tunnel_ip\": tunnel_ip,\n            \"paid_until\": payload.get(\"paid_until\"),\n            \"enabled\": True,\n            \"last_action\": action,\n            \"last_job_id\": job[\"id\"],\n            \"updated_at\": utc_now(),\n        }\n        save_state(state)\n        log(f\"REAL enable_peer peer_id={peer_id} tunnel_ip={tunnel_ip}\")\n        return\n\n    if action == \"disable_peer\":\n        public_key = require_wg_key(\"public_key\", payload[\"public_key\"])\n\n        script = \"\\n\".join([\n            \"set -eu\",\n            \"wg set \"\n            + shlex.quote(REMOTE_WG_INTERFACE)\n            + \" peer \"\n            + shlex.quote(public_key)\n            + \" remove\",\n            \"wg show \" + shlex.quote(REMOTE_WG_INTERFACE),\n        ])\n\n        remote_wg(script)\n\n        if public_key not in state[\"peers\"]:\n            state[\"peers\"][public_key] = {\n                \"peer_id\": peer_id,\n                \"public_key\": public_key,\n            }\n\n        state[\"peers\"][public_key][\"enabled\"] = False\n        state[\"peers\"][public_key][\"last_action\"] = action\n        state[\"peers\"][public_key][\"last_job_id\"] = job[\"id\"]\n        state[\"peers\"][public_key][\"updated_at\"] = utc_now()\n\n        save_state(state)\n        log(f\"REAL disable_peer peer_id={peer_id}\")\n        return\n\n    raise RuntimeError(f\"unsupported action: {action}\")\n\n\ndef run_once():\n    query = parse.urlencode({\"node_id\": NODE_ID, \"limit\": 20})\n    jobs = http_json(\"GET\", f\"/agent/jobs?{query}\") or []\n\n    if not jobs:\n        log(\"no pending jobs\")\n        return 0\n\n    log(f\"fetched pending jobs: {len(jobs)}\")\n\n    for job in jobs:\n        job_id = job[\"id\"]\n        log(f\"starting job_id={job_id} action={job['action']}\")\n\n        try:\n            started = http_json(\"POST\", f\"/agent/jobs/{job_id}/start\")\n            apply_job_real(started)\n            completed = http_json(\"POST\", f\"/agent/jobs/{job_id}/complete\")\n            log(f\"completed job_id={completed['id']} status={completed['status']} attempts={completed['attempts']}\")\n        except Exception as exc:\n            err = \"\".join(traceback.format_exception_only(type(exc), exc)).strip()\n            log(f\"failed job_id={job_id}: {err}\")\n            try:\n                http_json(\"POST\", f\"/agent/jobs/{job_id}/fail\", {\"error\": err})\n            except Exception as fail_exc:\n                log(f\"failed to report failure job_id={job_id}: {fail_exc}\")\n\n    return 0\n\n\nif __name__ == \"__main__\":\n    sys.exit(run_once())\n"
      },
      "/opt/wg-access/backend/app/api/admin.py": {
        "exists": true,
        "size": 13837,
        "text": "from datetime import datetime, timezone, timedelta\nfrom uuid import UUID\nimport secrets\nfrom urllib.parse import unquote\n\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom pydantic import BaseModel, EmailStr, Field\nfrom sqlalchemy import select\nfrom sqlalchemy.orm import Session\n\nfrom app.db.session import get_db\nfrom app.models import InviteCode, User, Subscription, Peer, ProvisioningJob\nfrom app.services.wireguard import (\n    generate_wg_keypair,\n    generate_wg_psk,\n    next_tunnel_ip,\n    build_client_config,\n)\n\nrouter = APIRouter(prefix=\"/admin\", tags=[\"admin\"])\n\n\nclass InviteCreateRequest(BaseModel):\n    note: str | None = None\n    max_uses: int = 1\n\n\nclass InviteResponse(BaseModel):\n    id: UUID\n    code: str\n    note: str | None\n    max_uses: int\n    used_count: int\n    active: bool\n    created_at: datetime\n\n    class Config:\n        from_attributes = True\n\n\n@router.post(\"/invites\", response_model=InviteResponse)\ndef create_invite(payload: InviteCreateRequest, db: Session = Depends(get_db)):\n    code = \"INV-\" + secrets.token_urlsafe(12).replace(\"-\", \"\").replace(\"_\", \"\")[:16].upper()\n    invite = InviteCode(\n        code=code,\n        note=payload.note,\n        max_uses=payload.max_uses,\n        used_count=0,\n        active=True,\n    )\n    db.add(invite)\n    db.commit()\n    db.refresh(invite)\n    return invite\n\n\n@router.get(\"/invites\", response_model=list[InviteResponse])\ndef list_invites(db: Session = Depends(get_db)):\n    rows = db.execute(select(InviteCode).order_by(InviteCode.created_at.desc())).scalars().all()\n    return rows\n\n\nclass PeerResponse(BaseModel):\n    id: UUID\n    user_id: UUID\n    node_id: str\n    public_key: str\n    tunnel_ip: str\n    paid_until: datetime\n    enabled: bool\n    created_at: datetime\n\n    class Config:\n        from_attributes = True\n\n\n@router.get(\"/peers\", response_model=list[PeerResponse])\ndef list_peers(db: Session = Depends(get_db)):\n    rows = db.execute(select(Peer).order_by(Peer.created_at.desc())).scalars().all()\n    return rows\n\n\n\nclass SubscriptionCreateRequest(BaseModel):\n    email: EmailStr | None = None\n    months: int = Field(default=1, ge=1, le=36)\n    plan_code: str = \"manual\"\n    node_id: str = \"ddn-test\"\n    auto_renew: bool = False\n\n\nclass SubscriptionCreateResponse(BaseModel):\n    user_id: UUID\n    subscription_id: UUID\n    peer_id: UUID\n    job_id: UUID\n    node_id: str\n    tunnel_ip: str\n    paid_until: datetime\n    private_key: str\n    public_key: str\n    preshared_key: str\n    client_config: str\n\n\n@router.post(\"/subscriptions\", response_model=SubscriptionCreateResponse)\ndef create_subscription(payload: SubscriptionCreateRequest, db: Session = Depends(get_db)):\n    # MVP admin/manual subscription endpoint.\n    # It creates a user, an active subscription, a WG peer and a pending enable_peer job.\n    # The private key is intentionally returned once and not stored in DB.\n    paid_until = datetime.now(timezone.utc) + timedelta(days=30 * payload.months)\n\n    user = User(email=payload.email)\n    db.add(user)\n    db.flush()\n\n    subscription = Subscription(\n        user_id=user.id,\n        status=\"active\",\n        plan_code=payload.plan_code,\n        auto_renew=payload.auto_renew,\n        paid_until=paid_until,\n    )\n    db.add(subscription)\n    db.flush()\n\n    tunnel_ip = next_tunnel_ip(db, payload.node_id)\n    private_key, public_key = generate_wg_keypair()\n    preshared_key = generate_wg_psk()\n    client_config = build_client_config(private_key, tunnel_ip, preshared_key)\n\n    peer = Peer(\n        user_id=user.id,\n        node_id=payload.node_id,\n        public_key=public_key,\n        preshared_key=preshared_key,\n        tunnel_ip=tunnel_ip,\n        paid_until=paid_until,\n        enabled=True,\n    )\n    db.add(peer)\n    db.flush()\n\n    job = ProvisioningJob(\n        node_id=payload.node_id,\n        action=\"enable_peer\",\n        peer_id=peer.id,\n        payload_json={\n            \"public_key\": peer.public_key,\n            \"preshared_key\": peer.preshared_key,\n            \"tunnel_ip\": peer.tunnel_ip,\n            \"paid_until\": peer.paid_until.isoformat(),\n        },\n        status=\"pending\",\n        attempts=0,\n    )\n    db.add(job)\n\n    db.commit()\n    db.refresh(subscription)\n    db.refresh(peer)\n    db.refresh(job)\n\n    return SubscriptionCreateResponse(\n        user_id=user.id,\n        subscription_id=subscription.id,\n        peer_id=peer.id,\n        job_id=job.id,\n        node_id=peer.node_id,\n        tunnel_ip=peer.tunnel_ip,\n        paid_until=peer.paid_until,\n        private_key=private_key,\n        public_key=peer.public_key,\n        preshared_key=peer.preshared_key,\n        client_config=client_config,\n    )\n\n\n\n\nclass ExpireSubscriptionsResponse(BaseModel):\n    expired_subscription_count: int\n    disabled_peer_count: int\n    job_ids: list[UUID]\n    subscription_ids: list[UUID]\n\n\n@router.post(\"/maintenance/expire-subscriptions\", response_model=ExpireSubscriptionsResponse)\ndef expire_subscriptions(db: Session = Depends(get_db)):\n    now = datetime.now(timezone.utc)\n\n    expired_subscriptions = db.execute(\n        select(Subscription)\n        .where(Subscription.status == \"active\")\n        .where(Subscription.paid_until <= now)\n        .order_by(Subscription.paid_until.asc())\n    ).scalars().all()\n\n    jobs = []\n    disabled_peer_count = 0\n\n    for subscription in expired_subscriptions:\n        subscription.status = \"expired\"\n        subscription.auto_renew = False\n\n        enabled_peers = db.execute(\n            select(Peer)\n            .where(Peer.user_id == subscription.user_id)\n            .where(Peer.enabled == True)  # noqa: E712\n            .order_by(Peer.created_at.desc())\n        ).scalars().all()\n\n        for peer in enabled_peers:\n            peer.enabled = False\n            peer.disabled_at = now\n            disabled_peer_count += 1\n\n            job = ProvisioningJob(\n                node_id=peer.node_id,\n                action=\"disable_peer\",\n                peer_id=peer.id,\n                payload_json={\n                    \"public_key\": peer.public_key,\n                    \"tunnel_ip\": peer.tunnel_ip,\n                },\n                status=\"pending\",\n                attempts=0,\n            )\n            db.add(job)\n            jobs.append(job)\n\n    db.commit()\n\n    for job in jobs:\n        db.refresh(job)\n\n    return ExpireSubscriptionsResponse(\n        expired_subscription_count=len(expired_subscriptions),\n        disabled_peer_count=disabled_peer_count,\n        job_ids=[job.id for job in jobs],\n        subscription_ids=[subscription.id for subscription in expired_subscriptions],\n    )\n\n\nclass CancelSubscriptionResponse(BaseModel):\n    subscription_id: UUID\n    user_id: UUID\n    status: str\n    canceled_at: datetime | None\n    disabled_peer_count: int\n    job_ids: list[UUID]\n\n\n@router.post(\"/subscriptions/{subscription_id}/cancel\", response_model=CancelSubscriptionResponse)\ndef cancel_subscription(subscription_id: UUID, db: Session = Depends(get_db)):\n    subscription = db.get(Subscription, subscription_id)\n    if not subscription:\n        raise HTTPException(status_code=404, detail=\"subscription not found\")\n\n    now = datetime.now(timezone.utc)\n\n    # Idempotent business-level cancel:\n    # cancel subscription once, then disable any currently enabled peers for this user.\n    subscription.status = \"canceled\"\n    subscription.auto_renew = False\n    if subscription.canceled_at is None:\n        subscription.canceled_at = now\n\n    enabled_peers = db.execute(\n        select(Peer)\n        .where(Peer.user_id == subscription.user_id)\n        .where(Peer.enabled == True)  # noqa: E712\n        .order_by(Peer.created_at.desc())\n    ).scalars().all()\n\n    jobs = []\n    for peer in enabled_peers:\n        peer.enabled = False\n        peer.disabled_at = now\n\n        job = ProvisioningJob(\n            node_id=peer.node_id,\n            action=\"disable_peer\",\n            peer_id=peer.id,\n            payload_json={\n                \"public_key\": peer.public_key,\n                \"tunnel_ip\": peer.tunnel_ip,\n            },\n            status=\"pending\",\n            attempts=0,\n        )\n        db.add(job)\n        jobs.append(job)\n\n    db.commit()\n    db.refresh(subscription)\n    for job in jobs:\n        db.refresh(job)\n\n    return CancelSubscriptionResponse(\n        subscription_id=subscription.id,\n        user_id=subscription.user_id,\n        status=subscription.status,\n        canceled_at=subscription.canceled_at,\n        disabled_peer_count=len(enabled_peers),\n        job_ids=[job.id for job in jobs],\n    )\n\n\nclass DisablePeerResponse(BaseModel):\n    peer_id: UUID\n    job_id: UUID\n    node_id: str\n    public_key: str\n    tunnel_ip: str\n    enabled: bool\n\n\n@router.post(\"/peers/{peer_id}/disable\", response_model=DisablePeerResponse)\ndef disable_peer(peer_id: UUID, db: Session = Depends(get_db)):\n    peer = db.get(Peer, peer_id)\n    if not peer:\n        from fastapi import HTTPException\n        raise HTTPException(status_code=404, detail=\"peer not found\")\n\n    peer.enabled = False\n    peer.disabled_at = datetime.now(timezone.utc)\n\n    job = ProvisioningJob(\n        node_id=peer.node_id,\n        action=\"disable_peer\",\n        peer_id=peer.id,\n        payload_json={\n            \"public_key\": peer.public_key,\n            \"tunnel_ip\": peer.tunnel_ip,\n        },\n        status=\"pending\",\n        attempts=0,\n    )\n    db.add(job)\n    db.commit()\n    db.refresh(peer)\n    db.refresh(job)\n\n    return DisablePeerResponse(\n        peer_id=peer.id,\n        job_id=job.id,\n        node_id=peer.node_id,\n        public_key=peer.public_key,\n        tunnel_ip=peer.tunnel_ip,\n        enabled=peer.enabled,\n    )\n\n\n\nclass JobResponse(BaseModel):\n    id: UUID\n    node_id: str\n    action: str\n    status: str\n    attempts: int\n    created_at: datetime\n    last_error: str | None\n\n    class Config:\n        from_attributes = True\n\n\n@router.get(\"/jobs\", response_model=list[JobResponse])\ndef list_jobs(db: Session = Depends(get_db)):\n    rows = db.execute(select(ProvisioningJob).order_by(ProvisioningJob.created_at.desc())).scalars().all()\n    return rows\n\n\nclass AdminSubscriptionResponse(BaseModel):\n    id: UUID\n    user_id: UUID\n    status: str\n    plan_code: str\n    auto_renew: bool\n    paid_until: datetime\n    created_at: datetime\n    canceled_at: datetime | None\n\n    class Config:\n        from_attributes = True\n\n\nclass AdminUserPeerResponse(BaseModel):\n    id: UUID\n    user_id: UUID\n    node_id: str\n    public_key: str\n    tunnel_ip: str\n    paid_until: datetime\n    enabled: bool\n    created_at: datetime\n    disabled_at: datetime | None\n\n    class Config:\n        from_attributes = True\n\n\nclass AdminUserJobResponse(BaseModel):\n    id: UUID\n    node_id: str\n    action: str\n    peer_id: UUID | None\n    status: str\n    attempts: int\n    created_at: datetime\n    last_error: str | None\n\n    class Config:\n        from_attributes = True\n\n\nclass AdminUserSummaryResponse(BaseModel):\n    id: UUID\n    email: str | None\n    created_at: datetime\n    subscriptions_count: int\n    peers_count: int\n    enabled_peers_count: int\n\n    class Config:\n        from_attributes = True\n\n\nclass AdminUserDetailResponse(BaseModel):\n    id: UUID\n    email: str | None\n    created_at: datetime\n    subscriptions: list[AdminSubscriptionResponse]\n    peers: list[AdminUserPeerResponse]\n    jobs: list[AdminUserJobResponse]\n\n\n@router.get(\"/users\", response_model=list[AdminUserSummaryResponse])\ndef list_users(db: Session = Depends(get_db)):\n    users = db.execute(select(User).order_by(User.created_at.desc())).scalars().all()\n\n    result = []\n    for user in users:\n        subscriptions = db.execute(\n            select(Subscription).where(Subscription.user_id == user.id)\n        ).scalars().all()\n\n        peers = db.execute(\n            select(Peer).where(Peer.user_id == user.id)\n        ).scalars().all()\n\n        result.append(AdminUserSummaryResponse(\n            id=user.id,\n            email=user.email,\n            created_at=user.created_at,\n            subscriptions_count=len(subscriptions),\n            peers_count=len(peers),\n            enabled_peers_count=len([p for p in peers if p.enabled]),\n        ))\n\n    return result\n\n\ndef build_admin_user_detail(user: User, db: Session) -> AdminUserDetailResponse:\n    subscriptions = db.execute(\n        select(Subscription)\n        .where(Subscription.user_id == user.id)\n        .order_by(Subscription.created_at.desc())\n    ).scalars().all()\n\n    peers = db.execute(\n        select(Peer)\n        .where(Peer.user_id == user.id)\n        .order_by(Peer.created_at.desc())\n    ).scalars().all()\n\n    peer_ids = [p.id for p in peers]\n    if peer_ids:\n        jobs = db.execute(\n            select(ProvisioningJob)\n            .where(ProvisioningJob.peer_id.in_(peer_ids))\n            .order_by(ProvisioningJob.created_at.desc())\n        ).scalars().all()\n    else:\n        jobs = []\n\n    return AdminUserDetailResponse(\n        id=user.id,\n        email=user.email,\n        created_at=user.created_at,\n        subscriptions=subscriptions,\n        peers=peers,\n        jobs=jobs,\n    )\n\n\n@router.get(\"/users/by-email/{email}\", response_model=AdminUserDetailResponse)\ndef get_user_by_email(email: str, db: Session = Depends(get_db)):\n    decoded_email = unquote(email)\n\n    user = db.execute(\n        select(User)\n        .where(User.email == decoded_email)\n        .order_by(User.created_at.desc())\n    ).scalars().first()\n\n    if not user:\n        raise HTTPException(status_code=404, detail=\"user not found\")\n\n    return build_admin_user_detail(user, db)\n\n\n@router.get(\"/users/{user_id}\", response_model=AdminUserDetailResponse)\ndef get_user(user_id: UUID, db: Session = Depends(get_db)):\n    user = db.get(User, user_id)\n    if not user:\n        raise HTTPException(status_code=404, detail=\"user not found\")\n\n    return build_admin_user_detail(user, db)\n"
      },
      "/opt/wg-access/backend/app/api/agent.py": {
        "exists": true,
        "size": 3789,
        "text": "from datetime import datetime, timezone\nfrom uuid import UUID\n\nfrom fastapi import APIRouter, Depends, Header, HTTPException\nfrom pydantic import BaseModel\nfrom sqlalchemy import select\nfrom sqlalchemy.orm import Session\n\nfrom app.config import settings\nfrom app.db.session import get_db\nfrom app.models import Peer, ProvisioningJob\n\nrouter = APIRouter(prefix=\"/agent\", tags=[\"agent\"])\n\n\ndef check_agent_token(x_agent_token: str | None = Header(default=None)):\n    if not x_agent_token or x_agent_token != settings.agent_token:\n        raise HTTPException(status_code=401, detail=\"invalid agent token\")\n\n\nclass AgentPeerResponse(BaseModel):\n    id: UUID\n    node_id: str\n    public_key: str\n    preshared_key: str\n    tunnel_ip: str\n    paid_until: datetime\n    enabled: bool\n\n    class Config:\n        from_attributes = True\n\n\n@router.get(\"/peers\", response_model=list[AgentPeerResponse])\ndef get_enabled_peers(\n    node_id: str,\n    db: Session = Depends(get_db),\n    _: None = Depends(check_agent_token),\n):\n    rows = db.execute(\n        select(Peer)\n        .where(Peer.node_id == node_id)\n        .where(Peer.enabled == True)\n        .order_by(Peer.created_at.asc())\n    ).scalars().all()\n    return rows\n\n\n\nclass AgentJobResponse(BaseModel):\n    id: UUID\n    node_id: str\n    action: str\n    peer_id: UUID | None\n    payload_json: dict\n    status: str\n    attempts: int\n    created_at: datetime\n\n    class Config:\n        from_attributes = True\n\n\nclass FailRequest(BaseModel):\n    error: str\n\n\n@router.get(\"/jobs\", response_model=list[AgentJobResponse])\ndef get_pending_jobs(\n    node_id: str,\n    limit: int = 10,\n    db: Session = Depends(get_db),\n    _: None = Depends(check_agent_token),\n):\n    rows = db.execute(\n        select(ProvisioningJob)\n        .where(ProvisioningJob.node_id == node_id)\n        .where(ProvisioningJob.status == \"pending\")\n        .order_by(ProvisioningJob.created_at.asc())\n        .limit(limit)\n    ).scalars().all()\n    return rows\n\n\n@router.post(\"/jobs/{job_id}/start\", response_model=AgentJobResponse)\ndef start_job(\n    job_id: UUID,\n    db: Session = Depends(get_db),\n    _: None = Depends(check_agent_token),\n):\n    job = db.get(ProvisioningJob, job_id)\n    if not job:\n        raise HTTPException(status_code=404, detail=\"job not found\")\n    if job.status != \"pending\":\n        raise HTTPException(status_code=409, detail=f\"job status is {job.status}\")\n\n    job.status = \"running\"\n    job.started_at = datetime.now(timezone.utc)\n    job.attempts += 1\n\n    db.commit()\n    db.refresh(job)\n    return job\n\n\n@router.post(\"/jobs/{job_id}/complete\", response_model=AgentJobResponse)\ndef complete_job(\n    job_id: UUID,\n    db: Session = Depends(get_db),\n    _: None = Depends(check_agent_token),\n):\n    job = db.get(ProvisioningJob, job_id)\n    if not job:\n        raise HTTPException(status_code=404, detail=\"job not found\")\n    if job.status not in (\"pending\", \"running\"):\n        raise HTTPException(status_code=409, detail=f\"job status is {job.status}\")\n\n    job.status = \"completed\"\n    job.completed_at = datetime.now(timezone.utc)\n    job.last_error = None\n\n    db.commit()\n    db.refresh(job)\n    return job\n\n\n@router.post(\"/jobs/{job_id}/fail\", response_model=AgentJobResponse)\ndef fail_job(\n    job_id: UUID,\n    payload: FailRequest,\n    db: Session = Depends(get_db),\n    _: None = Depends(check_agent_token),\n):\n    job = db.get(ProvisioningJob, job_id)\n    if not job:\n        raise HTTPException(status_code=404, detail=\"job not found\")\n    if job.status not in (\"pending\", \"running\"):\n        raise HTTPException(status_code=409, detail=f\"job status is {job.status}\")\n\n    job.status = \"failed\"\n    job.completed_at = datetime.now(timezone.utc)\n    job.last_error = payload.error\n\n    db.commit()\n    db.refresh(job)\n    return job\n"
      },
      "/opt/wg-access/backend/app/api/dev.py": {
        "exists": true,
        "size": 2791,
        "text": "from datetime import datetime, timedelta, timezone\nfrom uuid import UUID\n\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom pydantic import BaseModel, EmailStr\nfrom sqlalchemy.orm import Session\n\nfrom app.config import settings\nfrom app.db.session import get_db\nfrom app.models import User, Peer, ProvisioningJob\nfrom app.services.wireguard import (\n    generate_wg_keypair,\n    generate_wg_psk,\n    next_tunnel_ip,\n    build_client_config,\n)\n\nrouter = APIRouter(prefix=\"/dev\", tags=[\"dev\"])\n\ndef require_dev_environment():\n    if settings.environment != \"dev\":\n        raise HTTPException(status_code=404, detail=\"not found\")\n\n\n\nclass TestPeerRequest(BaseModel):\n    email: EmailStr | None = None\n    months: int = 2\n    node_id: str = \"ddn-test\"\n\n\nclass TestPeerResponse(BaseModel):\n    user_id: UUID\n    peer_id: UUID\n    job_id: UUID\n    tunnel_ip: str\n    paid_until: datetime\n    private_key: str\n    public_key: str\n    preshared_key: str\n    client_config: str\n\n\n@router.post(\"/create-test-peer\", response_model=TestPeerResponse)\ndef create_test_peer(\n    payload: TestPeerRequest,\n    db: Session = Depends(get_db),\n    _: None = Depends(require_dev_environment),\n):\n    # Dev-only endpoint.\n    # It intentionally mirrors the minimal old behavior but now uses shared WG helpers.\n    # The private key is returned once and is not stored in DB.\n    user = User(email=payload.email)\n    db.add(user)\n    db.flush()\n\n    paid_until = datetime.now(timezone.utc) + timedelta(days=30 * payload.months)\n    tunnel_ip = next_tunnel_ip(db, payload.node_id)\n\n    private_key, public_key = generate_wg_keypair()\n    preshared_key = generate_wg_psk()\n    client_config = build_client_config(private_key, tunnel_ip, preshared_key)\n\n    peer = Peer(\n        user_id=user.id,\n        node_id=payload.node_id,\n        public_key=public_key,\n        preshared_key=preshared_key,\n        tunnel_ip=tunnel_ip,\n        paid_until=paid_until,\n        enabled=True,\n    )\n    db.add(peer)\n    db.flush()\n\n    job = ProvisioningJob(\n        node_id=payload.node_id,\n        action=\"enable_peer\",\n        peer_id=peer.id,\n        payload_json={\n            \"public_key\": peer.public_key,\n            \"preshared_key\": peer.preshared_key,\n            \"tunnel_ip\": peer.tunnel_ip,\n            \"paid_until\": peer.paid_until.isoformat(),\n        },\n        status=\"pending\",\n        attempts=0,\n    )\n    db.add(job)\n\n    db.commit()\n    db.refresh(peer)\n    db.refresh(job)\n\n    return TestPeerResponse(\n        user_id=user.id,\n        peer_id=peer.id,\n        job_id=job.id,\n        tunnel_ip=peer.tunnel_ip,\n        paid_until=peer.paid_until,\n        private_key=private_key,\n        public_key=peer.public_key,\n        preshared_key=peer.preshared_key,\n        client_config=client_config,\n    )\n"
      },
      "/opt/wg-access/backend/app/main.py": {
        "exists": true,
        "size": 788,
        "text": "from fastapi import FastAPI\nfrom sqlalchemy import text\n\nfrom app.config import settings\nfrom app.db.session import engine\nfrom app.api.admin import router as admin_router\nfrom app.api.dev import router as dev_router\nfrom app.api.agent import router as agent_router\n\napp = FastAPI(title=\"WG Access Backend\", version=\"0.0.3\")\n\napp.include_router(admin_router)\napp.include_router(dev_router)\napp.include_router(agent_router)\n\n\n@app.get(\"/health\")\ndef health():\n    return {\n        \"status\": \"ok\",\n        \"service\": \"wg-access-backend\",\n        \"environment\": settings.environment,\n    }\n\n\n@app.get(\"/health/db\")\ndef health_db():\n    with engine.connect() as conn:\n        value = conn.execute(text(\"select 1\")).scalar_one()\n    return {\n        \"status\": \"ok\",\n        \"db\": value,\n    }\n"
      },
      "/usr/local/sbin/wg-access-agent-job-selector-plan.py": {
        "exists": true,
        "size": 4410,
        "text": "#!/usr/bin/env python3\nimport argparse\nimport json\nimport subprocess\nimport tempfile\nfrom pathlib import Path\n\ndef load_json(path):\n    return json.loads(Path(path).read_text(encoding=\"utf-8\", errors=\"replace\"))\n\ndef flatten_job(job):\n    payload = job.get(\"payload\", {})\n    peer = payload.get(\"peer\", {})\n    selector = payload.get(\"selector\", {})\n    provider = payload.get(\"provider\", {})\n\n    return {\n        \"job_type\": job.get(\"job_type\", \"\"),\n        \"dry_run\": True,\n        \"peer_id\": peer.get(\"peer_id\", payload.get(\"peer_id\", \"\")),\n        \"subscription_id\": peer.get(\"subscription_id\", payload.get(\"subscription_id\", \"\")),\n        \"tunnel_ip\": peer.get(\"tunnel_ip\", payload.get(\"tunnel_ip\", \"\")),\n        \"slot_code\": selector.get(\"slot_code\", payload.get(\"slot_code\", \"\")),\n        \"selector_class\": selector.get(\"selector_class\", payload.get(\"selector_class\", \"\")),\n        \"selector_label\": selector.get(\"selector_label\", payload.get(\"selector_label\", \"\")),\n        \"fwmark\": selector.get(\"fwmark\", payload.get(\"fwmark\", \"\")),\n        \"table_id\": selector.get(\"table_id\", payload.get(\"table_id\", \"\")),\n        \"interface_name\": selector.get(\"interface_name\", payload.get(\"interface_name\", \"\")),\n        \"provider_type\": provider.get(\"provider_type\", payload.get(\"provider_type\", \"\")),\n        \"manager_id\": provider.get(\"manager_id\", payload.get(\"manager_id\", \"\")),\n    }\n\ndef main():\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"--job\", required=True)\n    parser.add_argument(\"--selector-file\", required=True)\n    parser.add_argument(\"--slot-status-json\", required=True)\n    parser.add_argument(\"--selector-helper\", default=\"/usr/local/sbin/wg-access-agent-selector-apply.py\")\n    parser.add_argument(\"--mode\", choices=[\"dry-run\"], default=\"dry-run\")\n    parser.add_argument(\"--output-json\", action=\"store_true\")\n    args = parser.parse_args()\n\n    job = load_json(args.job)\n    flat = flatten_job(job)\n\n    result = {\n        \"schema\": \"wg-access-agent-job-selector-plan-v1\",\n        \"mode\": args.mode,\n        \"job\": {\n            \"schema\": job.get(\"schema\", \"\"),\n            \"job_id\": job.get(\"job_id\", \"\"),\n            \"job_type\": job.get(\"job_type\", \"\"),\n            \"dry_run\": bool(job.get(\"dry_run\", False)),\n        },\n        \"flattened_payload\": flat,\n        \"selector_helper\": args.selector_helper,\n        \"selector_plan_rc\": None,\n        \"selector_plan\": None,\n        \"apply_performed\": False,\n    }\n\n    with tempfile.NamedTemporaryFile(\"w\", encoding=\"utf-8\", delete=False) as tmp:\n        json.dump(flat, tmp, ensure_ascii=False, indent=2, sort_keys=True)\n        tmp_payload = tmp.name\n\n    cmd = [\n        args.selector_helper,\n        \"--payload\", tmp_payload,\n        \"--selector-file\", args.selector_file,\n        \"--slot-status-json\", args.slot_status_json,\n        \"--mode\", \"dry-run\",\n        \"--output-json\",\n    ]\n\n    proc = subprocess.run(cmd, text=True, capture_output=True)\n    result[\"selector_plan_rc\"] = proc.returncode\n    result[\"selector_plan_stdout\"] = proc.stdout\n    result[\"selector_plan_stderr\"] = proc.stderr\n\n    try:\n        plan = json.loads(proc.stdout)\n    except Exception as e:\n        plan = {\n            \"schema\": \"parse-error\",\n            \"contract_valid\": False,\n            \"errors\": [\"selector_plan_json_parse_error:\" + type(e).__name__],\n        }\n\n    result[\"selector_plan\"] = plan\n    result[\"contract_valid\"] = bool(plan.get(\"contract_valid\", False))\n    result[\"action\"] = plan.get(\"action\", \"\")\n    result[\"would_append\"] = bool(plan.get(\"would_append\", False))\n    result[\"idempotent_noop\"] = bool(plan.get(\"idempotent_noop\", False))\n    result[\"selector_entry_count_before\"] = plan.get(\"selector_entry_count_before\")\n    result[\"selector_entry_count_after_planned\"] = plan.get(\"selector_entry_count_after_planned\")\n    result[\"errors\"] = plan.get(\"errors\", [])\n    result[\"error_count\"] = len(result[\"errors\"])\n    result[\"apply_performed\"] = bool(plan.get(\"apply_performed\", False))\n\n    if args.output_json:\n        print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))\n    else:\n        print(\"contract_valid=\" + (\"YES\" if result[\"contract_valid\"] else \"NO\"))\n        print(\"action=\" + str(result[\"action\"]))\n        print(\"selector_plan_rc=\" + str(result[\"selector_plan_rc\"]))\n\n    return 0 if result[\"contract_valid\"] else 2\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n"
      },
      "/usr/local/sbin/wg-access-agent-selector-apply.py": {
        "exists": true,
        "size": 7168,
        "text": "#!/usr/bin/env python3\nimport argparse\nimport ipaddress\nimport json\nimport sys\nfrom pathlib import Path\n\nEXPECTED = {\n    \"vpn1\": {\"selector_class\": \"cs4\", \"selector_label\": \"canary_vpn1\", \"fwmark\": \"0x201\", \"table_id\": 201, \"interface_name\": \"vpn1\"},\n    \"vpn2\": {\"selector_class\": \"cs5\", \"selector_label\": \"canary_vpn2\", \"fwmark\": \"0x202\", \"table_id\": 202, \"interface_name\": \"vpn2\"},\n    \"vpn3\": {\"selector_class\": \"cs1\", \"selector_label\": \"canary_vpn3\", \"fwmark\": \"0x203\", \"table_id\": 203, \"interface_name\": \"vpn3\"},\n    \"vpn4\": {\"selector_class\": \"cs2\", \"selector_label\": \"canary_vpn4\", \"fwmark\": \"0x204\", \"table_id\": 204, \"interface_name\": \"vpn4\"},\n    \"vpn5\": {\"selector_class\": \"cs3\", \"selector_label\": \"canary_vpn5\", \"fwmark\": \"0x205\", \"table_id\": 205, \"interface_name\": \"vpn5\"},\n}\n\ndef load_json(path):\n    return json.loads(Path(path).read_text(encoding=\"utf-8\", errors=\"replace\"))\n\ndef parse_selector(path):\n    entries = []\n    raw_lines = Path(path).read_text(encoding=\"utf-8\", errors=\"replace\").splitlines()\n    for line_no, raw in enumerate(raw_lines, 1):\n        line = raw.strip()\n        if not line or line.startswith(\"#\"):\n            continue\n        parts = line.split()\n        if len(parts) < 3:\n            entries.append({\"line_no\": line_no, \"raw\": line, \"valid\": False})\n            continue\n        entries.append({\n            \"line_no\": line_no,\n            \"raw\": line,\n            \"valid\": True,\n            \"tunnel_ip\": parts[0],\n            \"selector_class\": parts[1],\n            \"selector_label\": parts[2],\n        })\n    return entries\n\ndef load_slots(status_json):\n    data = load_json(status_json)\n    return {slot.get(\"slot_code\"): slot for slot in data.get(\"slots\", [])}\n\ndef validate_and_plan(payload, selector_entries, slots, require_peer_visible=False, peer_visible=False):\n    errors = []\n    warnings = []\n\n    tunnel_ip = str(payload.get(\"tunnel_ip\", \"\"))\n    slot_code = str(payload.get(\"slot_code\", \"\"))\n    selector_class = str(payload.get(\"selector_class\", \"\"))\n    selector_label = str(payload.get(\"selector_label\", \"\"))\n    fwmark = str(payload.get(\"fwmark\", \"\"))\n    table_id = payload.get(\"table_id\")\n    interface_name = str(payload.get(\"interface_name\", \"\"))\n\n    try:\n        ipaddress.ip_address(tunnel_ip)\n    except Exception:\n        errors.append(\"invalid_tunnel_ip\")\n\n    expected = EXPECTED.get(slot_code)\n    if not expected:\n        errors.append(\"unknown_slot_code\")\n    else:\n        for key in (\"selector_class\", \"selector_label\", \"fwmark\", \"table_id\", \"interface_name\"):\n            if payload.get(key) != expected[key]:\n                errors.append(f\"mapping_mismatch_{key}\")\n\n    slot = slots.get(slot_code)\n    if not slot:\n        errors.append(\"slot_status_missing\")\n    else:\n        if slot.get(\"status\") != \"healthy\":\n            errors.append(\"slot_not_healthy\")\n        for key in (\"selector_class\", \"fwmark\", \"table_id\", \"interface_name\"):\n            if slot.get(key) != payload.get(key):\n                errors.append(f\"slot_status_mismatch_{key}\")\n\n    if require_peer_visible and not peer_visible:\n        errors.append(\"wg_peer_not_visible\")\n\n    exact_line = f\"{tunnel_ip} {selector_class} {selector_label}\"\n\n    valid_entries = [entry for entry in selector_entries if entry.get(\"valid\")]\n    exact_present = any(entry[\"raw\"] == exact_line for entry in valid_entries)\n\n    same_ip_conflicts = [\n        entry for entry in valid_entries\n        if entry.get(\"tunnel_ip\") == tunnel_ip and entry[\"raw\"] != exact_line\n    ]\n    same_class_conflicts = [\n        entry for entry in valid_entries\n        if entry.get(\"selector_class\") == selector_class and entry[\"raw\"] != exact_line\n    ]\n    same_label_conflicts = [\n        entry for entry in valid_entries\n        if entry.get(\"selector_label\") == selector_label and entry[\"raw\"] != exact_line\n    ]\n\n    if same_ip_conflicts:\n        errors.append(\"same_ip_conflict\")\n    if same_class_conflicts:\n        errors.append(\"same_selector_class_conflict\")\n    if same_label_conflicts:\n        errors.append(\"same_selector_label_conflict\")\n\n    if errors:\n        action = \"error\"\n    elif exact_present:\n        action = \"noop\"\n    else:\n        action = \"append\"\n\n    return {\n        \"schema\": \"wg-access-agent-selector-apply-plan-v1\",\n        \"contract_valid\": not errors,\n        \"errors\": errors,\n        \"warnings\": warnings,\n        \"action\": action,\n        \"exact_line\": exact_line,\n        \"selector_entry_count_before\": len(valid_entries),\n        \"selector_entry_count_after_planned\": len(valid_entries) + (1 if action == \"append\" else 0),\n        \"exact_present\": exact_present,\n        \"would_append\": action == \"append\",\n        \"idempotent_noop\": action == \"noop\",\n        \"same_ip_conflict_count\": len(same_ip_conflicts),\n        \"same_selector_class_conflict_count\": len(same_class_conflicts),\n        \"same_selector_label_conflict_count\": len(same_label_conflicts),\n        \"slot_status\": slot.get(\"status\") if slot else \"\",\n        \"payload_summary\": {\n            \"peer_id\": payload.get(\"peer_id\", \"\"),\n            \"subscription_id\": payload.get(\"subscription_id\", \"\"),\n            \"tunnel_ip\": tunnel_ip,\n            \"slot_code\": slot_code,\n            \"selector_class\": selector_class,\n            \"selector_label\": selector_label,\n            \"fwmark\": fwmark,\n            \"table_id\": table_id,\n            \"interface_name\": interface_name,\n            \"provider_type\": payload.get(\"provider_type\", \"\"),\n            \"manager_id\": payload.get(\"manager_id\", \"\"),\n        },\n    }\n\ndef main():\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"--payload\", required=True)\n    parser.add_argument(\"--selector-file\", required=True)\n    parser.add_argument(\"--slot-status-json\", required=True)\n    parser.add_argument(\"--mode\", choices=[\"dry-run\", \"apply-local\"], default=\"dry-run\")\n    parser.add_argument(\"--output-json\", action=\"store_true\")\n    args = parser.parse_args()\n\n    payload = load_json(args.payload)\n    selector_entries = parse_selector(Path(args.selector_file))\n    slots = load_slots(Path(args.slot_status_json))\n\n    plan = validate_and_plan(payload, selector_entries, slots)\n\n    plan[\"mode\"] = args.mode\n    plan[\"apply_performed\"] = False\n\n    if args.mode == \"apply-local\":\n        if not plan[\"contract_valid\"]:\n            pass\n        elif plan[\"action\"] == \"append\":\n            selector_path = Path(args.selector_file)\n            text = selector_path.read_text(encoding=\"utf-8\", errors=\"replace\")\n            if text and not text.endswith(\"\\n\"):\n                text += \"\\n\"\n            text += plan[\"exact_line\"] + \"\\n\"\n            selector_path.write_text(text, encoding=\"utf-8\")\n            plan[\"apply_performed\"] = True\n        elif plan[\"action\"] == \"noop\":\n            plan[\"apply_performed\"] = False\n\n    if args.output_json:\n        print(json.dumps(plan, ensure_ascii=False, indent=2, sort_keys=True))\n    else:\n        for key, value in plan.items():\n            if isinstance(value, (dict, list)):\n                continue\n            print(f\"{key}={value}\")\n\n    return 0 if plan[\"contract_valid\"] else 2\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n"
      },
      "/usr/local/sbin/wg-access-agent-selector-contract-dry-run.py": {
        "exists": true,
        "size": 5612,
        "text": "#!/usr/bin/env python3\nimport argparse\nimport ipaddress\nimport json\nimport re\nfrom pathlib import Path\n\nEXPECTED = {\n    \"vpn1\": {\"selector_class\": \"cs4\", \"selector_label\": \"canary_vpn1\", \"fwmark\": \"0x201\", \"table_id\": 201, \"interface_name\": \"vpn1\"},\n    \"vpn2\": {\"selector_class\": \"cs5\", \"selector_label\": \"canary_vpn2\", \"fwmark\": \"0x202\", \"table_id\": 202, \"interface_name\": \"vpn2\"},\n    \"vpn3\": {\"selector_class\": \"cs1\", \"selector_label\": \"canary_vpn3\", \"fwmark\": \"0x203\", \"table_id\": 203, \"interface_name\": \"vpn3\"},\n    \"vpn4\": {\"selector_class\": \"cs2\", \"selector_label\": \"canary_vpn4\", \"fwmark\": \"0x204\", \"table_id\": 204, \"interface_name\": \"vpn4\"},\n    \"vpn5\": {\"selector_class\": \"cs3\", \"selector_label\": \"canary_vpn5\", \"fwmark\": \"0x205\", \"table_id\": 205, \"interface_name\": \"vpn5\"},\n}\n\ndef load_selector(path: Path):\n    entries = []\n    for raw in path.read_text(encoding=\"utf-8\", errors=\"replace\").splitlines():\n        line = raw.strip()\n        if not line or line.startswith(\"#\"):\n            continue\n        parts = line.split()\n        if len(parts) < 3:\n            entries.append({\"raw\": line, \"valid\": False})\n            continue\n        entries.append({\n            \"raw\": line,\n            \"valid\": True,\n            \"tunnel_ip\": parts[0],\n            \"selector_class\": parts[1],\n            \"selector_label\": parts[2],\n        })\n    return entries\n\ndef load_status(path: Path):\n    data = json.loads(path.read_text(encoding=\"utf-8\", errors=\"replace\"))\n    return {s.get(\"slot_code\"): s for s in data.get(\"slots\", [])}\n\ndef bool_text(v):\n    return \"YES\" if v else \"NO\"\n\ndef main():\n    ap = argparse.ArgumentParser()\n    ap.add_argument(\"--payload\", required=True)\n    ap.add_argument(\"--selector-file\", required=True)\n    ap.add_argument(\"--slot-status-json\", required=True)\n    ap.add_argument(\"--json\", action=\"store_true\")\n    args = ap.parse_args()\n\n    payload = json.loads(Path(args.payload).read_text(encoding=\"utf-8\", errors=\"replace\"))\n    entries = load_selector(Path(args.selector_file))\n    slots = load_status(Path(args.slot_status_json))\n\n    errors = []\n    warnings = []\n\n    tunnel_ip = str(payload.get(\"tunnel_ip\", \"\"))\n    slot_code = str(payload.get(\"slot_code\", \"\"))\n    selector_class = str(payload.get(\"selector_class\", \"\"))\n    selector_label = str(payload.get(\"selector_label\", \"\"))\n    fwmark = str(payload.get(\"fwmark\", \"\"))\n    table_id = payload.get(\"table_id\")\n    interface_name = str(payload.get(\"interface_name\", \"\"))\n    dry_run_allow_missing_peer = bool(payload.get(\"dry_run_allow_missing_peer\", False))\n\n    try:\n        ipaddress.ip_address(tunnel_ip)\n    except Exception:\n        errors.append(\"invalid_tunnel_ip\")\n\n    expected = EXPECTED.get(slot_code)\n    if not expected:\n        errors.append(\"unknown_slot_code\")\n    else:\n        for key in (\"selector_class\", \"selector_label\", \"fwmark\", \"table_id\", \"interface_name\"):\n            if payload.get(key) != expected[key]:\n                errors.append(f\"mapping_mismatch_{key}\")\n\n    slot_status = slots.get(slot_code)\n    if not slot_status:\n        errors.append(\"slot_status_missing\")\n    else:\n        if slot_status.get(\"status\") != \"healthy\":\n            errors.append(\"slot_not_healthy\")\n        for key in (\"selector_class\", \"fwmark\", \"table_id\", \"interface_name\"):\n            if slot_status.get(key) != payload.get(key):\n                errors.append(f\"slot_status_mismatch_{key}\")\n\n    exact_line = f\"{tunnel_ip} {selector_class} {selector_label}\"\n\n    valid_entries = [e for e in entries if e.get(\"valid\")]\n    exact_present = any(e[\"raw\"] == exact_line for e in valid_entries)\n\n    same_ip_conflicts = [\n        e for e in valid_entries\n        if e.get(\"tunnel_ip\") == tunnel_ip and e[\"raw\"] != exact_line\n    ]\n    same_class_conflicts = [\n        e for e in valid_entries\n        if e.get(\"selector_class\") == selector_class and e[\"raw\"] != exact_line\n    ]\n    same_label_conflicts = [\n        e for e in valid_entries\n        if e.get(\"selector_label\") == selector_label and e[\"raw\"] != exact_line\n    ]\n\n    if same_ip_conflicts:\n        errors.append(\"same_ip_conflict\")\n    if same_class_conflicts:\n        errors.append(\"same_selector_class_conflict\")\n    if same_label_conflicts:\n        errors.append(\"same_selector_label_conflict\")\n\n    would_append = not exact_present and not errors\n    idempotent_noop = exact_present and not errors\n\n    simulated_entries = len(valid_entries) + (1 if would_append else 0)\n\n    result = {\n        \"schema\": \"wg-access-agent-selector-contract-dry-run-v1\",\n        \"contract_valid\": not errors,\n        \"errors\": errors,\n        \"warnings\": warnings,\n        \"payload\": payload,\n        \"exact_line\": exact_line,\n        \"selector_entry_count_before\": len(valid_entries),\n        \"selector_entry_count_after_simulated\": simulated_entries,\n        \"exact_present\": exact_present,\n        \"would_append\": would_append,\n        \"idempotent_noop\": idempotent_noop,\n        \"dry_run_allow_missing_peer\": dry_run_allow_missing_peer,\n        \"slot_status\": slot_status.get(\"status\") if slot_status else \"\",\n        \"same_ip_conflict_count\": len(same_ip_conflicts),\n        \"same_selector_class_conflict_count\": len(same_class_conflicts),\n        \"same_selector_label_conflict_count\": len(same_label_conflicts),\n    }\n\n    if args.json:\n        print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))\n    else:\n        for k, v in result.items():\n            if isinstance(v, (dict, list)):\n                continue\n            print(f\"{k}={v}\")\n\n    raise SystemExit(0 if not errors else 2)\n\nif __name__ == \"__main__\":\n    main()\n"
      },
      "/usr/local/sbin/wg-access-agent-selector-remote-plan.py": {
        "exists": true,
        "size": 6279,
        "text": "#!/usr/bin/env python3\nimport argparse\nimport hashlib\nimport json\nimport shlex\nimport subprocess\nfrom pathlib import Path\n\ndef load_json(path):\n    return json.loads(Path(path).read_text(encoding=\"utf-8\", errors=\"replace\"))\n\ndef q(value):\n    return shlex.quote(str(value))\n\ndef build_remote_script(payload, selector_plan, selector_conf, selector_service):\n    exact_line = selector_plan.get(\"exact_line\", \"\")\n    action = selector_plan.get(\"action\", \"\")\n    tunnel_ip = str(payload.get(\"tunnel_ip\", \"\"))\n    selector_class = str(payload.get(\"selector_class\", \"\"))\n\n    if action == \"append\":\n        apply_block = \"\"\"\nif grep -Fxq \"$LINE\" \"$CONF\"; then\n  echo \"selector_apply_action=noop_already_present\"\nelse\n  printf '%s\\\\n' \"$LINE\" >> \"$CONF\"\n  echo \"selector_apply_action=append\"\nfi\n\"\"\"\n    elif action == \"noop\":\n        apply_block = \"\"\"\ngrep -Fxq \"$LINE\" \"$CONF\"\necho \"selector_apply_action=noop_verified_present\"\n\"\"\"\n    else:\n        apply_block = \"\"\"\necho \"selector_apply_action=error_invalid_plan\"\nexit 42\n\"\"\"\n\n    script = f\"\"\"#!/bin/sh\nset -u\n\nCONF={q(selector_conf)}\nSERVICE={q(selector_service)}\nLINE={q(exact_line)}\nTUNNEL_IP={q(tunnel_ip)}\nSELECTOR_CLASS={q(selector_class)}\nBACKUP=\"/root/peers.conf.before-agent-selector-apply-$(date +%Y%m%d-%H%M%S)\"\nROLLBACK=\"/root/rollback-agent-selector-apply.sh\"\n\necho \"remote_script_schema=wg-access-agent-selector-remote-script-v3\"\necho \"remote_script_action={action}\"\n\ntest -f \"$CONF\"\n\ncp \"$CONF\" \"$BACKUP\"\necho \"selector_backup_path=$BACKUP\"\n\ncat > \"$ROLLBACK\" <<RB\n#!/bin/sh\nset -u\ncp \"$BACKUP\" \"$CONF\"\n/etc/init.d/$SERVICE start\nRB\nchmod 700 \"$ROLLBACK\"\necho \"rollback_path=$ROLLBACK\"\n\n{apply_block}\n\n/etc/init.d/$SERVICE start\n\ngrep -Fxq \"$LINE\" \"$CONF\"\n/etc/init.d/$SERVICE enabled >/dev/null 2>&1\nnft -a list ruleset 2>/dev/null | grep -q \"$TUNNEL_IP.*$SELECTOR_CLASS\"\n\necho \"postcheck_selector_line_present=YES\"\necho \"postcheck_selector_service_enabled=YES\"\necho \"postcheck_nft_rule_visible=YES\"\n\"\"\"\n    return script\n\ndef main():\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"--payload\", required=True)\n    parser.add_argument(\"--selector-file\", required=True)\n    parser.add_argument(\"--slot-status-json\", required=True)\n    parser.add_argument(\"--selector-helper\", default=\"/usr/local/sbin/wg-access-agent-selector-apply.py\")\n    parser.add_argument(\"--target-host\", default=\"10.71.100.1\")\n    parser.add_argument(\"--target-name\", default=\"vm100\")\n    parser.add_argument(\"--selector-conf\", default=\"/etc/router-wgpay-selector.d/peers.conf\")\n    parser.add_argument(\"--selector-service\", default=\"router-wgpay-canary\")\n    parser.add_argument(\"--mode\", choices=[\"remote-dry-run\"], default=\"remote-dry-run\")\n    parser.add_argument(\"--output-json\", action=\"store_true\")\n    args = parser.parse_args()\n\n    payload = load_json(args.payload)\n\n    helper_cmd = [\n        args.selector_helper,\n        \"--payload\", args.payload,\n        \"--selector-file\", args.selector_file,\n        \"--slot-status-json\", args.slot_status_json,\n        \"--mode\", \"dry-run\",\n        \"--output-json\",\n    ]\n\n    proc = subprocess.run(helper_cmd, text=True, capture_output=True)\n\n    try:\n        selector_plan = json.loads(proc.stdout)\n    except Exception as e:\n        selector_plan = {\n            \"schema\": \"parse-error\",\n            \"contract_valid\": False,\n            \"errors\": [\"selector_plan_json_parse_error:\" + type(e).__name__],\n        }\n\n    contract_valid = bool(selector_plan.get(\"contract_valid\", False))\n    action = selector_plan.get(\"action\", \"\")\n    remote_script = build_remote_script(payload, selector_plan, args.selector_conf, args.selector_service)\n\n    result = {\n        \"schema\": \"wg-access-agent-selector-remote-plan-v3\",\n        \"mode\": args.mode,\n        \"remote_script_model\": \"single_shell_script\",\n        \"target\": {\n            \"name\": args.target_name,\n            \"host\": args.target_host,\n            \"selector_conf\": args.selector_conf,\n            \"selector_service\": args.selector_service\n        },\n        \"payload_summary\": {\n            \"tunnel_ip\": payload.get(\"tunnel_ip\", \"\"),\n            \"slot_code\": payload.get(\"slot_code\", \"\"),\n            \"selector_class\": payload.get(\"selector_class\", \"\"),\n            \"selector_label\": payload.get(\"selector_label\", \"\"),\n            \"fwmark\": payload.get(\"fwmark\", \"\"),\n            \"table_id\": payload.get(\"table_id\", \"\"),\n            \"interface_name\": payload.get(\"interface_name\", \"\"),\n            \"provider_type\": payload.get(\"provider_type\", \"\"),\n            \"manager_id\": payload.get(\"manager_id\", \"\")\n        },\n        \"selector_plan_rc\": proc.returncode,\n        \"selector_plan\": selector_plan,\n        \"contract_valid\": contract_valid,\n        \"action\": action,\n        \"would_append\": bool(selector_plan.get(\"would_append\", False)),\n        \"idempotent_noop\": bool(selector_plan.get(\"idempotent_noop\", False)),\n        \"exact_line\": selector_plan.get(\"exact_line\", \"\"),\n        \"requires_wg_peer_before_real_apply\": True,\n        \"remote_commands_executed\": False,\n        \"apply_performed\": False,\n        \"remote_script_executed\": False,\n        \"remote_script_sha256\": hashlib.sha256(remote_script.encode(\"utf-8\")).hexdigest(),\n        \"remote_script_line_count\": len(remote_script.splitlines()),\n        \"remote_script_contains_runtime_backup_date\": \"$(date +%Y%m%d-%H%M%S)\" in remote_script,\n        \"remote_script_contains_actual_backup_rollback\": 'cp \"$BACKUP\" \"$CONF\"' in remote_script,\n        \"remote_script_contains_placeholder_backup\": \"YYYYmmdd-HHMMSS\" in remote_script,\n        \"remote_script_contains_bad_etc_initd_quoted_service\": '/etc/init.d \"$SERVICE\"' in remote_script,\n        \"remote_script_contains_clean_service_start\": \"/etc/init.d/$SERVICE start\" in remote_script,\n        \"remote_script\": remote_script,\n        \"errors\": selector_plan.get(\"errors\", []),\n        \"error_count\": len(selector_plan.get(\"errors\", [])),\n    }\n\n    if args.output_json:\n        print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))\n    else:\n        print(\"contract_valid=\" + (\"YES\" if contract_valid else \"NO\"))\n        print(\"action=\" + str(action))\n        print(\"remote_script_executed=NO\")\n\n    return 0 if contract_valid else 2\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n"
      },
      "/usr/local/sbin/wg-access-expire-subscriptions.sh": {
        "exists": true,
        "size": 1247,
        "text": "#!/usr/bin/env bash\nset -euo pipefail\n\nURL=\"${WG_ACCESS_EXPIRE_URL:-http://127.0.0.1:18080/admin/maintenance/expire-subscriptions}\"\nLOG_DIR=\"/opt/wg-access/maintenance-logs\"\nLOCK=\"/run/wg-access-expire-subscriptions.lock\"\n\nmkdir -p \"$LOG_DIR\"\n\nts=\"$(date -Is)\"\nout=\"$LOG_DIR/expire-subscriptions.$(date +%Y%m%d).log\"\n\n{\n  echo \"== $ts expire-subscriptions ==\"\n\n  flock -n 9 || {\n    echo \"another expire-subscriptions run is already active\"\n    exit 0\n  }\n\n  rm -f /tmp/wg-access-expire-subscriptions.out /tmp/wg-access-expire-subscriptions.err\n\n  code=\"$(\n    curl -fsS \\\n      -m 30 \\\n      -X POST \\\n      -o /tmp/wg-access-expire-subscriptions.out \\\n      -w \"%{http_code}\" \\\n      \"$URL\" 2>/tmp/wg-access-expire-subscriptions.err || true\n  )\"\n\n  echo \"http_code=$code\"\n\n  if [ -s /tmp/wg-access-expire-subscriptions.out ]; then\n    cat /tmp/wg-access-expire-subscriptions.out\n    echo\n  fi\n\n  if [ -s /tmp/wg-access-expire-subscriptions.err ]; then\n    echo \"stderr:\"\n    cat /tmp/wg-access-expire-subscriptions.err\n  fi\n\n  rm -f /tmp/wg-access-expire-subscriptions.out /tmp/wg-access-expire-subscriptions.err\n\n  case \"$code\" in\n    2*) exit 0 ;;\n    *) echo \"ERROR: expire-subscriptions failed\"; exit 1 ;;\n  esac\n} 9>\"$LOCK\" | tee -a \"$out\"\n"
      }
    }
  },
  "capability_matrix": {
    "agent_has_reconcile_logic": true,
    "agent_service_unit_present": true,
    "agent_timer_enabled": false,
    "backend_can_touch_host_bind_mount_likely": true,
    "backend_creates_provisioning_jobs": true,
    "backend_existing_host_systemctl_trigger_seen": false,
    "backend_existing_postgres_notify_seen": false,
    "backend_has_subscription_create_flow": true,
    "reconcile_service_unit_present": false,
    "reconcile_timer_unit_present": false,
    "selector_logic_seen": true
  },
  "changes": {
    "backend_changed": "NO",
    "manual_agent_start_used": "NO",
    "services_enabled": "NO",
    "services_started": "NO",
    "vm100_changed": "NO",
    "vm101_changed": "NO",
    "vm121_changed": "NO"
  },
  "design_options": {
    "A_backend_calls_host_systemctl_directly": {
      "description": "Backend directly runs systemctl start wg-access-agent.service after DB commit.",
      "reason": "Usually wrong from Docker container unless host systemd/socket is deliberately exposed; inventory did not confirm existing safe host-systemctl bridge.",
      "recommended": false
    },
    "B_backend_writes_trigger_file_host_systemd_path": {
      "description": "Backend touches a bind-mounted trigger file; host systemd.path starts wg-access-agent.service.",
      "reason": "Simple event trigger if backend has shared bind mount to /opt/wg-access or a dedicated trigger directory. Needs exact mount confirmation and code insertion after commit.",
      "recommended": true
    },
    "C_postgres_notify_host_listener": {
      "description": "DB trigger or backend issues NOTIFY; host watcher listens and starts agent.",
      "reason": "Clean event-driven model but more moving parts; good later if file trigger is unsuitable.",
      "recommended": false
    },
    "D_small_host_job_watcher_polling": {
      "description": "Host daemon polls pending jobs every few seconds and starts agent.",
      "reason": "Reliable fallback but reintroduces polling; acceptable as watchdog, not primary happy path.",
      "recommended": false
    },
    "E_enable_existing_agent_timer_as_primary": {
      "description": "Enable wg-access-agent.timer and let it poll jobs.",
      "reason": "Contradicts desired event-driven contract; useful only as temporary safety if no trigger exists.",
      "recommended": false
    },
    "F_reconcile_on_boot_plus_safety_timer": {
      "description": "Add explicit reconcile service/timer to restore VM100 runtime/selector from VM121 DB after reboot/drift.",
      "reason": "Needed because VM100 peers are runtime-only and UCI remains empty by design.",
      "recommended": true
    }
  },
  "docker": {
    "backend_env_names": {
      "rc": 0,
      "stderr": "",
      "stdout": "AGENT_TOKEN=<redacted>\nAPP_SECRET=<redacted>\nBACKEND_PORT=<redacted>\nDATABASE_URL=<redacted>\nENVIRONMENT=<redacted>\nGPG_KEY=<redacted>\nLANG=<redacted>\nPATH=<redacted>\nPOSTGRES_DB=<redacted>\nPOSTGRES_PASSWORD=<redacted>\nPOSTGRES_USER=<redacted>\nPYTHON_SHA256=<redacted>\nPYTHON_VERSION=<redacted>\nWG_CLIENT_ALLOWED_IPS=<redacted>\nWG_CLIENT_ENDPOINT=<redacted>\nWG_CLIENT_FIRST_IP=<redacted>\nWG_CLIENT_POOL_CIDR=<redacted>\nWG_SERVER_PUBLIC_KEY=<redacted>"
    },
    "backend_mounts": {
      "rc": 0,
      "stderr": "",
      "stdout": "[{\"Type\":\"bind\",\"Source\":\"/opt/wg-access/backend\",\"Destination\":\"/app\",\"Mode\":\"rw\",\"RW\":true,\"Propagation\":\"rprivate\"}]"
    },
    "compose_contents": {
      "/opt/wg-access/docker-compose.yml": {
        "exists": true,
        "size": 964,
        "text": "services:\n  postgres:\n    image: postgres:16-alpine\n    container_name: wgaccess-postgres\n    restart: unless-stopped\n    env_file:\n      - .env\n    volumes:\n      - wgaccess_pgdata:/var/lib/postgresql/data\n    ports:\n      - \"127.0.0.1:15432:5432\"\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U wg_access -d wg_access\"]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n\n  backend:\n    image: python:3.12-slim\n    container_name: wgaccess-backend\n    restart: unless-stopped\n    working_dir: /app\n    volumes:\n      - ./backend:/app\n    env_file:\n      - .env\n    ports:\n      - \"0.0.0.0:${BACKEND_PORT:-18080}:8000\"\n    command: >\n      sh -c \"\n      pip install --no-cache-dir fastapi uvicorn psycopg[binary] sqlalchemy alembic pydantic-settings email-validator cryptography &&\n      uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload\n      \"\n    depends_on:\n      postgres:\n        condition: service_healthy\n\nvolumes:\n  wgaccess_pgdata:\n"
      }
    },
    "compose_files": {
      "rc": 0,
      "stderr": "",
      "stdout": "/opt/wg-access/docker-compose.yml"
    },
    "ps": {
      "rc": 0,
      "stderr": "",
      "stdout": "wgaccess-backend python:3.12-slim Up 16 hours\nwgaccess-postgres postgres:16-alpine Up 16 hours (healthy)"
    }
  },
  "errors": [],
  "host": {
    "hostname": "wg-access-dev",
    "pwd": "/root"
  },
  "mode": "READONLY_DESIGN_INVENTORY",
  "recommended_contract": {
    "agent_contract": [
      "wg-access-agent.service remains Type=oneshot.",
      "It processes pending jobs idempotently.",
      "It may be started by event trigger.",
      "Happy-path tests must not call systemctl start manually."
    ],
    "backend_create_contract": [
      "Create peer and provisioning_job inside DB transaction.",
      "Commit transaction.",
      "Emit trigger after commit only.",
      "Do not publish private material in logs/reports."
    ],
    "happy_path_manual_agent_start_allowed": false,
    "implementation_order": [
      "042C implement chosen event trigger.",
      "042D implement explicit reconcile service/timer.",
      "042E happy-path create without manual agent start.",
      "042F VM100 reboot/reconcile recovery test.",
      "043 harness boolean contract fixes."
    ],
    "primary_happy_path_trigger": "B_backend_writes_trigger_file_host_systemd_path",
    "reconcile_contract": [
      "Create explicit wg-access-reconcile.service.",
      "Run reconcile on VM121 boot after network/docker/backend readiness.",
      "Add low-frequency safety timer, e.g. 5 minutes, for drift/reboot recovery.",
      "Reconcile restores runtime wg_paid peers and selector from VM121 DB/desired state.",
      "VM100 UCI peer persistence remains absent."
    ],
    "test_contract": [
      "042C/042D tests must record agent_start_source.",
      "manual_agent_start_used=False required for happy path.",
      "If backend create does not trigger agent, test must STOP_AGENT_EVENT_TRIGGER_BROKEN.",
      "VM100 reboot test must verify runtime peers vanish or reset, then recover via reconcile without manual agent start."
    ]
  },
  "schema": "router-step-facts-v1",
  "step": "STEP_042B_AGENT_EVENT_TRIGGER_RECONCILE_DESIGN_READONLY",
  "systemd": {
    "docker.service": {
      "cat": {
        "rc": 0,
        "stderr": "",
        "stdout": "# /lib/systemd/system/docker.service\n[Unit]\nDescription=Docker Application Container Engine\nDocumentation=https://docs.docker.com\nAfter=network-online.target nss-lookup.target docker.socket firewalld.service containerd.service time-set.target\nWants=network-online.target containerd.service\nRequires=docker.socket\nStartLimitBurst=3\nStartLimitIntervalSec=60\n\n[Service]\nType=notify\n# the default is not to use systemd for cgroups because the delegate issues still\n# exists and systemd currently does not support the cgroup feature set required\n# for containers run by docker\nExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock\nExecReload=/bin/kill -s HUP $MAINPID\nTimeoutStartSec=0\nRestartSec=2\nRestart=always\n\n# Having non-zero Limit*s causes performance problems due to accounting overhead\n# in the kernel. We recommend using cgroups to do container-local accounting.\nLimitNPROC=infinity\nLimitCORE=infinity\n\n# Comment TasksMax if your systemd version does not support it.\n# Only systemd 226 and above support this option.\nTasksMax=infinity\n\n# set delegate yes so that systemd does not reset the cgroups of docker containers\nDelegate=yes\n\n# kill only the docker process, not all processes in the cgroup\nKillMode=process\nOOMScoreAdjust=-500\n\n[Install]\nWantedBy=multi-user.target"
      },
      "is_active": {
        "rc": 0,
        "stderr": "",
        "stdout": "active"
      },
      "is_enabled": {
        "rc": 0,
        "stderr": "",
        "stdout": "enabled"
      }
    },
    "wg-access-agent.service": {
      "cat": {
        "rc": 0,
        "stderr": "",
        "stdout": "# /etc/systemd/system/wg-access-agent.service\n[Unit]\nDescription=WG Access Agent real poller\nAfter=network-online.target docker.service\nWants=network-online.target\n\n[Service]\nType=oneshot\nUser=root\nWorkingDirectory=/opt/wg-access/agent\nEnvironment=WG_ACCESS_AGENT_ENV=/opt/wg-access/agent/agent.env\nExecStart=/opt/wg-access/agent/wg_access_agent.py"
      },
      "is_active": {
        "rc": 3,
        "stderr": "",
        "stdout": "inactive"
      },
      "is_enabled": {
        "rc": 0,
        "stderr": "",
        "stdout": "static"
      }
    },
    "wg-access-agent.timer": {
      "cat": {
        "rc": 0,
        "stderr": "",
        "stdout": "# /etc/systemd/system/wg-access-agent.timer\n[Unit]\nDescription=Run WG Access Agent periodically\n\n[Timer]\nOnActiveSec=5sec\nOnUnitInactiveSec=30sec\nAccuracySec=5sec\nUnit=wg-access-agent.service\n\n[Install]\nWantedBy=timers.target"
      },
      "is_active": {
        "rc": 3,
        "stderr": "",
        "stdout": "inactive"
      },
      "is_enabled": {
        "rc": 1,
        "stderr": "",
        "stdout": "disabled"
      }
    },
    "wg-access-expire-subscriptions.timer": {
      "cat": {
        "rc": 0,
        "stderr": "",
        "stdout": "# /etc/systemd/system/wg-access-expire-subscriptions.timer\n[Unit]\nDescription=Run WG Access expire subscriptions maintenance periodically\n\n[Timer]\nOnBootSec=2min\nOnUnitActiveSec=5min\nAccuracySec=30s\nRandomizedDelaySec=30s\nPersistent=true\n\n[Install]\nWantedBy=timers.target"
      },
      "is_active": {
        "rc": 0,
        "stderr": "",
        "stdout": "active"
      },
      "is_enabled": {
        "rc": 0,
        "stderr": "",
        "stdout": "enabled"
      }
    },
    "wg-access-reconcile.service": {
      "cat": {
        "rc": 1,
        "stderr": "No files found for wg-access-reconcile.service.",
        "stdout": ""
      },
      "is_active": {
        "rc": 3,
        "stderr": "",
        "stdout": "inactive"
      },
      "is_enabled": {
        "rc": 1,
        "stderr": "Failed to get unit file state for wg-access-reconcile.service: No such file or directory",
        "stdout": ""
      }
    },
    "wg-access-reconcile.timer": {
      "cat": {
        "rc": 1,
        "stderr": "No files found for wg-access-reconcile.timer.",
        "stdout": ""
      },
      "is_active": {
        "rc": 3,
        "stderr": "",
        "stdout": "inactive"
      },
      "is_enabled": {
        "rc": 1,
        "stderr": "Failed to get unit file state for wg-access-reconcile.timer: No such file or directory",
        "stdout": ""
      }
    }
  }
}
