Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions ainfera_api/routers/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
70 changes: 70 additions & 0 deletions tests/integration/test_audit_public_cap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Loading