Skip to content

feat(agentex): filter GET /agents by agent card metadata - #411

Open
declan-scale wants to merge 2 commits into
mainfrom
declan-scale/agx1-1048-agent-card-metadata-filter
Open

feat(agentex): filter GET /agents by agent card metadata#411
declan-scale wants to merge 2 commits into
mainfrom
declan-scale/agx1-1048-agent-card-metadata-filter

Conversation

@declan-scale

@declan-scale declan-scale commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

🏆 Brought to you by the Golden Agent (Try it out)

Problem

AgentCard publishes self-description data through registration_metadata.agent_card, but GET /agents has no way to filter on its contents. Discovery flows that want to enumerate agents opting into a specific protocol capability (e.g. Permits' workflow submission protocol) have no server-side hook and must fetch everything client-side.

Linear: AGX1-1048

Change

  • Adds an optional agent_card_metadata query parameter to GET /agents. The value is a JSON-encoded object; malformed JSON or non-object payloads return 400.
  • The route parses the JSON and forwards it to AgentsUseCase.list, which reserves the key agent_card_metadata in the repository filters dict.
  • AgentRepository.list applies a JSONB @> filter at the top level:
    registration_metadata @> jsonb_build_object('agent_card', jsonb_build_object('metadata', :value))
    Wrapping under the same nested shape as the stored card means agents whose registration_metadata is NULL, missing agent_card, or missing agent_card.metadata are naturally excluded, and every requested key/value must be present at the correct nesting level.
  • No DB migration — agents.registration_metadata is already JSONB.
  • Existing pagination, ordering, task filtering and authorization behavior are preserved (the filter composes with the pre-existing task_id join, authorization id set, and status != DELETED clause).
  • openapi.yaml regenerated by hand to reflect the new query parameter; the paired SDK PR consumes the same spec.

Test coverage added

  • Integration tests in tests/integration/api/agents/test_agents_api.py:
    • Matching key/value returns only opted-in agents; agents with different values or no card at all are excluded.
    • Non-matching value returns an empty list.
    • Multi-key filter requires containment of every key/value.
    • The filter composes with limit/page_number.
    • Malformed JSON and non-object payloads return 400.
  • Unit tests in tests/unit/use_cases/test_agents_use_case.py that seed agents directly via the repository and exercise the use-case-to-repo plumbing against real Postgres (single-key, multi-key, absent-card, and omitted-filter cases). Tests use a per-invocation tag so they are safe against session-scoped container reuse.

Test plan (for reviewer, since local yarn/uv installs are skipped per Golden Agent policy)

  • CI unit tests (make test-unit) pass, including the two new use-case tests.
  • CI integration tests (make test-integration) pass, including the four new API tests.
  • Manually curl GET /agents?agent_card_metadata={\"permits_capable\":true} against a dev backend seeded with an agent card and confirm only that agent is returned.
  • Confirm openapi.yaml still matches the FastAPI-generated spec (make gen-openapi should produce no further diff).

Out of scope / follow-ups

  • Range queries, arbitrary operators, or a query language beyond exact containment.
  • Deployment-scoped cards or resolving card metadata through production_deployment_id.
  • Performance indexing — the endpoint uses top-level containment on an already-JSONB column and current agent counts don't warrant a GIN index yet. Revisit if listing at scale becomes a bottleneck.

Greptile Summary

The PR adds JSON-encoded agent-card metadata filtering to GET /agents, implemented through PostgreSQL JSONB containment. The latest revision fixes empty-object filtering so {} requires the metadata path to exist.

  • Parses and validates the new query parameter at the FastAPI boundary.
  • Propagates the filter through the use case into the agent repository.
  • Documents the mapping-typed parameter in OpenAPI.
  • Adds API and repository-backed tests for containment, pagination, invalid input, omitted filters, and empty-object behavior.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the previously reported empty-object bypass is fixed by applying the containment predicate for every non-null filter, including {}.

Important Files Changed

Filename Overview
agentex/src/api/routes/agents.py Adds schema-hidden runtime parsing and object validation while documenting the mapping-typed query parameter through OpenAPI metadata.
agentex/src/domain/use_cases/agents_use_case.py Preserves supplied metadata dictionaries, including empty dictionaries, under the repository-reserved filter key.
agentex/src/domain/repositories/agent_repository.py Applies nested JSONB containment whenever the metadata filter is present, resolving the previously reported empty-object bypass.
agentex/openapi.yaml Documents agent_card_metadata as a JSON-encoded object-valued query parameter.
agentex/tests/integration/api/agents/test_agents_api.py Covers matching, non-matching, multi-key, pagination, validation, omission, and empty-object filtering through the API and PostgreSQL repository path.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Client[GET /agents] --> Parse[Parse agent_card_metadata JSON]
  Parse --> Validate{JSON object?}
  Validate -->|No| BadRequest[400 response]
  Validate -->|Yes| UseCase[AgentsUseCase.list]
  UseCase --> Repository[AgentRepository.list]
  Repository --> Predicate[JSONB containment predicate]
  Predicate --> Results[Authorized non-deleted matching agents]
Loading

Reviews (2): Last reviewed commit: "fix(agentex): address review feedback on..." | Re-trigger Greptile

Context used:

Adds an optional `agent_card_metadata` query parameter to `GET /agents`
that applies an exact JSONB containment (`@>`) filter against
`registration_metadata.agent_card.metadata`. Agents whose card is
missing or does not contain every requested key/value are excluded; the
existing pagination, ordering, task filtering and authorization behavior
are preserved.

Enables discovery flows where consumers publish opt-in capability flags
via the AgentCard and need to enumerate only agents that advertise them.
@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown

✱ Stainless preview builds

This PR will update the agentex-sdk SDKs with the following commit messages.

openapi

feat(api): add agent_card_metadata query parameter

python

feat(api): add agent_card_metadata parameter to agents list method

typescript

feat(api): add agent_card_metadata parameter to list agents method

Edit this comment to update them. They will appear in their respective SDK's changelogs.

⚠️ agentex-sdk-openapi studio · code · diff

Your SDK build had at least one "warning" diagnostic, which is a regression from the base state.
generate ⚠️ (prev: generate ✅)

New diagnostics (1 warning)
⚠️ Parameter/MissingSchema: Defaulted parameter to `type: string` because no schema was defined.
⚠️ agentex-sdk-typescript studio · code · diff

Your SDK build had at least one new warning diagnostic, which is a regression from the base state.
generate ⚠️build ✅ (prev: build ⏭️) → lint ✅ (prev: lint ⏭️) → test ✅

npm install https://pkg.stainless.com/s/agentex-sdk-typescript/96527b033fb9e94a5d9046904b665e071cdef6e4/dist.tar.gz
New diagnostics (1 warning)
⚠️ Parameter/MissingSchema: Defaulted parameter to `type: string` because no schema was defined.
⚠️ agentex-sdk-python studio · code · diff

Your SDK build had at least one new warning diagnostic, which is a regression from the base state.
generate ⚠️build ✅ (prev: build ⏭️) → lint ✅ (prev: lint ⏭️) → test ✅

pip install https://pkg.stainless.com/s/agentex-sdk-python/9b8e7881f1bd2e125161e540de69b74cbe7f9afd/agentex_client-0.25.0-py3-none-any.whl
New diagnostics (1 warning)
⚠️ Parameter/MissingSchema: Defaulted parameter to `type: string` because no schema was defined.

This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push.
If you push custom code to the preview branch, re-run this workflow to update the comment.
Last updated: 2026-08-27 20:30:42 UTC

Comment thread agentex/src/domain/repositories/agent_repository.py Outdated

@basselatscale basselatscale left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSONB containment direction is right, and the integration coverage proves the important subset behavior: an agent whose card contains additional metadata still matches {"permits_capable": true}.

A few changes are needed before this is ready:

  1. list_agents() now fails when called directly without agent_card_metadata. Its default is a FastAPI Query object, so json.loads(agent_card_metadata) raises TypeError. This is currently failing the two authorization unit tests. Please use the Annotated[..., Query(...)] = None form, or otherwise ensure the Python default is actually None, and keep the direct-call tests passing.

  2. In AgentRepository.list, use if agent_card_metadata is not None: rather than a truthiness check. Otherwise an explicitly supplied {} silently bypasses the metadata predicate and includes agents with missing metadata.

  3. Please ensure the OpenAPI/SDK contract supports an ergonomic mapping input rather than requiring every caller to manually json.dumps it. The required consumer shape is:

    client.agents.list(
    agent_card_metadata={"permits_capable": True},
    )

If the wire parameter must remain JSON encoded, the generated/client layer should perform that encoding. The current string schema generates string-typed SDK parameters.

Once those are fixed, this server-side capability is sufficient for our immediate goal: discovering AgentCard-published workflow descriptors and removing the generated input-contract bundle.

- Use Annotated[str | None, Query(...)] = None so list_agents() called
  directly (outside FastAPI) defaults to None instead of a Query object
- Apply the containment predicate on `is not None` so an explicit {}
  filter still requires a card metadata object to be present
- Declare the query parameter with `content: application/json` and an
  object schema so SDK generators expose a mapping-typed parameter and
  perform the JSON wire encoding themselves

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@declan-scale

Copy link
Copy Markdown
Collaborator Author

Addressed all three review items in 52b3277:

  1. Direct-call default: agent_card_metadata now uses Annotated[str | None, Query(...)] = None, so calling list_agents() directly gets a real None default. The two authz unit tests pass again (updated to expect the forwarded agent_card_metadata=None kwarg).
  2. Empty-object bypass: the repository now applies the containment predicate on is not None, so an explicit {} requires a card metadata object to be present. Added an integration test covering this.
  3. SDK contract: the query parameter is now declared in the spec with content: application/json and an object schema (via openapi_extra; the runtime string param is schema-hidden). That's the OpenAPI signal for a mapping-typed SDK parameter that the client JSON-encodes on the wire, so client.agents.list(agent_card_metadata={"permits_capable": True}) becomes the generated shape. openapi.yaml regenerated via make gen-openapi.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants