diff --git a/ainfera_api/routers/audit.py b/ainfera_api/routers/audit.py index 73732db..98e3cc5 100644 --- a/ainfera_api/routers/audit.py +++ b/ainfera_api/routers/audit.py @@ -87,10 +87,31 @@ async def public_feed( "`limit` events ordered by created_at descending." ), ), + event_type: AuditEventType | None = Query( # noqa: B008 # FastAPI Query in default is idiomatic + None, + description=( + "If set, return only events with the matching event_type " + "(e.g. `inference.requested`, `inference.rejected_cap_violation`). " + "AIN-119 fix: prior to 2026-05-18 this param was silently ignored — " + "callers that depended on client-side filtering should now use " + "this server-side filter for efficiency." + ), + ), + agent: str | None = Query( + None, + description=( + "If set, return only events for the matching agent name (e.g. " + "`varda`, `tulkas`). Exact match. Useful for per-agent dashboards." + ), + ), ) -> PublicAuditFeed: stmt = select(AuditEventORM, AgentORM.name, AgentORM.owner_handle).join( AgentORM, AuditEventORM.agent_id == AgentORM.id ) + if event_type is not None: + stmt = stmt.where(AuditEventORM.event_type == event_type) + if agent is not None: + stmt = stmt.where(AgentORM.name == agent) if since_seq is not None: stmt = stmt.where(AuditEventORM.seq > since_seq).order_by(AuditEventORM.seq.asc()) else: diff --git a/tests/integration/test_audit_public_cap.py b/tests/integration/test_audit_public_cap.py index 46ec92b..2f71634 100644 --- a/tests/integration/test_audit_public_cap.py +++ b/tests/integration/test_audit_public_cap.py @@ -144,3 +144,73 @@ async def test_get_chain_since_seq_filters( cursored_seqs = [e["seq"] for e in cursored.json()["events"]] assert all(s > pivot for s in cursored_seqs) assert cursored_seqs == sorted(cursored_seqs) + + +# ---- /v1/audit/public filter params (AIN-119 fix) ---- + + +@pytest.mark.asyncio +async def test_public_feed_event_type_filter_returns_only_matching_type( + client: AsyncClient, internal_key_headers: dict[str, str] +) -> None: + """AIN-119: ?event_type=X returns ONLY events with event_type==X (not all events).""" + # Signup emits agent.registered + agent.card_issued + ledger.topped_up + await _signup(client, "ain119-type-a", internal_key_headers) + await _signup(client, "ain119-type-b", internal_key_headers) + + r = await client.get("/v1/audit/public?event_type=agent.registered&limit=500") + assert r.status_code == 200 + events = r.json()["events"] + assert events, "expected at least 1 agent.registered event" + for e in events: + assert e["event_type"] == "agent.registered", ( + f"filter leaked event_type={e['event_type']!r}; expected only agent.registered" + ) + + +@pytest.mark.asyncio +async def test_public_feed_event_type_filter_invalid_value_returns_422( + client: AsyncClient, +) -> None: + """Falsifiable: unknown event_type returns 422, not silent passthrough.""" + r = await client.get("/v1/audit/public?event_type=not.a.real.type") + assert r.status_code == 422 + detail = r.json()["detail"][0] + assert detail["loc"] == ["query", "event_type"] + + +@pytest.mark.asyncio +async def test_public_feed_agent_filter_returns_only_matching_agent( + client: AsyncClient, internal_key_headers: dict[str, str] +) -> None: + """AIN-119 sibling: ?agent=X returns ONLY events for agent named X.""" + await _signup(client, "ain119-ag-target", internal_key_headers) + await _signup(client, "ain119-ag-other", internal_key_headers) + + r = await client.get("/v1/audit/public?agent=ain119-ag-target&limit=500") + assert r.status_code == 200 + events = r.json()["events"] + assert events, "expected at least 1 event for the target agent" + for e in events: + assert e["agent_name"] == "ain119-ag-target", ( + f"filter leaked agent_name={e['agent_name']!r}; expected only ain119-ag-target" + ) + + +@pytest.mark.asyncio +async def test_public_feed_combined_event_type_and_agent_filters( + client: AsyncClient, internal_key_headers: dict[str, str] +) -> None: + """AIN-119: combined ?event_type=X&agent=Y returns intersection.""" + await _signup(client, "ain119-combo-target", internal_key_headers) + await _signup(client, "ain119-combo-other", internal_key_headers) + + r = await client.get( + "/v1/audit/public?event_type=agent.registered&agent=ain119-combo-target&limit=500" + ) + assert r.status_code == 200 + events = r.json()["events"] + assert events, "expected at least 1 matching event" + for e in events: + assert e["event_type"] == "agent.registered" + assert e["agent_name"] == "ain119-combo-target"