From 1d34fb1317e4b636cc80e8b3f3d46d94348ef71c Mon Sep 17 00:00:00 2001 From: Boyd Cohen Date: Tue, 25 Aug 2026 19:56:17 -0600 Subject: [PATCH 1/4] Remove ArcadiaB social exports and internal docs from the public tree Authorised by op-at-specs/OPEN-CLOSED-RULE.md ledger, 2026-08-25: "ArcadiaB CSVs + internal docs in website repo | Remove from public tree" "Internal docs in public trees | Removed, except op-adapters FIDELITY-NOTES" Removed: arcadiab-facebook.csv, arcadiab-linkedin.csv, arcadiab-linkedin-week.csv (added 2dd421c, 2026-03-13); internal/stats/index.html (88305b9, 2026-05-12); REPO_MAP.md (fc66257); IMPLEMENTATION_GUIDE.md (6d5bfba); DEMO-SCRIPT-MAR18.md (56f6324); scripts/ATLAS-HANDOFF.md (d45a490). Serving is unchanged by this commit: every one of these paths already returned a forced 404 from netlify.toml. What changes is the repository, which is public, so the files were readable there regardless of what the domain served. RESIDUAL HISTORY: removal from HEAD does not remove these blobs from history. They remain reachable at every commit from their addition to this one, on this branch and on every other ref that carries those commits, and in any clone or fork already taken. No force-push and no history rewrite, per the rule's principle 4 and the standing retirement of force-push over pushed commits. --- DEMO-SCRIPT-MAR18.md | 145 ------------------ IMPLEMENTATION_GUIDE.md | 186 ----------------------- REPO_MAP.md | 98 ------------ arcadiab-facebook.csv | 4 - arcadiab-linkedin-week.csv | 4 - arcadiab-linkedin.csv | 5 - internal/stats/index.html | 303 ------------------------------------- scripts/ATLAS-HANDOFF.md | 146 ------------------ 8 files changed, 891 deletions(-) delete mode 100644 DEMO-SCRIPT-MAR18.md delete mode 100644 IMPLEMENTATION_GUIDE.md delete mode 100644 REPO_MAP.md delete mode 100644 arcadiab-facebook.csv delete mode 100644 arcadiab-linkedin-week.csv delete mode 100644 arcadiab-linkedin.csv delete mode 100644 internal/stats/index.html delete mode 100644 scripts/ATLAS-HANDOFF.md diff --git a/DEMO-SCRIPT-MAR18.md b/DEMO-SCRIPT-MAR18.md deleted file mode 100644 index afbf47c..0000000 --- a/DEMO-SCRIPT-MAR18.md +++ /dev/null @@ -1,145 +0,0 @@ -# Observer Protocol Demo Script -## Lightning Labs Community Call — March 18, 2026 -**Duration:** 5 minutes -**Presenter:** Boyd -**Demo URL:** https://observerprotocol.org/demo - ---- - -## 1. Problem (15 seconds) - -> "L402 proves an agent *paid*. But here's the problem: how do you trust the agent's claims about its track record?" -> -> "Any agent can say 'I've processed 10,000 payments.' But where's the proof? Reputation scores can be gamed. Reviews can be faked." -> -> "What if we could verify claims cryptographically — not with authority, but with math?" - -**[Transition: Screen share the demo page]** - ---- - -## 2. Solution (15 seconds) - -> "Observer Protocol. It's a cryptographic trust layer for agentic payments." -> -> "Don't trust claims. Verify them. Every entry in this registry is backed by a Lightning preimage or an EVM signature." -> -> "Not a reputation score. Not a review. Math." - -**[Gesture to the hero section with live counter]** - ---- - -## 3. Live Feed (30 seconds) - -> "This is the live verification stream. Every few seconds, it polls the registry for new events." -> -> "Each row shows: the protocol — Lightning or x402, the transaction reference, whether it's cryptographically verified, and when it happened." -> -> "Notice the amber pulse? That happens when a new verification lands. It's subtle, but it's the moment math becomes truth." - -**[Point to feed items, scroll if needed]** - ---- - -## 4. The Moment (2 minutes) - -> "Now here's where it gets interesting. I'm going to generate a Lightning invoice. Someone on this call — anyone — can pay it." -> -> "And when that payment settles, you'll see it appear here in real time." - -**[Generate invoice via your LND node, display QR on screen]** - -> "Go ahead — scan it with any Lightning wallet. It's just a few sats." - -**[Wait for payment — this is the demo moment]** - -> "There! Did you see it? The amber pulse — that's the payment being cryptographically verified." -> -> "SHA256 of the preimage equals the payment hash. No trust required. The math is the record." -> -> "This entry is now permanently in the registry. Timestamped. Immutable." - ---- - -## 5. Badge (30 seconds) - -> "Every verified agent gets one of these." -> -> "[Show the SVG badge] It's a cryptographically signed badge that any agent can display. Embed it in your GitHub README, your website, your agent's profile." -> -> "The badge URL is deterministic: badge/{agent-id}.svg. It updates in real time as the agent builds its verification history." -> -> "This is maxi-0001 — my AI agent. It's been running autonomously, making payments, building a track record that can't be faked." - ---- - -## 6. CTA (1 minute) - -> "Here's the ask: We're proposing an `op-verify` skill for Lightning Agent Tools." -> -> "60-second integration. Any L402-enabled agent can join. No gatekeepers. No approval process. Just cryptographic proof." -> -> "The registry is live now at observerprotocol.org. The API is public. The SDK is on npm." -> -> "We're not selling anything. This is free infrastructure. We monetize intelligence at Agentic Terminal, not the protocol." -> -> "If you're building agents that need to prove their track record — or if you're selecting agents and need to verify their claims — talk to us." -> -> "The code is open source. The protocol is permissionless. The math is the trust." - -**[End screen share]** - ---- - -## Backup Plan - -If the live payment fails or takes too long: -- Have a pre-made payment hash ready -- Say: "While we're waiting, here's one that landed earlier today..." -- Click into the feed item to show the verification details - ---- - -## Technical Checklist (Day Of) - -- [ ] Test https://observerprotocol.org/demo loads -- [ ] Verify API endpoints responding: - - [ ] https://api.observerprotocol.org/observer/trends - - [ ] https://api.observerprotocol.org/observer/feed - - [ ] https://api.observerprotocol.org/observer/agents/list -- [ ] Generate test invoice beforehand (have QR ready) -- [ ] Have backup invoice in case first one fails -- [ ] Test screen sharing in call platform -- [ ] Close unnecessary tabs/apps -- [ ] Have LND node ready to generate invoices - ---- - -## Key Talking Points to Hit - -1. **L402 proves payment. OP proves claims.** — The distinction matters -2. **No reputation scores** — This is cryptographic, not social -3. **Rail-neutral** — Lightning today, x402 live, more coming -4. **Permanent record** — Once verified, always verifiable -5. **Free infrastructure** — Not a product pitch, a protocol proposal - ---- - -## Q&A Anticipated - -**Q: How is this different from a block explorer?** -A: Block explorers show *that* a payment happened. Observer Protocol proves *who* made it and builds a verifiable history *across* rails. - -**Q: Can agents fake their identity?** -A: They can create new identities, but they can't fake history. Every verification costs real sats. That's Sybil resistance without a gatekeeper. - -**Q: What's the business model?** -A: We don't monetize the protocol. We monetize intelligence at Agentic Terminal. This is public infrastructure. - -**Q: Is this live now?** -A: Yes. The registry has been running since February 22. The genesis transaction was between two AI agents on mainnet. - ---- - -*Good luck. The math is on your side.* diff --git a/IMPLEMENTATION_GUIDE.md b/IMPLEMENTATION_GUIDE.md deleted file mode 100644 index ebdd923..0000000 --- a/IMPLEMENTATION_GUIDE.md +++ /dev/null @@ -1,186 +0,0 @@ -# Observer Protocol — Site Upgrade v2.0 -## Implementation Guide for Maxi - -**Prepared by Boyd via Claude · March 2026** - ---- - -## What's in this package - -| File | Status | Priority | -|------|--------|----------| -| `shared-styles.css` | New | Deploy first — all pages depend on it | -| `index.html` | Full rewrite | P1 | -| `agents.html` | Full rewrite | P1 — was nearly empty | -| `api.html` | Full rewrite | P1 — replaces broken white Swagger aesthetics | -| `architecture.html` | Existing — no change needed | — | -| `spec.html` | Existing — no change needed | — | -| `sdk.html` | Existing — no change needed | — | - ---- - -## Step 1 — Deploy shared-styles.css - -All three new pages import `shared-styles.css` via a relative path. This file must exist at the root of the site before any pages are deployed, or fonts and variables will be missing. - -```bash -# Copy to site root (adjust path to your actual static file directory) -cp shared-styles.css /path/to/observerprotocol.org/public/shared-styles.css -``` - -The CSS imports IBM Plex Mono and IBM Plex Sans from Google Fonts via CDN. No local font files needed. - ---- - -## Step 2 — Replace index.html - -Drop `index.html` into the site root. Key changes from current version: - -- Hero: proper h1 scale, `npm install @observerprotocol/sdk` copy-to-clipboard block -- Status bar: live network stats strip (hardcoded for now — see Step 4 for live data) -- Genesis block: styled verification card with preimage math -- "What it answers" section: 4-cell grid -- Rails grid: Lightning (live), x402 (live), Solana/Ark/Fedimint (coming) -- 3-step integration with real code snippet -- Principles section with manifesto pull quote -- Footer: consistent across all pages - ---- - -## Step 3 — Replace agents.html - -The old agents page was essentially empty (12 agents listed as one line of text). The new page: - -- Registry stats bar (12 agents, 9 txns, 2 rails, genesis date) -- Filter bar: All / VERIFIED / PENDING / REGISTERED / Lightning / x402 -- Card grid with real data for Maxi (#0001), Vicky (#0002), AgentPay (#0003) -- Cards #0004–#0012 use placeholder key hashes — **update these with real data from the PostgreSQL registry** -- Click any named agent card to open a detail modal -- Badge levels section at bottom -- Register CTA linking to SDK - -### Data to update in agents.html - -Search for `TODO` comments — there are none, but update these hardcoded values with live data: - -| Element | Current Value | Source | -|---------|--------------|--------| -| `id="total-agents"` | 12 | `GET /api/v1/stats` | -| `id="verified-count"` | 9 | `GET /api/v1/stats` | -| Agent #0004–#0012 key hashes | placeholder sha256 stubs | PostgreSQL `agents` table | -| Agent #0004–#0012 statuses | PENDING / REGISTERED | PostgreSQL `agents` table | - -**Optional enhancement:** Replace the hardcoded agent grid with a fetch call to `GET /observer/feed` on page load so the registry stays current automatically. The card template is ready — just needs a JS render loop. - ---- - -## Step 4 — Replace api.html - -The existing site used the default Swagger UI (white background, generic styling). The new `api.html` is a custom dark API reference that: - -- Matches the site aesthetic fully -- Links to the live Swagger UI at `api.observerprotocol.org/docs` for those who want interactive testing -- Documents all 13 endpoints with expandable cards -- Shows real request/response schemas from the Observer Protocol spec -- Includes "Try it ↗" links that hit live endpoints directly - -No backend changes required. The Swagger UI can remain at `/docs` as a secondary reference. - ---- - -## Step 5 — Live data in the status bar (index.html) - -The hero status bar currently shows hardcoded values. To make it live, add this script before `` in `index.html`: - -```javascript -async function refreshStatus() { - try { - const r = await fetch('https://api.observerprotocol.org/api/v1/stats'); - const d = await r.json(); - // Update DOM elements as needed based on actual response shape - // e.g. document.querySelector('.status-bar .registry-entries').textContent = d.registry_entries; - } catch(e) { /* fail silently, hardcoded fallback shows */ } -} -refreshStatus(); -``` - -Adjust field names to match the actual `/stats` response shape. - ---- - -## Step 6 — Nav active states - -The `active` class on nav links is hardcoded per page (e.g., `agents.html` sets `class="active"` on the Agents link). This is intentional — no JS needed. Each file already has the correct active link set. - -If the site uses a template/include system (e.g., SSR or a static site generator), extract the nav into a shared partial and pass the active page as a variable. - ---- - -## Existing pages — no changes needed - -`architecture.html`, `spec.html`, and `sdk.html` are already well-structured with dark aesthetics and solid content. They share the same nav structure, so they will inherit the new nav style automatically once `shared-styles.css` is deployed — **but only if those pages also import it**. - -### Check existing pages for CSS import - -Open each existing page and verify it has: - -```html - -``` - -If existing pages use an inline ` - - -
-
Internal · Sovereign growth
-

Signups + onboarding

-
Loading…
- - - -
-
-
Distinct principals
-
-
- 24h - 7d -
-
-
-
Agents created
-
-
- 24h - 7d -
-
-
-
Delegations issued
-
-
- 24h - 7d -
-
-
-
Agents transacting
-
-
- 24h - 7d -
-
-
- -

Recent agents

-
Loading…
- -

Recent principals

-
Loading…
- -
- What each number means.
- Distinct principals — unique did:web identities that have claimed at least one agent. Proxy for signups that completed onboarding.
- Agents created — Sovereign-path agents (org-less). Each principal can create many.
- Delegations issued — signed delegation credentials. Real signal that someone went through the spending-cap flow.
- Agents transacting — agents whose first verified transaction has landed. The deepest funnel signal.

- Page polls the API every 60 seconds. Bookmark this URL with the ?t= token; it stays valid until rotated. -
-
- - - - diff --git a/scripts/ATLAS-HANDOFF.md b/scripts/ATLAS-HANDOFF.md deleted file mode 100644 index eadc7d9..0000000 --- a/scripts/ATLAS-HANDOFF.md +++ /dev/null @@ -1,146 +0,0 @@ -# Hand carriage to Atlas: the served-page audit - -> **STATUS 2026-08-20: NOT CARRIED. THE CORROBORATION LEG IS UNBUILT.** -> -> This document describes a scheduled second observer. It does not exist. Checked on this -> machine and on op-vps: no copy of `served-page-audit.mjs` outside this repository, no -> crontab entry running it, no systemd timer running it. Scoped to those two hosts, which -> is what was searched. -> -> **What that costs, precisely.** The audit's exit code 3 means "something disclosed is -> absent on a single observation", and it blocks rather than passing because one look -> cannot tell a real removal from a transient. Resolving it is this scheduler's job: -> a second observation, on a host that keeps its own file, is what turns a single sighting -> into an established absence. With nothing scheduled, exit 3 has nothing that resolves -> it, and the workflow comment in `.github/workflows/verify-published-artifacts.yml` used -> to describe the leg as though it ran. That comment is corrected. -> -> **A guard that reports an absence it cannot distinguish from a missing check is the -> defect this estate keeps finding.** Writing the design down is not building it, and a -> design document reads as a deployment to anyone who finds it without this box. -> -> The figures below are re-derived and current. Carrying it is a decision nobody has taken. - -Under `scripts/`, which `netlify.toml` 404s, so this file is not published. That is -deliberate and it is the same ruling that keeps the audit itself unserved. - -## The file - -| | | -|---|---| -| **file** | `scripts/served-page-audit.mjs` | -| **AUDIT_VERSION** | `3.6.0` | -| **AUDIT_SHA256** (self-hash, recorded in the file) | `624de5d4eec128c41bd68a17d686a233ae3eb99e181a40e12a4e3a09422f3ba6` | -| **sha256 of the file on disk** | `403081a3c94d5e62d378963d924f7834db38eb1e548da8e6afd4e6ff3a2d702a` | -| **size** | 20,007 bytes | -| **confirmed under** | Node **v22.22.3** on darwin | -| **needs** | a Node with global `fetch` (18+). No npm install, no dependencies, no checkout. | - -**Two different hashes, and they answer different questions.** `AUDIT_SHA256` is computed -over the file with its own hash line normalised away, so it does not change when the -recorded value is written in. It is what `--version` prints and what tells you two copies -are the same file. The plain sha256 of the bytes on disk is what you check a transfer with. - -### Carry it, do not fetch it - -The scheduler holds its own copy. It must not pull the script from the zone it is -watching: a monitor that downloads itself from its own subject cannot report that the -subject is compromised. - -### Confirm the copy arrived intact - -```bash -shasum -a 256 served-page-audit.mjs -# expect 403081a3c94d5e62d378963d924f7834db38eb1e548da8e6afd4e6ff3a2d702a - -node served-page-audit.mjs --version -# expect version 3.6.0 and recorded == computed -``` - -`recorded != computed` means the copy was edited and its header was not updated. Both -agreeing but differing from the value above means it is a different version of the file. - -## The daily invocation - -```bash -node served-page-audit.mjs https://observerprotocol.org/check \ - --observations=/var/lib/op-audit/observations.json -``` - -**Nothing is disclosed, and that is the invocation rather than an omission.** With no -`--disclose` the audit asserts that the page carries its own two scripts and nothing else, -so the absence of the analytics injection is *proven on every run* instead of having been -asserted once in a paragraph. The day anything is added at the edge, this goes red without -anyone having to remember what used to be there. - -`--observations` is a small JSON file the audit reads and writes. It is how a later run -corroborates what an earlier one saw. Give it a path that survives between runs. - -### Exit codes, which are the whole interface - -| code | meaning | what to do | -|---|---|---| -| `0` | what the page carries is exactly what is expected and disclosed | nothing | -| `1` | a mismatch, established | look. See below for the one that is expected. | -| `2` | could not reach the page | **not a pass.** Nobody looked. Alert differently from 1. | -| `3` | something disclosed is absent, on a single observation | **not a pass and not the signal.** It will resolve itself into 0 or 1 on the next run. | - -Do not collapse 2 and 3 into "failed", and do not treat either as "passed". A scheduler -that maps everything non-zero to one alarm throws away the distinction this file exists -to draw. - -## What happened, and what the invocation used to be - -Until 17 August 2026 this ran with `--disclose=cloudflareinsights`, because the CDN was -adding an analytics script to every HTML response on the zone and `/check` disclosed it. -The setting was disabled on 17 August. Two runs an hour apart, on 17 and 18 August, both -found it absent; on the second the audit reported `absent and corroborated` and exited 1, -which was the intended signal rather than a break. - -The repair was three edits made together on the website side: the disclosure paragraph -came off `/check`, the strong sentence went back to its full form, and `--disclose` came -out of this invocation. Nothing here should now return 1 in the disclosed-but-absent -direction, because nothing is disclosed. - -**If a red result appears, it means something was added.** That is what the invocation is -now shaped to find. - -### Why 60 minutes, and why corroboration at all - -Absence has two causes and one observation cannot separate them: the setting was changed, -or the edge transiently did not inject. The first is the thing being waited for; the -second means nothing. Corroboration across time is the cheapest thing that tells them -apart. - -It is also why a clean run **does not by itself** authorise the website change. That needs -all three of: - -1. the operator states the toggle was flipped -2. two runs at least 60 minutes apart, both corroborating the absence -3. the CDN's own status page showing the dashboard incident resolved - -Condition 3 is not ceremony. While the dashboard is in an incident the toggle cannot be -flipped at all, so an absence observed during one is evidence about the incident and not -about the setting. - -## What this establishes, and what it does not - -It reads markup. It executes nothing. **So it reports what was delivered, not what is -performed.** It sees every script the page carries, external or inline, and it cannot see -what any of them does once it runs. A runtime request made by a script that is already -disclosed is outside it. - -An earlier version read only external references, and an inline script issuing a request -on load came back clean and printed that the page loaded nothing. That is why inline is -adjudicated now, and why this paragraph is here rather than left to be discovered. - -## If the page legitimately changes - -The audit carries the sha256 of `check.html`'s own inline scripts, so that a **removal** of -the page's verifier fails as loudly as an injection. Those hashes go stale when the page -is edited, and the tempting repair is to delete the entry, which silently removes the -positive assertion. - -Do not. In the repository, `scripts/check-audit-expectations.mjs` recomputes them and fails -the build with the new values to paste in. Carry the updated file to Atlas afterwards and -re-check both hashes above. From 6facb1ccee2d0e6bdecf0218fcdd3aaee6051dcb Mon Sep 17 00:00:00 2001 From: Boyd Cohen Date: Tue, 25 Aug 2026 19:57:42 -0600 Subject: [PATCH 2/4] hermes-agents marks hermes-gate deprecated; integrate/ and ows/ say where the SDK repo pointer lands @observer-protocol/hermes-gate has carried an npm deprecation notice since 2026-08-04 (measured 2026-08-25: version 0.2.1, no repository field, superseded by @observer-protocol/policy-engine plus the per-rail adapters, nothing unpublished). The page still offered three npx bootstrap commands as the way in. The install block is gone, the copy button's script with it, and the Solution section now opens with the deprecation and where the replacement is. The .install-block CSS rules stay: nothing uses them, but removing them is a stylesheet edit this change does not need. @observer-protocol/sdk (npm) and observer-protocol (PyPI) both name github.com/observer-protocol/observer-protocol-spec as their repository. That repository was renamed and GitHub 301s it to observer-protocol/op-legacy-deployment (measured 2026-08-25, gh api: public, not archived). integrate/ links two paths under the old name and now says where they land; ows/ says the same beside its existing deprecation note. The links are kept: they resolve, and the note is what tells a reader why the destination has a different name. Session REMEDIATE-WEBSITE, under op-at-specs/OPEN-CLOSED-RULE.md ledger 2026-08-25 ("Internal docs in public trees"; "op-legacy-deployment" ruling not yet executed, so this copy states the redirect and not the visibility). --- hermes-agents.html | 25 +------------------------ integrate/index.html | 1 + ows/index.html | 4 +++- 3 files changed, 5 insertions(+), 25 deletions(-) diff --git a/hermes-agents.html b/hermes-agents.html index 5eabf58..2b0bc37 100644 --- a/hermes-agents.html +++ b/hermes-agents.html @@ -547,17 +547,7 @@

A spend gate that fails closed

hermes-gate sits between your agent and its wallet. Your agent asks to spend; the gate checks the request against a spend mandate you signed, and allows or denies, fail-closed, meaning anything it can't verify is denied, not waved through.

-
-
- Install - -
- - $npx @observer-protocol/hermes-gate bootstrap generate - $npx @observer-protocol/hermes-gate bootstrap provision - $npx @observer-protocol/hermes-gate bootstrap verify - -
+

@observer-protocol/hermes-gate is deprecated. The npm package carries a deprecation notice dated 2026-08-04 and no successor release is planned: it is superseded by @observer-protocol/policy-engine and the per-rail Observer Protocol adapters. Nothing has been unpublished, so every released version still installs, and the repository below stays public and archived. Nothing on this page is maintained. See the SDK page for what replaces which part.

@@ -716,19 +706,6 @@

Help us build it

- diff --git a/integrate/index.html b/integrate/index.html index e3a8928..97f6208 100644 --- a/integrate/index.html +++ b/integrate/index.html @@ -264,6 +264,7 @@

Resources

  • TypeScript SDK source
  • Python SDK source
  • +
  • Both SDK packages (@observer-protocol/sdk on npm, observer-protocol on PyPI) name observer-protocol/observer-protocol-spec as their repository. That repository has been renamed: GitHub redirects the pointer, and the two links above, to observer-protocol/op-legacy-deployment.
  • X402PaymentCredential schema
  • Delegation credential schema v2
  • Settlement receipt schema v1
  • diff --git a/ows/index.html b/ows/index.html index 814b4f9..a388bfd 100644 --- a/ows/index.html +++ b/ows/index.html @@ -313,7 +313,9 @@

    OWS Quickstart

    April 2026, is unmaintained, and carries a deprecation notice on the registry itself. It still installs and the steps below still run. For verifying a credential use @observer-protocol/policy-engine; the register-and-attest flow in steps 3 to 5 - has no replacement package yet, which is why this page still names the old one. See + has no replacement package yet, which is why this page still names the old one. The + package's repository pointer names observer-protocol/observer-protocol-spec, + which GitHub now redirects to observer-protocol/op-legacy-deployment. See the SDK page for what replaces which part.
npm install @observer-protocol/sdk
From 4d09165fc0b2fa17b06bd36d2ebec92aab3d41e0 Mon Sep 17 00:00:00 2001 From: Boyd Cohen Date: Tue, 25 Aug 2026 20:01:04 -0600 Subject: [PATCH 3/4] Publish a built _site/ from an allowlist instead of the repository root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit netlify.toml said `publish = "."` and documented the durable fix as "a repo restructure ... deliberately not being done here". This is that fix, done without moving a single served file: tools/build-site.sh builds _site/ from an allowlist (root files by web extension, plus fourteen named directories), refuses if any non-web kind reaches the output, and prints every tracked file it withheld. netlify.toml publishes _site/ and runs the script as the build command. Shipping a file is now a decision made in that script, in a commit; until now it was the default and nothing looked broken when it happened. VERIFIED LOCALLY BEFORE THIS COMMIT. The built set is 103 files, identical as a set to the served population derived from the tree (tracked, minus dotfiles, netlify.toml, scripts/, tools/, internal/ and the two forced-404 root files), and byte-identical file by file (cmp, 0 mismatches). Every path that returned 200 on the live site at 21aa1ff and is a file in the tree is present. Four mutations behaved: a .csv inside an allowlisted directory fails the build; a .md at the root and a new directory are withheld; a root .html ships. The per-file forced-404 rules are gone with the files they named (removed at 1d34fb1) and with the mechanism that made them necessary. The /internal/*, /scripts/*, /tools/* and two root-file rules go too: those paths are not in the allowlist, so they do not exist in the publish directory, and a rule for a path that cannot exist is a second representation of one decision. The five directory walkers in scripts/ (check-shared-copy, sweep-sentences, check-outbound-links, check-measured-figures, sentences-at-risk) now skip _site/. Measured: with a local build present, check-shared-copy re-reported three allowed exceptions under their _site/ paths and went red; with the skip it is green. CI never has a build present, so CI would not have shown this; a developer's local run would have, for the wrong reason. CONVENTIONS §3 rewritten: it said adding a non-web file makes it public, and that is no longer true. .gitignore excludes _site/. Session REMEDIATE-WEBSITE, under op-at-specs/OPEN-CLOSED-RULE.md ledger 2026-08-25 ("ArcadiaB CSVs + internal docs in website repo"; "Internal docs in public trees"), and the 2026-08-06 ruling to move the publish directory off the repository root. --- .github/CONVENTIONS.md | 24 ++++-- .gitignore | 3 + netlify.toml | 117 ++++------------------------- scripts/check-measured-figures.mjs | 2 +- scripts/check-outbound-links.mjs | 2 +- scripts/check-shared-copy.mjs | 2 +- scripts/sentences-at-risk.mjs | 4 +- scripts/sweep-sentences.mjs | 2 +- tools/build-site.sh | 96 +++++++++++++++++++++++ 9 files changed, 137 insertions(+), 115 deletions(-) create mode 100755 tools/build-site.sh diff --git a/.github/CONVENTIONS.md b/.github/CONVENTIONS.md index 4b4887c..4567435 100644 --- a/.github/CONVENTIONS.md +++ b/.github/CONVENTIONS.md @@ -316,14 +316,22 @@ someone might write. The second phrasing survives being checked carelessly; the And when reading a constraint someone else wrote, notice which shape it is. If it is a prohibition, convert it before searching: what is the subject, and where would that subject appear? -## 3. Adding a non-web file makes it public - -`publish = "."`. A new `.md`, `.sh`, `.csv` or `.py` at the root is served the moment it merges, and -nothing looks broken when it happens. `netlify.toml` carries a **denylist**, which does not close the -class. - -If you add a non-web file: put it under `.github/`, `scripts/` or `tools/` (the last two are -404'd by rule), or add a `netlify.toml` rule **in the same commit**. +## 3. Shipping a file is a decision, made in `tools/build-site.sh` + +`publish = "_site"`, and `_site/` is built by `tools/build-site.sh` from an allowlist: web files +at the repository root by extension, plus the directories the script names. A new root file of +any other kind, or a new directory, is **not served** until the script names it, in a commit. +The build refuses if a non-web kind (csv, py, sh, mjs, toml, lockfile, key material) reaches the +output, and it prints every tracked file it withheld, so the build log says what did not ship. + +What that does not decide: a file placed inside an allowlisted directory ships, whatever it is. +`verify-samples/README.md` ships on purpose; a notes file dropped into `credentials/` would ship +by the same rule. The directory list is the boundary, not the file. + +Until 2026-08-25 this section said the opposite, and it was true: `publish = "."` served the +whole repository and `netlify.toml` carried a denylist that could not close the class. Anything +you add outside the allowlist now stays in the repository, which is public, so it is still +readable there. That is a different exposure and the OPEN-CLOSED-RULE governs it. --- diff --git a/.gitignore b/.gitignore index 8033006..1a780c9 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,6 @@ node_modules/ # and a tracked .pyc showed as modified after every sitemap regeneration (2026-08-24). __pycache__/ *.pyc + +# The built publish directory (tools/build-site.sh). Netlify builds it; never commit it. +_site/ diff --git a/netlify.toml b/netlify.toml index ee17306..c33d905 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,110 +1,25 @@ [build] - publish = "." - -# Internal pages are not for public serving. Return 404 for anything -# under /internal/ (force = true so the static files are not served). -[[redirects]] - from = "/internal/*" - to = "/404.html" - status = 404 - force = true + publish = "_site" + command = "bash tools/build-site.sh" # --------------------------------------------------------------------------- -# NON-WEB FILES IN THIS REPOSITORY ARE SERVED BY DEFAULT. THESE ARE NOT. -# -# `publish = "."` above means the publish directory IS the repository, so every -# tracked file ships to the live domain whether or not any page links to it. -# Netlify withholds dotfiles and netlify.toml itself; it withholds nothing else. -# Measured 2026-08-06: nine files were fetchable at observerprotocol.org that -# were never meant to be, among them an internal repo map naming private repos -# and on-disk server paths, a site-upgrade guide describing which agent cards -# carried placeholder data, and three social-media exports belonging to a -# different company. -# -# The rules below return a forced 404 for each. Nothing on the site links to any -# of them, so this removes nothing a reader could reach by navigating. +# THE PUBLISH DIRECTORY IS BUILT, NOT THE REPOSITORY. # -# WHAT THIS DOES NOT DO, which matters more than what it does: +# Until 2026-08-25 this said `publish = "."`, so every tracked file shipped to the +# live domain unless a forced-404 rule below named it. That was a denylist and +# it did not close the class: a new .md, .csv, .sh or .py was public the moment +# it merged, and nothing looked broken when it happened. Measured 2026-08-06: +# nine such files were fetchable, among them an internal repo map, a site-upgrade +# guide, and three social-media exports belonging to a different company. # -# 1. It is a DENYLIST. A new .md, .sh, .csv or .py added to this repository is -# PUBLIC THE MOMENT IT IS MERGED, until someone remembers to add it here. -# Netlify requires the splat at the end of a path, so `/*.md` is not -# available and the class cannot be closed by extension. -# 2. It does not remove the files. They remain in git history and are still -# readable to anyone with repository access, which is the correct scope: -# the defect was public serving, not their existence. -# -# The durable fix is a publish directory that is not the repository root, so -# that shipping a file is a decision rather than the default. That is a repo -# restructure and is deliberately not being done here. +# tools/build-site.sh now builds `_site/` from an ALLOWLIST (root web files by +# extension, plus named directories), refuses if any non-web kind reaches the +# output, and prints every tracked file it withheld. Shipping a file is a +# decision made in that script, in a commit. The per-file forced-404 rules that +# stood here are gone with the files they named (removed from the tree 2026-08-25 +# under the OPEN-CLOSED-RULE ledger) and with the mechanism that made them +# necessary; scripts/ and tools/ are not in the allowlist and do not ship. # --------------------------------------------------------------------------- -[[redirects]] - from = "/REPO_MAP.md" - to = "/404.html" - status = 404 - force = true - -[[redirects]] - from = "/IMPLEMENTATION_GUIDE.md" - to = "/404.html" - status = 404 - force = true - -[[redirects]] - from = "/DEMO-SCRIPT-MAR18.md" - to = "/404.html" - status = 404 - force = true - -[[redirects]] - from = "/observer-protocol-sdk.py" - to = "/404.html" - status = 404 - force = true - -[[redirects]] - from = "/verify-did-resolution.sh" - to = "/404.html" - status = 404 - force = true - -# Directory rule, not a file rule: anything added under scripts/ later is -# covered without a further edit. This is the only part of the block above that -# closes a class rather than an instance. -[[redirects]] - from = "/scripts/*" - to = "/404.html" - status = 404 - force = true - -# Build and verification tooling. Not web content. Same reasoning as /scripts/*: -# the publish directory is the repository, so anything committed here is served -# unless a rule says otherwise. -[[redirects]] - from = "/tools/*" - to = "/404.html" - status = 404 - force = true - -# ArcadiaB social scheduling exports. A different company's marketing copy, -# including Mexican regulated-lending claims, on Observer Protocol's domain. -[[redirects]] - from = "/arcadiab-facebook.csv" - to = "/404.html" - status = 404 - force = true - -[[redirects]] - from = "/arcadiab-linkedin-week.csv" - to = "/404.html" - status = 404 - force = true - -[[redirects]] - from = "/arcadiab-linkedin.csv" - to = "/404.html" - status = 404 - force = true # OP Crossrail product page — canonical extension-less URL. [[redirects]] diff --git a/scripts/check-measured-figures.mjs b/scripts/check-measured-figures.mjs index 06ea90b..bc84cf1 100644 --- a/scripts/check-measured-figures.mjs +++ b/scripts/check-measured-figures.mjs @@ -74,7 +74,7 @@ for (const f of readdirSync(resultsDir)) { const htmlFiles = []; (function walk(dir) { for (const e of readdirSync(dir)) { - if (e === 'node_modules' || e === '.git' || e === 'results') continue; + if (e === '_site' || e === 'node_modules' || e === '.git' || e === 'results') continue; const p = join(dir, e); if (statSync(p).isDirectory()) walk(p); else if (e.endsWith('.html')) htmlFiles.push(p); diff --git a/scripts/check-outbound-links.mjs b/scripts/check-outbound-links.mjs index 236fab9..2ab265f 100644 --- a/scripts/check-outbound-links.mjs +++ b/scripts/check-outbound-links.mjs @@ -52,7 +52,7 @@ const TIMEOUT_MS = 20000; const htmlFiles = []; (function walk(dir) { for (const e of readdirSync(dir)) { - if (e === 'node_modules' || e === '.git' || e === 'results') continue; + if (e === '_site' || e === 'node_modules' || e === '.git' || e === 'results') continue; const p = join(dir, e); if (statSync(p).isDirectory()) walk(p); else if (e.endsWith('.html')) htmlFiles.push(p); diff --git a/scripts/check-shared-copy.mjs b/scripts/check-shared-copy.mjs index 4c67a16..e08f6bb 100644 --- a/scripts/check-shared-copy.mjs +++ b/scripts/check-shared-copy.mjs @@ -229,7 +229,7 @@ console.log(`Shared copy is consistent across ${checked} block(s).`); // Every HTML file in the repo, so a new page cannot carry the claim unnoticed. function allHtml(dir = root, acc = []) { for (const e of readdirSync(dir)) { - if (e === 'node_modules' || e === '.git') continue; + if (e === '_site' || e === 'node_modules' || e === '.git') continue; const full = join(dir, e); if (statSync(full).isDirectory()) allHtml(full, acc); else if (e.endsWith('.html')) acc.push(full); diff --git a/scripts/sentences-at-risk.mjs b/scripts/sentences-at-risk.mjs index c2e6c0f..0b86e77 100644 --- a/scripts/sentences-at-risk.mjs +++ b/scripts/sentences-at-risk.mjs @@ -129,7 +129,7 @@ for (const f of readdirSync(resultsDir).filter((n) => n.endsWith('.json'))) { const pages = []; (function walkDir(d) { for (const e of readdirSync(d)) { - if (e === 'dist' || e === 'node_modules' || e.startsWith('.')) continue; + if (e === '_site' || e === 'dist' || e === 'node_modules' || e.startsWith('.')) continue; const p = join(d, e); try { if (readdirSync(p).length >= 0) { walkDir(p); continue; } } catch { /* a file */ } if (e.endsWith('.html')) pages.push(p); @@ -187,7 +187,7 @@ const scalars = (txt, path) => { const servedDocs = []; (function walkDocs(d) { for (const e of readdirSync(d, { withFileTypes: true })) { - if (e.name === 'dist' || e.name === 'node_modules' || e.name.startsWith('.')) continue; + if (e.name === '_site' || e.name === 'dist' || e.name === 'node_modules' || e.name.startsWith('.')) continue; const p2 = join(d, e.name); if (e.isDirectory()) walkDocs(p2); else if (e.name.endsWith('.html') || e.name.endsWith('.md')) servedDocs.push(p2); diff --git a/scripts/sweep-sentences.mjs b/scripts/sweep-sentences.mjs index 31ee28b..553c5eb 100644 --- a/scripts/sweep-sentences.mjs +++ b/scripts/sweep-sentences.mjs @@ -52,7 +52,7 @@ export function sweepSource(src, subject, verbs, strip = keepNewlines) { function htmlFiles(dir, out = []) { for (const e of readdirSync(dir)) { - if (e === 'node_modules' || e === '.git') continue; + if (e === '_site' || e === 'node_modules' || e === '.git') continue; const p = join(dir, e); if (statSync(p).isDirectory()) htmlFiles(p, out); else if (e.endsWith('.html')) out.push(p); diff --git a/tools/build-site.sh b/tools/build-site.sh new file mode 100755 index 0000000..f992baf --- /dev/null +++ b/tools/build-site.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# ─── build-site.sh: the publish directory is BUILT, and shipping a file is a decision ─────── +# +# Until 2026-08-25 netlify.toml said `publish = "."`, so the publish directory was the +# repository and every tracked file shipped to observerprotocol.org unless a forced-404 rule +# named it. That was a denylist: a new .md, .csv, .sh or .py was public the moment it merged, +# and nothing looked broken when it happened. Three ArcadiaB social exports, an internal repo +# map and a site-upgrade guide went through exactly that gap (measured 2026-08-06). +# +# This script builds `_site/` from an ALLOWLIST and netlify.toml publishes `_site/`. A file +# reaches the domain only if it is (a) a web file at the repository root, by extension, or +# (b) inside a directory named below. A new root file of any other kind, or a new directory, +# is not served until someone adds it here, in a commit, on purpose. +# +# WHAT THIS DOES NOT DECIDE. A file placed inside an allowlisted directory ships, whatever it +# is: verify-samples/README.md ships on purpose, and a notes.md dropped into credentials/ would +# ship by the same rule. The directory list is the decision boundary, not the file. +# +# WHAT IT REPORTS. Every tracked file it did NOT ship is printed, so the build log says what +# was withheld rather than leaving it to be noticed. And it refuses outright if anything of a +# non-web kind (csv, py, sh, mjs, toml, lockfile, env, key material) reached the output, so a +# mistake in the allowlist fails the build instead of shipping. +# +# Usage: tools/build-site.sh [OUT] OUT defaults to /_site (untracked, .gitignore'd) +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +OUT="${1:-$ROOT/_site}" + +# ─── THE ALLOWLIST ──────────────────────────────────────────────────────────────────────── +# Directories copied whole (regular files only, dotfiles skipped, node_modules never). +DIRS=( + .well-known + blog + credentials + genesis + institutional + integrate + ows + papers + results + schemas + spec + status-lists + test-issuers + verify-samples +) +# Root-level files, by extension. Nothing else at the root ships. +ROOT_EXT='html|css|png|jpg|jpeg|gif|svg|ico|webp|xml|txt' +# Extensions that must never appear anywhere in the output. A second guard, not the mechanism. +REFUSE_EXT='csv|py|sh|mjs|cjs|toml|yml|yaml|env|pem|key|lock' + +case "$OUT" in "$ROOT"/*) ;; *) echo "build-site: OUT must be inside the repository: $OUT" >&2; exit 2;; esac +rm -rf "$OUT" +mkdir -p "$OUT" + +shipped=0 +# (a) root files by extension +for f in "$ROOT"/*; do + [ -f "$f" ] || continue + b="$(basename "$f")" + case "$b" in .*) continue;; esac + if printf '%s' "$b" | grep -Eq "\.($ROOT_EXT)$"; then + cp -p "$f" "$OUT/$b"; shipped=$((shipped+1)) + fi +done +# (b) allowlisted directories +for d in "${DIRS[@]}"; do + if [ ! -d "$ROOT/$d" ]; then echo "build-site: allowlisted directory absent: $d" >&2; exit 2; fi + while IFS= read -r -d '' f; do + rel="${f#"$ROOT"/}" + case "$rel" in */node_modules/*|node_modules/*) continue;; esac + mkdir -p "$OUT/$(dirname "$rel")" + cp -p "$f" "$OUT/$rel"; shipped=$((shipped+1)) + done < <(find "$ROOT/$d" -type f ! -name '.*' -print0) +done + +# ─── REFUSE non-web kinds in the output ──────────────────────────────────────────────────── +bad="$(cd "$OUT" && find . -type f | grep -E "\.($REFUSE_EXT)$" || true)" +if [ -n "$bad" ]; then + echo "build-site: REFUSED, non-web files reached the output:" >&2 + printf ' %s\n' $bad >&2 + exit 1 +fi + +# ─── REPORT what was withheld ────────────────────────────────────────────────────────────── +echo "build-site: shipped $shipped file(s) to ${OUT#"$ROOT"/}/" +if git -C "$ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + withheld=0 + while IFS= read -r t; do + [ -f "$OUT/$t" ] || { [ $withheld -eq 0 ] && echo "build-site: tracked and NOT shipped:"; echo " $t"; withheld=$((withheld+1)); } + done < <(git -C "$ROOT" ls-files) + echo "build-site: withheld $withheld tracked file(s)." +else + echo "build-site: not a git checkout here, so the withheld list is not derivable; nothing is reported." +fi From 7165173ed2c775435793b5541e71899aa78c30e7 Mon Sep 17 00:00:00 2001 From: Boyd Cohen Date: Tue, 25 Aug 2026 20:01:05 -0600 Subject: [PATCH 4/4] sitemap: lastmod for hermes-agents, integrate/, ows/ after 6facb1c Derived by tools/make-sitemap.py from git; the three pages changed in the previous content commit and the sitemap must be regenerated after, not before. Session REMEDIATE-WEBSITE. --- sitemap.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sitemap.xml b/sitemap.xml index 64c189d..568e041 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -46,7 +46,7 @@ https://observerprotocol.org/hermes-agents.html - 2026-08-24 + 2026-08-25 https://observerprotocol.org/institutional/ @@ -54,11 +54,11 @@ https://observerprotocol.org/integrate/ - 2026-08-23 + 2026-08-25 https://observerprotocol.org/ows/ - 2026-08-09 + 2026-08-25 https://observerprotocol.org/papers/