Skip to content

Messenger (SimpleX): bridge, gateway, provisioning, forum and per-session chats with scoped phone approvals - #416

Merged
hmaximilian merged 18 commits into
mainfrom
agent/messenger-simplex
Oct 26, 2026
Merged

hmaximilian merged 18 commits into
mainfrom
agent/messenger-simplex

Conversation

@hmaximilian

Copy link
Copy Markdown
Collaborator

Design document and phases 1–3:

  • A unix-socket bridge in the agent service (SO_PEERCRED, redacted, answer-only).
  • The gateway package delfin/messenger/: serves only the paired contact, with rate, size and replay limits.
  • A provisioning script for the Pi, x86_64 login nodes and other servers. simplex-chat v7.0.3 is pinned by SHA-256 per platform.
  • Then: a SimpleX forum, per-session chats, and approvals scoped to the phone.

scripts/pi/pi_messenger.sh provisions a Pi, a login node or any other
server the pins cover: it picks the platform from uname and the Ubuntu
release from os-release, downloads the pinned simplex-chat v7.0.3 binary
(one SHA-256 per platform, digests read from the release page
2026-10-25), installs it for a system user delfin-gw without rights,
writes hardened systemd units (NoNewPrivileges, ProtectSystem=strict,
PrivateTmp, CapabilityBoundingSet=, RestrictAddressFamilies) for daemon
and gateway, and an nftables rule opening the daemon API port only to
meta skuid delfin-gw. Idempotent; unit dir, os-release and other paths
overridable for tests and non-standard systems. Tests drive the script
with stand-ins for uname/curl/mktemp/sha256sum/systemctl/nft: pins per
platform, refusal of an unpinned machine before any download, abort on a
digest mismatch, unit hardening, nftables skuid rule, no fixed /tmp
name, no moving release reference.

Co-Authored-By: DELFIN-Agent
…rvice

Design doc: docs/design/messenger-simplex.md (phases 1 and 2).

The bridge serves the gateway over a unix socket in the agent service's
runtime dir, 0660; a connection is accepted only when SO_PEERCRED says
the peer runs as the configured uid. Fixed JSON-line commands (status,
sessions, pending, answer, prompt, stop); answer reaches only
attention.answer, so an approval is never granted from the phone; every
line out goes through the service Redactor and is cut. The gateway (new
package delfin/messenger/) speaks the simplex-chat daemon's local
WebSocket API and the bridge, accepts only the paired contact, applies
rate/size/replay limits, answers questions by number or text, and routes
tasks with @name or to the phone session. Entry points:
python -m delfin.messenger pair|run|revoke|status. Tests for both
against fake service and fake daemon; no network in the tests.

Co-Authored-By: DELFIN-Agent
Comment thread delfin/agent/service/bridge.py Dismissed
configured_peer_uid returned DELFIN_MESSENGER_PEER_UID unchanged. Set to
the service's own uid, the bridge admitted every process of that user,
the agent's shell commands included, which could then approve the
agent's own requests through the phone channel.
configured_peer_uid returns None (nobody is admitted) and says so on
stderr when DELFIN_MESSENGER_PEER_UID equals the service's own uid. The
provisioning runs the gateway as a user of its own; only a hand-set value
on a host without that user hit this, and there it admitted every
process of the user, including the agent's shell commands.

Tests: the new file 2/2 (1 red before); all messenger test files: 103
passed. CI lint rule set: clean.
…wins

Against a real agent service (tui_service.ServiceProcess):
- the window answers first: the phone's code for that request is
  refused afterwards and the request resolves as the window decided;
- the same shell command asked again next turn gets a new request id,
  and the code issued for the first does not approve it; it stays
  pending for the window.

Both hold on 51ad607; 3 runs, 2/2 each.
Rooms.voice gets the local transcript, the chat and the reply reference:
- no reply: the next prompt, at once;
- reply to an approval: exactly a decision word ('Ja.', 'nein!', an
  offered label) answers it like the typed word;
- a sentence, a mumble or an empty transcript frees nothing;
- a lasting rule is never confirmed by voice;
- reply to a question: the whole transcript is the answer.
All seven fail on 9a13bf1 (no Rooms.voice).
Rooms.voice decides on a local transcript, and handle_file calls it
right after transcription:
- no reply reference: the session's next prompt, at once (before: shown
  and submitted only after a typed "senden");
- reply to a question: the transcript is the answer;
- reply to an approval: only a transcript that is exactly a decision
  word (ja/jawohl/yes, nein/no) or an offered option's label, after
  dropping case, outer space and sentence punctuation, goes through the
  same Rooms.reply path as the typed word; the phone is told what was
  understood. Anything else frees nothing and says so;
- a lasting rule (confirmChoice) is never confirmed by voice.
Reply-reference binding, the 600 s expiry and the bridge's
request-bound codes are unchanged, because voice goes through reply().

Rejected: matching yes/no anywhere in a sentence ("ja aber nicht
jetzt" would approve).

Tests: the voice file 7/7 (red before); all messenger files: 112
passed. CI lint rule set: clean.
faster-whisper 1.2.1 needs numpy 2.x; DELFIN in the gateway venv keeps
numpy below 2. transcribe ran 'sys.executable -m delfin.messenger.media'
in the gateway venv. Red: the named interpreter is not used.
Four checks on scripts/pi/pi_messenger.sh with the stand-in tools:
the model revision and all four file digests are in the script and the
packages install with --require-hashes from per-arch lists; a wrong
digest installs no model and fails; a verified model reaches the gateway
unit as DELFIN_WHISPER_MODEL/DELFIN_WHISPER_PYTHON; a gateway python
other than 3.11 is refused. The harness's mktemp stand-in learns -d and
sha256sum can answer per file. Red on 2 commits back; the 10 existing
provisioning tests still pass.
- media.transcribe runs DELFIN_WHISPER_PYTHON (default: this
  interpreter) with -I and a self-contained script that imports only
  faster-whisper; faster-whisper needs numpy 2, DELFIN keeps numpy < 2.
- pi_messenger.sh --whisper: a venv of its own from the gateway's
  CPython 3.11 (another version is refused, the wheels are cp311);
  faster-whisper 1.2.1 with --require-hashes --no-deps from
  whisper-requirements-{aarch64,x86_64}.txt (23 pins each, uv
  --generate-hashes); faster-whisper-small at revision 536b0662, the
  four files checked against their SHA-256 (a mismatch installs no
  model), root-owned 0644 under /opt/delfin-messenger/models. The
  gateway unit carries DELFIN_WHISPER_MODEL/_PYTHON only for a verified
  model. Files already there with the right digest are kept.
- docs/design and scripts/pi/README describe the voice rules.

Checked here (x86_64): the venv from the pinned list installs
(faster-whisper 1.2.1, numpy 2.4.6), the four model files pass
sha256sum -c, and transcribe() returns the upstream test recording
jfk.flac verbatim in 4.3 s. Tests: all messenger and transcriber files
green; the provisioning tests 15/15; CI lint clean.
CI on 3752f57 failed four shards:
- test_no_personal_data_is_committed: the whisper requirement lists
  carried a '# via -r <session scratch path>' line from their
  generation. Replaced with the requested package.
- the trust-store and capabilities tests: sandbox._HOME_SECRET_DIRS
  gained .delfin/messenger_sessions, boundaries.MASKED_HOME_ENTRIES
  did not. Both lists name it now.
- two edit-baseline tests: a shell command could not create a file in
  its own workspace ('Permission denied'). runtime_masked_dirs created
  and masked activation._DIR. Inside a workspace that contains it (the
  tests' tmp_path; a workspace at $HOME in the field) the workspace
  lost its write root under Landlock, which cannot carve a hole into an
  allowed tree. The runtime mask and its mkdir on every cage build are
  gone. The store stays masked as a home secret and refused by the path
  deny globs, like the trust store.

tests/test_messenger_session_activation.py now pins that layout.
Locally: the five failing files, all messenger files, the race and
transcriber tests: 180 passed. CI lint clean.
@hmaximilian
hmaximilian merged commit 8d95041 into main Oct 26, 2026
26 checks passed
@hmaximilian
hmaximilian deleted the agent/messenger-simplex branch October 26, 2026 21:12
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