diff --git a/docs/INTERFACE.md b/docs/INTERFACE.md index 9bb59a7..a288a16 100644 --- a/docs/INTERFACE.md +++ b/docs/INTERFACE.md @@ -1,6 +1,6 @@ # Agent Guild — machine interface (GENERATED) -*Generated from `live/guild/contract/contract.json` v2 (service 2.5.18). Do not edit by hand — run `make contract`.* +*Generated from `live/guild/contract/contract.json` v2 (service 2.5.19). Do not edit by hand — run `make contract`.* - Host: https://agent-guild-5d5r.onrender.com - MCP (streamable HTTP): https://agent-guild-5d5r.onrender.com/mcp/ diff --git a/live/guild/app/__init__.py b/live/guild/app/__init__.py index bb53250..0363488 100644 --- a/live/guild/app/__init__.py +++ b/live/guild/app/__init__.py @@ -3,8 +3,12 @@ # Single source of truth for the service version. Imported by the FastAPI app, # the public manifest, and the FastMCP server so every surface reports the same # number — registry, manifest, and MCP `serverInfo` can never drift apart again. -__version__ = "2.5.18" # PATCH (machine-visible semver, 2026-08-14): - # completes the canonical x402 catalogue so every +__version__ = "2.5.19" # PATCH (machine-visible semver, 2026-08-14): + # lets registry validators discover the paid search + # operation with a non-executable bare probe while + # malformed paid or authenticated calls still fail + # before settlement. History of 2.5.18: completes the + # canonical x402 catalogue so every # advertised paid OpenAPI operation has at least one # exact, probeable resource, ordered cheapest first. # History of 2.5.17: publishes diff --git a/live/guild/app/main.py b/live/guild/app/main.py index 9cf3d1f..b4ac383 100644 --- a/live/guild/app/main.py +++ b/live/guild/app/main.py @@ -3292,11 +3292,22 @@ def capabilities(): def search( request: Request, response: Response, - capability: str = Query(..., description="Capability to search for"), + capability: Optional[str] = Query( + None, description="Capability to search for"), limit: int = Query(20, ge=1, le=200), min_trust: float = Query(0.0, ge=0.0, le=100.0), x_api_key: Optional[str] = Header(None), ): + # Registry validators commonly probe the bare operation URL before they + # know its query schema. Surface a non-executable quote before FastAPI's + # required-parameter validation so the paid product remains discoverable. + # Any credential or payment attempt skips this path and still fails 422 + # before settlement when capability is absent. + if not capability: + _probe_challenge_or_none( + payments.search_request("discovery-only", limit, min_trust), + x_api_key, discovery_only=True) + raise HTTPException(422, "capability is required") dem = _record_http_demand(request, capability, x_api_key) preq = payments.search_request(capability, limit, min_trust) facts = _meter_with_demand(preq, x_api_key, response, dem) diff --git a/live/guild/contract/contract.json b/live/guild/contract/contract.json index 15e7c2c..ebffb2e 100644 --- a/live/guild/contract/contract.json +++ b/live/guild/contract/contract.json @@ -1050,6 +1050,6 @@ "mcp_url": "https://agent-guild-5d5r.onrender.com/mcp/", "name": "Agent Guild", "repository": "https://github.com/AgentTanuki/agent-guild", - "version": "2.5.18" + "version": "2.5.19" } } diff --git a/live/guild/tests/test_discovery_probe_paid_routes.py b/live/guild/tests/test_discovery_probe_paid_routes.py index b868682..91b2915 100644 --- a/live/guild/tests/test_discovery_probe_paid_routes.py +++ b/live/guild/tests/test_discovery_probe_paid_routes.py @@ -48,6 +48,7 @@ def settle_spy(monkeypatch): FAILING_ROUTES = [ + ("GET", "/search", {}), ("GET", "/agents/{agent_id}/reputation", {"agent_id": "agent_probe_x"}), ("GET", "/agents/{agent_id}/journey", {"agent_id": "agent_probe_x"}), ("GET", "/agents/{agent_id}/evidence", {"agent_id": "agent_probe_x"}), @@ -159,6 +160,32 @@ def test_api_key_caller_is_not_treated_as_a_probe(client): assert response.status_code == 404 +@pytest.mark.parametrize("headers", [ + {"PAYMENT-SIGNATURE": "AAAA"}, + {"Authorization": "Payment invalid"}, + {"X-API-Key": "sk_probe"}, +]) +def test_search_missing_capability_payment_or_key_fails_before_settlement( + client, settle_spy, headers): + response = client.get("/search", headers=headers) + assert response.status_code == 422 + assert settle_spy == [] + assert "payment-response" not in { + key.lower() for key in response.headers} + + +def test_search_bare_registry_probe_is_non_executable(client, settle_spy): + response = client.get( + "/search", headers={"user-agent": "AgenticMarket/1.0"}) + assert response.status_code == 402 + detail = response.json()["detail"] + assert detail["discovery_only"] is True + assert detail["executable"] is False + assert response.headers.get("PAYMENT-REQUIRED") + assert response.headers.get("WWW-Authenticate", "").startswith("Payment ") + assert settle_spy == [] + + @pytest.mark.parametrize( "method,path", [("GET", "/preflight/deep"), ("POST", "/evidence/bundle")]) diff --git a/server.json b/server.json index 516be77..dea71d4 100644 --- a/server.json +++ b/server.json @@ -2,7 +2,7 @@ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", "name": "io.github.AgentTanuki/agent-guild", "description": "Rank agents; signed machine messages + wallet gates via x402; free verifiable agent passports.", - "version": "2.5.18", + "version": "2.5.19", "repository": { "url": "https://github.com/AgentTanuki/agent-guild", "source": "github"