diff --git a/docs/INTERFACE.md b/docs/INTERFACE.md index a288a16..e58b5c4 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.19). Do not edit by hand — run `make contract`.* +*Generated from `live/guild/contract/contract.json` v2 (service 2.5.20). 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 0363488..3fbd21f 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.19" # PATCH (machine-visible semver, 2026-08-14): - # lets registry validators discover the paid search +__version__ = "2.5.20" # PATCH (machine-visible semver, 2026-08-14): + # makes the x402 Bazaar output contract for /search + # match its paid ranked SearchResponse, so validating + # autonomous buyers can accept the result after + # settlement. History of 2.5.19: 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 diff --git a/live/guild/app/x402.py b/live/guild/app/x402.py index f0e112d..e250d57 100644 --- a/live/guild/app/x402.py +++ b/live/guild/app/x402.py @@ -670,14 +670,77 @@ def resource_info(preq: "PaidRequest") -> ResourceInfo: }, } +# Some HTTP resources deliberately share a billing operation while returning +# different wire contracts. In particular, GET /check and GET /search both +# settle as ``best_agent`` but /search returns a ranked SearchResponse rather +# than the check response. A paying machine must be able to validate the +# response against the exact schema advertised before payment, so route-level +# contracts take precedence over the operation-level discovery fallback. +_BAZAAR_ROUTE_OUTPUTS = { + ("best_agent", "/search"): { + "capability": "fact-check", + "count": 1, + "results": [{ + "id": "agent_example", + "did": "did:key:z6MkExampleAgent", + "name": "Example Agent", + "capabilities": ["fact-check"], + "metadata": {}, + "trust": 0.93, + "rank": 1, + "confidence": 0.82, + "attestations_received": 4, + }], + }, +} + +_BAZAAR_ROUTE_OUTPUT_SCHEMAS = { + ("best_agent", "/search"): { + "type": "object", + "properties": { + "capability": {"type": "string"}, + "count": {"type": "integer", "minimum": 0}, + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "did": {"type": "string", "pattern": "^did:"}, + "name": {"type": "string"}, + "capabilities": { + "type": "array", + "items": {"type": "string"}, + }, + "metadata": {"type": "object"}, + "trust": {"type": "number"}, + "rank": {"type": "integer"}, + "confidence": {"type": "number"}, + "attestations_received": {"type": "integer"}, + }, + "required": [ + "id", "did", "name", "capabilities", "metadata", + "trust", "rank", "confidence", + "attestations_received", + ], + }, + }, + }, + "required": ["capability", "count", "results"], + }, +} + def bazaar_extension(preq: "PaidRequest") -> dict[str, Any]: query = dict(preq.query) + route_key = (preq.operation, preq.path) output: dict[str, Any] = { "type": "json", - "example": _BAZAAR_OUTPUT.get(preq.operation, {}), + "example": _BAZAAR_ROUTE_OUTPUTS.get( + route_key, _BAZAAR_OUTPUT.get(preq.operation, {})), } - output_schema = _BAZAAR_OUTPUT_SCHEMAS.get(preq.operation) + output_schema = _BAZAAR_ROUTE_OUTPUT_SCHEMAS.get( + route_key, _BAZAAR_OUTPUT_SCHEMAS.get(preq.operation)) if output_schema: output["schema"] = output_schema info: dict[str, Any] = { diff --git a/live/guild/contract/contract.json b/live/guild/contract/contract.json index ebffb2e..39f6395 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.19" + "version": "2.5.20" } } diff --git a/live/guild/tests/test_bazaar_service_metadata.py b/live/guild/tests/test_bazaar_service_metadata.py index af3b0ff..cd38d03 100644 --- a/live/guild/tests/test_bazaar_service_metadata.py +++ b/live/guild/tests/test_bazaar_service_metadata.py @@ -6,7 +6,7 @@ from app.payments import ( check_request, deep_preflight_request, evidence_bundle_request, machine_envelope_request, payment_decision_request, - protected_payment_decision_request, + protected_payment_decision_request, search_request, ) from x402.extensions.bazaar import ( extract_discovery_info_from_extension, @@ -69,9 +69,26 @@ def test_other_paid_products_receive_operation_specific_tags_without_schema_drif assert "decision" not in str(output) +def test_search_declares_its_ranked_result_contract_without_changing_check(): + search_output = x402.bazaar_extension( + search_request("fact-check"))["info"]["output"] + check_output = x402.bazaar_extension( + check_request("fact-check"))["info"]["output"] + + assert set(search_output["schema"]["required"]) == { + "capability", "count", "results"} + assert set(search_output["schema"]["properties"]["results"][ + "items"]["required"]) >= { + "id", "did", "name", "capabilities", "trust", "rank", + "confidence", "attestations_received"} + assert set(check_output["schema"]["required"]) == { + "schema_version", "capability", "status"} + + def test_flagship_bazaar_examples_match_declared_schemas_and_official_sdk(): requests = [ check_request("fact-check"), + search_request("fact-check"), check_request("fact-check", signed=True, ttl_seconds=3600), machine_envelope_request("a" * 64), payment_decision_request("b" * 64), diff --git a/live/guild/tests/test_discovery_probe_paid_routes.py b/live/guild/tests/test_discovery_probe_paid_routes.py index 91b2915..436a862 100644 --- a/live/guild/tests/test_discovery_probe_paid_routes.py +++ b/live/guild/tests/test_discovery_probe_paid_routes.py @@ -182,6 +182,12 @@ def test_search_bare_registry_probe_is_non_executable(client, settle_spy): assert detail["discovery_only"] is True assert detail["executable"] is False assert response.headers.get("PAYMENT-REQUIRED") + challenge = json.loads(base64.b64decode( + response.headers["PAYMENT-REQUIRED"])) + output_schema = challenge["extensions"]["bazaar"]["info"][ + "output"]["schema"] + assert set(output_schema["required"]) == { + "capability", "count", "results"} assert response.headers.get("WWW-Authenticate", "").startswith("Payment ") assert settle_spy == [] diff --git a/live/guild/tests/test_x402_v2.py b/live/guild/tests/test_x402_v2.py index f0eb58b..ac7faf8 100644 --- a/live/guild/tests/test_x402_v2.py +++ b/live/guild/tests/test_x402_v2.py @@ -29,6 +29,7 @@ import pytest from fastapi.testclient import TestClient +from jsonschema import Draft202012Validator from x402.schemas import PaymentPayload, ResourceInfo @@ -202,10 +203,17 @@ def test_paid_request_end_to_end_serves_result_and_settlement(monkeypatch): monkeypatch.setattr(x402, "_facilitator", lambda: fac) from app.main import app with TestClient(app) as client: + quote = client.get("/search?capability=anything") + assert quote.status_code == 402 + quoted = json.loads(base64.b64decode( + quote.headers["PAYMENT-REQUIRED"])) + output_schema = quoted["extensions"]["bazaar"]["info"][ + "output"]["schema"] p = make_payload(SEARCH) r = client.get("/search?capability=anything", headers={"PAYMENT-SIGNATURE": sig_header(p)}) assert r.status_code == 200 + Draft202012Validator(output_schema).validate(r.json()) resp_hdr = r.headers.get("PAYMENT-RESPONSE") assert resp_hdr, "success must carry the PAYMENT-RESPONSE header" settled = json.loads(base64.b64decode(resp_hdr)) diff --git a/server.json b/server.json index dea71d4..3a43891 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.19", + "version": "2.5.20", "repository": { "url": "https://github.com/AgentTanuki/agent-guild", "source": "github"