From c54d0969165195599994a06a22a3a7fb297438d3 Mon Sep 17 00:00:00 2001 From: AgentTanuki <294486129+AgentTanuki@users.noreply.github.com> Date: Tue, 18 Aug 2026 13:06:28 +0100 Subject: [PATCH 1/2] fix(attribution): demote agentguild-skill UA to propagation_client (HTTP+MCP), never auto-qualified genuine external; retro read-time reclassification; invert discovery-reach census test --- live/guild/app/attribution.py | 27 +++++++++++++++++--- live/guild/app/store.py | 6 +++-- live/guild/tests/test_analytics_invariant.py | 4 +-- live/guild/tests/test_caller_classes.py | 16 ++++++++++++ live/guild/tests/test_discovery_reach.py | 14 +++++++--- 5 files changed, 56 insertions(+), 11 deletions(-) diff --git a/live/guild/app/attribution.py b/live/guild/app/attribution.py index 5715f39..5cf017f 100644 --- a/live/guild/app/attribution.py +++ b/live/guild/app/attribution.py @@ -11,8 +11,8 @@ ITSELF AS AN AGENT in a way our own traffic does not: * an MCP client that named itself in the handshake (`mcp:`) and is not one of ours, OR - * a recognised agent-framework user-agent (httpx/langchain/openai/... ) that is not - bare tooling. + * a recognised independent agent-framework user-agent + (httpx/langchain/openai/... ) that is not bare tooling. Deliberately NOT sufficient (all indistinguishable from our own traffic, so counting them would fool us): @@ -33,10 +33,18 @@ FRAMEWORK_RE = re.compile( r"(httpx|aiohttp|langchain|langgraph|openai|anthropic|claude|llamaindex|" - r"crewai|autogen|agentguild-skill|" + r"crewai|autogen|" r"semantic-kernel|node-fetch|undici|axios|okhttp|go-http-client|reqwest|" r"cursor|cline|continue|windsurf|cody|dify|n8n|flowise)", re.I) +# Agent Guild publishes this User-Agent in its own installable skill and tells +# downstream runtimes to preserve it. It proves propagation of our client +# instructions, not an independent counterparty: the same identifier can be +# emitted by our own worker or copied by any caller. Keep it distinct from +# AG_TEST (a real third party may use the skill) and from EXTERNAL_* (the UA +# alone is never enough to claim external demand or revenue). +PROPAGATION_UA_RE = re.compile(r"\bagentguild-skill(?:/|\b)", re.I) + # Bare tooling — indistinguishable from our own verification calls. NOT genuine. # `guild-ops-check` is our own scheduled ops probe and is named here explicitly # so advertised telemetry (discovery_stats) can never count our own heartbeat @@ -162,6 +170,12 @@ def is_genuine_external(event: dict[str, Any]) -> bool: return False ua = (event.get("ua", event.get("user_agent")) or "").strip() + # This AG-authored identity may appear as either an HTTP User-Agent or an + # MCP clientInfo name (`mcp:agentguild-skill/...`). Guard it before the MCP + # short-circuit: neither transport proves an independent counterparty. + if PROPAGATION_UA_RE.search(ua): + return False + # A self-identified MCP client that isn't one of ours. client = _mcp_client(ua) if client is not None: @@ -189,6 +203,8 @@ def attribution_class(event: dict[str, Any]) -> str: return "ag_test" # our own self-identified harnesses if CRAWLER_UA_RE.search(ua): return "registry_crawler" # indexes manifests, never an agent + if PROPAGATION_UA_RE.search(ua): + return "propagation_client" # AG-authored identifier, no external proof if ua == "mcp/remote": return "unattributable_mcp" if not ua or TOOLING_UA_RE.search(ua): @@ -208,7 +224,8 @@ def attribution_class(event: dict[str, Any]) -> str: CALLER_CLASSES = ( "AG_INTERNAL", "AG_TEST", "REGISTRY_CRAWLER", - "EXTERNAL_UNKNOWN", "EXTERNAL_VERIFIED", "EXTERNAL_MEMBER", "OPERATOR", + "PROPAGATION_CLIENT", "EXTERNAL_UNKNOWN", "EXTERNAL_VERIFIED", + "EXTERNAL_MEMBER", "OPERATOR", ) # Registry / search-engine / uptime crawlers: they index manifests, they do @@ -305,6 +322,8 @@ def caller_class(event: Mapping[str, Any], *, return "AG_TEST" if CRAWLER_UA_RE.search(ua): return "REGISTRY_CRAWLER" + if PROPAGATION_UA_RE.search(ua): + return "PROPAGATION_CLIENT" if member and verified: return "EXTERNAL_VERIFIED" if member: diff --git a/live/guild/app/store.py b/live/guild/app/store.py index c2da1cb..89d7998 100644 --- a/live/guild/app/store.py +++ b/live/guild/app/store.py @@ -3026,7 +3026,8 @@ def discovery_reach(self, target: int = 25_000, *, * T1: a key-proved Guild member; * T2: a named third-party MCP client; - * T3: a recognised agent-framework UA (Sybil-unresolvable). + * T3: a recognised independent agent-framework UA + (Sybil-unresolvable). First-party traffic, Guild tests, generic tooling, registry crawlers, anonymous/unlinkable calls and repeated fetches are structurally @@ -3219,7 +3220,8 @@ def discovery_reach(self, target: int = 25_000, *, "One durable privacy-safe actor key counts once, across every " "origin surface and repeat call, only when it is a key-proved " "Guild member (T1), a named third-party MCP client (T2), or a " - "recognised agent-framework caller (T3). First-party traffic, Guild " + "recognised independent agent-framework caller (T3). First-party " + "traffic, Guild " "tests, generic tooling, registry crawlers, anonymous or " "unlinkable traffic, unproved bare members, and raw repeat " "impressions never count."), diff --git a/live/guild/tests/test_analytics_invariant.py b/live/guild/tests/test_analytics_invariant.py index 0cabfaf..e0551fc 100644 --- a/live/guild/tests/test_analytics_invariant.py +++ b/live/guild/tests/test_analytics_invariant.py @@ -128,5 +128,5 @@ def test_operator_kill_switch_event_is_audit_only(): def test_taxonomy_is_closed(): assert set(CALLER_CLASSES) == { - "AG_INTERNAL", "AG_TEST", "REGISTRY_CRAWLER", "EXTERNAL_UNKNOWN", - "EXTERNAL_VERIFIED", "EXTERNAL_MEMBER", "OPERATOR"} + "AG_INTERNAL", "AG_TEST", "REGISTRY_CRAWLER", "PROPAGATION_CLIENT", + "EXTERNAL_UNKNOWN", "EXTERNAL_VERIFIED", "EXTERNAL_MEMBER", "OPERATOR"} diff --git a/live/guild/tests/test_caller_classes.py b/live/guild/tests/test_caller_classes.py index d1b1dbf..b13773c 100644 --- a/live/guild/tests/test_caller_classes.py +++ b/live/guild/tests/test_caller_classes.py @@ -63,6 +63,22 @@ def test_growth_gate_is_closed_by_type(): assert external == {"EXTERNAL_UNKNOWN", "EXTERNAL_VERIFIED", "EXTERNAL_MEMBER"} +def test_agentguild_skill_ua_is_propagation_not_external_demand(): + """Our published skill UA cannot prove an independent counterparty.""" + from app.attribution import attribution_class + + for ua in ( + "agentguild-skill/1.0 (host=openclaw)", + "agentguild-skill/1.1 (host=codex; source=awesome-copilot)", + "mcp:agentguild-skill/1.1 (host=codex)", + ): + event = _e(ua=ua) + assert caller_class(event) == "PROPAGATION_CLIENT" + assert attribution_class(event) == "propagation_client" + assert not may_count_as_external_growth(caller_class(event)) + assert not is_genuine_external(event) + + def test_instrumentation_exposes_caller_class_counts(): r = client.post("/agents/register", json={"name": "CCTest", "capabilities": ["x"]}, diff --git a/live/guild/tests/test_discovery_reach.py b/live/guild/tests/test_discovery_reach.py index 2c67328..6e4e29f 100644 --- a/live/guild/tests/test_discovery_reach.py +++ b/live/guild/tests/test_discovery_reach.py @@ -92,16 +92,24 @@ def test_machine_resource_fetch_records_one_noncommercial_observation(): } for event in new) -def test_agent_skill_identity_is_transparent_and_qualified(tmp_path): +def test_agent_skill_identity_is_transparent_but_not_independent(tmp_path): store = Store(path=str(tmp_path / "guild.json")) store.record_event( "http:skill-user", "query", ua="agentguild-skill/1.0 (host=openclaw)", endpoint="check", actor_distinct=True, ) + store.record_event( + "mcp:skill-user", "query", + ua="mcp:agentguild-skill/1.1 (host=codex)", + endpoint="check", actor_distinct=True, + ) report = store.discovery_reach() - assert report["qualified_distinct_autonomous_agents"] == 1 - assert report["tiers"]["T3_framework_ua_actors"] == 1 + assert report["qualified_distinct_autonomous_agents"] == 0 + assert report["tiers"]["T3_framework_ua_actors"] == 0 + assert report["evidence"]["excluded_distinct_actors_by_reason"] == { + "propagation_client": 2, + } def test_ard_has_every_free_web_discovery_hook(): From e0aea7becccafa412cf26c8578ae9dc8e4820b91 Mon Sep 17 00:00:00 2001 From: AgentTanuki Date: Mon, 24 Aug 2026 18:42:31 +0100 Subject: [PATCH 2/2] chore(ship): retrigger certification after 3-round refusal (issue #151); no code change