From b06f4a826137c7b5f1b5ccc493792b9e31ecbd1e Mon Sep 17 00:00:00 2001 From: boburning Date: Fri, 28 Aug 2026 09:54:02 -0400 Subject: [PATCH 1/2] NET-04D: expose guarded Wi-Fi profile operations --- build/dependencies.lock | 2 +- build/legacy-manifest.json | 4 +-- docs/1.0_TEST_MATRIX.md | 2 +- docs/ARCHITECTURE.md | 9 ++++- docs/DEVELOPMENT.md | 23 ++++++++++-- static/build/.tmp_update/bin/bloomctl | 33 +++++++++++++++++- test/shell/bloomctl_info.bats | 50 +++++++++++++++++++++++++++ 7 files changed, 114 insertions(+), 9 deletions(-) diff --git a/build/dependencies.lock b/build/dependencies.lock index 8d159390d..9774312a9 100644 --- a/build/dependencies.lock +++ b/build/dependencies.lock @@ -186,7 +186,7 @@ manifest = "build/legacy-manifest.json" schema = 1 component_count = 332 file_count = 2131 -byte_count = 314676226 +byte_count = 314678142 source_build_components = 228 unresolved_components = 104 inherited_baseline = "07505ea58c7bba698d6b9220ff43946a43cac76b" diff --git a/build/legacy-manifest.json b/build/legacy-manifest.json index 1c7b252c7..12a05cb59 100644 --- a/build/legacy-manifest.json +++ b/build/legacy-manifest.json @@ -1021,8 +1021,8 @@ "kind": "runtime", "path": "static/build/.tmp_update/bin/bloomctl", "file_count": 1, - "byte_count": 35372, - "tree_sha256": "653222365d8f06ca4bae9a750525e04649991943afab1701eb6a0f6919a6578d", + "byte_count": 37288, + "tree_sha256": "3a652088eb1b1384f5f22a60b1ecb44c1fca8dafe1c1fa6803c94a131a4e5748", "source": "https://github.com/boburning/BloomOS", "source_revision": "release-commit", "license": "GPL-3.0-only", diff --git a/docs/1.0_TEST_MATRIX.md b/docs/1.0_TEST_MATRIX.md index 82211ea59..9750949c1 100644 --- a/docs/1.0_TEST_MATRIX.md +++ b/docs/1.0_TEST_MATRIX.md @@ -47,7 +47,7 @@ shoulder button. | Settings | One sectioned surface; inline values; capability filtering; one-layer details; hidden Developer section; optional overlapping L1/R1 list paging | | Quick Settings | START from core surfaces; immediate brightness/volume/mute; capability-gated Wi-Fi; readable battery; B/START close | | Network services | Desired/runtime separation; carrier without IPv4 remains acquiring/not-ready; canonical DHCP restart and bounded post-address listener reconciliation; SSH shell and SFTP independent key-only states; canonical transfer roots and modes; Saves read-only; private roots denied; traversal/symlink/path bounds; same-filesystem atomic publication; authorized-key import; quiesce/health; stale FTP/Telnet/FileBrowser/inherited-Samba/WPS/hotspot flags cannot activate listeners; process-aware listener audit; Samba hidden until a materially different source-build candidate passes feasibility | -| Wi-Fi profile store | JFFS2 private root; stdin-only bounded replacement and single-profile upsert; requested/document ID binding; inactive-ID deletion with active-profile refusal; schema/unknown-field/count/size enforcement; WPA2-PSK, explicit open, and manual hidden WPA2; WEP and hidden open denial; unique IDs/SSIDs; redacted status; mode-0600 atomic single-profile supplicant projection; symlink, occupied-slot, live-lock, invalid-profile, and prior-byte preservation | +| Wi-Fi profile store | JFFS2 private root; stdin-only bounded replacement and `bloomctl network profile upsert ID`; requested/document ID binding; explicit `network forget ID`; inactive-ID deletion with active-profile refusal; active forget only after a separately confirmed fallback connection; schema/unknown-field/count/size enforcement; WPA2-PSK, explicit open, and manual hidden WPA2; WEP and hidden open denial; unique IDs/SSIDs; redacted status; mode-0600 atomic single-profile supplicant projection; symlink, occupied-slot, live-lock, invalid-profile, and prior-byte preservation | | Wi-Fi scan API | Asynchronous start; redacted status; explicit ID-scoped local results; cancel and terminal discard; one active worker; five-second rate limit; 15-second runtime; 64-KiB raw ceiling; 32 unique printable SSIDs; eight-operation history; open/WPA2-PSK/unsupported classification; duplicate signal reconciliation; backend failure, timeout, oversize, unsafe-path, stale/live-lock, and unsupported-hardware handling | | GameSwitcher | MENU open/toggle; Left/Right select; L1/R1 page; A resume; X actions; START Quick Settings; B Home; no inherited brightness/view/delete behavior | | Apps/Tweaks | Every app disposition recorded; normal configuration requires no Tweaks; supervised launch remains bounded | diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 60e49cad1..d8e28937f 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -294,7 +294,8 @@ the requested handle and republishes the complete validated store without returning profile data. Delete refuses the currently projected ID, identified by a non-secret managed-config marker, so forgetting a profile cannot silently strand or mutate the active station. Active-profile replacement and removal -remain the responsibility of the later rollback-bounded association slice. +use the rollback-bounded association slice described below: independently +confirm a different profile first, then delete the now-inactive old ID. The service never scans, associates, changes the Wi-Fi preference, or restarts the radio; those behaviors remain with later NET-04 API work and the existing `bloom-network`/`bloom-wifi` owners. @@ -322,6 +323,12 @@ recovery verify the private snapshot, atomically restore it, restart the station lifecycle, and recheck services and listeners. Confirmation still requires a fresh host-key-verified SSH connection; neither status nor evidence exposes a profile, SSID, credential, address, nonce, or snapshot content. +`bloomctl network profile upsert ID` preserves the stdin-only secret boundary, +`profile status` remains redacted, and `network forget ID` delegates only the +explicit non-secret ID to the same private store. Forget therefore fails closed +for the projected profile. The supported active-forget sequence is a confirmed +`network connect` transaction to a fallback ID followed by forgetting the old +ID; there is no delete-first or implicit-fallback operation. Listener reconciliation is event-driven at existing fixed boundaries: boot, Wi-Fi reconciliation, Wi-Fi preference changes, remote-key publication, and diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 38aa8072d..bd5125709 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -293,9 +293,8 @@ associates to a network. Run the focused gate with the pinned shell-test image: Do not copy a live `/appconfigs/wpa_supplicant.conf` into the repository, fixture output, shell history, logs, screenshots, or evidence. Device preflight may inspect only filesystem type, mode, byte count, structural counts, and -redacted health. The remaining NET-04 scan/connect orchestration and active -profile removal must retain stdin or an equivalent fixed private input -boundary and the existing rollback rules. +redacted health. NET-04 orchestration retains stdin or an equivalent fixed +private input boundary and the existing rollback rules. The NET-04 scan slice is asynchronous and keeps result data only under `/tmp`: @@ -333,6 +332,24 @@ invoke `network-rollback` from an independently safe path. Never exercise this transition over the only unattended device route until its exact candidate has passed the host fault matrix and a local synthetic device fixture. +The bounded device-facing aliases are: + +```sh +bloomctl network profile status +bloomctl network profile upsert PROFILE_ID < private-profile.json +bloomctl network connect begin 120 +bloomctl network connect apply NONCE PROFILE_ID +bloomctl network connect confirm NONCE +bloomctl network connect cancel +bloomctl network forget INACTIVE_PROFILE_ID +``` + +To forget the active profile, first apply and independently confirm a different +stored fallback profile. Only then call `network forget` for the old ID. The +private authority rejects deleting the currently projected ID, so an +interrupted or failed cutover retains both the exact rollback configuration and +the old stored profile. Do not add a delete-first shortcut. + An activated update is not considered successful merely because the installer or MainUI started. The runtime records each unconfirmed boot in durable `.bloom/update` state. After device checks pass, developers can promote the diff --git a/static/build/.tmp_update/bin/bloomctl b/static/build/.tmp_update/bin/bloomctl index 87c7d974a..e331cc8f7 100755 --- a/static/build/.tmp_update/bin/bloomctl +++ b/static/build/.tmp_update/bin/bloomctl @@ -27,6 +27,7 @@ BLOOM_NETWORK="${BLOOM_NETWORK_BIN:-$SCRIPT_DIR/bloom-network}" BLOOM_NETWORK_SERVICES="${BLOOM_NETWORK_SERVICES_BIN:-$SCRIPT_DIR/bloom-network-services}" BLOOM_NETWORK_TRANSACTION="${BLOOM_NETWORK_TRANSACTION_BIN:-$SCRIPT_DIR/bloom-network-transaction}" BLOOM_WIFI_SCAN="${BLOOM_WIFI_SCAN_BIN:-$SCRIPT_DIR/bloom-wifi-scan}" +BLOOM_WIFI_PROFILES="${BLOOM_WIFI_PROFILES_BIN:-$SCRIPT_DIR/bloom-wifi-profiles}" BLOOM_JQ="${BLOOM_JQ_BIN:-$SCRIPT_DIR/jq}" BLOOM_CONTROLS="${BLOOM_CONTROLS_BIN:-$SCRIPT_DIR/bloom-controls}" BLOOM_TIME="${BLOOM_TIME_BIN:-$SCRIPT_DIR/bloom-time}" @@ -379,7 +380,7 @@ usage() { printf ' bloomctl platform capabilities\n' >&2 printf ' bloomctl settings {status|values|import-onion|sync-onion|reconcile-onion|materialize-onion|activate-bloom|rollback-authority|reset-defaults|first-run-status|complete-first-run|set FIELD VALUE}\n' >&2 printf ' bloomctl power {status|reboot|poweroff}\n' >&2 - printf ' bloomctl network {status|reconcile|enable|disable|scan start|status ID|results ID|cancel ID|discard ID|transaction status|begin SECONDS|apply NONCE OPERATION [ID]|confirm NONCE|rollback}\n' >&2 + printf ' bloomctl network {status|reconcile|enable|disable|profile status|upsert ID|forget ID|scan start|status ID|results ID|cancel ID|discard ID|connect begin SECONDS|apply NONCE PROFILE_ID|confirm NONCE|cancel|transaction status|begin SECONDS|apply NONCE OPERATION [ID]|confirm NONCE|rollback}\n' >&2 printf ' bloomctl controls {status|brightness VALUE|volume VALUE|mute true}\n' >&2 printf ' bloomctl time status\n' >&2 printf ' bloomctl lid status\n' >&2 @@ -626,6 +627,22 @@ network_command() { 1:status:) "$BLOOM_NETWORK" status ;; 1:reconcile:) "$BLOOM_NETWORK" request reconcile ;; 1:enable:|1:disable:) "$BLOOM_NETWORK" request "$1" ;; + 2:profile:status) + [ -x "$BLOOM_WIFI_PROFILES" ] || { printf 'bloomctl: Wi-Fi profile service is unavailable\n' >&2; return 1; } + "$BLOOM_WIFI_PROFILES" status --json + ;; + 3:profile:upsert) + case "$3" in ''|.*|-*|*[!a-z0-9._-]*) usage; return 2 ;; esac + [ "${#3}" -le 32 ] || { usage; return 2; } + [ -x "$BLOOM_WIFI_PROFILES" ] || { printf 'bloomctl: Wi-Fi profile service is unavailable\n' >&2; return 1; } + "$BLOOM_WIFI_PROFILES" upsert "$3" + ;; + 2:forget:*) + case "$2" in ''|.*|-*|*[!a-z0-9._-]*) usage; return 2 ;; esac + [ "${#2}" -le 32 ] || { usage; return 2; } + [ -x "$BLOOM_WIFI_PROFILES" ] || { printf 'bloomctl: Wi-Fi profile service is unavailable\n' >&2; return 1; } + "$BLOOM_WIFI_PROFILES" delete "$2" + ;; 2:scan:start) [ -x "$BLOOM_WIFI_SCAN" ] || { printf 'bloomctl: Wi-Fi scan service is unavailable\n' >&2; return 1; } "$BLOOM_WIFI_SCAN" start @@ -654,6 +671,20 @@ network_command() { [ -x "$BLOOM_NETWORK_TRANSACTION" ] || { printf 'bloomctl: network transaction service is unavailable\n' >&2; return 1; } "$BLOOM_NETWORK_TRANSACTION" apply "$3" "$4" "$5" ;; + 3:connect:begin|3:connect:confirm) + [ -x "$BLOOM_NETWORK_TRANSACTION" ] || { printf 'bloomctl: network transaction service is unavailable\n' >&2; return 1; } + "$BLOOM_NETWORK_TRANSACTION" "$2" "$3" + ;; + 2:connect:cancel) + [ -x "$BLOOM_NETWORK_TRANSACTION" ] || { printf 'bloomctl: network transaction service is unavailable\n' >&2; return 1; } + "$BLOOM_NETWORK_TRANSACTION" rollback + ;; + 4:connect:apply) + case "$4" in ''|.*|-*|*[!a-z0-9._-]*) usage; return 2 ;; esac + [ "${#4}" -le 32 ] || { usage; return 2; } + [ -x "$BLOOM_NETWORK_TRANSACTION" ] || { printf 'bloomctl: network transaction service is unavailable\n' >&2; return 1; } + "$BLOOM_NETWORK_TRANSACTION" apply "$3" wifi-profile-connect "$4" + ;; *) usage; return 2 ;; esac } diff --git a/test/shell/bloomctl_info.bats b/test/shell/bloomctl_info.bats index ed3b8b79f..d6d65e862 100644 --- a/test/shell/bloomctl_info.bats +++ b/test/shell/bloomctl_info.bats @@ -717,6 +717,14 @@ SH printf '{"schema":1,"scan_arguments":"%s"}\n' "$*" SH chmod +x "$scanner" + profiles="$BATS_TEST_TMPDIR/bloom-wifi-profiles" + cat >"$profiles" <<'SH' +#!/bin/sh +input='' +[ "$1" != upsert ] || input=$(cat) +printf '{"schema":1,"profile_arguments":"%s","input":"%s"}\n' "$*" "$input" +SH + chmod +x "$profiles" run env BLOOM_NETWORK_BIN="$service" \ sh /workspace/static/build/.tmp_update/bin/bloomctl network status @@ -738,6 +746,20 @@ SH [ "$status" -eq 0 ] [ "$output" = '{"schema":1,"arguments":"request disable"}' ] + run env BLOOM_NETWORK_BIN="$service" BLOOM_WIFI_PROFILES_BIN="$profiles" \ + sh /workspace/static/build/.tmp_update/bin/bloomctl network profile status + [ "$status" -eq 0 ] + [ "$output" = '{"schema":1,"profile_arguments":"status --json","input":""}' ] + + run bash -c 'printf %s "{synthetic}" | BLOOM_NETWORK_BIN="$1" BLOOM_WIFI_PROFILES_BIN="$2" sh /workspace/static/build/.tmp_update/bin/bloomctl network profile upsert fixture-home' _ "$service" "$profiles" + [ "$status" -eq 0 ] + [ "$output" = '{"schema":1,"profile_arguments":"upsert fixture-home","input":"{synthetic}"}' ] + + run env BLOOM_NETWORK_BIN="$service" BLOOM_WIFI_PROFILES_BIN="$profiles" \ + sh /workspace/static/build/.tmp_update/bin/bloomctl network forget fixture-old + [ "$status" -eq 0 ] + [ "$output" = '{"schema":1,"profile_arguments":"delete fixture-old","input":""}' ] + run env BLOOM_NETWORK_BIN="$service" BLOOM_WIFI_SCAN_BIN="$scanner" \ sh /workspace/static/build/.tmp_update/bin/bloomctl network scan start [ "$status" -eq 0 ] @@ -777,6 +799,34 @@ SH [ "$status" -eq 0 ] [ "$output" = '{"schema":1,"transaction_arguments":"rollback"}' ] + run env BLOOM_NETWORK_BIN="$service" BLOOM_NETWORK_TRANSACTION_BIN="$transaction" \ + sh /workspace/static/build/.tmp_update/bin/bloomctl network connect begin 60 + [ "$status" -eq 0 ] + [ "$output" = '{"schema":1,"transaction_arguments":"begin 60"}' ] + + run env BLOOM_NETWORK_BIN="$service" BLOOM_NETWORK_TRANSACTION_BIN="$transaction" \ + sh /workspace/static/build/.tmp_update/bin/bloomctl network connect apply nonce fixture-home + [ "$status" -eq 0 ] + [ "$output" = '{"schema":1,"transaction_arguments":"apply nonce wifi-profile-connect fixture-home"}' ] + + run env BLOOM_NETWORK_BIN="$service" BLOOM_NETWORK_TRANSACTION_BIN="$transaction" \ + sh /workspace/static/build/.tmp_update/bin/bloomctl network connect confirm nonce + [ "$status" -eq 0 ] + [ "$output" = '{"schema":1,"transaction_arguments":"confirm nonce"}' ] + + run env BLOOM_NETWORK_BIN="$service" BLOOM_NETWORK_TRANSACTION_BIN="$transaction" \ + sh /workspace/static/build/.tmp_update/bin/bloomctl network connect cancel + [ "$status" -eq 0 ] + [ "$output" = '{"schema":1,"transaction_arguments":"rollback"}' ] + + run env BLOOM_NETWORK_BIN="$service" BLOOM_WIFI_PROFILES_BIN="$profiles" \ + sh /workspace/static/build/.tmp_update/bin/bloomctl network forget '../escape' + [ "$status" -eq 2 ] + + run env BLOOM_NETWORK_BIN="$service" BLOOM_NETWORK_TRANSACTION_BIN="$transaction" \ + sh /workspace/static/build/.tmp_update/bin/bloomctl network connect apply nonce '../escape' + [ "$status" -eq 2 ] + run env BLOOM_NETWORK_BIN="$service" \ sh /workspace/static/build/.tmp_update/bin/bloomctl network restart [ "$status" -eq 2 ] From 7dd029066b81b3fd22e29a2d96e01341a5e84874 Mon Sep 17 00:00:00 2001 From: boburning Date: Fri, 28 Aug 2026 09:56:13 -0400 Subject: [PATCH 2/2] docs(program): record NET-04D pull request --- docs/program/BloomOS_Work_Program_v3.json | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/program/BloomOS_Work_Program_v3.json b/docs/program/BloomOS_Work_Program_v3.json index 651fe2be1..c616cfda3 100644 --- a/docs/program/BloomOS_Work_Program_v3.json +++ b/docs/program/BloomOS_Work_Program_v3.json @@ -834,12 +834,13 @@ "github_prs": [ 487, 488, - 489 + 489, + 490 ], "risk_tier": 2, "deployment_tier": "warm", "device_gate_required": true, - "last_audited_main": "5f155d6d16fd927d775f18219353c76ae641d750", + "last_audited_main": "8ffc258c61748deea3c773a69b4ddc9fe0402d52", "evidence": [ "issue #486 tracks NET-04 after NET-03 merged as fca10bb0168d8bc38737881745b68cd3bab9da59; no prior NET-04 issue or implementation PR exists and current main has no bounded scan/connect/forget orchestration", "draft PR #487 is the first host-safe NET-04 slice and extends the private NET-03 authority with stdin-only atomic single-profile upsert, requested/document ID binding, inactive-profile deletion, a non-secret projected-profile marker, and fail-closed refusal to replace or forget the active profile; 16/16 focused tests passed in 4.8 seconds with exact 314651736-byte dependency preflight, and the slice does not scan, associate, restart Wi-Fi, change preferences, or complete NET-04", @@ -847,7 +848,9 @@ "draft PR #488 adds a fixed-interface asynchronous scan worker with immediate high-entropy operation IDs, redacted status, explicit local-only results, cancellation, terminal discard, one active worker, bounded rate/runtime/raw output/results/history, open/WPA2-PSK/unsupported classification, and bloomctl delegation; 9/9 affected tests passed in 13.8 seconds and 13/13 focused manifest/provenance tests passed in 271 seconds with exact 314669717-byte dependency preflight, and the slice does not connect, forget, change a preference, restart Wi-Fi, modify DHCP, or complete NET-04", "exact head b5de4b1ae7f7d558886a8c55047b3a7ada141621 passed risk-scoped shell workflow 33173965383 with 1m39s test execution; its exact 17244-byte helper with SHA-256 0d3b3e1ac893fb787fcd7c3d28b2c526175475d360bab83836d9072efab695b3 and a 156-byte synthetic platform fixture were transferred through strict key-only SSH and exercised only under /tmp with /bin/echo replacing iw, transitioned RUNNING to SUCCEEDED with zero results, enforced 0700/0700/0600/0600 root-operation-state-result modes, discarded the terminal operation, removed every task file, retained live supplicant SHA-256 d7fbadd510e76c423638513337620e17944d51b4505ff314354971d97114bdef, and left aggregate device health green without a real scan or network mutation", "PR #489 adds a rollback-bounded wifi-profile-connect transaction that preserves the exact prior supplicant bytes in a mode-0600 snapshot below a mode-0700 JFFS2-private root, places only its SHA-256 in public transaction metadata, routes station replacement through the canonical network and Wi-Fi owners, and restores the exact prior configuration after apply failure, explicit rollback, timeout, or interrupted boot; 88/88 direct affected tests and 140/140 exhaustive focused tests passed with exact 314676226-byte inventory preflight", - "exact candidate ff5b0775 was 33007 bytes with SHA-256 d3bed4f13e6e3b84344ba9c370f6a258b09d5e23a96378068a64eff30b2ab68e and passed an isolated Mini Plus /tmp fixture using synthetic station content and mocked dependencies: apply changed only the fixture, rollback restored the exact prior hash, private root/snapshot modes were 0700/0600, no public secret copy existed, all task files were removed, and the live device remained associated and healthy without a real scan, profile transition, Wi-Fi toggle, DHCP mutation, listener mutation, update, or reboot" + "exact candidate ff5b0775 was 33007 bytes with SHA-256 d3bed4f13e6e3b84344ba9c370f6a258b09d5e23a96378068a64eff30b2ab68e and passed an isolated Mini Plus /tmp fixture using synthetic station content and mocked dependencies: apply changed only the fixture, rollback restored the exact prior hash, private root/snapshot modes were 0700/0600, no public secret copy existed, all task files were removed, and the live device remained associated and healthy without a real scan, profile transition, Wi-Fi toggle, DHCP mutation, listener mutation, update, or reboot", + "PR #490 exposes the existing private profile and guarded transaction authorities through bounded bloomctl network profile status, profile upsert ID, connect begin/apply/confirm/cancel, and forget ID commands; profile input remains stdin-only, traversal IDs fail locally, forget still refuses the projected profile, and active forget is only the safe sequence of independently confirming a distinct fallback before deleting the now-inactive old ID; 76/76 direct tests and 99/99 exhaustive focused tests passed with exact 314678142-byte inventory preflight", + "exact candidate b06f4a82 bloomctl was 37288 bytes with SHA-256 f9e765dd668aeff9137772ecd06bf3bd02b7fe30597e46243dcb2b1caa29046e and passed an isolated Mini Plus /tmp fixture against mocked network, transaction, and profile helpers with synthetic stdin content; every task file was removed and live networking remained associated without a real scan, profile transition, active deletion, Wi-Fi toggle, DHCP or listener mutation, update, or reboot" ] }, {