From 5645289c7419f0e5d6514ef4b63248043597fb0b Mon Sep 17 00:00:00 2001 From: boburning Date: Sun, 30 Aug 2026 18:50:10 -0400 Subject: [PATCH] feat(network): consume sync connectivity events --- build/dependencies.lock | 8 +- build/legacy-manifest.json | 21 ++++- docs/ARCHITECTURE.md | 11 ++- static/build/.tmp_update/bin/bloom-dev-deploy | 2 + .../build/.tmp_update/bin/bloom-network-sync | 66 +++++++++++++++ static/build/.tmp_update/bin/bloom-wifi | 2 + test/shell/dev_deploy.bats | 1 + test/shell/legacy_manifest.bats | 4 +- test/shell/network_sync.bats | 83 +++++++++++++++++++ test/shell/transfer_policy.bats | 3 +- test/shell/wifi_backend.bats | 27 ++++++ tools/dev-deploy-manifest.py | 1 + tools/legacy_manifest.py | 1 + 13 files changed, 217 insertions(+), 13 deletions(-) create mode 100755 static/build/.tmp_update/bin/bloom-network-sync create mode 100644 test/shell/network_sync.bats diff --git a/build/dependencies.lock b/build/dependencies.lock index 85cd78a58..73695c296 100644 --- a/build/dependencies.lock +++ b/build/dependencies.lock @@ -207,10 +207,10 @@ legacy_channels = ["development"] [legacy_inventory] manifest = "build/legacy-manifest.json" schema = 1 -component_count = 334 -file_count = 2133 -byte_count = 314701761 -source_build_components = 230 +component_count = 335 +file_count = 2134 +byte_count = 314704844 +source_build_components = 231 unresolved_components = 104 inherited_baseline = "07505ea58c7bba698d6b9220ff43946a43cac76b" diff --git a/build/legacy-manifest.json b/build/legacy-manifest.json index 093da25f3..6c5114b6c 100644 --- a/build/legacy-manifest.json +++ b/build/legacy-manifest.json @@ -488,8 +488,8 @@ "kind": "runtime", "path": "static/build/.tmp_update/bin/bloom-dev-deploy", "file_count": 1, - "byte_count": 32818, - "tree_sha256": "d07c796d29881fc16ce3674d028d3232fe1079288d71c0311acbcdc45c9511ea", + "byte_count": 32904, + "tree_sha256": "ac0661051a48261c2a585f1dc21ce172c61ed19edb1b2a182660f0202950bc9d", "source": "https://github.com/boburning/BloomOS", "source_revision": "release-commit", "license": "GPL-3.0-only", @@ -639,6 +639,19 @@ "build_recipe": "Makefile", "resolution": "source-build" }, + { + "id": "runtime-tmp-update-bin-bloom-network-sync", + "kind": "runtime", + "path": "static/build/.tmp_update/bin/bloom-network-sync", + "file_count": 1, + "byte_count": 2845, + "tree_sha256": "fc3e0cc0ec29b5082344fdb988e4c2e2ceeaccafdbde4015ad0e2f79cde4bb69", + "source": "https://github.com/boburning/BloomOS", + "source_revision": "release-commit", + "license": "GPL-3.0-only", + "build_recipe": "Makefile", + "resolution": "source-build" + }, { "id": "runtime-tmp-update-bin-bloom-network-transaction", "kind": "runtime", @@ -1008,8 +1021,8 @@ "kind": "runtime", "path": "static/build/.tmp_update/bin/bloom-wifi", "file_count": 1, - "byte_count": 10455, - "tree_sha256": "06534f60dada7c8ccbbdd86e54b99c69a57408c85e01a114f928a779aa3d3d38", + "byte_count": 10607, + "tree_sha256": "4a7baa305d3812dc727118c2ce996a58b569a8b6c61ccd4d2476097c9cb5f836", "source": "https://github.com/boburning/BloomOS", "source_revision": "release-commit", "license": "GPL-3.0-only", diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e61e6068d..ad15b87ec 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -988,8 +988,15 @@ the explicit `provider_unconfigured` disposition and offline events receive the explicit `offline` disposition. The compatibility network script no longer launches the inherited TLS-bypassing check or publishes its legacy availability marker. Acknowledgement failure leaves the event pending, and consumer failure -remains secondary to Wi-Fi health. Sync consumption remains a separate follow-up -integration step. +remains secondary to Wi-Fi health. + +`bloom-network-sync` completes the fixed event fan-out without claiming the +later Save Sync program. It validates and acknowledges only its exact sync +generation, returns `engine_unavailable` while SYNC-01/02 have supplied neither +the save-bundle contract nor `bloom-sync`, and returns `offline` for an offline +observation. It never reads or writes Saves, starts a transport, or treats the +filesystem `sync` command as Save Sync. This explicit disposition lets NET-07 +complete while preserving SYNC-01 through SYNC-04 as separately gated work. Bounded `request enable` and `request disable` operations persist only `device.wifi_enabled` through the active `bloom-settings` authority before diff --git a/static/build/.tmp_update/bin/bloom-dev-deploy b/static/build/.tmp_update/bin/bloom-dev-deploy index 996eb1276..f81960d46 100755 --- a/static/build/.tmp_update/bin/bloom-dev-deploy +++ b/static/build/.tmp_update/bin/bloom-dev-deploy @@ -84,6 +84,7 @@ allowed_path() { network-helpers:bin/bloom-settings|network-helpers:bin/bloom-network|\ network-helpers:bin/bloom-network-metadata|\ network-helpers:bin/bloom-network-update|\ + network-helpers:bin/bloom-network-sync|\ network-helpers:bin/bloom-network-services|network-helpers:bin/bloom-network-transaction|\ network-helpers:bin/bloom-platform|network-helpers:bin/bloom-dev-ssh|\ network-helpers:bin/bloom-sftp-gate|network-helpers:bin/bloom-sftp-server|\ @@ -195,6 +196,7 @@ validate_manifest() { bin/bloom-network \ bin/bloom-network-metadata \ bin/bloom-network-update \ + bin/bloom-network-sync \ bin/bloom-network-services \ bin/bloom-network-transaction \ bin/bloom-platform \ diff --git a/static/build/.tmp_update/bin/bloom-network-sync b/static/build/.tmp_update/bin/bloom-network-sync new file mode 100755 index 000000000..545e54307 --- /dev/null +++ b/static/build/.tmp_update/bin/bloom-network-sync @@ -0,0 +1,66 @@ +#!/bin/sh + +set -u + +SYSTEM_ROOT=${BLOOM_NETWORK_SYNC_SYSTEM_ROOT:-/mnt/SDCARD/.tmp_update} +NETWORK=${BLOOM_NETWORK_SYNC_NETWORK_BIN:-$SYSTEM_ROOT/bin/bloom-network} +JQ=${BLOOM_NETWORK_SYNC_JQ_BIN:-$SYSTEM_ROOT/bin/jq} + +result() { + printf '{"schema":1,"service":"bloom-network-sync","applied":%s,"state":"%s"}\n' "$1" "$2" +} + +reconcile() { + [ -x "$NETWORK" ] && [ -x "$JQ" ] || { result false dependency_unavailable; return 1; } + event=$($NETWORK events status sync 2>/dev/null) || { result false event_unavailable; return 1; } + fields=$(printf '%s\n' "$event" | "$JQ" -er ' + if .schema == 1 and .service == "bloom-network-events" and + .operation == "status" and .client == "sync" and + (.connectivity == "unknown" or .connectivity == "associated" or + .connectivity == "offline") and + (.generation | type) == "number" and .generation >= 0 and + .generation <= 2147483647 and (.generation | floor) == .generation and + (.pending | type) == "boolean" + then "\(.connectivity) \(.generation) \(.pending)" + else error("invalid sync network event") end + ' 2>/dev/null) || { result false event_invalid; return 1; } + set -- $fields + [ "$#" -eq 3 ] || { result false event_invalid; return 1; } + connectivity=$1 + generation=$2 + pending=$3 + if [ "$pending" = false ]; then result false no_pending_event; return 0; fi + case "$connectivity:$generation" in + associated:[1-9]*|offline:[1-9]*) ;; + *) result false event_invalid; return 1 ;; + esac + + # SYNC-01/02 have not yet supplied a save-bundle contract or bloom-sync + # engine. This consumer records that explicit boundary; it never reads or + # writes Saves, starts a transport, or interprets filesystem sync as Save Sync. + case "$connectivity" in + associated) disposition=engine_unavailable ;; + offline) disposition=offline ;; + esac + + acknowledgement=$($NETWORK events ack sync "$generation" 2>/dev/null) || { + result false acknowledgement_failed + return 1 + } + printf '%s\n' "$acknowledgement" | "$JQ" -e \ + --argjson generation "$generation" --arg connectivity "$connectivity" ' + .schema == 1 and .service == "bloom-network-events" and + .operation == "ack" and .client == "sync" and + .connectivity == $connectivity and .generation == $generation and + .pending == false + ' >/dev/null 2>&1 || { result false acknowledgement_invalid; return 1; } + printf '{"schema":1,"service":"bloom-network-sync","applied":true,' + printf '"state":"acknowledged","connectivity":"%s","generation":%s,' \ + "$connectivity" "$generation" + printf '"disposition":"%s"}\n' "$disposition" +} + +case "$#:${1:-}" in + 1:reconcile) reconcile ;; + *) result false invalid_arguments; exit 2 ;; +esac diff --git a/static/build/.tmp_update/bin/bloom-wifi b/static/build/.tmp_update/bin/bloom-wifi index bfe412c54..954918220 100755 --- a/static/build/.tmp_update/bin/bloom-wifi +++ b/static/build/.tmp_update/bin/bloom-wifi @@ -20,6 +20,7 @@ SERVICES=${BLOOM_WIFI_SERVICES_BIN:-/mnt/SDCARD/.tmp_update/bin/bloom-network-se NETWORK=${BLOOM_WIFI_NETWORK_BIN:-/mnt/SDCARD/.tmp_update/bin/bloom-network} METADATA=${BLOOM_WIFI_METADATA_EVENT_BIN:-/mnt/SDCARD/.tmp_update/bin/bloom-network-metadata} UPDATE=${BLOOM_WIFI_UPDATE_EVENT_BIN:-/mnt/SDCARD/.tmp_update/bin/bloom-network-update} +SYNC=${BLOOM_WIFI_SYNC_EVENT_BIN:-/mnt/SDCARD/.tmp_update/bin/bloom-network-sync} NOHUP=${BLOOM_WIFI_NOHUP_BIN:-/usr/bin/nohup} SELF=${BLOOM_WIFI_SELF:-/mnt/SDCARD/.tmp_update/bin/bloom-wifi} INTERFACE=${BLOOM_WIFI_INTERFACE:-wlan0} @@ -60,6 +61,7 @@ notify_connectivity() { if "$NETWORK" events publish "$1" >/dev/null 2>&1; then [ ! -x "$METADATA" ] || "$METADATA" reconcile >/dev/null 2>&1 || true [ ! -x "$UPDATE" ] || "$UPDATE" reconcile >/dev/null 2>&1 || true + [ ! -x "$SYNC" ] || "$SYNC" reconcile >/dev/null 2>&1 || true fi return 0 } diff --git a/test/shell/dev_deploy.bats b/test/shell/dev_deploy.bats index 40ecfe429..7d70f70a6 100644 --- a/test/shell/dev_deploy.bats +++ b/test/shell/dev_deploy.bats @@ -161,6 +161,7 @@ stage_network_bundle() { bin/bloom-network \ bin/bloom-network-metadata \ bin/bloom-network-update \ + bin/bloom-network-sync \ bin/bloom-network-services \ bin/bloom-network-transaction \ bin/bloom-platform \ diff --git a/test/shell/legacy_manifest.bats b/test/shell/legacy_manifest.bats index 5df591561..e6b5d95e0 100644 --- a/test/shell/legacy_manifest.bats +++ b/test/shell/legacy_manifest.bats @@ -6,7 +6,7 @@ --manifest /workspace/build/legacy-manifest.json [ "$status" -eq 0 ] - [[ "$output" == "legacy manifest validate: 334 components" ]] + [[ "$output" == "legacy manifest validate: 335 components" ]] [ ! -e "/workspace/static/packages/RApp/SNK - Neo Geo (GnGeo)" ] [ ! -e "/workspace/static/packages/RApp/PICO-8 (PICO-8 standalone)" ] [ ! -e "/workspace/static/packages/RApp/SCUMM (ScummVM standalone)" ] @@ -104,7 +104,7 @@ import sys components = json.load(open(sys.argv[1], encoding="utf-8"))["components"] runtime = [item for item in components if item["id"].startswith("runtime-")] bloom = [item for item in runtime if item["source"] == "https://github.com/boburning/BloomOS"] -assert len(bloom) == 57 +assert len(bloom) == 58 assert all(item["resolution"] == "source-build" for item in bloom) assert all(item["source_revision"] == "release-commit" for item in bloom) PY diff --git a/test/shell/network_sync.bats b/test/shell/network_sync.bats new file mode 100644 index 000000000..b0b6daf0b --- /dev/null +++ b/test/shell/network_sync.bats @@ -0,0 +1,83 @@ +#!/usr/bin/env bats + +setup() { + export SERVICE=/workspace/static/build/.tmp_update/bin/bloom-network-sync + export BLOOM_NETWORK_SYNC_NETWORK_BIN="$BATS_TEST_TMPDIR/bloom-network" + export BLOOM_NETWORK_SYNC_JQ_BIN=/usr/bin/jq + export EVENT_STATUS="$BATS_TEST_TMPDIR/event.json" + export ACK_STATUS="$BATS_TEST_TMPDIR/ack.json" + export CLIENT_LOG="$BATS_TEST_TMPDIR/client.log" + cat >"$BLOOM_NETWORK_SYNC_NETWORK_BIN" <<'SH' +#!/bin/sh +printf '%s\n' "$*" >>"$CLIENT_LOG" +case "$*" in + 'events status sync') cat "$EVENT_STATUS" ;; + 'events ack sync '*) [ ! -e "$ACK_STATUS.exit" ] || exit 1; cat "$ACK_STATUS" ;; + *) exit 2 ;; +esac +SH + chmod +x "$BLOOM_NETWORK_SYNC_NETWORK_BIN" +} + +write_event() { + printf '{"schema":1,"service":"bloom-network-events","operation":"status","client":"sync","connectivity":"%s","generation":%s,"changed":false,"pending":%s}\n' "$1" "$2" "$3" >"$EVENT_STATUS" +} + +write_ack() { + printf '{"schema":1,"service":"bloom-network-events","operation":"ack","client":"sync","connectivity":"%s","generation":%s,"changed":false,"pending":false}\n' "$1" "$2" >"$ACK_STATUS" +} + +@test "sync consumer treats an absent pending generation as a no-op" { + write_event unknown 0 false + run "$SERVICE" reconcile + [ "$status" -eq 0 ] + [[ "$output" == *'"state":"no_pending_event"'* ]] +} + +@test "associated sync event records the unavailable engine boundary" { + write_event associated 7 true + write_ack associated 7 + run "$SERVICE" reconcile + [ "$status" -eq 0 ] + [[ "$output" == *'"generation":7,"disposition":"engine_unavailable"'* ]] + [ "$(sed -n '2p' "$CLIENT_LOG")" = 'events ack sync 7' ] +} + +@test "offline sync event receives an explicit offline disposition" { + write_event offline 8 true + write_ack offline 8 + run "$SERVICE" reconcile + [ "$status" -eq 0 ] + [[ "$output" == *'"generation":8,"disposition":"offline"'* ]] +} + +@test "sync consumer rejects malformed event authority output" { + printf '%s\n' '{"schema":1,"client":"sync","connectivity":"associated","generation":1,"pending":true}' >"$EVENT_STATUS" + run "$SERVICE" reconcile + [ "$status" -eq 1 ] + [[ "$output" == *'"state":"event_invalid"'* ]] +} + +@test "sync event acknowledgement failure retains the pending generation" { + write_event associated 9 true + write_ack associated 9 + : >"$ACK_STATUS.exit" + run "$SERVICE" reconcile + [ "$status" -eq 1 ] + [[ "$output" == *'"state":"acknowledgement_failed"'* ]] +} + +@test "sync consumer rejects mismatched acknowledgement output" { + write_event associated 10 true + write_ack offline 10 + run "$SERVICE" reconcile + [ "$status" -eq 1 ] + [[ "$output" == *'"state":"acknowledgement_invalid"'* ]] +} + +@test "sync event disposition has no transport or save mutation surface" { + ! grep -E 'curl|wget|rsync|syncthing|/Saves|^[[:space:]]*sync([[:space:]]|$)' "$SERVICE" + run "$SERVICE" arbitrary + [ "$status" -eq 2 ] + [[ "$output" == *'"state":"invalid_arguments"'* ]] +} diff --git a/test/shell/transfer_policy.bats b/test/shell/transfer_policy.bats index 2497ea8ba..b97557ec0 100755 --- a/test/shell/transfer_policy.bats +++ b/test/shell/transfer_policy.bats @@ -104,6 +104,7 @@ teardown() { bin/bloom-network \ bin/bloom-network-metadata \ bin/bloom-network-update \ + bin/bloom-network-sync \ bin/bloom-network-services \ bin/bloom-network-transaction \ bin/bloom-platform \ @@ -131,7 +132,7 @@ teardown() { run python3 /workspace/tools/dev-deploy-manifest.py network-helpers "$payload" \ net02-fixture 0123456789abcdef0123456789abcdef01234567 "$manifest" [ "$status" -eq 0 ] || printf '%s\n' "$output" >&3 - [ "$(jq '.files | length' "$manifest")" -eq 19 ] + [ "$(jq '.files | length' "$manifest")" -eq 20 ] rm "$payload/bin/bloom-platform" run python3 /workspace/tools/dev-deploy-manifest.py network-helpers "$payload" \ diff --git a/test/shell/wifi_backend.bats b/test/shell/wifi_backend.bats index 4e188127e..5afbbf135 100644 --- a/test/shell/wifi_backend.bats +++ b/test/shell/wifi_backend.bats @@ -20,6 +20,7 @@ setup() { export BLOOM_WIFI_NETWORK_BIN="$BATS_TEST_TMPDIR/bloom-network" export BLOOM_WIFI_METADATA_EVENT_BIN="$BATS_TEST_TMPDIR/bloom-network-metadata" export BLOOM_WIFI_UPDATE_EVENT_BIN="$BATS_TEST_TMPDIR/bloom-network-update" + export BLOOM_WIFI_SYNC_EVENT_BIN="$BATS_TEST_TMPDIR/bloom-network-sync" export BLOOM_WIFI_NOHUP_BIN=/usr/bin/nohup export BLOOM_WIFI_SELF="$SERVICE" export BLOOM_WIFI_WATCH_DIR="$BATS_TEST_TMPDIR/watcher" @@ -28,6 +29,7 @@ setup() { export EVENT_LOG="$BATS_TEST_TMPDIR/events.log" export METADATA_LOG="$BATS_TEST_TMPDIR/metadata.log" export UPDATE_LOG="$BATS_TEST_TMPDIR/update.log" + export SYNC_LOG="$BATS_TEST_TMPDIR/sync.log" mkdir -p "$BLOOM_WIFI_ROOT/sys/class/net/wlan0" printf down >"$BLOOM_WIFI_ROOT/sys/class/net/wlan0/operstate" printf 0 >"$BLOOM_WIFI_ROOT/sys/class/net/wlan0/carrier" @@ -72,6 +74,10 @@ SH cat >"$BLOOM_WIFI_UPDATE_EVENT_BIN" <<'SH' #!/bin/sh printf '%s\n' "$*" >>"$UPDATE_LOG" +SH + cat >"$BLOOM_WIFI_SYNC_EVENT_BIN" <<'SH' +#!/bin/sh +printf '%s\n' "$*" >>"$SYNC_LOG" SH cat >"$BLOOM_WIFI_IP_BIN" <<'SH' #!/bin/sh @@ -81,6 +87,7 @@ SH chmod +x "$BLOOM_WIFI_IP_BIN" "$BLOOM_WIFI_SLEEP_BIN" "$BLOOM_WIFI_SERVICES_BIN" \ "$BLOOM_WIFI_NETWORK_BIN" "$BLOOM_WIFI_METADATA_EVENT_BIN" chmod +x "$BLOOM_WIFI_UPDATE_EVENT_BIN" + chmod +x "$BLOOM_WIFI_SYNC_EVENT_BIN" chmod +x "$BLOOM_PLATFORM_BIN" "$BLOOM_WIFI_JSONVAL_BIN" } @@ -95,6 +102,7 @@ SH [ "$(cat "$EVENT_LOG")" = 'events publish associated' ] [ "$(cat "$METADATA_LOG")" = reconcile ] [ "$(cat "$UPDATE_LOG")" = reconcile ] + [ "$(cat "$SYNC_LOG")" = reconcile ] } @test "associated Plus without an address restarts canonical DHCP" { @@ -262,6 +270,7 @@ SH [ ! -s "$WIFI_LOG" ] [ ! -e "$METADATA_LOG" ] [ ! -e "$UPDATE_LOG" ] + [ ! -e "$SYNC_LOG" ] } @test "metadata observation failure never restarts or rolls back healthy Wi-Fi" { @@ -277,6 +286,7 @@ SH [ ! -s "$WIFI_LOG" ] [ "$(cat "$EVENT_LOG")" = 'events publish associated' ] [ "$(cat "$UPDATE_LOG")" = reconcile ] + [ "$(cat "$SYNC_LOG")" = reconcile ] } @test "update disposition failure never restarts or rolls back healthy Wi-Fi" { @@ -292,6 +302,23 @@ SH [ ! -s "$WIFI_LOG" ] [ "$(cat "$EVENT_LOG")" = 'events publish associated' ] [ "$(cat "$METADATA_LOG")" = reconcile ] + [ "$(cat "$SYNC_LOG")" = reconcile ] +} + +@test "sync disposition failure never restarts or rolls back healthy Wi-Fi" { + printf up >"$BLOOM_WIFI_ROOT/sys/class/net/wlan0/operstate" + printf 1 >"$BLOOM_WIFI_ROOT/sys/class/net/wlan0/carrier" + : >"$BLOOM_WIFI_ROOT/sys/class/net/wlan0/ipv4" + printf '#!/bin/sh\nexit 1\n' >"$BLOOM_WIFI_SYNC_EVENT_BIN" + chmod +x "$BLOOM_WIFI_SYNC_EVENT_BIN" + + run "$SERVICE" reconcile + [ "$status" -eq 0 ] + [[ "$output" == *'"applied":false,"state":"preserved"'* ]] + [ ! -s "$WIFI_LOG" ] + [ "$(cat "$EVENT_LOG")" = 'events publish associated' ] + [ "$(cat "$METADATA_LOG")" = reconcile ] + [ "$(cat "$UPDATE_LOG")" = reconcile ] } @test "disabling Wi-Fi stops an in-flight address recovery watcher before radio teardown" { diff --git a/tools/dev-deploy-manifest.py b/tools/dev-deploy-manifest.py index 9eeebca38..9b38f115c 100755 --- a/tools/dev-deploy-manifest.py +++ b/tools/dev-deploy-manifest.py @@ -46,6 +46,7 @@ "bin/bloom-network", "bin/bloom-network-metadata", "bin/bloom-network-update", + "bin/bloom-network-sync", "bin/bloom-network-services", "bin/bloom-network-transaction", "bin/bloom-platform", diff --git a/tools/legacy_manifest.py b/tools/legacy_manifest.py index 15e74f909..b0c22286f 100644 --- a/tools/legacy_manifest.py +++ b/tools/legacy_manifest.py @@ -62,6 +62,7 @@ "runtime-tmp-update-bin-bloom-network", "runtime-tmp-update-bin-bloom-network-metadata", "runtime-tmp-update-bin-bloom-network-update", + "runtime-tmp-update-bin-bloom-network-sync", "runtime-tmp-update-bin-bloom-network-services", "runtime-tmp-update-bin-bloom-network-transaction", "runtime-tmp-update-bin-bloom-remote-keys",