1
0

runtime: publish SUPPORT-261006-SNIKKET-WGEDGE-DESIGN-STATE-RCA-99739R1

This commit is contained in:
homelab-runtime-publisher
2026-10-06 17:55:51 +00:00
parent d8bd6eef93
commit 1dc3cbb4eb
4 changed files with 885 additions and 95 deletions
File diff suppressed because one or more lines are too long
@@ -0,0 +1,438 @@
CHAT_OUTPUT_BEGIN
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
REFERENCE_REGISTER_CHECK=OK
REFERENCE_SHA256=5a3d8e5154c41cb582a4e0aca68090be1f0138918bf82131a948df326f9d8d66
ERROR_REGISTER_CHECK=OK
ERROR_REGISTER_SHA256=3b09a553ec0f527ed3afeed4753f52a74ee3036045bbfb3c685e9f8af4ba7ba0
COMMAND_SHA256=a60a8c29d502a49e98c500b961f57451d0ca3fd661de694738452d4ba50dff96
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=64709396903adcb76579d798fddab1c280c55806f26dc184c45423275962c07d
SANITIZED_OUTPUT_SHA256=64709396903adcb76579d798fddab1c280c55806f26dc184c45423275962c07d
OUTPUT_BEGIN
SNIKKET_WGEDGE_DESIGN_STATE_RCA=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
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}
OUTPUT_END
CHAT_OUTPUT_END
+8 -8
View File
File diff suppressed because one or more lines are too long
+401 -87
View File
@@ -1,15 +1,15 @@
CHAT_OUTPUT_BEGIN
COMMAND_ID=SUPPORT-261006-SNIKKET-WGEDGE-SOURCE-HISTORY-RCA-99738R1
COMMAND_ID=SUPPORT-261006-SNIKKET-WGEDGE-DESIGN-STATE-RCA-99739R1
STATUS=OK
RC=0
HOST=pve01
MODE=read-only
COMPONENT=snikket-wgedge-source-history-rca
COMPONENT=snikket-wgedge-design-state-rca
REFERENCE_REGISTER_CHECK=OK
REFERENCE_SHA256=5a3d8e5154c41cb582a4e0aca68090be1f0138918bf82131a948df326f9d8d66
ERROR_REGISTER_CHECK=OK
ERROR_REGISTER_SHA256=3b09a553ec0f527ed3afeed4753f52a74ee3036045bbfb3c685e9f8af4ba7ba0
COMMAND_SHA256=b72f8aea0efdedfd1585f74fe26fff8d183e9a4538dce609a82f6bae4fef6667
COMMAND_SHA256=a60a8c29d502a49e98c500b961f57451d0ca3fd661de694738452d4ba50dff96
DUPLICATE_FAILED_COMMAND_BLOCKED=false
EXECUTION_STARTED=true
CHANGE_DECLARED=false
@@ -24,101 +24,415 @@ MUTATION_OUTCOME=NO_MUTATION
SANITIZED=yes
SECRETS_INCLUDED=no
PRIVATE_ADDRESSES_INCLUDED=no
RAW_EVIDENCE_SHA256=7c2f6f882be15d7f108ee4daada8307d99e6ee9f71276b816e814a4ad06919c5
SANITIZED_OUTPUT_SHA256=7c2f6f882be15d7f108ee4daada8307d99e6ee9f71276b816e814a4ad06919c5
RAW_EVIDENCE_SHA256=64709396903adcb76579d798fddab1c280c55806f26dc184c45423275962c07d
SANITIZED_OUTPUT_SHA256=64709396903adcb76579d798fddab1c280c55806f26dc184c45423275962c07d
OUTPUT_BEGIN
SNIKKET_WGEDGE_SOURCE_HISTORY_RCA=V1
SNIKKET_WGEDGE_DESIGN_STATE_RCA=V1
MODE=read-only
NO_MUTATION=true
SOURCE_REPO_MATCH_COUNT=1
SOURCE_REPO_BASENAME=homelab-ops
SOURCE_REPO_PATH_SHA=8598ef1c63466691ec91c66fa47f29edcd2530f1de043b13ed32760a1dbac3ba
SOURCE_HEAD=bd85699d8b588a34c480f10dee71323687bd48f1
SOURCE_BRANCH=main
SOURCE_WORKTREE_DIRTY_COUNT=0
TARGET_COMMIT=83b3d27c1de533cabb2d23e337579b06ddd0c355
TARGET_PARENT=d399d24440f948b518761ecfd516521993861b43
TARGET_DATE=2026-09-28T08:44:44+03:00
TARGET_SUBJECT=skladchik: align NetBird route health with overlay peer
TARGET_CHANGED_FILE_COUNT=3
REC|TARGET_FILE|BASENAME=skladchik-reports-monitor-health-alert.py|PARENT=runtime|PATH_SHA=2cf94533e16decd5f8d6b2996104d83857a74de1d151c1a79750c4532597edb8
REC|TARGET_FILE|BASENAME=skladchik-reports-monitor-status-edge|PARENT=runtime|PATH_SHA=afa86a4ed42cb9238cc2e5e9b2594a9a1fd6c779d76de2fea8ab15dcf17e3480
REC|TARGET_FILE|BASENAME=skladchik-reports-monitor-supervisor|PARENT=runtime|PATH_SHA=3a84408d15ba97496865eab207054845e1d90af4decf786591ec5f01b3d4c27e
HISTORY_WG_EDGE01_COUNT=1
REC|HISTORY|NEEDLE=WG_EDGE01|COMMIT=911d8cc52d5e8a677f132b736d01ade7a5879e59|DATE=2026-08-17T10:27:24+03:00|SUBJECT=CR-2026-0080: build Snikket EDGE cutover task
HISTORY_SNIKKET_EDGE_INGRESS_NFT_COUNT=0
HISTORY_17777_COUNT=3
REC|HISTORY|NEEDLE=17777|COMMIT=85a325224532505bce79ef3bc2a224a162a32dd1|DATE=2026-08-17T10:08:28+03:00|SUBJECT=CR-2026-0080: approve Snikket EDGE cutover design
REC|HISTORY|NEEDLE=17777|COMMIT=bcee02410290c4563fea4507a35fdf6dce08d674|DATE=2026-08-17T10:13:58+03:00|SUBJECT=CR-2026-0080: plan Snikket EDGE cutover implementation
REC|HISTORY|NEEDLE=17777|COMMIT=911d8cc52d5e8a677f132b736d01ade7a5879e59|DATE=2026-08-17T10:27:24+03:00|SUBJECT=CR-2026-0080: build Snikket EDGE cutover task
HISTORY_51820_COUNT=6
REC|HISTORY|NEEDLE=51820|COMMIT=d29752e1de327ad588bf3f1689f277f55e33e58c|DATE=2026-07-17T08:05:30+03:00|SUBJECT=CR-2026-0001 recover handoff and operational context
REC|HISTORY|NEEDLE=51820|COMMIT=b0f9935dce45a7dee2949aef342cadec90ff38a2|DATE=2026-07-17T13:48:47+03:00|SUBJECT=CR-2026-0003 remediate cluster storage retention
REC|HISTORY|NEEDLE=51820|COMMIT=f9d3ba8caf65005605aa8aee2ede7e4e7e94ee49|DATE=2026-07-31T10:37:56+03:00|SUBJECT=GRAM1: capture initial site baseline 20260731T071400Z
REC|HISTORY|NEEDLE=51820|COMMIT=542f34b5903a437f43c869c81d3736dbfe081b81|DATE=2026-08-06T05:41:37+03:00|SUBJECT=homelab: canonical cluster handbook and Error System V2
REC|HISTORY|NEEDLE=51820|COMMIT=b2853f444fe8725a590f96bf40c45bd694eddfb4|DATE=2026-08-06T05:41:39+03:00|SUBJECT=rollback: restore pre-transaction path states
REC|HISTORY|NEEDLE=51820|COMMIT=772e862ee9b775a2d3eaea10b214688ab75b7434|DATE=2026-08-19T07:04:49+03:00|SUBJECT=docs(CR-2026-0084): add cluster operational knowledge base
CURRENT_SOURCE_REFERENCE_COUNT=20
REC|SOURCE_REF|BASENAME=2026-08-17-snikket-edge-cutover.md|PARENT=plans|LINE=20|PATH_SHA=72bcfe0160d221fc3f06466006c3163dc2e5f67f2224cacd1c5c50e02135eb37
REC|SOURCE_REF|BASENAME=2026-08-17-snikket-edge-cutover.md|PARENT=plans|LINE=23|PATH_SHA=72bcfe0160d221fc3f06466006c3163dc2e5f67f2224cacd1c5c50e02135eb37
REC|SOURCE_REF|BASENAME=2026-08-17-snikket-edge-cutover-design.md|PARENT=specs|LINE=8|PATH_SHA=224ed8ac7b09df73545e49a1ac681848f67b00df285ec5a3b7b8398f21d015ee
REC|SOURCE_REF|BASENAME=2026-08-17-snikket-edge-cutover-design.md|PARENT=specs|LINE=20|PATH_SHA=224ed8ac7b09df73545e49a1ac681848f67b00df285ec5a3b7b8398f21d015ee
REC|SOURCE_REF|BASENAME=task.json|PARENT=snikket-edge-cutover-v1|LINE=23|PATH_SHA=9a63e2769a24f3ce2c23c172837f46c1f044cbd8f886e45313a3b0e7e467e55d
REC|SOURCE_REF|BASENAME=task.json|PARENT=snikket-edge-cutover-v1|LINE=37|PATH_SHA=9a63e2769a24f3ce2c23c172837f46c1f044cbd8f886e45313a3b0e7e467e55d
REC|SOURCE_REF|BASENAME=task.json|PARENT=snikket-edge-cutover-v1|LINE=66|PATH_SHA=9a63e2769a24f3ce2c23c172837f46c1f044cbd8f886e45313a3b0e7e467e55d
REC|SOURCE_REF|BASENAME=task.json|PARENT=snikket-edge-cutover-v1|LINE=137|PATH_SHA=9a63e2769a24f3ce2c23c172837f46c1f044cbd8f886e45313a3b0e7e467e55d
REC|SOURCE_REF|BASENAME=cutover.py|PARENT=tools|LINE=13|PATH_SHA=d90388278e2775b1ac7534b35946dfa3dadd03f65cd16117db653731b47346ad
REC|SOURCE_REF|BASENAME=cutover.py|PARENT=tools|LINE=175|PATH_SHA=d90388278e2775b1ac7534b35946dfa3dadd03f65cd16117db653731b47346ad
REC|SOURCE_REF|BASENAME=cutover.py|PARENT=tools|LINE=181|PATH_SHA=d90388278e2775b1ac7534b35946dfa3dadd03f65cd16117db653731b47346ad
REC|SOURCE_REF|BASENAME=cutover.py|PARENT=tools|LINE=205|PATH_SHA=d90388278e2775b1ac7534b35946dfa3dadd03f65cd16117db653731b47346ad
REC|SOURCE_REF|BASENAME=cutover.py|PARENT=tools|LINE=237|PATH_SHA=d90388278e2775b1ac7534b35946dfa3dadd03f65cd16117db653731b47346ad
REC|SOURCE_REF|BASENAME=cutover.py|PARENT=tools|LINE=238|PATH_SHA=d90388278e2775b1ac7534b35946dfa3dadd03f65cd16117db653731b47346ad
REC|SOURCE_REF|BASENAME=cutover.py|PARENT=tools|LINE=244|PATH_SHA=d90388278e2775b1ac7534b35946dfa3dadd03f65cd16117db653731b47346ad
REC|SOURCE_REF|BASENAME=cutover.py|PARENT=tools|LINE=245|PATH_SHA=d90388278e2775b1ac7534b35946dfa3dadd03f65cd16117db653731b47346ad
REC|SOURCE_REF|BASENAME=cutover.py|PARENT=tools|LINE=247|PATH_SHA=d90388278e2775b1ac7534b35946dfa3dadd03f65cd16117db653731b47346ad
REC|SOURCE_REF|BASENAME=cutover.py|PARENT=tools|LINE=252|PATH_SHA=d90388278e2775b1ac7534b35946dfa3dadd03f65cd16117db653731b47346ad
REC|SOURCE_REF|BASENAME=cutover.py|PARENT=tools|LINE=253|PATH_SHA=d90388278e2775b1ac7534b35946dfa3dadd03f65cd16117db653731b47346ad
REC|SOURCE_REF|BASENAME=cutover.py|PARENT=tools|LINE=258|PATH_SHA=d90388278e2775b1ac7534b35946dfa3dadd03f65cd16117db653731b47346ad
SOURCE_SUBJECT_HIT_COUNT=31
REC|SOURCE_LOG|COMMIT=0a045c3f90a98b78547f635a5d80c63901e118e7|DATE=2026-08-31T19:15:39Z|SUBJECT=Merge pull request 'CR0104 add explicit edge rollback' (#41) from cr-2026-0104-command-stack-retirement into main
REC|SOURCE_LOG|COMMIT=3bc25b7abde3ef263d728549e92de5ec6dc4a027|DATE=2026-08-31T22:13:52+03:00|SUBJECT=CR0104 add explicit edge rollback
REC|SOURCE_LOG|COMMIT=73c4cb4cc446ec8a10f81ba635624f2bf9373467|DATE=2026-08-31T22:01:03+03:00|SUBJECT=CR0104 add edge ingress retirement
REC|SOURCE_LOG|COMMIT=005074d9abde9d599b7394b92fc33f56596e7589|DATE=2026-08-23T06:34:35+03:00|SUBJECT=fix(CR-2026-0096): include email ledger in backup contract
REC|SOURCE_LOG|COMMIT=02160e152d6ed349c5eb7614283b2a6a2cd414d1|DATE=2026-08-23T06:17:38+03:00|SUBJECT=fix(CR-2026-0096): delegate email ledger path from integration hook
REC|SOURCE_LOG|COMMIT=f178e01b7f8ec831fa212be027c95adfbe941910|DATE=2026-08-23T06:01:45+03:00|SUBJECT=fix(CR-2026-0096): place email ledger beside shadow ledger
REC|SOURCE_LOG|COMMIT=634bb165226627a5694d37c7d858a9fda00a04c9|DATE=2026-08-22T09:48:54+03:00|SUBJECT=feat: add calibrated models decision and execution ledger
REC|SOURCE_LOG|COMMIT=772e862ee9b775a2d3eaea10b214688ab75b7434|DATE=2026-08-19T07:04:49+03:00|SUBJECT=docs(CR-2026-0084): add cluster operational knowledge base
REC|SOURCE_LOG|COMMIT=d8ae1b62d5d09a2a0249541967daf1ffd15eb932|DATE=2026-08-18T11:24:38Z|SUBJECT=Merge pull request 'Cr 2026 0083 gitea edge publisher v3' (#17) from cr-2026-0083-gitea-edge-publisher-v3 into main
REC|SOURCE_LOG|COMMIT=064c6f23ab8a7038b0b0f64545ddf1965c5f860e|DATE=2026-08-18T08:42:28+03:00|SUBJECT=fix(CR-2026-0083): harden EDGE Publisher V3 deployment
REC|SOURCE_LOG|COMMIT=f0d2d006953079a07c9510e2489d01c4969826b4|DATE=2026-08-18T00:30:16+03:00|SUBJECT=CR-2026-0083: add Gitea EDGE Publisher V3 source
REC|SOURCE_LOG|COMMIT=8a2e58e3ace69b76d4508434290cd9600cba3270|DATE=2026-08-17T15:11:21Z|SUBJECT=Merge pull request 'CR-2026-0081: fix VM150 EDGE egress probe' (#16) from cr-2026-0081-snikket-hardening into main
REC|SOURCE_LOG|COMMIT=f418f45ae134724c4f23a2712718fb842db9a6ae|DATE=2026-08-17T18:10:53+03:00|SUBJECT=CR-2026-0081: fix VM150 EDGE egress probe parsing
REC|SOURCE_LOG|COMMIT=17e094035bd395a8a632bafef2570be5004f91cc|DATE=2026-08-17T15:04:12Z|SUBJECT=Merge pull request 'CR-2026-0081: fix Netcraze Snikket forward parsing' (#15) from cr-2026-0081-snikket-hardening into main
REC|SOURCE_LOG|COMMIT=58313735ab3507975331922e9e016d0b20e473d0|DATE=2026-08-17T14:51:25Z|SUBJECT=Merge pull request 'CR-2026-0081: retire obsolete Snikket home forwards' (#14) from cr-2026-0081-snikket-hardening into main
REC|SOURCE_LOG|COMMIT=0bdb4794d3910ec579bc539dcb75f49dd36f86cc|DATE=2026-08-17T13:49:14+03:00|SUBJECT=CR-2026-0081: retire obsolete Snikket home forwards
REC|SOURCE_LOG|COMMIT=6c2f65c3d98a20f6b5581cb019bbc6b371d343fb|DATE=2026-08-17T10:10:26Z|SUBJECT=Merge pull request 'CR-2026-0081: fix legacy TURN no-consumer scan' (#13) from cr-2026-0081-snikket-hardening into main
REC|SOURCE_LOG|COMMIT=6c3c5db756b1c5fa77dee0d14fcb23432662240a|DATE=2026-08-17T13:09:58+03:00|SUBJECT=CR-2026-0081: harden optional Snikket scan roots
REC|SOURCE_LOG|COMMIT=093efa5ee15da274e54bdd10b86fb97768414ef5|DATE=2026-08-17T08:42:24Z|SUBJECT=Merge pull request 'CR-2026-0081: retire legacy turn.gram1.ru DNS' (#12) from cr-2026-0081-snikket-hardening into main
REC|SOURCE_LOG|COMMIT=2af4c5cb638754862c376bc710f1e25389d96072|DATE=2026-08-17T11:10:01+03:00|SUBJECT=CR-2026-0081: approve Snikket post-cutover hardening design
REC|SOURCE_LOG|COMMIT=81dceedd5bbbc900c25d1638b476437ae69a0783|DATE=2026-08-17T08:02:10Z|SUBJECT=Merge pull request 'CR-2026-0080: harden post-restart readiness' (#11) from cr-2026-0080-snikket-edge-cutover into main
REC|SOURCE_LOG|COMMIT=c920450e5a2a1ba987cba8c6ede9c4f08b217783|DATE=2026-08-17T07:55:54Z|SUBJECT=Merge pull request 'CR-2026-0080: fix coturn container shell quoting' (#10) from cr-2026-0080-snikket-edge-cutover into main
REC|SOURCE_LOG|COMMIT=b261135e87a9acb1e1e289a41d16baef8885cb3c|DATE=2026-08-17T07:50:18Z|SUBJECT=Merge pull request 'CR-2026-0080: harden failed Snikket EDGE apply' (#9) from cr-2026-0080-snikket-edge-cutover into main
REC|SOURCE_LOG|COMMIT=cdf7e519df309d0a1dd6c2c4eeb4b051488fd7bf|DATE=2026-08-17T07:29:47Z|SUBJECT=Merge pull request 'CR-2026-0080: Snikket EDGE traffic cutover' (#8) from cr-2026-0080-snikket-edge-cutover into main
REC|SOURCE_LOG|COMMIT=911d8cc52d5e8a677f132b736d01ade7a5879e59|DATE=2026-08-17T10:27:24+03:00|SUBJECT=CR-2026-0080: build Snikket EDGE cutover task
REC|SOURCE_LOG|COMMIT=bcee02410290c4563fea4507a35fdf6dce08d674|DATE=2026-08-17T10:13:58+03:00|SUBJECT=CR-2026-0080: plan Snikket EDGE cutover implementation
REC|SOURCE_LOG|COMMIT=85a325224532505bce79ef3bc2a224a162a32dd1|DATE=2026-08-17T10:08:28+03:00|SUBJECT=CR-2026-0080: approve Snikket EDGE cutover design
REC|SOURCE_LOG|COMMIT=81174ec87f42a04f893df6ce2a3a4bcde8e1e2ee|DATE=2026-08-16T04:45:00Z|SUBJECT=Merge pull request 'CR-2026-0078: harden Snikket TURN durability' (#6) from cr-2026-0078-snikket-turn-durability into main
REC|SOURCE_LOG|COMMIT=eb010cef52173ddba0f24f5486acab471efa05d6|DATE=2026-08-16T07:08:28+03:00|SUBJECT=CR-2026-0078: harden Snikket TURN durability
REC|SOURCE_LOG|COMMIT=c1a71bc168f06fbd78e8956bc3836e75a6241244|DATE=2026-08-15T18:04:07+03:00|SUBJECT=CR-2026-0076: install Docker CLI for Snikket guest
REC|SOURCE_LOG|COMMIT=08e77a62ccbf5d7684c0946f922fb7e28d1f4073|DATE=2026-08-15T02:26:48+03:00|SUBJECT=CR-2026-0076: harden Snikket VM150 rebuild source
DECISION=PASS_SNIKKET_WGEDGE_SOURCE_HISTORY_RCA
NEXT_GATE=RESTORE_FROM_SOURCE_HISTORY
TASK_RESULT=SNIKKET_WGEDGE_SOURCE_HISTORY_RCA
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
CHANGES_MADE=false
ROLLBACK_STARTED=false
ROLLBACK_RESTORED=null
MUTATION_OUTCOME=NO_MUTATION
HOMELAB_RESULT_CONTRACT={"version":1,"command_id":"SUPPORT-261006-SNIKKET-WGEDGE-SOURCE-HISTORY-RCA-99738R1","status":"OK","changes_made":false,"rollback_started":false,"rollback_restored":null}
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}
OUTPUT_END
CHAT_OUTPUT_END