feat(agent): add learn action flow and grounding guards

This commit is contained in:
Dom
2026-06-02 16:24:10 +02:00
parent 86b3c8f7e7
commit d38f0b0f2f
39 changed files with 5901 additions and 212 deletions

View File

@@ -15,8 +15,10 @@ garantit que l'env est defini AVANT tout import.
from __future__ import annotations
import os
import sqlite3
import sys
import tempfile
import time
from pathlib import Path
import pytest
@@ -273,6 +275,107 @@ def test_reenroll_after_uninstall_reactivates(agents_client):
assert agent["version"] == "1.1.0"
def test_reenroll_after_admin_revoke_is_forbidden(agents_client):
client, token, _ = agents_client
client.post(
"/api/v1/agents/enroll",
json={"machine_id": "revoked-001", "user_name": "Revoked"},
headers=_auth_headers(token),
)
revoke = client.post(
"/api/v1/agents/uninstall",
json={"machine_id": "revoked-001", "reason": "admin_revoke"},
headers=_auth_headers(token),
)
assert revoke.status_code == 200
resp = client.post(
"/api/v1/agents/enroll",
json={"machine_id": "revoked-001", "user_name": "Revoked Again"},
headers=_auth_headers(token),
)
assert resp.status_code == 403, resp.text
detail = resp.json()["detail"]
assert detail["error"] == "agent_revoked"
assert detail["existing"]["machine_id"] == "revoked-001"
assert detail["existing"]["uninstall_reason"] == "admin_revoke"
def test_revoked_agent_cannot_stream_or_poll(agents_client):
client, token, _ = agents_client
client.post(
"/api/v1/agents/enroll",
json={"machine_id": "revoked-runtime-001", "user_name": "Runtime"},
headers=_auth_headers(token),
)
client.post(
"/api/v1/agents/uninstall",
json={"machine_id": "revoked-runtime-001", "reason": "admin_revoke"},
headers=_auth_headers(token),
)
event_resp = client.post(
"/api/v1/traces/stream/event",
json={
"session_id": "sess_revoked_runtime",
"timestamp": time.time(),
"event": {"type": "heartbeat"},
"machine_id": "revoked-runtime-001",
},
headers=_auth_headers(token),
)
assert event_resp.status_code == 403, event_resp.text
assert event_resp.json()["detail"]["error"] == "agent_not_active"
next_resp = client.get(
"/api/v1/traces/stream/replay/next",
params={
"session_id": "sess_revoked_runtime",
"machine_id": "revoked-runtime-001",
},
headers=_auth_headers(token),
)
assert next_resp.status_code == 403, next_resp.text
assert next_resp.json()["detail"]["error"] == "agent_not_active"
def test_active_agent_stream_updates_last_seen(agents_client):
client, token, registry = agents_client
machine_id = "last-seen-001"
client.post(
"/api/v1/agents/enroll",
json={"machine_id": machine_id, "user_name": "Seen"},
headers=_auth_headers(token),
)
stale = "2000-01-01T00:00:00+00:00"
with sqlite3.connect(str(registry.db_path)) as conn:
conn.execute(
"UPDATE enrolled_agents SET last_seen_at = ? WHERE machine_id = ?",
(stale, machine_id),
)
conn.commit()
resp = client.post(
"/api/v1/traces/stream/event",
json={
"session_id": "sess_last_seen",
"timestamp": time.time(),
"event": {"type": "heartbeat"},
"machine_id": machine_id,
},
headers=_auth_headers(token),
)
assert resp.status_code == 200, resp.text
row = registry.get(machine_id)
assert row is not None
assert row["last_seen_at"] != stale
# ---------------------------------------------------------------------------
# GET /api/v1/agents/fleet
# ---------------------------------------------------------------------------