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
