Publish the ACS site and schemas from this repository - #45
Merged
Merged
Conversation
Records the design for publishing three artifacts from this repository to GitHub Pages on every merge to main: a landing page matching the visual design of agentcontrolstandard.org, the existing MkDocs specification site, and the JSON schemas. The schema piece closes a known gap. All 44 schemas declare an $id under the project-controlled GitHub Pages base, but Pages has never been enabled, so every one of those URIs returns 404. Publish paths derive from each schema's own $id rather than from hardcoded directory names, and the build asserts that every $id and $ref in the package resolves to a published file. A future spec version directory publishes with no workflow edit. Drops the Google Analytics block from the site config. Verified against the real config that Material emits a googletagmanager script tag with an empty ID when no key is set, including when the variable is unset entirely, which leaks referrer and client IP for no benefit.
Six tasks, each ending in an independently testable deliverable: the schema publisher, the landing page, build-time content injection, the analytics removal, the deploy workflow, and Pages enablement. Both pieces of custom logic in the plan were run against real repository data before the plan was written down. The publisher places all 44 schemas at their declared paths, including relocating acs_schema.json out of ACS/ into v0.1.0/, and passes ref closure. The governance parser reads five workstreams and skips the project lead table above it.
Six independent reviewers attacked the plan and returned 36 findings at Plausible or above, four of them Critical. Most were reproduced by execution rather than argued, and the fixes below were verified the same way against the real 44-schema tree. The four Critical findings: $id was an unvalidated filesystem write primitive. A tail of ../index.html overwrote the rendered landing page inside the shipping artifact, and an absolute tail escaped the output tree entirely, because pathlib discards the left operand when the right is absolute. It is now pattern-validated, percent-decoded, and containment-checked. GOVERNANCE.md could inject HTML into the published page. Escaping with quote=False left a double quote intact while interpolating the captured URL into an href attribute. Now escaped with quote=True behind a scheme allowlist, asserted by parsing the output rather than grepping it. The test suite never ran in CI. The deploy installs with --no-dev, which excludes the group pytest lives in, so every guard expired the moment a developer stopped running it by hand. A test job now gates the build. The enablement runbook could not pass its own second step, because configure-pages fails before Pages exists. It is skipped on pull requests, and enablement moves ahead of the first deploy. Also closed: fragment refs were verified to the file and never the pointer, so renaming a $defs target shipped a broken package green; the hero diagram was a hand-applied paste invisible to the placeholder check, which shipped a blank hero with all tests passing; the smoke test read a status code with the body discarded and its retry flags were inert against a 404; a second spec version failed the deploy the design advertised it would survive; duplicate $id collapsed two schemas silently; five inherited colour tokens failed measured WCAG contrast, the focus ring at 1.48:1 against a 3:1 requirement; and the page loaded fonts from the same vendor whose analytics tag was removed for leaking referrer and client IP. Policy catches up with hosting: SECURITY.md put the site out of scope and named the wrong domain, LICENSING.md covered none of the new directories, and CODEOWNERS gated the workflow while leaving the code it runs under the widest rule in the file.
The aggregate of 63 was right while both components were wrong, so the per-task numbers were recomputed from the plan text rather than trusted. Task 1 defines 29 tests once the parametrized cases are counted, and Task 3 defines 30 across its two files.
Derives each destination from the schema's own $id rather than from hardcoded directory names, then asserts that every $ref inside the namespace resolves, fragment included. Treats $id as untrusted input. It is a pull-request-writable string used to build a filesystem path, and a fork's copy reaches this code on the runner before review. An $id tail of ../index.html overwrote the rendered landing page in the artifact, and an absolute tail wrote outside the output tree entirely, because pathlib discards the left operand when the right is absolute. Both are now rejected by pattern and by a resolved-path containment check. Also rejects a duplicate $id, which previously collapsed two schemas into one published file with no error, and a draft under proposals/ claiming the normative namespace. Verifying the fragment as well as the file catches a renamed $defs target, which the package depends on for its signature definition.
Tokens mirror agentcontrolstandard.org, except five that failed a measured contrast check and are annotated with their computed ratios. The focus ring was a translucent overlay compositing to 1.48:1 against the page where WCAG SC 1.4.11 requires 3:1, and the hexagon stroke at the centre of the diagram measured 1.47:1 against its own fill. The hero is a machine-filled placeholder rather than a hand-applied paste, so the renderer's placeholder check covers it. Inter ships with the site so the page contacts no third party, which is the same reasoning that removed analytics. The contact section separates general questions from vulnerability reporting and Code of Conduct reports, so publishing an address does not route those away from the channels that handle them independently.
tests/test_landing_page.py appeared in both Task 2's and Task 3's file lists. It belongs to Task 3: the guards assert properties of the rendered page, which does not exist until the renderer does.
The shipped comments carried three semicolons in prose and three British spellings, both of which the project's writing constraints forbid. The wording came from the plan, so the plan is corrected in the same change.
The landing page review found three prose semicolons and three British spellings in comments the plan supplied verbatim. Sweeping the whole document for that class caught six more instances of licence in Task 6, including a step heading. The constraint applies to the shipped artifact, so the plan is corrected to match what Task 2 now ships rather than the reverse.
Reads the version from the schema $id namespace and the roster from GOVERNANCE.md, so neither can drift. The hero diagram is injected the same way, because a hand-applied paste was invisible to the placeholder check and shipped a blank hero with a green suite. Escapes GOVERNANCE.md content with quote=True and rebuilds links only from an allowlisted scheme. The previous quote=False left a double quote intact while interpolating the URL into an href attribute, so a roster row could add an event handler or a javascript: target to the published page. The parser now tolerates heading case and spacing, keeps escaped pipes, stops at any heading level, and fails loudly on a wrong-width row rather than dropping a workstream in silence. Page guards now run against render() output. Running them against the template left the injected sections, the only part no human reviews, outside every check.
Three defects survived the earlier sweep because they were joined to other tokens: hex_colour inside an identifier, licence inside a test name, and actually inside a docstring. The word-boundary pattern used for the sweep could not see any of them. The implementer corrected all three while transcribing and reported doing so. The plan now matches what ships.
Material emits a googletagmanager script tag whenever the analytics block exists, including when GOOGLE_ANALYTICS_KEY is unset entirely, and separately fetches Roboto from fonts.googleapis.com on every page. Publishing as configured would have sent two third-party requests carrying the referrer and client IP of every documentation reader, one of them for no analytics data at all. No environment value suppresses either, so the analytics block is gone and theme fonts are off. The guard now asserts that no page fetches an unexpected origin, rather than grepping for one vendor string.
The previous pattern matched every href, so ordinary prose links in the documentation counted as asset fetches and had to be allowlisted one by one. That widened the allowlist to include Google-hosted domains, which would have let a real third-party script through the guard that exists to stop one. Matching only loading constructs makes the correct result an empty set, so the guard needs no maintenance as the prose adds citations.
The test matched every href, so prose citations in the documentation counted as asset fetches and had to be allowlisted individually. That widened the list to include Google-hosted domains, which would have let a real third-party script pass the guard that exists to stop one. Matching only loading constructs makes an empty set the correct result, so the guard needs no maintenance as the prose adds citations.
Task 4 supplies the guard. Task 5's test job, which build depends on, is what runs it on every pull request. The header claimed enforcement Task 4 has no means to provide. pyproject.toml was also listed as a Task 4 modification, though Task 1 had already added the dev dependency group.
Adds a test job so the guards run on every pull request. Without it the suite only ran when a developer remembered to, and the deploy job installs with --no-dev, which excludes the group pytest lives in. The deploy job now also checks the ref. workflow_dispatch can target any branch, and gating on event name alone let a feature branch publish to the production site. Concurrency groups every deploy together so they serialise on the one Pages site, while pull request builds group per ref and cancel stale runs. Verification fetches and parses each document and asserts it serves its own $id. The previous check read a status code with the body discarded, so a file overwritten with different content still passed, and its retry flags were inert: curl does not retry a 404 without --fail, which is exactly how CDN propagation lag presents. configure-pages is skipped on pull requests. It fails when Pages is not enabled and a fork's token cannot read the Pages API. Artifact retention goes to 30 days so re-running the last good deploy stays available as a rollback.
The deploy job runs actions/checkout so the verify step can reach tools/verify_published.py, but its permissions block listed only pages and id-token. A job that declares its own block gets none for every scope it omits, so the checkout had no read grant. The other three jobs already restated contents: read for the same reason. Only deploy was missing it.
The job runs actions/checkout so the verify step can reach tools/verify_published.py, but its permissions block listed only pages and id-token. A job that declares its own block gets none for every scope it omits, so the checkout had no read grant and the deploy would have failed before publishing anything. The workflow keeps permissions denied by default at the top level, so each job restates what it needs.
SECURITY.md put the documentation site out of scope and named the wrong domain. Once this workflow ships the project operates a site, so a researcher reading the policy would have been told not to report a finding against it. The contact-channels paragraph is replaced whole. Replacing only its first sentence would have left a later sentence forbidding the exception the first half grants, and dropping the paragraph would have deleted the RFC 2606 rule that licenses eleven example addresses in docs/. LICENSING.md covered none of landing/, tools/, tests/, or design/, so the footer named licenses for files the scope map did not reach. It also now records where the reused design tokens and diagram came from. CODEOWNERS gated the workflow file and left the code that workflow runs under the widest rule in the file. tools/, landing/, and GOVERNANCE.md now carry the same admin review as the CI surface, because all three decide what the public front door says.
CLAUDE.md records that the schema base 404s because Pages is not enabled. That is accurate until Pages is turned on and false the moment after, so Task 7 now replaces it once the URIs are confirmed resolving rather than before.
The three rows the plan added carried two cells against a three-column table, so they lacked the machine-readable identifier the table exists to provide. The hosting paragraph also described the site as already published, which contradicted the schema namespace section a few paragraphs later saying the base is not served because Pages is not enabled. The namespace sentence is the accurate one today, so the hosting text now waits, and Task 7 Step 10 updates both together once the URIs resolve.
The three rows added for landing, tools, tests, design, and the vendored font carried two cells against a three-column table, so they lacked the machine-readable identifier the table exists to provide. The hosting section also described the site as already published. Pages is not enabled yet, which the schema namespace section states correctly a few paragraphs later, so the two contradicted each other.
The built-with section listed seven names in a sentence. Each now carries what ACS does with it, which says more than the logo wall the source site used and avoids vendoring third-party marks. The theme toggle rendered as a raw browser button beside pill-shaped calls to action. Body text is also capped at a readable measure, which was why the middle sections looked thin on a wide screen. The page's origin guard matched every href, so the citations this adds would have failed it. Scoping it to resource loads matches the sibling guard in test_site_config.py, which had the same defect fixed earlier.
The two surfaces read as different websites: different typeface, different palette, different mark. That was fine when the specification link pointed at a separate property. Serving both from one origin makes the break look like a mistake. Material's own variables carry the landing page's tokens, so the docs inherit the type, color, and shape without giving up Material's navigation and search. The vendored Inter is reused rather than refetched, so nothing loads from a third party. Fixes a bug found while investigating. MkDocs built into _site/docs/ while site_url named the site root, so all 37 pages declared a canonical URL missing the /docs/ segment and the sitemap listed 36 URLs that all returned 404.
Ships the SIL Open Font License text and the Inter copyright notice with the font the landing page vendors, which OFL-1.1 requires of anyone redistributing it. NOTICE and the licensing scope map name it. Points the README at the site this repository publishes. It still sent readers to the .ai domain that SECURITY.md dropped in the same branch, so the new front door had no inbound link from the repository. Extends the restricted owner list to the build inputs that carry the same publish privilege as the tools themselves. mkdocs.yml accepts a hooks key that executes Python inside the build job. Records in CONTRIBUTING.md that Pages must be enabled before these workflows land, because the deploy and the monitor both fail until it is, and nothing in the repository said so. Also stops the sidebar clipping its own theme toggle on a short viewport, drops a class the stylesheet never defined, stops publishing a second copy of the inlined diagram, and makes the recorded font checksum load-bearing rather than decorative.
… load Material's bundled JavaScript fetches release and star counts from api.github.com whenever the source partial carries data-md-component="source". That sends every reader's IP address and referrer to a third party on every page load, the same property this branch already closed twice, once for Google Analytics and once for the Roboto font fetch. The existing guards scan src, href, and @import in markup, so a runtime fetch was invisible to them. Overriding the partial under overrides/, outside docs_dir so the override itself is never published, keeps the repository link and drops the attribute that starts the fetch. The new tests assert the trigger is absent from the built pages and that the link survives the override.
overrides/partials/source.html renders into every documentation page, so a change there reaches the published site as directly as a change to the tools that build it. Adding the directory closed the gap that adding the directory opened. Also drops two em dashes from CONTRIBUTING.md, which the repository's writing rules exclude.
The no-third-party guards read only HTML, so a url() in the stylesheet every documentation page loads would have shipped with the suite green. Demonstrated before fixing. Both guards now read stylesheets too and share one definition, because two copies of the same regex is how one of them stops matching without anyone noticing. The directories holding that stylesheet and the theme assets now carry the same restricted ownership as the theme templates, for the same reason: all three render into every page, and a stylesheet reaches a third party with no script at all. Records that the source partial derives from Material for MkDocs under the MIT License and notes that the mark exists at two paths under one license. The overrides scope row for LICENSING.md is held pending a separate licensing question and is not part of this commit. Also fixes the theme toggle needing two clicks when the system preference is dark, guards the duplicated mark and the diagram exclusion against drift, and stops CLAUDE.md telling a future session the schema namespace cannot be resolved.
The tree exists only to render the documentation site, so it belongs with the documentation in the license scope map rather than with the code. The source partial is now written for this project rather than copied from the theme. It keeps the class names the theme's stylesheet supplies and drops the icon block, so the header shows the repository name and links to it without the component hook that fetched release counts from a third party on every page load. The staleness test compared the file against the theme's own partial, which only made sense while it was a copy. It now asserts the theme still defines the classes the override depends on, so an upstream rename fails the suite instead of silently unstyling the header.
Uses the GitHub mark from the Simple Icons set the theme bundles, which is CC0 and so adds no attribution obligation to the pages that inline it. The theme's default mark comes from a set with heavier terms, and the icon is redistributed in every published page. The component hook that fetched release counts from a third party stays absent, which is the reason this override exists.
The icon disappeared once when the partial was rewritten, and a person had to notice. Asserting the mark is inlined makes that a failing build rather than something to spot.
The landing page had a pill reading "Switch theme" and the documentation had a switch glyph cycling three states. Neither used the marks readers already know, and the two behaved differently on one site. Both are now two-state controls showing the theme a click would give rather than the one already showing. The documentation drops its named Material palettes at the same time, because the ACS tokens in the docs stylesheet already supersede them. The landing page's marks are plain geometry written for this page, so the control adds no third-party license.
The theme sets --md-typeset-a-color from its primary colour, and the built pages carry a primary attribute whether or not the palette names one. That tied with the scheme rules at equal specificity, so documentation links rendered the theme's indigo in dark mode instead of the project's blue. Matching on both attributes settles it without touching the other tokens.
The landing sidebar drew the project mark as styled text while the documentation header drew the same file as an image. Both now show the image. The specification moved out of the external resources block. It is served from this site, so it sits with the sections and leaves that heading to GitHub and Slack, which are genuinely elsewhere. Records that OWASP holds the rights to the reused design, which the licensing notes had carried as an open question.
rocklambros
requested review from
GangGreenTemperTatum,
afogel,
almogbhl,
bar-capsule,
fewdisc,
mamicidal,
sclintonowasp and
stefanoamorelli
as code owners
September 5, 2026 23:15
rocklambros
added a commit
that referenced
this pull request
Sep 5, 2026
Follow-up to #45, deliberately deferred until the URIs were confirmed resolving. The hosting notes in `CLAUDE.md` were written conditionally while Pages was disabled, so they described what would happen rather than what does. Writing them earlier would have traded one false statement for another. Also records the rollback procedure the design carried as an open item, and the live status: first deploy succeeded, all 44 schema URIs confirmed serving their own `$id`, environment restricted to protected branches.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Enables GitHub Pages and publishes three things on every merge to
main: a landing page matching the agentcontrolstandard.org design, the existing MkDocs specification site, and all 44 JSON schemas at the URIs their$idvalues declare.Those
$idURIs have always returned 404 because Pages was never enabled. Closing that is the point of this change.What ships
//docs//schema/v0.1.0/$iddeclaresSchema publish paths derive from each schema's own
$id, validated and contained, and the build fails if any$idor$refdoes not resolve, fragment included.Review history
The plan went through a six-perspective adversarial premortem, per-task reviews with fix loops, a whole-branch review, and a re-review. Findings closed include:
$idwas an unvalidated filesystem write primitive. A tail of../index.htmloverwrote build output and an absolute tail escaped the output tree entirely.GOVERNANCE.mdcould inject HTML into the published page through an unescaped quote in anhrefattribute./docs/segment, and all 36 sitemap entries pointed at addresses that 404.api.github.comon every page load, sending each reader's IP to a third party.url()in the stylesheet every page loads would have shipped with the suite green. Demonstrated, then fixed, then demonstrated failing.83 tests, all passing.
Ordering note
CONTRIBUTING.mdrecords that Pages must be enabled before this merges. Until it is, the deploy fails at Configure Pages and the scheduled monitor fails on its cadence.Design:
design/2026-09-05-github-pages-landing.mdPlan:
design/plans/2026-09-05-github-pages-site.md