Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions live/guild/app/attribution.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:<client>`) 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):
Expand All @@ -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
Expand Down Expand Up @@ -233,6 +241,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:
Expand Down Expand Up @@ -260,6 +274,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):
Expand All @@ -279,7 +295,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
Expand Down Expand Up @@ -378,6 +395,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:
Expand Down
6 changes: 4 additions & 2 deletions live/guild/app/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -3033,7 +3033,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
Expand Down Expand Up @@ -3236,7 +3237,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."),
Expand Down
4 changes: 2 additions & 2 deletions live/guild/tests/test_analytics_invariant.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
16 changes: 16 additions & 0 deletions live/guild/tests/test_caller_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]},
Expand Down
14 changes: 11 additions & 3 deletions live/guild/tests/test_discovery_reach.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_exact_registry_scanner_aliases_are_excluded_without_ua_overreach(
Expand Down
Loading