Files
NotesAgentic/backend/app/sidecar.py
T

174 lines
7.0 KiB
Python

"""Authenticated desktop entry point. Bootstrap secrets travel only over stdin.
stdout is reserved for the bounded handshake; application output goes to stderr.
The parent keeps stdin open for the lifetime of the Core. EOF shuts it down.
"""
from __future__ import annotations
import asyncio
import hashlib
import hmac
import json
import os
from pathlib import Path
import re
import socket
import sys
import threading
PROTOCOL = 1
MAX_BOOTSTRAP = 16384
UUID_PATTERN = re.compile(
r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
)
def bootstrap(line: bytes) -> dict:
if len(line) > MAX_BOOTSTRAP or not line.endswith(b"\n"):
raise ValueError("CORE_BOOTSTRAP_INVALID")
try:
value = json.loads(line)
if value["protocol"] != PROTOCOL:
raise ValueError("PROTOCOL_INCOMPATIBLE")
for key in ("secret", "challenge", "generation"):
if not isinstance(value[key], str) or not re.fullmatch(r"[0-9a-f]{64}", value[key]):
raise ValueError("CORE_BOOTSTRAP_INVALID")
if not Path(value["data_dir"]).is_absolute():
raise ValueError("CORE_BOOTSTRAP_INVALID")
if type(value.get("launcher_pid")) is not int or not 0 < value["launcher_pid"] <= 0xFFFFFFFF:
raise ValueError("CORE_BOOTSTRAP_INVALID")
except (KeyError, TypeError, json.JSONDecodeError) as exc:
raise ValueError("CORE_BOOTSTRAP_INVALID") from exc
return value
def proof(secret: str, challenge: str, generation: str, pid: int, port: int, launcher_pid: int) -> str:
message = f"{PROTOCOL}:{challenge}:{generation}:{launcher_pid}:{pid}:{port}".encode("ascii")
return hmac.new(bytes.fromhex(secret), message, hashlib.sha256).hexdigest()
class SessionAuth:
"""Outermost ASGI layer: unauthenticated input never reaches business logs."""
def __init__(self, app, secret: str, generation: str, port: int):
self.app = app
self.expected = f"Bearer {secret}".encode("ascii")
self.generation = generation.encode("ascii")
self.host = f"127.0.0.1:{port}".encode("ascii")
async def __call__(self, scope, receive, send):
if scope["type"] not in {"http", "websocket"}:
return await self.app(scope, receive, send)
headers = scope.get("headers", [])
def single(name):
values = [v for k, v in headers if k.lower() == name]
return values[0] if len(values) == 1 else b""
authorized = (
hmac.compare_digest(single(b"authorization"), self.expected)
and hmac.compare_digest(single(b"x-core-generation"), self.generation)
and single(b"host") == self.host
# Host transport does not send Origin. Browser traffic is never trusted.
and not any(k.lower() == b"origin" for k, _ in headers)
)
if not authorized:
if scope["type"] == "websocket":
await send({"type": "websocket.close", "code": 1008})
else:
body = b'{"error":{"code":"AUTH_REQUIRED"}}'
await send({"type": "http.response.start", "status": 401,
"headers": [(b"content-type", b"application/json"),
(b"cache-control", b"no-store")]})
await send({"type": "http.response.body", "body": body})
return
from app import host_bridge
vault = single(b"x-opennexus-vault").decode("ascii", errors="replace")
token = host_bridge.vault_id.set(vault if UUID_PATTERN.fullmatch(vault) else None)
# Mutating clients may retain a UUID across an ambiguous response. Other
# endpoint-specific idempotency tokens remain available to the route but
# do not enter the Host journal unless they are valid operation UUIDs.
idempotency = single(b"idempotency-key").decode("ascii", errors="replace")
operation = (
idempotency
if UUID_PATTERN.fullmatch(idempotency)
else single(b"x-request-id").decode("ascii", errors="replace")
)
operation_token = host_bridge.operation_id.set(
operation if UUID_PATTERN.fullmatch(operation) else None
)
try:
await self.app(scope, receive, send)
finally:
host_bridge.vault_id.reset(token)
host_bridge.operation_id.reset(operation_token)
def main() -> int:
channel = sys.stdin.buffer
try:
config = bootstrap(channel.readline(MAX_BOOTSTRAP + 1))
except (ValueError, OSError):
print("CORE_BOOTSTRAP_INVALID", file=sys.stderr)
return 2
handshake = sys.stdout
sys.stdout = sys.stderr
root = Path(config["data_dir"])
# Override every data path before importing the application/container.
os.environ.update({
"APP_ENVIRONMENT": "desktop", "APP_DATA_DIR": str(root),
"APP_DB_PATH": str(root / "app.db"),
"APP_VAULT_PATH": str(root / "unbound-vault"),
"APP_ATTACHMENTS_PATH": str(root / "attachments"),
"APP_EXPORTS_PATH": str(root / "exports"),
"APP_BENCHMARK_DATASETS_PATH": str(root / "benchmarks"),
})
import uvicorn
from app import host_bridge
host_bridge.active = host_bridge.HostBridge(channel, handshake)
from app.main import app
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(("127.0.0.1", 0))
sock.listen(128)
port = sock.getsockname()[1]
app.openapi_url = None
app.router.routes[:] = [r for r in app.router.routes
if getattr(r, "path", "") not in {"/docs", "/redoc", "/openapi.json", "/docs/oauth2-redirect"}]
server = uvicorn.Server(uvicorn.Config(
SessionAuth(app, config["secret"], config["generation"], port),
log_config=None, access_log=False, lifespan="on", timeout_graceful_shutdown=5,
))
def watch_parent():
host_bridge.active.listen(lambda: setattr(server, "should_exit", True))
threading.Thread(target=watch_parent, name="host-lifetime", daemon=True).start()
async def run():
task = asyncio.create_task(server.serve(sockets=[sock]))
for _ in range(3000):
if task.done():
await task
return
if server.started:
payload = {"protocol": PROTOCOL, "pid": os.getpid(), "port": port,
"generation": config["generation"], "launcher_pid": config["launcher_pid"],
"proof": proof(config["secret"], config["challenge"],
config["generation"], os.getpid(), port, config["launcher_pid"])}
handshake.write(json.dumps(payload, separators=(",", ":")) + "\n")
handshake.flush()
await task
return
await asyncio.sleep(0.01)
server.should_exit = True
await task
raise RuntimeError("CORE_READY_TIMEOUT")
try:
asyncio.run(run())
finally:
sock.close()
return 0
if __name__ == "__main__":
raise SystemExit(main())