Repository navigation
Description
The gap
docs/architecture/egress.md lists this under Known gaps:
Deleting a connection leaves its host's grant in
egress_destinations, hidden from Settings › Network, so a later connection to the same host is allowed without asking.
Why it matters
Disconnecting a server is how a user withdraws it. Afterwards Settings › Network stops listing that host, because the list is built from the hosts among the stored connections — so as far as the user can see, the permission is gone. The row is still in the table with enabled true, and the next connection to the same host inherits it: added, probed and talking to the endpoint without anyone being asked.
That is the one thing the egress design is meant to make impossible. It is also silent in both directions — the user cannot see the stale grant to revoke it, and they get no dialog when it is spent.
Where
surfsense_local/backend/modules/llm/connections/router.py—delete_connection()deletes the row and flushes. It never touchesegress_destinations.surfsense_local/backend/modules/egress/service.py—host_destination(base_url)gives the destination for a connection's URL and returnsNonefor loopback, andlist_destinations()already computes the set of hosts among the stored connections. Reuse both; do not parse a hostname by hand.
Note that selected_models.connection_id cascades on delete, so the model selections that used the connection are already cleared by the database. The grant is the one thing left behind.
What done looks like
- Deleting a connection to a host no other connection uses leaves no enabled row for that host, so adding a connection to it again asks.
- Deleting one of two connections that share a host does not withdraw the host, because the other one is still there and the user still consented to it. This is the case to get right, and the one your test should cover first.
- A loopback connection deletes cleanly, having had no destination to begin with.
- Settings › Network shows the same thing before and after, with no orphan row appearing.
- Delete the Known gaps line from
docs/architecture/egress.mdin the same PR.
What not to change
host:huggingface.co is BUILT_IN in modules/egress/service.py: it is contacted whether or not any connection points at it, and it is listed with no connection to discover it from. Whatever you write must not be able to drop it — a user who somehow adds a connection pointing at huggingface.co and then deletes it should not silently lose their model-download consent. Guard against BUILT_IN explicitly rather than relying on nobody doing that.
Do not change list_destinations() to solve this by hiding rows. The bug is the stored grant, not the listing; hiding it would leave the same row to be spent by the next connection.
How to verify
surfsense_local/backend/tests/integration/llm/test_egress.py is the file — it already covers a connection being refused before it is probed, a stored connection listed off and refused, and loopback needing no permission. Write the failing test first: allow a host, delete its connection, add a new connection to the same host, and assert it is refused.
cd surfsense_local/backend
uv run pytest -m integration -k egress
uv run pytest -m unit
uv run ruff check .
cd ../.. && python scripts/check_docs.pyBefore you start
Read CONTRIBUTING.md, the egress doc and ADR 0017. This is backend-only and needs no migration — the table shape is fine, only what writes to it is wrong. Open your PR against dev, not main. Comment here to claim this.
Activity
8rulerstar commentedon Sep 29, 2026
I'd like to take this one. Plan: in delete_connection(), drop the host's grant only when no other stored connection resolves to the same host (via host_destination / list_destinations), skip loopback, and explicitly guard BUILT_IN so huggingface.co can never be dropped. The first test will be the shared-host case, then allow → delete → re-add → refused. I'll remove the Known gaps line in the same PR, against dev.
MODSetter commentedon Sep 29, 2026
Fixed by #2027,
fix(local): withdraw a host's egress grant when its last connection is deleted, by @8rulerstar. Merged intodev.Closing by hand rather than by keyword:
Fixes #1956only auto-closes on a merge to the default branch, and this repo merges throughdev(CONTRIBUTING), so this would have stayed open and claimable while the work was already done. It reachesmainwith the next release.Nothing left to pick up here.