87 lines
3.6 KiB
Python
87 lines
3.6 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
"""Private Unix-socket bridge for owner-approved CodexPi master sessions."""
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import argparse
|
|||
|
|
import hmac
|
|||
|
|
import json
|
|||
|
|
import os
|
|||
|
|
import socket
|
|||
|
|
import subprocess
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
|
|||
|
|
def receive(connection: socket.socket, limit: int = 100_000) -> dict:
|
|||
|
|
chunks, size = [], 0
|
|||
|
|
while True:
|
|||
|
|
block = connection.recv(8192)
|
|||
|
|
if not block:
|
|||
|
|
break
|
|||
|
|
size += len(block)
|
|||
|
|
if size > limit:
|
|||
|
|
raise ValueError("request too large")
|
|||
|
|
chunks.append(block)
|
|||
|
|
if b"\n" in block:
|
|||
|
|
break
|
|||
|
|
return json.loads(b"".join(chunks).split(b"\n", 1)[0])
|
|||
|
|
|
|||
|
|
|
|||
|
|
def run_agent(codex: str, prompt: str, privileged: bool) -> tuple[bool, int, str]:
|
|||
|
|
rules = (
|
|||
|
|
"Tu es l’agent maître CodexPi sur l’hôte réel. Vérifie la cible avant toute écriture, protège les secrets, "
|
|||
|
|
"utilise des commandes structurées, évite toute action destructive non explicitement demandée et rends compte des vérifications. "
|
|||
|
|
"Les privilèges disponibles proviennent uniquement de l’identité de ce service et de la politique sudo de l’hôte."
|
|||
|
|
)
|
|||
|
|
if privileged:
|
|||
|
|
rules += " Cette demande a reçu la confirmation privilégiée du propriétaire."
|
|||
|
|
command = [codex, "--dangerously-bypass-approvals-and-sandbox", "exec", "--json", "--skip-git-repo-check", rules + "\n\nDEMANDE DU PROPRIÉTAIRE\n" + prompt]
|
|||
|
|
process = subprocess.run(command, cwd="/", text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=3600, check=False)
|
|||
|
|
answers = []
|
|||
|
|
for line in process.stdout.splitlines():
|
|||
|
|
try:
|
|||
|
|
event = json.loads(line)
|
|||
|
|
except ValueError:
|
|||
|
|
continue
|
|||
|
|
item = event.get("item") or {}
|
|||
|
|
text = item.get("text") if item.get("type") in {"agent_message", "assistant_message"} else None
|
|||
|
|
if text:
|
|||
|
|
answers.append(str(text))
|
|||
|
|
answer = answers[-1] if answers else "L’agent maître n’a produit aucun compte rendu exploitable."
|
|||
|
|
return process.returncode == 0, process.returncode, answer
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main() -> int:
|
|||
|
|
parser = argparse.ArgumentParser()
|
|||
|
|
parser.add_argument("--socket", required=True)
|
|||
|
|
parser.add_argument("--token-file", required=True)
|
|||
|
|
parser.add_argument("--codex", default="codex")
|
|||
|
|
args = parser.parse_args()
|
|||
|
|
socket_path, token_path = Path(args.socket), Path(args.token_file)
|
|||
|
|
token = token_path.read_text(encoding="utf-8").strip()
|
|||
|
|
if len(token) < 32 or token_path.stat().st_mode & 0o077:
|
|||
|
|
raise SystemExit("unsafe host-agent token")
|
|||
|
|
socket_path.parent.mkdir(parents=True, exist_ok=True)
|
|||
|
|
if socket_path.exists():
|
|||
|
|
socket_path.unlink()
|
|||
|
|
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as server:
|
|||
|
|
server.bind(str(socket_path))
|
|||
|
|
os.chmod(socket_path, 0o660)
|
|||
|
|
server.listen(4)
|
|||
|
|
while True:
|
|||
|
|
connection, _ = server.accept()
|
|||
|
|
with connection:
|
|||
|
|
try:
|
|||
|
|
request = receive(connection)
|
|||
|
|
if not hmac.compare_digest(str(request.get("token") or ""), token):
|
|||
|
|
raise PermissionError("unauthorized")
|
|||
|
|
ok, code, answer = run_agent(args.codex, str(request.get("prompt") or ""), bool(request.get("privileged")))
|
|||
|
|
response = {"ok": ok, "exit_code": code, "answer": answer}
|
|||
|
|
except Exception:
|
|||
|
|
response = {"ok": False, "exit_code": 1, "answer": "Le canal hôte a refusé ou interrompu la demande."}
|
|||
|
|
connection.sendall(json.dumps(response, ensure_ascii=False).encode() + b"\n")
|
|||
|
|
return 0
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
raise SystemExit(main())
|