FrameBridge 0.3.1 community source release
This commit is contained in:
commit
b23819fff1
1354 files changed
+343716
No files matched your search
@@ -0,0 +1,358 @@
|
||||
"""Private connection logs and evidence-based terminal status.
|
||||
|
||||
This module monitors the packaged tunnel client. A ready tunnel is not evidence
|
||||
that the intended ChatGPT account has completed consent or made a tool call.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import fcntl
|
||||
import http.client
|
||||
import ipaddress
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import selectors
|
||||
import stat
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
import unicodedata
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
MAX_LINE = 64 * 1024
|
||||
MAX_LOG = 2 * 1024 * 1024
|
||||
LOG_COPIES = 3
|
||||
POLL_FRESH_SECONDS = 75
|
||||
_ANSI = re.compile(r"\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\))")
|
||||
|
||||
|
||||
class ConnectionMonitorError(RuntimeError):
|
||||
"""A safe public error; details belong only in sanitized logs."""
|
||||
|
||||
|
||||
def _private_directory(path):
|
||||
path = Path(path).absolute()
|
||||
for part in [path, *path.parents]:
|
||||
if part.is_symlink():
|
||||
raise ConnectionMonitorError("The private connection directory is unsafe.")
|
||||
try:
|
||||
path.mkdir(mode=0o700)
|
||||
except FileExistsError:
|
||||
pass
|
||||
info = path.lstat()
|
||||
if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid() or info.st_mode & 0o077:
|
||||
raise ConnectionMonitorError("The private connection directory is unsafe.")
|
||||
return path
|
||||
|
||||
|
||||
def redact(text, secret):
|
||||
"""Normalize terminal controls before redacting and persisting a record."""
|
||||
text = _ANSI.sub("", str(text))
|
||||
text = "".join(c for c in text if c in "\n\t" or not unicodedata.category(c).startswith("C"))
|
||||
if secret:
|
||||
text = text.replace(secret, "[REDACTED]")
|
||||
# A child may exit after emitting only a credential prefix.
|
||||
tail = text.rstrip("\r\n")
|
||||
suffix = text[len(tail):]
|
||||
for length in range(min(len(secret) - 1, len(tail)), 7, -1):
|
||||
if tail.endswith(secret[:length]):
|
||||
text = tail[:-length] + "[REDACTED]" + suffix
|
||||
break
|
||||
text = re.sub(r"\bsk-[A-Za-z0-9_-]{8,}", "[REDACTED]", text)
|
||||
# Preserve a useful proxy/server location without credential-bearing userinfo.
|
||||
text = re.sub(r"(?i)(\b[a-z][a-z0-9+.-]*://)[^/\s?#]*@", r"\1[REDACTED]@", text)
|
||||
# Conservative whole-tail redaction prevents partial Cookie/password leaks.
|
||||
text = re.sub(r"(?im)(\b(?:authorization|proxy-authorization|api[_-]?key|access[_-]?token|"
|
||||
r"refresh[_-]?token|password|passwd|cookie|set-cookie)\b[\"']?\s*[:=]\s*).*$",
|
||||
r"\1[REDACTED]", text)
|
||||
return text
|
||||
|
||||
|
||||
class PrivateLog:
|
||||
"""A bounded rotating log, protected against symlinks and shared files."""
|
||||
def __init__(self, state, secret, *, max_bytes=MAX_LOG):
|
||||
root = _private_directory(state)
|
||||
root = _private_directory(root / "logs")
|
||||
self.directory = _private_directory(root / "chatgpt")
|
||||
self.path = self.directory / "connection.log"
|
||||
self.secret = secret
|
||||
self.max_bytes = max_bytes
|
||||
self.directory_fd = os.open(self.directory, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
|
||||
try:
|
||||
self.lock_fd = self._open("connection.lock", os.O_RDWR | os.O_CREAT)
|
||||
except Exception:
|
||||
os.close(self.directory_fd)
|
||||
raise
|
||||
self.mutex = threading.Lock()
|
||||
|
||||
def _open(self, name, flags):
|
||||
fd = os.open(name, flags | os.O_NOFOLLOW | os.O_CLOEXEC | os.O_NONBLOCK, 0o600, dir_fd=self.directory_fd)
|
||||
info = os.fstat(fd)
|
||||
if (not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid()
|
||||
or info.st_nlink != 1 or info.st_mode & 0o077):
|
||||
os.close(fd)
|
||||
raise ConnectionMonitorError("The private connection log is unsafe.")
|
||||
return fd
|
||||
|
||||
def _validate_existing(self, name):
|
||||
try:
|
||||
fd = self._open(name, os.O_RDONLY)
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
os.close(fd)
|
||||
return True
|
||||
|
||||
def write(self, text):
|
||||
clean = redact(str(text), self.secret)
|
||||
# All callers provide a complete bounded logical record.
|
||||
if len(clean.encode("utf-8")) > MAX_LINE * 4:
|
||||
clean = "[Oversized log record omitted]"
|
||||
stamp = datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds")
|
||||
data = (stamp + " " + clean.rstrip("\r\n") + "\n").encode("utf-8", errors="replace")
|
||||
with self.mutex:
|
||||
deadline = time.monotonic() + 1.5
|
||||
while True:
|
||||
try:
|
||||
fcntl.flock(self.lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
break
|
||||
except BlockingIOError:
|
||||
if time.monotonic() >= deadline:
|
||||
raise ConnectionMonitorError("The private connection log is busy.") from None
|
||||
time.sleep(0.02)
|
||||
try:
|
||||
fd = self._open("connection.log", os.O_WRONLY | os.O_APPEND | os.O_CREAT)
|
||||
try:
|
||||
size = os.fstat(fd).st_size
|
||||
finally:
|
||||
os.close(fd)
|
||||
if size + len(data) > self.max_bytes:
|
||||
existing = {i: self._validate_existing("connection.log" + ("." + str(i) if i else ""))
|
||||
for i in range(LOG_COPIES)}
|
||||
if existing[LOG_COPIES - 1]:
|
||||
os.unlink("connection.log." + str(LOG_COPIES - 1), dir_fd=self.directory_fd)
|
||||
for i in range(LOG_COPIES - 2, -1, -1):
|
||||
if existing[i]:
|
||||
os.rename("connection.log" + ("." + str(i) if i else ""),
|
||||
"connection.log." + str(i + 1),
|
||||
src_dir_fd=self.directory_fd, dst_dir_fd=self.directory_fd)
|
||||
fd = self._open("connection.log", os.O_WRONLY | os.O_APPEND | os.O_CREAT)
|
||||
try:
|
||||
view = memoryview(data)
|
||||
while view:
|
||||
count = os.write(fd, view)
|
||||
if count <= 0:
|
||||
raise OSError("Short private log write")
|
||||
view = view[count:]
|
||||
finally:
|
||||
os.close(fd)
|
||||
finally:
|
||||
fcntl.flock(self.lock_fd, fcntl.LOCK_UN)
|
||||
|
||||
def close(self):
|
||||
self.secret = None
|
||||
for name in ("lock_fd", "directory_fd"):
|
||||
fd = getattr(self, name, None)
|
||||
if fd is not None:
|
||||
os.close(fd)
|
||||
setattr(self, name, None)
|
||||
|
||||
|
||||
class Attempt:
|
||||
def __init__(self, state):
|
||||
root = _private_directory(state)
|
||||
self.root = _private_directory(root / "connection-runtime")
|
||||
self.directory = _private_directory(self.root / uuid.uuid4().hex)
|
||||
self.health_file = self.directory / "health.url"
|
||||
|
||||
def close(self):
|
||||
# Remove only this attempt's own known file, never a symlink or unknown data.
|
||||
try:
|
||||
info = self.health_file.lstat()
|
||||
if stat.S_ISREG(info.st_mode) and info.st_uid == os.getuid() and info.st_nlink == 1:
|
||||
self.health_file.unlink()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
try:
|
||||
self.directory.rmdir()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def health_endpoint(path):
|
||||
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_CLOEXEC | os.O_NONBLOCK)
|
||||
try:
|
||||
info = os.fstat(fd)
|
||||
if (not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid() or info.st_nlink != 1
|
||||
or info.st_mode & 0o022 or not 0 < info.st_size <= 512):
|
||||
raise ConnectionMonitorError("The tunnel health endpoint is unsafe.")
|
||||
text = os.read(fd, 513).decode("ascii").strip()
|
||||
finally:
|
||||
os.close(fd)
|
||||
value = urlsplit(text)
|
||||
try:
|
||||
address = ipaddress.ip_address(value.hostname or "")
|
||||
port = value.port
|
||||
except ValueError:
|
||||
raise ConnectionMonitorError("The tunnel health endpoint is unsafe.") from None
|
||||
if (value.scheme != "http" or not address.is_loopback or not port or "%" in text
|
||||
or value.username or value.password or value.path not in ("", "/")
|
||||
or value.query or value.fragment):
|
||||
raise ConnectionMonitorError("The tunnel health endpoint is unsafe.")
|
||||
return str(address), port
|
||||
|
||||
|
||||
def _get(endpoint, path, limit):
|
||||
# Direct literal-loopback HTTP: no DNS, inherited proxy, or redirects.
|
||||
connection = http.client.HTTPConnection(*endpoint, timeout=0.6)
|
||||
try:
|
||||
connection.request("GET", path, headers={"Connection": "close"})
|
||||
response = connection.getresponse()
|
||||
body = response.read(limit + 1)
|
||||
if len(body) > limit:
|
||||
raise ConnectionMonitorError("The tunnel health response is too large.")
|
||||
return response.status, body
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
|
||||
def probe(health_file, expected_tunnel, expected_mcp, started, *, now=None):
|
||||
"""Return a public state and private detail from the pinned client contract."""
|
||||
now = time.time() if now is None else now
|
||||
try:
|
||||
endpoint = health_endpoint(health_file)
|
||||
except FileNotFoundError:
|
||||
return "starting", "Waiting for the tunnel health endpoint."
|
||||
except (OSError, ValueError, ConnectionMonitorError):
|
||||
return "checking", "The tunnel health endpoint is not available or valid."
|
||||
try:
|
||||
code, body = _get(endpoint, "/readyz", 8192)
|
||||
message = body.decode("utf-8", errors="replace").strip()
|
||||
if code != 200 or message != "ready":
|
||||
return "checking", "MCP readiness is pending: " + message
|
||||
code, body = _get(endpoint, "/api/status", 65536)
|
||||
if code != 200:
|
||||
return "checking", "Tunnel status is not available."
|
||||
status = json.loads(body)
|
||||
if not isinstance(status, dict):
|
||||
return "checking", "Tunnel status has an unexpected format."
|
||||
if (status.get("control_plane_tunnel_id") != expected_tunnel
|
||||
or status.get("mcp_server_url") != expected_mcp):
|
||||
return "checking", "Tunnel status does not match this profile."
|
||||
created = datetime.datetime.fromisoformat(status["started_at"].replace("Z", "+00:00")).timestamp()
|
||||
if not started - 5 <= created <= now + 5:
|
||||
return "checking", "Tunnel status belongs to a different startup."
|
||||
metadata = status.get("tunnel_metadata") or {}
|
||||
if not isinstance(metadata, dict):
|
||||
return "checking", "Tunnel metadata has an unexpected format."
|
||||
if status.get("tunnel_metadata_error"):
|
||||
detail = str(status["tunnel_metadata_error"])
|
||||
if re.search(r"\b(?:401|403|unauthorized|forbidden)\b", detail, re.I):
|
||||
return "rejected", "Tunnel access was rejected: " + detail
|
||||
return "access", "Tunnel metadata request failed: " + detail
|
||||
if metadata.get("ID") != expected_tunnel:
|
||||
return "access", "Waiting for authenticated tunnel metadata."
|
||||
code, body = _get(endpoint, "/metrics", 256 * 1024)
|
||||
if code != 200:
|
||||
return "access", "Waiting for authenticated polling evidence."
|
||||
values = []
|
||||
for line in body.decode("ascii", errors="replace").splitlines():
|
||||
if line.startswith("#"):
|
||||
continue
|
||||
match = re.fullmatch(r"commands_poll_last_successful_timestamp_seconds(?:\{[^\n]*\})?\s+([0-9.eE+-]+)(?:\s+\d+)?", line)
|
||||
if match:
|
||||
values.append(float(match.group(1)))
|
||||
if not values or not all(math.isfinite(v) for v in values):
|
||||
return "access", "Waiting for authenticated polling evidence."
|
||||
recent = max(values)
|
||||
if recent < started - 5 or recent > now + 5 or now - recent > POLL_FRESH_SECONDS:
|
||||
return "access", "A successful authenticated poll has not been confirmed recently."
|
||||
return "ready", "Local MCP initialization, tunnel identity, metadata and recent authenticated polling verified."
|
||||
except (OSError, ValueError, KeyError, TypeError, http.client.HTTPException, ConnectionMonitorError):
|
||||
return "checking", "The tunnel health check is temporarily unavailable."
|
||||
|
||||
|
||||
class Progress:
|
||||
def __init__(self, output):
|
||||
self.output = output
|
||||
self.tty = bool(getattr(output, "isatty", lambda: False)())
|
||||
self.state = None
|
||||
self.message = ""
|
||||
self.width = 0
|
||||
self.index = 0
|
||||
|
||||
def change(self, state, message):
|
||||
if self.state == state and self.message == message:
|
||||
return
|
||||
self.clear()
|
||||
self.state, self.message = state, message
|
||||
self.output.write(message + "\n")
|
||||
self.output.flush()
|
||||
|
||||
def clear(self):
|
||||
if self.tty and self.width:
|
||||
self.output.write("\r" + " " * self.width + "\r")
|
||||
self.width = 0
|
||||
|
||||
def tick(self):
|
||||
if self.tty and self.state not in {"ready", "closed", "error", "rejected", "unverified"}:
|
||||
text = "Connecting " + "|/-\\"[self.index % 4] + " Press Ctrl+C to cancel."
|
||||
self.index += 1
|
||||
self.width = max(self.width, len(text))
|
||||
self.output.write("\r" + text.ljust(self.width))
|
||||
self.output.flush()
|
||||
|
||||
|
||||
def capture_output(stream, log, errors, stop):
|
||||
"""Drain binary output without ever forwarding it to the terminal."""
|
||||
buffer = bytearray()
|
||||
dropping = False
|
||||
try:
|
||||
fd = stream.fileno()
|
||||
selector = selectors.DefaultSelector()
|
||||
selector.register(fd, selectors.EVENT_READ)
|
||||
while True:
|
||||
if not selector.select(timeout=0.1):
|
||||
if stop.is_set():
|
||||
break
|
||||
continue
|
||||
chunk = os.read(fd, 4096)
|
||||
if not chunk:
|
||||
break
|
||||
for part in chunk.splitlines(keepends=True):
|
||||
ending = part.endswith(b"\n") or part.endswith(b"\r")
|
||||
if not dropping:
|
||||
if len(buffer) + len(part) > MAX_LINE:
|
||||
buffer.clear()
|
||||
dropping = True
|
||||
else:
|
||||
buffer.extend(part)
|
||||
if ending:
|
||||
if dropping:
|
||||
log.write("[Oversized client output line omitted]")
|
||||
else:
|
||||
log.write(buffer.decode("utf-8", errors="replace"))
|
||||
buffer.clear()
|
||||
dropping = False
|
||||
if buffer and not dropping:
|
||||
log.write(buffer.decode("utf-8", errors="replace"))
|
||||
elif dropping:
|
||||
log.write("[Oversized client output line omitted]")
|
||||
except Exception:
|
||||
errors.append("Connection output could not be written to its private log.")
|
||||
finally:
|
||||
if "selector" in locals():
|
||||
selector.close()
|
||||
if stop.is_set():
|
||||
# If the main thread timed out waiting for draining, this thread
|
||||
# remains the owner of its live stream/log until it finishes.
|
||||
try:
|
||||
stream.close()
|
||||
except Exception:
|
||||
errors.append("Connection output stream cleanup failed.")
|
||||
try:
|
||||
log.close()
|
||||
except Exception:
|
||||
errors.append("Private connection log cleanup failed.")
|
||||
Reference in new issue
Block a user