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
|