From 23d7909ab90cf9f10a12ff32d05395e37f9c1295 Mon Sep 17 00:00:00 2001 From: Clay <“”william.c.hooten@gmail.com> Date: Mon, 29 Jun 2026 11:10:25 -0500 Subject: [PATCH] fix: harden loopy harness checks --- AGENTS.md | 5 +- README.md | 1 + loop-library/scripts/check.mjs | 18 ++ loop-library/worker/src/auth-votes.js | 1 + loop-library/worker/test/auth-votes.test.js | 18 ++ loop-library/worker/test/loop-routes.test.js | 174 ++++++++++++++++++- 6 files changed, 213 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7de8574..ba5a62b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,13 +53,14 @@ this repository layout. node loop-library/scripts/check.mjs npm --prefix loop-library/worker run check python3 -m json.tool loop-library/site/.herenow/data.json >/dev/null + python3 -m json.tool loop-library/site/.herenow/proxy.json >/dev/null python3 -m json.tool loop-library/scripts/seo-geo-query-benchmark.json >/dev/null git diff --check ``` - Do not publish a loop unless its public homepage row, detail page, - `catalog.json`, `catalog.md`, sitemap, and feed all read back from production - with the expected slug and modified date. + `catalog.json`, `catalog.md`, `catalog.txt`, `llms.txt`, sitemap, and feed + all read back from production with the expected slug and modified date. ## Protected forms diff --git a/README.md b/README.md index 3408b36..2483528 100644 --- a/README.md +++ b/README.md @@ -331,6 +331,7 @@ node --check loop-library/site/script.js node loop-library/scripts/check.mjs npm --prefix loop-library/worker run check python3 -m json.tool loop-library/site/.herenow/data.json >/dev/null +python3 -m json.tool loop-library/site/.herenow/proxy.json >/dev/null python3 -m json.tool loop-library/scripts/seo-geo-query-benchmark.json >/dev/null git diff --check ``` diff --git a/loop-library/scripts/check.mjs b/loop-library/scripts/check.mjs index e35f621..bbd71ce 100644 --- a/loop-library/scripts/check.mjs +++ b/loop-library/scripts/check.mjs @@ -40,6 +40,7 @@ const [ legacySkillPublish, readme, agents, + ciWorkflow, ] = await Promise.all([ readFile(path.join(siteRoot, "index.html"), "utf8"), readFile(path.join(siteRoot, "learn", "index.html"), "utf8"), @@ -69,6 +70,7 @@ const [ readFile(path.join(legacySkillRoot, "references", "publish.md"), "utf8"), readFile(path.join(repoRoot, "README.md"), "utf8"), readFile(path.join(repoRoot, "AGENTS.md"), "utf8"), + readFile(path.join(repoRoot, ".github", "workflows", "ci.yml"), "utf8"), ]); const workerPackage = JSON.parse(workerPackageSource); @@ -371,5 +373,21 @@ assert(readme.includes("loops:export")); assert(readme.includes("loops:restore")); assert(agents.includes("Do not commit")); assert(agents.includes("Never publish the empty shell")); +assert( + agents.includes("`catalog.json`, `catalog.md`, `catalog.txt`, `llms.txt`, sitemap, and feed"), +); +for (const command of [ + "node --check loop-library/site/script.js", + "node loop-library/scripts/check.mjs", + "npm --prefix loop-library/worker run check", + "python3 -m json.tool loop-library/site/.herenow/data.json >/dev/null", + "python3 -m json.tool loop-library/site/.herenow/proxy.json >/dev/null", + "python3 -m json.tool loop-library/scripts/seo-geo-query-benchmark.json >/dev/null", + "git diff --check", +]) { + assert(readme.includes(command), `README.md missing validation command: ${command}`); + assert(agents.includes(command), `AGENTS.md missing validation command: ${command}`); + assert(ciWorkflow.includes(command), `.github/workflows/ci.yml missing validation command: ${command}`); +} console.log("Loop Library database-only checks passed."); diff --git a/loop-library/worker/src/auth-votes.js b/loop-library/worker/src/auth-votes.js index 5e30d08..f33f644 100644 --- a/loop-library/worker/src/auth-votes.js +++ b/loop-library/worker/src/auth-votes.js @@ -42,6 +42,7 @@ export async function handleAuthVoteRoute( if (body instanceof Response) return body; const viewer = await readSession(body.sessionToken, env); if (!viewer) return jsonResponse({ viewer: null, viewerVotes: {} }); + if (!env.VOTE_STORE) return unavailable("Voting is not configured."); const response = await voteStoreFetch( env, `/votes?voter=${encodeURIComponent(viewer.sub)}`, diff --git a/loop-library/worker/test/auth-votes.test.js b/loop-library/worker/test/auth-votes.test.js index df1ed45..b783b74 100644 --- a/loop-library/worker/test/auth-votes.test.js +++ b/loop-library/worker/test/auth-votes.test.js @@ -219,6 +219,24 @@ test("voting UI is fail-closed unless the launch flag is exactly true", async () assert.equal((await enabled.json()).uiEnabled, true); }); +test("session lookup fails closed when vote storage is unavailable", async () => { + const env = makeEnv(); + const sessionToken = await githubSession(env); + delete env.VOTE_STORE; + + const session = await handleAuthVoteRoute( + new Request(`${BASE}/auth/session`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ sessionToken }), + }), + env, + ); + + assert.equal(session.status, 503); + assert.equal((await session.json()).code, "not_configured"); +}); + test("vote writes reject anonymous, cross-site, malformed, and unpublished requests", async () => { const env = makeEnv(); const anonymous = await handleAuthVoteRoute( diff --git a/loop-library/worker/test/loop-routes.test.js b/loop-library/worker/test/loop-routes.test.js index 9df6e3c..a913fdd 100644 --- a/loop-library/worker/test/loop-routes.test.js +++ b/loop-library/worker/test/loop-routes.test.js @@ -16,8 +16,13 @@ class MemoryLoopCatalogNamespace { revisions = new Map(); restore = null; - constructor(active = true) { + constructor(active = true, options = {}) { this.active = active; + this.validateWrites = options.validateWrites === true; + + for (const loop of options.seedLoops || []) { + this.loops.set(loop.slug, { ...loop }); + } } idFromName(name) { @@ -39,6 +44,10 @@ class MemoryLoopCatalogNamespace { { status: 409 }, ); } + const catalogError = this.catalogIntegrityError([ + { loop: body.loop, status: body.status }, + ]); + if (catalogError) return catalogError; this.loops.set(body.loop.slug, { ...body.loop, status: body.status }); const revisions = this.revisions.get(body.loop.slug) || []; revisions.unshift({ @@ -76,6 +85,10 @@ class MemoryLoopCatalogNamespace { if (init.method === "POST" && url.pathname === "/import") { const body = JSON.parse(init.body); + const catalogError = this.catalogIntegrityError( + body.loops.map((loop) => ({ loop, status: body.status })), + ); + if (catalogError) return catalogError; for (const loop of body.loops) this.loops.set(loop.slug, { ...loop, status: body.status }); if (body.activate) this.active = true; return Response.json({ imported: body.loops.length }); @@ -191,11 +204,31 @@ class MemoryLoopCatalogNamespace { }, }; } + + catalogIntegrityError(changes) { + if (!this.validateWrites) return null; + + try { + assertCatalogIntegrity([...this.loops.values()], changes); + return null; + } catch (error) { + return Response.json( + { + error: error instanceof Error ? error.message : "Invalid catalog graph", + code: "invalid_catalog_graph", + }, + { status: 409 }, + ); + } + } } function makeEnv(options = {}) { return { - LOOP_CATALOG: new MemoryLoopCatalogNamespace(options.active ?? true), + LOOP_CATALOG: new MemoryLoopCatalogNamespace(options.active ?? true, { + validateWrites: options.validateCatalog === true, + seedLoops: options.seedLoops, + }), LOOP_PUBLISH_TOKEN: "test-publish-token", BOOTSTRAP_CATALOG_DIGEST: options.bootstrapDigest || "test-bootstrap-digest", BOOTSTRAP_LOOP_COUNT: String(options.bootstrapLoopCount ?? 50), @@ -237,6 +270,63 @@ function exampleLoop(overrides = {}) { }; } +function overnightDocsLoop(overrides = {}) { + return exampleLoop({ + number: "900", + slug: "overnight-docs-sweep", + title: "The overnight docs sweep", + summary: "Keeps documentation refreshed during a bounded maintenance pass.", + seoTitle: "Overnight Docs Sweep | Loop Library", + description: "A loop for refreshing docs with a concrete overnight check.", + prompt: "Review stale docs, update the highest-value page, verify links, and stop.", + verifyTitle: "Docs pass the configured link and freshness checks.", + verifyDetail: "Run the documentation checks and inspect the changed page.", + useWhen: "Use this when documentation has drifted after product or code changes.", + steps: [ + "Find the stale documentation surface.", + "Make one focused update.", + "Run the documentation verification checks.", + ], + why: "Small bounded updates keep docs useful without broad rewrites.", + note: "Stop when the check passes or when ownership is unclear.", + keywords: ["documentation", "maintenance", "freshness"], + related: ["catalog-support-loop"], + ...overrides, + }); +} + +function catalogSupportLoop(overrides = {}) { + return exampleLoop({ + number: "901", + slug: "catalog-support-loop", + title: "The catalog support loop", + summary: "Provides a stable fixture for catalog graph validation.", + seoTitle: "Catalog Support Loop | Loop Library", + description: "A support loop used to validate related-loop relationships.", + prompt: "Check the catalog graph, confirm related links resolve, and stop.", + verifyTitle: "Every related loop slug resolves.", + verifyDetail: "Read the generated catalog and verify each related link.", + useWhen: "Use this when validating catalog graph behavior.", + steps: [ + "Load the catalog records.", + "Trace each related-loop slug.", + "Report any missing or unpublished relationship.", + ], + why: "Graph validation prevents public pages from hiding broken relationships.", + note: "Keep the fixture plain so tests can assert the target loop explicitly.", + keywords: ["catalog graph", "related loops", "validation"], + related: ["overnight-docs-sweep"], + ...overrides, + }); +} + +function publishedSupportLoops() { + return [ + { ...overnightDocsLoop(), status: "published" }, + { ...catalogSupportLoop(), status: "published" }, + ]; +} + function adminRequest(loop, options = {}) { const headers = { "Content-Type": "application/json" }; if (options.authorized !== false) headers.Authorization = "Bearer test-publish-token"; @@ -286,6 +376,60 @@ test("rejects unauthorized and invalid publishing requests", async () => { assert.equal((await invalid.json()).code, "invalid_loop"); }); +test("publishing routes reject invalid catalog graphs through the catalog store", async () => { + const missingRelated = makeEnv({ validateCatalog: true }); + const missingRelatedResponse = await handleRequest( + adminRequest(exampleLoop({ related: ["missing-loop"] })), + missingRelated, + ); + assert.equal(missingRelatedResponse.status, 409); + assert.match( + (await missingRelatedResponse.json()).error, + /references unavailable related loop missing-loop/, + ); + + const invalidImport = makeEnv({ validateCatalog: true }); + const importResponse = await handleRequest( + new Request(`${WORKER_ORIGIN}/admin/loops/import`, { + method: "POST", + headers: { + Authorization: "Bearer test-publish-token", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + loops: [exampleLoop({ related: ["missing-loop"] })], + status: "published", + }), + }), + invalidImport, + ); + assert.equal(importResponse.status, 409); + assert.equal((await importResponse.json()).code, "invalid_catalog_graph"); + + const duplicateMetadata = makeEnv({ + validateCatalog: true, + seedLoops: [ + ...publishedSupportLoops(), + { ...exampleLoop({ related: ["overnight-docs-sweep"] }), status: "published" }, + ], + }); + const duplicateResponse = await handleRequest( + adminRequest( + exampleLoop({ + number: "052", + slug: "duplicate-title-loop", + related: ["overnight-docs-sweep"], + }), + ), + duplicateMetadata, + ); + assert.equal(duplicateResponse.status, 409); + assert.match( + (await duplicateResponse.json()).error, + /duplicate-title-loop.title duplicates database-publishing-loop.title/, + ); +}); + test("rejects a stale publisher instead of overwriting a newer revision", async () => { const env = makeEnv(); const loop = exampleLoop(); @@ -412,6 +556,31 @@ test("renders database content into the canonical homepage and detail page", asy assert.match(detailHtml, /name="twitter:image:alt"/); }); +test("renders related links from a catalog graph validated by the store", async () => { + const env = makeEnv({ + validateCatalog: true, + seedLoops: publishedSupportLoops(), + }); + const publish = await handleRequest( + adminRequest(exampleLoop({ related: ["overnight-docs-sweep"] })), + env, + ); + assert.equal(publish.status, 201); + + const detail = await handleRequest( + new Request(`${SITE_ORIGIN}/loop-library/loops/database-publishing-loop/`), + env, + ); + const detailHtml = await detail.text(); + + assert.equal(detail.status, 200); + assert.match(detailHtml, /Related loops/); + assert.match( + detailHtml, + /href="\.\.\/overnight-docs-sweep\/">The overnight docs sweep<\/a>/, + ); +}); + test("renders homepage headers for HEAD by fetching the origin shell with GET", async () => { const env = makeEnv(); await handleRequest(adminRequest(exampleLoop()), env); @@ -563,6 +732,7 @@ test("generates catalogs, sitemap, and feed from the same record", async () => { for (const [path, expected] of [ ["catalog.json", '"loopCount":1'], ["catalog.md", "The database publishing loop"], + ["catalog.txt", "The database publishing loop"], ["llms.txt", "Published loops: 1."], ["sitemap.xml", "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/loops/database-publishing-loop/"], ["feed.xml", "The database publishing loop"],