fix: remediate 2026-10-03 code review (F1-F8) - #32
Merged
Merged
Conversation
Two High, three Medium, three Low findings from the v1.3.0 audit. No image behaviour change for correct deployments; the point of F1/F2/F3 is that failure modes stop being silent. Fixed: - install-websockets.sh: install into and verify with the gateway's interpreter (/app/venv, include-system-site-packages=false), not bare pip/python3. The old pair installed websockets where the venv cannot import it and then "verified" it with the same wrong interpreter, so it reported success over a broken gateway. Install is now pinned to the image's websockets version; a missing venv warns instead of silently falling back. - install-websockets.sh: locate the adapter with the gateway interpreter and widen the find fallback. `find /app/venv -path '*/simplex/adapter.py'` returns nothing on a current Hermes layout, so the v1.3.0 DM-send verification silently skipped and exited 0. - entrypoint.sh: add port_listening() and use it for both readiness gates. `ss -tln | grep -q :5225` also matched 15225/52250/52251, and under host networking `ss` sees the whole host — an unrelated service could satisfy the startup gate. Both branches now anchor on the exact port: the `sport = :PORT` filter is re-verified with awk rather than `grep -q .`, since an ss that ignores the filter prints the whole socket table. - entrypoint.sh: /_address_settings now uses the userId read from the /user activeUser response (contract: bots/api/COMMANDS.md) instead of a literal 1, and confirms success by correlating corrId instead of matching an event substring. Skips with a warning when no id is known. - base-refresh.yml: skopeo is not on ubuntu-latest and had no install step, so the digest could come back empty with no set -e to catch it. Use the preinstalled buildx, set -euo pipefail, and fail loudly on empty. - README:196 described the removed sed -i adapter patching, contradicting line 210. Corrected, plus a warning on why the installer targets the venv. - Unraid template floated :latest while compose pinned a digest; pinned to v1.3.0 and README install steps updated to match. - CHANGELOG: pin prose said v1.2.0 above a v1.3.0 digest. Added: - SECURITY.md with a private reporting path and an explicit statement of the threat model (unauthenticated API, loopback default, socat opt-in). - tests/ regression suites, run by tests/run-all.sh and wired into the lint CI job: exact-port gate against real TCP listeners, installer interpreter/venv behaviour with docker+pip stubbed, and the setup handshake against a fake WebSocket daemon including a wrong-corrId decoy. check-docs.py enforces pin and env-var consistency. Verified: shellcheck clean; 4 suites green. Each behavioural suite was mutation-tested against a re-introduced version of its bug and failed as expected. Not verified: no container was built or booted (no Docker CLI), so socat startup, live SMP first-run setup, and real-silicon arm64 rest on the existing CI smoke matrix. Refs 2026-10-03 review (F1-F8)
The setup-handshake suite imported the real `websockets` at module scope and again inside run_case, so it failed on any runner without the package (caught by CI on PR #32: ModuleNotFoundError). The code under test only calls websockets.connect(), which the suite replaces with a fake daemon anyway, so inject a stub module into sys.modules for the duration of exec instead. Verified: 18/18 pass both with the real package present and with it made unimportable via a sys.meta_path blocker.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Remediation of the 2026-10-03 code review of
main@82c8882(v1.3.0). Eight findings: two High, three Medium, three Low.No image behaviour change for a correctly-configured deployment. The point of F1/F2/F3 is that three failure modes stop being silent and start being loud.
Fixes the review of
f3117dd(2026-09-29) landed are all still intact — this builds on PRs #28–#31 rather than reverting them.The two High findings
F1 —
install-websockets.shinstalledwebsocketswhere the gateway cannot import it.The script used bare
pip/python3viadocker exec. The gateway runs/app/venv/bin/python3, in a venv created withinclude-system-site-packages = false:So the dependency landed in the system interpreter (or
~/.local) — neither is on the venv'ssys.path— and the script then "verified" it with the same wrong interpreter, printed success, and left the platform unable to load. The documented happy path (README tells users to re-run this after every rebuild) could not work.The same defect made the adapter lookup fail:
import plugins...under the wrong interpreter raises, thenfind /app/venv -path '*/simplex/adapter.py'returns nothing on a current Hermes layout (the plugin lives in/app/hermes-agent-src). The script printed⚠ skipping DM send verificationand exited 0 — so the verification PR #28 added as its headline fix never ran.Now: resolves the venv explicitly (
HERMES_VENV, default/app/venv) for install and verification, pinswebsockets==17.0.1to match the image, widens thefind, and warns loudly if the venv is missing instead of silently falling back.F2 — both readiness gates matched any port containing the number.
Under
network_mode: host— which this project requires, so the containers share loopback —sslists the entire host's listening sockets. An unrelated host service on a port containing5225would satisfy the startup gate and report the WebSocket API ready whilesimplex-chatwas still starting or already dead. The socat gate had the same exposure.Replaced with a shared
port_listening()helper that anchors on the exact port. Both code paths anchor deliberately: thesport = :PORTfilter is also re-verified with awk rather than trusted viagrep -q ., because anssthat doesn't understand the filter prints the whole socket table andgrep -q .would match any line. (That second bug was in my first attempt at this fix — the mutation test caught it.)Medium and Low
/_address_settings 1 …used a literal1. The daemon's contract is/_address_settings <userId> <json(settings)>(verified inbots/api/COMMANDS.md@ v7.0.2), so the id now comes from the/useractiveUserresponse, withuserContactLinkCreated/usersListfallbacks. Success was detected by searching any event for the substringuserContactLinkUpdated; it now correlates thecorrIdthe daemon echoes (Server.hswraps every response as{corrId, resp}). Skips with a warning when no id is determinable, rather than applying settings to a guessed profile.README.md:196still described the removedsed -iadapter patching, fourteen lines below the note saying it was removed.base-refresh.ymlcalledskopeo, which isn't onubuntu-latestand had no install step — and with noset -ethe empty result was silent. Now uses preinstalleddocker buildx imagetools inspect,set -euo pipefail, and fails loudly on an empty digest.:latestwhile compose pinned a digest, so Unraid's Update button could silently jump versions. Pinned tov1.3.0.README.md:238said "v1.2.0" directly above a# v1.3.0digest.SECURITY.md— private reporting path plus an explicit threat-model statement (unauthenticated API, loopback default, socat opt-in removes that protection).Tests
No Docker or network needed —
bash tests/run-all.sh:test-port-gate.sh— creates real TCP listeners and assertsport_listening()matches an exact port: decoy-port case, the awk fallback for old iproute2, empty socket table, and fail-closed whenssis absent. The function body is extracted fromentrypoint.shat runtime so the test can't drift from what ships.test-installer-interpreter.sh— stubsdockerandpip, asserts the installer uses only absolute venv paths, pins the version, finds an adapter outside the venv, and fails on an unrecognised adapter. Skips (exit 77) where no Hermes install exists.test-setup-userid.py— drives the real setup block fromentrypoint.shagainst a fake WebSocket daemon, including a wrong-corrIddecoy that the old substring match would have accepted as success.check-docs.py— pin/version agreement across README, compose, and the template; documented env vars exist in code; installer executes nosed -i.Wired into the
lintCI job so a regression fails the build rather than surfacing at runtime.Verification
bash tests/run-all.shshellcheck entrypoint.sh install-websockets.sh tests/*.shpy_compile(healthcheck + tests)Not verified: no container was built or booted (no Docker CLI in the review environment). Socat bridge startup, first-run setup against live SMP servers, and real-silicon ARM64 remain covered only by the existing CI smoke matrix — which will exercise the reworked
port_listening()on both amd64 and emulated arm64.Upstream CLI flags used by the entrypoint were re-verified against
simplex-chatv7.0.2 source (-pchat-server-port,-yyes-migrate,-rmark-read,-xSOCKS5 :9050,--create-bot-*), as was the-dfile-prefix DB behaviour.