From b90c92fd5a9468c4ad64cb4de7f1420e9a24872b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 17:53:22 +0000 Subject: [PATCH] Revert "fix: enforce no-relay objective parity (#160)" This reverts commit 6499b9f6a01dda73e96b49dbe42d235b8d6bc21f. --- docs/INTERFACE.md | 2 +- live/guild/app/__init__.py | 9 +- live/guild/app/a2a.py | 23 +- live/guild/app/main.py | 207 +----------------- live/guild/app/mcp_server.py | 100 --------- live/guild/app/objective_match.py | 70 +----- live/guild/app/store.py | 30 +-- live/guild/contract/contract.json | 2 +- .../tests/test_event_retention_measurement.py | 34 --- live/guild/tests/test_incident_reporting.py | 15 -- .../guild/tests/test_machine_first_contact.py | 67 ------ live/guild/tests/test_mcp_x402.py | 32 --- registry/x402-payment-safety/server.json | 2 +- server.json | 2 +- 14 files changed, 43 insertions(+), 552 deletions(-) diff --git a/docs/INTERFACE.md b/docs/INTERFACE.md index d19e8b9..05eddb6 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` v3 (service 2.6.1). Do not edit by hand — run `make contract`.* +*Generated from `live/guild/contract/contract.json` v3 (service 2.6.0). 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 b4bc6e0..191c36b 100644 --- a/live/guild/app/__init__.py +++ b/live/guild/app/__init__.py @@ -3,14 +3,7 @@ # 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.6.1" # PATCH no-relay transport parity (2026-08-31): - # maps natural objectives before HTTP/MCP demand and - # payment handling; makes /check and /incidents 422s - # machine-useful without rejected-input echo; counts - # objective metrics from durable event history; and - # closes non-obvious objective→probe undercounting. - # History of 2.6.0: - # MINOR machine-first communications (2026-08-31): +__version__ = "2.6.0" # MINOR machine-first communications (2026-08-31): # adds the confidential AGIR-1 incident drop box with # signed hash-only receipts; deterministic hash-bound # objective matching and sub-1KiB first-contact diff --git a/live/guild/app/a2a.py b/live/guild/app/a2a.py index 78a09f2..c9df783 100644 --- a/live/guild/app/a2a.py +++ b/live/guild/app/a2a.py @@ -648,9 +648,26 @@ def _note_objective_followthrough(actor: str, capability: str, ua: str) -> None: session id, so same actor + canonical capability in the retained event tail is the strongest available link. A parent hash is counted once. """ - _objective.note_followthrough( - store, actor, capability, ua=ua, - endpoint="a2a_message", transport="a2a") + parent = None + for event in reversed(store.events[-500:]): + if (event.get("key") == actor + and event.get("type") == "query" + and event.get("caller_kind") == "objective_ask" + and event.get("capability") == capability): + parent = event.get("request_sha256") + break + if not parent: + return + if any(event.get("type") == "objective_action_followed" + and event.get("parent_request_sha256") == parent + and event.get("action") == "trust.check.full" + for event in store.events[-500:]): + return + store.record_event( + actor, "objective_action_followed", ua=ua, + endpoint="a2a_message", capability=capability, + action="trust.check.full", parent_request_sha256=parent, + attribution="same_actor_capability_recent_tail") @router.post("/a2a") diff --git a/live/guild/app/main.py b/live/guild/app/main.py index 6676bcd..c95bd19 100644 --- a/live/guild/app/main.py +++ b/live/guild/app/main.py @@ -19,10 +19,9 @@ import uuid import contextvars from urllib.parse import quote -from typing import Any, Callable, Literal, Optional +from typing import Any, Literal, Optional from fastapi import FastAPI, HTTPException, Header, Path, Query, Request, Response -from fastapi.exceptions import RequestValidationError from fastapi.exception_handlers import http_exception_handler from datetime import datetime, timezone @@ -72,7 +71,6 @@ from . import crypto from . import callerproof from . import demand -from . import objective_match from . import market from . import walletbinding from . import paymentdecision @@ -566,74 +564,6 @@ async def _machine_payment_required_handler(request: Request, headers=headers) -def _safe_validation_issues( - exc: RequestValidationError, allowed_fields: set[str]) -> list[dict]: - """Reduce framework validation errors to server-owned identifiers only. - - FastAPI's default 422 body includes the rejected ``input`` value. That is - useful for a human form and unsafe at a machine coordination boundary: it - can relay attacker-controlled text into the next agent context. Locations - are also allowlisted because an arbitrary object key can otherwise become - part of ``loc``. - """ - issues = [] - for error in exc.errors(): - location = next((str(part) for part in reversed(error.get("loc") or ()) - if str(part) in allowed_fields), "request") - issues.append({"field": location, - "code": str(error.get("type") or "invalid")}) - return issues[:16] - - -@app.exception_handler(RequestValidationError) -async def _machine_validation_handler(request: Request, - exc: RequestValidationError): - """Useful no-relay 422s for the two machine-first coordination surfaces.""" - if request.url.path in ("/check", "/search"): - return JSONResponse(status_code=422, content={ - "schema": "AGERR-1/1.0", - "kind": "capability_input_invalid", - "error": { - "code": "request_validation_error", - "issues": _safe_validation_issues( - exc, {"capability", "signed", "ttl_seconds", - "limit", "min_trust"}), - }, - "accepted": { - "canonical_id": "fact-check", - "natural_objective": "I need to verify a claim", - }, - "authority": {"mode": "advisory", "grants": []}, - "available_actions": [{ - "id": "capabilities.list", "effect": "read", - "requires_local_authorisation": False, - "call": {"method": "GET", "path": "/capabilities"}, - }], - }) - if request.url.path == "/incidents": - return JSONResponse(status_code=422, content={ - "schema": "AGERR-1/1.0", - "kind": "incident_report_invalid", - "error": { - "code": "request_validation_error", - "issues": _safe_validation_issues(exc, { - "category", "severity", "details", "content_sha256", - "task_ref", "mandate_ref", "nonce", - }), - }, - "authority": {"mode": "advisory", "grants": []}, - "available_actions": [{ - "id": "incident.schema", "effect": "read", - "requires_local_authorisation": False, - "call": {"method": "GET", "path": "/openapi.json"}, - }], - }) - # Preserve framework behavior elsewhere; this task hardens the two - # coordination boundaries without silently changing every API contract. - from fastapi.exception_handlers import request_validation_exception_handler - return await request_validation_exception_handler(request, exc) - - @app.exception_handler(PaymentIdConflict) async def _payment_id_conflict_handler(request: Request, exc: PaymentIdConflict): @@ -999,98 +929,6 @@ def _record_http_demand(request: Request, capability: str, caller_proof_verified=verified, caller_did=did) -def _http_objective_first_response( - request: Request, capability: str, x_api_key: Optional[str], *, - signed: bool = False, ttl_seconds: int = 3600, - endpoint: str = "check", - action_id: str = "trust.check.full", - paid_request_builder: Optional[Callable[[str], PaidRequest]] = None, - action_path_builder: Optional[Callable[[str], str]] = None, - ) -> tuple[Optional[str], Optional[JSONResponse]]: - """Separate canonical capability ids from natural-language objectives. - - Canonical ids continue to the existing demand + payment path. Prose is - mapped before either side effect, and receives a compact hash-bound capsule - rather than being slugified, billed, or reflected into an x402 challenge. - """ - if not (capability or "").strip(): - return None, JSONResponse(status_code=422, content={ - "schema": "AGERR-1/1.0", - "kind": "capability_input_invalid", - "error": {"code": "empty_capability"}, - "authority": {"mode": "advisory", "grants": []}, - "available_actions": [{ - "id": "capabilities.list", "effect": "read", - "requires_local_authorisation": False, - "call": {"method": "GET", "path": "/capabilities"}, - }], - }) - canonical = objective_match.canonical_input(capability) - if canonical is not None: - if endpoint == "check": - objective_match.note_followthrough( - store, _http_demand_actor(request, x_api_key), canonical, - ua=_ua.get(), endpoint=endpoint, transport="http") - return canonical, None - - matched = objective_match.match( - capability, store.capability_index().keys()) - mapped = matched.get("kind") in ( - "exact_canonical", "versioned_alias", "deterministic_tokens") - caller_kind = ("objective_ask" if mapped - else "objective_ambiguous" - if matched.get("kind") == "ambiguous" - else "objective_no_match") - actor = _http_demand_actor(request, x_api_key) - canonical = matched.get("canonical_capability") if mapped else None - binding = matched["request"] - store.record_event( - actor, "query", ua=_ua.get(), endpoint=endpoint, transport="http", - request_sha256=binding["sha256"], - request_utf8_bytes=binding["utf8_bytes"], - caller_kind=caller_kind, capability=canonical, - objective_match_kind=matched.get("kind")) - - if mapped: - # Recording demonstrated demand remains free, but records only the - # server-selected canonical id—not a slug made from caller prose. - _record_http_demand(request, canonical, x_api_key) - priced = billing.billing_enforced() and x402.enabled() - preq = (paid_request_builder(canonical) - if paid_request_builder is not None - else payments.check_request(canonical, signed, ttl_seconds)) - if action_path_builder is not None: - action_path = action_path_builder(canonical) - else: - action_path = "/check?capability=" + quote(canonical, safe="") - if signed: - action_path += ("&signed=true&ttl_seconds=" + str(ttl_seconds)) - payload = objective_match.objective_capsule( - matched, - None if priced else store.check(canonical, demand_recorded=True), - price_credits=(preq.cost if priced else None), - action_path=action_path, action_id=action_id) - if priced: - store.record_event( - actor, "paid_offer_shown", ua=_ua.get(), - endpoint="first_contact_capsule", transport="http", - challenged_operation=preq.operation, - impression="action_link", actor_distinct=True, - price_credits=preq.cost) - else: - payload = objective_match.unresolved_capsule( - matched, transport="http") - - raw = json.dumps(payload, default=str, ensure_ascii=False, - separators=(",", ":")).encode("utf-8") - store.record_event( - actor, "first_contact_response", ua=_ua.get(), endpoint=endpoint, - transport="http", caller_kind=caller_kind, capability=canonical, - request_sha256=binding["sha256"], response_bytes=len(raw), - response_kind=payload.get("kind")) - return None, JSONResponse(content=payload) - - def _meter_with_demand(preq: PaidRequest, x_api_key: Optional[str], response: Response, dem: Optional[dict]) -> dict: """meter(), with the FREE machine-readable `no_supply` block attached to @@ -3318,16 +3156,6 @@ def check_discovery_quote( demand, or returns protected content. A payment-bearing ``HEAD`` retry is therefore still only a quote and cannot settle funds. """ - canonical = objective_match.canonical_input(capability) - if canonical is None: - raise HTTPException(422, { - "error": "canonical_capability_required", - "detail": ("HEAD is a discovery quote and accepts only a canonical " - "capability id; use GET /check for objective mapping"), - "action": {"method": "GET", "path": "/check", - "query": {"capability": ""}}, - }) - capability = canonical preq = payments.check_request(capability, signed, ttl_seconds) challenge = PaymentChallenge(preq, extra={ "discovery_only": True, @@ -3361,12 +3189,6 @@ def check( provenance-labelled PROOF the Guild improves outcomes, and how to contribute back. `signed=true` returns a Guild-signed, offline-verifiable decision for gateway caching. hire/caution/avoid is legacy presentation.""" - capability, first_response = _http_objective_first_response( - request, capability, x_api_key, - signed=signed, ttl_seconds=ttl_seconds, endpoint="check") - if first_response is not None: - return first_response - assert capability is not None dem = _record_http_demand(request, capability, x_api_key) preq = payments.check_request(capability, signed, ttl_seconds) facts = _meter_with_demand(preq, x_api_key, response, dem) @@ -3755,17 +3577,6 @@ def search( payments.search_request("discovery-only", limit, min_trust), x_api_key, discovery_only=True) raise HTTPException(422, "capability is required") - capability, first_response = _http_objective_first_response( - request, capability, x_api_key, endpoint="search", - action_id="trust.search.full", - paid_request_builder=lambda cap: payments.search_request( - cap, limit, min_trust), - action_path_builder=lambda cap: ( - "/search?capability=" + quote(cap, safe="") - + "&limit=" + str(limit) + "&min_trust=" + str(min_trust))) - if first_response is not None: - return first_response - assert capability is not None 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) @@ -4861,20 +4672,8 @@ def report_incident( return incidents.submit( store, **report.model_dump(), reporter_agent=reporter, transport="http") - except ValueError: - # The rejected report is private even when invalid. Never put its - # content—or an exception that may contain it—back on the wire. - return JSONResponse(status_code=422, content={ - "schema": "AGERR-1/1.0", - "kind": "incident_report_invalid", - "error": {"code": "incident_integrity_check_failed"}, - "authority": {"mode": "advisory", "grants": []}, - "available_actions": [{ - "id": "incident.schema", "effect": "read", - "requires_local_authorisation": False, - "call": {"method": "GET", "path": "/openapi.json"}, - }], - }) + except ValueError as exc: + raise HTTPException(422, str(exc)) @app.get("/.well-known/agent-guild.json") diff --git a/live/guild/app/mcp_server.py b/live/guild/app/mcp_server.py index d113f9f..82bcbc4 100644 --- a/live/guild/app/mcp_server.py +++ b/live/guild/app/mcp_server.py @@ -19,7 +19,6 @@ import uuid from typing import Any, Callable, Optional from typing_extensions import TypedDict -from urllib.parse import quote from fastmcp import Context, FastMCP from fastmcp.tools.tool import ToolResult @@ -30,11 +29,9 @@ from . import __version__ from . import abuse -from . import billing from . import callerproof from . import incidents from . import demand -from . import objective_match from . import inbox as inbox_engine from . import journey as journey_engine from . import payments @@ -457,76 +454,6 @@ def _record_mcp_demand(capability: str, ctx: "Context | None", caller_did=(did if verified else "")) -def _mcp_objective_first_response( - capability: str, ctx: "Context | None", api_key: str = "", *, - endpoint: str = "guild_check", - action_id: str = "trust.check.full", - paid_request_builder: Optional[Callable[[str], PaidRequest]] = None, - action_path_builder: Optional[Callable[[str], str]] = None, - ) -> tuple[Optional[str], Optional[dict[str, Any]]]: - """MCP twin of HTTP's no-relay objective boundary.""" - canonical = objective_match.canonical_input(capability) - actor, distinct = _mcp_actor(ctx, api_key) - ua = _client_ua(ctx) - if canonical is not None: - if endpoint == "guild_check": - objective_match.note_followthrough( - store, actor, canonical, ua=ua, - endpoint=endpoint, transport="mcp") - return canonical, None - - matched = objective_match.match( - capability, store.capability_index().keys()) - mapped = matched.get("kind") in ( - "exact_canonical", "versioned_alias", "deterministic_tokens") - caller_kind = ("objective_ask" if mapped - else "objective_ambiguous" - if matched.get("kind") == "ambiguous" - else "objective_no_match") - canonical = matched.get("canonical_capability") if mapped else None - binding = matched["request"] - store.record_event( - actor, "query", ua=ua, endpoint=endpoint, transport="mcp", - request_sha256=binding["sha256"], - request_utf8_bytes=binding["utf8_bytes"], - caller_kind=caller_kind, capability=canonical, - objective_match_kind=matched.get("kind")) - - if mapped: - _record_mcp_demand(canonical, ctx, api_key) - priced = billing.billing_enforced() and x402.enabled() - preq = (paid_request_builder(canonical) - if paid_request_builder is not None - else payments.check_request(canonical)) - action_path = (action_path_builder(canonical) - if action_path_builder is not None else None) - payload = objective_match.objective_capsule( - matched, - None if priced else store.check(canonical, demand_recorded=True), - price_credits=(preq.cost if priced else None), - action_path=action_path, action_id=action_id) - if priced: - store.record_event( - actor, "paid_offer_shown", ua=ua, - endpoint="first_contact_capsule", transport="mcp", - challenged_operation=preq.operation, - impression="action_link", actor_distinct=distinct, - price_credits=preq.cost) - else: - payload = objective_match.unresolved_capsule( - matched, transport="mcp") - - raw = _json.dumps(payload, default=str, ensure_ascii=False, - separators=(",", ":")).encode("utf-8") - store.record_event( - actor, "first_contact_response", ua=ua, - endpoint=endpoint, transport="mcp", - caller_kind=caller_kind, capability=canonical, - request_sha256=binding["sha256"], response_bytes=len(raw), - response_kind=payload.get("kind")) - return None, payload - - def _with_inbox(result: Any, presented_key: str) -> Any: """In-band guild_inbox delivery for the MCP transport: when a tool call's credential authenticates a subject agent and the result is a plain dict @@ -1071,11 +998,6 @@ def guild_check(capability: str, api_key: str = "", Returns {capability, best_agent, verdict, shortlist, proof, why_trust_this, how_to_contribute}. Use guild_search / guild_risk_score for finer control. """ - capability, first_response = _mcp_objective_first_response( - capability, ctx, api_key) - if first_response is not None: - return first_response - assert capability is not None dem = _record_mcp_demand(capability, ctx, api_key) preq = payments.check_request(capability) @@ -1117,17 +1039,6 @@ def guild_search(capability: str, min_trust: float = 0.0, limit: int = 10, Example: guild_search(capability="fact-check", min_trust=40, limit=5) Returns a ranked list of {id, name, trust, confidence, price_per_call, rank}. """ - capability, first_response = _mcp_objective_first_response( - capability, ctx, api_key, endpoint="guild_search", - action_id="trust.search.full", - paid_request_builder=lambda cap: payments.search_request( - cap, limit, min_trust), - action_path_builder=lambda cap: ( - "/search?capability=" + quote(cap, safe="") - + "&limit=" + str(limit) + "&min_trust=" + str(min_trust))) - if first_response is not None: - return first_response - assert capability is not None dem = _record_mcp_demand(capability, ctx, api_key) preq = payments.search_request(capability, limit, min_trust) @@ -1164,17 +1075,6 @@ def guild_best_agent(capability: str, min_trust: float = 0.0, Example: guild_best_agent(capability="summarize") Returns one {id, name, trust, confidence, price_per_call, rank} or null. """ - capability, first_response = _mcp_objective_first_response( - capability, ctx, api_key, endpoint="guild_best_agent", - action_id="trust.search.full", - paid_request_builder=lambda cap: payments.search_request( - cap, 1, min_trust), - action_path_builder=lambda cap: ( - "/search?capability=" + quote(cap, safe="") - + "&limit=1&min_trust=" + str(min_trust))) - if first_response is not None: - return first_response - assert capability is not None dem = _record_mcp_demand(capability, ctx, api_key) preq = payments.search_request(capability, 1, min_trust) diff --git a/live/guild/app/objective_match.py b/live/guild/app/objective_match.py index 5a2f2ff..d6bbdda 100644 --- a/live/guild/app/objective_match.py +++ b/live/guild/app/objective_match.py @@ -50,15 +50,12 @@ _KNOWN_CANONICAL = frozenset(canonical for _, canonical, _ in ALIASES) _TOKEN_RE = re.compile(r"[a-z0-9]+", re.I) -_CAPABILITY_ID_RE = re.compile(r"[a-z0-9][a-z0-9_.\-]{0,63}", re.I) _EXPLICIT_RE = re.compile( r"^\s*(?:capability|check|hire|vet)\b\s*[:=]?\s*" r"([a-z0-9][a-z0-9_.\-]{0,63})\s*$", re.I) _OBJECTIVE_RE = re.compile( - r"(?:\b(?:can|could|would|will)\s+you\b|" - r"\b(?:need|find|looking|seeking|recommend|want|help|assist|perform|" - r"handle|complete|who\s+can|analyse|analyze|review|delegate|hire|vet|" - r"require|please)\b)", re.I) + r"\b(?:need|find|looking|recommend|want|help|who\s+can|analyse|analyze|" + r"review|delegate|hire|vet|require)\b", re.I) def request_binding(text: str) -> dict[str, Any]: @@ -70,21 +67,6 @@ def looks_like_objective(text: str) -> bool: return bool(_OBJECTIVE_RE.search(text)) -def canonical_input(text: str) -> str | None: - """Return a safe canonical capability id, or ``None`` for prose. - - This check happens before demand recording and payment quoting on every - capability-bearing trust surface. It deliberately accepts unknown ids: - an explicit ``korean-legal`` ask is legitimate unmet demand. It rejects - free text so a sentence can never be slugified into the demand namespace - or relayed into a payment challenge. - """ - stripped = (text or "").strip() - if not stripped or _CAPABILITY_ID_RE.fullmatch(stripped) is None: - return None - return canonical_capability(stripped) - - def _byte_span(text: str, start: int, end: int) -> dict[str, int]: return { "utf8_start": len(text[:start].encode("utf-8")), @@ -111,7 +93,8 @@ def match(text: str, live_capabilities: Iterable[str] = ()) -> dict[str, Any]: stripped = text.strip() whole = canonical_capability(stripped) - if stripped and whole in catalog and _CAPABILITY_ID_RE.fullmatch(stripped): + if stripped and whole in catalog and re.fullmatch( + r"[a-z0-9][a-z0-9_.\-]{0,63}", stripped, re.I): start = len(text) - len(text.lstrip()) return {"contract": CONTRACT, "alias_version": ALIAS_VERSION, "request": binding, "kind": "exact_canonical", @@ -209,8 +192,6 @@ def objective_capsule( full_check: dict[str, Any] | None, *, price_credits: int | None = None, - action_path: str | None = None, - action_id: str = "trust.check.full", ) -> dict[str, Any]: canonical = matched["canonical_capability"] if full_check is None: @@ -227,12 +208,11 @@ def objective_capsule( "reachability": decision.get("reachability_status"), } action = { - "id": action_id, + "id": "trust.check.full", "effect": ("metered_read" if price_credits is not None else "read"), "requires_local_authorisation": price_credits is not None, "call": {"method": "GET", - "path": (action_path or - "/check?capability=" + quote(canonical, safe=""))}, + "path": "/check?capability=" + quote(canonical, safe="")}, } if price_credits is not None: action["price_credits"] = price_credits @@ -249,8 +229,7 @@ def objective_capsule( } -def unresolved_capsule( - matched: dict[str, Any], *, transport: str = "a2a") -> dict[str, Any]: +def unresolved_capsule(matched: dict[str, Any]) -> dict[str, Any]: match_block = {k: v for k, v in matched.items() if k != "request"} if match_block.get("kind") == "ambiguous": candidates = match_block.get("candidates") or [] @@ -259,9 +238,6 @@ def unresolved_capsule( # the total plus a stable prefix and can inspect the linked catalog. match_block["candidate_count"] = len(candidates) match_block["candidates"] = candidates[:4] - action_call = ({"transport": "a2a", "send": "capabilities"} - if transport == "a2a" - else {"method": "GET", "path": "/capabilities"}) return { "schema": "AGFC-1/1.0", "kind": ("objective_ambiguous" if matched["kind"] == "ambiguous" @@ -272,36 +248,6 @@ def unresolved_capsule( "available_actions": [{ "id": "capabilities.list", "effect": "read", "requires_local_authorisation": False, - "call": action_call, + "call": {"transport": "a2a", "send": "capabilities"}, }], } - - -def note_followthrough(store: Any, actor: str, capability: str, *, - ua: str, endpoint: str, transport: str) -> None: - """Link an explicit canonical retry to a recent compact mapping. - - This remains a deliberately labelled heuristic: transports do not share a - conversation identifier. It stores only the parent's request hash and - canonical capability, never text, and counts a parent at most once. - """ - parent = None - for event in reversed(store.events[-500:]): - if (event.get("key") == actor - and event.get("type") == "query" - and event.get("caller_kind") == "objective_ask" - and event.get("capability") == capability): - parent = event.get("request_sha256") - break - if not parent: - return - if any(event.get("type") == "objective_action_followed" - and event.get("parent_request_sha256") == parent - and event.get("action") == "trust.check.full" - for event in store.events[-500:]): - return - store.record_event( - actor, "objective_action_followed", ua=ua, - endpoint=endpoint, transport=transport, capability=capability, - action="trust.check.full", parent_request_sha256=parent, - attribution="same_actor_capability_recent_tail") diff --git a/live/guild/app/store.py b/live/guild/app/store.py index 607b8ec..e0fb718 100644 --- a/live/guild/app/store.py +++ b/live/guild/app/store.py @@ -3785,12 +3785,8 @@ def objective_to_action_funnel(self) -> dict[str, Any]: All input is represented by SHA-256 and byte counts only. Follow-through is a labelled anonymous heuristic (same actor + canonical capability in the recent retained tail), never presented as a cryptographic session. - Counts come from one durable event snapshot when SQLite is active; the - bounded serving cache is not a measurement boundary. """ - events, coverage = self.measurement_event_snapshot(types=( - "query", "first_contact_response", "objective_action_followed")) - queries = [e for e in events + queries = [e for e in self.events if e.get("type") == "query" and e.get("caller_kind") in ( "objective_ask", "objective_ambiguous", @@ -3800,7 +3796,7 @@ def objective_to_action_funnel(self) -> dict[str, Any]: if e.get("caller_kind") == "objective_ambiguous"] no_match = [e for e in queries if e.get("caller_kind") == "objective_no_match"] - responses = [e for e in events + responses = [e for e in self.events if e.get("type") == "first_contact_response" and e.get("caller_kind") in ( "objective_ask", "objective_ambiguous", @@ -3809,7 +3805,7 @@ def objective_to_action_funnel(self) -> dict[str, Any]: if e.get("caller_kind") != "probe"] sizes = [int(e.get("response_bytes") or 0) for e in objective_responses if e.get("response_bytes") is not None] - follows = [e for e in events + follows = [e for e in self.events if e.get("type") == "objective_action_followed" and e.get("action") == "trust.check.full"] matched_parents = {e.get("request_sha256") for e in mapped @@ -3817,12 +3813,6 @@ def objective_to_action_funnel(self) -> dict[str, Any]: followed_parents = {e.get("parent_request_sha256") for e in follows if e.get("parent_request_sha256") in matched_parents} total = len(queries) - floor = coverage.get("history_floor") or self.event_history_floor or {} - if coverage.get("source") == "sqlite_durable": - unrecoverable = int(floor.get("omitted_before_cutover") or 0) - else: - unrecoverable = int( - coverage.get("events_omitted_by_retention") or 0) return { "schema": "AGFC-METRICS-1/1.0", "objective_requests": total, @@ -3848,16 +3838,10 @@ def objective_to_action_funnel(self) -> dict[str, Any]: "attribution": "same_actor_capability_recent_tail", }, "retention": { - "history_complete": bool(coverage.get("history_complete")), - "measurement_source": coverage.get("source"), - "scope": ("complete_durable_history" - if coverage.get("history_complete") - else "incomplete_history"), - # Compatibility field now means genuinely unavailable events, - # not rows merely absent from the serving-memory tail. - "events_omitted": unrecoverable, - "in_memory_tail_omitted": self.events_omitted_by_retention, - "history_floor": floor or None, + "history_complete": ( + self.events_omitted_by_retention == 0 + and not bool(self.event_history_floor)), + "events_omitted": self.events_omitted_by_retention, }, "privacy": "request hashes and canonical capabilities only; no caller text", } diff --git a/live/guild/contract/contract.json b/live/guild/contract/contract.json index 13c4219..d8b7f56 100644 --- a/live/guild/contract/contract.json +++ b/live/guild/contract/contract.json @@ -1195,6 +1195,6 @@ "payment_safety_mcp_card": "https://agent-guild-5d5r.onrender.com/.well-known/mcp/payment-safety-server-card.json", "payment_safety_mcp_url": "https://agent-guild-5d5r.onrender.com/mcp/payment-safety/", "repository": "https://github.com/AgentTanuki/agent-guild", - "version": "2.6.1" + "version": "2.6.0" } } diff --git a/live/guild/tests/test_event_retention_measurement.py b/live/guild/tests/test_event_retention_measurement.py index 4af1fba..2f16043 100644 --- a/live/guild/tests/test_event_retention_measurement.py +++ b/live/guild/tests/test_event_retention_measurement.py @@ -89,40 +89,6 @@ def test_funnel_experiment_and_revenue_read_durable_history(sqlite_store): assert revenue["attributed_external_payments"] == 1 -def test_objective_metrics_use_durable_history_not_serving_tail(sqlite_store): - s = sqlite_store - parent = "a" * 64 - s.record_event( - "a2a:buyer", "query", ua="a2a:test", endpoint="a2a_message", - caller_kind="objective_ask", capability="fact-check", - request_sha256=parent, request_utf8_bytes=24) - s.record_event( - "a2a:buyer", "first_contact_response", ua="a2a:test", - endpoint="a2a_message", caller_kind="objective_ask", - capability="fact-check", request_sha256=parent, - response_bytes=640, response_kind="objective_match") - s.record_event( - "a2a:buyer", "objective_action_followed", ua="a2a:test", - action="trust.check.full", parent_request_sha256=parent, - capability="fact-check") - for i in range(8): - s.record_event(None, "filler", i=i) - - assert s.events_omitted_by_retention > 0 - assert not any(event.get("request_sha256") == parent - for event in s.events) - metrics = s.objective_to_action_funnel() - assert metrics["objective_requests"] == 1 - assert metrics["mapped"] == 1 - assert metrics["response_bytes"]["observed"] == 1 - assert metrics["full_detail_followthrough"]["followed"] == 1 - retention = metrics["retention"] - assert retention["measurement_source"] == "sqlite_durable" - assert retention["history_complete"] is True - assert retention["events_omitted"] == 0 - assert retention["in_memory_tail_omitted"] > 0 - - def test_running_arm_and_price_state_survive_restart_unchanged(sqlite_store): s = sqlite_store baseline = {metric: 0 for metric in experiments.PRIMARY_METRICS} diff --git a/live/guild/tests/test_incident_reporting.py b/live/guild/tests/test_incident_reporting.py index 8263d79..ef66ec7 100644 --- a/live/guild/tests/test_incident_reporting.py +++ b/live/guild/tests/test_incident_reporting.py @@ -104,21 +104,6 @@ def test_reporter_can_supply_digest_without_relaying_content(): assert mismatch.status_code == 422 -def test_invalid_report_validation_never_echoes_rejected_body(): - marker = "PRIVATE-INVALID-INCIDENT-" + uuid.uuid4().hex - response = client.post("/incidents", json={ - "category": marker, - "severity": "high", - "details": marker, - }) - assert response.status_code == 422 - payload = response.json() - assert payload["schema"] == "AGERR-1/1.0" - assert payload["kind"] == "incident_report_invalid" - assert marker not in response.text - assert all("input" not in issue for issue in payload["error"]["issues"]) - - def test_public_read_surfaces_do_not_exist_and_admin_fails_closed(monkeypatch): response = _report("private operator record " + uuid.uuid4().hex) assert response.status_code == 201 diff --git a/live/guild/tests/test_machine_first_contact.py b/live/guild/tests/test_machine_first_contact.py index 8557387..47dd9aa 100644 --- a/live/guild/tests/test_machine_first_contact.py +++ b/live/guild/tests/test_machine_first_contact.py @@ -102,73 +102,6 @@ def test_unknown_objective_returns_hash_bound_no_match_without_demand_guess(): for action in payload["available_actions"]) -def test_can_you_unknown_objective_is_no_match_not_probe_ack(): - text = "Can you investigate flibbertigibbet phenomena SECRET-NO-RELAY" - payload, raw = _send(text) - assert payload["kind"] == "objective_no_match" - _assert_binding(payload, text) - assert "SECRET-NO-RELAY" not in raw.decode() - - -def test_http_natural_objective_is_compact_and_never_becomes_a_slug( - monkeypatch): - monkeypatch.setenv("GUILD_X402_ENABLED", "1") - monkeypatch.setenv("GUILD_X402_PAY_TO", "0x" + "11" * 20) - monkeypatch.setenv("GUILD_BILLING_ENFORCED", "1") - text = ("I need to fact-check a report for HTTP " - "SECRET-HTTP-RELAY-MARKER") - response = client.get("/check", params={"capability": text}) - assert response.status_code == 200 - assert len(response.content) < 1024 - assert "PAYMENT-REQUIRED" not in response.headers - payload = response.json() - assert payload["kind"] == "objective_match" - assert payload["match"]["canonical_capability"] == "fact-check" - assert payload["result"]["status"] == "mapping_only" - assert payload["available_actions"][0]["effect"] == "metered_read" - assert "SECRET-HTTP-RELAY-MARKER" not in response.text - - from app.state import store - demands = [event for event in store.events - if event.get("type") == "capability_demand"] - # The demand recorder deliberately deduplicates same-actor retries, so the - # canonical row may predate this call in a full-suite process. - assert any(event["capability"] == "fact-check" for event in demands) - assert "secret-http" not in json.dumps(store.events).lower() - - search_text = "Please find fact checking help SEARCH-RELAY-MARKER" - search = client.get("/search", params={"capability": search_text}) - assert search.status_code == 200 and len(search.content) < 1024 - assert "PAYMENT-REQUIRED" not in search.headers - search_body = search.json() - assert search_body["kind"] == "objective_match" - assert search_body["match"]["canonical_capability"] == "fact-check" - assert search_body["available_actions"][0]["id"] == "trust.search.full" - assert search_body["available_actions"][0]["call"]["path"].startswith( - "/search?capability=fact-check") - assert "SEARCH-RELAY-MARKER" not in search.text - - -def test_bare_check_has_useful_machine_error_without_framework_input_echo(): - response = client.get("/check") - assert response.status_code == 422 - payload = response.json() - assert payload["schema"] == "AGERR-1/1.0" - assert payload["kind"] == "capability_input_invalid" - assert payload["error"]["issues"][0]["field"] == "capability" - assert "input" not in payload["error"]["issues"][0] - assert payload["available_actions"][0]["call"]["path"] == "/capabilities" - - marker = "INVALID-SEARCH-RELAY-MARKER" - invalid_search = client.get("/search", params={ - "capability": "I need fact checking " + marker, - "min_trust": marker, - }) - assert invalid_search.status_code == 422 - assert invalid_search.json()["kind"] == "capability_input_invalid" - assert marker not in invalid_search.text - - def test_unicode_offsets_are_utf8_byte_offsets_and_matching_is_repeatable(): text = "🤖 Please help with fact checking" first, _ = _send(text) diff --git a/live/guild/tests/test_mcp_x402.py b/live/guild/tests/test_mcp_x402.py index de7b102..659761e 100644 --- a/live/guild/tests/test_mcp_x402.py +++ b/live/guild/tests/test_mcp_x402.py @@ -59,38 +59,6 @@ def test_unpaid_mcp_read_returns_challenge_not_the_payload(): assert "decision" not in sc -def test_natural_mcp_check_maps_before_payment_without_relay_or_slug(): - marker = "SECRET-MCP-RELAY-MARKER" - text = "I need help with fact checking " + marker - r = _call("guild_check", {"capability": text}) - assert not r.is_error - body = r.structured_content - assert body["kind"] == "objective_match" - assert body["match"]["canonical_capability"] == "fact-check" - assert body["result"]["status"] == "mapping_only" - assert body["available_actions"][0]["effect"] == "metered_read" - assert marker not in json.dumps(body) - - from app.state import store - demands = [event for event in store.events - if event.get("type") == "capability_demand"] - assert any(event["capability"] == "fact-check" for event in demands) - assert "secret-mcp" not in json.dumps(store.events).lower() - - -@pytest.mark.parametrize("tool", ["guild_search", "guild_best_agent"]) -def test_natural_mcp_search_surfaces_share_no_relay_boundary(tool): - marker = "SECRET-MCP-SEARCH-" + tool - r = _call(tool, {"capability": "Please find fact checking help " + marker}) - assert not r.is_error - body = r.structured_content - assert body["kind"] == "objective_match" - assert body["match"]["canonical_capability"] == "fact-check" - assert body["result"]["status"] == "mapping_only" - assert body["available_actions"][0]["id"] == "trust.search.full" - assert marker not in json.dumps(body) - - def test_unpaid_mcp_search_and_risk_also_gated(): r = _call("guild_search", {"capability": "x"}) assert r.is_error and r.structured_content["x402Version"] == 2 diff --git a/registry/x402-payment-safety/server.json b/registry/x402-payment-safety/server.json index 74d1bdd..98bc8d5 100644 --- a/registry/x402-payment-safety/server.json +++ b/registry/x402-payment-safety/server.json @@ -2,7 +2,7 @@ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", "name": "io.github.AgentTanuki/x402-payment-safety", "description": "Authorize x402 payments before signing with request-bound, signed safety decisions.", - "version": "2.6.1", + "version": "2.6.0", "repository": { "url": "https://github.com/AgentTanuki/agent-guild", "source": "github" diff --git a/server.json b/server.json index 890db8d..565cfc2 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.6.1", + "version": "2.6.0", "repository": { "url": "https://github.com/AgentTanuki/agent-guild", "source": "github"