From 53dc4eefeb72f2b05760a9f4847b6fcf2e1487b0 Mon Sep 17 00:00:00 2001 From: homelab-runtime-publisher Date: Tue, 6 Oct 2026 18:03:00 +0000 Subject: [PATCH] runtime: publish SUPPORT-261006-SNIKKET-HOME-ROLLBACK-PREFLIGHT-99740R1 --- ...IKKET-HOME-ROLLBACK-PREFLIGHT-99740R1.json | 38 ++ ...NIKKET-HOME-ROLLBACK-PREFLIGHT-99740R1.txt | 99 ++++ runtime/latest.json | 16 +- runtime/latest.txt | 467 +++--------------- 4 files changed, 209 insertions(+), 411 deletions(-) create mode 100644 runtime/history/SUPPORT-261006-SNIKKET-HOME-ROLLBACK-PREFLIGHT-99740R1.json create mode 100644 runtime/history/SUPPORT-261006-SNIKKET-HOME-ROLLBACK-PREFLIGHT-99740R1.txt diff --git a/runtime/history/SUPPORT-261006-SNIKKET-HOME-ROLLBACK-PREFLIGHT-99740R1.json b/runtime/history/SUPPORT-261006-SNIKKET-HOME-ROLLBACK-PREFLIGHT-99740R1.json new file mode 100644 index 00000000..984956af --- /dev/null +++ b/runtime/history/SUPPORT-261006-SNIKKET-HOME-ROLLBACK-PREFLIGHT-99740R1.json @@ -0,0 +1,38 @@ +{ + "schema_version": 1, + "channel": "homelab-runtime", + "command_id": "SUPPORT-261006-SNIKKET-HOME-ROLLBACK-PREFLIGHT-99740R1", + "status": "OK", + "rc": 0, + "host": "pve01", + "mode": "read-only", + "component": "snikket-home-rollback-preflight", + "started_at_utc": "2026-10-06T18:02:31Z", + "finished_at_utc": "2026-10-06T18:03:00Z", + "reference_register_checked": true, + "reference_sha256": "5a3d8e5154c41cb582a4e0aca68090be1f0138918bf82131a948df326f9d8d66", + "error_register_checked": true, + "error_register_sha256": "3b09a553ec0f527ed3afeed4753f52a74ee3036045bbfb3c685e9f8af4ba7ba0", + "command_sha256": "4336c13341dc97c6ef0a781e34aa07cfa6829de4686391dfbda4b3b00dc90150", + "duplicate_failed_command_blocked": false, + "block_reason": null, + "execution_started": true, + "change_declared": false, + "result_contract_valid": true, + "result_contract_status": null, + "result_contract_error": null, + "command_rc": 0, + "changes_made": false, + "rollback_started": false, + "rollback_restored": null, + "mutation_outcome": "NO_MUTATION", + "sanitized": true, + "secrets_included": false, + "private_addresses_included": false, + "raw_evidence_retained_locally": true, + "raw_evidence_sha256": "4737052a112e41dfe5fe93073b59c14131211a94191d7ad6c2dd0dd76e8164c6", + "sanitized_output_sha256": "4737052a112e41dfe5fe93073b59c14131211a94191d7ad6c2dd0dd76e8164c6", + "output_truncated_in_json": false, + "full_sanitized_output_url": "https://git.gram1.ru/.well-known/homelab-runtime/latest.txt", + "output": "SNIKKET_HOME_ROLLBACK_PREFLIGHT=V1\nMODE=read-only\nNO_MUTATION=true\nSOURCE_HEAD=bd85699d8b588a34c480f10dee71323687bd48f1\nSOURCE_WORKTREE_DIRTY_COUNT=0\nSOURCE_ROLLBACK_NETWORK_PRESENT=true\nSOURCE_ROLLBACK_USES_CLOUDFLARE=true\nROLLBACK_STATE_PRESENT=true\nROLLBACK_STATE_SHA=b4696a99b8e13042680d3c56537e0e5e5b6d3e7fae69e01fb8b8f60d68c39fc7\nROLLBACK_STATE_MODE=600\nROLLBACK_STATE_MTIME=1786953752\nROLLBACK_STATE_JSON_VALID=true\nROLLBACK_STATE_HAS_DNS_CONTENT=true\nROLLBACK_STATE_HAS_DEFAULT_ROUTE=true\nROLLBACK_OLD_ROUTE_SHA=7c5515603e424ad8e85718ba716657be12298988ee11416f41081ec82e023409\nROLLBACK_HOME_IP_SHA=26ac437d63a8bcf254255cc970d7614e78bd0a67b97b5575a42a1c3ca7a301ee\nPVE01_IPIFY_RC=0\nPVE01_PUBLIC_IP_SHA=26ac437d63a8bcf254255cc970d7614e78bd0a67b97b5575a42a1c3ca7a301ee\nROLLBACK_HOME_MATCHES_CURRENT_PVE01_PUBLIC=true\nVM150_QGA_TRANSPORT_RC=0\nREC|VM150|QGA_EXIT=0\nREC|VM150|DOMAIN_SHA=04cb4a04e408d91abb9c63701ae969abf16d8a12b6f4f144c5edf2c28e03256b\nREC|VM150|EDGE_EGRESS_UNIT_PRESENT=true\nREC|VM150|EDGE_EGRESS_ACTIVE=active\nREC|VM150|EDGE_EGRESS_ENABLED=enabled\nREC|VM150|EDGE_EGRESS_UNIT_SHA=83a20356e221dfb22ea2a0d99e327f7c2f791a3d3127a46af2d1625b33c95b5d\nREC|VM150|OLDGW_SHA=4f9da1283166a1e9f4e6bc7dd240c76aef85772177f717f92abb6e8e2bb8de90\nREC|VM150|NEWGW_SHA=bc517b4af3298846906234d3d697d3820b0b652ade1880c937c9d71cd802c834\nREC|VM150|OLDGW_PING_RC=1\nREC|VM150|CURRENT_GW_SHA=bc517b4af3298846906234d3d697d3820b0b652ade1880c937c9d71cd802c834\nREC|VM150|REC|CONTAINER|NAME=snikket|RUNNING=true|RESTARTS=0\nREC|VM150|REC|CONTAINER|NAME=snikket-proxy|RUNNING=true|RESTARTS=0\nREC|VM150|REC|CONTAINER|NAME=snikket-certs|RUNNING=true|RESTARTS=0\nREC|VM150|REC|CONTAINER|NAME=snikket-portal|RUNNING=true|RESTARTS=0\nREC|VM150|LOCAL_HTTPS_RC=0\nREC|VM150|LOCAL_HTTPS_CODE=302\nVM130_QGA_TRANSPORT_RC=0\nREC|VM130|QGA_EXIT=0\nREC|VM130|ROLE=VM130\nREC|VM130|WG_CONFIG_PRESENT=true\nREC|VM130|WG_BACKUP_PRESENT=true\nREC|VM130|WG_CONFIG_SHA=42b046d580b2ccd6af8f3573fba4882c71c92b2938f3fed8edf58258bbc89615\nREC|VM130|WG_BACKUP_SHA=9012ece867ad7efa1b2d07020c1cdfc9d3a63f86fde9117bdd7435eef29a9735\nREC|VM130|WG_BACKUP_MODE=600\nREC|VM130|PRIORITY101_COUNT=1\nREC|VM130|TABLE17777_DEFAULT_COUNT=1\nREC|VM130|WG_EDGE_ACTIVE=active\nCURRENT_AUTHORITATIVE_BEGET=true\nREC|DNS|ROLE=primary|A_COUNT=1|MATCH_ROLLBACK_HOME=true\nREC|DNS|ROLE=groups|A_COUNT=1|MATCH_ROLLBACK_HOME=true\nREC|DNS|ROLE=share|A_COUNT=1|MATCH_ROLLBACK_HOME=true\nBEGET_LOGIN_CANDIDATE_COUNT=0\nBEGET_PASSWORD_CANDIDATE_COUNT=0\nBEGET_CREDENTIALS_UNIQUE=false\nREC|HOME_TCP|PORT=443|OPEN=true|MSEC=1\nREC|HOME_TCP|PORT=5000|OPEN=true|MSEC=0\nREC|HOME_TCP|PORT=5222|OPEN=true|MSEC=0\nREC|HOME_TCP|PORT=5349|OPEN=false|ERR=TimeoutError\nROLLBACK_PREFLIGHT_READY=false\nDECISION=PASS_SNIKKET_HOME_ROLLBACK_PREFLIGHT\nNEXT_GATE=ROLLBACK_PREFLIGHT_BLOCKER_REPAIR\nTASK_RESULT=SNIKKET_HOME_ROLLBACK_PREFLIGHT\nCHANGES_MADE=false\nROLLBACK_STARTED=false\nROLLBACK_RESTORED=null\nMUTATION_OUTCOME=NO_MUTATION\nHOMELAB_RESULT_CONTRACT={\"version\":1,\"command_id\":\"SUPPORT-261006-SNIKKET-HOME-ROLLBACK-PREFLIGHT-99740R1\",\"status\":\"OK\",\"changes_made\":false,\"rollback_started\":false,\"rollback_restored\":null}\n" +} diff --git a/runtime/history/SUPPORT-261006-SNIKKET-HOME-ROLLBACK-PREFLIGHT-99740R1.txt b/runtime/history/SUPPORT-261006-SNIKKET-HOME-ROLLBACK-PREFLIGHT-99740R1.txt new file mode 100644 index 00000000..c299c75d --- /dev/null +++ b/runtime/history/SUPPORT-261006-SNIKKET-HOME-ROLLBACK-PREFLIGHT-99740R1.txt @@ -0,0 +1,99 @@ +CHAT_OUTPUT_BEGIN +COMMAND_ID=SUPPORT-261006-SNIKKET-HOME-ROLLBACK-PREFLIGHT-99740R1 +STATUS=OK +RC=0 +HOST=pve01 +MODE=read-only +COMPONENT=snikket-home-rollback-preflight +REFERENCE_REGISTER_CHECK=OK +REFERENCE_SHA256=5a3d8e5154c41cb582a4e0aca68090be1f0138918bf82131a948df326f9d8d66 +ERROR_REGISTER_CHECK=OK +ERROR_REGISTER_SHA256=3b09a553ec0f527ed3afeed4753f52a74ee3036045bbfb3c685e9f8af4ba7ba0 +COMMAND_SHA256=4336c13341dc97c6ef0a781e34aa07cfa6829de4686391dfbda4b3b00dc90150 +DUPLICATE_FAILED_COMMAND_BLOCKED=false +EXECUTION_STARTED=true +CHANGE_DECLARED=false +RESULT_CONTRACT_VALID=true +RESULT_CONTRACT_STATUS=NOT_APPLICABLE +RESULT_CONTRACT_ERROR=NONE +COMMAND_RC=0 +CHANGES_MADE=false +ROLLBACK_STARTED=false +ROLLBACK_RESTORED=null +MUTATION_OUTCOME=NO_MUTATION +SANITIZED=yes +SECRETS_INCLUDED=no +PRIVATE_ADDRESSES_INCLUDED=no +RAW_EVIDENCE_SHA256=4737052a112e41dfe5fe93073b59c14131211a94191d7ad6c2dd0dd76e8164c6 +SANITIZED_OUTPUT_SHA256=4737052a112e41dfe5fe93073b59c14131211a94191d7ad6c2dd0dd76e8164c6 +OUTPUT_BEGIN +SNIKKET_HOME_ROLLBACK_PREFLIGHT=V1 +MODE=read-only +NO_MUTATION=true +SOURCE_HEAD=bd85699d8b588a34c480f10dee71323687bd48f1 +SOURCE_WORKTREE_DIRTY_COUNT=0 +SOURCE_ROLLBACK_NETWORK_PRESENT=true +SOURCE_ROLLBACK_USES_CLOUDFLARE=true +ROLLBACK_STATE_PRESENT=true +ROLLBACK_STATE_SHA=b4696a99b8e13042680d3c56537e0e5e5b6d3e7fae69e01fb8b8f60d68c39fc7 +ROLLBACK_STATE_MODE=600 +ROLLBACK_STATE_MTIME=1786953752 +ROLLBACK_STATE_JSON_VALID=true +ROLLBACK_STATE_HAS_DNS_CONTENT=true +ROLLBACK_STATE_HAS_DEFAULT_ROUTE=true +ROLLBACK_OLD_ROUTE_SHA=7c5515603e424ad8e85718ba716657be12298988ee11416f41081ec82e023409 +ROLLBACK_HOME_IP_SHA=26ac437d63a8bcf254255cc970d7614e78bd0a67b97b5575a42a1c3ca7a301ee +PVE01_IPIFY_RC=0 +PVE01_PUBLIC_IP_SHA=26ac437d63a8bcf254255cc970d7614e78bd0a67b97b5575a42a1c3ca7a301ee +ROLLBACK_HOME_MATCHES_CURRENT_PVE01_PUBLIC=true +VM150_QGA_TRANSPORT_RC=0 +REC|VM150|QGA_EXIT=0 +REC|VM150|DOMAIN_SHA=04cb4a04e408d91abb9c63701ae969abf16d8a12b6f4f144c5edf2c28e03256b +REC|VM150|EDGE_EGRESS_UNIT_PRESENT=true +REC|VM150|EDGE_EGRESS_ACTIVE=active +REC|VM150|EDGE_EGRESS_ENABLED=enabled +REC|VM150|EDGE_EGRESS_UNIT_SHA=83a20356e221dfb22ea2a0d99e327f7c2f791a3d3127a46af2d1625b33c95b5d +REC|VM150|OLDGW_SHA=4f9da1283166a1e9f4e6bc7dd240c76aef85772177f717f92abb6e8e2bb8de90 +REC|VM150|NEWGW_SHA=bc517b4af3298846906234d3d697d3820b0b652ade1880c937c9d71cd802c834 +REC|VM150|OLDGW_PING_RC=1 +REC|VM150|CURRENT_GW_SHA=bc517b4af3298846906234d3d697d3820b0b652ade1880c937c9d71cd802c834 +REC|VM150|REC|CONTAINER|NAME=snikket|RUNNING=true|RESTARTS=0 +REC|VM150|REC|CONTAINER|NAME=snikket-proxy|RUNNING=true|RESTARTS=0 +REC|VM150|REC|CONTAINER|NAME=snikket-certs|RUNNING=true|RESTARTS=0 +REC|VM150|REC|CONTAINER|NAME=snikket-portal|RUNNING=true|RESTARTS=0 +REC|VM150|LOCAL_HTTPS_RC=0 +REC|VM150|LOCAL_HTTPS_CODE=302 +VM130_QGA_TRANSPORT_RC=0 +REC|VM130|QGA_EXIT=0 +REC|VM130|ROLE=VM130 +REC|VM130|WG_CONFIG_PRESENT=true +REC|VM130|WG_BACKUP_PRESENT=true +REC|VM130|WG_CONFIG_SHA=42b046d580b2ccd6af8f3573fba4882c71c92b2938f3fed8edf58258bbc89615 +REC|VM130|WG_BACKUP_SHA=9012ece867ad7efa1b2d07020c1cdfc9d3a63f86fde9117bdd7435eef29a9735 +REC|VM130|WG_BACKUP_MODE=600 +REC|VM130|PRIORITY101_COUNT=1 +REC|VM130|TABLE17777_DEFAULT_COUNT=1 +REC|VM130|WG_EDGE_ACTIVE=active +CURRENT_AUTHORITATIVE_BEGET=true +REC|DNS|ROLE=primary|A_COUNT=1|MATCH_ROLLBACK_HOME=true +REC|DNS|ROLE=groups|A_COUNT=1|MATCH_ROLLBACK_HOME=true +REC|DNS|ROLE=share|A_COUNT=1|MATCH_ROLLBACK_HOME=true +BEGET_LOGIN_CANDIDATE_COUNT=0 +BEGET_PASSWORD_CANDIDATE_COUNT=0 +BEGET_CREDENTIALS_UNIQUE=false +REC|HOME_TCP|PORT=443|OPEN=true|MSEC=1 +REC|HOME_TCP|PORT=5000|OPEN=true|MSEC=0 +REC|HOME_TCP|PORT=5222|OPEN=true|MSEC=0 +REC|HOME_TCP|PORT=5349|OPEN=false|ERR=TimeoutError +ROLLBACK_PREFLIGHT_READY=false +DECISION=PASS_SNIKKET_HOME_ROLLBACK_PREFLIGHT +NEXT_GATE=ROLLBACK_PREFLIGHT_BLOCKER_REPAIR +TASK_RESULT=SNIKKET_HOME_ROLLBACK_PREFLIGHT +CHANGES_MADE=false +ROLLBACK_STARTED=false +ROLLBACK_RESTORED=null +MUTATION_OUTCOME=NO_MUTATION +HOMELAB_RESULT_CONTRACT={"version":1,"command_id":"SUPPORT-261006-SNIKKET-HOME-ROLLBACK-PREFLIGHT-99740R1","status":"OK","changes_made":false,"rollback_started":false,"rollback_restored":null} + +OUTPUT_END +CHAT_OUTPUT_END diff --git a/runtime/latest.json b/runtime/latest.json index 006c8037..984956af 100644 --- a/runtime/latest.json +++ b/runtime/latest.json @@ -1,19 +1,19 @@ { "schema_version": 1, "channel": "homelab-runtime", - "command_id": "SUPPORT-261006-SNIKKET-WGEDGE-DESIGN-STATE-RCA-99739R1", + "command_id": "SUPPORT-261006-SNIKKET-HOME-ROLLBACK-PREFLIGHT-99740R1", "status": "OK", "rc": 0, "host": "pve01", "mode": "read-only", - "component": "snikket-wgedge-design-state-rca", - "started_at_utc": "2026-10-06T17:55:49Z", - "finished_at_utc": "2026-10-06T17:55:50Z", + "component": "snikket-home-rollback-preflight", + "started_at_utc": "2026-10-06T18:02:31Z", + "finished_at_utc": "2026-10-06T18:03:00Z", "reference_register_checked": true, "reference_sha256": "5a3d8e5154c41cb582a4e0aca68090be1f0138918bf82131a948df326f9d8d66", "error_register_checked": true, "error_register_sha256": "3b09a553ec0f527ed3afeed4753f52a74ee3036045bbfb3c685e9f8af4ba7ba0", - "command_sha256": "a60a8c29d502a49e98c500b961f57451d0ca3fd661de694738452d4ba50dff96", + "command_sha256": "4336c13341dc97c6ef0a781e34aa07cfa6829de4686391dfbda4b3b00dc90150", "duplicate_failed_command_blocked": false, "block_reason": null, "execution_started": true, @@ -30,9 +30,9 @@ "secrets_included": false, "private_addresses_included": false, "raw_evidence_retained_locally": true, - "raw_evidence_sha256": "64709396903adcb76579d798fddab1c280c55806f26dc184c45423275962c07d", - "sanitized_output_sha256": "64709396903adcb76579d798fddab1c280c55806f26dc184c45423275962c07d", + "raw_evidence_sha256": "4737052a112e41dfe5fe93073b59c14131211a94191d7ad6c2dd0dd76e8164c6", + "sanitized_output_sha256": "4737052a112e41dfe5fe93073b59c14131211a94191d7ad6c2dd0dd76e8164c6", "output_truncated_in_json": false, "full_sanitized_output_url": "https://git.gram1.ru/.well-known/homelab-runtime/latest.txt", - "output": "SNIKKET_WGEDGE_DESIGN_STATE_RCA=V1\nMODE=read-only\nNO_MUTATION=true\nSOURCE_REPO_BASENAME=homelab-ops\nSOURCE_HEAD=bd85699d8b588a34c480f10dee71323687bd48f1\nSOURCE_BRANCH=main\nSOURCE_WORKTREE_DIRTY_COUNT=0\nREC|COMMIT|LABEL=ORIGIN|PRESENT=true|COMMIT=911d8cc52d5e8a677f132b736d01ade7a5879e59|DATE=2026-08-17T10:27:24+03:00|SUBJECT=CR-2026-0080: build Snikket EDGE cutover task|PARENTS=bcee02410290c4563fea4507a35fdf6dce08d674\nREC|COMMIT_FILES|LABEL=ORIGIN|COUNT=17\nREC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=task-package.sha256|PARENT=CR-2026-0080|PATH_SHA=070b91ffb62312169dc9f08e768758b846237b4c56e71cc58e5946da174dafc1\nREC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=2026-08-17-snikket-edge-cutover.md|PARENT=plans|PATH_SHA=72bcfe0160d221fc3f06466006c3163dc2e5f67f2224cacd1c5c50e02135eb37\nREC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=1|# Snikket EDGE Cutover Implementation Plan\nREC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=3|**Goal:** finish the traffic cutover of chat.gram1.ru to EDGE-01 while preserving rollback and keeping VM150 source-policy disabled until DNS convergence.\nREC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=5|## Task 1 — Version the production package\nREC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=6|- Create `tasks/snikket-edge-cutover-v1/task.json` with schema 2, controlled_apply, fail_fast, seven phases, exact expected outputs and rollback contract.\nREC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=7|- Create executable phase wrappers: `phases/preflight.sh`, `prepare.sh`, `dry-run.sh`, `apply.sh`, `verify.sh`, `rollback.sh`, `seal.sh`.\nREC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=8|- Create `tools/cutover.py` as the single orchestration implementation; wrappers only select the phase.\nREC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=9|- Create `assets/edge01-public-ingress.nft` that removes only the temporary source restriction while retaining transitional symmetric SNAT.\nREC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=10|- Create `assets/edge01-home-restricted-ingress.nft` as the exact operator rollback contract.\nREC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=14|- Create `tests/test_cr_2026_0080_snikket_edge_cutover.py` before implementation.\nREC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=15|- RED assertions: task package absent; public ingress must not contain `ip saddr IP_REDACTED`; source-policy activation must be gated after DNS convergence; seven phase files must exist and be executable; rollback must restore old A and routing state.\nREC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=20|- preflight: prove chat A is still IP_REDACTED, persistent ingress canary is green, edge-vm table 17777 exists, VM150 policy rule is absent, VM150 network renderer has the expected old gateway, and NPMplus can perform a non-mutating Cloudflare record lookup.\nREC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=21|- prepare: capture exact non-sensitive DNS metadata, edge-vm routing configuration and VM150 network-file bytes into the run rollback directory.\nREC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=22|- dry-run: calculate the exact A-record mutation, policy rule, VM150 gateway change and rollback operations without production mutation.\nREC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=23|- apply: require explicit EDGE-01 public-ingress operator attestation; change only chat.gram1.ru A to IP_REDACTED; wait for authoritative and public convergence; only then install priority-101 source policy for IP_REDACTED via table 17777 and change VM150 gateway to IP_REDACTED; refresh only the Snikket server container if effective TURN addressing still shows the old origin.\nREC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=24|- verify: HTTPS, TCP 5000/5222/3478/3479/5349/5350, XMPP STARTTLS, TURN TLS, UDP STUN, VM150 outbound public IP IP_REDACTED, DNS convergence and effective TURN address without IP_REDACTED.\nREC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=25|- rollback: restore VM150 network bytes and live default route, remove priority-101 policy and persistence, restore the previous chat A value; operator applies the versioned EDGE-01 restricted-ingress rollback asset if EDGE ingress must also be reverted.\nREC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=26|- seal: record only the proven traffic cutover. Do not remove turn.gram1.ru, rotate TURN auth material, alter certificate renewal or remove home-router forwards.\nREC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=31|- On `root@edge01`, apply the merged public-ingress asset with host guard, backup, `nft -c`, live canary and rollback.\nREC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=2026-08-17-snikket-edge-cutover-design.md|PARENT=specs|PATH_SHA=224ed8ac7b09df73545e49a1ac681848f67b00df285ec5a3b7b8398f21d015ee\nREC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover-design.md|LINE=1|# Snikket EDGE Traffic Cutover Design\nREC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover-design.md|LINE=7|Move chat.gram1.ru from home IP IP_REDACTED to EDGE-01 IP_REDACTED. Persistent EDGE-01 and edge-vm inbound transit is already installed and TCP, XMPP STARTTLS, TURN TLS, UDP STUN and HTTPS canaries pass.\nREC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover-design.md|LINE=8|Before cutover, EDGE-01 ingress is still restricted to source IP_REDACTED, chat.gram1.ru A is still IP_REDACTED, edge-vm table 17777 is ready, and no source rule for VM150 IP_REDACTED is active.\nREC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover-design.md|LINE=11|CR0080 covers only production traffic cutover: publicize the verified EDGE-01 ingress contract, change only chat.gram1.ru A to IP_REDACTED, wait for DNS convergence, activate VM150 egress through edge-vm and EDGE-01, refresh effective TURN addressing, verify, rollback and seal.\nREC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover-design.md|LINE=12|Excluded post-cutover work: remove turn.gram1.ru, rotate the TURN static auth se=REDACTED\nREC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover-design.md|LINE=15|Use tasks/snikket-edge-cutover-v1 on pve01.gram1.ru with controlled_apply and phases preflight, prepare, dry-run, apply, verify, rollback and seal.\nREC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover-design.md|LINE=16|EDGE-01 remains manual-operator access only. CR0080 versions the exact public-ingress contract, but the operator applies it from the established root@edge01 session; the pve01 task must not invent an automated EDGE-01 SSH path.\nREC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover-design.md|LINE=17|DNS mutation is fail-closed: apply requires operator attestation that the exact EDGE-01 public-ingress contract is active and an approved local DNS credential file path. Credential values are never printed.\nREC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover-design.md|LINE=20|Apply order is fixed: prove pre-state; require EDGE-01 public-ingress attestation and canary; change chat A; wait for authoritative and public DNS convergence; only then activate edge-vm source policy for IP_REDACTED via table 17777 and move VM150 Internet traffic through IP_REDACTED; refresh only the required Snikket or coturn component.\nREC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover-design.md|LINE=21|Verification covers HTTPS chat/groups/share; TCP 5000,5222,3478,3479,5349,5350; XMPP STARTTLS; TURN TLS; UDP STUN and relay 60000-60199; VM150 outbound IP IP_REDACTED; DNS convergence; and no effective TURN advertisement containing IP_REDACTED.\nREC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover-design.md|LINE=23|## Failure and rollback\nREC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover-design.md|LINE=24|Every phase is fail-fast and no later phase runs after a failed gate. Prepare re=REDACTED\nREC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover-design.md|LINE=25|Zero-secret diagnostics are mandatory: never print passwords, API credentials, p=REDACTED\nREC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=README.txt|PARENT=snikket-edge-cutover-v1|PATH_SHA=418b863db2e962e7c0bf3a5540f0f4b9a328c01120ffd81371c9cdf43a8664df\nREC|SOURCE_TEXT|LABEL=ORIGIN:README.txt|LINE=1|snikket-edge-cutover-v1\nREC|SOURCE_TEXT|LABEL=ORIGIN:README.txt|LINE=4|- production traffic cutover for chat.gram1.ru from IP_REDACTED to EDGE-01 IP_REDACTED;\nREC|SOURCE_TEXT|LABEL=ORIGIN:README.txt|LINE=5|- DNS convergence before VM150 source-policy activation;\nREC|SOURCE_TEXT|LABEL=ORIGIN:README.txt|LINE=6|- rollback-safe edge-vm and VM150 routing transition;\nREC|SOURCE_TEXT|LABEL=ORIGIN:README.txt|LINE=7|- no automated EDGE-01 access from pve01.\nREC|SOURCE_TEXT|LABEL=ORIGIN:README.txt|LINE=9|Manual EDGE-01 gate:\nREC|SOURCE_TEXT|LABEL=ORIGIN:README.txt|LINE=10|1. Apply assets/edge01-public-ingress.nft from the established root@edge01 MobaXterm session.\nREC|SOURCE_TEXT|LABEL=ORIGIN:README.txt|LINE=11|2. Verify nft -c and full public service canary.\nREC|SOURCE_TEXT|LABEL=ORIGIN:README.txt|LINE=12|3. Supply HOMELAB_SNIKKET_EDGE01_PUBLIC_INGRESS_ATTEST_SHA256 equal to the merged asset SHA256 when running apply.\nREC|SOURCE_TEXT|LABEL=ORIGIN:README.txt|LINE=17|turn.gram1.ru cleanup, TURN secret rotation, certificate renewal changes, home-r=REDACTED\nREC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=edge01-home-restricted-ingress.nft|PARENT=assets|PATH_SHA=ae3180f52ef9fecc84167c6f8618aa48a669c99f5387914f04f96b3ae4bff345\nREC|SOURCE_TEXT|LABEL=ORIGIN:edge01-home-restricted-ingress.nft|LINE=1|add chain inet edge01_filter snikket_prod_ingress\nREC|SOURCE_TEXT|LABEL=ORIGIN:edge01-home-restricted-ingress.nft|LINE=2|add rule inet edge01_filter snikket_prod_ingress iifname \"eth0\" oifname \"wg-home\" ip saddr IP_REDACTED ip daddr IP_REDACTED tcp dport { 3478, 3479, 5000, 5222, 5269, 5349, 5350 } accept\nREC|SOURCE_TEXT|LABEL=ORIGIN:edge01-home-restricted-ingress.nft|LINE=3|add rule inet edge01_filter snikket_prod_ingress iifname \"eth0\" oifname \"wg-home\" ip saddr IP_REDACTED ip daddr IP_REDACTED udp dport { 3478-3479, 5349-5350, 60000-60199 } accept\nREC|SOURCE_TEXT|LABEL=ORIGIN:edge01-home-restricted-ingress.nft|LINE=4|add rule inet edge01_filter snikket_prod_ingress iifname \"wg-home\" oifname \"eth0\" ip saddr IP_REDACTED ip daddr IP_REDACTED ct state established,related accept\nREC|SOURCE_TEXT|LABEL=ORIGIN:edge01-home-restricted-ingress.nft|LINE=5|insert rule inet edge01_filter forward jump snikket_prod_ingress comment \"SNIKKET_PROD_INGRESS_JUMP\"\nREC|SOURCE_TEXT|LABEL=ORIGIN:edge01-home-restricted-ingress.nft|LINE=7|table ip snikket_prod_ingress {\nREC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=edge01-public-ingress.nft|PARENT=assets|PATH_SHA=1678d03b6d6be773e5c875c7f85a548c0bd95314c58645ed8f567df9e8aaacfd\nREC|SOURCE_TEXT|LABEL=ORIGIN:edge01-public-ingress.nft|LINE=1|add chain inet edge01_filter snikket_prod_ingress\nREC|SOURCE_TEXT|LABEL=ORIGIN:edge01-public-ingress.nft|LINE=2|add rule inet edge01_filter snikket_prod_ingress iifname \"eth0\" oifname \"wg-home\" ip daddr IP_REDACTED tcp dport { 3478, 3479, 5000, 5222, 5349, 5350 } accept\nREC|SOURCE_TEXT|LABEL=ORIGIN:edge01-public-ingress.nft|LINE=3|add rule inet edge01_filter snikket_prod_ingress iifname \"eth0\" oifname \"wg-home\" ip daddr IP_REDACTED udp dport { 3478-3479, 5349-5350, 60000-60199 } accept\nREC|SOURCE_TEXT|LABEL=ORIGIN:edge01-public-ingress.nft|LINE=4|add rule inet edge01_filter snikket_prod_ingress iifname \"wg-home\" oifname \"eth0\" ip saddr IP_REDACTED ct state established,related accept\nREC|SOURCE_TEXT|LABEL=ORIGIN:edge01-public-ingress.nft|LINE=5|insert rule inet edge01_filter forward jump snikket_prod_ingress comment \"SNIKKET_PROD_INGRESS_JUMP\"\nREC|SOURCE_TEXT|LABEL=ORIGIN:edge01-public-ingress.nft|LINE=7|table ip snikket_prod_ingress {\nREC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=npmplus-cloudflare-a.sh|PARENT=assets|PATH_SHA=48c7bcfaa5f7de8282caa4b35cabc573876f7c8336c5988c3029b66c074fd1f4\nREC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=apply.sh|PARENT=phases|PATH_SHA=c375ac75c7c4370efdd0c29fce9f5afc7a46ac38e10bf4e031d2fdeb44525324\nREC|SOURCE_TEXT|LABEL=ORIGIN:apply.sh|LINE=10|python3 \"$TASK_DIR/tools/cutover.py\" apply\nREC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=dry-run.sh|PARENT=phases|PATH_SHA=a1bf65bfca5d89a9e6f37e7ef1e95d23300b884df043c9e68fae1dd95944482d\nREC|SOURCE_TEXT|LABEL=ORIGIN:dry-run.sh|LINE=10|python3 \"$TASK_DIR/tools/cutover.py\" dry-run\nREC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=preflight.sh|PARENT=phases|PATH_SHA=cd56fdeeae17cfc3be7d68b996f8b5350ccd930252c88da7871bbbe7f97a0def\nREC|SOURCE_TEXT|LABEL=ORIGIN:preflight.sh|LINE=10|python3 \"$TASK_DIR/tools/cutover.py\" preflight\nREC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=prepare.sh|PARENT=phases|PATH_SHA=f4c8867f85674ee74bc7f04a82b8336eeeefaa959ec9b198ca460170de1e1990\nREC|SOURCE_TEXT|LABEL=ORIGIN:prepare.sh|LINE=10|python3 \"$TASK_DIR/tools/cutover.py\" prepare\nREC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=rollback.sh|PARENT=phases|PATH_SHA=f2998ffe82d9ff129223379159c31aaa5bd5f4769b2489b3c8d471b7d21d7064\nREC|SOURCE_TEXT|LABEL=ORIGIN:rollback.sh|LINE=5| printf 'ROOT_CAUSE_COUNT=1\\nFAILED_PHASE=rollback\\n' >&2\nREC|SOURCE_TEXT|LABEL=ORIGIN:rollback.sh|LINE=10|python3 \"$TASK_DIR/tools/cutover.py\" rollback\nREC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=seal.sh|PARENT=phases|PATH_SHA=49326c4de4695fa8d0fb4ae205ba863305f1234c60308c011ce7f1cb87afdea2\nREC|SOURCE_TEXT|LABEL=ORIGIN:seal.sh|LINE=10|python3 \"$TASK_DIR/tools/cutover.py\" seal\nREC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=verify.sh|PARENT=phases|PATH_SHA=e562fbaefd2c1e7f70ef0cdbb832e4a09472614da0c6824eac57f8267df00c74\nREC|SOURCE_TEXT|LABEL=ORIGIN:verify.sh|LINE=10|python3 \"$TASK_DIR/tools/cutover.py\" verify\nREC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=task.json|PARENT=snikket-edge-cutover-v1|PATH_SHA=9a63e2769a24f3ce2c23c172837f46c1f044cbd8f886e45313a3b0e7e467e55d\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=3| \"task_id\": \"snikket-edge-cutover-v1\",\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=6| \"production_change\": true,\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=14| \"rollback\": \"phases/rollback.sh\",\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=18| \"active_backup_restore_max\": 0\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=22| \"EDGE-01 public service path at IP_REDACTED\",\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=23| \"edge-vm IP_REDACTED routing and /etc/wireguard/wg-edge01.conf metadata\",\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=24| \"VM150 QGA network and Snikket runtime state\",\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=33| \"$HOMELAB_RUN_DIR/rollback.json\",\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=35| \"/mnt/staging/homelab-admin-rollback/snikket-edge-cutover-v1/state.json\",\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=37| \"edge-vm policy rule priority 101 and wg-edge01.conf PostUp/PostDown\",\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=38| \"VM150 /etc/systemd/system/snikket-edge-egress.service\"\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=42| \"edge-vm policy routing for source IP_REDACTED\",\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=43| \"VM150 default Internet route via IP_REDACTED\",\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=44| \"Snikket server container restart after DNS convergence\"\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=47| \"EDGE-01 is never accessed automatically by this task\",\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=48| \"turn.gram1.ru is not modified or deleted\",\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=49| \"TURN static auth material is not read, printed, or rotated\",\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=51| \"home router forwards are unchanged\",\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=57| \"tools/cutover.py\"\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=66| \"verification\": \"pre-cutover DNS, public canary, edge-vm table 17777, absent VM150 source policy, VM150 old default route, non-mutating Cloudflare lookup\"\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=71| \"verification\": \"non-secret rollback state is persisted before mutation\"=REDACTED\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=76| \"verification\": \"exact DNS/routing/service mutations and rollback order are calculated without mutation\"\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=81| \"verification\": \"EDGE-01 public ingress attestation, DNS convergence, source policy, VM150 egress, Snikket refresh\"\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=86| \"verification\": \"DNS, HTTPS, XMPP, TURN/STUN, VM150 egress and effective coturn external address\"\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=89| \"path\": \"$HOMELAB_RUN_DIR/rollback.json\",\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=90| \"producer\": \"phases/rollback.sh\",\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=91| \"verification\": \"old DNS and pre-cutover edge-vm/VM150 routing are restored; EDGE-01 rollback remains manual\"\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=96| \"verification\": \"traffic cutover is verified without post-cutover hardening changes\"\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=106| \"host\": \"edge-vm IP_REDACTED\",\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=111| \"host\": \"VM150\",\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=116| \"host\": \"EDGE-01 IP_REDACTED\",\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=118| \"compile\": \"versioned nft asset; nft -c; host guard; external canary\"\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=121| \"rollback\": {\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=122| \"command\": \"HOMELAB_CONTROLLED_APPLY_CONFIRM=snikket-edge-cutover-v1 /opt/homelab-admin/bin/homelab-admin run TASK_DIR rollback\",\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=123| \"verification\": \"restore old chat A, remove VM150 source policy and persistent egress service, restore old default route; manual EDGE-01 restricted-ingress asset remains available\"\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=130| \"edge01_public_ip\": \"IP_REDACTED\",\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=131| \"edge01_wg_ip\": \"IP_REDACTED\",\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=132| \"edgevm_wg_ip\": \"IP_REDACTED\",\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=133| \"edgevm_lan_ip\": \"IP_REDACTED\",\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=134| \"vm150_ip\": \"IP_REDACTED\",\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=137| \"policy_table\": 17777,\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=139| \"turn_udp_min\": 60000,\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=140| \"turn_udp_max\": 60199,\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=141| \"edge01_automated_access\": false,\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=142| \"turn_legacy_record_mutation\": false,\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=143| \"turn_secret_rotation\": false,=REDACTED\nREC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=145| \"home_router_mutation\": false,\nREC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=cutover.py|PARENT=tools|PATH_SHA=d90388278e2775b1ac7534b35946dfa3dadd03f65cd16117db653731b47346ad\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=9|EDGE = \"IP_REDACTED\"\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=10|EDGEVM = \"IP_REDACTED\"\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=11|VM150 = \"IP_REDACTED\"\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=13|WG_TABLE = \"17777\"\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=14|RULE_PRIO = \"101\"\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=15|ROLLBACK = pathlib.Path(\"/mnt/staging/homelab-admin-rollback/snikket-edge-cutover-v1\")\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=16|EDGE_SSH = [\"ssh\",\"-o\",\"BatchMode=yes\",\"-o\",\"StrictHostKeyChecking=yes\",\"-o\",\"ConnectTimeout=8\",\"debian@IP_REDACTED\"]\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=19|class CutoverError(RuntimeError):\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=24| if check and p.returncode:\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=25| raise CutoverError(f\"command-failed rc={p.returncode} tool={pathlib.Path(cmd[0]).name}\")\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=26| return p\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=31| raise CutoverError(\"HOMELAB_RUN_DIR missing\")\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=34| return p\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=41| raise CutoverError(\"qga-json\") from e\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=43| raise CutoverError(\"qga-command-failed\")\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=44| return d.get(\"out-data\",\"\")\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=46|def edgevm(script,timeout=90):\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=47| return sh(EDGE_SSH+[\"sudo\",\"-n\",\"bash\",\"-s\"],input_text=script,timeout=timeout).stdout\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=51| cmd=EDGE_SSH+[\"sudo\",\"-n\",\"docker\",\"exec\",\"-i\",\"npmplus\",\"sh\",\"-s\",\"--\",action,CHAT]\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=58| raise CutoverError(\"cloudflare-safe-json\") from e\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=60| raise CutoverError(\"cloudflare-operation-failed\")\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=61| return d\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=66| raise CutoverError(\"no-authoritative-ns\")\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=67| return out[0].rstrip(\".\")\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=75| return out[-1] if out else \"\"\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=82| return last\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=84| raise CutoverError(\"dns-convergence-timeout:\"+json.dumps(last,sort_keys=True))\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=97| raise CutoverError(\"tcp-canary\")\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=98| return out\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=109| raise CutoverError(\"wan-5269-must-remain-closed\")\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=110| return True\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=118| raise CutoverError(\"https-canary\")\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=119| return out\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=122| x=sh([\"openssl\",\"s_client\",\"-connect\",f\"{EDGE}:5222\",\"-starttls\",\"xmpp\",\"-xmpphost\",CHAT,\"-verify_return_error\"],input_text=\"\",check=False,timeout=15)\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=123| t=sh([\"openssl\",\"s_client\",\"-connect\",f\"{EDGE}:5349\",\"-servername\",CHAT,\"-verify_return_error\"],input_text=\"\",check=False,timeout=15)\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=124| if \"Verify return code: 0 (ok)\" not in x.stdout+x.stderr:\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=125| raise CutoverError(\"xmpp-tls\")\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=126| if \"Verify return code: 0 (ok)\" not in t.stdout+t.stderr:\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=127| raise CutoverError(\"turn-tls\")\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=128| return {\"xmpp\":True,\"turn\":True}\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=144| raise CutoverError(\"udp-stun\")\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=145| return out\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=148| return {\"tcp\":tcp_canary(EDGE),\"https\":https_canary(EDGE),\"tls\":tls_canary(),\"stun\":udp_stun(EDGE)}\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=150|def edgevm_rule_present():\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=151| return bool(re.search(r\"^101:\\s+from 192\\.168\\.50\\.31(?:/32)? lookup 17777$\",edgevm(\"ip -4 rule show\"),re.M))\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=153|def edgevm_priority_free():\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=154| return not bool(re.search(r\"^101:\",edgevm(\"ip -4 rule show\"),re.M))\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=156|def edgevm_table_ready():\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=157| return \"default dev wg-edge01\" in edgevm(\"ip -4 route show table 17777\")\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=160| return qga(\"ip -4 route show default | head -n1\").strip()\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=162|def vm_egress():\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=163| return qga(\"curl -4 -fsS --connect-timeout 5 --max-time 12 https://api.ipify.org\").strip()\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=165|def coturn_external():\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=166| inner='pid=\"$(pgrep -xo turnserver || true)\"; [ -n \"$pid\" ] || exit 2; tr \"\\\\0\" \"\\\\n\" <\"/proc/$pid/cmdline\" | grep -A1 -x -- \"-X\" | tail -n1'\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=167| return qga(\"docker exec snikket sh -lc \"+json.dumps(inner)).strip()\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=171| raise CutoverError(\"chat-authoritative-a-not-home\")\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=172| if not edgevm_table_ready():\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=173| raise CutoverError(\"table-17777-not-ready\")\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=174| if edgevm_rule_present():\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=175| raise CutoverError(\"vm150-source-rule-already-present\")\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=176| if not edgevm_priority_free():\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=177| raise CutoverError(\"policy-priority-101-in-use\")\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=180| raise CutoverError(\"vm150-default-not-home\")\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=183| raise CutoverError(\"cloudflare-record-prestate\")\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=184| return {\"dns\":rec,\"default_route\":d,\"public_canary\":public_canary()}\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=186|def save_rollback(state):\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=187| ROLLBACK.mkdir(parents=True,exist_ok=True); os.chmod(ROLLBACK,0o700)\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=188| p=ROLLBACK/\"state.json\"\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=190| raise CutoverError(\"rollback-state-already-exists\")\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=193|def load_rollback():\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=194| p=ROLLBACK/\"state.json\"\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=196| raise CutoverError(\"rollback-state-missing\")\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=197| return json.loads(p.read_text())\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=199|def install_edgevm_policy():\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=201|CFG=/etc/wireguard/wg-edge01.conf\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=202|BK=/root/wg-edge01.before-snikket-edge-cutover-v1.conf\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=208|up=\"PostUp = ip rule add priority 101 from IP_REDACTED lookup 17777\"\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=209|down=\"PostDown = ip rule del priority 101 from IP_REDACTED lookup 17777 2>/dev/null || true\"\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=211| marker=\"PostDown = ip route del table 17777 default dev wg-edge01 2>/dev/null || true\"\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=216|ip rule add priority 101 from IP_REDACTED lookup 17777\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=217|ip -4 rule show | grep -Eq '^101:.*from 192\\.168\\.50\\.31(/32)? lookup 17777$'\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=219| edgevm(script)\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=221|def remove_edgevm_policy():\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=222| edgevm('set -Eeuo pipefail; ip rule del priority 101 2>/dev/null || true; BK=/root/wg-edge01.before-snikket-edge-cutover-v1.conf; CFG=/etc/wireguard/wg-edge01.conf; if [ -f \"$BK\" ]; then cp -a \"$BK\" \"$CFG\"; fi')\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=225|Description=Snikket Internet egress via edge-vm\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=231|ExecStart=/usr/sbin/ip route replace default via IP_REDACTED dev eth0\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=232|ExecStop=/usr/sbin/ip route replace default via IP_REDACTED dev eth0\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=239|def install_vm_egress():\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=241| qga(f'set -Eeuo pipefail; test ! -e /etc/systemd/system/snikket-edge-egress.service; printf %s {b64} | base64 -d >/etc/systemd/system/snikket-edge-egress.service; chmod 0644 /etc/systemd/system/snikket-edge-egress.service; systemctl daemon-reload; systemctl enable --now snikket-edge-egress.service; ip -4 route show default | grep -F \"default via IP_REDACTED dev eth0\"')\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=243|def remove_vm_egress():\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=244| qga('set -Eeuo pipefail; systemctl disable --now snikket-edge-egress.service 2>/dev/null || true; rm -f /etc/systemd/system/snikket-edge-egress.service; systemctl daemon-reload; ip route replace default via IP_REDACTED dev eth0')\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=246|def refresh_snikket():\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=247| qga(\"docker restart snikket >/dev/null; for i in $(seq 1 30); do docker exec snikket pgrep -x turnserver >/dev/null 2>&1 && exit 0; sleep 2; done; exit 1\",timeout=80)\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=250| dns=dns_converged(EDGE,60); can=public_canary(); federation_5269_closed(EDGE)\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=251| if not edgevm_rule_present():\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=252| raise CutoverError(\"vm150-source-rule-missing\")\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=254| if f\"default via {EDGEVM} \" not in d:\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=255| raise CutoverError(\"vm150-default-not-edgevm\")\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=256| eg=vm_egress()\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=257| if eg != EDGE:\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=258| raise CutoverError(\"vm150-egress-ip\")\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=259| ext=coturn_external()\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=260| if EDGE not in ext or HOME in ext:\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=261| raise CutoverError(\"coturn-external-address\")\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=262| return {\"dns\":dns,\"canary\":can,\"vm_default\":d,\"vm_egress\":eg,\"coturn_external\":ext}\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=266| d[\"edge01_public_ingress_asset_sha256\"]=hashlib.sha256((ASSETS/\"edge01-public-ingress.nft\").read_bytes()).hexdigest()\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=271| save_rollback(st)\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=272| write_phase(\"prepare\",{\"status\":\"OK\",\"rollback_state\":str(ROLLBACK/\"state.json\"),\"old_dns_content\":st[\"dns\"][\"content\"],\"old_default_route\":st[\"default_route\"]})\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=275| st=load_rollback()\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=277| raise CutoverError(\"rollback-old-dns-not-home\")\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=278| actions=[\"operator applies versioned EDGE-01 public-ingress asset\",f\"chat A {HOME} -> {EDGE}\",\"wait authoritative + IP_REDACTED + IP_REDACTED convergence\",f\"edge-vm rule priority {RULE_PRIO} from {VM150}/32 lookup {WG_TABLE}\",f\"VM150 default route {OLD_GW} -> {EDGEVM}\",\"restart snikket container to refresh coturn external address\",\"full verification\"]\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=279| write_phase(\"dry-run\",{\"status\":\"OK\",\"actions\":actions,\"rollback\":[\"restore VM150 route\",\"remove edge-vm source rule/persistence\",f\"chat A -> {HOME}\",\"operator may apply EDGE-01 restricted-ingress asset\"]})\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=282| expected=hashlib.sha256((ASSETS/\"edge01-public-ingress.nft\").read_bytes()).hexdigest()\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=283| if os.environ.get(\"HOMELAB_SNIKKET_EDGE01_PUBLIC_INGRESS_ATTEST_SHA256\") != expected:\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=284| raise CutoverError(\"edge01-public-ingress-attestation-missing-or-wrong\")\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=285| load_rollback(); public_canary(); federation_5269_closed(EDGE); cf(\"set\",EDGE); dns=dns_converged(EDGE,360)\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=286| install_edgevm_policy(); install_vm_egress()\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=287| eg=vm_egress()\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=288| if eg != EDGE:\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=289| raise CutoverError(\"vm150-egress-after-cutover\")\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=290| refresh_snikket(); ext=coturn_external()\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=291| if EDGE not in ext or HOME in ext:\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=292| raise CutoverError(\"coturn-external-after-refresh\")\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=293| write_phase(\"apply\",{\"status\":\"OK\",\"dns\":dns,\"vm150_egress\":eg,\"coturn_external\":ext})\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=298|def phase_rollback():\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=299| st=load_rollback(); remove_vm_egress(); remove_edgevm_policy(); old=st[\"dns\"][\"content\"]; cf(\"set\",old); dns=dns_converged(old,360)\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=300| write_phase(\"rollback\",{\"status\":\"OK\",\"dns\":dns,\"vm_default\":vm_default(),\"edge01_manual_asset\":\"assets/edge01-home-restricted-ingress.nft\"})\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=304| write_phase(\"seal\",{\"status\":\"SEALED\",\"traffic_cutover\":True,\"verified_utc\":=REDACTED\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=306|PHASES={\"preflight\":phase_preflight,\"prepare\":phase_prepare,\"dry-run\":phase_dry_run,\"apply\":phase_apply,\"verify\":phase_verify,\"rollback\":phase_rollback,\"seal\":phase_seal}\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=309| raise SystemExit(\"usage: cutover.py PHASE\")\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=312| except CutoverError as e:\nREC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=313| print(f\"CUTOVER_ERROR={e}\",file=sys.stderr); raise SystemExit(1)\nREC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=test_cr_2026_0080_snikket_edge_cutover.py|PARENT=tests|PATH_SHA=4166d64d93a4f81a59f9cf85d667034d01aad54a3b75c3d4aa1858fcfe9797bd\nREC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=3|TASK=ROOT/\"tasks\"/\"snikket-edge-cutover-v1\"\nREC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=5|class CR0080CutoverTest(unittest.TestCase):\nREC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=12| self.assertEqual(task[\"task_id\"],\"snikket-edge-cutover-v1\")\nREC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=13| self.assertTrue(task[\"production_change\"])\nREC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=16| self.assertEqual(set(task[\"phases\"]),{\"preflight\",\"prepare\",\"dry-run\",\"apply\",\"verify\",\"rollback\",\"seal\"})\nREC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=17| expected=[f\"$HOMELAB_RUN_DIR/{name}.json\" for name in (\"preflight\",\"prepare\",\"dry-run\",\"apply\",\"verify\",\"rollback\",\"seal\")]\nREC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=28| def test_public_ingress_removes_only_home_source_guard(self):\nREC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=29| pub=(TASK/\"assets/edge01-public-ingress.nft\").read_text()\nREC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=30| rb=(TASK/\"assets/edge01-home-restricted-ingress.nft\").read_text()\nREC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=33| for needle in (\"IP_REDACTED\",\"IP_REDACTED\",\"snat to IP_REDACTED\",\"60000-60199\",\"SNIKKET_PROD_INGRESS_JUMP\"):\nREC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=38| def test_apply_is_dns_then_policy_then_vmroute(self):\nREC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=39| src=(TASK/\"tools/cutover.py\").read_text()\nREC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=41| self.assertIn(\"HOMELAB_SNIKKET_EDGE01_PUBLIC_INGRESS_ATTEST_SHA256\",body)\nREC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=42| self.assertIn(\"federation_5269_closed(EDGE)\",body)\nREC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=43| self.assertLess(body.index('cf(\"set\",EDGE)'),body.index(\"dns_converged(EDGE\"))\nREC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=44| self.assertIn(\"edge01-public-ingress-attestation-missing-or-wrong\",body)\nREC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=45| self.assertLess(body.index(\"dns_converged(EDGE\"),body.index(\"install_edgevm_policy()\"))\nREC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=46| self.assertLess(body.index(\"install_edgevm_policy()\"),body.index(\"install_vm_egress()\"))\nREC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=47| self.assertLess(body.index(\"install_vm_egress()\"),body.index(\"refresh_snikket()\"))\nREC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=49| def test_rollback_reverses_routing_before_dns(self):\nREC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=50| src=(TASK/\"tools/cutover.py\").read_text()\nREC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=51| body=src[src.index(\"def phase_rollback():\"):src.index(\"def phase_seal():\")]\nREC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=52| self.assertLess(body.index(\"remove_vm_egress()\"),body.index(\"remove_edgevm_policy()\"))\nREC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=53| self.assertLess(body.index(\"remove_edgevm_policy()\"),body.index('cf(\"set\",old)'))\nREC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=54| self.assertIn(\"edge01-home-restricted-ingress.nft\",body)\nREC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=59| self.assertIn(\"turn.gram1.ru is not modified or deleted\",joined)\nREC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=60| self.assertIn(\"TURN static auth material is not read, printed, or rotated\",joined)\nREC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=62| self.assertIn(\"home router forwards are unchanged\",joined)\nREC|COMMIT|LABEL=RETIRE|PRESENT=true|COMMIT=73c4cb4cc446ec8a10f81ba635624f2bf9373467|DATE=2026-08-31T22:01:03+03:00|SUBJECT=CR0104 add edge ingress retirement|PARENTS=53934522eaae9882f6c835e36b51a3b2162ec382\nREC|COMMIT_FILES|LABEL=RETIRE|COUNT=3\nREC|COMMIT_FILE|LABEL=RETIRE|BASENAME=edge-retire.sh|PARENT=homelab-command-stack-retirement|PATH_SHA=c9764b4b5fc86c1c3f92d9c26a0912de947256337e686df9991a336bbec22074\nREC|SOURCE_TEXT|LABEL=RETIRE:edge-retire.sh|LINE=5|REWRITE=\"$HERE/edge-rewrite.py\"\nREC|SOURCE_TEXT|LABEL=RETIRE:edge-retire.sh|LINE=7|EDGE=\"${HOMELAB_EDGE_HOST:HOST_REDACTED\nREC|SOURCE_TEXT|LABEL=RETIRE:edge-retire.sh|LINE=8|SSH=(ssh -o BatchMode=yes -o ConnectTimeout=10 \"$EDGE\")\nREC|SOURCE_TEXT|LABEL=RETIRE:edge-retire.sh|LINE=14|BACKUP_ROOT='/var/backups/homelab-command-stack-retirement'\nREC|SOURCE_TEXT|LABEL=RETIRE:edge-retire.sh|LINE=17|BACKUP=\"$BACKUP_ROOT/$STAMP/edge\"\nREC|SOURCE_TEXT|LABEL=RETIRE:edge-retire.sh|LINE=58|rollback() {\nREC|SOURCE_TEXT|LABEL=RETIRE:edge-retire.sh|LINE=71| echo 'EDGE_RETIRE_ROLLBACK=OK' >&2\nREC|SOURCE_TEXT|LABEL=RETIRE:edge-retire.sh|LINE=73| echo 'EDGE_RETIRE_ROLLBACK=FAILED' >&2\nREC|SOURCE_TEXT|LABEL=RETIRE:edge-retire.sh|LINE=80|trap rollback ERR\nREC|SOURCE_TEXT|LABEL=RETIRE:edge-retire.sh|LINE=111| echo 'EDGE_RETIRE_CHECK=OK'\nREC|SOURCE_TEXT|LABEL=RETIRE:edge-retire.sh|LINE=121|\"${SCP[@]}\" \"$TMP/9.conf\" \"$EDGE:$RPROXY\"\nREC|SOURCE_TEXT|LABEL=RETIRE:edge-retire.sh|LINE=122|\"${SCP[@]}\" \"$TMP/http.conf\" \"$EDGE:$RHTTP\"\nREC|SOURCE_TEXT|LABEL=RETIRE:edge-retire.sh|LINE=146|echo \"EDGE_RETIRE_STATUS=OK BACKUP=$BACKUP\"\nREC|COMMIT_FILE|LABEL=RETIRE|BASENAME=edge-rewrite.py|PARENT=homelab-command-stack-retirement|PATH_SHA=5d82cec197ad13cd81785224c92618441b52804db7e20d7ba2ff3c18805206d9\nREC|SOURCE_TEXT|LABEL=RETIRE:edge-rewrite.py|LINE=26| return pos + 1\nREC|SOURCE_TEXT|LABEL=RETIRE:edge-rewrite.py|LINE=32| return text.rfind(\"\\n\", 0, pos) + 1\nREC|SOURCE_TEXT|LABEL=RETIRE:edge-rewrite.py|LINE=42| return pos\nREC|SOURCE_TEXT|LABEL=RETIRE:edge-rewrite.py|LINE=59| return text[:start] + text[end:]\nREC|SOURCE_TEXT|LABEL=RETIRE:edge-rewrite.py|LINE=83| return result\nREC|COMMIT_FILE|LABEL=RETIRE|BASENAME=test_cr_2026_0104_command_stack_retirement.py|PARENT=tests|PATH_SHA=a1fdf025e885c9a1b1fd5ce5a36c6e2930f3c9ede718660ae9705c01a6cf328b\nREC|COMMIT|LABEL=ROLLBACK|PRESENT=true|COMMIT=3bc25b7abde3ef263d728549e92de5ec6dc4a027|DATE=2026-08-31T22:13:52+03:00|SUBJECT=CR0104 add explicit edge rollback|PARENTS=7d9d9a81d64a88b3d09b21b0980f92f33dd26b15\nREC|COMMIT_FILES|LABEL=ROLLBACK|COUNT=3\nREC|COMMIT_FILE|LABEL=ROLLBACK|BASENAME=edge-retire.sh|PARENT=homelab-command-stack-retirement|PATH_SHA=c9764b4b5fc86c1c3f92d9c26a0912de947256337e686df9991a336bbec22074\nREC|SOURCE_TEXT|LABEL=ROLLBACK:edge-retire.sh|LINE=5|REWRITE=\"$HERE/edge-rewrite.py\"\nREC|SOURCE_TEXT|LABEL=ROLLBACK:edge-retire.sh|LINE=7|EDGE=\"${HOMELAB_EDGE_HOST:HOST_REDACTED\nREC|SOURCE_TEXT|LABEL=ROLLBACK:edge-retire.sh|LINE=8|SSH=(ssh -o BatchMode=yes -o ConnectTimeout=10 \"$EDGE\")\nREC|SOURCE_TEXT|LABEL=ROLLBACK:edge-retire.sh|LINE=14|BACKUP_ROOT='/var/backups/homelab-command-stack-retirement'\nREC|SOURCE_TEXT|LABEL=ROLLBACK:edge-retire.sh|LINE=17|BACKUP=\"$BACKUP_ROOT/$STAMP/edge\"\nREC|SOURCE_TEXT|LABEL=ROLLBACK:edge-retire.sh|LINE=58|rollback() {\nREC|SOURCE_TEXT|LABEL=ROLLBACK:edge-retire.sh|LINE=71| echo 'EDGE_RETIRE_ROLLBACK=OK' >&2\nREC|SOURCE_TEXT|LABEL=ROLLBACK:edge-retire.sh|LINE=73| echo 'EDGE_RETIRE_ROLLBACK=FAILED' >&2\nREC|SOURCE_TEXT|LABEL=ROLLBACK:edge-retire.sh|LINE=80|trap rollback ERR\nREC|SOURCE_TEXT|LABEL=ROLLBACK:edge-retire.sh|LINE=111| echo 'EDGE_RETIRE_CHECK=OK'\nREC|SOURCE_TEXT|LABEL=ROLLBACK:edge-retire.sh|LINE=120| sudo -n tee '$BACKUP_ROOT/last-edge-backup' >/dev/null\nREC|SOURCE_TEXT|LABEL=ROLLBACK:edge-retire.sh|LINE=123|\"${SCP[@]}\" \"$TMP/9.conf\" \"$EDGE:$RPROXY\"\nREC|SOURCE_TEXT|LABEL=ROLLBACK:edge-retire.sh|LINE=124|\"${SCP[@]}\" \"$TMP/http.conf\" \"$EDGE:$RHTTP\"\nREC|SOURCE_TEXT|LABEL=ROLLBACK:edge-retire.sh|LINE=148|echo \"EDGE_RETIRE_STATUS=OK BACKUP=$BACKUP\"\nREC|COMMIT_FILE|LABEL=ROLLBACK|BASENAME=edge-rollback.sh|PARENT=homelab-command-stack-retirement|PATH_SHA=5d21efde6be6c7c450c4521a51f902c96f16e6a40ab8bcbf4eb710e947a06da7\nREC|SOURCE_TEXT|LABEL=ROLLBACK:edge-rollback.sh|LINE=4|EDGE=\"${HOMELAB_EDGE_HOST:HOST_REDACTED\nREC|SOURCE_TEXT|LABEL=ROLLBACK:edge-rollback.sh|LINE=5|SSH=(ssh -o BatchMode=yes -o ConnectTimeout=10 \"$EDGE\")\nREC|SOURCE_TEXT|LABEL=ROLLBACK:edge-rollback.sh|LINE=9|BACKUP_ROOT='/var/backups/homelab-command-stack-retirement'\nREC|SOURCE_TEXT|LABEL=ROLLBACK:edge-rollback.sh|LINE=10|POINTER=\"$BACKUP_ROOT/last-edge-backup\"\nREC|SOURCE_TEXT|LABEL=ROLLBACK:edge-rollback.sh|LINE=19| '$BACKUP_ROOT'/*/edge) ;;\nREC|SOURCE_TEXT|LABEL=ROLLBACK:edge-rollback.sh|LINE=20| *) echo 'INVALID_EDGE_BACKUP_POINTER' >&2; exit 1 ;;\nREC|SOURCE_TEXT|LABEL=ROLLBACK:edge-rollback.sh|LINE=44| echo \\\"EDGE_ROLLBACK_STATUS=OK BACKUP=\\$BACKUP\\\"\nREC|COMMIT_FILE|LABEL=ROLLBACK|BASENAME=test_cr_2026_0104_command_stack_retirement.py|PARENT=tests|PATH_SHA=a1fdf025e885c9a1b1fd5ce5a36c6e2930f3c9ede718660ae9705c01a6cf328b\nREC|COMMIT|LABEL=MERGE_RETIRE|PRESENT=false\nREC|CURRENT_FILE|BASENAME=task.json|PARENT=snikket-edge-cutover-v1|PATH_SHA=9a63e2769a24f3ce2c23c172837f46c1f044cbd8f886e45313a3b0e7e467e55d|SHA=9cad3f6291663d6e85a556882c8ed751eaaa61b816b9bae934032fb0677267fd\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=3| \"task_id\": \"snikket-edge-cutover-v1\",\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=6| \"production_change\": true,\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=14| \"rollback\": \"phases/rollback.sh\",\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=18| \"active_backup_restore_max\": 0\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=22| \"EDGE-01 public service path at IP_REDACTED\",\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=23| \"edge-vm IP_REDACTED routing and /etc/wireguard/wg-edge01.conf metadata\",\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=24| \"VM150 QGA network and Snikket runtime state\",\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=33| \"$HOMELAB_RUN_DIR/rollback.json\",\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=35| \"/mnt/staging/homelab-admin-rollback/snikket-edge-cutover-v1/state.json\",\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=37| \"edge-vm policy rule priority 101 and wg-edge01.conf PostUp/PostDown\",\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=38| \"VM150 /etc/systemd/system/snikket-edge-egress.service\"\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=42| \"edge-vm policy routing for source IP_REDACTED\",\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=43| \"VM150 default Internet route via IP_REDACTED\",\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=44| \"Snikket server container restart after DNS convergence\"\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=47| \"EDGE-01 is never accessed automatically by this task\",\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=48| \"turn.gram1.ru is not modified or deleted\",\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=49| \"TURN static auth material is not read, printed, or rotated\",\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=51| \"home router forwards are unchanged\",\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=57| \"tools/cutover.py\"\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=66| \"verification\": \"pre-cutover DNS, public canary, edge-vm table 17777, absent VM150 source policy, VM150 old default route, non-mutating Cloudflare lookup\"\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=71| \"verification\": \"non-secret rollback state is persisted before mutation\"=REDACTED\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=76| \"verification\": \"exact DNS/routing/service mutations and rollback order are calculated without mutation\"\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=81| \"verification\": \"EDGE-01 public ingress attestation, DNS convergence, source policy, VM150 egress, Snikket refresh\"\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=86| \"verification\": \"DNS, HTTPS, XMPP, TURN/STUN, VM150 egress and effective coturn external address\"\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=89| \"path\": \"$HOMELAB_RUN_DIR/rollback.json\",\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=90| \"producer\": \"phases/rollback.sh\",\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=91| \"verification\": \"old DNS and pre-cutover edge-vm/VM150 routing are restored; EDGE-01 rollback remains manual\"\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=96| \"verification\": \"traffic cutover is verified without post-cutover hardening changes\"\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=106| \"host\": \"edge-vm IP_REDACTED\",\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=111| \"host\": \"VM150\",\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=116| \"host\": \"EDGE-01 IP_REDACTED\",\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=118| \"compile\": \"versioned nft asset; nft -c; host guard; external canary\"\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=121| \"rollback\": {\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=122| \"command\": \"HOMELAB_CONTROLLED_APPLY_CONFIRM=snikket-edge-cutover-v1 /opt/homelab-admin/bin/homelab-admin run TASK_DIR rollback\",\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=123| \"verification\": \"restore old chat A, remove VM150 source policy and persistent egress service, restore old default route; manual EDGE-01 restricted-ingress asset remains available\"\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=130| \"edge01_public_ip\": \"IP_REDACTED\",\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=131| \"edge01_wg_ip\": \"IP_REDACTED\",\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=132| \"edgevm_wg_ip\": \"IP_REDACTED\",\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=133| \"edgevm_lan_ip\": \"IP_REDACTED\",\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=134| \"vm150_ip\": \"IP_REDACTED\",\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=137| \"policy_table\": 17777,\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=139| \"turn_udp_min\": 60000,\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=140| \"turn_udp_max\": 60199,\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=141| \"edge01_automated_access\": false,\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=142| \"turn_legacy_record_mutation\": false,\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=143| \"turn_secret_rotation\": false,=REDACTED\nREC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=145| \"home_router_mutation\": false,\nCURRENT_EDGE_REFERENCE_FILE_COUNT=4\nREC|CURRENT_EDGE_REF_FILE|BASENAME=2026-08-17-snikket-edge-cutover-design.md|PARENT=specs|PATH_SHA=f0862c0e9d53ebf2bf9e6213cd190fc39de58a3f11c508d952aa26ad475d1e02\nREC|CURRENT_EDGE_REF_FILE|BASENAME=2026-08-17-snikket-post-cutover-hardening-design.md|PARENT=specs|PATH_SHA=7e742cf139b78adf59e860e331f4b8f02fbb75e42a5492dd969bbe003fdc65ec\nREC|CURRENT_EDGE_REF_FILE|BASENAME=task.json|PARENT=snikket-edge-cutover-v1|PATH_SHA=d0a271727e96d64818be706231c4f74d0dbe77bcadfaada5325e43b874fab0ba\nREC|CURRENT_EDGE_REF_FILE|BASENAME=cutover.py|PARENT=tools|PATH_SHA=6dbc303494115f30477da0b911dd387ed0e5959e1a3e322680980fb7cbb8b83a\nCURRENT_WGEDGE_EXACT_REFERENCE_COUNT=16\nDECISION=PASS_SNIKKET_WGEDGE_DESIGN_STATE_RCA\nNEXT_GATE=APPLY_CURRENT_SOURCE_DESIGN\nTASK_RESULT=SNIKKET_WGEDGE_DESIGN_STATE_RCA\nCHANGES_MADE=false\nROLLBACK_STARTED=false\nROLLBACK_RESTORED=null\nMUTATION_OUTCOME=NO_MUTATION\nHOMELAB_RESULT_CONTRACT={\"version\":1,\"command_id\":\"SUPPORT-261006-SNIKKET-WGEDGE-DESIGN-STATE-RCA-99739R1\",\"status\":\"OK\",\"changes_made\":false,\"rollback_started\":false,\"rollback_restored\":null}\n" + "output": "SNIKKET_HOME_ROLLBACK_PREFLIGHT=V1\nMODE=read-only\nNO_MUTATION=true\nSOURCE_HEAD=bd85699d8b588a34c480f10dee71323687bd48f1\nSOURCE_WORKTREE_DIRTY_COUNT=0\nSOURCE_ROLLBACK_NETWORK_PRESENT=true\nSOURCE_ROLLBACK_USES_CLOUDFLARE=true\nROLLBACK_STATE_PRESENT=true\nROLLBACK_STATE_SHA=b4696a99b8e13042680d3c56537e0e5e5b6d3e7fae69e01fb8b8f60d68c39fc7\nROLLBACK_STATE_MODE=600\nROLLBACK_STATE_MTIME=1786953752\nROLLBACK_STATE_JSON_VALID=true\nROLLBACK_STATE_HAS_DNS_CONTENT=true\nROLLBACK_STATE_HAS_DEFAULT_ROUTE=true\nROLLBACK_OLD_ROUTE_SHA=7c5515603e424ad8e85718ba716657be12298988ee11416f41081ec82e023409\nROLLBACK_HOME_IP_SHA=26ac437d63a8bcf254255cc970d7614e78bd0a67b97b5575a42a1c3ca7a301ee\nPVE01_IPIFY_RC=0\nPVE01_PUBLIC_IP_SHA=26ac437d63a8bcf254255cc970d7614e78bd0a67b97b5575a42a1c3ca7a301ee\nROLLBACK_HOME_MATCHES_CURRENT_PVE01_PUBLIC=true\nVM150_QGA_TRANSPORT_RC=0\nREC|VM150|QGA_EXIT=0\nREC|VM150|DOMAIN_SHA=04cb4a04e408d91abb9c63701ae969abf16d8a12b6f4f144c5edf2c28e03256b\nREC|VM150|EDGE_EGRESS_UNIT_PRESENT=true\nREC|VM150|EDGE_EGRESS_ACTIVE=active\nREC|VM150|EDGE_EGRESS_ENABLED=enabled\nREC|VM150|EDGE_EGRESS_UNIT_SHA=83a20356e221dfb22ea2a0d99e327f7c2f791a3d3127a46af2d1625b33c95b5d\nREC|VM150|OLDGW_SHA=4f9da1283166a1e9f4e6bc7dd240c76aef85772177f717f92abb6e8e2bb8de90\nREC|VM150|NEWGW_SHA=bc517b4af3298846906234d3d697d3820b0b652ade1880c937c9d71cd802c834\nREC|VM150|OLDGW_PING_RC=1\nREC|VM150|CURRENT_GW_SHA=bc517b4af3298846906234d3d697d3820b0b652ade1880c937c9d71cd802c834\nREC|VM150|REC|CONTAINER|NAME=snikket|RUNNING=true|RESTARTS=0\nREC|VM150|REC|CONTAINER|NAME=snikket-proxy|RUNNING=true|RESTARTS=0\nREC|VM150|REC|CONTAINER|NAME=snikket-certs|RUNNING=true|RESTARTS=0\nREC|VM150|REC|CONTAINER|NAME=snikket-portal|RUNNING=true|RESTARTS=0\nREC|VM150|LOCAL_HTTPS_RC=0\nREC|VM150|LOCAL_HTTPS_CODE=302\nVM130_QGA_TRANSPORT_RC=0\nREC|VM130|QGA_EXIT=0\nREC|VM130|ROLE=VM130\nREC|VM130|WG_CONFIG_PRESENT=true\nREC|VM130|WG_BACKUP_PRESENT=true\nREC|VM130|WG_CONFIG_SHA=42b046d580b2ccd6af8f3573fba4882c71c92b2938f3fed8edf58258bbc89615\nREC|VM130|WG_BACKUP_SHA=9012ece867ad7efa1b2d07020c1cdfc9d3a63f86fde9117bdd7435eef29a9735\nREC|VM130|WG_BACKUP_MODE=600\nREC|VM130|PRIORITY101_COUNT=1\nREC|VM130|TABLE17777_DEFAULT_COUNT=1\nREC|VM130|WG_EDGE_ACTIVE=active\nCURRENT_AUTHORITATIVE_BEGET=true\nREC|DNS|ROLE=primary|A_COUNT=1|MATCH_ROLLBACK_HOME=true\nREC|DNS|ROLE=groups|A_COUNT=1|MATCH_ROLLBACK_HOME=true\nREC|DNS|ROLE=share|A_COUNT=1|MATCH_ROLLBACK_HOME=true\nBEGET_LOGIN_CANDIDATE_COUNT=0\nBEGET_PASSWORD_CANDIDATE_COUNT=0\nBEGET_CREDENTIALS_UNIQUE=false\nREC|HOME_TCP|PORT=443|OPEN=true|MSEC=1\nREC|HOME_TCP|PORT=5000|OPEN=true|MSEC=0\nREC|HOME_TCP|PORT=5222|OPEN=true|MSEC=0\nREC|HOME_TCP|PORT=5349|OPEN=false|ERR=TimeoutError\nROLLBACK_PREFLIGHT_READY=false\nDECISION=PASS_SNIKKET_HOME_ROLLBACK_PREFLIGHT\nNEXT_GATE=ROLLBACK_PREFLIGHT_BLOCKER_REPAIR\nTASK_RESULT=SNIKKET_HOME_ROLLBACK_PREFLIGHT\nCHANGES_MADE=false\nROLLBACK_STARTED=false\nROLLBACK_RESTORED=null\nMUTATION_OUTCOME=NO_MUTATION\nHOMELAB_RESULT_CONTRACT={\"version\":1,\"command_id\":\"SUPPORT-261006-SNIKKET-HOME-ROLLBACK-PREFLIGHT-99740R1\",\"status\":\"OK\",\"changes_made\":false,\"rollback_started\":false,\"rollback_restored\":null}\n" } diff --git a/runtime/latest.txt b/runtime/latest.txt index 11c80238..c299c75d 100644 --- a/runtime/latest.txt +++ b/runtime/latest.txt @@ -1,15 +1,15 @@ CHAT_OUTPUT_BEGIN -COMMAND_ID=SUPPORT-261006-SNIKKET-WGEDGE-DESIGN-STATE-RCA-99739R1 +COMMAND_ID=SUPPORT-261006-SNIKKET-HOME-ROLLBACK-PREFLIGHT-99740R1 STATUS=OK RC=0 HOST=pve01 MODE=read-only -COMPONENT=snikket-wgedge-design-state-rca +COMPONENT=snikket-home-rollback-preflight REFERENCE_REGISTER_CHECK=OK REFERENCE_SHA256=5a3d8e5154c41cb582a4e0aca68090be1f0138918bf82131a948df326f9d8d66 ERROR_REGISTER_CHECK=OK ERROR_REGISTER_SHA256=3b09a553ec0f527ed3afeed4753f52a74ee3036045bbfb3c685e9f8af4ba7ba0 -COMMAND_SHA256=a60a8c29d502a49e98c500b961f57451d0ca3fd661de694738452d4ba50dff96 +COMMAND_SHA256=4336c13341dc97c6ef0a781e34aa07cfa6829de4686391dfbda4b3b00dc90150 DUPLICATE_FAILED_COMMAND_BLOCKED=false EXECUTION_STARTED=true CHANGE_DECLARED=false @@ -24,415 +24,76 @@ MUTATION_OUTCOME=NO_MUTATION SANITIZED=yes SECRETS_INCLUDED=no PRIVATE_ADDRESSES_INCLUDED=no -RAW_EVIDENCE_SHA256=64709396903adcb76579d798fddab1c280c55806f26dc184c45423275962c07d -SANITIZED_OUTPUT_SHA256=64709396903adcb76579d798fddab1c280c55806f26dc184c45423275962c07d +RAW_EVIDENCE_SHA256=4737052a112e41dfe5fe93073b59c14131211a94191d7ad6c2dd0dd76e8164c6 +SANITIZED_OUTPUT_SHA256=4737052a112e41dfe5fe93073b59c14131211a94191d7ad6c2dd0dd76e8164c6 OUTPUT_BEGIN -SNIKKET_WGEDGE_DESIGN_STATE_RCA=V1 +SNIKKET_HOME_ROLLBACK_PREFLIGHT=V1 MODE=read-only NO_MUTATION=true -SOURCE_REPO_BASENAME=homelab-ops SOURCE_HEAD=bd85699d8b588a34c480f10dee71323687bd48f1 -SOURCE_BRANCH=main SOURCE_WORKTREE_DIRTY_COUNT=0 -REC|COMMIT|LABEL=ORIGIN|PRESENT=true|COMMIT=911d8cc52d5e8a677f132b736d01ade7a5879e59|DATE=2026-08-17T10:27:24+03:00|SUBJECT=CR-2026-0080: build Snikket EDGE cutover task|PARENTS=bcee02410290c4563fea4507a35fdf6dce08d674 -REC|COMMIT_FILES|LABEL=ORIGIN|COUNT=17 -REC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=task-package.sha256|PARENT=CR-2026-0080|PATH_SHA=070b91ffb62312169dc9f08e768758b846237b4c56e71cc58e5946da174dafc1 -REC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=2026-08-17-snikket-edge-cutover.md|PARENT=plans|PATH_SHA=72bcfe0160d221fc3f06466006c3163dc2e5f67f2224cacd1c5c50e02135eb37 -REC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=1|# Snikket EDGE Cutover Implementation Plan -REC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=3|**Goal:** finish the traffic cutover of chat.gram1.ru to EDGE-01 while preserving rollback and keeping VM150 source-policy disabled until DNS convergence. -REC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=5|## Task 1 — Version the production package -REC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=6|- Create `tasks/snikket-edge-cutover-v1/task.json` with schema 2, controlled_apply, fail_fast, seven phases, exact expected outputs and rollback contract. -REC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=7|- Create executable phase wrappers: `phases/preflight.sh`, `prepare.sh`, `dry-run.sh`, `apply.sh`, `verify.sh`, `rollback.sh`, `seal.sh`. -REC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=8|- Create `tools/cutover.py` as the single orchestration implementation; wrappers only select the phase. -REC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=9|- Create `assets/edge01-public-ingress.nft` that removes only the temporary source restriction while retaining transitional symmetric SNAT. -REC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=10|- Create `assets/edge01-home-restricted-ingress.nft` as the exact operator rollback contract. -REC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=14|- Create `tests/test_cr_2026_0080_snikket_edge_cutover.py` before implementation. -REC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=15|- RED assertions: task package absent; public ingress must not contain `ip saddr IP_REDACTED`; source-policy activation must be gated after DNS convergence; seven phase files must exist and be executable; rollback must restore old A and routing state. -REC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=20|- preflight: prove chat A is still IP_REDACTED, persistent ingress canary is green, edge-vm table 17777 exists, VM150 policy rule is absent, VM150 network renderer has the expected old gateway, and NPMplus can perform a non-mutating Cloudflare record lookup. -REC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=21|- prepare: capture exact non-sensitive DNS metadata, edge-vm routing configuration and VM150 network-file bytes into the run rollback directory. -REC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=22|- dry-run: calculate the exact A-record mutation, policy rule, VM150 gateway change and rollback operations without production mutation. -REC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=23|- apply: require explicit EDGE-01 public-ingress operator attestation; change only chat.gram1.ru A to IP_REDACTED; wait for authoritative and public convergence; only then install priority-101 source policy for IP_REDACTED via table 17777 and change VM150 gateway to IP_REDACTED; refresh only the Snikket server container if effective TURN addressing still shows the old origin. -REC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=24|- verify: HTTPS, TCP 5000/5222/3478/3479/5349/5350, XMPP STARTTLS, TURN TLS, UDP STUN, VM150 outbound public IP IP_REDACTED, DNS convergence and effective TURN address without IP_REDACTED. -REC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=25|- rollback: restore VM150 network bytes and live default route, remove priority-101 policy and persistence, restore the previous chat A value; operator applies the versioned EDGE-01 restricted-ingress rollback asset if EDGE ingress must also be reverted. -REC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=26|- seal: record only the proven traffic cutover. Do not remove turn.gram1.ru, rotate TURN auth material, alter certificate renewal or remove home-router forwards. -REC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover.md|LINE=31|- On `root@edge01`, apply the merged public-ingress asset with host guard, backup, `nft -c`, live canary and rollback. -REC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=2026-08-17-snikket-edge-cutover-design.md|PARENT=specs|PATH_SHA=224ed8ac7b09df73545e49a1ac681848f67b00df285ec5a3b7b8398f21d015ee -REC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover-design.md|LINE=1|# Snikket EDGE Traffic Cutover Design -REC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover-design.md|LINE=7|Move chat.gram1.ru from home IP IP_REDACTED to EDGE-01 IP_REDACTED. Persistent EDGE-01 and edge-vm inbound transit is already installed and TCP, XMPP STARTTLS, TURN TLS, UDP STUN and HTTPS canaries pass. -REC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover-design.md|LINE=8|Before cutover, EDGE-01 ingress is still restricted to source IP_REDACTED, chat.gram1.ru A is still IP_REDACTED, edge-vm table 17777 is ready, and no source rule for VM150 IP_REDACTED is active. -REC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover-design.md|LINE=11|CR0080 covers only production traffic cutover: publicize the verified EDGE-01 ingress contract, change only chat.gram1.ru A to IP_REDACTED, wait for DNS convergence, activate VM150 egress through edge-vm and EDGE-01, refresh effective TURN addressing, verify, rollback and seal. -REC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover-design.md|LINE=12|Excluded post-cutover work: remove turn.gram1.ru, rotate the TURN static auth se=REDACTED -REC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover-design.md|LINE=15|Use tasks/snikket-edge-cutover-v1 on pve01.gram1.ru with controlled_apply and phases preflight, prepare, dry-run, apply, verify, rollback and seal. -REC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover-design.md|LINE=16|EDGE-01 remains manual-operator access only. CR0080 versions the exact public-ingress contract, but the operator applies it from the established root@edge01 session; the pve01 task must not invent an automated EDGE-01 SSH path. -REC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover-design.md|LINE=17|DNS mutation is fail-closed: apply requires operator attestation that the exact EDGE-01 public-ingress contract is active and an approved local DNS credential file path. Credential values are never printed. -REC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover-design.md|LINE=20|Apply order is fixed: prove pre-state; require EDGE-01 public-ingress attestation and canary; change chat A; wait for authoritative and public DNS convergence; only then activate edge-vm source policy for IP_REDACTED via table 17777 and move VM150 Internet traffic through IP_REDACTED; refresh only the required Snikket or coturn component. -REC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover-design.md|LINE=21|Verification covers HTTPS chat/groups/share; TCP 5000,5222,3478,3479,5349,5350; XMPP STARTTLS; TURN TLS; UDP STUN and relay 60000-60199; VM150 outbound IP IP_REDACTED; DNS convergence; and no effective TURN advertisement containing IP_REDACTED. -REC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover-design.md|LINE=23|## Failure and rollback -REC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover-design.md|LINE=24|Every phase is fail-fast and no later phase runs after a failed gate. Prepare re=REDACTED -REC|SOURCE_TEXT|LABEL=ORIGIN:2026-08-17-snikket-edge-cutover-design.md|LINE=25|Zero-secret diagnostics are mandatory: never print passwords, API credentials, p=REDACTED -REC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=README.txt|PARENT=snikket-edge-cutover-v1|PATH_SHA=418b863db2e962e7c0bf3a5540f0f4b9a328c01120ffd81371c9cdf43a8664df -REC|SOURCE_TEXT|LABEL=ORIGIN:README.txt|LINE=1|snikket-edge-cutover-v1 -REC|SOURCE_TEXT|LABEL=ORIGIN:README.txt|LINE=4|- production traffic cutover for chat.gram1.ru from IP_REDACTED to EDGE-01 IP_REDACTED; -REC|SOURCE_TEXT|LABEL=ORIGIN:README.txt|LINE=5|- DNS convergence before VM150 source-policy activation; -REC|SOURCE_TEXT|LABEL=ORIGIN:README.txt|LINE=6|- rollback-safe edge-vm and VM150 routing transition; -REC|SOURCE_TEXT|LABEL=ORIGIN:README.txt|LINE=7|- no automated EDGE-01 access from pve01. -REC|SOURCE_TEXT|LABEL=ORIGIN:README.txt|LINE=9|Manual EDGE-01 gate: -REC|SOURCE_TEXT|LABEL=ORIGIN:README.txt|LINE=10|1. Apply assets/edge01-public-ingress.nft from the established root@edge01 MobaXterm session. -REC|SOURCE_TEXT|LABEL=ORIGIN:README.txt|LINE=11|2. Verify nft -c and full public service canary. -REC|SOURCE_TEXT|LABEL=ORIGIN:README.txt|LINE=12|3. Supply HOMELAB_SNIKKET_EDGE01_PUBLIC_INGRESS_ATTEST_SHA256 equal to the merged asset SHA256 when running apply. -REC|SOURCE_TEXT|LABEL=ORIGIN:README.txt|LINE=17|turn.gram1.ru cleanup, TURN secret rotation, certificate renewal changes, home-r=REDACTED -REC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=edge01-home-restricted-ingress.nft|PARENT=assets|PATH_SHA=ae3180f52ef9fecc84167c6f8618aa48a669c99f5387914f04f96b3ae4bff345 -REC|SOURCE_TEXT|LABEL=ORIGIN:edge01-home-restricted-ingress.nft|LINE=1|add chain inet edge01_filter snikket_prod_ingress -REC|SOURCE_TEXT|LABEL=ORIGIN:edge01-home-restricted-ingress.nft|LINE=2|add rule inet edge01_filter snikket_prod_ingress iifname "eth0" oifname "wg-home" ip saddr IP_REDACTED ip daddr IP_REDACTED tcp dport { 3478, 3479, 5000, 5222, 5269, 5349, 5350 } accept -REC|SOURCE_TEXT|LABEL=ORIGIN:edge01-home-restricted-ingress.nft|LINE=3|add rule inet edge01_filter snikket_prod_ingress iifname "eth0" oifname "wg-home" ip saddr IP_REDACTED ip daddr IP_REDACTED udp dport { 3478-3479, 5349-5350, 60000-60199 } accept -REC|SOURCE_TEXT|LABEL=ORIGIN:edge01-home-restricted-ingress.nft|LINE=4|add rule inet edge01_filter snikket_prod_ingress iifname "wg-home" oifname "eth0" ip saddr IP_REDACTED ip daddr IP_REDACTED ct state established,related accept -REC|SOURCE_TEXT|LABEL=ORIGIN:edge01-home-restricted-ingress.nft|LINE=5|insert rule inet edge01_filter forward jump snikket_prod_ingress comment "SNIKKET_PROD_INGRESS_JUMP" -REC|SOURCE_TEXT|LABEL=ORIGIN:edge01-home-restricted-ingress.nft|LINE=7|table ip snikket_prod_ingress { -REC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=edge01-public-ingress.nft|PARENT=assets|PATH_SHA=1678d03b6d6be773e5c875c7f85a548c0bd95314c58645ed8f567df9e8aaacfd -REC|SOURCE_TEXT|LABEL=ORIGIN:edge01-public-ingress.nft|LINE=1|add chain inet edge01_filter snikket_prod_ingress -REC|SOURCE_TEXT|LABEL=ORIGIN:edge01-public-ingress.nft|LINE=2|add rule inet edge01_filter snikket_prod_ingress iifname "eth0" oifname "wg-home" ip daddr IP_REDACTED tcp dport { 3478, 3479, 5000, 5222, 5349, 5350 } accept -REC|SOURCE_TEXT|LABEL=ORIGIN:edge01-public-ingress.nft|LINE=3|add rule inet edge01_filter snikket_prod_ingress iifname "eth0" oifname "wg-home" ip daddr IP_REDACTED udp dport { 3478-3479, 5349-5350, 60000-60199 } accept -REC|SOURCE_TEXT|LABEL=ORIGIN:edge01-public-ingress.nft|LINE=4|add rule inet edge01_filter snikket_prod_ingress iifname "wg-home" oifname "eth0" ip saddr IP_REDACTED ct state established,related accept -REC|SOURCE_TEXT|LABEL=ORIGIN:edge01-public-ingress.nft|LINE=5|insert rule inet edge01_filter forward jump snikket_prod_ingress comment "SNIKKET_PROD_INGRESS_JUMP" -REC|SOURCE_TEXT|LABEL=ORIGIN:edge01-public-ingress.nft|LINE=7|table ip snikket_prod_ingress { -REC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=npmplus-cloudflare-a.sh|PARENT=assets|PATH_SHA=48c7bcfaa5f7de8282caa4b35cabc573876f7c8336c5988c3029b66c074fd1f4 -REC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=apply.sh|PARENT=phases|PATH_SHA=c375ac75c7c4370efdd0c29fce9f5afc7a46ac38e10bf4e031d2fdeb44525324 -REC|SOURCE_TEXT|LABEL=ORIGIN:apply.sh|LINE=10|python3 "$TASK_DIR/tools/cutover.py" apply -REC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=dry-run.sh|PARENT=phases|PATH_SHA=a1bf65bfca5d89a9e6f37e7ef1e95d23300b884df043c9e68fae1dd95944482d -REC|SOURCE_TEXT|LABEL=ORIGIN:dry-run.sh|LINE=10|python3 "$TASK_DIR/tools/cutover.py" dry-run -REC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=preflight.sh|PARENT=phases|PATH_SHA=cd56fdeeae17cfc3be7d68b996f8b5350ccd930252c88da7871bbbe7f97a0def -REC|SOURCE_TEXT|LABEL=ORIGIN:preflight.sh|LINE=10|python3 "$TASK_DIR/tools/cutover.py" preflight -REC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=prepare.sh|PARENT=phases|PATH_SHA=f4c8867f85674ee74bc7f04a82b8336eeeefaa959ec9b198ca460170de1e1990 -REC|SOURCE_TEXT|LABEL=ORIGIN:prepare.sh|LINE=10|python3 "$TASK_DIR/tools/cutover.py" prepare -REC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=rollback.sh|PARENT=phases|PATH_SHA=f2998ffe82d9ff129223379159c31aaa5bd5f4769b2489b3c8d471b7d21d7064 -REC|SOURCE_TEXT|LABEL=ORIGIN:rollback.sh|LINE=5| printf 'ROOT_CAUSE_COUNT=1\nFAILED_PHASE=rollback\n' >&2 -REC|SOURCE_TEXT|LABEL=ORIGIN:rollback.sh|LINE=10|python3 "$TASK_DIR/tools/cutover.py" rollback -REC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=seal.sh|PARENT=phases|PATH_SHA=49326c4de4695fa8d0fb4ae205ba863305f1234c60308c011ce7f1cb87afdea2 -REC|SOURCE_TEXT|LABEL=ORIGIN:seal.sh|LINE=10|python3 "$TASK_DIR/tools/cutover.py" seal -REC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=verify.sh|PARENT=phases|PATH_SHA=e562fbaefd2c1e7f70ef0cdbb832e4a09472614da0c6824eac57f8267df00c74 -REC|SOURCE_TEXT|LABEL=ORIGIN:verify.sh|LINE=10|python3 "$TASK_DIR/tools/cutover.py" verify -REC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=task.json|PARENT=snikket-edge-cutover-v1|PATH_SHA=9a63e2769a24f3ce2c23c172837f46c1f044cbd8f886e45313a3b0e7e467e55d -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=3| "task_id": "snikket-edge-cutover-v1", -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=6| "production_change": true, -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=14| "rollback": "phases/rollback.sh", -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=18| "active_backup_restore_max": 0 -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=22| "EDGE-01 public service path at IP_REDACTED", -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=23| "edge-vm IP_REDACTED routing and /etc/wireguard/wg-edge01.conf metadata", -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=24| "VM150 QGA network and Snikket runtime state", -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=33| "$HOMELAB_RUN_DIR/rollback.json", -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=35| "/mnt/staging/homelab-admin-rollback/snikket-edge-cutover-v1/state.json", -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=37| "edge-vm policy rule priority 101 and wg-edge01.conf PostUp/PostDown", -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=38| "VM150 /etc/systemd/system/snikket-edge-egress.service" -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=42| "edge-vm policy routing for source IP_REDACTED", -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=43| "VM150 default Internet route via IP_REDACTED", -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=44| "Snikket server container restart after DNS convergence" -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=47| "EDGE-01 is never accessed automatically by this task", -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=48| "turn.gram1.ru is not modified or deleted", -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=49| "TURN static auth material is not read, printed, or rotated", -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=51| "home router forwards are unchanged", -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=57| "tools/cutover.py" -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=66| "verification": "pre-cutover DNS, public canary, edge-vm table 17777, absent VM150 source policy, VM150 old default route, non-mutating Cloudflare lookup" -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=71| "verification": "non-secret rollback state is persisted before mutation"=REDACTED -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=76| "verification": "exact DNS/routing/service mutations and rollback order are calculated without mutation" -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=81| "verification": "EDGE-01 public ingress attestation, DNS convergence, source policy, VM150 egress, Snikket refresh" -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=86| "verification": "DNS, HTTPS, XMPP, TURN/STUN, VM150 egress and effective coturn external address" -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=89| "path": "$HOMELAB_RUN_DIR/rollback.json", -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=90| "producer": "phases/rollback.sh", -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=91| "verification": "old DNS and pre-cutover edge-vm/VM150 routing are restored; EDGE-01 rollback remains manual" -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=96| "verification": "traffic cutover is verified without post-cutover hardening changes" -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=106| "host": "edge-vm IP_REDACTED", -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=111| "host": "VM150", -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=116| "host": "EDGE-01 IP_REDACTED", -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=118| "compile": "versioned nft asset; nft -c; host guard; external canary" -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=121| "rollback": { -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=122| "command": "HOMELAB_CONTROLLED_APPLY_CONFIRM=snikket-edge-cutover-v1 /opt/homelab-admin/bin/homelab-admin run TASK_DIR rollback", -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=123| "verification": "restore old chat A, remove VM150 source policy and persistent egress service, restore old default route; manual EDGE-01 restricted-ingress asset remains available" -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=130| "edge01_public_ip": "IP_REDACTED", -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=131| "edge01_wg_ip": "IP_REDACTED", -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=132| "edgevm_wg_ip": "IP_REDACTED", -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=133| "edgevm_lan_ip": "IP_REDACTED", -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=134| "vm150_ip": "IP_REDACTED", -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=137| "policy_table": 17777, -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=139| "turn_udp_min": 60000, -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=140| "turn_udp_max": 60199, -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=141| "edge01_automated_access": false, -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=142| "turn_legacy_record_mutation": false, -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=143| "turn_secret_rotation": false,=REDACTED -REC|SOURCE_TEXT|LABEL=ORIGIN:task.json|LINE=145| "home_router_mutation": false, -REC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=cutover.py|PARENT=tools|PATH_SHA=d90388278e2775b1ac7534b35946dfa3dadd03f65cd16117db653731b47346ad -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=9|EDGE = "IP_REDACTED" -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=10|EDGEVM = "IP_REDACTED" -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=11|VM150 = "IP_REDACTED" -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=13|WG_TABLE = "17777" -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=14|RULE_PRIO = "101" -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=15|ROLLBACK = pathlib.Path("/mnt/staging/homelab-admin-rollback/snikket-edge-cutover-v1") -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=16|EDGE_SSH = ["ssh","-o","BatchMode=yes","-o","StrictHostKeyChecking=yes","-o","ConnectTimeout=8","debian@IP_REDACTED"] -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=19|class CutoverError(RuntimeError): -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=24| if check and p.returncode: -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=25| raise CutoverError(f"command-failed rc={p.returncode} tool={pathlib.Path(cmd[0]).name}") -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=26| return p -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=31| raise CutoverError("HOMELAB_RUN_DIR missing") -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=34| return p -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=41| raise CutoverError("qga-json") from e -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=43| raise CutoverError("qga-command-failed") -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=44| return d.get("out-data","") -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=46|def edgevm(script,timeout=90): -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=47| return sh(EDGE_SSH+["sudo","-n","bash","-s"],input_text=script,timeout=timeout).stdout -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=51| cmd=EDGE_SSH+["sudo","-n","docker","exec","-i","npmplus","sh","-s","--",action,CHAT] -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=58| raise CutoverError("cloudflare-safe-json") from e -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=60| raise CutoverError("cloudflare-operation-failed") -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=61| return d -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=66| raise CutoverError("no-authoritative-ns") -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=67| return out[0].rstrip(".") -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=75| return out[-1] if out else "" -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=82| return last -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=84| raise CutoverError("dns-convergence-timeout:"+json.dumps(last,sort_keys=True)) -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=97| raise CutoverError("tcp-canary") -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=98| return out -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=109| raise CutoverError("wan-5269-must-remain-closed") -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=110| return True -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=118| raise CutoverError("https-canary") -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=119| return out -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=122| x=sh(["openssl","s_client","-connect",f"{EDGE}:5222","-starttls","xmpp","-xmpphost",CHAT,"-verify_return_error"],input_text="",check=False,timeout=15) -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=123| t=sh(["openssl","s_client","-connect",f"{EDGE}:5349","-servername",CHAT,"-verify_return_error"],input_text="",check=False,timeout=15) -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=124| if "Verify return code: 0 (ok)" not in x.stdout+x.stderr: -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=125| raise CutoverError("xmpp-tls") -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=126| if "Verify return code: 0 (ok)" not in t.stdout+t.stderr: -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=127| raise CutoverError("turn-tls") -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=128| return {"xmpp":True,"turn":True} -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=144| raise CutoverError("udp-stun") -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=145| return out -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=148| return {"tcp":tcp_canary(EDGE),"https":https_canary(EDGE),"tls":tls_canary(),"stun":udp_stun(EDGE)} -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=150|def edgevm_rule_present(): -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=151| return bool(re.search(r"^101:\s+from 192\.168\.50\.31(?:/32)? lookup 17777$",edgevm("ip -4 rule show"),re.M)) -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=153|def edgevm_priority_free(): -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=154| return not bool(re.search(r"^101:",edgevm("ip -4 rule show"),re.M)) -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=156|def edgevm_table_ready(): -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=157| return "default dev wg-edge01" in edgevm("ip -4 route show table 17777") -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=160| return qga("ip -4 route show default | head -n1").strip() -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=162|def vm_egress(): -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=163| return qga("curl -4 -fsS --connect-timeout 5 --max-time 12 https://api.ipify.org").strip() -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=165|def coturn_external(): -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=166| inner='pid="$(pgrep -xo turnserver || true)"; [ -n "$pid" ] || exit 2; tr "\\0" "\\n" <"/proc/$pid/cmdline" | grep -A1 -x -- "-X" | tail -n1' -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=167| return qga("docker exec snikket sh -lc "+json.dumps(inner)).strip() -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=171| raise CutoverError("chat-authoritative-a-not-home") -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=172| if not edgevm_table_ready(): -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=173| raise CutoverError("table-17777-not-ready") -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=174| if edgevm_rule_present(): -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=175| raise CutoverError("vm150-source-rule-already-present") -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=176| if not edgevm_priority_free(): -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=177| raise CutoverError("policy-priority-101-in-use") -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=180| raise CutoverError("vm150-default-not-home") -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=183| raise CutoverError("cloudflare-record-prestate") -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=184| return {"dns":rec,"default_route":d,"public_canary":public_canary()} -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=186|def save_rollback(state): -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=187| ROLLBACK.mkdir(parents=True,exist_ok=True); os.chmod(ROLLBACK,0o700) -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=188| p=ROLLBACK/"state.json" -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=190| raise CutoverError("rollback-state-already-exists") -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=193|def load_rollback(): -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=194| p=ROLLBACK/"state.json" -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=196| raise CutoverError("rollback-state-missing") -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=197| return json.loads(p.read_text()) -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=199|def install_edgevm_policy(): -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=201|CFG=/etc/wireguard/wg-edge01.conf -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=202|BK=/root/wg-edge01.before-snikket-edge-cutover-v1.conf -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=208|up="PostUp = ip rule add priority 101 from IP_REDACTED lookup 17777" -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=209|down="PostDown = ip rule del priority 101 from IP_REDACTED lookup 17777 2>/dev/null || true" -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=211| marker="PostDown = ip route del table 17777 default dev wg-edge01 2>/dev/null || true" -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=216|ip rule add priority 101 from IP_REDACTED lookup 17777 -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=217|ip -4 rule show | grep -Eq '^101:.*from 192\.168\.50\.31(/32)? lookup 17777$' -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=219| edgevm(script) -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=221|def remove_edgevm_policy(): -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=222| edgevm('set -Eeuo pipefail; ip rule del priority 101 2>/dev/null || true; BK=/root/wg-edge01.before-snikket-edge-cutover-v1.conf; CFG=/etc/wireguard/wg-edge01.conf; if [ -f "$BK" ]; then cp -a "$BK" "$CFG"; fi') -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=225|Description=Snikket Internet egress via edge-vm -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=231|ExecStart=/usr/sbin/ip route replace default via IP_REDACTED dev eth0 -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=232|ExecStop=/usr/sbin/ip route replace default via IP_REDACTED dev eth0 -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=239|def install_vm_egress(): -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=241| qga(f'set -Eeuo pipefail; test ! -e /etc/systemd/system/snikket-edge-egress.service; printf %s {b64} | base64 -d >/etc/systemd/system/snikket-edge-egress.service; chmod 0644 /etc/systemd/system/snikket-edge-egress.service; systemctl daemon-reload; systemctl enable --now snikket-edge-egress.service; ip -4 route show default | grep -F "default via IP_REDACTED dev eth0"') -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=243|def remove_vm_egress(): -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=244| qga('set -Eeuo pipefail; systemctl disable --now snikket-edge-egress.service 2>/dev/null || true; rm -f /etc/systemd/system/snikket-edge-egress.service; systemctl daemon-reload; ip route replace default via IP_REDACTED dev eth0') -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=246|def refresh_snikket(): -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=247| qga("docker restart snikket >/dev/null; for i in $(seq 1 30); do docker exec snikket pgrep -x turnserver >/dev/null 2>&1 && exit 0; sleep 2; done; exit 1",timeout=80) -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=250| dns=dns_converged(EDGE,60); can=public_canary(); federation_5269_closed(EDGE) -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=251| if not edgevm_rule_present(): -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=252| raise CutoverError("vm150-source-rule-missing") -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=254| if f"default via {EDGEVM} " not in d: -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=255| raise CutoverError("vm150-default-not-edgevm") -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=256| eg=vm_egress() -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=257| if eg != EDGE: -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=258| raise CutoverError("vm150-egress-ip") -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=259| ext=coturn_external() -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=260| if EDGE not in ext or HOME in ext: -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=261| raise CutoverError("coturn-external-address") -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=262| return {"dns":dns,"canary":can,"vm_default":d,"vm_egress":eg,"coturn_external":ext} -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=266| d["edge01_public_ingress_asset_sha256"]=hashlib.sha256((ASSETS/"edge01-public-ingress.nft").read_bytes()).hexdigest() -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=271| save_rollback(st) -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=272| write_phase("prepare",{"status":"OK","rollback_state":str(ROLLBACK/"state.json"),"old_dns_content":st["dns"]["content"],"old_default_route":st["default_route"]}) -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=275| st=load_rollback() -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=277| raise CutoverError("rollback-old-dns-not-home") -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=278| actions=["operator applies versioned EDGE-01 public-ingress asset",f"chat A {HOME} -> {EDGE}","wait authoritative + IP_REDACTED + IP_REDACTED convergence",f"edge-vm rule priority {RULE_PRIO} from {VM150}/32 lookup {WG_TABLE}",f"VM150 default route {OLD_GW} -> {EDGEVM}","restart snikket container to refresh coturn external address","full verification"] -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=279| write_phase("dry-run",{"status":"OK","actions":actions,"rollback":["restore VM150 route","remove edge-vm source rule/persistence",f"chat A -> {HOME}","operator may apply EDGE-01 restricted-ingress asset"]}) -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=282| expected=hashlib.sha256((ASSETS/"edge01-public-ingress.nft").read_bytes()).hexdigest() -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=283| if os.environ.get("HOMELAB_SNIKKET_EDGE01_PUBLIC_INGRESS_ATTEST_SHA256") != expected: -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=284| raise CutoverError("edge01-public-ingress-attestation-missing-or-wrong") -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=285| load_rollback(); public_canary(); federation_5269_closed(EDGE); cf("set",EDGE); dns=dns_converged(EDGE,360) -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=286| install_edgevm_policy(); install_vm_egress() -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=287| eg=vm_egress() -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=288| if eg != EDGE: -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=289| raise CutoverError("vm150-egress-after-cutover") -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=290| refresh_snikket(); ext=coturn_external() -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=291| if EDGE not in ext or HOME in ext: -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=292| raise CutoverError("coturn-external-after-refresh") -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=293| write_phase("apply",{"status":"OK","dns":dns,"vm150_egress":eg,"coturn_external":ext}) -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=298|def phase_rollback(): -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=299| st=load_rollback(); remove_vm_egress(); remove_edgevm_policy(); old=st["dns"]["content"]; cf("set",old); dns=dns_converged(old,360) -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=300| write_phase("rollback",{"status":"OK","dns":dns,"vm_default":vm_default(),"edge01_manual_asset":"assets/edge01-home-restricted-ingress.nft"}) -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=304| write_phase("seal",{"status":"SEALED","traffic_cutover":True,"verified_utc":=REDACTED -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=306|PHASES={"preflight":phase_preflight,"prepare":phase_prepare,"dry-run":phase_dry_run,"apply":phase_apply,"verify":phase_verify,"rollback":phase_rollback,"seal":phase_seal} -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=309| raise SystemExit("usage: cutover.py PHASE") -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=312| except CutoverError as e: -REC|SOURCE_TEXT|LABEL=ORIGIN:cutover.py|LINE=313| print(f"CUTOVER_ERROR={e}",file=sys.stderr); raise SystemExit(1) -REC|COMMIT_FILE|LABEL=ORIGIN|BASENAME=test_cr_2026_0080_snikket_edge_cutover.py|PARENT=tests|PATH_SHA=4166d64d93a4f81a59f9cf85d667034d01aad54a3b75c3d4aa1858fcfe9797bd -REC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=3|TASK=ROOT/"tasks"/"snikket-edge-cutover-v1" -REC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=5|class CR0080CutoverTest(unittest.TestCase): -REC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=12| self.assertEqual(task["task_id"],"snikket-edge-cutover-v1") -REC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=13| self.assertTrue(task["production_change"]) -REC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=16| self.assertEqual(set(task["phases"]),{"preflight","prepare","dry-run","apply","verify","rollback","seal"}) -REC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=17| expected=[f"$HOMELAB_RUN_DIR/{name}.json" for name in ("preflight","prepare","dry-run","apply","verify","rollback","seal")] -REC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=28| def test_public_ingress_removes_only_home_source_guard(self): -REC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=29| pub=(TASK/"assets/edge01-public-ingress.nft").read_text() -REC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=30| rb=(TASK/"assets/edge01-home-restricted-ingress.nft").read_text() -REC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=33| for needle in ("IP_REDACTED","IP_REDACTED","snat to IP_REDACTED","60000-60199","SNIKKET_PROD_INGRESS_JUMP"): -REC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=38| def test_apply_is_dns_then_policy_then_vmroute(self): -REC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=39| src=(TASK/"tools/cutover.py").read_text() -REC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=41| self.assertIn("HOMELAB_SNIKKET_EDGE01_PUBLIC_INGRESS_ATTEST_SHA256",body) -REC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=42| self.assertIn("federation_5269_closed(EDGE)",body) -REC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=43| self.assertLess(body.index('cf("set",EDGE)'),body.index("dns_converged(EDGE")) -REC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=44| self.assertIn("edge01-public-ingress-attestation-missing-or-wrong",body) -REC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=45| self.assertLess(body.index("dns_converged(EDGE"),body.index("install_edgevm_policy()")) -REC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=46| self.assertLess(body.index("install_edgevm_policy()"),body.index("install_vm_egress()")) -REC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=47| self.assertLess(body.index("install_vm_egress()"),body.index("refresh_snikket()")) -REC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=49| def test_rollback_reverses_routing_before_dns(self): -REC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=50| src=(TASK/"tools/cutover.py").read_text() -REC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=51| body=src[src.index("def phase_rollback():"):src.index("def phase_seal():")] -REC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=52| self.assertLess(body.index("remove_vm_egress()"),body.index("remove_edgevm_policy()")) -REC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=53| self.assertLess(body.index("remove_edgevm_policy()"),body.index('cf("set",old)')) -REC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=54| self.assertIn("edge01-home-restricted-ingress.nft",body) -REC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=59| self.assertIn("turn.gram1.ru is not modified or deleted",joined) -REC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=60| self.assertIn("TURN static auth material is not read, printed, or rotated",joined) -REC|SOURCE_TEXT|LABEL=ORIGIN:test_cr_2026_0080_snikket_edge_cutover.py|LINE=62| self.assertIn("home router forwards are unchanged",joined) -REC|COMMIT|LABEL=RETIRE|PRESENT=true|COMMIT=73c4cb4cc446ec8a10f81ba635624f2bf9373467|DATE=2026-08-31T22:01:03+03:00|SUBJECT=CR0104 add edge ingress retirement|PARENTS=53934522eaae9882f6c835e36b51a3b2162ec382 -REC|COMMIT_FILES|LABEL=RETIRE|COUNT=3 -REC|COMMIT_FILE|LABEL=RETIRE|BASENAME=edge-retire.sh|PARENT=homelab-command-stack-retirement|PATH_SHA=c9764b4b5fc86c1c3f92d9c26a0912de947256337e686df9991a336bbec22074 -REC|SOURCE_TEXT|LABEL=RETIRE:edge-retire.sh|LINE=5|REWRITE="$HERE/edge-rewrite.py" -REC|SOURCE_TEXT|LABEL=RETIRE:edge-retire.sh|LINE=7|EDGE="${HOMELAB_EDGE_HOST:HOST_REDACTED -REC|SOURCE_TEXT|LABEL=RETIRE:edge-retire.sh|LINE=8|SSH=(ssh -o BatchMode=yes -o ConnectTimeout=10 "$EDGE") -REC|SOURCE_TEXT|LABEL=RETIRE:edge-retire.sh|LINE=14|BACKUP_ROOT='/var/backups/homelab-command-stack-retirement' -REC|SOURCE_TEXT|LABEL=RETIRE:edge-retire.sh|LINE=17|BACKUP="$BACKUP_ROOT/$STAMP/edge" -REC|SOURCE_TEXT|LABEL=RETIRE:edge-retire.sh|LINE=58|rollback() { -REC|SOURCE_TEXT|LABEL=RETIRE:edge-retire.sh|LINE=71| echo 'EDGE_RETIRE_ROLLBACK=OK' >&2 -REC|SOURCE_TEXT|LABEL=RETIRE:edge-retire.sh|LINE=73| echo 'EDGE_RETIRE_ROLLBACK=FAILED' >&2 -REC|SOURCE_TEXT|LABEL=RETIRE:edge-retire.sh|LINE=80|trap rollback ERR -REC|SOURCE_TEXT|LABEL=RETIRE:edge-retire.sh|LINE=111| echo 'EDGE_RETIRE_CHECK=OK' -REC|SOURCE_TEXT|LABEL=RETIRE:edge-retire.sh|LINE=121|"${SCP[@]}" "$TMP/9.conf" "$EDGE:$RPROXY" -REC|SOURCE_TEXT|LABEL=RETIRE:edge-retire.sh|LINE=122|"${SCP[@]}" "$TMP/http.conf" "$EDGE:$RHTTP" -REC|SOURCE_TEXT|LABEL=RETIRE:edge-retire.sh|LINE=146|echo "EDGE_RETIRE_STATUS=OK BACKUP=$BACKUP" -REC|COMMIT_FILE|LABEL=RETIRE|BASENAME=edge-rewrite.py|PARENT=homelab-command-stack-retirement|PATH_SHA=5d82cec197ad13cd81785224c92618441b52804db7e20d7ba2ff3c18805206d9 -REC|SOURCE_TEXT|LABEL=RETIRE:edge-rewrite.py|LINE=26| return pos + 1 -REC|SOURCE_TEXT|LABEL=RETIRE:edge-rewrite.py|LINE=32| return text.rfind("\n", 0, pos) + 1 -REC|SOURCE_TEXT|LABEL=RETIRE:edge-rewrite.py|LINE=42| return pos -REC|SOURCE_TEXT|LABEL=RETIRE:edge-rewrite.py|LINE=59| return text[:start] + text[end:] -REC|SOURCE_TEXT|LABEL=RETIRE:edge-rewrite.py|LINE=83| return result -REC|COMMIT_FILE|LABEL=RETIRE|BASENAME=test_cr_2026_0104_command_stack_retirement.py|PARENT=tests|PATH_SHA=a1fdf025e885c9a1b1fd5ce5a36c6e2930f3c9ede718660ae9705c01a6cf328b -REC|COMMIT|LABEL=ROLLBACK|PRESENT=true|COMMIT=3bc25b7abde3ef263d728549e92de5ec6dc4a027|DATE=2026-08-31T22:13:52+03:00|SUBJECT=CR0104 add explicit edge rollback|PARENTS=7d9d9a81d64a88b3d09b21b0980f92f33dd26b15 -REC|COMMIT_FILES|LABEL=ROLLBACK|COUNT=3 -REC|COMMIT_FILE|LABEL=ROLLBACK|BASENAME=edge-retire.sh|PARENT=homelab-command-stack-retirement|PATH_SHA=c9764b4b5fc86c1c3f92d9c26a0912de947256337e686df9991a336bbec22074 -REC|SOURCE_TEXT|LABEL=ROLLBACK:edge-retire.sh|LINE=5|REWRITE="$HERE/edge-rewrite.py" -REC|SOURCE_TEXT|LABEL=ROLLBACK:edge-retire.sh|LINE=7|EDGE="${HOMELAB_EDGE_HOST:HOST_REDACTED -REC|SOURCE_TEXT|LABEL=ROLLBACK:edge-retire.sh|LINE=8|SSH=(ssh -o BatchMode=yes -o ConnectTimeout=10 "$EDGE") -REC|SOURCE_TEXT|LABEL=ROLLBACK:edge-retire.sh|LINE=14|BACKUP_ROOT='/var/backups/homelab-command-stack-retirement' -REC|SOURCE_TEXT|LABEL=ROLLBACK:edge-retire.sh|LINE=17|BACKUP="$BACKUP_ROOT/$STAMP/edge" -REC|SOURCE_TEXT|LABEL=ROLLBACK:edge-retire.sh|LINE=58|rollback() { -REC|SOURCE_TEXT|LABEL=ROLLBACK:edge-retire.sh|LINE=71| echo 'EDGE_RETIRE_ROLLBACK=OK' >&2 -REC|SOURCE_TEXT|LABEL=ROLLBACK:edge-retire.sh|LINE=73| echo 'EDGE_RETIRE_ROLLBACK=FAILED' >&2 -REC|SOURCE_TEXT|LABEL=ROLLBACK:edge-retire.sh|LINE=80|trap rollback ERR -REC|SOURCE_TEXT|LABEL=ROLLBACK:edge-retire.sh|LINE=111| echo 'EDGE_RETIRE_CHECK=OK' -REC|SOURCE_TEXT|LABEL=ROLLBACK:edge-retire.sh|LINE=120| sudo -n tee '$BACKUP_ROOT/last-edge-backup' >/dev/null -REC|SOURCE_TEXT|LABEL=ROLLBACK:edge-retire.sh|LINE=123|"${SCP[@]}" "$TMP/9.conf" "$EDGE:$RPROXY" -REC|SOURCE_TEXT|LABEL=ROLLBACK:edge-retire.sh|LINE=124|"${SCP[@]}" "$TMP/http.conf" "$EDGE:$RHTTP" -REC|SOURCE_TEXT|LABEL=ROLLBACK:edge-retire.sh|LINE=148|echo "EDGE_RETIRE_STATUS=OK BACKUP=$BACKUP" -REC|COMMIT_FILE|LABEL=ROLLBACK|BASENAME=edge-rollback.sh|PARENT=homelab-command-stack-retirement|PATH_SHA=5d21efde6be6c7c450c4521a51f902c96f16e6a40ab8bcbf4eb710e947a06da7 -REC|SOURCE_TEXT|LABEL=ROLLBACK:edge-rollback.sh|LINE=4|EDGE="${HOMELAB_EDGE_HOST:HOST_REDACTED -REC|SOURCE_TEXT|LABEL=ROLLBACK:edge-rollback.sh|LINE=5|SSH=(ssh -o BatchMode=yes -o ConnectTimeout=10 "$EDGE") -REC|SOURCE_TEXT|LABEL=ROLLBACK:edge-rollback.sh|LINE=9|BACKUP_ROOT='/var/backups/homelab-command-stack-retirement' -REC|SOURCE_TEXT|LABEL=ROLLBACK:edge-rollback.sh|LINE=10|POINTER="$BACKUP_ROOT/last-edge-backup" -REC|SOURCE_TEXT|LABEL=ROLLBACK:edge-rollback.sh|LINE=19| '$BACKUP_ROOT'/*/edge) ;; -REC|SOURCE_TEXT|LABEL=ROLLBACK:edge-rollback.sh|LINE=20| *) echo 'INVALID_EDGE_BACKUP_POINTER' >&2; exit 1 ;; -REC|SOURCE_TEXT|LABEL=ROLLBACK:edge-rollback.sh|LINE=44| echo \"EDGE_ROLLBACK_STATUS=OK BACKUP=\$BACKUP\" -REC|COMMIT_FILE|LABEL=ROLLBACK|BASENAME=test_cr_2026_0104_command_stack_retirement.py|PARENT=tests|PATH_SHA=a1fdf025e885c9a1b1fd5ce5a36c6e2930f3c9ede718660ae9705c01a6cf328b -REC|COMMIT|LABEL=MERGE_RETIRE|PRESENT=false -REC|CURRENT_FILE|BASENAME=task.json|PARENT=snikket-edge-cutover-v1|PATH_SHA=9a63e2769a24f3ce2c23c172837f46c1f044cbd8f886e45313a3b0e7e467e55d|SHA=9cad3f6291663d6e85a556882c8ed751eaaa61b816b9bae934032fb0677267fd -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=3| "task_id": "snikket-edge-cutover-v1", -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=6| "production_change": true, -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=14| "rollback": "phases/rollback.sh", -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=18| "active_backup_restore_max": 0 -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=22| "EDGE-01 public service path at IP_REDACTED", -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=23| "edge-vm IP_REDACTED routing and /etc/wireguard/wg-edge01.conf metadata", -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=24| "VM150 QGA network and Snikket runtime state", -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=33| "$HOMELAB_RUN_DIR/rollback.json", -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=35| "/mnt/staging/homelab-admin-rollback/snikket-edge-cutover-v1/state.json", -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=37| "edge-vm policy rule priority 101 and wg-edge01.conf PostUp/PostDown", -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=38| "VM150 /etc/systemd/system/snikket-edge-egress.service" -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=42| "edge-vm policy routing for source IP_REDACTED", -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=43| "VM150 default Internet route via IP_REDACTED", -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=44| "Snikket server container restart after DNS convergence" -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=47| "EDGE-01 is never accessed automatically by this task", -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=48| "turn.gram1.ru is not modified or deleted", -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=49| "TURN static auth material is not read, printed, or rotated", -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=51| "home router forwards are unchanged", -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=57| "tools/cutover.py" -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=66| "verification": "pre-cutover DNS, public canary, edge-vm table 17777, absent VM150 source policy, VM150 old default route, non-mutating Cloudflare lookup" -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=71| "verification": "non-secret rollback state is persisted before mutation"=REDACTED -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=76| "verification": "exact DNS/routing/service mutations and rollback order are calculated without mutation" -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=81| "verification": "EDGE-01 public ingress attestation, DNS convergence, source policy, VM150 egress, Snikket refresh" -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=86| "verification": "DNS, HTTPS, XMPP, TURN/STUN, VM150 egress and effective coturn external address" -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=89| "path": "$HOMELAB_RUN_DIR/rollback.json", -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=90| "producer": "phases/rollback.sh", -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=91| "verification": "old DNS and pre-cutover edge-vm/VM150 routing are restored; EDGE-01 rollback remains manual" -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=96| "verification": "traffic cutover is verified without post-cutover hardening changes" -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=106| "host": "edge-vm IP_REDACTED", -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=111| "host": "VM150", -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=116| "host": "EDGE-01 IP_REDACTED", -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=118| "compile": "versioned nft asset; nft -c; host guard; external canary" -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=121| "rollback": { -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=122| "command": "HOMELAB_CONTROLLED_APPLY_CONFIRM=snikket-edge-cutover-v1 /opt/homelab-admin/bin/homelab-admin run TASK_DIR rollback", -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=123| "verification": "restore old chat A, remove VM150 source policy and persistent egress service, restore old default route; manual EDGE-01 restricted-ingress asset remains available" -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=130| "edge01_public_ip": "IP_REDACTED", -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=131| "edge01_wg_ip": "IP_REDACTED", -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=132| "edgevm_wg_ip": "IP_REDACTED", -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=133| "edgevm_lan_ip": "IP_REDACTED", -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=134| "vm150_ip": "IP_REDACTED", -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=137| "policy_table": 17777, -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=139| "turn_udp_min": 60000, -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=140| "turn_udp_max": 60199, -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=141| "edge01_automated_access": false, -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=142| "turn_legacy_record_mutation": false, -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=143| "turn_secret_rotation": false,=REDACTED -REC|SOURCE_TEXT|LABEL=HEAD:task.json|LINE=145| "home_router_mutation": false, -CURRENT_EDGE_REFERENCE_FILE_COUNT=4 -REC|CURRENT_EDGE_REF_FILE|BASENAME=2026-08-17-snikket-edge-cutover-design.md|PARENT=specs|PATH_SHA=f0862c0e9d53ebf2bf9e6213cd190fc39de58a3f11c508d952aa26ad475d1e02 -REC|CURRENT_EDGE_REF_FILE|BASENAME=2026-08-17-snikket-post-cutover-hardening-design.md|PARENT=specs|PATH_SHA=7e742cf139b78adf59e860e331f4b8f02fbb75e42a5492dd969bbe003fdc65ec -REC|CURRENT_EDGE_REF_FILE|BASENAME=task.json|PARENT=snikket-edge-cutover-v1|PATH_SHA=d0a271727e96d64818be706231c4f74d0dbe77bcadfaada5325e43b874fab0ba -REC|CURRENT_EDGE_REF_FILE|BASENAME=cutover.py|PARENT=tools|PATH_SHA=6dbc303494115f30477da0b911dd387ed0e5959e1a3e322680980fb7cbb8b83a -CURRENT_WGEDGE_EXACT_REFERENCE_COUNT=16 -DECISION=PASS_SNIKKET_WGEDGE_DESIGN_STATE_RCA -NEXT_GATE=APPLY_CURRENT_SOURCE_DESIGN -TASK_RESULT=SNIKKET_WGEDGE_DESIGN_STATE_RCA +SOURCE_ROLLBACK_NETWORK_PRESENT=true +SOURCE_ROLLBACK_USES_CLOUDFLARE=true +ROLLBACK_STATE_PRESENT=true +ROLLBACK_STATE_SHA=b4696a99b8e13042680d3c56537e0e5e5b6d3e7fae69e01fb8b8f60d68c39fc7 +ROLLBACK_STATE_MODE=600 +ROLLBACK_STATE_MTIME=1786953752 +ROLLBACK_STATE_JSON_VALID=true +ROLLBACK_STATE_HAS_DNS_CONTENT=true +ROLLBACK_STATE_HAS_DEFAULT_ROUTE=true +ROLLBACK_OLD_ROUTE_SHA=7c5515603e424ad8e85718ba716657be12298988ee11416f41081ec82e023409 +ROLLBACK_HOME_IP_SHA=26ac437d63a8bcf254255cc970d7614e78bd0a67b97b5575a42a1c3ca7a301ee +PVE01_IPIFY_RC=0 +PVE01_PUBLIC_IP_SHA=26ac437d63a8bcf254255cc970d7614e78bd0a67b97b5575a42a1c3ca7a301ee +ROLLBACK_HOME_MATCHES_CURRENT_PVE01_PUBLIC=true +VM150_QGA_TRANSPORT_RC=0 +REC|VM150|QGA_EXIT=0 +REC|VM150|DOMAIN_SHA=04cb4a04e408d91abb9c63701ae969abf16d8a12b6f4f144c5edf2c28e03256b +REC|VM150|EDGE_EGRESS_UNIT_PRESENT=true +REC|VM150|EDGE_EGRESS_ACTIVE=active +REC|VM150|EDGE_EGRESS_ENABLED=enabled +REC|VM150|EDGE_EGRESS_UNIT_SHA=83a20356e221dfb22ea2a0d99e327f7c2f791a3d3127a46af2d1625b33c95b5d +REC|VM150|OLDGW_SHA=4f9da1283166a1e9f4e6bc7dd240c76aef85772177f717f92abb6e8e2bb8de90 +REC|VM150|NEWGW_SHA=bc517b4af3298846906234d3d697d3820b0b652ade1880c937c9d71cd802c834 +REC|VM150|OLDGW_PING_RC=1 +REC|VM150|CURRENT_GW_SHA=bc517b4af3298846906234d3d697d3820b0b652ade1880c937c9d71cd802c834 +REC|VM150|REC|CONTAINER|NAME=snikket|RUNNING=true|RESTARTS=0 +REC|VM150|REC|CONTAINER|NAME=snikket-proxy|RUNNING=true|RESTARTS=0 +REC|VM150|REC|CONTAINER|NAME=snikket-certs|RUNNING=true|RESTARTS=0 +REC|VM150|REC|CONTAINER|NAME=snikket-portal|RUNNING=true|RESTARTS=0 +REC|VM150|LOCAL_HTTPS_RC=0 +REC|VM150|LOCAL_HTTPS_CODE=302 +VM130_QGA_TRANSPORT_RC=0 +REC|VM130|QGA_EXIT=0 +REC|VM130|ROLE=VM130 +REC|VM130|WG_CONFIG_PRESENT=true +REC|VM130|WG_BACKUP_PRESENT=true +REC|VM130|WG_CONFIG_SHA=42b046d580b2ccd6af8f3573fba4882c71c92b2938f3fed8edf58258bbc89615 +REC|VM130|WG_BACKUP_SHA=9012ece867ad7efa1b2d07020c1cdfc9d3a63f86fde9117bdd7435eef29a9735 +REC|VM130|WG_BACKUP_MODE=600 +REC|VM130|PRIORITY101_COUNT=1 +REC|VM130|TABLE17777_DEFAULT_COUNT=1 +REC|VM130|WG_EDGE_ACTIVE=active +CURRENT_AUTHORITATIVE_BEGET=true +REC|DNS|ROLE=primary|A_COUNT=1|MATCH_ROLLBACK_HOME=true +REC|DNS|ROLE=groups|A_COUNT=1|MATCH_ROLLBACK_HOME=true +REC|DNS|ROLE=share|A_COUNT=1|MATCH_ROLLBACK_HOME=true +BEGET_LOGIN_CANDIDATE_COUNT=0 +BEGET_PASSWORD_CANDIDATE_COUNT=0 +BEGET_CREDENTIALS_UNIQUE=false +REC|HOME_TCP|PORT=443|OPEN=true|MSEC=1 +REC|HOME_TCP|PORT=5000|OPEN=true|MSEC=0 +REC|HOME_TCP|PORT=5222|OPEN=true|MSEC=0 +REC|HOME_TCP|PORT=5349|OPEN=false|ERR=TimeoutError +ROLLBACK_PREFLIGHT_READY=false +DECISION=PASS_SNIKKET_HOME_ROLLBACK_PREFLIGHT +NEXT_GATE=ROLLBACK_PREFLIGHT_BLOCKER_REPAIR +TASK_RESULT=SNIKKET_HOME_ROLLBACK_PREFLIGHT CHANGES_MADE=false ROLLBACK_STARTED=false ROLLBACK_RESTORED=null MUTATION_OUTCOME=NO_MUTATION -HOMELAB_RESULT_CONTRACT={"version":1,"command_id":"SUPPORT-261006-SNIKKET-WGEDGE-DESIGN-STATE-RCA-99739R1","status":"OK","changes_made":false,"rollback_started":false,"rollback_restored":null} +HOMELAB_RESULT_CONTRACT={"version":1,"command_id":"SUPPORT-261006-SNIKKET-HOME-ROLLBACK-PREFLIGHT-99740R1","status":"OK","changes_made":false,"rollback_started":false,"rollback_restored":null} OUTPUT_END CHAT_OUTPUT_END