115 lines
5.0 KiB
Python
115 lines
5.0 KiB
Python
from __future__ import annotations
|
||
|
||
import json
|
||
import os
|
||
import socket
|
||
import threading
|
||
import uuid
|
||
from pathlib import Path
|
||
|
||
from codexpi.database import utcnow
|
||
|
||
|
||
def fake_host_channel(path: Path, token: str):
|
||
ready = threading.Event()
|
||
|
||
def serve():
|
||
if path.exists():
|
||
path.unlink()
|
||
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as server:
|
||
server.bind(str(path))
|
||
server.listen(1)
|
||
ready.set()
|
||
connection, _ = server.accept()
|
||
with connection:
|
||
request = json.loads(connection.recv(100_000).split(b"\n", 1)[0])
|
||
assert request["token"] == token
|
||
connection.sendall(json.dumps({"ok": True, "exit_code": 0, "answer": "Action hôte de test terminée."}).encode() + b"\n")
|
||
|
||
thread = threading.Thread(target=serve, daemon=True)
|
||
thread.start()
|
||
ready.wait(2)
|
||
return thread
|
||
|
||
|
||
def test_master_agent_channel_is_separate_confirmed_and_audited(logged_in):
|
||
client, app, _, _, headers = logged_in
|
||
base = app.state.settings.data_dir / "host-agent"
|
||
base.mkdir()
|
||
token_file, socket_path = base / "agent.token", base / "agent.sock"
|
||
token = "fixture-token-which-is-longer-than-thirty-two-characters"
|
||
token_file.write_text(token, encoding="utf-8")
|
||
os.chmod(token_file, 0o600)
|
||
thread = fake_host_channel(socket_path, token)
|
||
|
||
created = client.post(
|
||
"/api/master-agent/sessions", headers=headers,
|
||
json={"title": "Maintenance de test", "privileged": True},
|
||
)
|
||
assert created.status_code == 201
|
||
session_id = created.json()["id"]
|
||
refused = client.post(
|
||
f"/api/master-agent/sessions/{session_id}/messages", headers=headers,
|
||
json={"prompt": "Effectuer une action réversible de test.", "acknowledge_privileged": False},
|
||
)
|
||
assert refused.status_code == 409
|
||
accepted = client.post(
|
||
f"/api/master-agent/sessions/{session_id}/messages", headers=headers,
|
||
json={"prompt": "Effectuer une action réversible de test.", "acknowledge_privileged": True},
|
||
)
|
||
assert accepted.status_code == 200, accepted.text
|
||
assert accepted.json()["answer"] == "Action hôte de test terminée."
|
||
thread.join(2)
|
||
history = client.get("/api/master-agent").json()["sessions"]
|
||
assert [message["role"] for message in history[0]["messages"]] == ["user", "assistant"]
|
||
|
||
|
||
def test_project_user_cannot_access_master_agent(logged_in):
|
||
client, app, _, _, headers = logged_in
|
||
app.state.auth.create_user("member", "a-different-long-test-password", is_admin=False)
|
||
client.post("/api/auth/logout", headers=headers)
|
||
login = client.post("/api/auth/login", json={"username": "member", "password": "a-different-long-test-password"}).json()
|
||
member_headers = {"X-CSRF-Token": login["csrf_token"]}
|
||
assert client.get("/api/master-agent").status_code == 403
|
||
assert client.post("/api/master-agent/sessions", headers=member_headers, json={"title": "No", "privileged": False}).status_code == 403
|
||
|
||
|
||
def test_additional_admin_is_not_the_host_owner(logged_in):
|
||
client, app, _, _, headers = logged_in
|
||
app.state.auth.create_user("other-admin", "another-long-administrator-password", is_admin=True)
|
||
client.post("/api/auth/logout", headers=headers)
|
||
login = client.post("/api/auth/login", json={"username": "other-admin", "password": "another-long-administrator-password"}).json()
|
||
admin_headers = {"X-CSRF-Token": login["csrf_token"]}
|
||
assert client.get("/api/master-agent").status_code == 403
|
||
assert client.post("/api/master-agent/sessions", headers=admin_headers, json={"title": "No", "privileged": False}).status_code == 403
|
||
|
||
|
||
def test_additional_admin_cannot_queue_the_privileged_pi_doctor(logged_in):
|
||
client, app, projects, _, headers = logged_in
|
||
owner_id = str(app.state.db.one("SELECT id FROM users LIMIT 1")["id"])
|
||
project_id = str(uuid.uuid4())
|
||
conversation_id = str(uuid.uuid4())
|
||
root = projects / "pi-doctor"
|
||
root.mkdir()
|
||
stamp = utcnow()
|
||
app.state.db.execute(
|
||
"INSERT INTO projects(id,name,path,owner_id,full_access,execution_group,created_at) VALUES(?,?,?,?,?,?,?)",
|
||
(project_id, "Pi Doctor", str(root), owner_id, 1, "__global_agent__", stamp),
|
||
)
|
||
app.state.db.execute(
|
||
"INSERT INTO conversations(id,project_id,user_id,title,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?)",
|
||
(conversation_id, project_id, owner_id, "Host", "idle", stamp, stamp),
|
||
)
|
||
app.state.auth.create_user("other-host-admin", "another-long-administrator-password", is_admin=True)
|
||
client.post("/api/auth/logout", headers=headers)
|
||
login = client.post(
|
||
"/api/auth/login",
|
||
json={"username": "other-host-admin", "password": "another-long-administrator-password"},
|
||
).json()
|
||
response = client.post(
|
||
f"/api/conversations/{conversation_id}/queue",
|
||
headers={"X-CSRF-Token": login["csrf_token"]},
|
||
json={"content": "Inspecter l’hôte.", "attachment_ids": []},
|
||
)
|
||
assert response.status_code == 403
|