Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0a2e258
docs: add reviewed dev plan for UDS server-side trust-boundary hardening
vr000m Jun 27, 2026
5059610
docs: harden UDS trust-boundary plan per Codex review
vr000m Jun 27, 2026
839e718
conduct: phase 1 — Parent-directory enforcement
vr000m Jun 27, 2026
49b34dc
conduct: phase 2 — Peer-credential resolver
vr000m Jun 27, 2026
7773c79
conduct: phase 3 — Wire peer-cred into the handshake
vr000m Jun 27, 2026
35de5ec
conduct: phase 4 — Local end-to-end smoke (multi-connection + cross-uid)
vr000m Jun 27, 2026
37aa5f2
conduct: phase 5 — Docs + plan/README sync
vr000m Jun 27, 2026
f1f62cf
feat: onboard backend extras via just stt-install/stt-enable
vr000m Jun 27, 2026
ee1e66e
feat: actionable error when a backend's optional extra is missing
vr000m Jun 27, 2026
0d9410e
fix: make cross-uid peercred smoke actually reach the gate
vr000m Jun 27, 2026
f9e8cdf
chore(release): prepare 0.3.3 (UDS trust-boundary hardening)
vr000m Jun 27, 2026
9b167e3
test: add stdlib cross-uid peer-cred verifier (nobody, no venv)
vr000m Jun 27, 2026
16cccea
docs: record cross-uid peer-cred verification (real foreign uid)
vr000m Jun 27, 2026
ed7a9c2
test: narrow Koda-surface guard to imported surface; repoint per-ASR …
vr000m Jun 27, 2026
2d87e46
fix: address code-review findings (peer-cred unsigned uid, ImportErro…
vr000m Jun 27, 2026
ed5f827
docs: sync documentation with feature/uds-trust-boundary-hardening ch…
vr000m Jun 28, 2026
ffd921f
fix(server): bind UDS on the verified resolved socket path
vr000m Jun 28, 2026
62def0c
fix(cli): broaden serve startup handler to ImportError
vr000m Jun 28, 2026
f24121f
test(peercred): mirror unsigned 3I SO_PEERCRED format in mock
vr000m Jun 28, 2026
8f13cca
refactor(scripts): extract shared assert_chain_traversable
vr000m Jun 28, 2026
cd66ad9
docs(changelog): note websockets>=16 consumer-conflict caveat
vr000m Jun 28, 2026
a50624c
fix(server): verify literal socket-dir chain, reject symlink components
vr000m Jun 28, 2026
f9f41b6
fix(install): validate socket path before mutating the filesystem
vr000m Jun 28, 2026
c07cd6d
docs: sync documentation for 0.3.3 release
vr000m Jun 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,63 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.3.3] - 2026-06-28

Server-side hardening of the same-host UDS trust boundary. No new user-facing
features; clients connect unchanged (no protocol or client-API change).

### Added

- **Server-side UDS peer-credential authentication.** The server rejects any
connection whose kernel-reported peer uid `!= os.geteuid()` with a
pre-handshake HTTP `403` (`peer not permitted`), before the WebSocket
handshake. Uses macOS `getpeereid(2)` (via `ctypes`) / Linux `SO_PEERCRED`;
every resolver failure path fails closed (rejects). UDS only — TCP keeps the
Origin + bearer-token checks. No client change required.
- **Socket-directory ancestor enforcement.** Before bind, the server walks every
directory from the socket's parent up to and including `$HOME` and refuses to
start unless each is owner-owned and not group/other-writable (sticky-bit dirs
excepted), creating missing dirs `0700`. The walk is over the **literal** path
clients traverse and **rejects any symlink component** (a symlinked, writable
lexical ancestor would otherwise be repointable post-startup to hijack the
client-visible socket). This makes the socket un-plantable (no foreign uid can
`unlink`+`bind` a replacement).
- **Backend-extra onboarding.** `just stt-install` / `stt-enable` now ensure the
selected backend's optional extra is installed (`uv sync --extra <X>
--inexact`) so a freshly installed agent doesn't crash-loop on a missing
import; `PIPECAT_STT_SKIP_DEP_SYNC=1` opts out.
- `scripts/smoke_peercred.py` + `just smoke-peercred`: a local cross-uid /
multi-connection peer-cred smoke (cross-uid leg skips cleanly without a second
local uid).
- `scripts/verify_peercred_crossuid.py`: a stdlib-only cross-uid verifier that
drives a probe as both the owning uid and `nobody` against one permissive
socket — proving peer-cred (not the filesystem) is the discriminator. Needs no
venv/`websockets`, so it works with `nobody` where the smoke's `sudo` path
can't.

### Changed

- Pinned `websockets` to `>=16,<17` (was `>=13`; the `_process_request`
handshake/transport contract depends on the v16 API). Applies to every extra,
including `client`. See Upgrade notes for the consumer-conflict caveat.
- `scripts/install_stt_agent.sh` creates the socket directory `0700` (was the
install-shell umask default) and self-heals a pre-existing `0755` dir. It now
validates a custom `PIPECAT_STT_SOCKET` against the same rules the server
enforces (absolute, under `$HOME`, no symlink component) **before** any
`mkdir`/`chmod`, so a path the server would reject fails the install cleanly
with no filesystem mutation instead of tightening the directory and then
crash-looping the agent.
- Startup failures — socket-dir enforcement, the `ServerConfig` `ValueError`,
bind `OSError`s, and a missing backend extra — surface as
`stt_server: <msg>` + exit 1 instead of a bare traceback.

### Security

- Closes the same-host UDS plant/swap and foreign-uid-connect vectors: the
owner-only ancestor chain is the primary filesystem boundary and peer-cred is
the kernel-authoritative backstop. The bearer token is retained for TCP/remote
(which has neither boundary). See [`docs/operations.md`](docs/operations.md).

### Documentation

- **Split the 593-line README into a focused top page + `docs/`.** The README now
Expand All @@ -19,6 +76,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
and [`docs/migration.md`](docs/migration.md) (0.1.x → 0.2.0 upgrade). A
Documentation index links them; all cross-references were repointed. No content
was dropped.
- Documented the same-host UDS trust model and the same-uid precondition in
`docs/operations.md`; added a pre-handshake connection-rejection table to
`docs/protocol.md`.

### Upgrade notes

- **May require action on existing hosts.** Because the server now refuses to
start against a group/other-writable socket-dir ancestor, an existing `0755`
socket directory — or a custom `STT_WS_SOCKET` / `KODA_STT_SOCKET` pointing
outside `$HOME` — will block startup. Re-run `scripts/install_stt_agent.sh`
(self-heals the dir to `0700`), or `chmod 700` the dir / move the socket under
`$HOME`. Koda hosts run the server from the checkout, so fold this into the
next checkout update; no client pin bump is needed.
- **`websockets>=16` may conflict for library consumers.** The floor moved from
`>=13` to `>=16,<17` across every extra (`client` included), so an environment
that resolves an older `websockets` (or another package capping it `<16`) will
hit a dependency conflict when installing this version. The wire protocol and
`stt_server.client` API are unchanged — only the dependency floor moved — so
this is a packaging bump, not a behavioural one, and stays a patch release
(`0.3.3`).

## [0.3.2] - 2026-06-08

Expand Down Expand Up @@ -232,6 +309,8 @@ import name `stt_server`.
- Wire protocol is unchanged: `PROTOCOL_VERSION == "0.1"`; the `server.hello`
and `server.status` shapes are stable.

[0.3.3]: https://github.com/vr000m/pipecat-local-stt-server/releases/tag/v0.3.3
[0.3.2]: https://github.com/vr000m/pipecat-local-stt-server/releases/tag/v0.3.2
[0.3.1]: https://github.com/vr000m/pipecat-local-stt-server/releases/tag/v0.3.1
[0.3.0]: https://github.com/vr000m/pipecat-local-stt-server/releases/tag/v0.3.0
[0.2.0]: https://github.com/vr000m/pipecat-local-stt-server/releases/tag/v0.2.0
Expand Down
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@ uv run python -m stt_server --host 127.0.0.1 --port 8765 --auth-token-file /path

> **Backends need their own install.** Every `--backend` above except `echo`
> requires a `uv sync` extra first — `mlx`/`parakeet`/`nemotron` (e.g.
> `uv sync --extra mlx`). Without it you get e.g. `ModuleNotFoundError: No module
> named 'mlx_whisper'`. See [Choosing a backend and model](#choosing-a-backend-and-model)
> `uv sync --extra mlx`). Without it the server exits with an actionable
> `stt_server: the '<extra>' extra is not installed … run: uv sync --extra <extra>
> --inexact` message. See [Choosing a backend and model](#choosing-a-backend-and-model)
> for each backend's install command and `--model` defaults.

The CLI accepts both `python -m stt_server <flags>` (the legacy flat form,
Expand Down Expand Up @@ -98,14 +99,20 @@ just stt-list # every pipecat.stt-server* agent: state, pid, live b
just stt-status nemotron # wire health probe for one backend
just stt-disable whisper # stop until next login (keeps the plist)
just stt-enable whisper # re-load it from the existing plist
just stt-install parakeet # delegates to install_stt_agent.sh
just stt-install parakeet # delegates to install_stt_agent.sh (+ ensures the extra)
just stt-uninstall parakeet
just smoke-peercred # local UDS peer-cred smoke (cross-uid leg needs a 2nd uid)
```

`<backend>` is one of `whisper` / `parakeet` / `nemotron`, mapped to the labels
and sockets in the [per-ASR table](docs/operations.md#per-asr-socket-convention)
(the justfile map is a checked mirror of that table — a test fails CI on drift).

`stt-install` / `stt-enable` also ensure the backend's optional Python extra is
installed (`uv sync --extra <backend> --inexact`) so a freshly installed agent
doesn't crash-loop on a missing import; set `PIPECAT_STT_SKIP_DEP_SYNC=1` to
manage the extras yourself.

`stt-list` prints each agent's `socket:` line in the same `~`-form a consumer's
config uses for its endpoint (e.g. onoats' `config.toml` `[stt] ws_socket`), so
you can match a config line to a running agent directly. Note whisper's socket is
Expand Down
Loading
Loading