Parent / governing decisions
Owner
- ACTIVE_ROLE:
implementation
- DOMAIN:
device
State
READY_FOR_IMPLEMENTATION
Defect report
Physical use after #196 still observes the undesired behavior:
- Connect the Web app to the Device.
- Complete Trusted Browser Unlock and confirm the Device is
UNLOCKED.
- Close the browser tab or reload the page.
- The Device is subsequently observed as
LOCKED.
This contradicts the approved contract:
- browser/page/Web Serial lifecycle teardown must not implicitly Lock a still-running Device;
- same physical runtime may remain
UNLOCKED;
- only explicit Lock, configured automatic-lock expiry, reboot/actual power loss, or another approved Device security boundary destroys the active VMK.
A USB cable removal that leaves the StickS3 running on battery has been observed by the human owner to preserve UNLOCKED, reinforcing the desired behavior.
Current static evidence
Specification re-checked current main = b3aa8fc0cc06143990dcf32a971853264f73eba8.
Web
web/src/browser-lifecycle.ts:
- page lifecycle cleanup calls
management.disconnectTransport() when management exists;
- otherwise closes only the raw serial session;
- no
device.lock request is present.
CanonicalDeviceManagement:
close() sends device.lock and is the explicit Lock path;
disconnectTransport() clears browser-side management state and calls transport close() only.
SerialSession.close():
- cancels/releases stream locks and closes the Web Serial port;
- it does not send a Protocol-v2
device.lock.
Device
Production USB disconnect handling in firmware/main/app_main.cpp calls:
protocol.disconnect();
- transport PRE_HANDSHAKE reset.
CanonicalProtocolV2Handler::disconnect():
- disconnects protocol/session state;
- cancels pending VMK delivery/reset presence;
- does not call
runtime_.lock().
The normal runtime_.lock() path is reached through approved security boundaries such as explicit device.lock or automatic-lock expiry.
Therefore the source-level intended contract appears correct. The observed physical outcome requires runtime diagnosis rather than another speculative Web-only change.
Objective
Determine why tab close/reload results in a physically/reconnect-observed LOCKED Device despite the non-locking source contract, then fix the actual causal path while preserving #195.
The diagnosis must distinguish at least:
- an unexpected
device.lock request;
- a Device reboot/reset caused or correlated with Web Serial/page teardown;
- an unintended Device security-boundary call;
- automatic-lock expiry;
- stale/incorrect UI interpretation where the Device did not actually Lock;
- old/stale Web or firmware build behavior not matching current main;
- another transport/browser lifecycle interaction.
Reproduction / evidence requirements
A. Establish exact versions
Before changing code, record non-secret identities:
- exact Web build/commit served by the tested page;
- exact Device firmware/build commit from normal
hello;
- configured automatic-lock setting/state;
- browser version used for the physical reproduction.
Do not record Device ID, account metadata, OTPs, TOTP secrets, Vault identifiers, Recovery data, keys, or credential-bearing logs.
If the physical Device firmware is not a build containing the current lifecycle contract, reproduce again with the intended current candidate before claiming a current-main regression.
B. Separate the lifecycle cases
Using the same unlocked Device/runtime where practical, characterize independently:
- page reload;
- tab close;
- navigation away;
- browser process close, if safely reproducible;
- explicit
Lock & Disconnect as positive control;
- direct transport-only disconnect through the application's non-locking path, if a diagnostic harness can invoke it safely;
- USB cable removal while battery keeps Device running as physical control.
For each case record only:
- Device state before action;
- whether display/runtime remains alive continuously;
- Device state immediately after;
- reconnect
hello.state;
- whether a fresh Unlock physical-confirmation request is required.
C. Distinguish Lock from reboot/reset
A tab/reload observation must not be classified merely from the reloaded Web UI.
Use the least-invasive non-secret evidence available to determine whether the Device:
- remained in the same boot/runtime and changed to LOCKED; or
- rebooted/reset, thereby correctly losing RAM-only VMK.
Prefer existing/sanitized diagnostics. If additional diagnostic instrumentation is necessary, it may expose only non-secret lifecycle evidence such as:
- boot/reset counter/reason;
- monotonic boot continuity marker;
- whether an explicit Device Lock operation was accepted;
- transport connect/disconnect transitions;
- security-boundary reason enum/counter.
It must not expose VMK/key material, Vault contents/IDs, account metadata, credentials, Passphrases, OTPs, session private data, or raw protocol secret payloads.
Production diagnostic expansion requires normal security review if it becomes user-accessible; isolated diagnostic builds are preferred during root-cause work.
D. Check Web request boundary
Prove whether a device.lock Protocol request is emitted in each lifecycle case.
The existing mock regression from #196 is necessary but no longer sufficient because the physical behavior disagrees.
If browser teardown itself cannot reliably complete asynchronous logging, use a bounded local test harness/transport spy that records only operation names, never parameters or secret-bearing payloads.
E. Check Device boundary
Instrument/test the Device boundary sufficiently to prove which event, if any, changes runtime from UNLOCKED to LOCKED.
At minimum distinguish:
- explicit
device.lock;
- automatic-lock expiry;
- reboot/reset;
- other
lock_security_boundary() caller.
Do not weaken automatic-lock or other legitimate fail-closed boundaries to make the test pass.
Fix policy
If Web sends an unexpected explicit Lock
Remove that implicit path while preserving:
- explicit
Lock & Disconnect;
- browser transient-secret cleanup;
- best-effort transport/resource release.
If Web Serial close/reload causes Device reboot/reset
Do not disguise the reset as a Web-state issue.
Determine the causal browser/USB/Device path and apply the narrowest fix. If avoiding an explicit port.close() during page teardown is necessary, prove:
- browser reload/tab-close still releases ownership sufficiently for reconnect;
- no serial resource/permission deadlock is introduced;
- explicit user disconnect remains deterministic;
- no security boundary is weakened.
If the reset is unavoidable platform behavior, return to Specification with physical evidence before changing the product lifecycle expectation.
If Device incorrectly locks on transport/session disconnect
Separate session teardown from VMK runtime lifetime per #195. Keep:
- pending session/authorization cancellation;
- framing/session fail-closed behavior;
- explicit Lock;
- automatic Lock;
- reboot/power-loss lock semantics.
If stale build is the cause
Do not create unnecessary production changes. Record exact stale/current identities, confirm the current intended build physically satisfies #195, and close this Defect with evidence.
Tests
Add/extend regression coverage for the causal fix.
Required end-state tests include:
- lifecycle page teardown does not issue
device.lock;
- transport disconnect does not call Device runtime lock;
- explicit
Lock & Disconnect still locks;
- automatic-lock expiry still locks;
- reboot/power-loss model still starts LOCKED;
- reconnect to same still-running runtime after non-locking teardown reports
UNLOCKED;
- any fixed browser/serial teardown path can reconnect without stale port ownership;
- transient browser secrets are still cleared.
Where feasible add a Chrome/browser integration test covering actual reload/port teardown behavior in addition to mocks.
Acceptance
Collision / coordination
At Specification time there are no open PRs, but #203 is a newly-open Web Task without a PR yet and is expected to touch shared Web layout rather than lifecycle/serial code.
Device Implementation owns this Defect initially because current static Web and Device contracts both appear non-locking and the unresolved question is the physical Device state transition/runtime continuity.
If evidence proves the causal fix is exclusively Web-side, persist the finding here and hand off to Web Implementation rather than silently crossing domains.
Re-check current Issues/PRs/branches before claim.
References
Parent / governing decisions
Owner
implementationdeviceState
READY_FOR_IMPLEMENTATIONDefect report
Physical use after #196 still observes the undesired behavior:
UNLOCKED.LOCKED.This contradicts the approved contract:
UNLOCKED;A USB cable removal that leaves the StickS3 running on battery has been observed by the human owner to preserve
UNLOCKED, reinforcing the desired behavior.Current static evidence
Specification re-checked current
main = b3aa8fc0cc06143990dcf32a971853264f73eba8.Web
web/src/browser-lifecycle.ts:management.disconnectTransport()when management exists;device.lockrequest is present.CanonicalDeviceManagement:close()sendsdevice.lockand is the explicit Lock path;disconnectTransport()clears browser-side management state and calls transportclose()only.SerialSession.close():device.lock.Device
Production USB disconnect handling in
firmware/main/app_main.cppcalls:protocol.disconnect();CanonicalProtocolV2Handler::disconnect():runtime_.lock().The normal
runtime_.lock()path is reached through approved security boundaries such as explicitdevice.lockor automatic-lock expiry.Therefore the source-level intended contract appears correct. The observed physical outcome requires runtime diagnosis rather than another speculative Web-only change.
Objective
Determine why tab close/reload results in a physically/reconnect-observed
LOCKEDDevice despite the non-locking source contract, then fix the actual causal path while preserving #195.The diagnosis must distinguish at least:
device.lockrequest;Reproduction / evidence requirements
A. Establish exact versions
Before changing code, record non-secret identities:
hello;Do not record Device ID, account metadata, OTPs, TOTP secrets, Vault identifiers, Recovery data, keys, or credential-bearing logs.
If the physical Device firmware is not a build containing the current lifecycle contract, reproduce again with the intended current candidate before claiming a current-main regression.
B. Separate the lifecycle cases
Using the same unlocked Device/runtime where practical, characterize independently:
Lock & Disconnectas positive control;For each case record only:
hello.state;C. Distinguish Lock from reboot/reset
A tab/reload observation must not be classified merely from the reloaded Web UI.
Use the least-invasive non-secret evidence available to determine whether the Device:
Prefer existing/sanitized diagnostics. If additional diagnostic instrumentation is necessary, it may expose only non-secret lifecycle evidence such as:
It must not expose VMK/key material, Vault contents/IDs, account metadata, credentials, Passphrases, OTPs, session private data, or raw protocol secret payloads.
Production diagnostic expansion requires normal security review if it becomes user-accessible; isolated diagnostic builds are preferred during root-cause work.
D. Check Web request boundary
Prove whether a
device.lockProtocol request is emitted in each lifecycle case.The existing mock regression from #196 is necessary but no longer sufficient because the physical behavior disagrees.
If browser teardown itself cannot reliably complete asynchronous logging, use a bounded local test harness/transport spy that records only operation names, never parameters or secret-bearing payloads.
E. Check Device boundary
Instrument/test the Device boundary sufficiently to prove which event, if any, changes runtime from UNLOCKED to LOCKED.
At minimum distinguish:
device.lock;lock_security_boundary()caller.Do not weaken automatic-lock or other legitimate fail-closed boundaries to make the test pass.
Fix policy
If Web sends an unexpected explicit Lock
Remove that implicit path while preserving:
Lock & Disconnect;If Web Serial close/reload causes Device reboot/reset
Do not disguise the reset as a Web-state issue.
Determine the causal browser/USB/Device path and apply the narrowest fix. If avoiding an explicit
port.close()during page teardown is necessary, prove:If the reset is unavoidable platform behavior, return to Specification with physical evidence before changing the product lifecycle expectation.
If Device incorrectly locks on transport/session disconnect
Separate session teardown from VMK runtime lifetime per #195. Keep:
If stale build is the cause
Do not create unnecessary production changes. Record exact stale/current identities, confirm the current intended build physically satisfies #195, and close this Defect with evidence.
Tests
Add/extend regression coverage for the causal fix.
Required end-state tests include:
device.lock;Lock & Disconnectstill locks;UNLOCKED;Where feasible add a Chrome/browser integration test covering actual reload/port teardown behavior in addition to mocks.
Acceptance
UNLOCKEDwithout a new Unlock confirmation.Lock & Disconnectstill destroys the unlocked session.Collision / coordination
At Specification time there are no open PRs, but #203 is a newly-open Web Task without a PR yet and is expected to touch shared Web layout rather than lifecycle/serial code.
Device Implementation owns this Defect initially because current static Web and Device contracts both appear non-locking and the unresolved question is the physical Device state transition/runtime continuity.
If evidence proves the causal fix is exclusively Web-side, persist the finding here and hand off to Web Implementation rather than silently crossing domains.
Re-check current Issues/PRs/branches before claim.
References
web/src/browser-lifecycle.tsweb/src/canonical-management.tsweb/src/serial.tsfirmware/main/app_main.cppfirmware/components/m5auth_provisioning/canonical_protocol_v2.cpp