20. Relay a set of loopback ports to every agent#
Date: 2026-09-10
Status#
Accepted
Supersedes in part 19. Add Pi and an explicit single-port localhost relay (ADR 19), which restricted the relay to Pi sessions and to a single port.
Context#
ADR 19 added a local-model-port relay so Pi could use an lllm2 model bound
to the outer container’s 127.0.0.1:1920 from inside the egress jail. It was
gated to Pi because Pi was the only agent with a local-model use case, and to
one port because one was all that case needed.
Other loopback services are legitimate agent targets too: lllm2’s own panel
API on 8082 for running and comparing experiments, a local database, a dev
server under test. Nothing in the jail lets a Claude or Codex session reach
any of them. allow-ip punches an IP route through the gateway, which cannot
reach loopback, and pasta’s port forwarding and gateway mapping are
deliberately disabled for all agents (ADR 19). The only remaining option was
the CLAUDE_SANDBOX_EGRESS_JAIL=0 escape hatch, which gives the session the
whole host network to reach one local port.
Decision#
Make the relay a set of ports for every agent. local-model-port keeps
its meaning as the port Pi discovers a model on and stays in the set; the new
repeatable local-port key adds further ports, and CLAUDE_SANDBOX_LOCAL_PORTS
in the environment adds ports for one session. Conf and environment entries
are merged and deduplicated. The Pi-only gate on local_model_enabled is
gone.
The mechanism is unchanged per port: one relay, one IPv4 loopback port, a
private Unix socket between two socat processes, no IP route, no pasta
forwarding. Only bare port numbers are accepted; the host side is always
127.0.0.1 on the outer container, so no configuration can turn the relay
into a route to another host. That job stays with allow-ip.
The shipped default stays local-model-port = 1920, so a Claude or Codex
session sees lllm2’s model API on its loopback by default, the same as Pi.
Pi’s model discovery at startup remains Pi-only.
Consequences#
A configured relay is the way to reach loopback services from any jailed session; the jail escape hatch is no longer needed for that.
Every jailed session now runs two relay processes per configured port, and any Claude or Codex session can reach the whole service on the shipped port
1920. That service is a local model server, which the threat model does not treat as sensitive; operators who run something else there should setlocal-model-port = 0or another port.Each relayed port exposes the whole service behind it; the relay is not a path filter.
local-portlives in/etc, so a compromised session cannot add ports for its next launch, and the environment route is only available to whoever launches the session.The conf key
local-model-portkeeps its name for compatibility even though it is now one member of a more general set.tests/local_model.shruns the real relay probe with a model port and alocal-portentry, for both a Pi and a Claude profile, and checks that an unlisted neighbouring port stays unreachable;tests/pi.shcovers merging, deduplication, validation and the model-port-zero case.