mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 20:27:15 +01:00
New agent_ready/ package (dist and live-url modes, 100% branch coverage) checks robots, sitemap, llms.txt, ai-catalog, 404, no-JS text, markdown mirrors and JSON-LD. repo_contract gains web-repo detection and installs the agent-ready pre-commit hook, CI workflow and check_agent_ready.sh shim; scripts/check_agent_ready.sh is the shared entry point. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NBScELfMUiieK9ManFbS3Y
157 lines
5.3 KiB
Python
157 lines
5.3 KiB
Python
"""Act stage: AR-ACT-01..10. Opt-in (``--act``): only for sites with user actions."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
from agent_ready import formats
|
|
from agent_ready.backends import HttpSite
|
|
from agent_ready.context import CATALOG, Ctx
|
|
|
|
_INIT = {
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "initialize",
|
|
"params": {
|
|
"protocolVersion": "2025-06-18",
|
|
"capabilities": {},
|
|
"clientInfo": {"name": "agentready-check", "version": "1"},
|
|
},
|
|
}
|
|
|
|
|
|
def _json(ctx: Ctx, path: str) -> object | None:
|
|
"""Parsed JSON at ``path`` when it is 200 and parses, else None."""
|
|
resp = ctx.site.get(path)
|
|
if resp.status != 200:
|
|
return None
|
|
try:
|
|
return json.loads(resp.body)
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def check_auth(ctx: Ctx) -> None:
|
|
"""AR-ACT-01: OAuth metadata (RFC 9728 ``resource`` or RFC 8414 ``issuer``)."""
|
|
prm = _json(ctx, "/.well-known/oauth-protected-resource")
|
|
asm = _json(ctx, "/.well-known/oauth-authorization-server")
|
|
ok = (isinstance(prm, dict) and "resource" in prm) or (
|
|
isinstance(asm, dict) and "issuer" in asm
|
|
)
|
|
ctx.report.verdict(
|
|
"AR-ACT-01",
|
|
[]
|
|
if ok
|
|
else [
|
|
"no OAuth metadata under /.well-known (oauth-protected-resource / oauth-authorization-server)"
|
|
],
|
|
"OAuth metadata discoverable under /.well-known",
|
|
)
|
|
|
|
|
|
def check_openapi(ctx: Ctx) -> None:
|
|
"""AR-ACT-02: /openapi.json with paths and an operationId on every operation."""
|
|
doc = _json(ctx, "/openapi.json")
|
|
if not isinstance(doc, dict) or not isinstance(doc.get("openapi"), str):
|
|
ctx.report.fail("AR-ACT-02", "/openapi.json missing or not an OpenAPI document")
|
|
return
|
|
paths = doc.get("paths") if isinstance(doc.get("paths"), dict) else {}
|
|
ops = [
|
|
op
|
|
for item in paths.values()
|
|
if isinstance(item, dict)
|
|
for m, op in item.items()
|
|
if m in ("get", "post", "put", "patch", "delete")
|
|
]
|
|
missing = sum(
|
|
1 for op in ops if not (isinstance(op, dict) and op.get("operationId"))
|
|
)
|
|
problems = ["no operations in paths"] if not ops else []
|
|
if missing:
|
|
problems.append(f"{missing} operations lack operationId")
|
|
ctx.report.verdict(
|
|
"AR-ACT-02", problems, f"/openapi.json describes {len(ops)} operations"
|
|
)
|
|
|
|
|
|
def _mcp_ok(ctx: Ctx) -> bool | None:
|
|
"""True if /mcp answers initialize (or is OAuth-gated); None in dist mode."""
|
|
if not isinstance(ctx.site, HttpSite):
|
|
return None
|
|
headers = {
|
|
"Content-Type": "application/json",
|
|
"Accept": "application/json, text/event-stream",
|
|
}
|
|
resp = ctx.site.request("/mcp", data=json.dumps(_INIT).encode(), headers=headers)
|
|
if resp.status == 401:
|
|
return "www-authenticate" in resp.headers
|
|
return resp.status == 200 and '"jsonrpc"' in resp.text and '"result"' in resp.text
|
|
|
|
|
|
def check_mcp(ctx: Ctx) -> None:
|
|
"""AR-ACT-03 MCP at /mcp, AR-ACT-04 server card, AR-ACT-10 an interface exists."""
|
|
ok = _mcp_ok(ctx)
|
|
if ok is None:
|
|
ctx.report.skip("AR-ACT-03", "/mcp is a server endpoint: check in url mode")
|
|
ctx.report.skip("AR-ACT-10", "MCP/CLI interface: check in url mode")
|
|
else:
|
|
ctx.report.verdict(
|
|
"AR-ACT-03",
|
|
[]
|
|
if ok
|
|
else [
|
|
"/mcp does not answer an MCP initialize (nor 401 + WWW-Authenticate)"
|
|
],
|
|
"/mcp answers MCP initialize",
|
|
)
|
|
ctx.report.verdict(
|
|
"AR-ACT-10",
|
|
[]
|
|
if ok
|
|
else ["no MCP server at /mcp; a CLI (AR-ACT-06) cannot be auto-detected"],
|
|
"agents can act through /mcp",
|
|
)
|
|
cards = [
|
|
t
|
|
for t in formats.catalog_types(_json(ctx, CATALOG))
|
|
if t == "application/mcp-server-card+json"
|
|
]
|
|
ctx.report.verdict(
|
|
"AR-ACT-04",
|
|
[] if cards else ["AI Catalog lists no application/mcp-server-card+json entry"],
|
|
"AI Catalog lists an MCP server card",
|
|
)
|
|
|
|
|
|
def check_webmcp(ctx: Ctx) -> None:
|
|
"""AR-ACT-07: WebMCP tools registered on document.modelContext."""
|
|
resp, page = ctx.home()
|
|
sources = [resp.text]
|
|
for src in page.scripts[:10]:
|
|
path = ctx.site.to_path(src)
|
|
if path:
|
|
sources.append(ctx.site.get(path).text)
|
|
found = any("modelContext" in s for s in sources)
|
|
ctx.report.verdict(
|
|
"AR-ACT-07",
|
|
[] if found else ["no document.modelContext usage in homepage HTML/scripts"],
|
|
"homepage registers WebMCP tools",
|
|
)
|
|
|
|
|
|
def run_act(ctx: Ctx) -> None:
|
|
"""All act checks, or one SKIP line when the stage is off."""
|
|
if not ctx.act:
|
|
ctx.report.skip(
|
|
"AR-ACT-10", "act stage off (pass --act for sites with user actions)"
|
|
)
|
|
return
|
|
ctx.report.guard("AR-ACT-01", lambda: check_auth(ctx))
|
|
ctx.report.guard("AR-ACT-02", lambda: check_openapi(ctx))
|
|
ctx.report.guard("AR-ACT-03", lambda: check_mcp(ctx))
|
|
ctx.report.guard("AR-ACT-07", lambda: check_webmcp(ctx))
|
|
ctx.report.skip("AR-ACT-05", "MCP Apps ui:// view: manual review")
|
|
ctx.report.skip("AR-ACT-06", "SDKs on npm/PyPI and a CLI: manual review")
|
|
ctx.report.skip("AR-ACT-08", "agent payments: n/a unless the product charges")
|
|
ctx.report.skip("AR-ACT-09", "NLWeb /ask endpoint: manual review")
|