39 lines
52 KiB
JSON
39 lines
52 KiB
JSON
{
|
|
"schema_version": 1,
|
|
"channel": "homelab-runtime",
|
|
"command_id": "SUPPORT-261006-SNIKKET-WGEDGE-DESIGN-STATE-RCA-99739R1",
|
|
"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",
|
|
"reference_register_checked": true,
|
|
"reference_sha256": "5a3d8e5154c41cb582a4e0aca68090be1f0138918bf82131a948df326f9d8d66",
|
|
"error_register_checked": true,
|
|
"error_register_sha256": "3b09a553ec0f527ed3afeed4753f52a74ee3036045bbfb3c685e9f8af4ba7ba0",
|
|
"command_sha256": "a60a8c29d502a49e98c500b961f57451d0ca3fd661de694738452d4ba50dff96",
|
|
"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": "64709396903adcb76579d798fddab1c280c55806f26dc184c45423275962c07d",
|
|
"sanitized_output_sha256": "64709396903adcb76579d798fddab1c280c55806f26dc184c45423275962c07d",
|
|
"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"
|
|
}
|