Provide a private Unix listener for owner-metered Messages
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
tegwick 2026-09-09 22:20:55 +02:00
parent dc77f434a8
commit 718e6730e4
4 changed files with 143 additions and 4 deletions

View file

@ -81,3 +81,16 @@ Protocol references inspected 2026-09-09:
[streaming](https://platform.claude.com/docs/en/build-with-claude/streaming),
[context editing](https://platform.claude.com/docs/en/build-with-claude/context-editing),
[prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching).
## Private Unix hosting
`MessagesServer(..., unix_path=Path(...))` binds AF_UNIX only, mode 0600 inside
an owner-owned mode 0700 directory. It refuses an existing path rather than
unlinking another listener, and removes only its own socket inode on stop.
At most 16 request handlers are active; idle header reads time out. TCP host/port
selection cannot coexist with Unix mode. The protocol and durable meter interface
are unchanged. Rein's MessagesOwner supplies the private listener, accepted lease
and cancellation hooks; sand-boxer mounts only the socket and enforces sole routing.
The provider key and ledger remain outside the workload. This source/library path
is tested with a fake provider; accepted custody, protected bootstrap/artifact and
Railiance placement are still required by LLM-WP-0009-T03.

View file

@ -10,11 +10,16 @@ from __future__ import annotations
import hashlib
import http.client
import json
import os
import re
import socket
import stat
import threading
import time
from dataclasses import asdict, dataclass
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from socketserver import ThreadingUnixStreamServer
from typing import Any, NoReturn, Protocol, TypeGuard, cast
from urllib.parse import urlsplit
@ -345,6 +350,34 @@ class _OwnerHTTPServer(ThreadingHTTPServer):
owner: MessagesServer
class _OwnerUnixServer(ThreadingUnixStreamServer):
owner: MessagesServer
daemon_threads = True
def server_bind(self) -> None:
self._slots = threading.BoundedSemaphore(16)
super().server_bind()
def process_request(self, request: socket.socket | tuple[bytes, socket.socket], client_address: Any) -> None:
if not isinstance(request, socket.socket):
raise TypeError("Unix stream socket required")
if not self._slots.acquire(blocking=False):
request.close()
return
request.settimeout(10)
try:
super().process_request(request, client_address)
except BaseException:
self._slots.release()
raise
def process_request_thread(self, request: socket.socket | tuple[bytes, socket.socket], client_address: Any) -> None:
try:
super().process_request_thread(request, client_address)
finally:
self._slots.release()
class _Handler(BaseHTTPRequestHandler):
def log_message(self, format: str, *args: Any) -> None:
pass
@ -360,7 +393,7 @@ class _Handler(BaseHTTPRequestHandler):
self.wfile.write(raw)
def do_POST(self) -> None:
owner = cast(_OwnerHTTPServer, self.server).owner
owner = cast(_OwnerHTTPServer | _OwnerUnixServer, self.server).owner
upstream = None
sent = False
try:
@ -496,6 +529,7 @@ class MessagesServer:
host: str = "127.0.0.1",
port: int = 0,
allow_test_http: bool = False,
unix_path: Path | None = None,
) -> None:
endpoint = urlsplit(upstream_url)
if endpoint.scheme != "https" and not (
@ -521,13 +555,36 @@ class MessagesServer:
raise RequestRefused("explicit provider credential required")
self.policy, self.meter, self.endpoint = policy, meter, endpoint
self._provider_key = provider_key
self._httpd = _OwnerHTTPServer((host, port), _Handler)
self._unix_path = unix_path
self._httpd: _OwnerHTTPServer | _OwnerUnixServer
if unix_path is not None:
if host != "127.0.0.1" or port != 0:
raise RequestRefused("Unix route cannot also select a TCP listener")
parent = unix_path.parent
metadata = parent.lstat()
if (
not unix_path.is_absolute()
or parent.resolve() != parent
or not stat.S_ISDIR(metadata.st_mode)
or metadata.st_uid != os.getuid()
or stat.S_IMODE(metadata.st_mode) != 0o700
):
raise RequestRefused("Unix route requires a private owner directory")
# bind refuses existing paths, including dangling symlinks. Never unlink
# someone else's socket to recover a crashed or duplicated owner.
self._httpd = _OwnerUnixServer(str(unix_path), _Handler)
unix_path.chmod(0o600)
self._unix_inode = unix_path.stat().st_ino
else:
self._httpd = _OwnerHTTPServer((host, port), _Handler)
self._httpd.owner = self
self._thread: threading.Thread | None = None
@property
def port(self) -> int:
return int(self._httpd.server_address[1])
if self._unix_path is not None:
raise RequestRefused("Unix route has no TCP port")
return int(cast(_OwnerHTTPServer, self._httpd).server_address[1])
def start(self) -> None:
self._thread = threading.Thread(target=self._httpd.serve_forever, daemon=True)
@ -538,3 +595,9 @@ class MessagesServer:
self._httpd.shutdown()
self._thread.join()
self._httpd.server_close()
if self._unix_path is not None:
try:
if self._unix_path.lstat().st_ino == self._unix_inode:
self._unix_path.unlink()
except FileNotFoundError:
pass

View file

@ -187,3 +187,37 @@ def test_fixed_origin_no_redirect_proxy_or_request_url(policy, url):
MessagesServer(
policy, DeniedMeter(), provider_key="dummy", upstream_url=url, allow_test_http=True
)
def test_unix_listener_private_directory_and_no_tcp(tmp_path, policy):
import http.client
import socket
from llm_connect.messages_gate import MessagesServer, RequestRefused
private = tmp_path / "private"
private.mkdir(mode=0o700)
path = private / "messages.sock"
server = MessagesServer(policy, DeniedMeter(), provider_key="dummy", unix_path=path)
assert path.stat().st_mode & 0o777 == 0o600
with pytest.raises(RequestRefused, match="no TCP"):
_ = server.port
with pytest.raises(OSError):
MessagesServer(policy, DeniedMeter(), provider_key="dummy", unix_path=path)
server.start()
try:
connection = http.client.HTTPConnection("localhost", timeout=3)
connection.sock = socket.socket(socket.AF_UNIX)
connection.sock.settimeout(3)
connection.sock.connect(str(path))
connection.request("POST", "/execute", "{}", {"Content-Type": "application/json"})
response = connection.getresponse()
assert response.status == 404
response.read()
connection.close()
finally:
server.stop()
assert not path.exists()
private.chmod(0o755)
with pytest.raises(RequestRefused, match="private"):
MessagesServer(policy, DeniedMeter(), provider_key="dummy", unix_path=path)

View file

@ -57,7 +57,7 @@ No actual inference, provider credential or live price/FX policy is involved.
id: LLM-WP-0009-T03
status: wait
priority: high
blocking_reason: "Requires trusted owner hosting and actual lease/token delivery, provider custody and direct-route denial under HFACT T03/T04; accepted tariff/FX and G0 remain HFACT T01."
blocking_reason: "Local Unix hosting, worker lease/token lifecycle and bwrap confinement proved; requires admitted credential-to-owner bootstrap, matched protected artifact and Railiance custody/placement under HFACT T03/T04; live tariff/FX and G0 remain HFACT T01."
state_hub_task_id: "98a38d75-73ab-5f37-b710-5df9d9681e49"
```
@ -81,6 +81,35 @@ Resolve or explicitly disposition those existing checks before an owner accepts
the protected artifact/release. Evidence:
`docs/evidence/2026-09-09-request-admission-quality.json`.
### Local owner-route integration return — 2026-09-09
`MessagesOwner` now starts an owner-only Unix Messages listener after the worker
reserves its parent envelope. The initial accepted Activity Core heartbeat supplies
its exact expiry, run, worker and attempt binding. Lease loss, timeout, signals,
gateway exceptions and normal exit revoke the route. A timer also enforces the
initial lease deadline; heartbeat renewal does not extend this first route.
Only the opaque token and namespace-local base URL reach the child. Provider key,
ledger and owner socket directory remain outside the workload's mounts and PID
namespace. The sandbox refuses direct egress, alternate credential delivery,
extra host mounts, consumer mismatches and additional sandboxes for this binding.
Real local bwrap tests prove sole-route forwarding, direct host/public-IP denial,
private-state absence, revocation with no second forward, and teardown. The actual
worker/Glas/bwrap/ledger path also imports its permitted fixture commit and replays
a lost terminal close without repeating the request or authoring. Queue, provider,
credential and authoring remain deterministic fixtures; factory attempts remain 0.
Worker suite: 377 passed, including installed CLI and real namespace tests.
Sand-boxer required `make check`: lint clean, 199 tests passed. LLM suite: 264
passed; changed transport adds no lint/type diagnostics, with existing full-repo
177 Ruff/36 mypy diagnostics still requiring pre-release disposition.
This closes local source route/lease/token/confinement wiring. Remaining return:
admitted owner bootstrap that supplies the provider key to `MessagesOwner`, matched
protected runtime/CLI artifact, Railiance host/profile/consumer/custody/recovery
admission, live provider compatibility and accepted bounds/tariffs/FX, then G0 and
natural model/queue evidence. No protected runtime was installed or promoted,
no existing CCR changed, no secret read or paid execution took place.
## Repair historical source identities blocking primary synchronization
```task