Add SSH Console Ownership Boundary (Phase 8D.4)
Implement transport-qualified session identity and immutable owner adapters for SSH console lifecycle and output-drain operations. Add focused host tests covering admission, stale identities, deferred actions, completion races, prompts, backpressure, and slot reuse. Update Phase 8D documentation and current-state tracking.
This commit is contained in:
@@ -157,6 +157,8 @@ 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.
|
||||
|
||||
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.
|
||||
|
||||
Admin SSH `exit`, remote reboot, SSH stop/disconnect, and host-key rotate/reset use deferred control. The control task waits up to ten seconds for command state plus administration and transport application buffers to clear, then adds a short delay; this is a bounded best-effort heuristic, not peer-delivery confirmation. UART0 invokes these actions synchronously. User mutations and their revocations are not part of this mechanism. UART0 linenoise and the SSH editor consume the same manually maintained completion matcher and candidate formatter, so the two administration routes cannot drift in offered or displayed ambiguous completions; the hints can still drift from command registration and are not an authorization list.
|
||||
|
||||
@@ -110,6 +110,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`.
|
||||
|
||||
## Wi-Fi
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ This file is working memory. Update it during active work and before handoff; do
|
||||
|
||||
## Development state
|
||||
|
||||
- **8D.4 implemented / host-tested / build-verified (2026-09-06), target pending:** User separately requested the small console boundary. Added transport-qualified identity and immutable output-drain/lifecycle owner callbacks in `admin_ssh_console`, retained the SSH admission API in `ssh_transport`, and kept two shared slots, one dispatcher and all capacities/stacks. Shared completion scratch now has nonblocking ownership; occupied-slot replacement and deferred input fail closed. `python3 tests/admin_console_boundary/run.py` and existing policy suite pass; independent review found no defects. `pio run` passes in **43.61 s**, **95,084 B RAM / 1,627,173 B flash** (**-424 / +1,448 B** versus recorded latest 8D.3). See `docs/phase8d4_implementation.md` for contracts, host-test limits, accounting and target checklist. No browser admin routes, uploads or 8D.5 work. Prior M1 sign-off stands; numeric reserves and 8D.4 target regression remain pending. Stop for validation/user decision before separately requested 8D.5.
|
||||
|
||||
- **8D.3/M1 completed by explicit user sign-off (2026-09-06):** User closes M1 after post-soak evidence, superseding all older pending/blocked/in-progress statements below. Four broker clients remain active at 115200 baud (SSH writer; USB/two web observers), both SSH routes and two cookie sessions active. Post-soak internal/DMA/PSRAM free **38,656 / 30,900 / 8,138,320 B**, minima **13,756 / 6,000 / 8,072,744 B**, largest **25,600 / 25,600 / 7,995,392 B**; SSH stack minimum-free **16,288 B**. Minima/largest unchanged from earlier loaded sample; no reported transport failures. Record/evidence limits in `docs/phase8d3_implementation.md`. Sample is still connected, not disconnected cleanup; exact soak duration/revision not supplied. Unrecorded detailed checks do not reopen signed-off M1 or become claims of execution. Numeric reserves remain open; empty-Enter fix target retest remains unrecorded separately. **Wait for a separate 8D.4 request; no M2 implementation performed.**
|
||||
|
||||
- **8D.3 both-role target login confirmed / mixed-client evidence (2026-09-05):** User reports successful HTTPS user+admin login after Origin fix; previous login blocker is resolved. Settled internal/DMA/PSRAM free **71,204 / 63,448 / 8,247,744 B**. Mixed load free **33,868 / 26,112 / 8,089,060 B**, minima **13,756 / 6,000 / 8,072,744 B**, largest **25,600 / 25,600 / 7,995,392 B**; SSH stack minimum-free **16,288 B**. 115200 baud, four broker clients (SSH sole writer, USB + two web observers), user/admin SSH active. No reported web transport or SSH I/O errors; 6 login attempts/3 invalid credentials/1 logout, zero security rejections. Two identical loaded heap samples are not a soak/leak or reserve proof. Full details/provenance in `docs/phase8d3_implementation.md`. **M1 validation in progress, not signed off.**
|
||||
|
||||
@@ -82,6 +82,8 @@ 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.
|
||||
|
||||
**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
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# Phase 8D.4 - Small Console Boundary
|
||||
|
||||
Status (2026-09-06): **Implemented / host-tested / build-verified; target validation pending.** M1 and previous phase sign-offs stand. Numeric reserve gates remain open. No 8D.5 implementation or browser admin route is included.
|
||||
|
||||
## Scope And Contract
|
||||
|
||||
Starting revision: `f9ee6eec9cbe06fe5120ee718507eaffd69787a2`, initially clean worktree. Changed production files are `src/admin_ssh_console.{c,h}` and `src/ssh_transport.c`; focused tests are in `tests/admin_console_boundary/`. No console-input changes were necessary because its prompt routing already uses the shared dispatcher.
|
||||
|
||||
- `admin_ssh_console_open_owned()` accepts a copied transport-qualified token/principal and an immutable firmware-lifetime owner adapter. Existing SSH admission remains available through `admin_ssh_console_open()`, now implemented beside its owner in `ssh_transport.c`.
|
||||
- The adapter supplies nonblocking application-output drain checks and deferred lifecycle handling, called by the existing control task outside console locks. SSH validates slot/session/generation and uses existing transport control APIs, never runtime wolfSSH calls from the control task.
|
||||
- Two console slots remain shared, not two per transport. Occupied and still-executing slots reject replacement. Future admission must coordinate this pool; this phase does not promise simultaneous browser and full SSH admin capacity.
|
||||
- The token includes transport, slot, session ID and generation. Owners serialize input per session, exclusively consume output, maintain transport authentication/expiry, and close on disconnect/revocation. The core retains admission/dispatch principal checks and queued-work identity checks.
|
||||
- Completion scratch has a nonblocking claim so different owners cannot race the shared buffer outside the lock. A competing TAB remains unconsumed for retry; stale completion cannot publish into a reused slot.
|
||||
- Shell exit uses an owner-relative `SELF_CLOSE` action; existing STOP/DISCONNECT/key actions still mean SSH. Unsupported actions fail before side effects. Deferred input is rejected at both admission and feed, and identity is checked again after the final delay.
|
||||
- The same dispatcher, canonical registry, queue, editor/history/prompts, UART0 policy and output ring remain. Five-second output backpressure and ten-second deferred drain plus 200 ms remain bounded best-effort heuristics, not delivery confirmation.
|
||||
|
||||
No new tasks, browser endpoints, UI changes, serial/broker changes, generated assets, persistence changes, uploads, erases or commits.
|
||||
|
||||
## Verification And Resources
|
||||
|
||||
- `python3 tests/admin_console_boundary/run.py`: PASS for production console and extracted production SSH adapter with deterministic host fakes. Covers readiness/admission, cross-transport and stale identities, completion contention and close/reopen during completion, history, queued disconnect/revocation, UART dispatch, executing cleanup, hidden/visible/cancelled/disconnected prompts, exit-to-SELF_CLOSE routing, unsupported actions, queue rejection, drain timeout/delay, SSH action routing and slow output.
|
||||
- `python3 tests/admin_ssh_policy/run.py`: PASS, including quiet empty input and physical-only command restrictions.
|
||||
- Independent code review found no defects. `git diff --check`: PASS.
|
||||
- `pio run`: PASS in **43.61 seconds**, PlatformIO espressif32 6.12.0 / ESP-IDF 5.5.0, N16R8 release. Linked RAM **95,084 B**, flash **1,627,173 B**. Against the recorded latest 8D.3 build (95,508 / 1,625,725 B): **-424 B RAM / +1,448 B flash**. Against recorded 8D.0 (94,532 / 1,599,973 B): **+552 B RAM / +27,200 B flash**. These comparisons use recorded builds, not a fresh baseline rebuild.
|
||||
|
||||
Static savings come from removing the full SSH snapshot scratch in the console and reading only the matching published slot under the SSH lock. Added costs are transport tags, owner pointers in sessions/control requests, the immutable adapter, and one completion-claim flag. No payload buffer or capacity increase. Two 4 KiB output rings, four-entry command queue, two-entry control queue, four-line per-session history and task stack sizes (dispatcher 12 KiB, UART frontend 6 KiB, control 4 KiB) are unchanged. No new module heap allocation, socket, TLS connection, HTTP handler, web session or ticket capacity. Runtime heap, PSRAM and stack margins were not measured; no reserve approval is inferred from linked RAM.
|
||||
|
||||
Host fakes do not establish actual FreeRTOS scheduling, task-local stdio redirection, real queue capacities, socket behavior or hardware regression. The command runner and lifecycle operations are doubled; direct production helpers test routing and output separately. Sanitizer execution was unavailable because the host lacks the required runtime library, not a sanitizer pass.
|
||||
|
||||
## Target Handoff
|
||||
|
||||
Do not stack 8D.5 on this unverified runtime change without target sign-off or an explicit user decision. After flashing through the user's normal workflow:
|
||||
|
||||
1. Boot and capture UART0 `memory`, status and `ssh status`. Verify UART0 recovery and empty Enter/normal commands through admin SSH (also covers the separately pending 8D.3 empty-line fix).
|
||||
2. Exercise UART0/admin-SSH serialization, completion/history, visible/hidden/cancelled prompts, and disconnect/revocation while work is queued or a prompt is active. Check reconnect/slot reuse does not receive old output.
|
||||
3. Exercise slow-reader backpressure and recovery, `exit`, empty-line Ctrl+D, and the existing deferred SSH stop/disconnect/key/reboot paths as appropriate. Confirm acknowledgement remains best effort and stale sessions cannot act on reused identities.
|
||||
4. Run the always-on browser login/serial explicit disconnect/reconnect, USB UART1 and user/admin SSH smoke. Repeat five serial lifecycle cycles per role and collect comparable settled/full-client-mix/cleanup `memory` and SSH stack telemetry at the supported 115200-baud workload.
|
||||
|
||||
No target checks above were executed by the agent. Exact next chunk is **8D.5: bounded admin WebSocket backend**, only when separately requested after the applicable validation decision.
|
||||
@@ -1,6 +1,6 @@
|
||||
# Phase 8D — Incremental web administration plan
|
||||
|
||||
Status: **8D.0–8D.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. Numeric reserve gates remain open. 8D.4–8D.22 remain planned, each requiring a separate implementation request.** See the [8D.3 implementation record](phase8d3_implementation.md) and [8D.0 baseline/M1 contract](phase8d_baseline.md).
|
||||
Status: **8D.0–8D.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 is implemented, host-tested and build-verified; target regression remains pending. Numeric reserve gates remain open. 8D.5–8D.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).
|
||||
|
||||
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.
|
||||
|
||||
@@ -102,6 +102,8 @@ If 8D.3 exceeds the work-unit limit, first split out inert login-page rendering
|
||||
|
||||
### 8D.4 — Small transport-neutral console boundary
|
||||
|
||||
**Implementation checkpoint (2026-09-06):** [8D.4 implementation, resource accounting and target handoff](phase8d4_implementation.md). Implemented / host-tested / build-verified; target regression pending. Owner callbacks and transport-qualified identity retain the existing two shared slots and SSH API, with no browser routes. Build **95,084 B RAM / 1,627,173 B flash**, **-424 / +1,448 B** versus recorded latest 8D.3. Numeric reserves remain open. This supersedes the overview's older planned status for 8D.4 only; stop before 8D.5 pending validation or explicit user decision.
|
||||
|
||||
**Start in:** `src/admin_ssh_console.{c,h}`, `src/console_input.{c,h}`, and only the necessary `src/ssh_transport.c` callers.
|
||||
|
||||
**Scope:** Introduce the minimal transport identity/output/lifecycle boundary needed by a future web frontend. Retain the single dispatcher, fixed queue, line editing/history/completion/prompts, currentness checks, generation tokens, and existing SSH API compatibility where practical. Do not duplicate per-SSH buffers for hypothetical web capacity or rename the whole module. No web endpoint or second dispatcher/task.
|
||||
@@ -167,7 +169,7 @@ Update the roadmap and user/command documentation to distinguish completed featu
|
||||
|
||||
## Progress and next-request template
|
||||
|
||||
Progress: **8D.0–8D.3 and M1 validated by user sign-off. 8D.3 implemented / host-tested / build-verified; numeric reserves remain open. 8D.4–8D.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.0–8D.3 and M1 validated by user sign-off. 8D.4 implemented / host-tested / build-verified, target regression pending; numeric reserves remain open. 8D.5–8D.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:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user