Skip to content

Stakia firmware: expressive eyes and authenticated local TLS - #2

Merged
rustyorb merged 5 commits into
mainfrom
codex/stakia-upgrade
Sep 17, 2026
Merged

rustyorb merged 5 commits into
mainfrom
codex/stakia-upgrade

Conversation

@rustyorb

@rustyorb rustyorb commented Sep 16, 2026 •

Copy link
Copy Markdown
Owner

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:

  • Larger emerald eyes, pupils, highlights, gaze, independent lids and emotion mappings.
  • Direct authenticated TLS connection to the PC. Embed the public local CA and robot credential; enforce certificate verification with no vendor/plaintext fallback.
  • Bypass vendor activation, saved vendor endpoints, account/apps, uploads, network updates and public time services.
  • Native build helper pinned to the commercial StackChan revision, ESP network 3.6.4, and ESP-IDF 5.5.4. Fixed-executable argv execution, secret-safe generated files, and no automatic flashing.

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:

  • Add expressive emerald-eye rendering with gaze, pupils, highlights, independent lids, and emotion mappings.
  • Connect firmware directly to the local host over authenticated TLS using embedded CA and robot credentials.
  • Provide a pinned StackChan factory build workflow for preparing and compiling Stakia firmware without automatic flashing.

Bug Fixes:

  • Prevent insecure or unintended firmware connections by disabling vendor bootstrap, fallback endpoints, certificate bypasses, public time services, and related cloud features.

Enhancements:

  • Enforce private LAN endpoint validation, reproducible source and dependency revisions, secure generated files, and fixed-executable build commands.

Build:

  • Pin the commercial StackChan source and ESP-IDF 5.5.4/ESP network 3.6.4 build environment.

Documentation:

  • Document the Stakia build, TLS provisioning, download mode, flashing, runtime limitations, and validation boundaries.

Tests:

  • Add eye-geometry coverage for sizing, gaze limits, blinking, and rotation normalization.
  • Add build-helper tests for endpoint validation, shell-safety, and secure TLS configuration.

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.
@sourcery-ai

sourcery-ai Bot commented Sep 16, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Delivers 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 recreation

sequenceDiagram
    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
Loading

Sequence diagram for bounded dialogue relay to the selected model

sequenceDiagram
    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
Loading

File-Level Changes

Change Details Files
Introduces persisted, validated conversation controls and explicit local/cloud model configuration.
  • Adds defaults and validation for speech pause, finite/Never idle timeout, farewell, and LM Studio/OpenRouter selection.
  • Persists settings atomically and renders a generated server configuration with environment-secret expansion.
  • Adds dashboard controls and documents restart/recreate behavior.
bridge/host_settings.py
bridge/dashboard.py
bridge/templates/dashboard.html
bridge/templates/host_settings.html
scripts/configure-host.py
scripts/start-host.ps1
scripts/doctor-host.ps1
tests/test_host_settings.py
Reworks the voice runtime for local speech processing and bounded multi-turn relay history.
  • Requires Whisper, Silero, and Piper assets locally and disables automatic Whisper downloads.
  • Mounts local providers and an owned prompt into a pinned Xiaozhi server build.
  • Forwards sanitized, bounded structured dialogue through the optional bridge relay.
  • Adds patches and tests covering both upstream silence-idle close paths.
custom-providers/asr/whisper_local.py
custom-providers/zeroclaw/zeroclaw.py
bridge/dialogue.py
custom-providers/xiaozhi-patches/never-idle.patch
docker/server.Dockerfile
docker-compose.yml
config/stakia-local-prompt.txt
tests/test_dialogue_relay.py
tests/test_idle_policy.py
tests/test_upstream_idle_patch.py
tests/test_compose_local_providers.py
Adds a reproducible commercial StackChan firmware preparation path with direct private-LAN connectivity and expressive eyes.
  • Pins the StackChan source and prepares idempotent local-only and eye-rendering patches.
  • Generates a LAN WebSocket endpoint and rejects non-private or invalid hosts.
  • Adds layered emerald eyes with pupils, highlights, gaze, eyelids, and six emotion mappings.
  • Provides native ESP-IDF version checks, dependency preparation, build-only behavior, geometry tests, and flashing handoff documentation.
firmware/build_factory.py
firmware/STAKIA-BUILD.md
firmware/README.md
firmware/stakia/expressive-eyes.patch
firmware/stakia/local-only-factory.patch
firmware/stakia/local-only-xiaozhi.patch
firmware/stakia/stakia_geometry.h
firmware/tests/eye_geometry_test.cpp
firmware/tests/test_build_factory.py
Reorients project documentation and deployment defaults around an unauthenticated private-LAN, vendor-independent runtime.
  • Replaces the floating server image with a pinned source build and local model mounts.
  • Documents Windows startup, explicit model acquisition, diagnostics, firmware preparation, verified-port flashing, limitations, and untested bench validation.
  • Preserves the upgrade research while recording implementation status and remaining risks.
README.md
docs/implementation-status.md
docs/upgrade-plan.md
.gitignore
.gitattributes

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@rustyorb
rustyorb marked this pull request as ready for review September 17, 2026 11:31
Copilot AI lite review requested due to automatic review settings September 17, 2026 11:31

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread bridge/host_settings.py Outdated
Comment thread bridge/host_settings.py Outdated
Comment thread firmware/STAKIA-BUILD.md Outdated
Comment thread firmware/build_factory.py Outdated
Comment thread firmware/build_factory.py Outdated
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.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New security issues found

Comment thread docs/pr-2-review.md Outdated
Comment thread docs/pr-2-review.md Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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/data nor data/models/piper exists. Invoke-WebRequest -OutFile does 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_KEY makes write_server_config raise the ValueError intentionally covered by tests/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 room is 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 unpacked model.bin, tokenizer, or Piper voice file passes this preflight and only fails later with a provider-specific load error. Use is_dir() for the Whisper directory and is_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::Neutral case, 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_setup and calls requestXiaozhiStart(). The companion patch consumes that flag inside WifiBoard::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 as 8.8.8.8, api.example.com, or 192.168.1.1/evil are reported as valid even though firmware/build_factory.py:22-29 rejects 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.

Comment thread bridge.py

class MessageIn(BaseModel):
content: str
messages: list[dict[str, str]] | None = None
Comment thread bridge/host_settings.py Outdated
Comment on lines +78 to +79
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")
Comment thread bridge/host_settings.py Outdated
Comment on lines +95 to +98
"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},
Comment thread bridge/host_settings.py
"tool_call_timeout": 30,
"device_max_output_size": 0,
"exit_commands": ["exit conversation", "goodbye stakia"],
"mcp_endpoint": "",
Comment on lines +638 to +640
+ 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.
@rustyorb rustyorb changed the title Stakia: local voice controls, independent firmware connection, and expressive eyes Stakia firmware: expressive eyes and authenticated local TLS Sep 17, 2026
@rustyorb
rustyorb changed the base branch from main to codex/stakia-host-runtime September 17, 2026 12:07
Base automatically changed from codex/stakia-host-runtime to main September 17, 2026 12:17
@rustyorb
rustyorb merged commit bb27539 into main Sep 17, 2026
3 checks passed
@rustyorb
rustyorb deleted the codex/stakia-upgrade branch September 17, 2026 12:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants