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 set local-model-port = 0 or another port.

  • Each relayed port exposes the whole service behind it; the relay is not a path filter. local-port lives 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-port keeps its name for compatibility even though it is now one member of a more general set.

  • tests/local_model.sh runs the real relay probe with a model port and a local-port entry, for both a Pi and a Claude profile, and checks that an unlisted neighbouring port stays unreachable; tests/pi.sh covers merging, deduplication, validation and the model-port-zero case.