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),
|
[streaming](https://platform.claude.com/docs/en/build-with-claude/streaming),
|
||||||
[context editing](https://platform.claude.com/docs/en/build-with-claude/context-editing),
|
[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).
|
[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 hashlib
|
||||||
import http.client
|
import http.client
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
|
import socket
|
||||||
|
import stat
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from dataclasses import asdict, dataclass
|
from dataclasses import asdict, dataclass
|
||||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
from pathlib import Path
|
||||||
|
from socketserver import ThreadingUnixStreamServer
|
||||||
from typing import Any, NoReturn, Protocol, TypeGuard, cast
|
from typing import Any, NoReturn, Protocol, TypeGuard, cast
|
||||||
from urllib.parse import urlsplit
|
from urllib.parse import urlsplit
|
||||||
|
|
||||||
|
|
@ -345,6 +350,34 @@ class _OwnerHTTPServer(ThreadingHTTPServer):
|
||||||
owner: MessagesServer
|
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):
|
class _Handler(BaseHTTPRequestHandler):
|
||||||
def log_message(self, format: str, *args: Any) -> None:
|
def log_message(self, format: str, *args: Any) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
@ -360,7 +393,7 @@ class _Handler(BaseHTTPRequestHandler):
|
||||||
self.wfile.write(raw)
|
self.wfile.write(raw)
|
||||||
|
|
||||||
def do_POST(self) -> None:
|
def do_POST(self) -> None:
|
||||||
owner = cast(_OwnerHTTPServer, self.server).owner
|
owner = cast(_OwnerHTTPServer | _OwnerUnixServer, self.server).owner
|
||||||
upstream = None
|
upstream = None
|
||||||
sent = False
|
sent = False
|
||||||
try:
|
try:
|
||||||
|
|
@ -496,6 +529,7 @@ class MessagesServer:
|
||||||
host: str = "127.0.0.1",
|
host: str = "127.0.0.1",
|
||||||
port: int = 0,
|
port: int = 0,
|
||||||
allow_test_http: bool = False,
|
allow_test_http: bool = False,
|
||||||
|
unix_path: Path | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
endpoint = urlsplit(upstream_url)
|
endpoint = urlsplit(upstream_url)
|
||||||
if endpoint.scheme != "https" and not (
|
if endpoint.scheme != "https" and not (
|
||||||
|
|
@ -521,13 +555,36 @@ class MessagesServer:
|
||||||
raise RequestRefused("explicit provider credential required")
|
raise RequestRefused("explicit provider credential required")
|
||||||
self.policy, self.meter, self.endpoint = policy, meter, endpoint
|
self.policy, self.meter, self.endpoint = policy, meter, endpoint
|
||||||
self._provider_key = provider_key
|
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._httpd.owner = self
|
||||||
self._thread: threading.Thread | None = None
|
self._thread: threading.Thread | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def port(self) -> int:
|
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:
|
def start(self) -> None:
|
||||||
self._thread = threading.Thread(target=self._httpd.serve_forever, daemon=True)
|
self._thread = threading.Thread(target=self._httpd.serve_forever, daemon=True)
|
||||||
|
|
@ -538,3 +595,9 @@ class MessagesServer:
|
||||||
self._httpd.shutdown()
|
self._httpd.shutdown()
|
||||||
self._thread.join()
|
self._thread.join()
|
||||||
self._httpd.server_close()
|
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(
|
MessagesServer(
|
||||||
policy, DeniedMeter(), provider_key="dummy", upstream_url=url, allow_test_http=True
|
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
|
id: LLM-WP-0009-T03
|
||||||
status: wait
|
status: wait
|
||||||
priority: high
|
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"
|
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:
|
the protected artifact/release. Evidence:
|
||||||
`docs/evidence/2026-09-09-request-admission-quality.json`.
|
`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
|
## Repair historical source identities blocking primary synchronization
|
||||||
|
|
||||||
```task
|
```task
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue