diff --git a/.github/workflows/house-style.yml b/.github/workflows/house-style.yml index 72424f1..40ebfab 100644 --- a/.github/workflows/house-style.yml +++ b/.github/workflows/house-style.yml @@ -11,6 +11,9 @@ name: House style # rendering, added in #31 after the catalog was found to be rendered from # remote JSON without them. It was never wired to a workflow, so it has been # passing or failing unobserved since. +# +# search.test.js covers the same guarantees for /search/, which renders text +# fetched from seven other hosts. on: pull_request: @@ -34,3 +37,5 @@ jobs: run: node tools/check-dashes.js - name: Marketplace rendering guards run: node marketplace/marketplace.test.js + - name: Search ranking and escaping + run: node search/search.test.js diff --git a/.gitignore b/.gitignore index c18dd8d..a771bb9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ __pycache__/ +availability-results.json diff --git a/404.html b/404.html index e380dd7..92ae424 100644 --- a/404.html +++ b/404.html @@ -3,7 +3,7 @@ Page not found | AgenTrust - + @@ -49,5 +49,5 @@
  • Get started with a specification or search integrations
  • Community and partners or the agentic controls
  • - + diff --git a/community/index.html b/community/index.html index eb41ebc..cf97036 100644 --- a/community/index.html +++ b/community/index.html @@ -31,7 +31,7 @@ - + @@ -335,6 +335,6 @@

    Read the field guide. Then run the companion examples.

    })(); - + diff --git a/demos/index.html b/demos/index.html index 99c13b9..b55d500 100644 --- a/demos/index.html +++ b/demos/index.html @@ -32,7 +32,7 @@ - + @@ -299,7 +299,7 @@

    Where to go next

    - + diff --git a/design-system.css b/design-system.css index 0e2af24..1aa0521 100644 --- a/design-system.css +++ b/design-system.css @@ -2094,3 +2094,25 @@ html:has(body.agentrust-hub) { scroll-behavior: smooth; } .on-this-page span { color: var(--at-muted); font-size: .66rem; letter-spacing: .11em; } .on-this-page a { color: var(--at-ink); text-decoration: none; } .on-this-page a:hover { color: var(--at-red); } + +/* /search/: one box across every AgenTrust site. search/search.js fills it. */ +.site-search-section { padding-top: 3.5rem; } +.site-search-lead { max-width: 46rem; color: var(--at-muted); } +.site-search-form { display: flex; gap: .6rem; margin: 1.6rem 0 1rem; } +.site-search-form input { flex: 1; min-width: 0; padding: .85rem 1rem; border: 1px solid var(--at-line); border-radius: 12px; background: var(--at-white); color: var(--at-ink); font: 1rem var(--at-sans); } +.site-search-form input:focus { outline: 2px solid var(--at-red); outline-offset: 1px; } +.site-search-filters { display: flex; flex-wrap: wrap; gap: .45rem; } +.site-search-filters button { padding: .35rem .8rem; border: 1px solid var(--at-line); border-radius: 999px; background: transparent; color: var(--at-ink); font: 600 .78rem var(--at-sans); cursor: pointer; } +.site-search-filters button[aria-pressed="true"] { border-color: var(--at-ink); background: var(--at-ink); color: var(--at-paper); } +.site-search-status { margin: 1.1rem 0 0; color: var(--at-muted); font: .8rem var(--at-sans); } +.site-search-count { margin: .2rem 0 0; font: 700 .85rem var(--at-sans); } +.site-search-results { margin: 1rem 0 0; padding: 0; list-style: none; } +.search-hit { padding: 1rem 0; border-top: 1px solid var(--at-line); } +.search-site { display: inline-block; margin-bottom: .25rem; color: var(--at-red); font: 800 .66rem var(--at-sans); letter-spacing: .08em; text-transform: uppercase; } +.search-title { display: block; color: var(--at-ink); font: 600 1.05rem var(--at-sans); text-decoration: none; } +.search-title:hover, .search-title:focus { color: var(--at-red); text-decoration: underline; } +.search-snippet { margin: .3rem 0; color: var(--at-muted); font-size: .92rem; line-height: 1.55; overflow-wrap: anywhere; } +.search-url { color: var(--at-muted); font: .72rem var(--at-mono); overflow-wrap: anywhere; } +.search-hit mark { background: rgba(185, 28, 28, .14); color: inherit; border-radius: 3px; padding: 0 .1em; } +.site-search-empty { margin-top: 1.5rem; padding: 2rem; border: 1px dashed var(--at-line); border-radius: 16px; text-align: center; } +@media (max-width: 560px) { .site-search-form { flex-direction: column; } } diff --git a/extensions/ca2a/v0.1/index.html b/extensions/ca2a/v0.1/index.html index 76e0d5e..006e62e 100644 --- a/extensions/ca2a/v0.1/index.html +++ b/extensions/ca2a/v0.1/index.html @@ -32,7 +32,7 @@ - + @@ -166,6 +166,6 @@

    This page is orientation

    - + diff --git a/go/agent-anomaly-detection/index.html b/go/agent-anomaly-detection/index.html index 899ee51..b5ccb54 100644 --- a/go/agent-anomaly-detection/index.html +++ b/go/agent-anomaly-detection/index.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@

    Anomaly detection on agent behaviour

    - + diff --git a/go/agent-circuit-breaker/index.html b/go/agent-circuit-breaker/index.html index fa0ea7e..19b718b 100644 --- a/go/agent-circuit-breaker/index.html +++ b/go/agent-circuit-breaker/index.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@

    Circuit breaker on agent loops

    - + diff --git a/go/agent-identity-credential/index.html b/go/agent-identity-credential/index.html index 8385929..b37d6db 100644 --- a/go/agent-identity-credential/index.html +++ b/go/agent-identity-credential/index.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@

    Agent identity credential

    - + diff --git a/go/agent-key-binding/index.html b/go/agent-key-binding/index.html index e5880b2..a1ea5aa 100644 --- a/go/agent-key-binding/index.html +++ b/go/agent-key-binding/index.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@

    Agent key binding and custody

    - + diff --git a/go/agent-purpose-declaration/index.html b/go/agent-purpose-declaration/index.html index 4ff9557..aad565b 100644 --- a/go/agent-purpose-declaration/index.html +++ b/go/agent-purpose-declaration/index.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@

    Declared agent purpose and scope

    - + diff --git a/go/agent-rate-limiting/index.html b/go/agent-rate-limiting/index.html index 7e05ed6..29e3331 100644 --- a/go/agent-rate-limiting/index.html +++ b/go/agent-rate-limiting/index.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@

    Rate limiting on agent actions

    - + diff --git a/go/agent-session-revocation/index.html b/go/agent-session-revocation/index.html index 17531ce..b51139a 100644 --- a/go/agent-session-revocation/index.html +++ b/go/agent-session-revocation/index.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@

    Agent session revocation

    - + diff --git a/go/agent-state-rollback/index.html b/go/agent-state-rollback/index.html index f452dff..ffa551b 100644 --- a/go/agent-state-rollback/index.html +++ b/go/agent-state-rollback/index.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@

    State rollback after an agent action

    - + diff --git a/go/agent-termination/index.html b/go/agent-termination/index.html index 834bf4f..a99d911 100644 --- a/go/agent-termination/index.html +++ b/go/agent-termination/index.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@

    Terminate a running agent

    - + diff --git a/go/attested-a2a-channel/index.html b/go/attested-a2a-channel/index.html index 9cea460..31d7183 100644 --- a/go/attested-a2a-channel/index.html +++ b/go/attested-a2a-channel/index.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@

    Attested agent-to-agent channel

    - + diff --git a/go/behavioural-baseline/index.html b/go/behavioural-baseline/index.html index d62a5b2..0ed194c 100644 --- a/go/behavioural-baseline/index.html +++ b/go/behavioural-baseline/index.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@

    Behavioural baseline for an agent

    - + diff --git a/go/blast-radius-containment/index.html b/go/blast-radius-containment/index.html index 99e8667..aa3f457 100644 --- a/go/blast-radius-containment/index.html +++ b/go/blast-radius-containment/index.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@

    Blast radius containment for agent execution

    - + diff --git a/go/capability-attenuation/index.html b/go/capability-attenuation/index.html index c92016e..7ffbeab 100644 --- a/go/capability-attenuation/index.html +++ b/go/capability-attenuation/index.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@

    Capability attenuation across the delegation chain

    - + diff --git a/go/capability-manifest/index.html b/go/capability-manifest/index.html index dbda4b1..3a7c776 100644 --- a/go/capability-manifest/index.html +++ b/go/capability-manifest/index.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@

    Declared capability manifest

    - + diff --git a/go/context-provenance/index.html b/go/context-provenance/index.html index 490c28a..de26081 100644 --- a/go/context-provenance/index.html +++ b/go/context-provenance/index.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@

    Context provenance for agent working memory

    - + diff --git a/go/continuous-usage-control/index.html b/go/continuous-usage-control/index.html index 2497e43..27264f9 100644 --- a/go/continuous-usage-control/index.html +++ b/go/continuous-usage-control/index.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@

    Continuous usage control after grant

    - + diff --git a/go/evidence-transparency-anchoring/index.html b/go/evidence-transparency-anchoring/index.html index a6959c7..0df9f7f 100644 --- a/go/evidence-transparency-anchoring/index.html +++ b/go/evidence-transparency-anchoring/index.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@

    Transparency-log anchoring of evidence

    - + diff --git a/go/graceful-degradation/index.html b/go/graceful-degradation/index.html index d6b8218..25fb85b 100644 --- a/go/graceful-degradation/index.html +++ b/go/graceful-degradation/index.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@

    Graceful degradation on policy engine failure

    - + diff --git a/go/index.html b/go/index.html index 266a9f2..04e2582 100644 --- a/go/index.html +++ b/go/index.html @@ -38,7 +38,7 @@ } - + @@ -405,7 +405,7 @@

    What this page does not do

    - + diff --git a/go/input-schema-validation/index.html b/go/input-schema-validation/index.html index dd20852..1142524 100644 --- a/go/input-schema-validation/index.html +++ b/go/input-schema-validation/index.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@

    Schema validation of agent input

    - + diff --git a/go/model-weight-custody/index.html b/go/model-weight-custody/index.html index c3e1a97..42874f6 100644 --- a/go/model-weight-custody/index.html +++ b/go/model-weight-custody/index.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@

    Model weight custody against the hosting operator

    - + diff --git a/go/output-encoding/index.html b/go/output-encoding/index.html index 9413e6c..e94ac27 100644 --- a/go/output-encoding/index.html +++ b/go/output-encoding/index.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@

    Encoding and injection prevention

    - + diff --git a/go/output-personal-data/index.html b/go/output-personal-data/index.html index d5d13fc..0497ad7 100644 --- a/go/output-personal-data/index.html +++ b/go/output-personal-data/index.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@

    Personal data protection in agent output

    - + diff --git a/go/policy-verdict-rationale/index.html b/go/policy-verdict-rationale/index.html index 1e21a78..3208f6b 100644 --- a/go/policy-verdict-rationale/index.html +++ b/go/policy-verdict-rationale/index.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@

    Policy verdict rationale

    - + diff --git a/go/prompt-injection-prevention/index.html b/go/prompt-injection-prevention/index.html index 5406714..149dd86 100644 --- a/go/prompt-injection-prevention/index.html +++ b/go/prompt-injection-prevention/index.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@

    Prompt injection prevention

    - + diff --git a/go/resource-allowlist/index.html b/go/resource-allowlist/index.html index 81ecde7..75226c5 100644 --- a/go/resource-allowlist/index.html +++ b/go/resource-allowlist/index.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@

    Resource allowlist

    - + diff --git a/go/runtime-attestation-evidence/index.html b/go/runtime-attestation-evidence/index.html index 5849f22..709cc5a 100644 --- a/go/runtime-attestation-evidence/index.html +++ b/go/runtime-attestation-evidence/index.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@

    Runtime attestation evidence

    - + diff --git a/go/structured-action-logging/index.html b/go/structured-action-logging/index.html index 1c3e860..68d8652 100644 --- a/go/structured-action-logging/index.html +++ b/go/structured-action-logging/index.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@

    Structured action logging

    - + diff --git a/go/tool-authorization/index.html b/go/tool-authorization/index.html index d1e7c71..8b5520b 100644 --- a/go/tool-authorization/index.html +++ b/go/tool-authorization/index.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@

    Tool authorization decision

    - + diff --git a/go/transaction-limits/index.html b/go/transaction-limits/index.html index 93be5d8..cc47a24 100644 --- a/go/transaction-limits/index.html +++ b/go/transaction-limits/index.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@

    Transaction and spend limits

    - + diff --git a/go/verifiable-evidence-record/index.html b/go/verifiable-evidence-record/index.html index b119945..ebe53f4 100644 --- a/go/verifiable-evidence-record/index.html +++ b/go/verifiable-evidence-record/index.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@

    Signed, third-party-verifiable evidence record

    - + diff --git a/index.html b/index.html index 780d98e..eea3a0b 100644 --- a/index.html +++ b/index.html @@ -126,7 +126,7 @@ ] } - + @@ -416,6 +416,6 @@

    Help steward it

    - + diff --git a/marketplace/catalog/index.html b/marketplace/catalog/index.html index 6cedde6..82244cd 100644 --- a/marketplace/catalog/index.html +++ b/marketplace/catalog/index.html @@ -4,7 +4,7 @@ All AgenTrust Marketplace Integrations | AgenTrust - + diff --git a/marketplace/index.html b/marketplace/index.html index 680fb5b..1f4194c 100644 --- a/marketplace/index.html +++ b/marketplace/index.html @@ -8,7 +8,7 @@ - + @@ -57,5 +57,5 @@
    AgenTrust

    Open specifications for verifiable AI

    - + diff --git a/quickstart/index.html b/quickstart/index.html index c3e2545..2928e02 100644 --- a/quickstart/index.html +++ b/quickstart/index.html @@ -55,7 +55,7 @@ } - + @@ -164,7 +164,7 @@

    Check real hardware evidence first

    - + diff --git a/registry/index.html b/registry/index.html index fcdc1a6..752f769 100644 --- a/registry/index.html +++ b/registry/index.html @@ -47,7 +47,7 @@ } - + @@ -296,7 +296,7 @@

    Three ways in

    - + diff --git a/search/index.html b/search/index.html new file mode 100644 index 0000000..9fc0272 --- /dev/null +++ b/search/index.html @@ -0,0 +1,130 @@ + + + + + +Search | AgenTrust + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + +
    + +
    + Every AgenTrust site +

    Search

    +

    Matches the documentation of WCM, Agent Manifest, cMCP, cA2A, TRACE, the conformance suite and the governance list, plus the pages of this site. Each site's own search index is fetched when this page opens, so results match what is deployed.

    + + + + +
    + +
    + +
    +
    +
    + AgenTrust +
    Open specifications for verifiable AI
    +
    + +
    Sponsored by OPAQUE, which funds the engineering, infrastructure and confidential-computing work behind these projects.
    +
    + +
    + + + + + + diff --git a/search/search.js b/search/search.js new file mode 100644 index 0000000..f419f42 --- /dev/null +++ b/search/search.js @@ -0,0 +1,315 @@ +/* Search across every AgenTrust site, for agentrust-io.com/search/. + * + * When the page loads it fetches each MkDocs site's own search_index.json (all + * seven send Access-Control-Allow-Origin: *) and the hub pages listed in + * sitemap.xml, then matches in the browser. There is no build step and no + * third-party service, so results are as fresh as each site's deployed index. + * + * Source text is only ever written with textContent and text nodes; matches are + * wrapped in elements built here, never parsed from the source. + */ +(function () { + 'use strict'; + + var SOURCES = [ + { id: 'wcm', label: 'WCM', base: 'https://wcm.agentrust-io.com/' }, + { id: 'manifest', label: 'Manifest', base: 'https://manifest.agentrust-io.com/' }, + { id: 'cmcp', label: 'cMCP', base: 'https://cmcp.agentrust-io.com/' }, + { id: 'ca2a', label: 'cA2A', base: 'https://ca2a.agentrust-io.com/' }, + { id: 'trace', label: 'TRACE', base: 'https://trace.agentrust-io.com/' }, + { id: 'tests', label: 'Tests', base: 'https://tests.agentrust-io.com/' }, + { id: 'governance', label: 'Governance', base: 'https://governance.agentrust-io.com/' } + ]; + var HUB = { id: 'hub', label: 'AgenTrust hub' }; + var LIMIT = 50; // results shown + var PER_PAGE = 2; // hits kept from one page, so one long page cannot fill the list + var SNIPPET = 180; // characters of context around the first match + + var ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ' }; + + function stripHtml(html) { + return String(html || '') + .replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gi, ' ') + .replace(/<[^>]*>/g, ' ') + .replace(/&(#\d+|#x[0-9a-f]+|[a-z]+);/gi, function (match, name) { + var key = name.toLowerCase(); + if (ENTITIES[key]) return ENTITIES[key]; + if (key[0] !== '#') return match; + var code = key[1] === 'x' ? parseInt(key.slice(2), 16) : parseInt(key.slice(1), 10); + return code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : ' '; + }) + .replace(/\s+/g, ' ') + .trim(); + } + + function tokenize(query) { + var seen = {}; + return (String(query || '').toLowerCase().match(/[\p{L}\p{N}][\p{L}\p{N}_.-]*/gu) || []) + .map(function (term) { return term.replace(/[._-]+$/, ''); }) + .filter(function (term) { return term && !seen[term] && (seen[term] = true); }); + } + + function buildUrl(base, location) { + return new URL(location || '', base).href; + } + + function makeDoc(source, url, page, title, pageTitle, text, isPage) { + title = title || pageTitle; + return { + site: source.id, label: source.label, url: url, page: source.id + ':' + page, + title: title, pageTitle: pageTitle, text: text, isPage: isPage, + head: (title + ' ' + pageTitle).toLowerCase(), body: text.toLowerCase() + }; + } + + // MkDocs writes one entry per page (location "page/") and one per heading + // ("page/#section"), with HTML in both title and text. + function fromMkDocs(source, index) { + var entries = (index && index.docs) || []; + var pageTitles = {}; + entries.forEach(function (entry) { + if (entry.location.indexOf('#') < 0) pageTitles[entry.location] = stripHtml(entry.title); + }); + return entries.map(function (entry) { + var page = entry.location.split('#')[0]; + var isPage = entry.location.indexOf('#') < 0; + return makeDoc(source, buildUrl(source.base, entry.location), page, stripHtml(entry.title), + pageTitles[page] || '', stripHtml(entry.text), isPage); + }).filter(function (doc) { return doc.title || doc.text; }); + } + + function occurrences(text, term) { + var n = 0, at = text.indexOf(term); + while (at >= 0 && n < 3) { n++; at = text.indexOf(term, at + term.length); } + return n; + } + + // Every term must appear. A term in the title or page title is worth 10, each + // of up to three in the text 1, the whole query as a phrase 15 in a title or 5 + // in the text, and a page-level entry 3 over one of its own sections. + function score(doc, terms, phrase) { + var total = 0; + for (var i = 0; i < terms.length; i++) { + var inHead = doc.head.indexOf(terms[i]) >= 0; + var inBody = doc.body.indexOf(terms[i]) >= 0; + if (!inHead && !inBody) return 0; + if (inHead) total += 10; + if (inBody) total += occurrences(doc.body, terms[i]); + } + if (terms.length > 1 && phrase) { + if (doc.head.indexOf(phrase) >= 0) total += 15; + else if (doc.body.indexOf(phrase) >= 0) total += 5; + } + return total + (doc.isPage ? 3 : 0); + } + + function search(docs, query, site) { + var terms = tokenize(query); + if (!terms.length) return []; + var phrase = String(query).toLowerCase().replace(/\s+/g, ' ').trim(); + var hits = []; + docs.forEach(function (doc) { + if (site && site !== 'all' && doc.site !== site) return; + var points = score(doc, terms, phrase); + if (points > 0) hits.push({ doc: doc, score: points }); + }); + hits.sort(function (a, b) { + return b.score - a.score || (b.doc.isPage - a.doc.isPage) || a.doc.title.localeCompare(b.doc.title); + }); + var perPage = {}, capped = []; + for (var i = 0; i < hits.length; i++) { + var key = hits[i].doc.page; + perPage[key] = (perPage[key] || 0) + 1; + if (perPage[key] <= PER_PAGE) capped.push(hits[i]); + } + return (site && site !== 'all' ? capped : interleave(capped)).slice(0, LIMIT); + } + + // Under All, the largest sites would otherwise fill the list on any common + // word. Take each site's best remaining hit in turn, strongest site first in + // every round, so every site with a match shows within the first few results. + function interleave(hits) { + var bySite = {}, order = [], out = []; + hits.forEach(function (hit) { + if (!bySite[hit.doc.site]) { bySite[hit.doc.site] = []; order.push(hit.doc.site); } + bySite[hit.doc.site].push(hit); + }); + while (out.length < hits.length) { + order.forEach(function (id) { if (bySite[id].length) out.push(bySite[id].shift()); }); + } + return out; + } + + function snippet(text, terms) { + var lower = text.toLowerCase(), first = -1; + terms.forEach(function (term) { + var at = lower.indexOf(term); + if (at >= 0 && (first < 0 || at < first)) first = at; + }); + var start = Math.max(0, first - 60); + return (start > 0 ? '…' : '') + text.slice(start, start + SNIPPET) + (start + SNIPPET < text.length ? '…' : ''); + } + + function highlight(text, terms) { + var lower = text.toLowerCase(), parts = [], i = 0; + while (i < text.length) { + var next = -1, length = 0; + terms.forEach(function (term) { + var at = lower.indexOf(term, i); + if (at >= 0 && (next < 0 || at < next || (at === next && term.length > length))) { next = at; length = term.length; } + }); + if (next < 0) { parts.push({ text: text.slice(i), mark: false }); break; } + if (next > i) parts.push({ text: text.slice(i, next), mark: false }); + parts.push({ text: text.slice(next, next + length), mark: true }); + i = next + length; + } + return parts; + } + + function appendParts(document, element, parts) { + parts.forEach(function (part) { + if (!part.mark) { element.append(document.createTextNode(part.text)); return; } + var mark = document.createElement('mark'); + mark.textContent = part.text; + element.append(mark); + }); + } + + function renderResult(document, hit, terms) { + var doc = hit.doc; + var item = document.createElement('li'); + item.className = 'search-hit'; + var site = document.createElement('span'); + site.className = 'search-site'; + site.textContent = doc.label; + var link = document.createElement('a'); + link.className = 'search-title'; + link.href = doc.url; + appendParts(document, link, highlight(doc.isPage || !doc.pageTitle || doc.pageTitle === doc.title + ? doc.title : doc.pageTitle + ' › ' + doc.title, terms)); + var text = document.createElement('p'); + text.className = 'search-snippet'; + appendParts(document, text, highlight(snippet(doc.text, terms), terms)); + var url = document.createElement('span'); + url.className = 'search-url'; + url.textContent = doc.url; + item.append(site, link, text, url); + return item; + } + + if (typeof module !== 'undefined' && module.exports) { + module.exports = { SOURCES: SOURCES, stripHtml: stripHtml, tokenize: tokenize, buildUrl: buildUrl, fromMkDocs: fromMkDocs, + score: score, search: search, interleave: interleave, snippet: snippet, highlight: highlight, renderResult: renderResult, LIMIT: LIMIT, PER_PAGE: PER_PAGE }; + return; + } + + var root = document.getElementById('site-search'); + if (!root) return; + var input = document.getElementById('search-q'); + var list = document.getElementById('search-results'); + var count = document.getElementById('search-count'); + var status = document.getElementById('search-status'); + var empty = document.getElementById('search-empty'); + var chips = Array.prototype.slice.call(root.querySelectorAll('[data-site]')); + var ids = ['all', HUB.id].concat(SOURCES.map(function (s) { return s.id; })); + var params = new URLSearchParams(location.search); + var state = { q: params.get('q') || '', site: ids.indexOf(params.get('site')) >= 0 ? params.get('site') : 'all' }; + var docs = [], done = 0, failed = [], total = SOURCES.length + 1, timer; + + function cached(key, load) { + try { var hit = sessionStorage.getItem(key); if (hit) return Promise.resolve(hit); } catch (e) { /* storage blocked */ } + return load().then(function (text) { + try { if (text.length < 1500000) sessionStorage.setItem(key, text); } catch (e) { /* quota or blocked */ } + return text; + }); + } + + function fetchText(url) { + return fetch(url).then(function (response) { + if (!response.ok) throw new Error(url + ': HTTP ' + response.status); + return response.text(); + }); + } + + function hubDocs(path, html) { + var page = new DOMParser().parseFromString(html, 'text/html'); + var clean = function (node) { return node ? stripHtml(node.textContent) : ''; }; + page.querySelectorAll('script, style, noscript, header, footer, nav').forEach(function (n) { n.remove(); }); + var title = clean(page.querySelector('title')).replace(/\s*\|\s*AgenTrust$/, ''); + var description = page.querySelector('meta[name="description"]'); + var main = page.querySelector('main') || page.body; + var url = location.origin + path; + var found = [makeDoc(HUB, url, path, title, title, ((description && description.content) || '') + ' ' + clean(main), true)]; + main.querySelectorAll('section[id]').forEach(function (section) { + var heading = section.querySelector('h2'); + if (heading) found.push(makeDoc(HUB, url + '#' + section.id, path, clean(heading), title, clean(section), false)); + }); + return found; + } + + function loadHub() { + return fetchText('/sitemap.xml').then(function (xml) { + var paths = (xml.match(/[^<]+<\/loc>/g) || []).map(function (loc) { + return new URL(loc.replace(/<\/?loc>/g, '')).pathname; + }); + return Promise.all(paths.map(function (path) { + return fetchText(path).then(function (html) { return hubDocs(path, html); }, function () { return []; }); + })); + }).then(function (pages) { return [].concat.apply([], pages); }); + } + + function loaded(label, promise) { + return promise.then(function (found) { docs = docs.concat(found); }, function () { failed.push(label); }) + .then(function () { done++; showStatus(); run(); }); + } + + function showStatus() { + var text = done < total ? 'Loading indexes: ' + done + ' of ' + total + : 'Searching ' + docs.length + ' entries from ' + (total - failed.length) + ' of ' + total + ' sites'; + status.textContent = failed.length ? text + '. Could not load: ' + failed.join(', ') + '.' : text + '.'; + } + + function run() { + state.q = input.value; + var next = new URLSearchParams(); + if (state.q) next.set('q', state.q); + if (state.site !== 'all') next.set('site', state.site); + history.replaceState(null, '', location.pathname + (next.toString() ? '?' + next : '')); + chips.forEach(function (chip) { chip.setAttribute('aria-pressed', String(chip.getAttribute('data-site') === state.site)); }); + var terms = tokenize(state.q); + var hits = search(docs, state.q, state.site); + list.replaceChildren.apply(list, hits.map(function (hit) { return renderResult(document, hit, terms); })); + count.textContent = !terms.length ? '' : (hits.length === LIMIT ? 'First ' + LIMIT : hits.length) + (hits.length === 1 ? ' result' : ' results') + + (done < total ? ' so far' : ''); + empty.hidden = !(terms.length && !hits.length && done === total); + } + + function links() { return Array.prototype.slice.call(list.querySelectorAll('a.search-title')); } + + input.value = state.q; + input.addEventListener('input', function () { clearTimeout(timer); timer = setTimeout(run, 120); }); + input.addEventListener('keydown', function (event) { + if (event.key === 'ArrowDown' && links().length) { event.preventDefault(); links()[0].focus(); } + }); + list.addEventListener('keydown', function (event) { + var all = links(), at = all.indexOf(document.activeElement); + if (at < 0 || (event.key !== 'ArrowDown' && event.key !== 'ArrowUp')) return; + event.preventDefault(); + if (event.key === 'ArrowUp' && at === 0) input.focus(); + else (all[at + (event.key === 'ArrowDown' ? 1 : -1)] || all[at]).focus(); + }); + root.querySelector('form').addEventListener('submit', function (event) { event.preventDefault(); clearTimeout(timer); run(); }); + chips.forEach(function (chip) { + chip.addEventListener('click', function () { state.site = chip.getAttribute('data-site'); run(); }); + }); + + root.hidden = false; + showStatus(); + run(); + SOURCES.forEach(function (source) { + loaded(source.label, cached('agt-search:' + source.id, function () { + return fetchText(source.base + 'search/search_index.json'); + }).then(function (text) { return fromMkDocs(source, JSON.parse(text)); })); + }); + loaded(HUB.label, loadHub()); +})(); diff --git a/search/search.test.js b/search/search.test.js new file mode 100644 index 0000000..980bae0 --- /dev/null +++ b/search/search.test.js @@ -0,0 +1,113 @@ +'use strict'; + +// Covers the parts of search.js that decide what a reader sees: text cleaning, +// tokenizing, the all-terms rule, ranking, the per-page cap, absolute URLs per +// host, and that source text can never become markup. + +const assert = require('node:assert/strict'); +const s = require('./search.js'); + +// HTML stripping: tags, scripts and entities. +assert.equal(s.stripHtml('

    Key release & attest's quote

    '), "Key release & attest's quote"); +assert.equal(s.stripHtml('ab'), 'a b'); +assert.equal(s.stripHtml('😀 &bogus; �'), '\u{1F600} &bogus;'); + +// Tokenizing: lower case, deduplicated, punctuation dropped, dotted names kept. +assert.deepEqual(s.tokenize(' TDX quote, tdx! search_index.json '), ['tdx', 'quote', 'search_index.json']); +assert.deepEqual(s.tokenize(' quote'); +assert.equal(url.textContent, 'https://wcm.agentrust-io.com/x/'); + +console.log('Search cleaning, ranking, per-page cap, URLs and escaping passed.'); diff --git a/supernav.js b/supernav.js index a73a19b..be70a79 100644 --- a/supernav.js +++ b/supernav.js @@ -17,6 +17,7 @@ { id: 'ca2a', label: 'cA2A', url: 'https://ca2a.agentrust-io.com', ext: false }, { id: 'trace', label: 'TRACE', url: 'https://trace.agentrust-io.com', ext: false }, { id: 'governance', label: 'Governance', url: 'https://governance.agentrust-io.com', ext: false }, + { id: 'search', label: 'Search', url: 'https://agentrust-io.com/search/', ext: false }, { id: 'github', label: 'GitHub', url: 'https://github.com/agentrust-io', ext: true } ]; @@ -30,6 +31,7 @@ // moved to wcm.agentrust-io.com. The conformance suite belongs under TRACE. var APEX = HOST === 'agentrust-io.com'; var CURRENT_ID = (APEX && PATH.indexOf('/verify') === 0) ? 'verify' + : (APEX && PATH.indexOf('/search') === 0) ? 'search' : (APEX && PATH.indexOf('/wcm') === 0) ? 'wcm' : (APEX && PATH.indexOf('/extensions/ca2a') === 0) ? 'ca2a' : APEX ? 'home' @@ -42,6 +44,13 @@ : HOST.indexOf('governance.') === 0 ? 'governance' : 'home'; + // Search opens on agentrust-io.com. From a docs site it starts filtered to that + // site; tests. has its own filter even though the bar highlights TRACE there. + var SEARCH_SITE = APEX ? '' : HOST.split('.')[0]; + if (['wcm', 'manifest', 'cmcp', 'ca2a', 'trace', 'tests', 'governance'].indexOf(SEARCH_SITE) < 0) { + SEARCH_SITE = ''; + } + var NAV_ID = 'agt-supernav'; var STYLE_ID = 'agt-supernav-style'; @@ -107,9 +116,10 @@ html += ''; } var active = s.id === CURRENT_ID ? ' class="active"' : ''; + var url = s.id === 'search' && SEARCH_SITE ? s.url + '?site=' + SEARCH_SITE : s.url; var target = s.ext ? ' target="_blank" rel="noopener noreferrer"' : ''; var extIcon = s.ext ? '↗' : ''; - html += '' + html += '' + s.label + extIcon + ''; @@ -135,6 +145,20 @@ document.body.insertBefore(nav, document.body.firstChild); } + // "/" opens search on agentrust-io.com pages. The MkDocs sites already bind "/" + // and "s" to their own search box, so the shortcut is not added there. + if (APEX) { + document.addEventListener('keydown', function (event) { + var el = event.target; + if (event.key !== '/' || event.ctrlKey || event.metaKey || event.altKey) return; + if (el && (el.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName))) return; + var box = document.getElementById('search-q'); + event.preventDefault(); + if (box) box.focus(); + else location.href = '/search/'; + }); + } + // Initial injection if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', injectNav); diff --git a/telemetry/index.html b/telemetry/index.html index 0ae83c5..3cd8440 100644 --- a/telemetry/index.html +++ b/telemetry/index.html @@ -47,7 +47,7 @@ } - + @@ -329,7 +329,7 @@

    The documents to implement against

    - + diff --git a/tools/build-header.py b/tools/build-header.py index afe25c2..e29afeb 100644 --- a/tools/build-header.py +++ b/tools/build-header.py @@ -34,6 +34,7 @@ 'registry/index.html': ('/registry/', None), 'go/index.html': (None, 'Specs'), 'extensions/ca2a/v0.1/index.html': (None, 'Specs'), + 'search/index.html': ('/search/', None), '404.html': (None, None), } diff --git a/tools/check-availability.py b/tools/check-availability.py index 4720bda..615b5a0 100644 --- a/tools/check-availability.py +++ b/tools/check-availability.py @@ -24,7 +24,12 @@ def asset_versions(host): """Report which shared asset URLs a docs host's home page references, and whether they are current.""" url = f'https://{host}agentrust-io.com/' try: - request = Request(url, headers={'User-Agent': 'AgenTrust-availability-check/1.0'}) + # The CDN can answer the bare URL from a copy cached before the latest deploy: + # on 2026-09-14 trace.agentrust-io.com/ returned HTML from 21:01 (Age 426) with + # unversioned URLs while ?v=