feat: add bounded admin WebSocket backend (Phase 8D.5)

- Require current admin cookie sessions, Origin checks and single-use
  tickets
- Reuse the shared console with session-aware authorization and slot
  allocation
- Add HTTPD-owned I/O, bounded buffering and revocation cleanup
- Prevent LRU eviction of serial clients and stale admin socket closure
- Reject unsupported web-shell mutations before side effects
- Add host regressions, a smoke client and resource accounting

Validated by user sign-off after a 15-minute full-client soak at 230400
baud, with a few broker drops under heavy output. Browser UI remains
for Phase 8D.6; numeric memory reserves remain open.
This commit is contained in:
2026-09-06 14:41:41 +02:00
parent e5dce12ed4
commit aeb2043396
37 changed files with 3651 additions and 91 deletions
+7 -3
View File
@@ -98,9 +98,9 @@ TinyUSB callbacks enqueue/copy data and state; the transport task owns broker li
`web_server` runs HTTPS only on port 443 using the device-specific self-signed P-256 certificate from `web_security`. Current routes provide the UI, static assets, status, ticket issuance, and serial WebSocket upgrade.
HTTPS login uses `user_database` and opaque server-side cookie sessions; Basic authentication and its cache are removed in 8D.3. Before administrator bootstrap, the migrated role-`user` account is synchronized from the legacy credential, so that username/password can authenticate through the database; after bootstrap, the legacy blob is independent recovery material and is no longer consulted for authentication or synchronized into role-based accounts. Both `user` and `admin` roles currently receive the same web status/terminal experience; web administration is not implemented.
HTTPS login uses `user_database` and opaque server-side cookie sessions; Basic authentication and its cache are removed in 8D.3. Before administrator bootstrap, the migrated role-`user` account is synchronized from the legacy credential, so that username/password can authenticate through the database; after bootstrap, the legacy blob is independent recovery material and is no longer consulted for authentication or synchronized into role-based accounts. Both roles retain the same shipped web status/serial UI. 8D.5 adds an admin-only backend without a normal UI entry.
`web_cookie_auth` owns login/session/logout policy: four 120-second digest-only pre-login challenges, explicit same-origin bootstrap, five credential verifications per 60-second global window, and no live-record eviction. Host-only `__Host-` Secure/HttpOnly/SameSite=Strict cookies have absolute lifetimes. Login consumes a challenge, validates bounded JSON and issues a fresh session; logout invalidates only its originating session. Mutations require CSRF and strict canonical HTTPS Origin; serial upgrade requires matching cookie/Origin/ticket. Neither role has web administration yet.
`web_cookie_auth` owns login/session/logout policy: four 120-second digest-only pre-login challenges, explicit same-origin bootstrap, five credential verifications per 60-second global window, and no live-record eviction. Host-only `__Host-` Secure/HttpOnly/SameSite=Strict cookies have absolute lifetimes. Login consumes a challenge, validates bounded JSON and issues a fresh session; logout invalidates only its originating session. Mutations require CSRF and strict canonical HTTPS Origin; serial/admin upgrades require matching cookie/Origin/ticket, with admin role additionally required by the admin endpoints.
`web_session_store` holds four static records with token/origin digests, copied principal, separate CSRF state, one-hour absolute expiry and non-reused 64-bit session IDs. These are live cookie sessions in 8D.3, with no sliding renewal. A portMUX protects short state copies/mutations; database/RNG/SHA calls occur outside it. Resolution rechecks ID/expiry after database validation; issuance also checks an invalidation epoch. Stop wipes records without resetting IDs/epochs. Only admitted HTTPS starts initialize the store; failed starts and accepted stops disable it before cleanup. Authentication/store-init failure now gates HTTPS startup rather than falling back to Basic. Sensitive views must be wiped by callers; snapshots contain only counts and storage sizes. Focused host checks live in `tests/web_session_store/`.
@@ -114,6 +114,10 @@ Web serial initialization is failure-isolated from the base HTTPS service: if th
`web_ui.c` contains authored index/application strings and response policy; it validates `/api/session` before connect/restore, adds explicit Sign out, and cancels stale work on 401/logout/page exit. `web_login_ui` is a standalone public page without protected-asset dependencies. Both authentication documents and app script are no-store. Its restrictive CSP contains a hard-coded hash of the inline loader, so those two must change atomically; preserve same-origin connections, no-referrer behavior, frame denial, and the existing cache policy. `web_assets_data.c` contains checked-in generated arrays for vendored compressed xterm assets and the logo. Normal builds compile these arrays directly; they do not regenerate assets.
### Browser admin backend
8D.5 additionally supplies `web_admin_transport` and `web_admin_tickets`: one optional admin socket, two 30-second digest-only tickets bound to current originating session/principal, the same two shared console slots, no serial broker client. Ticket POST requires cookie/Origin/CSRF/admin; ordinary GET upgrade requires cookie/Origin/admin/ticket and console admission before 101. Six total HTTPS sockets remain, LRU purge is disabled, and two routes bring the handler budget to 16. Optional admin registration/PSRAM allocation failures do not take down M1. A 20 ms ESP timer queues at most one HTTPD poll, with no new task; only HTTPD accesses the 1,552-byte PSRAM-only RX/TX payload or socket IO. Notifiers close the generation-qualified console and flag the socket. HTTPD shuts down the verified current fd directly and owns subsequent read cleanup, avoiding IDF's queued reusable `sock_db *` close race. Detach fences submissions; failed stop retains ownership, and queued state is retired only after successful HTTPD stop. Console dispatcher/prompt and owner input/output/idle checks enforce session and principal currentness. WEB supports deferred self-close only; parsed canonical policy denies unsupported lifecycle/network/account mutations before handler side effects. No normal UI entry, typed settings or lifecycle parity is included. See `docs/phase8d5_implementation.md` for validation limits and exact restrictions.
### SSH
`ssh_transport` uses wolfSSH on port 22 with two fixed session/handshake slots. Initialization calls `wolfSSH_Init()` in the caller before task creation; after that, one owner task pinned to core 1 exclusively owns runtime contexts/sessions and wolfSSH calls. It enforces bounded handshakes, authentication attempts, receive work, and session buffers.
@@ -157,7 +161,7 @@ admin SSH line editor ----/ |
The dispatcher is the sole caller of `esp_console_run()`, serializing UART0 and all admin SSH commands. This is required because the console registry is treated as non-reentrant, but it also means a long command or interactive prompt blocks all administration entry routes.
Phase 8D.4 retains this module and exposes `admin_ssh_console_open_owned()`: copied transport-qualified slot/session/generation identity plus a firmware-lifetime immutable owner adapter. The existing two console slots are shared, not multiplied per frontend; active/executing slots cannot be replaced. Owners serialize per-session input, consume output and enforce transport liveness; completion scratch is claimed nonblockingly across owners. The existing control task calls drain/lifecycle adapters outside console locks. SSH implements its adapter and compatible admission entry point in `ssh_transport.c`, using generation-checked published snapshots and existing control APIs, never wolfSSH from the control task. `SELF_CLOSE` is owner-relative; legacy SSH actions remain SSH-specific and unsupported owner actions are rejected. No browser admin transport is yet implemented.
The 8D.4/8D.5 boundary retains `admin_ssh_console_open_owned()` and adds available-slot admission for runtime SSH/browser owners: copied transport-qualified slot/session/generation identity plus a firmware-lifetime immutable owner adapter. The existing two console slots are shared, not multiplied per frontend; active/executing slots cannot be replaced. Owners serialize per-session input, consume output and enforce transport liveness; completion scratch is claimed nonblockingly across owners. The existing control task calls drain/lifecycle adapters outside console locks. SSH uses generation-checked published snapshots, principal copies and its assigned console index, never wolfSSH from the control task. `SELF_CLOSE` is owner-relative; legacy SSH actions remain SSH-specific and unsupported owner actions are rejected. Dispatcher-side owner `is_current` checks run outside console locks, with full identity recheck after validation. Commands revalidate immediately before the runner; prompts revalidate before publication and after waits (250 ms polling plus check/scheduling latency), rejecting revoked submitted input and stale wakes. SSH preserves close intent through external-close consumption. Consumed output is wiped. These checks do not cancel arbitrary executing handlers or replace owner-side input/output and lifecycle validation.
For SSH, standard output/error is redirected to the invoking session's bounded output ring. `console_input` routes visible or hidden prompts to UART0 or the active SSH session. `exit` and Ctrl+D on an empty admin SSH line use bounded deferred self-disconnect after their acknowledgement drains; role-`user` SSH remains a binary-transparent serial stream. Session tokens include slot and generation so late queued work cannot attach to a reused SSH slot. Only the SSH owner task moves ring output through wolfSSH.
+8 -1
View File
@@ -74,6 +74,13 @@ This is a semantic map, not a complete file inventory. Start here, then read the
- 8D.3 UI: `src/web_login_ui.{c,h}` serves standalone `/login`; `web_ui.c` validates session before serial connect/restore and handles logout/401 safely. Both scripts hash-bound, auth documents/app no-store. Tests: `python3 tests/web_login_ui/run.py` and `python3 tests/web_ui_session/run.py`. Live cutover host-tested/build-verified, M1 validated by user sign-off (numeric reserves open): `docs/phase8d3_implementation.md`.
- Asset constraint: `web_assets_data.c` is checked-in generated input to the build; do not hand-edit or regenerate casually.
### Browser admin backend (8D.5)
- Files: `src/web_admin_transport.{c,h}`, `src/web_admin_tickets.{c,h}`, protected registration/lifecycle in `web_server.c`, revocation through `web_serial_transport_revoke_*`, diagnostics in `web_console.c`.
- Routes: CSRF-protected admin-only `POST /api/admin/ws-ticket`; ordinary `GET /ws/admin` with cookie/Origin/ticket/shared-console admission before explicit 101. No UI entry or broker client. One socket, two tickets, existing two shared console slots; six total HTTPD sockets, LRU disabled, 16 URI handlers.
- Ownership: 20 ms ESP timer queues at most one HTTPD poll, no new task; HTTPD owns 1,552 B PSRAM-only payload and IO. Closure uses HTTPD-owned `shutdown`, not IDF's reusable-pointer queued close. Detach fences submitters; only successful HTTPD stop retires queued state before restart. Session/principal currentness and generation checks protect all sensitive boundaries.
- Tests: `python3 tests/web_admin_transport/run.py --tickets`, `python3 tests/web_admin_transport/server_lifecycle.py`, `python3 tests/web_cookie_auth/run.py --admin`; manual smoke client/procedure in `tests/web_admin_transport/README.md` and `docs/phase8d5_implementation.md`. Final shutdown fix is host-tested and build-verified by the parent's sequential final `pio run`; target validation remains pending.
## SSH
**Responsibility:** authenticate SSH, route users to serial and administrators to the command dispatcher, and own wolfSSH lifecycle.
@@ -110,7 +117,7 @@ This is a semantic map, not a complete file inventory. Start here, then read the
- Ownership: dispatcher is sole `esp_console_run()` caller; the SSH owner exclusively performs post-initialization wolfSSH runtime calls.
- Lifecycle: remote session tokens include slot generation; fixed output/history/prompt state is wiped immediately on idle close or after an executing handler returns. Admin SSH `exit` and Ctrl+D on an empty command line request bounded deferred self-disconnect after best-effort output draining.
- Constraint: one slow command or prompt serializes all administration. Admin SSH is unavailable until command registration and UART frontend creation complete; supported deferred actions wait only for a bounded application-buffer drain heuristic.
- 8D.4 boundary: `admin_ssh_console_open_owned()` copies transport-qualified identity and retains an immutable firmware-lifetime drain/lifecycle adapter; the existing two slots are shared. SSH compatibility admission/adapter resides in `ssh_transport.c`; no browser admin caller yet. Owners handle liveness/output; completion scratch is nonblockingly serialized. Focused host command: `python3 tests/admin_console_boundary/run.py`.
- 8D.4/8D.5 boundary: `admin_ssh_console_open_owned()` retains explicit-index admission; runtime SSH and browser owners use `admin_ssh_console_open_available()` for the same two slots. Copied transport-qualified identity and immutable firmware-lifetime currentness/drain/lifecycle adapters; SSH publishes its allocated console index separately from its physical SSH slot. Owners handle liveness/output; dispatcher and prompt waits additionally require owner currentness (250 ms polling plus check/scheduling latency). SSH publishes locked principal copies; consumed console output is wiped. Completion scratch is nonblockingly serialized. Browser unsupported lifecycle/account mutations are rejected before execution. Focused host command: `python3 tests/admin_console_boundary/run.py`.
## Wi-Fi
+8 -2
View File
@@ -4,6 +4,12 @@ This file is working memory. Update it during active work and before handoff; do
## Development state
- **8D.5 validated by explicit user sign-off (2026-09-06):** User supplied settled cold-boot telemetry and reports successful **15-minute full-client-mix active-use soak at 230400 baud**, with a few broker drops under extremely fast/dmesg output, then explicitly closes 8D.5. Supersedes older pending/incomplete notes below. Boot internal/DMA/PSRAM free **70,876 / 63,120 / 8,246,360 B**, minima **59,560 / 51,804 / 8,242,140 B**, largest **31,744 / 31,744 / 8,126,464 B**; SSH stack minimum-free **18,472 B**. Admin initialized/attached with ESP_OK and 167/240/1,552 B static/ticket/payload accounting; no active clients at boot. Full mix/soak is user-reported, not evidenced by the idle snapshot; exact drop count/client, flashed revision and loaded/post-soak/cleanup telemetry not supplied. See `docs/phase8d5_implementation.md`. These limits do not reopen sign-off or imply zero-drop operation. Numeric reserves/runtime socket cost remain open. No new build/device/source action for sign-off. Wait for separate **8D.6** request; M2 not yet complete.
- **8D.5 backend implemented / host-tested / build-verified (2026-09-06), target pending:** Preserved interrupted tickets/transport/server/shared-console/SSH/policy/revocation work and inherited test tooling. One admin socket, two tickets, existing two shared console slots, 1,552 B PSRAM-only payload, 20 ms ESP timer/no new task, six HTTPS sockets/no LRU eviction, 16 URI handlers. Added real cookie/store/ticket/transport endpoint integration tests. Final IDF lifecycle review found queued `httpd_sess_trigger_close` retains a reusable socket-slot pointer; changed the admin path to HTTPD-owned `shutdown`, with HTTPD read cleanup and retry/reuse regressions. Transport 19 groups + tickets 12, server lifecycle 11, combined endpoints, both console suites, store/serial, cookie/parser (268), login UI (8), serial UI (9) all pass; diff check passes. **Parent reports sequential final `pio run` after the shutdown fix passed in 23.55 s at 95,580 B RAM / 1,637,273 B flash: +416/+9,224 versus prerequisite, +496/+10,100 versus 8D.4, +1,048/+37,300 versus 8D.0.** This supersedes the continuation's earlier pre-fix build; history remains in the implementation record. Parent's final independent security integration reviewer reported no actionable findings. Target ELF payload/slot/ticket sizes 1,552/80/96 B, ticket state+lock 240 B, transport static symbols 167 B before padding, ESP timer internal allocation 32 B before heap overhead. See `docs/phase8d5_implementation.md` for exact commands, inherited-versus-final fixes, resource accounting, restrictions and manual target/client checklist. Client `--help` only exercised in the continuation; this final evidence update changed documentation only, with no source/tests/build/device/network/asset/UI/upload/commit action. **Stop before 8D.6; target acceptance, runtime socket cost, numeric reserves and M2 remain open.** This supersedes the prerequisite-only pause below.
- **8D.5 prerequisite resumed / host-tested / build-verified (2026-09-06), target pending:** User requested continuation. Preserved existing uncommitted dispatcher/owner-currentness and prompt-cleanup changes in the console/SSH adapter; extended tests to production SSH snapshot/principal publication and wiping. Both console suites pass; independent production review found no actionable defects. `pio run` passes in **38.46 s**, **95,164 B RAM / 1,628,049 B flash** (**+80 / +876 B** versus recorded 8D.4); map confirms 80 B for two principal copies. No new tasks/routes/sockets/UI or backend yet. See `docs/phase8d5_implementation.md` for contract, scope split, test limits and target checklist. **8D.5/M2 incomplete.** Obtain target regression or explicit user decision before stacking live backend work. Exact next increment is still 8D.5 tickets/HTTPD-owned admin transport/protected admission/revocation/restrictions and integration checks, not 8D.6. Previous 8D.4/M1 sign-offs stand; numeric reserves remain open. This supersedes older wait-for-8D.5-request notes below.
- **8D.4 validated by explicit user sign-off (2026-09-06):** User confirms successful empty Enter and soak testing and explicitly closes Phase 8D.4. This supersedes older pending/in-progress notes below, including the 8D.3 empty-Enter target retest. Boot/full-client-mix evidence and sign-off are in `docs/phase8d4_implementation.md`. Exact soak duration/revision, post-soak/cleanup telemetry and other detailed checklist results were not supplied; these are evidence limitations, not blockers to the user-approved closure or claims of execution. Numeric reserves remain open. No new build/device operation for sign-off. Wait for a separate **8D.5** request; no browser admin backend or M2 completion is claimed.
- **8D.4 target validation in progress (2026-09-06):** User supplied clean-boot/full-client-mix UART0 evidence, recorded in `docs/phase8d4_implementation.md`. Loaded internal/DMA/PSRAM free **34,632 / 26,876 / 8,089,284 B**, minima **20,648 / 12,892 / 8,077,516 B**, largest **25,600 / 25,600 / 7,995,392 B**; SSH stack minimum-free **16,280 B** (boot **18,472 B**). Four broker clients at 115200 baud: SSH writer, USB and two web observers; user/admin SSH and both browser roles admitted successfully, no reported I/O/transport failures. One cumulative SSH broker revocation retained without diagnosis. Exact flashed revision/duration not supplied. Console-specific regressions, lifecycle/soak/cleanup and explicit phase sign-off remain pending; numeric reserves remain open. No 8D.5 request or implementation.
@@ -60,8 +66,8 @@ Based on checked-in source plus `README.md` and `docs/roadmap.md`:
## Clearly incomplete or transitional areas
- Phase 8C hardware validation passed, including route separation, shared command serialization, history/completion, prompts, output backpressure, revocation during queued work, deferred SSH lifecycle/reboot actions, and full concurrent transport operation. At 460800 baud with SSH and WebSocket clients in parallel, substantial packet drops and slow display controls were observed under load, without memory exhaustion; no baud-rate reduction is planned.
- Current HTTPS has no web-based user administration and gives both roles the same status/terminal routes.
- Browser authentication now uses cookie login/logout without Basic fallback. M1 target checkpoint is signed off; browser administrative routes remain future separately requested M2 work.
- Current shipped HTTPS UI gives both roles the same status/serial terminal. The 8D.5 admin backend is implemented without a UI entry; its temporary policy denies all user mutations and unsupported self-affecting lifecycle commands.
- Browser authentication uses cookie login/logout without Basic fallback. M1 target checkpoint is signed off; 8D.5 is implemented, host-tested and build-verified, with target gates recorded above. Browser selector/lifecycle parity remain future separately requested 8D.6/8D.7 work, not completed M2.
- NVS encryption, secure boot/flash encryption review, production certificate/provisioning policy, and OTA are not implemented. HTTPS login has a bounded global five-verifications/60-second throttle, not comprehensive cross-transport DoS protection.
## Known inconsistencies
+3 -1
View File
@@ -82,12 +82,14 @@ Phase 8D.2 adds a third identity: non-reused 64-bit originating web-session IDs
**Consequence for future changes:** Actions that would invalidate their own SSH transport should integrate with deferred control when acknowledgement preservation matters. Prevent new input while an action is pending, keep the wait bounded, and do not describe it as guaranteed delivery.
Phase 8D.4 routes drain/lifecycle operations through a firmware-lifetime immutable owner adapter on the existing control task, outside console locks. Tokens include a transport namespace; owners revalidate full identity and marshal to their transport APIs. `SELF_CLOSE` targets the invoking frontend while existing SSH action meanings remain unchanged. Unsupported actions must fail before side effects. The two console slots remain a shared bounded pool, with no hypothetical browser capacity allocated.
Phase 8D.4 routes drain/lifecycle operations through a firmware-lifetime immutable owner adapter on the existing control task, outside console locks. Tokens include a transport namespace; owners revalidate full identity and marshal to their transport APIs. `SELF_CLOSE` targets the invoking frontend while existing SSH action meanings remain unchanged. Unsupported actions must fail before side effects. The two console slots remain a shared bounded pool, with no hypothetical browser capacity allocated. The 8D.5 prerequisite additionally requires owner currentness on the dispatcher, outside console locks, before commands and during prompts; account currentness alone cannot establish originating browser-session liveness. Recheck token identity after external validation, reject revoked submitted replies, and wipe consumed output. Polling is not a hard cancellation deadline and cannot roll back arbitrary handlers; owners retain admission/input/output/lifecycle responsibilities.
**Relevant files:** `src/admin_ssh_console.c`, `src/system_console.c`, `src/ssh_console.c`, `src/ssh_transport.c`
## Authentication uses copied principals and fail-safe currentness checks
**8D.5 web owner extension:** Browser and runtime SSH admission allocate from the same two console slots; a physical SSH slot is not a console index. WEB supports owner-relative self-close only and rejects unsupported network/lifecycle/account mutations at parsed command policy before execution. One web-admin socket and two tickets do not increase six-socket HTTPD capacity; disable LRU rather than evict retained serial clients. The optional owner uses one PSRAM-only payload and ESP timer scheduling, not a new task. HTTPD alone sends/shuts down its verified current fd. Do not use IDF's queued raw-`sock_db *` close from admin polling: free/reuse before that work executes could close a replacement. Detach must fence queue submissions before HTTPD stop; retire queued markers only after successful stop, retaining ownership across failures. No browser UI or generic HTTP command runner is part of this boundary.
**Decision:** Network sessions retain secret-free copied principals. Account mutations invalidate generations/IDs; after commit, the command layer requests best-effort targeted transport revocation, while ongoing currentness checks are authoritative.
**Rationale/evidence:** `user_database` issues principals without secrets; web/SSH check currentness during admission and active sessions. Mutating console paths call transport revocation hooks.
+146
View File
@@ -0,0 +1,146 @@
# Phase 8D.5 — Admin WebSocket backend
Status (2026-09-06): **8D.5 backend implemented / host-tested / build-verified / validated by explicit user sign-off.** The user supplied settled cold-boot telemetry and reported a successful 15-minute full-client-mix active-use soak at 230400 baud, with a few broker drops under heavy output, and explicitly closed 8D.5. Final build: **23.55 s**, **95,580 B RAM / 1,637,273 B flash**. No 8D.6 UI work or M2 acceptance. Prior 8D.4/M1 sign-offs stand; numeric resource reserves remain open.
## Target Sign-Off (2026-09-06)
User reports: "With full client mix, running and active use for 15 mins, soaked, only a few dropped broker packets at 230400 baud with extremely fast and dmesg output. Mark 8D.5 as validated."
This is explicit phase acceptance and supersedes older pending/incomplete validation statements below and in project memory. The reported broker drops are preserved, not treated as zero-drop or byte-integrity evidence; no cause, exact count or affected client was supplied. This 230400-baud workload is distinct from earlier 115200-baud samples. No baud-rate or capacity reduction is made.
Settled cold-boot UART0 measurements supplied with sign-off:
| Heap (bytes) | Free | Minimum-free | Largest block |
|---|---:|---:|---:|
| Internal 8-bit | 70,876 | 59,560 | 31,744 |
| Internal DMA | 63,120 | 51,804 | 31,744 |
| External PSRAM | 8,246,360 | 8,242,140 | 8,126,464 |
SSH owner stack: **20,480 B configured / 18,472 B minimum-free**. Internal/DMA capabilities overlap; their free bytes are not additive. Minimum-free is the firmware's conservative sum of matching heap regions' lifetime minima.
- HTTPS and SSH initialized/running with ESP_OK, each with one successful start and zero startup failures. mDNS initialized/announced with ESP_OK.
- Admin backend initialized/attached with ESP_OK, no active admin socket or tickets, and all admin counters zero. Reported transport static/ticket/PSRAM payload storage **167 / 240 / 1,552 B**, matching implementation accounting.
- No SSH, cookie or serial WebSocket sessions, challenges, tickets or broker clients. Web/SSH traffic/authentication/failure counters zero at boot; this does not describe post-soak counters.
- UART service stopped, RS-232 owner idle, configuration **230400 8N1/no flow**, RX/TX pending zero. USB initialized/attached but host closed with DTR/RTS false and no broker client. Diagnostic 9600 host line coding does not reconfigure UART1.
Exact flashed revision, settling duration, loaded/post-soak/cleanup memory and counters, individual client identities and detailed checklist results were not supplied. The full client mix and 15-minute successful soak are user-reported, not reconstructed from the idle boot sample. Missing details remain evidence limitations, not blockers to user-approved phase closure or claims of unreported test execution. Runtime per-admin-socket cost and numeric reserve approval remain open. No agent build, device operation or source change was performed to record sign-off. **Next is 8D.6 only on a separate request; M2 remains incomplete.**
## Combined backend completion
The user explicitly authorized finishing the entire interrupted 8D.5 implementation, superseding the prerequisite pause below. Extensive uncommitted source/tests were preserved: ticket store, transport, protected server routes, shared-console allocator and SSH mapping, command restrictions, revocation integration, diagnostics, transport regressions, lifecycle harness and local smoke client. This continuation reviewed them, added real authenticated endpoint integration tests and fixed the final admin socket close/reuse race. No upload, erase, commit, generated asset or normal UI change.
### Admission and protocol
- `POST /api/admin/ws-ticket` runs the existing strict cookie/Origin/CSRF mutation policy, then admin-role validation. Two digest-only, non-evicting, single-use tickets expire after 30 seconds and bind the originating session ID and full current password principal. Crypto/database checks are outside critical sections; epoch/generation checks reject stale publication, consumption and prune work.
- `GET /ws/admin?ticket=<64 hex>` is an ordinary HTTP route, not an automatic WebSocket route. Cookie, strict Origin, current admin role, exact ticket shape/consumption, one transport-slot reservation and a free shared console slot precede explicit 101. Upgrade has no CSRF header requirement: the CSRF-protected ticket plus cookie/Origin authorizes it, including browser clients that cannot add custom WebSocket headers. Rejections never execute console commands.
- Exactly one admin socket, two admin tickets and the existing two shared console slots. SSH now retains its allocated console index separately from the physical SSH slot and resolves published owner state by full identity. Busy/executing console slots cannot be replaced. Admin does not join the serial broker or obtain a writer lease.
- Final unfragmented binary frames carry console input (maximum 512 bytes); binary output chunks are at most 1024 bytes. Text, fragmented, oversized and overlapping pending input fail closed. Partially consumed input has a five-second deadline, checked before retry. Consumed input/output and retired payload/console state are wiped. Empty binary frames must not invoke IDF's zero-length header probe twice.
- Saturating lifetime admin counters and allocation sizes are available through `web status`/`web counters`, without token, CSRF, verifier or private-key disclosure. `web clear-counters` does not reset admin counters; diagnostics say so.
### Ownership and failure isolation
- One permanent 20 ms ESP timer schedules at most one HTTPD poll. It does no database, console, payload or socket work. No new application task/stack/dispatcher is created. Blocking HTTPD queue-work configuration makes the optional admin initializer fail closed.
- HTTPD exclusively owns admission, frame input, payload mutation, output sends and session-context cleanup. Revocation/control callers only flag closure and close the generation-qualified console token. Authoritative session/principal checks guard admission, dispatcher execution/prompts, input, output and idle polls. These checks cannot roll back arbitrary already-running commands.
- Detach disables admission/tickets and console access before fencing timer submissions for up to two seconds. Timeout retains the live HTTPD handle and requires a stop retry. Failed SSL stop retains admin ownership; only successful HTTPD stop permits clearing queued-work state and reattachment. Queued polls after detach do no IO; successfully stopped HTTPD cannot execute discarded work.
- **Final lifecycle fix:** installed IDF 5.5.0 `httpd_sess_trigger_close()` queues a raw reusable `sock_db *`. A poll could queue closure, then a frame error free that slot and acceptance reuse it before the queued close executes, potentially closing an unrelated serial client. Admin polling now calls `shutdown(fd, SHUT_RDWR)` directly on HTTPD after checking its session context. HTTPD's next read owns deletion; there is no late queued close pointer. Failed shutdown retries on later polls, with send-failure accounting. This deliberately does not promise a graceful WebSocket close frame or peer delivery. The existing serial transport's use of IDF queued close was not changed; the new admin path cannot introduce this eviction route.
- HTTPS retains six client sockets, now with LRU purge disabled, and grows from 14 to 16 URI handlers. Full socket capacity can delay/refuse new HTTP/TLS connections rather than evict a retained serial writer. Optional admin allocation/registration failure preserves M1 routes and serial attachment; failed optional-ticket unregister leaves an authenticated but unattached/unavailable ticket handler, not a bypass.
- Logout invalidates its cookie session before serial/admin ticket/socket cleanup; account/global revocation follows the same order through the existing `web_serial_transport_revoke_*` integration hooks. Lost notifications still fail session/principal currentness. Unrelated session notifications do not close the admin socket.
### Temporary command restrictions
The parsed canonical command policy rejects unsupported actions before `esp_console_run()`, not after a handler has mutated configuration. From web: only `web status`, `wifi status`, `mdns status`; only bare `user`, `user status`, `user list`, `user show <name>` in the user group; no `reboot`, SSH stop/disconnect/reset or SSH host-key action except `ssh host-key info`. Thus web/network identity changes, all account mutations and one-time generated credentials remain unavailable here until the later lifecycle phase. Ordinary permitted commands, empty Enter and `exit`/empty-line Ctrl+D use the existing dispatcher/editor. Only owner-relative deferred self-close is supported by WEB. UART0 bootstrap/recovery remains physical-only; SSH policy otherwise remains unchanged. See the policy suite for quoted forms.
## Final local validation
All commands below were executed in this continuation and passed. Host compiler warnings are errors; tests are deterministic dependency interleavings, not real multicore execution.
| Command | Actual result |
| --- | --- |
| `python3 tests/web_admin_transport/run.py --tickets` | 19 transport groups plus 12 ticket groups; rerun after shutdown fix |
| `python3 tests/web_admin_transport/server_lifecycle.py` | 11 groups, including all 16 required registration failure positions, two optional positions, failed unregister, failed stop/retry and six-socket/no-LRU configuration |
| `python3 tests/web_cookie_auth/run.py --admin` | Real cookie policy, parser, store, tickets, transport and private adapter linked together; endpoint rejection before 101, cross-session replay burn, admission, isolated logout, missed account revocation, expiry and restart; rerun after fix |
| `python3 tests/web_cookie_auth/run.py` | Existing cookie/HTTPD policy and embedded store regressions pass |
| `python3 tests/admin_console_boundary/run.py` | Shared two-owner allocation, production SSH publication/mapping, queued currentness, prompts, deferred actions, history/completion and wiping pass |
| `python3 tests/admin_ssh_policy/run.py` | SSH policy and browser restrictions using installed IDF parser pass |
| `python3 tests/web_session_store/run.py` | Store API/failure/race suite passes with OpenSSL SHA-256 |
| `python3 tests/web_session_store/run.py --serial` | Serial/session binding, revocation, races and non-eviction regressions pass |
| `python3 tests/web_auth_parse/run.py` | 268 cases, zero failures |
| `python3 tests/web_login_ui/run.py` | C/header/CSP checks and eight browser-behavior groups pass |
| `python3 tests/web_ui_session/run.py` | C/header/CSP checks and nine browser-behavior groups pass |
| `python3 tests/web_admin_transport/client.py --help` | Local import/CLI smoke only; no network/device operation |
| `git diff --check` | Pass |
The combined endpoint harness doubles console execution/IO and the logout revocation hook (matching reviewed production ordering); the console harness separately runs real shared-console code. Installed HTTPD getter/setter/pending-reader functions are extracted, but TLS, handshake writes, actual HTTPD event processing and FreeRTOS are doubled. The server lifecycle harness extracts production lifecycle/table code, not live HTTPD. No sanitizer run/pass is claimed in this continuation; inherited harness notes record missing ASan/UBSan libraries. Manual client offline evidence in its README is inherited, not rerun here beyond `--help`.
### Firmware and resources
**Final build, parent-reported:** the necessary sequential `pio run` after the shutdown fix **passed in 23.55 seconds**, reporting **95,580 B linked RAM / 1,637,273 B flash** on the existing PlatformIO espressif32 6.12.0 / ESP-IDF 5.5.0, N16R8 release configuration. This verifies the final source, including the shutdown fix. Parent also reports the final independent security integration review found **no actionable findings**. This documentation-only follow-up ran no build or tests.
Historical build: the continuation's one 120-second-bounded `pio run` passed in **22.67 seconds**, at **95,580 B RAM / 1,637,277 B flash**, before the shutdown fix. Both affected production-C host suites passed after the fix; the parent's subsequent final build supersedes that earlier image for final-source verification and saves 4 B flash with unchanged linked RAM.
Final-image deltas (baselines not rebuilt):
| Baseline | RAM delta | Flash delta |
| --- | ---: | ---: |
| 8D.5 prerequisite: 95,164 / 1,628,049 B | +416 B | +9,224 B |
| 8D.4: 95,084 / 1,627,173 B | +496 B | +10,100 B |
| 8D.0: 94,532 / 1,599,973 B | +1,048 B | +37,300 B |
Target ELF/DWARF/map inspection, with no device access: admin payload **1,552 B PSRAM-only** (512 RX + 1024 TX + 16 metadata), slot **80 B**, ticket **96 B** x two, ticket state **232 B** + lock **8 B** = **240 B**. Transport static symbols total **167 B** before placement padding (168 B occupied); retained timer handle is included there. IDF `struct esp_timer` is **32 B**, allocated with internal/8-bit capabilities, excluding heap metadata. Payload/timer persist across HTTPS restarts; PSRAM allocation has no internal fallback. SSH adds two console-index bytes in published state and retains the prerequisite's 80 B principal copies. No added console rings, queue capacities or task stacks. Dynamic TLS/socket/request allocations, heap fragmentation, HTTPD/dispatcher/timer stack margins and internal/DMA reserves remain unmeasured; these static figures are not per-socket runtime cost or reserve approval.
## Target Regression Procedure
No target/network exercise was performed by the agent. The user's target sign-off is recorded above; this original checklist is retained as regression coverage, not as outstanding gates to that closure. Use the already present bounded, stdlib-only `tests/web_admin_transport/client.py`; full usage/security caveats are in its README. It prompts for credentials without echo, keeps cookies only in memory, never logs tickets/CSRF/credential metadata, and attempts logout in `finally`. Prefer trusted certificate/hostname validation; `--insecure` is explicit test-only exposure to active interception, not a local-routing guarantee. Its console output is intentionally raw terminal output: use a trusted target and do not record secret-bearing command output.
```sh
python3 tests/web_admin_transport/client.py --url https://device.local --cafile device-cert.pem --smoke --max-runtime 60
```
1. Have the operator flash the final build-verified image through the usual approved procedure. Record exact revision/diff, clean-boot and 60-second settled `memory`, `web status`, `ssh status` and broker/serial counters. Build verification does not establish target acceptance.
2. Run the smoke client separately with disposable role-user and role-admin accounts. Require user ticket 403; admin cookie/ticket/101, same-ticket replay 403, `help`, empty frame, empty Enter and `exit`, then logout and session 401. Repeat five times per role. No automatic credential retry; respect the five/60-second throttle.
3. Keep two browser serial sockets, USB and role-user SSH at 115200 baud, with a known sole writer; concurrently admit one admin SSH plus web admin. Verify the same broker client IDs/writer before and after web admin open/exit/failure. Attempt a second admin socket and fill both console slots with SSH before web admission: reject, never replace. Fill remaining HTTPS sockets; no serial eviction. Capture live TLS/heap cost rather than infer it from six configured sockets.
4. With a temporary authenticated development client (not a firmware endpoint), test missing/foreign/null/duplicate Origin, missing/duplicate cookie, absent/wrong CSRF on ticket POST, expired/wrong-session/replayed tickets and direct user-role upgrade. Require rejection before any 101. Check raw responses without publishing auth headers or ticket URLs. The supplied smoke client only automates the documented subset, not this full negative matrix.
5. Exercise shared command serialization with UART0 and admin SSH, completion/history, visible/hidden/cancelled prompts, disconnect/revoke/expiry while queued or prompting, and slow input/output. Use disposable secrets and approved existing non-restricted commands; do not type secrets into a retained browser developer-console history. Unsupported web lifecycle/account mutations must report rejection before any state change. The supplied smoke client is not interactive and does not claim prompt/completion coverage.
6. Logout one session with serial+admin; only that session's sockets/tickets close. Change its disposable account from UART0/admin SSH, test deletion/recreation and let a session reach its one-hour absolute expiry. Verify unrelated sessions, queued-command rejection, no stale prompt/output after slot reuse, and no lingering reserved console slot after an executing handler returns.
7. From UART0, repeat five HTTPS stop/start cycles with active admin and pending output; inject detach/queue/SSL-stop failures where feasible, retry stop and ensure no handle reuse until successful stop. Stress simultaneous peer disconnect and new serial admission during admin closure, specifically validating the shutdown/reuse fix. USB and UART0 must remain usable if HTTPS is unavailable.
8. Run at least a 15-minute full-client-mix/slow-reader soak, collect free/minimum/largest internal/DMA/PSRAM and available stack telemetry, disconnect all optional clients, wait 60 seconds and compare cleanup figures. Numeric reserve floors and real per-admin-socket cost still need approval/evidence. Stop before 8D.6; M2 also requires separately requested 8D.6/8D.7 work.
## Historical prerequisite record
The sections below record the earlier prerequisite-only checkpoint. Their no-backend statements and request to pause were superseded by the combined backend authorization/results above; their old build measurements are retained as provenance.
## Scope and provenance
Resumed at revision `e5dce12ed43154dacd086437de0f2d156014d58c` with existing uncommitted prerequisite changes in `src/admin_ssh_console.{c,h}`, `src/ssh_transport.c`, and `tests/admin_console_boundary/`. Preserved and reviewed that work, extended the harness to exercise production SSH snapshot/principal publication and wiping, ran both console suites and the firmware build, and recorded the handoff.
The plan's work-unit review splits the full ticket store, socket owner, HTTP policy/routes and integration tests from this runtime-changing prerequisite. No browser route, ticket store, new task, UI entry, broker client, generated asset, persistence change or device operation is included. No commit or branch change was made.
## Implemented contract
- The immutable console owner adapter now requires `is_current(token, principal)`. It runs on the dispatcher outside console locks, validates full transport identity and originating-session/principal binding, and must not call socket libraries or console handlers. Admission and transport input/output liveness remain owner responsibilities; admission need not already be published to this callback.
- Core checks account and owner currentness for queued work, again immediately before the canonical command runner, and after dispatch. Identity/owner are rechecked after external calls so late validation cannot close a replacement slot. UART0 remains independent.
- Visible and hidden prompts check currentness before publishing and after each wait. Waits poll at 250 ms plus validation/scheduling latency, not a hard real-time deadline; stale semaphore wakes cannot submit a still-waiting prompt. Revoked submitted replies are not returned to handlers. Close wipes prompt input immediately, including submitted input; executing session storage remains reserved until handler cleanup.
- SSH publishes two copied principals under the same lock as its snapshots. Its dispatcher adapter validates active authenticated admin route, transport/session/generation, close intent and full principal binding, without reading owner-task slots or calling wolfSSH. Consuming an external close preserves published close intent until cleanup.
- Reading console output wipes consumed ring segments, including wraparound, while retaining unread output.
These checks do not cancel or roll back arbitrary executing handlers, nor make authorization atomic with subsequent side effects. Database currentness can wait on its mutex. A future browser owner must still enforce cookie-session expiry/logout/revocation at admission, input, output and periodic cleanup; console polling is not a replacement for transport cleanup.
## Executed validation and resources
- `python3 tests/admin_console_boundary/run.py`: PASS. Production console plus extracted production SSH token/publication/adapter code; covers stale owner with current account, unrelated-session isolation, close/reuse during validation, invalidation immediately before execution, revoked/disconnected submitted prompts, unanswered prompt expiry without notification, stale wakes, UART0 recovery, output wiping, published principal cleanup, route/auth/principal/identity rejection and external-close handoff. Existing completion/history, prompts, deferred control and backpressure regressions also pass.
- `python3 tests/admin_ssh_policy/run.py`: PASS, including empty input, ordinary commands and quoted physical-only restrictions.
- Independent static review of the production prerequisite found no actionable defects. Final `git diff --check`: PASS.
- `pio run`: PASS in **38.46 s**, PlatformIO espressif32 6.12.0 / ESP-IDF 5.5.0, N16R8 release. **95,164 B linked RAM / 1,628,049 B flash**. Versus recorded 8D.4 (95,084 / 1,627,173): **+80 / +876 B**. Versus recorded 8D.0 (94,532 / 1,599,973): **+632 / +28,076 B**. Baselines were not rebuilt.
- Link map attributes 80 B (`0x50`) to `s_console_principals`. The owner callback adds code/read-only adapter storage, no per-session payload. Two shared console slots, two 4 KiB output rings, four-entry request queue, two-entry control queue, four-line history and task stacks (dispatcher 12 KiB, UART0 6 KiB, control 4 KiB) are unchanged. No new module heap/PSRAM allocation or increased socket/TLS/HTTP handler/session/ticket capacity; actual web-admin socket/slot cost is not yet available.
Host fakes are deterministic, not concurrent: locks are counters, waits use hooks, console execution/lifecycle operations are doubled. Production publication is now exercised, but the complete SSH owner loop, real FreeRTOS scheduling, task-local stdio, socket behavior and runtime memory/stack margins are not proven. No sanitizer or target pass is claimed.
## Target checkpoint and exact next increment
Before stacking the live backend on this runtime-changing prerequisite, obtain target regression or an explicit user decision to proceed under the plan:
1. Boot and capture UART0 status/`memory` and SSH stack telemetry. Confirm admin SSH empty Enter, commands, history/completion, visible/hidden/cancelled prompts and `exit`/Ctrl+D.
2. Disconnect or revoke an admin SSH account with work queued and while a prompt waits; confirm prompt cancellation, dispatcher/UART0 recovery, no reply/output crossover after reconnect, and isolation of unrelated sessions. Only use disposable test accounts and approved mutations; do not publish secrets.
3. Run browser login/serial disconnect/reconnect, native USB UART1 and user/admin SSH smoke at the supported 115200-baud workload. Repeat five serial lifecycle cycles per role and compare settled/full-client-mix/cleanup heap and SSH stack telemetry. Check slow readers do not compromise UART0 recovery.
Next implementation remains **8D.5**, not 8D.6: bounded admin-only digest tickets bound to current cookie session/principal; coordinated admission to the existing two shared console slots; a separate bounded admin transport using HTTPD-owned socket work; full pre-101 Origin/cookie/ticket admission; session/account/global invalidation and authoritative liveness checks; fail-before-side-effect restrictions for unsupported self-affecting commands; counters and focused authenticated integration checks. Review scope and socket/allocation budgets before coding and split further if needed. No normal UI entry or generic HTTP command runner. Admission must never evict a serial client/writer. Test real two-WebSocket coexistence before claiming M2 capacity or acceptance.
+9 -3
View File
@@ -1,6 +1,6 @@
# Phase 8D — Incremental web administration plan
Status: **8D.08D.2 validated by user sign-off on 2026-09-05; 8D.3/M1 validated by explicit user sign-off on 2026-09-06 after post-soak evidence. Live login/logout is implemented, host-tested and build-verified. 8D.4 validated by explicit user sign-off on 2026-09-06, confirming successful empty Enter and soak testing. Numeric reserve gates remain open. 8D.58D.22 remain planned, each requiring a separate implementation request.** See the [8D.4 implementation record](phase8d4_implementation.md), [8D.3 implementation record](phase8d3_implementation.md) and [8D.0 baseline/M1 contract](phase8d_baseline.md).
Status: **8D.08D.5 and M1 validated by explicit user sign-off. 8D.5 closed on 2026-09-06 after settled cold-boot evidence and a reported successful 15-minute full-client-mix soak at 230400 baud, with a few broker drops under heavy output. Numeric reserve gates remain open. 8D.68D.22 remain planned, each requiring a separate implementation request.** See the [8D.5 implementation record](phase8d5_implementation.md), [8D.4 implementation record](phase8d4_implementation.md), [8D.3 implementation record](phase8d3_implementation.md) and [8D.0 baseline/M1 contract](phase8d_baseline.md).
This is the execution plan for [roadmap Phase 8D](roadmap.md#phase-8--role-based-users-and-administrative-access). The roadmap retains the feature/security requirements; this document defines small work units, dependencies, and release gates. The [administration test matrix](user_administration_tests.md#planned-phase-8d-integrated-web-administration) remains the final acceptance checklist.
@@ -112,6 +112,12 @@ If 8D.3 exceeds the work-unit limit, first split out inert login-page rendering
### 8D.5 — Bounded admin WebSocket backend, no normal UI entry yet
**Target sign-off (2026-09-06):** User explicitly validates 8D.5 after settled cold-boot telemetry and successful 15-minute full-client-mix active-use soak at **230400 baud**, reporting a few broker drops under extremely fast/dmesg output. [Evidence and limitations](phase8d5_implementation.md#target-sign-off-2026-09-06). This supersedes the older pending acceptance/checkpoint notes below. No zero-drop claim or loaded/cleanup telemetry is inferred. Numeric reserves remain open; missing detailed results do not reopen signed-off 8D.5. Wait for a separate 8D.6 request; M2 is not yet complete.
**Combined backend checkpoint (2026-09-06):** User authorized finishing all of 8D.5, superseding the prerequisite-only pause below. Backend, shared-console allocation, protected admission, revocation, fail-before-side-effect restrictions and local/manual test tooling are **implemented / host-tested / build-verified**. All relevant host suites pass, including real cookie/store/ticket/transport endpoint integration. Parent reports the sequential final `pio run` after the HTTPD-owned shutdown/reuse fix passed at **95,580 B RAM / 1,637,273 B flash**, **23.55 s**. Deltas: **+416/+9,224 B** versus prerequisite, **+496/+10,100 B** versus 8D.4, **+1,048/+37,300 B** versus 8D.0. Final independent security integration review reported no actionable findings. **Target runtime/socket measurements, numeric reserves and acceptance remain pending.** No device operation or 8D.6/UI/M2 completion. Details and historical build evidence: [implementation record](phase8d5_implementation.md). 8D.6 onward remain separately requested work.
**Preparatory checkpoint (2026-09-06):** [8D.5 prerequisite, validation and handoff](phase8d5_implementation.md). Resumed existing uncommitted console-owner currentness/prompt cleanup work; reviewed and extended production-publication tests. Both console suites and `pio run` pass: **95,164 B RAM / 1,628,049 B flash**, **+80 / +876 B** versus recorded 8D.4. No routes/tasks/sockets/UI added. Work-unit review keeps the live backend in the next increment within 8D.5; backend/M2 remain incomplete. Target regression or explicit user decision is needed before stacking runtime changes; numeric reserves remain open.
**Start in:** The console boundary from 8D.4, `src/web_server.{c,h}`, and a small web-admin transport adapter as justified. Reuse existing HTTPD scheduling patterns without mixing admin data into the serial transport.
**Scope:** Admin-only, short-lived single-use tickets bound to both current web session and principal; bounded console admission/input/output; session expiry/logout/revocation cleanup. HTTPD owns socket work and the dispatcher owns command execution. No broker client for this route. An absent UI is not authorization: every ticket, upgrade, and sensitive operation is checked on the server. For self-affecting web actions not safely supported yet, explicitly reject before side effects and list the temporary restrictions for 8D.7.
@@ -169,11 +175,11 @@ Update the roadmap and user/command documentation to distinguish completed featu
## Progress and next-request template
Progress: **8D.08D.4 and M1 validated by user sign-off; numeric reserves remain open. 8D.58D.22 planned.** Record incremental results in `docs/agent/current-state.md`, retaining the [baseline](phase8d_baseline.md) and cumulative resource measurements as work proceeds. The baseline records user-provided evidence and sign-off; this does not imply completion of later browser-authentication acceptance checks.
Progress: **8D.08D.5 and M1 validated by user sign-off; numeric reserves remain open. 8D.68D.22 planned.** Record incremental results in `docs/agent/current-state.md`, retaining the [baseline](phase8d_baseline.md) and cumulative resource measurements as work proceeds. The baseline records user-provided evidence and sign-off; this does not imply completion of later browser-authentication acceptance checks.
Suggested next request:
> M1 and 8D.4 are validated by explicit user sign-off. Work on Phase 8D.5 only: verify the recorded prerequisites, then implement the bounded admin WebSocket backend without a normal UI entry. Preserve the single dispatcher, existing serial ownership, signed-off evidence and open numeric reserve gates.
> M1 and 8D.5 are validated by explicit user sign-off. Work on Phase 8D.6 only: implement the browser terminal selector while preserving the serial connection, broker identity and writer lease when hidden. Preserve signed-off evidence and open numeric reserve gates; do not implement 8D.7 lifecycle parity or settings.
For later chunks:
+1 -1
View File
@@ -199,7 +199,7 @@ Implementation sequence:
- Keep SFTP, SCP, `exec`, forwarding, subsystems, and unauthenticated shells disabled.
- Target-hardware validation passed for route separation, history/Tab editing, interactive visible/hidden prompts, output/backpressure, generated and entered user/password/key management including the longest ECDSA P-256 import, ping event routing, deferred reboot/SSH lifecycle drain behavior, bootstrap/recovery rejection, targeted self/other-user revocation during queued work, UART0/SSH administration serialization, and concurrent USB/WebSocket/user-SSH/admin-SSH operation. Stress at 460800 baud with SSH and WebSocket clients caused substantial expected packet drops and slower display controls, but did not exhaust memory or require lowering the supported baud-rate range.
4. **Phase 8D — Integrated web administration — Planned, staged delivery**
- **Implementation checkpoint:** 8D.08D.4 and M1 validated by user sign-off; M1 explicitly closed on 2026-09-06 after post-soak evidence, and [8D.4 console boundary](phase8d4_implementation.md) closed the same day with successful empty Enter and soak testing. [8D.3 live login/logout](phase8d3_implementation.md) is implemented, host-tested and build-verified with cookie sessions and no Basic fallback. Numeric reserves remain open. Browser admin shell/settings remain planned; begin 8D.5 only on a separate implementation request.
- **Implementation checkpoint:** 8D.08D.5 and M1 validated by user sign-off. [8D.5 admin WebSocket backend](phase8d5_implementation.md) closed on 2026-09-06 after settled cold-boot telemetry and a reported successful 15-minute full-client-mix soak at 230400 baud, with a few broker drops under heavy output. Backend is implemented, host-tested and build-verified without a normal UI entry: **95,580 B RAM / 1,637,273 B flash**. Numeric reserves remain open. Browser selector/lifecycle parity and settings remain planned; wait for separately requested 8D.6. M2 is not yet accepted.
- **Execution plan:** [Phase 8D incremental plan](phase8d_plan.md). Implement one numbered chunk per request, with a build, focused regression checks, memory accounting, and a handoff before stopping. The requirements below describe the final scope, not one implementation task.
- **Milestones:** 8D.08D.3 establish a measured baseline and reliable login/logout with the existing serial UI; 8D.48D.7 add the shared browser admin shell and verify retained serial ownership; 8D.88D.21 add typed settings and contextual controls one domain at a time; 8D.22 performs final integration acceptance. Login and runtime-memory target validation gate the first two milestones; do not defer them until the entire phase is implemented. No wholesale import of the rolled-back experimental implementation.
- Begin with integrated authentication: replace browser-facing HTTP Basic authentication with a same-origin HTTPS login page, explicit logout, and bounded opaque server-side sessions. Store only a digest of each random session token with a copied secret-free principal, expiry, CSRF state, and authentication-generation binding. Send the raw token only in a host-only `__Host-` cookie with `Secure`, `HttpOnly`, `SameSite=Strict`, `Path=/`, no `Domain`, and an explicit lifetime; never retain passwords, Basic headers, raw tokens, verifiers, or SSH-key blobs in snapshots or logs.