Changelog — 2026 Week 38

2026-09-17

2026-09-17

  • Operations: scripts/rescore-all.ts --urls= and --status= now select rows and actually write them — --urls= was documented for rescoring, but selectSites() ignored it and the flag routed to calibration mode, which is dry-run always: on 2026-09-16 a 374-site "remainder" run crawled for four hours, logged plausible scores and wrote nothing. --urls=<source urls> and --status=blocked,unknown_failure,… now filter the candidate set on the writing path. --urls is matched against all candidates before --status narrows, so a URL that exists but is excluded by status reports "zero sites" instead of a false "unmatched URL"; matching normalises origin plus path (trailing slash dropped, host lowercased, query string kept, www. deliberately not stripped, because over-matching would write a row nobody named). Calibration moved behind an explicit --calibrate (which requires --urls); --rubric-compare is unchanged and still read-only. Any mode that writes nothing now says so in its first log line and in its exit summary, and a run that resolves to zero sites aborts loudly rather than printing DONE. Argument handling is strict, because the failure class here is silent over-selection: unknown flags are rejected (a --url= typo used to select — and write — all ~1.4k sites), a present-but-empty value (--status=$UNSET_VAR) aborts instead of degrading to "no filter", repeated value flags are rejected, and --rubric-compare combinations that cannot mean anything (--dry-run, --urls with --status, --out= outside compare mode) are refused. Regression tests: tests/rescore-all-select.test.ts.
  • Audit accuracy: DataDome and PerimeterX challenges are named, from live captures (core tarball update) — scripts/probe-audit-failures.ts --dump captures status, headers, a 64 KB body prefix and the current classifier verdict for each unknown_failure host into an operator-named file (read-only: no DB writes, one fetch attempt per host, --status selects the whole filtered set unless --limit is given), and the two new CHALLENGE_FINGERPRINTS entries were written from those captures rather than from memory: DataDome's geo.captcha-delivery.com (not the bare delivery domain, which is a documented CSP value customers ship on ordinary pages) and PerimeterX's id="px-captcha" (not the bare substring, whose first occurrence is a CSS rule). The honest result: 6 of the 50 unknown_failure hosts are named, not the ~40 the plan hoped for. Most of that population is not a challenge at all — roughly 16 are Akamai hard blocks — 12 of them the same static 360–384 byte "Access Denied" page, the other 4 larger custom block pages, none with a JS challenge or a captcha, ~10 are Cloudflare Error 1020 blocks carrying only JavaScript-Detections instrumentation (the standing www.g2.com negative control, correctly excluded), and the rest are bare 403s behind an ELB or no identifiable vendor. Naming those needs a mechanism other than detectChallenge → blocked, because that path's message asserts the site "served an anti-bot challenge". No Akamai or Imperva fingerprint ships, deliberately: zero Akamai challenges exist in 26 Akamai-fronted captures, and Imperva's only evidence is an HTTP 200 interstitial, which no body fingerprint can reach at any size cap because the classifier is status-gated. PerimeterX ships dormant — both real captures sit beyond the 4 KB body window. Regression tests: core tests/challenge.test.ts (every new pattern has a negative control that must not match); platform tests/probe-audit-dump.test.ts.
  • Audit accuracy: the agent probe set is derived from the registry (core tarball update) — AGENT_UAS was a hardcoded three, so adding a documented user agent to bot-tokens.ts silently left it in notProbed as though it were unprobeable. It is now derived from BOT_TOKENS.filter(purpose === "user" && uaString), and the homepage is probed under all six: the three that back a scored reach-* check first, pinned in their historical positions and keys (ChatGPT-User, Perplexity-User, Google-Agent), then the rest in registry order (Amzn-User, MistralAI-User, Meta-ExternalFetcher). The pinning is not cosmetic: the homepage loop is budget-gated per fetch, so a token's position decides whether its check is measured at all on a slow site, and plain registry order would have flipped reach-perplexity and reach-google between pass and unmeasured on identical input. Extra key pages stay at the first three — the pass runs on a min(timeout*3, 15s) wall-clock budget that already produced 82 unmeasured results, and six UAs across three pages would buy budget-skipped results rather than coverage. edge-agent-access completeness is therefore measured per page, against the tokens probed on that page, with an explicit gate that no probed token went unanswered everywhere (which the old union rule gave for free); a token probed on the homepage but not on an extra page now shows per page in details as <url>.notProbed: … (extra-pages-only) instead of being indistinguishable from "probed and failed", and the site-wide notProbed line is Claude-User alone, since Anthropic publishes no UA string. metadata.multiUaResponses gains an Amzn-User, MistralAI-User and Meta-ExternalFetcher key — a derived entry is keyed by its own registry token, so a vendor-string typo cannot move a scored key — and the homepage results grow from three entries to six, so any reader indexing them by position past the third moves. No score changes: edge-agent-access is weight 0 and tracked, excluded from category renormalization, and the three scored reach-* checks keep their probe positions, their keys and their verdicts — the three CheckResults were captured over seven fixture scenarios before and after the change and the dumps are byte-identical, and budget exhaustion between agent fetches (the case that discriminates the orders) is pinned by its own tests. RUBRIC_VERSION stays 3.0 (see core docs/RUBRIC-CHANGELOG.md for why widening a tracked check's inputs owes no bump). At the next rescore expect more edge-agent-access results to read unmeasured — 12 agent fetches per site (6 on the homepage, 3 on each of up to 2 extra pages) instead of 9, against the same budget, plus the new unanswered gate — and longer details on every audited site. How much more is unmeasured, and the affected population is the ~1225 ok sites, not the failures: the planned owner re-run covers the failed statuses only, so it cannot size this — a site can flip consistent → unmeasured on a tracked check while its audit_status stays ok. Sizing it needs a rescore (or dry-run comparison) over a sample of the ok population with a before/after count of the check's outcomes. Regression tests: core tests/edge-probe-registry.test.ts, tests/edge-probe.test.ts, tests/edge-agent-access.test.ts, tests/edge-evidence-summary.test.ts, tests/calibration-fixes.test.ts.
  • Docs: counts, statuses and shipped surface re-synced to the code — no behaviour change. docs/PRODUCT.md documents the rescore flags as they now behave (--urls=/--status= write; --calibrate --urls=, --dry-run and --rubric-compare do not; the abort rules), audit_history.edge_evidence, the fingerprinted challenge vendors (PerimeterX dormant behind the 4 KB body prefix) and the null public score for a failed latest attempt, and corrects the rubric notice date to 2026-09-15 (the 2026-09-14 entry below predates the cutover date being set). docs/API.md corrects the webhook trigger (an Authorization: Bearer token, not a body field, with its 401/503 responses) and states that /api/v0/servers returns a null audit_score/audit_grade for a site whose latest audit failed. docs/BUGS.md closes the p0 webhook auth bypass, fixed in df2fe77. Core README.md, README_CN.md and docs/GETTING-STARTED.md replace the pre-2.0 "60 checks / six categories / A = 90+" scoring description with rubric 3.0's 56 checks (50 scored, 6 tracked), seven category weights and 75/60/45/30 grade bands (the sample report and the Nuxt/Astro adapter check lists too), and core docs/RUBRIC-CHANGELOG.md logs the DataDome/PerimeterX fingerprints as a no-bump 3.0 note.
  • Methodology spec v1.0 published at /methodology — the spec is now canonical in the OSS repo (docs/spec/agent-readiness-spec-v1.0.md, text licensed CC BY 4.0) and the platform renders a byte-identical copy at /methodology (react-markdown + remark-gfm), stamped with the spec version, rubric version and publish date, listed in the sitemap and linked from the footer Learn section. scripts/sync-spec.ts copies the canonical file from the sibling OSS checkout (../w2agent, or a path argument / W2AGENT_REPO), and --check exits non-zero when the copies differ; tests/methodology-spec.test.ts fails when the copy's engine version differs from core's RUBRIC_VERSION or when any registered core, site-type or platform check lacks its documented weight, detection rule, evidence boundary, maturity tier or weight rationale. Spec content audited against the 3.0 registry (56 core checks, 50 scored and 6 tracked): adds the tracked usage-preference-declared and edge-agent-access entries, audit-level failures and challenge fingerprints, crawl inputs and the multi-UA probe set, maturity tiers (M1–M4 / H), site-type and platform adapter checks (disclosing that astro-sitemap-integration always fails and nuxt-llms-module ignores the llms.txt probe), and states that weights are not yet calibrated (P4-2 pending). No score changes.

2026-09-15

  • Fix: agent subdomains serve their own well-known files again — since the E3 mcp.json change, middleware.ts passed every /.well-known/* request through before the agent-subdomain rewrites, so agent.w2agent.dev and <slug>.agent.w2agent.dev returned w2agent.dev's own agent card and /.well-known/ucp 404'd. Agent hosts now route /.well-known/agent-card.json to /api/meta-agent/agent-card / /api/agent/<slug>/agent-card and /.well-known/ucp to /api/agent/<slug>/ucp. agent.w2agent.dev/.well-known/mcp.json still serves the meta MCP server card. Other /.well-known/* paths on agent hosts, including mcp.json on slug hosts, now fall through to the agent route instead of the apex files. Apex hosts are unchanged, and the Clerk and no-Clerk branches now share one routing function. Regression tests: tests/middleware-routing.test.ts.

  • Fix: hosted agents speak the A2A v1.0 wire format their cards advertise — hosted agent cards claimed JSON-RPC protocolVersion: "1.0", but SendMessage returned a bare pre-0.3 task (state: "completed", {type: "text"} parts, artifact id) that v1.0 clients can't parse. The response is now result: {task} with TASK_STATE_COMPLETED/TASK_STATE_FAILED, contextId, status.timestamp, artifactId and bare {text} parts. Input accepts v1.0 {text} parts and still takes legacy kind: "text" / type: "text" parts. Behaviour changes for callers: a message with no non-empty text part now returns JSON-RPC -32602 instead of a failed task; message.taskId returns -32001 (tasks are not stored); an A2A-Version header (or ?A2A-Version= query parameter) with a major other than 1 returns -32009 (no version is still served). Hosted cards now say capabilities.streaming: false (the handler never streamed), provider.organization instead of provider.name, and drop the non-spec capabilities.a2a. Regression tests: tests/a2a.test.ts (spec §6.1 request fixture), lib/agent-host.test.ts.

  • Fix: w2agent's own agent cards are honest A2A v1.0 cards that pass our audit — the meta agent (agent.w2agent.dev) now answers A2A SendMessage by running search_websites on the message text (after the existing IP rate limit; MCP requests and invalid JSON reach the MCP transport as before). Its card and w2agent.dev's /.well-known/agent-card.json are now v1.0 cards (supportedInterfaces → https://agent.w2agent.dev/a2a, streaming: false, text/plain modes, provider.organization) that list only search_websites. The meta card drops the five MCP-only tools, and the apex card drops the audit / llms.txt generation / deploy skills, which no A2A endpoint serves. Hosted agent cards advertise A2A at https://<slug>.agent.w2agent.dev/a2a instead of the homepage, so self-audits score pass instead of stub 50; the root still answers A2A. Both agent endpoints now return the spec's errors for the other v1.0 methods instead of routing them to MCP: -32004 for streaming, subscribe and the extended card, -32003 for push-notification configs, -32001 for GetTask/CancelTask; ListTasks returns an empty page. Fixed a bridge bug: hosted-agent SendMessage always returned TASK_STATE_FAILED, because the MCP transport answered as SSE and the bridge parsed JSON. Adapter agents (data/adapters/*.yaml) have no A2A endpoint, so their agent card now 404s, and neither get_website_info nor the site page links one. Regression tests: tests/agent-cards.test.ts (core well-known-agent-card check on the served cards), tests/a2a-routes.test.ts, tests/a2a.test.ts, tests/middleware-routing.test.ts.

  • Fix: /a2a speaks only A2A, and A2A answers follow the spec more strictly — the advertised https://agent.w2agent.dev/a2a and https://<slug>.agent.w2agent.dev/a2a now reach dedicated A2A-only routes (/api/meta-agent/a2a, /api/agent/<slug>/a2a) with the same rate limit, plan quota and usage/bot tracking as before. An A2A client sending Accept: application/json used to get MCP's 406 or parse error there. It now gets JSON-RPC -32700 for invalid JSON, -32600 for non-object bodies, and -32601 for MCP methods; GET is 405 Allow: POST. The root paths still detect A2A. Hosted agent cards now list only search-site (they also listed page, FAQ, form, OpenAPI and commerce tools, which A2A never reaches; those stay on MCP). Behaviour changes on every A2A path: a tool that throws is now TASK_STATE_FAILED with a generic message (the SDK's error result used to come back as a completed task containing the error text); A2A-Version must be 1.0 (1.1 now gets -32009); a bad JSON-RPC envelope (jsonrpc ≠ "2.0", boolean/object id) is -32600; ListTasks pageSize outside 1-100 is -32602; configuration.returnImmediately: true is -32004. w2agent.dev's card gets the legacy top-level url the other w2agent cards have. Regression tests: tests/a2a-routes.test.ts, tests/a2a.test.ts, lib/agent-host.test.ts, tests/middleware-routing.test.ts.

  • Failure honesty: an unmeasurable site names its cause (core tarball update) — core used to throw one Could not reach <host> for DNS failures, TLS errors, timeouts and rate limits alike, and a homepage 403 did not throw at all: the engine scored the anti-bot page. The homepage response is now classified once, after redirects and retries, into unreachable / blocked / auth_gated / rate_limited / timeout / unknown_failure and thrown as a typed AuditError carrying the evidence (final status, challenge vendor, signal). An anti-bot challenge is detected from a vendor-cited fingerprint table (cf-mitigated: challenge, Cloudflare's challenge-platform orchestrate path, window._cf_chl_opt, AWS WAF's x-amzn-waf-action, CAPTCHA scripts on a 403/429/503) and is never retried and never scored. Behaviour change at the next rescore: a homepage 403 with no challenge markers no longer produces a score, so sites currently ranked on an anti-bot or WAF error page drop out of the ranking and appear under "Couldn't measure" instead (scripts/probe-audit-failures.ts --all sizes this before deploy). Regression tests: core tests/challenge.test.ts, tests/audit-errors.test.ts; platform tests/probe-audit-failures.test.ts.

  • Failure honesty: every entrypoint that audits a registered site records the failed attempt, and /api/audit returns 422 — a failing audit from deploy, directory submit, self-hosted register or scheduled refresh used to leave no trace at all (only rescore-all recorded, as a flat error). runSiteAudit now writes an audit_history failure row with the typed status, error message and user_id, sets sites.audit_status, then rethrows, so every caller's existing error handling is unchanged. POST /api/audit answers an unmeasurable site with 422 {error: "audit_failed", kind, message} instead of a 500 and no longer reports it to Sentry (it audits a URL rather than a registered site and never goes through runSiteAudit, so it still writes no audit_history row — the typed reason reaches the caller only); its stream error event, and /api/directory/submit's, carry the same kind alongside the message string clients already parse. Requires pnpm db:migrate:prod (0030_audit_status_typed, which widens the audit_history.status and sites.audit_status CHECK constraints) before this deploy — code first would make every typed failure write violate the old constraint. No row is relabelled retroactively; the next rescore writes typed statuses. Regression tests: tests/audit-error.test.ts, tests/audit-write.test.ts, tests/rescore-site.test.ts, tests/run-site-audit-failure.test.ts, tests/refresh-site-audit-failure.test.ts, tests/audit-route-failures.test.ts.

  • Failure honesty: the leaderboard and the audit tool say why a site has no score — /leaderboard's "Couldn't measure" list showed bare domains; each row now carries a status badge (Anti-bot block / Unreachable / Auth-gated / Rate-limited / Timeout / Couldn't tell / Error / Not attempted). /leaderboard/[domain] heads an unmeasured domain with "This site has no public score", the badge, a next action naming what the site must change (no surface promises a rescore it cannot deliver), and an "Audit now" link that runs the audit live; its page metadata says "unmeasured (<label>)" instead of "Site Not Found". The audit tool and the directory submit form label a failed run with the same badge plus the engine's message, and an unrecognised kind from a newer core falls back instead of blanking the page. Archived leaderboard editions, which predate these statuses, are normalised on read to Error rather than a guessed kind. Until the rescore runs, today's error rows simply render an "Error" badge. Regression tests: tests/leaderboard-unmeasured.test.tsx, tests/audit-client-failure.test.ts, tests/audit-failure-ui.test.tsx, tests/submit-stream-kind.test.ts.

  • Audit accuracy: declared AI-usage preferences are reported (tracked, core tarball update) — new tracked check usage-preference-declared (weight 0, not scored) reads the robots.txt the crawl already fetched for Content-Signal:, AIPREF Content-Usage: and RSL License: lines, the homepage Content-Usage response header, and one new well-known probe /.well-known/tdmrep.json (behind the catch-all guard). It passes when any declaration is found and reports which vocabulary declared it, whether a training opt-out applies to a generic agent at the site root, and whether the robots file carries Cloudflare's managed Content Signals block — so a deliberate training opt-out reads as a stated preference rather than a misconfiguration. Declarations scoped to another user-agent or path are reported as scoped instead of being counted as the site-wide verdict. Nothing moves in any score: the check is weight 0, so the rubric stays 3.0 and category sums are unchanged. The rubric is now 56 checks, 6 of them tracked (site-type adapter checks still run on top of it, as before), and the new row shows under "Tracked (not scored)"; leaderboard rows pick it up at the next rescore. Regression tests: core tests/preferences.test.ts, tests/catch-all-guard.test.ts.

  • Audit accuracy: edge treatment of agent user-agents is reported (tracked, core tarball update) — robots.txt can say "allowed" while a CDN or WAF challenges the agent at the edge, which no check could see. The existing multi-user-agent pass now also records each response's status, a small header set, a 4 KB body prefix and any challenge fingerprint, for the homepage plus up to two extra pages. The probe picks those extra pages itself, inside probeSiteMetadata — the crawl's own ranking re-run over the homepage's links and the sitemap, so it takes the URLs a profile crawl would take first — but it fetches them itself instead of reusing the crawl's fetches, so the probed set is not guaranteed to be the set the audit scored. Each page gets its own control under our declared crawler, as a separate single-shot fetch — never the crawl's retrying page fetch, which could turn one transient 503 into an agent-rejected — and the fetches this pass adds are paced by the crawl's requestDelay. New tracked check edge-agent-access (weight 0, not scored) compares them: a page counts as evidence only when its control came back ok, 2xx and challenge-free, so an edge that challenges every user-agent alike no longer reads as an agent block. A challenge, 403 or 429 where the control got 2xx is agent-blocked, another non-2xx is agent-rejected, a 402 is paywalled; consistent needs completeness as well as agreement — some comparable page where every probed user-agent answered 2xx — and no usable control, no agent response, or no page where all of them answered is unmeasured; details carries the per-page, per-agent grid and the documented agents with no published UA string are listed as not probed. It is tracked rather than scored because the probe identifies as an agent from our own network and so is not a CDN-verified agent request, and the reach-* checks are unchanged so nothing is counted twice. No score moves; from the next rescore every ok audit_history row records the pass/fail status of both tracked checks in check_results, which is what a future decision to score this check would measure prevalence from. check_results holds only {status, score} per check, so the per-agent grid and the outcome value are not queryable across sites — they survive only inside each run's full report JSON (audits/<id>.json behind /audit/[id], sites/<slug>/audit.json for a hosted site) and in the rendered report. Regression tests: core tests/edge-probe.test.ts, tests/edge-agent-access.test.ts; platform tests/audit-summary-tracked-persistence.test.ts.

2026-09-14

  • Score integrity: deploy no longer accepts client-asserted scores — POST /api/agent/deploy ignores auditScore/auditGrade in the body; only the server-side post-deploy audit writes audit_history. That audit now records the full result (category scores + llms.txt/agent-card/MCP flags) via the shared summarizeAuditReport helper (lib/audit-summary.ts, also used by scripts/rescore-all.ts), and the score-only minimal write path is removed from recordAuditAttempt.
  • Score integrity: failed-audit sites no longer leak stale scores — directory search min_score only matches sites whose latest audit succeeded (also in the fuzzy-match fallback, which now honours all structured filters and only serves the first page) and orders by the public score (failed-audit sites and unscored sites last); the quality-score cron ignores audit_score when the latest audit failed; has_mcp now means the mcp-discovery check passed (WebMCP / MCP metadata checks no longer set it — leaderboard MCP adoption will drop after the next rescore; expected); scripts/rescore-all.ts counts a failed DB write as a write error instead of recording the site as status='error'.
  • Score integrity: one crawl profile for every score — deploy, refresh, directory submit/register, /api/audit, scripts/rescore-all.ts and calibration now audit on core's AUDIT_CRAWL_PROFILE (10 pages, 10 s; previously rescore 5 pages/15 s, refresh/submit 20 pages, deploy the whole deploy crawl). /api/audit no longer honours a client options.maxPages. The platform's reduced fetchSiteMetadata is removed — spider.cloud and incremental crawls take metadata from core probeSiteMetadata (MCP init, 8 well-knowns, multi-UA), so a spider-cloud redeploy no longer flips has_mcp/category scores against rescore. Spider-cloud deploys' audits now run their own fresh profile crawl. Scores for sites last scored by rescore-all at 5 pages may move on their next audit.
  • Score integrity: audit_history.crawl_profile — every audit attempt row (ok and failed) now records core's AUDIT_CRAWL_PROFILE_ID, so a score move caused by a crawl-profile change is visible in history. Migration 0028_audit_crawl_profile adds the nullable column; older rows stay NULL (unknown profile). Requires pnpm db:migrate:prod before deploy.
  • Security: call-time SSRF re-validation of stored agent endpoints — the health-check cron (checkEndpoint), route_query routing and workflow steps now re-check the stored endpoint (DNS included) at call time, so an endpoint registered while public that later resolves to a private/metadata address is refused and never contacted. Routing and workflows refuse before connecting (DNS bounded to 5 s), and their MCP transports send every request (initialize, tools/list, tools/call, SSE GET, DELETE) through core guardedFetch, so each request URL and each redirect hop is checked. Health checks also go through guardedFetch: redirects to public hosts are followed and the final status is judged (as before), redirects to private hosts are refused; latency now includes the per-hop DNS checks. A refused, unresolvable or DNS-timed-out endpoint counts as a health-check failure. The same per-hop check now covers hosted-agent form-submit and OpenAPI tool calls and the register endpoint reachability probe. IPv4-mapped/compatible and NAT64 IPv6 addresses (any spelling, e.g. ::ffff:a00:1) are decoded and judged by the IPv4 rules, and :: is blocked.
  • Security: every audit/crawl/generate fetch and redirect hop is private-IP checked — w2agent-core gained an isAllowedTarget hook applied to each fetch and each redirect hop; the platform passes isAllowedCrawlTarget (lib/url-validation.ts: validateCrawlUrl bounded to the audit per-request timeout, 10 s) into all audits (runProfileAudit: deploy, refresh, submit, register, rescore; /api/audit), legacy/spider-cloud/incremental crawls, the register metadata probe, /api/generate and on-demand search page fetches. Incremental-refresh page refetches now go through core guardedFetch, so a refused target keeps the stored page. Previously only the submitted URL was validated, so a public site's sitemap entry (subdomain) or a redirect could point the crawler at an internal address. Crawled page URLs whose path matches a cloud-metadata pattern (e.g. /metadata/v1/…) are now skipped, and each crawl fetch costs up to two extra DNS lookups.
  • CI: typecheck + production build — .github/workflows/ci.yml now runs Lint → Typecheck (tsc --noEmit) → Test → Build (next build). The 51 pre-existing type errors (all in test fixtures/mocks) are fixed; the build needs no secrets or env beyond NEXT_TELEMETRY_DISABLED.
  • Audit accuracy (rubric v3.0 groundwork, core tarball update) — catch-all guard: a well-known file only counts when it is served 200 with a JSON content-type and differs from the site's soft-404 body; robots.txt/llms.txt served as HTML count as absent; sites answering 200 for non-existent paths get a catch-all-responses report warning. Tracked (not scored): checks marked tracked are excluded from category scores, top issues, warnings and site flags, and the audit check list shows them in a separate neutral "Tracked (not scored)" section, never as fixes (no check is tracked yet). Retired: well-known-agents-json, well-known-ara and well-known-ai-plugin are removed and those paths are no longer probed (5 well-knowns now); sim-chatgpt expects only an OpenAPI spec; directory register no longer falls back to agents.json as an agent card. llms.txt reweight: llms-txt-exists 2→0.5, llms-txt-valid 1→0.5, llms-txt-links-resolve 1→0.5, and the missing-llms.txt message no longer claims LLMs can't discover the site. Rubric version stays "2.0" until the v3.0 bump; scores (agent-discovery especially, and sites that passed on catch-all 200s) will move at the next rescore.
  • Audit accuracy: MCP discovery rewrite (core tarball update) — the audit now finds MCP servers by posting server/discover (MCP 2026-07-28) to same-site /mcp//sse/mcp.* links on the homepage, then /mcp, mcp.<host>/mcp and mcp.<host>/, falling back to a 2025-11-25 initialize (and a legacy SSE endpoint event) per candidate. /.well-known/mcp is no longer probed. mcp-discovery passes 100 on a live handshake, 75 on an OAuth 401 (WWW-Authenticate: Bearer with resource_metadata=), partial 50 for legacy HTTP+SSE only; only 307/308 redirects are followed. The MCP auth/metadata checks (C1/C2/B3/D4) are N/A only when no MCP endpoint was confirmed. Directory register (and scripts/auto-discover.ts) mark a site self-hosted when core confirmed an MCP endpoint and use that URL as the default agent endpoint (previously <origin>/.well-known/mcp).
  • Audit accuracy: api-catalog and operator signing (core tarball update) — api-catalog-rfc9727 now requires an RFC 9727 linkset served as application/linkset+json (at least one linkset entry with an anchor and item/service-desc/service-doc links); the old apis[] shape fails. web-bot-auth-directory is retired and replaced by the tracked (not scored) agent-operator-signing check: a signing directory served as application/http-message-signatures-directory+json with an Ed25519 key and a signature_agent on the site's own origin. Tracked checks that skip (the site has no such file) are hidden from the audit check list and the CLI verbose report.
  • Audit accuracy: AI agent access and user-agent reach (core tarball update) — robots-txt-ai-bots is replaced by ai-agent-access, which reads robots.txt per purpose tier from a vendor-cited token table (training, search, user-initiated fetchers). Only blocking search or user-fetch agents lowers the score; training opt-outs are neutral. agent-allowlist is retired. The five sim-* checks are replaced by reach-openai-user-agent, reach-perplexity-user-agent and reach-google-user-agent (weight 0.5 each; the homepage must return 2xx with a body under ChatGPT-User, Perplexity-User and Google-Agent). Claude is omitted because Anthropic publishes no user-agent string, and DeepSeek is dropped. A new check status error means "unmeasured" (for example, the crawl budget ran out): it is left out of the score, raises an "unmeasured checks" audit warning, and renders in the audit check list as a blue ? marked "unmeasured" (hidden for tracked checks).
  • Audit accuracy: robots.txt rate limits and Crawl-delay (core tarball update) — a robots.txt that keeps answering 429 through every retry now scores ai-agent-access as error (unmeasured) instead of pass, since the file was never seen; genuine 403/404/410 still pass. The generated robots.txt fix keeps a moved agent's Crawl-delay.
  • Audit accuracy: OAuth discovery, server card, A2A v1.0 agent cards (core tarball update) — six OAuth/MCP-auth checks (oauth-support, oauth-protected-resource, oauth-metadata-rfc9728, mcp-auth-mechanism, mcp-oauth-metadata, mcp-pkce-s256) are replaced by oauth-discovery (follows a 401 WWW-Authenticate or protected-resource metadata to an issuer-matched authorization server, including off-origin servers through the SSRF guard), mcp-auth and cimd-support; pkce-s256 now reads the discovered server. mcp-server-card moves to Agent Discovery as a tracked check (<mcp endpoint>/server-card, an ai-catalog entry, or /.well-known/mcp/server-card.json; /.well-known/mcp.json is no longer probed). well-known-agent-card validates the A2A v1.0 shape: v0.3 cards and cards whose only interface is the homepage score partial. 54 checks total. Platform: the leaderboard/directory agent-card flag now counts a pass or partial well-known-agent-card (a v0.3 card is still a card; a JSON file that is not an A2A card no longer counts); directory register reads the agent endpoint from v1.0 supportedInterfaces (JSONRPC preferred); hosted agent cards add the v1.0 fields (supportedInterfaces, defaultInputModes/defaultOutputModes, skill tags); /learn/agent-card documents the v1.0 shape.
  • Audit accuracy: UCP profile validation and WebMCP tracked (core tarball update) — well-known-ucp now passes only for a profile with a top-level ucp object carrying a YYYY-MM-DD version and non-empty services and payment_handlers maps; any other JSON fails and the message names the missing fields. A missing or invalid profile counts only on sites detected as ecommerce (not applicable elsewhere; a valid profile still passes on a store the detector missed). Both WebMCP checks (webmcp-declarative, webmcp-imperative) are now tracked (not scored): they show under "Tracked (not scored)" when detected and are hidden otherwise. Imperative detection recognises document.modelContext (and navigator.modelContext) in inline scripts, up to 5 same-origin script files when no inline code is found (www/apex variants included, <base href> honoured, redirects followed only within the site, fetched through the SSRF guard), and a WebMCP origin-trial token; declarative detection counts every toolname form plus unannotated task forms (unannotated search, login and newsletter forms excluded). Generated WebMCP scripts use document.modelContext with a navigator.modelContext fallback. Agent-protocols category scores move at the next rescore. Hosted ecommerce agents' generated /.well-known/ucp manifest has no payment_handlers, so it fails well-known-ucp when audited.
  • Rubric 3.0 (core tarball 0.2.0) — the platform takes RUBRIC_VERSION from core (lib/rubric.ts re-exports it; no platform copy), so audits and audit_history rows are stamped 3.0. The dismissible "Rubric updated" banner on /audit and /leaderboard is replaced by a permanent notice "Rubric 3.0 since 2026-09-14 — see what changed"; /audit/[id] labels a stored report with the rubric it was scored under (a 2.0 report stays "Rubric v2.0"). Migration 0029_audit_check_results adds nullable audit_history.check_results ({ status, score } per check, tracked checks included; NULL on failed attempts) and drops the '2.0' default on rubric_version. scripts/rescore-all.ts --rubric-compare runs the frozen 2.0 engine and the 3.0 engine side by side per site without writing to the database and writes a JSON report (median deltas overall, per category and per site type; per-check status transitions). Requires pnpm db:migrate:prod (0028 and 0029) before deploy.
  • Methodology spec v1.0-draft and rubric changelog — the methodology spec moves from its v0.1 filename to docs/spec/agent-readiness-spec-v1.0-draft.md and is updated to rubric 3.0: the error (unmeasured) status and tracked checks in the scoring algorithm, the N/A cascade table as the engine runs it, the catch-all / soft-404 evidence rule, a catalog of the 54 current checks with a "Tracked (not scored)" list and a "Retired in 3.0" list, and a new normative emerging-standards policy (scored / tracked / retired states, changes only at a major version bump, newly promoted checks at weight 0.5 or less for one cycle, merges are not promotions, documented exceptions for UCP, cimd-support and llms.txt). Core ships docs/RUBRIC-CHANGELOG.md (version bump rules and every 3.0 check change with old/new weight and rationale; the measured 2.0 → 3.0 delta is pending the owner's --rubric-compare run). No score changes.
Changelog — Changelog — 2026 Week 38 | w2agent