| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157 |
- from __future__ import annotations
- USER_UID = "01900000-0000-7000-8000-000000009801"
- AGENT_UID = "01900000-0000-7000-8000-000000009802"
- REQUEST_UID = "01900000-0000-7000-8000-000000009803"
- class FakeAgentGovernanceService:
- def __init__(self):
- self.calls = []
- def list_agents(self, **filters):
- self.calls.append(("list_agents", filters))
- return [{"uid": AGENT_UID, "status": "active", "current_version": 2}]
- def register_agent(self, payload, actor_uid):
- self.calls.append(("register_agent", payload, actor_uid))
- return {"uid": AGENT_UID, "status": "draft", "current_version": 1}
- def create_tool_grant(self, uid, payload, actor_uid):
- self.calls.append(("create_tool_grant", uid, payload, actor_uid))
- return {"uid": "grant-1", "agent_uid": uid, **payload}
- def issue_credential(self, uid, payload, actor_uid):
- self.calls.append(("issue_credential", uid, payload, actor_uid))
- return {"token": "only-once", "expires_at": "2026-08-02T12:05:00+08:00"}
- def authorize_action(self, uid, token, payload):
- self.calls.append(("authorize_action", uid, token, payload))
- return {"uid": REQUEST_UID, "decision": "denied", "current_version": 1}
- def list_requests(self, **filters):
- return [{"uid": REQUEST_UID, "decision": "denied", **filters}]
- def reconcile_action(self, uid, expected_version, actor_uid):
- self.calls.append(("reconcile_action", uid, expected_version, actor_uid))
- return {"uid": uid, "decision": "authorized", "current_version": 2}
- def replay(self, uid):
- return {"request": {"uid": uid, "input_digest": "a" * 64}, "events": []}
- def dashboard(self):
- return {"agent_count": 1, "denied_count": 1}
- def _headers(role, **extra):
- return {"Authorization": f"Bearer {role}", **extra}
- def _client(monkeypatch):
- from app import create_app
- from app.api.knowledge_base import agent_governance_routes
- service = FakeAgentGovernanceService()
- monkeypatch.setattr(agent_governance_routes, "_service", lambda: service)
- monkeypatch.setattr(
- "app.core.system.auth.load_identity_from_token",
- lambda token, secret: (
- {"id": USER_UID, "username": token, "roles": [token]}
- if token in {"viewer", "editor", "admin"}
- else None
- ),
- )
- app = create_app()
- app.config.update(TESTING=True)
- return app.test_client(), service
- def test_viewer_reads_agent_inventory_dashboard_and_replay(monkeypatch):
- client, _service = _client(monkeypatch)
- listing = client.get(
- "/api/knowledge/agents?status=active", headers=_headers("viewer")
- )
- assert listing.status_code == 200
- assert listing.get_json()["data"][0]["uid"] == AGENT_UID
- dashboard = client.get("/api/knowledge/agents/dashboard", headers=_headers("viewer"))
- assert dashboard.get_json()["data"]["denied_count"] == 1
- replay = client.get(
- f"/api/knowledge/agents/actions/{REQUEST_UID}/replay",
- headers=_headers("viewer"),
- )
- assert replay.status_code == 200
- assert replay.headers["Cache-Control"] == "no-store"
- def test_editor_operates_grants_and_one_time_credentials_but_cannot_register(monkeypatch):
- client, service = _client(monkeypatch)
- forbidden = client.post(
- "/api/knowledge/agents", json={"code": "NOPE"}, headers=_headers("editor")
- )
- assert forbidden.status_code == 403
- grant = client.post(
- f"/api/knowledge/agents/{AGENT_UID}/grants",
- json={"tool_name": "knowledge.search"}, headers=_headers("editor"),
- )
- assert grant.status_code == 201
- credential = client.post(
- f"/api/knowledge/agents/{AGENT_UID}/credentials",
- json={"ttl_seconds": 300}, headers=_headers("editor"),
- )
- assert credential.status_code == 201
- assert credential.headers["Cache-Control"] == "no-store"
- assert credential.get_json()["data"]["token"] == "only-once"
- assert service.calls[-1][0] == "issue_credential"
- def test_admin_registers_and_reconciles_while_machine_credential_is_explicit(monkeypatch):
- client, service = _client(monkeypatch)
- created = client.post(
- "/api/knowledge/agents", json={"code": "READ_GOV"}, headers=_headers("admin")
- )
- assert created.status_code == 201
- assert created.headers["ETag"] == '"1"'
- decision = client.post(
- f"/api/knowledge/agents/{AGENT_UID}/actions/authorize",
- json={"prompt": "read"},
- headers=_headers("editor", **{"X-Agent-Credential": "machine-token"}),
- )
- assert decision.status_code == 201
- assert decision.get_json()["data"]["decision"] == "denied"
- assert service.calls[-1][2] == "machine-token"
- missing = client.post(
- f"/api/knowledge/agents/actions/{REQUEST_UID}/reconcile",
- headers=_headers("admin"),
- )
- assert missing.status_code == 428
- reconciled = client.post(
- f"/api/knowledge/agents/actions/{REQUEST_UID}/reconcile",
- headers=_headers("admin", **{"If-Match": '"1"'}),
- )
- assert reconciled.status_code == 200
- assert reconciled.headers["ETag"] == '"2"'
- def test_production_requires_a_dedicated_machine_credential_secret():
- import pytest
- from app import create_app
- from app.api.knowledge_base.agent_governance_routes import (
- AgentGovernanceUnavailable,
- _effective_credential_secret,
- _require_credential_secret,
- )
- app = create_app()
- app.config.update(TESTING=False, FLASK_ENV="production", AGENT_CREDENTIAL_SECRET="")
- with app.app_context():
- fallback, dedicated = _effective_credential_secret()
- assert len(fallback) == 64 and dedicated is False
- with pytest.raises(AgentGovernanceUnavailable):
- _require_credential_secret()
- app.config["AGENT_CREDENTIAL_SECRET"] = "production-agent-secret-with-at-least-32-bytes"
- _secret, dedicated = _effective_credential_secret()
- assert dedicated is True
- _require_credential_secret()
|