From 845ec774d5e8f22ff6f213409c0eadf548034b6b Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Sun, 4 Oct 2026 15:41:09 +0800 Subject: [PATCH 01/18] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F(docs):=20close=20an?= =?UTF-8?q?alytics=20URL=20and=20referrer=20leakage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/scripts/test_analytics_privacy.py | 24 ++++++++++++++++++++++++ docs/theme/aa-funnel-events.js | 21 ++++++++++++++++----- docs/theme/head.hbs | 12 +++++++++--- 3 files changed, 49 insertions(+), 8 deletions(-) create mode 100644 docs/scripts/test_analytics_privacy.py diff --git a/docs/scripts/test_analytics_privacy.py b/docs/scripts/test_analytics_privacy.py new file mode 100644 index 0000000..d9e770a --- /dev/null +++ b/docs/scripts/test_analytics_privacy.py @@ -0,0 +1,24 @@ +"""Regression checks for the public docs analytics privacy boundary.""" +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +FUNNEL = (ROOT / "theme" / "aa-funnel-events.js").read_text(encoding="utf-8") +HEAD = (ROOT / "theme" / "head.hbs").read_text(encoding="utf-8") + + +def test_ga_config_denies_automatic_page_view_and_referrer() -> None: + assert "'send_page_view': false" in HEAD + assert "'page_referrer': ''" in HEAD + + +def test_event_payload_uses_closed_page_and_domain_values() -> None: + assert "page_id: pageId()" in FUNNEL + assert "link_url" not in FUNNEL + assert "location.hostname" not in FUNNEL + assert "document.title" not in FUNNEL + + +def test_feedback_does_not_capture_visitor_url() -> None: + assert "var body = 'Page: ' + location.href" not in HEAD + assert "page_id': 'docs'" in HEAD diff --git a/docs/theme/aa-funnel-events.js b/docs/theme/aa-funnel-events.js index 715f37e..54736fc 100644 --- a/docs/theme/aa-funnel-events.js +++ b/docs/theme/aa-funnel-events.js @@ -17,11 +17,21 @@ const GITHUB_EXAMPLES = 'github.com/ai-agent-assembly/examples'; const GITHUB_ISSUE_ANY = /^https?:\/\/github\.com\/ai-agent-assembly\/[^/]+\/issues/; + // Closed vocabulary: never send raw URL, title, hostname, query, fragment, + // or referrer values, which may contain pasted prompts or repository names. + function pageId() { + const path = location.pathname; + if (/quickstart-saas|installation/.test(path)) { return 'installation'; } + if (/security-model/.test(path)) { return 'security_model'; } + if (/python-sdk/.test(path)) { return 'python_sdk'; } + if (/node-sdk/.test(path)) { return 'node_sdk'; } + if (/go-sdk/.test(path)) { return 'go_sdk'; } + return 'docs'; + } + function baseParams() { return { - hostname: location.hostname, - page_path: location.pathname, - page_title: document.title, + page_id: pageId(), surface: SURFACE }; } @@ -106,8 +116,9 @@ const params = { cta_location: ctaLocationFor(a), - link_url: a.href, - link_domain: a.hostname || '', + link_domain: /github\.com/.test(a.href) ? 'github' + : /agent-assembly\.com/.test(a.href) ? 'agent_assembly' + : /horonomy\.dev/.test(a.href) ? 'horonomy' : 'docs', target_product: targetProductFor(a.href) }; diff --git a/docs/theme/head.hbs b/docs/theme/head.hbs index 3e7185b..3292deb 100644 --- a/docs/theme/head.hbs +++ b/docs/theme/head.hbs @@ -19,7 +19,13 @@ } } catch (e) {} gtag('js', new Date()); - gtag('config', 'G-EZTLJDFS6J', { 'anonymize_ip': true }); + // Suppress GA's automatic page_view and clear referrer attribution. Events + // use only the closed vocabulary in aa-funnel-events.js. + gtag('config', 'G-EZTLJDFS6J', { + 'anonymize_ip': true, + 'page_referrer': '', + 'send_page_view': false + }); @@ -107,12 +113,12 @@ function sendFeedback(value) { // Consent Mode (AAASM-3554) governs whether this hit is stored; fire either way. if (typeof gtag === 'function') { - gtag('event', 'feedback', { 'value': value, 'page_path': location.pathname }); + gtag('event', 'feedback', { 'value': value, 'page_id': 'docs' }); } } function issueUrl() { var title = 'Docs feedback: ' + location.pathname; - var body = 'Page: ' + location.href + '\n\nWhat could be improved?\n'; + var body = 'Page: documentation\n\nWhat could be improved?\n'; return 'https://github.com/ai-agent-assembly/' + REPO + '/issues/new?labels=docs,feedback' + '&title=' + encodeURIComponent(title) + '&body=' + encodeURIComponent(body); } From 3568c788faa230da3bce26cb56ed794042960b46 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Sun, 4 Oct 2026 15:44:54 +0800 Subject: [PATCH 02/18] =?UTF-8?q?=F0=9F=94=92=20fix:=20pin=20docs=20analyt?= =?UTF-8?q?ics=20page=20context?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/scripts/test_analytics_privacy.mjs | 23 +++++++++++++++++++++++ docs/scripts/test_analytics_privacy.py | 24 ------------------------ docs/theme/head.hbs | 2 ++ 3 files changed, 25 insertions(+), 24 deletions(-) create mode 100644 docs/scripts/test_analytics_privacy.mjs delete mode 100644 docs/scripts/test_analytics_privacy.py diff --git a/docs/scripts/test_analytics_privacy.mjs b/docs/scripts/test_analytics_privacy.mjs new file mode 100644 index 0000000..5f3c675 --- /dev/null +++ b/docs/scripts/test_analytics_privacy.mjs @@ -0,0 +1,23 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import vm from 'node:vm'; +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const head = fs.readFileSync(path.join(root, 'theme/head.hbs'), 'utf8'); +const funnel = fs.readFileSync(path.join(root, 'theme/aa-funnel-events.js'), 'utf8'); +const scripts = [...head.matchAll(/ From 7e2bbdc3651209fb65bd69fc39fae263d109c15d Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Sun, 4 Oct 2026 15:45:01 +0800 Subject: [PATCH 03/18] =?UTF-8?q?=E2=9C=85=20test:=20run=20hosted=20analyt?= =?UTF-8?q?ics=20privacy=20VM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/hub-metadata-check.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/hub-metadata-check.yml b/.github/workflows/hub-metadata-check.yml index 6edb9b0..8e938cf 100644 --- a/.github/workflows/hub-metadata-check.yml +++ b/.github/workflows/hub-metadata-check.yml @@ -118,6 +118,9 @@ jobs: - name: Validate every page's capability_ids against the capability manifest run: python3 docs/scripts/validate_capability_ids.py + - name: Run hosted analytics privacy VM checks + run: node docs/scripts/test_analytics_privacy.mjs + - name: Run the claim-vocabulary self-test run: python3 docs/scripts/check_claim_vocabulary.py --selftest From 7a395f5f6fe3ebc2bbd882c29ef0621c340a716c Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Sun, 4 Oct 2026 15:46:11 +0800 Subject: [PATCH 04/18] =?UTF-8?q?=F0=9F=A7=AA=20test:=20execute=20hub=20an?= =?UTF-8?q?alytics=20bootstrap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/scripts/test_analytics_privacy.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/scripts/test_analytics_privacy.mjs b/docs/scripts/test_analytics_privacy.mjs index 5f3c675..394cc80 100644 --- a/docs/scripts/test_analytics_privacy.mjs +++ b/docs/scripts/test_analytics_privacy.mjs @@ -6,7 +6,7 @@ import path from 'node:path'; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const head = fs.readFileSync(path.join(root, 'theme/head.hbs'), 'utf8'); const funnel = fs.readFileSync(path.join(root, 'theme/aa-funnel-events.js'), 'utf8'); -const scripts = [...head.matchAll(/ From d95b27325ed18d70f2a53c08c26fe63adc5a9003 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Sun, 4 Oct 2026 15:57:09 +0800 Subject: [PATCH 11/18] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20(docs):=20keep=20?= =?UTF-8?q?feedback=20titles=20free=20of=20visitor=20paths?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/theme/head.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/theme/head.hbs b/docs/theme/head.hbs index f8a3e82..39ca16f 100644 --- a/docs/theme/head.hbs +++ b/docs/theme/head.hbs @@ -130,7 +130,7 @@ } } function issueUrl() { - var title = 'Docs feedback: ' + location.pathname; + var title = 'Docs feedback'; var body = 'Page: documentation\n\nWhat could be improved?\n'; return 'https://github.com/ai-agent-assembly/' + REPO + '/issues/new?labels=docs,feedback' + '&title=' + encodeURIComponent(title) + '&body=' + encodeURIComponent(body); From d765b12f20da46400fc6e8f6493a7b6d46b1130e Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Sun, 4 Oct 2026 16:34:14 +0800 Subject: [PATCH 12/18] =?UTF-8?q?=F0=9F=94=92=20build:=20harden=20aggregat?= =?UTF-8?q?ed=20core=20analytics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/scripts/aggregate.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/scripts/aggregate.sh b/docs/scripts/aggregate.sh index 623219f..57d5ace 100755 --- a/docs/scripts/aggregate.sh +++ b/docs/scripts/aggregate.sh @@ -143,6 +143,12 @@ PY python3 docs/ci/build_versions.py latest latest "$out/versions.json" ) cp "$src/docs/site-root-index.html" "$out/index.html" + + # Core publishes historical version artifacts rebuilt from tags. Apply the + # Core-owned privacy hardener after every version and the root redirect are + # assembled, so older public snapshots receive the same fixed analytics + # identity without changing their product content. + node "$src/docs/scripts/harden_published_analytics.mjs" "$out" --public-prefix /core/ } build_python() { # mike-published version tree (gh-pages) -> public/python-sdk (AAASM-3752) From 2d13d28bf6b7c73786aa6c1aa4a0916479f4d44d Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Sun, 4 Oct 2026 16:34:35 +0800 Subject: [PATCH 13/18] =?UTF-8?q?=F0=9F=94=92=20build:=20pin=20reviewed=20?= =?UTF-8?q?core=20analytics=20hardener?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/scripts/aggregate.sh | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/scripts/aggregate.sh b/docs/scripts/aggregate.sh index 57d5ace..e3fe27a 100755 --- a/docs/scripts/aggregate.sh +++ b/docs/scripts/aggregate.sh @@ -148,7 +148,20 @@ PY # Core-owned privacy hardener after every version and the root redirect are # assembled, so older public snapshots receive the same fixed analytics # identity without changing their product content. - node "$src/docs/scripts/harden_published_analytics.mjs" "$out" --public-prefix /core/ + local hardener="$src/docs/scripts/harden_published_analytics.mjs" + if [[ ! -f "$hardener" ]]; then + # The hardener is supplied by the reviewed Core change until that change + # reaches the default branch; pin the exact reviewed source revision so + # aggregation cannot silently use an unreviewed copy. + local hardener_ref="892953782d0b89017750125e9e9e4b9955171891" + git -C "$src" fetch --quiet origin "$hardener_ref" + hardener="$(mktemp)" + git -C "$src" show "$hardener_ref:docs/scripts/harden_published_analytics.mjs" > "$hardener" + node "$hardener" "$out" --public-prefix /core/ + rm -f "$hardener" + else + node "$hardener" "$out" --public-prefix /core/ + fi } build_python() { # mike-published version tree (gh-pages) -> public/python-sdk (AAASM-3752) From 017f86319182272eaa7159ac8aeca5026713aa81 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Sun, 4 Oct 2026 16:39:02 +0800 Subject: [PATCH 14/18] =?UTF-8?q?=F0=9F=94=A7=20fix:=20preserve=20hardener?= =?UTF-8?q?=20module=20extension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/scripts/aggregate.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/scripts/aggregate.sh b/docs/scripts/aggregate.sh index e3fe27a..ac505ff 100755 --- a/docs/scripts/aggregate.sh +++ b/docs/scripts/aggregate.sh @@ -155,7 +155,7 @@ PY # aggregation cannot silently use an unreviewed copy. local hardener_ref="892953782d0b89017750125e9e9e4b9955171891" git -C "$src" fetch --quiet origin "$hardener_ref" - hardener="$(mktemp)" + hardener="$(mktemp "${TMPDIR:-/tmp}/aa-core-hardener.XXXXXX.mjs")" git -C "$src" show "$hardener_ref:docs/scripts/harden_published_analytics.mjs" > "$hardener" node "$hardener" "$out" --public-prefix /core/ rm -f "$hardener" From 18293b16c6e65bb22a545d04b43a716a064f13b1 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Sun, 4 Oct 2026 17:12:27 +0800 Subject: [PATCH 15/18] =?UTF-8?q?=F0=9F=94=92=20build:=20pin=20latest=20co?= =?UTF-8?q?re=20analytics=20hardener?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/scripts/aggregate.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/scripts/aggregate.sh b/docs/scripts/aggregate.sh index ac505ff..5547851 100755 --- a/docs/scripts/aggregate.sh +++ b/docs/scripts/aggregate.sh @@ -153,7 +153,7 @@ PY # The hardener is supplied by the reviewed Core change until that change # reaches the default branch; pin the exact reviewed source revision so # aggregation cannot silently use an unreviewed copy. - local hardener_ref="892953782d0b89017750125e9e9e4b9955171891" + local hardener_ref="cc27d8421d76bb27f0dd2a90a9fe680dd9ee9b23" git -C "$src" fetch --quiet origin "$hardener_ref" hardener="$(mktemp "${TMPDIR:-/tmp}/aa-core-hardener.XXXXXX.mjs")" git -C "$src" show "$hardener_ref:docs/scripts/harden_published_analytics.mjs" > "$hardener" From 205534c4555125a2e56dec3794c1333e77315460 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Sun, 4 Oct 2026 17:19:15 +0800 Subject: [PATCH 16/18] =?UTF-8?q?=F0=9F=94=92=20build:=20pin=20final=20cor?= =?UTF-8?q?e=20analytics=20hardener?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/scripts/aggregate.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/scripts/aggregate.sh b/docs/scripts/aggregate.sh index 5547851..fb37784 100755 --- a/docs/scripts/aggregate.sh +++ b/docs/scripts/aggregate.sh @@ -153,7 +153,7 @@ PY # The hardener is supplied by the reviewed Core change until that change # reaches the default branch; pin the exact reviewed source revision so # aggregation cannot silently use an unreviewed copy. - local hardener_ref="cc27d8421d76bb27f0dd2a90a9fe680dd9ee9b23" + local hardener_ref="ef2998b6675e59028b2048a13c70e628a118ed1c" git -C "$src" fetch --quiet origin "$hardener_ref" hardener="$(mktemp "${TMPDIR:-/tmp}/aa-core-hardener.XXXXXX.mjs")" git -C "$src" show "$hardener_ref:docs/scripts/harden_published_analytics.mjs" > "$hardener" From ae448fccdd8638d334e71501db130410894ae6b1 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Sun, 4 Oct 2026 17:26:07 +0800 Subject: [PATCH 17/18] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20build:=20require?= =?UTF-8?q?=20core=20analytics=20hardening?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/scripts/aggregate.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/scripts/aggregate.sh b/docs/scripts/aggregate.sh index fb37784..3d26c19 100755 --- a/docs/scripts/aggregate.sh +++ b/docs/scripts/aggregate.sh @@ -157,10 +157,12 @@ PY git -C "$src" fetch --quiet origin "$hardener_ref" hardener="$(mktemp "${TMPDIR:-/tmp}/aa-core-hardener.XXXXXX.mjs")" git -C "$src" show "$hardener_ref:docs/scripts/harden_published_analytics.mjs" > "$hardener" - node "$hardener" "$out" --public-prefix /core/ + hardened_count="$(node "$hardener" "$out" --public-prefix /core/ | awk '/^Hardened analytics identity in [0-9]+ rendered HTML file\(s\)\.$/ {print $5}')" + [[ "${hardened_count:-0}" -gt 0 ]] || fail "Core analytics hardener changed no rendered pages" rm -f "$hardener" else - node "$hardener" "$out" --public-prefix /core/ + hardened_count="$(node "$hardener" "$out" --public-prefix /core/ | awk '/^Hardened analytics identity in [0-9]+ rendered HTML file\(s\)\.$/ {print $5}')" + [[ "${hardened_count:-0}" -gt 0 ]] || fail "Core analytics hardener changed no rendered pages" fi } From e4f2397ed16c34a7f1e0bc1353b5a2f663b8caf7 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Sun, 4 Oct 2026 17:39:05 +0800 Subject: [PATCH 18/18] =?UTF-8?q?=F0=9F=94=92=20build:=20pin=20final=20cor?= =?UTF-8?q?e=20hardener=20safely?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/scripts/aggregate.sh | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/docs/scripts/aggregate.sh b/docs/scripts/aggregate.sh index 3d26c19..e8d21de 100755 --- a/docs/scripts/aggregate.sh +++ b/docs/scripts/aggregate.sh @@ -149,21 +149,26 @@ PY # assembled, so older public snapshots receive the same fixed analytics # identity without changing their product content. local hardener="$src/docs/scripts/harden_published_analytics.mjs" + local temporary_hardener="" if [[ ! -f "$hardener" ]]; then - # The hardener is supplied by the reviewed Core change until that change - # reaches the default branch; pin the exact reviewed source revision so - # aggregation cannot silently use an unreviewed copy. - local hardener_ref="ef2998b6675e59028b2048a13c70e628a118ed1c" + # Pin the reviewed Core source until its hardener reaches the default branch. + local hardener_ref="5f37f5f1e97c089b1199ee170bba807dbca4583a" git -C "$src" fetch --quiet origin "$hardener_ref" - hardener="$(mktemp "${TMPDIR:-/tmp}/aa-core-hardener.XXXXXX.mjs")" + temporary_hardener="$(mktemp "${TMPDIR:-/tmp}/aa-core-hardener.XXXXXX.mjs")" + hardener="$temporary_hardener" git -C "$src" show "$hardener_ref:docs/scripts/harden_published_analytics.mjs" > "$hardener" - hardened_count="$(node "$hardener" "$out" --public-prefix /core/ | awk '/^Hardened analytics identity in [0-9]+ rendered HTML file\(s\)\.$/ {print $5}')" - [[ "${hardened_count:-0}" -gt 0 ]] || fail "Core analytics hardener changed no rendered pages" - rm -f "$hardener" - else - hardened_count="$(node "$hardener" "$out" --public-prefix /core/ | awk '/^Hardened analytics identity in [0-9]+ rendered HTML file\(s\)\.$/ {print $5}')" - [[ "${hardened_count:-0}" -gt 0 ]] || fail "Core analytics hardener changed no rendered pages" fi + local hardener_output hardener_status + set +e + hardener_output="$(node "$hardener" "$out" --public-prefix /core/ 2>&1)" + hardener_status=$? + set -e + [[ -z "$temporary_hardener" ]] || rm -f "$temporary_hardener" + printf '%s\n' "$hardener_output" + [[ "$hardener_status" -eq 0 ]] || fail "Core analytics hardener failed" + local hardened_count + hardened_count="$(printf '%s\n' "$hardener_output" | awk '/^Hardened analytics identity in [0-9]+ rendered HTML file\(s\)\.$/ {print $5}')" + [[ "${hardened_count:-0}" -gt 0 ]] || fail "Core analytics hardener changed no rendered pages" } build_python() { # mike-published version tree (gh-pages) -> public/python-sdk (AAASM-3752)