# Approval Control 0.3.0 validation

Date: 2026-10-03, Asia/Shanghai. Firmware, Control Layer, FastAPI and protocol: 0.3.0.
Physical device flashing/testing was not available and is not claimed.

The accepted 0.2.2 commit `bc55ef3` was audited against its acceptance record,
fast-forwarded from `89252d1` to main and pushed before new development. The worktree
was clean at that boundary.

## Real Pi 0.85.1 proof

One SDK session: `01a0ff53-def5-7220-97d5-b98a4e2f704f`.
Fresh project: `/tmp/device-lab-approval-smoke-9w_wu0qw/pi-project`.
File: `/tmp/device-lab-pi-approval-smoke-bf0445ea844841f8bd363dd48755fca5.txt`.

- Official read tool completed with no approval created.
- Mutation reached tool_call → Unix RPC → Registry PENDING / WAITING_APPROVAL.
- Admin reject `848f1dc6fde34070904a373c4ca3ce33`: file did not exist after completion.
- New request, simulated device B reject `a8fa792ef7a246e8b71b21a978adecaa`: file still absent.
- New request, simulated device A approve `d49ddccc0c444a2682398e861c9d691d`: file created with `PI_APPROVED`.
- Every mutation was held PENDING for two seconds and its file was verified absent
  before the decision. All three calls used the same live Pi session. The write
  hook did not run past the gate while awaiting a decision.

## Real Codex 0.154.0 proof

Dedicated App Server on a temporary Unix socket; production daemon untouched.
New thread: `01a0ff54-8e93-7052-b7f1-7c8c0517c575`.
`approvalPolicy=on-request`, `approvalsReviewer=user`, `sandbox=read-only`.

- `item/commandExecution/requestApproval` JSON-RPC ID **0** became approval
  `2381b1d8806f4964a708ea30b855c539`.
- Admin reject → decline; `codex-project/reject.txt` remained absent; turn completed.
- A different command emitted JSON-RPC ID **1**, approval
  `066012a0a1e34a6b8e6c062928df7564`.
- Admin approve → accept; `codex-project/approve.txt` contained `CODEX_APPROVED`;
  turn completed. Both paths are inside the dedicated temporary project.
- Both commands remained unexecuted for two seconds while Registry was PENDING.

The first smoke exposed a real protocol compatibility detail: availableDecisions
omits decline for some commands, even though the generated response enum permits
it. The adapter now preserves one-shot accept/decline without exposing policy or
session amendments. The corrected full smoke exited successfully.

File-change approval uses the same Registry/response path and was verified through
protocol tests, including paths from item/started. A real file-change model smoke
is not claimed. Disconnect/no replay was tested using actual Unix WebSocket
connections and a test App Server that reuses JSON-RPC IDs after reconnect.

Machine-readable proof: [approval-0.3.0-smoke.json](approval-0.3.0-smoke.json).
The live smoke script saves full test-thread events in its printed /tmp directory.

## Regression and firmware

`./scripts/test-server.sh`: **27 Python tests, 11 Node tests**, native C++ approval
controller test, and TCP/HTTP/WebSocket smoke passed. Coverage includes:

- Registry create, binary decisions, duplicates, FIFO, monotonic expiry and cancel.
- Codex pending/nonblocking reader, typed original response ID, file paths,
  availableDecisions compatibility, timeout, external resolution, shutdown,
  disconnect cancellation and no response replay across reconnect/ID reuse.
- Pi inspection/mutation policy, real hook handler, approve/reject, absolute client
  timeout, disconnect/runtime shutdown, sequential calls and independent sessions.
- Authenticated simulated device A/B, stale/malformed responses, normal button
  events, reconnect, multi-device first-wins and unchanged target.
- Admin list/decisions, stable focused buttons across polls, in-flight decisions
  and old polls arriving after a completed decision.
- Firmware controller: held old key, simultaneous/double clicks, feedback window,
  next queued item, expiration and disconnect invalidation.

Only existing Starlette/AnyIO BlockingPortal deprecation warning remains.

`./scripts/build.sh m5stack-stopwatch`: passed on ESP-IDF 5.5.4.
Artifact: `dist/m5stack-stopwatch/merged-flash.bin`, **1,184,016 bytes**.
SHA-256: `55d5c6b5c80292c1fd26a9f1fbc767c0b4564cb7dd3d9ca9e658bb0c176d1249`.
`git diff --check`: passed.

Sound/vibration distinction, round-screen readability and physical button feel
still need a flashed StopWatch. Software tests and firmware compilation do not
substitute for that check. Existing interactive Pi sessions need `/reload` to
load the new hook; Gateway SDK runtimes load it automatically.

**PI APPROVAL READY: YES**

**CODEX APPROVAL READY: YES**

**READY FOR PHYSICAL APPROVAL VERIFICATION: YES**
