Provide a private Unix listener for owner-metered Messages
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
parent
dc77f434a8
commit
718e6730e4
4 changed files with 143 additions and 4 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue