Stakia firmware: expressive eyes and authenticated local TLS - #2
Conversation
Continue the existing host stack with selectable LM Studio/OpenRouter, configurable speech pause and idle behavior, local speech assets, and native ESP-IDF firmware patches that connect directly to the LAN host. Host and geometry checks pass; Docker runtime, full IDF build, and physical device validation remain bench work.
Reviewer's GuideDelivers a pinned, independently hosted Stakia voice stack with persisted conversation controls, local speech assets, bounded dialogue relay, patched idle behavior, expressive StackChan eyes, and direct private-LAN firmware connectivity; review deployment safety, generated configuration/provider compatibility, patch correctness, and the explicitly untested Docker/ESP-IDF/hardware paths. Sequence diagram for persisted conversation settings and server recreationsequenceDiagram
actor User
participant Dashboard
participant SettingsStore
participant ConfigRenderer
participant VoiceServer
User->>Dashboard: host_settings_save(profile, pause_seconds, idle_mode, idle_minutes, idle_farewell)
Dashboard->>SettingsStore: save(settings)
Dashboard->>ConfigRenderer: write_server_config(settings, lan_host, path)
ConfigRenderer-->>Dashboard: generated configuration
Dashboard-->>User: Saved for server recreate
User->>VoiceServer: docker compose up -d --force-recreate
VoiceServer->>VoiceServer: Apply pause and idle policy
Sequence diagram for bounded dialogue relay to the selected modelsequenceDiagram
participant StackChan
participant VoiceServer
participant Zeroclaw
participant Bridge
participant LLM
StackChan->>VoiceServer: Voice turn
VoiceServer->>Zeroclaw: Dialogue history
Zeroclaw->>Zeroclaw: bounded_dialogue(dialogue)
Zeroclaw->>Bridge: MessageIn(content, messages)
Bridge->>Bridge: bounded_dialogue(messages)
Bridge->>LLM: Chat completions with bounded messages
LLM-->>Bridge: Streamed response
Bridge-->>VoiceServer: Response text
VoiceServer-->>StackChan: Synthesized speech
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 4 security issues, and 1 other issue
Security issues:
- Insecure WebSocket Detected. WebSocket Secure (wss) should be used for all WebSocket connections. (link)
- Insecure WebSocket Detected. WebSocket Secure (wss) should be used for all WebSocket connections. (link)
- Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
- Insecure WebSocket Detected. WebSocket Secure (wss) should be used for all WebSocket connections. (link)
Fixed security issues:
- Insecure WebSocket Detected. WebSocket Secure (wss) should be used for all WebSocket connections. (link)
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="bridge/host_settings.py" line_range="97" />
<code_context>
+ "standalone_config": True,
+ "server": {"ip": "0.0.0.0", "port": 8000, "http_port": 8003,
+ "websocket": f"ws://{lan_host}:8000/xiaozhi/v1/",
+ "auth": {"enabled": False, "allowed_devices": []},
+ "mqtt_gateway": None, "udp_gateway": None},
+ "manager-api": {"url": "", "secret": ""},
</code_context>
<issue_to_address>
**issue (bug_risk):** The standalone generated configuration omits `server.auth_key`, but both the patched `WebSocketServer` and `OTAHandler` unconditionally read `config["server"]["auth_key"]` during initialization. Starting the pinned server therefore raises `KeyError` before the WebSocket or OTA service can run, even though authentication is deliberately disabled.
**Triggers:** When the generated standalone configuration is used to start the server.
**Suggested fix:** Include a generated `auth_key` value under `server` even when `auth.enabled` is false, or patch both consumers to use a safe default when authentication is disabled.
</issue_to_address>
### Comment 2
<location path="bridge/host_settings.py" line_range="96" />
<code_context>
"websocket": f"ws://{lan_host}:8000/xiaozhi/v1/",
</code_context>
<issue_to_address>
**security (javascript.lang.security.detect-insecure-websocket):** Insecure WebSocket Detected. WebSocket Secure (wss) should be used for all WebSocket connections.
*Source: opengrep*
</issue_to_address>
### Comment 3
<location path="firmware/STAKIA-BUILD.md" line_range="28" />
<code_context>
This build connects directly to `ws://YOUR_PC_LAN_IP:8000/xiaozhi/v1/`. It bypasses vendor OTA/bootstrap and activation entirely. Old saved vendor URLs and tokens are ignored. There is no vendor fallback if the local server is unavailable. Model selection happens on your host; OpenRouter is used only when you select it.
</code_context>
<issue_to_address>
**security (javascript.lang.security.detect-insecure-websocket):** Insecure WebSocket Detected. WebSocket Secure (wss) should be used for all WebSocket connections.
*Source: opengrep*
</issue_to_address>
### Comment 4
<location path="firmware/build_factory.py" line_range="18-19" />
<code_context>
return subprocess.run([str(a) for a in args], cwd=cwd, check=True,
text=True, stdout=subprocess.PIPE if capture else None)
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
*Source: opengrep*
</issue_to_address>
### Comment 5
<location path="firmware/build_factory.py" line_range="31" />
<code_context>
f'#define STAKIA_WEBSOCKET_URL "ws://{ip}:{port}/xiaozhi/v1/"\n')
</code_context>
<issue_to_address>
**security (javascript.lang.security.detect-insecure-websocket):** Insecure WebSocket Detected. WebSocket Secure (wss) should be used for all WebSocket connections.
*Source: opengrep*
</issue_to_address>Sourcery assessment
Needs a human reviewer. 5 findings to address first, and the generated server binds unauthenticated WebSocket and HTTP services to 0.0.0.0, while the firmware is hard-coded to trust that LAN endpoint; any reachable device could potentially start sessions or invoke exposed controls, and misuse would occur before a revert. Reverting removes the configuration, but it cannot undo data exposure or actions taken while the unauthenticated trust boundary was active.
Blocking findings: bridge/host_settings.py:97, bridge/host_settings.py:96, firmware/STAKIA-BUILD.md:28, firmware/build_factory.py:19, firmware/build_factory.py:31
Add a regression executing the pinned upstream entry point and constructors with external work substituted. Document the auth/subprocess assessment and keep the real LAN transport limitations explicitly open.
There was a problem hiding this comment.
🔵 Needs a closer look
Unresolved critical findings remain, and end-to-end runtime and physical firmware behavior are unverified.
Pull request overview
This PR adds a self-hosted Stakia voice runtime with configurable conversation controls, direct-LAN firmware connectivity, local speech providers, and expressive eyes.
Changes:
- Adds persisted idle, pause, farewell, model, and local asset controls.
- Pins and patches the voice server and StackChan firmware for independent operation.
- Adds Windows tooling, documentation, tests, and expressive eye rendering.
File summaries
| File | Reviewed changes |
|---|---|
tests/test_upstream_idle_patch.py |
Tests patched upstream idle behavior. |
tests/test_idle_policy.py |
Tests finite and Never idle policies. |
tests/test_host_settings.py |
Tests settings, assets, and configuration rendering. |
tests/test_dialogue_relay.py |
Tests bounded dialogue preservation. |
tests/test_compose_local_providers.py |
Tests provider mounts and prompt isolation. |
scripts/start-host.ps1 |
Starts configured host services. |
scripts/start-host.bat |
Provides a Windows startup wrapper. |
scripts/doctor-host.ps1 |
Validates host configuration and ports. |
scripts/doctor-host.bat |
Provides a Windows diagnostics wrapper. |
scripts/configure-host.py |
Generates validated runtime configuration. |
README.md |
Documents local deployment and operation. |
firmware/tests/test_build_factory.py |
Tests firmware endpoint validation. |
firmware/tests/eye_geometry_test.cpp |
Tests eye geometry bounds. |
firmware/stakia/stakia_geometry.h |
Defines eye geometry helpers. |
firmware/stakia/local-only-xiaozhi.patch |
Configures direct-LAN connectivity and disables vendor services. |
firmware/stakia/local-only-factory.patch |
Removes vendor firmware and application paths. |
firmware/stakia/expressive-eyes.patch |
Adds layered expressive eye rendering. |
firmware/STAKIA-BUILD.md |
Documents firmware building and flashing. |
firmware/README.md |
Describes the current firmware workflow. |
firmware/build_factory.py |
Prepares, validates, and builds pinned firmware. |
docs/upgrade-plan.md |
Preserves and updates upgrade research. |
docs/implementation-status.md |
Records implementation and validation status. |
docker/server.Dockerfile |
Builds the pinned patched server image. |
docker-compose.yml |
Defines server, model, and provider mounts. |
custom-providers/zeroclaw/zeroclaw.py |
Relays structured dialogue history. |
custom-providers/xiaozhi-patches/never-idle.patch |
Patches standalone configuration and idle handling. |
custom-providers/asr/whisper_local.py |
Provides local Whisper speech recognition. |
config/stakia-local-prompt.txt |
Defines the local prompt template. |
bridge/templates/host_settings.html |
Provides conversation settings controls. |
bridge/templates/dashboard.html |
Embeds the settings panel. |
bridge/idle_policy.py |
Provides idle policy helpers. |
bridge/host_settings.py |
Persists settings and renders server configuration. |
bridge/dialogue.py |
Bounds conversation history. |
bridge/dashboard.py |
Adds settings routes. |
bridge.py |
Supports structured dialogue relay. |
.gitignore |
Excludes generated runtime and firmware files. |
.gitattributes |
Normalizes patch whitespace handling. |
.env.local.example |
Documents local and optional cloud configuration. |
Review details
Suppressed comments (7)
README.md:43
- On a fresh checkout, neither
data/models/silero/src/silero_vad/datanordata/models/piperexists.Invoke-WebRequest -OutFiledoes not create parent directories, so these documented model-acquisition commands fail before the validator can run. Create both directories before downloading the files.
Invoke-WebRequest https://raw.githubusercontent.com/snakers4/silero-vad/master/src/silero_vad/data/silero_vad.onnx -OutFile data/models/silero/src/silero_vad/data/silero_vad.onnx
Invoke-WebRequest https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/lessac/medium/en_US-lessac-medium.onnx -OutFile data/models/piper/voice.onnx
Invoke-WebRequest https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/lessac/medium/en_US-lessac-medium.onnx.json -OutFile data/models/piper/voice.onnx.json
bridge/dashboard.py:369
- Selecting OpenRouter without
OPENROUTER_API_KEYmakeswrite_server_configraise theValueErrorintentionally covered bytests/test_host_settings.py:84-87, but this route does not catch it. The dashboard therefore returns an unhelpful HTTP 500 and gives no indication in the form that the profile was not saved. Handle validation errors and render a clear form error (or disable the profile until its key is configured).
lan_host = os.environ.get("STAKIA_LAN_HOST", "")
write_server_config(settings, lan_host=lan_host, path=SERVER_CONFIG_PATH)
HOST_SETTINGS.save(settings)
bridge/dialogue.py:23
- When
roomis odd and the retained history alternates user/assistant messages,turns[-room:]can start with an assistant message. That forwards an orphaned assistant reply without its user turn, contradicting the function's “latest complete turns” contract and can confuse the model; drop a leading assistant from the retained slice (or otherwise select complete user/assistant pairs).
return keep_system + turns[-room:] if room else keep_system
bridge/host_settings.py:71
Path.exists()also accepts directories for every required asset, so a wrongly unpackedmodel.bin, tokenizer, or Piper voice file passes this preflight and only fails later with a provider-specific load error. Useis_dir()for the Whisper directory andis_file()for each model/config asset so the promised local-asset guard rejects invalid paths before startup.
missing = [label for label, path in required.items() if not path.exists()]
firmware/stakia/expressive-eyes.patch:237
- The new switch has no
Emotion::Neutralcase, so after a Happy, Angry, Sad, Doubt, or Sleepy emotion, a transition to Neutral leaves the previous eyelid weight and rotation in place. The removed implementation explicitly reset Neutral to weight 100 and rotation 0; restore that mapping before the default case.
+ default: break;
firmware/stakia/local-only-factory.patch:69
- Selecting Change Wi-Fi only writes
wifi_setupand callsrequestXiaozhiStart(). The companion patch consumes that flag insideWifiBoard::StartNetwork(), which runs only on the next boot, while this callback explicitly disables both menu teardown and the warm reset. As a result the advertised local configuration AP is not entered when the menu item is selected and no reboot is scheduled to consume the request; invoke configuration mode after initialization or schedule a reboot while preserving the flag.
+ Settings local("stakia", true);
+ local.SetInt("wifi_setup", 1);
+ _destroy_menu = false;
+ _need_warm_reset = false;
+ GetHAL().requestXiaozhiStart();
scripts/doctor-host.ps1:18
- The doctor accepts any value except loopback or
0.0.0.0, so values such as8.8.8.8,api.example.com, or192.168.1.1/evilare reported as valid even thoughfirmware/build_factory.py:22-29rejects them. This makes the diagnostic disagree with the actual deployment validator and can bless a non-private endpoint; apply the same RFC1918 IPv4 validation here.
- Files reviewed: 39/40 changed files
- Comments generated: 5
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
|
||
| class MessageIn(BaseModel): | ||
| content: str | ||
| messages: list[dict[str, str]] | None = None |
| if not lan_host or lan_host in {"127.0.0.1", "localhost", "0.0.0.0"}: | ||
| raise ValueError("lan_host must be an address reachable by StackChan") |
| "server": {"ip": "0.0.0.0", "port": 8000, "http_port": 8003, | ||
| "websocket": f"ws://{lan_host}:8000/xiaozhi/v1/", | ||
| "auth": {"enabled": False, "allowed_devices": []}, | ||
| "mqtt_gateway": None, "udp_gateway": None}, |
| "tool_call_timeout": 30, | ||
| "device_max_output_size": 0, | ||
| "exit_commands": ["exit conversation", "goodbye stakia"], | ||
| "mcp_endpoint": "", |
| + const std::string url = STAKIA_WEBSOCKET_URL; | ||
| + const std::string token; // Local server auth is disabled on the private LAN. | ||
| + version_ = 1; // Raw Opus frames, supported by the local Xiaozhi server. |
Require verified TLS and a private robot credential, close unused listeners, keep credentials out of logs and public file modes, and add real TLS/upstream regression checks to GitHub Actions. Address the audited shell-free build runner finding with executable restrictions, literal-argument tests, and one scoped suppression. Preserve firmware bench-validation limits.
…iews Preserve the complete upgrade tree and branch. Keep the full research on its documentation branch; retain all executable and security changes within the linked PRs.
This firmware PR depends on host PR #3. The original combined update exceeded Sourcery's review size limit, so the host and firmware now receive separate complete reviews. The complete working tree is still on codex/stakia-upgrade; no user-side Git juggling is required.
Firmware changes:
Validation: host/TLS tests plus firmware helper, C++ eye geometry, repeatable clean source preparation and full protocol patch application. The test workflow runs against exact upstream pins. Full ESP-IDF compilation and physical robot behavior remain bench checks. A screenshot confirms ESP-IDF 6.1 installed and COM15 listed as USB Serial Device; use 5.5.4 alongside it and identify the device's port before flashing.
After host PR #3 is merged, retarget this PR to main. No merge is performed as part of preparing these changes.
Setup and validation limits: firmware/STAKIA-BUILD.md and docs/implementation-status.md. The complete research document is preserved on docs/stakia-research-plan.
Summary by Sourcery
Deliver expressive Stakia firmware with authenticated local TLS and a reproducible, non-flashing factory build path.
New Features:
Bug Fixes:
Enhancements:
Build:
Documentation:
Tests: