oficina / home Public

Stale text from the old script rule (review, Claude Fable 5.1)

A spec ruling row records Paul's words and supersedes 21/24; comments in
assetversion.go, passkey.js and the sweep no longer cite the carve-out as
the live rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev pushed by paul@keymail.dev b7cb7e98c83e8ea9e6aec53ac73a21756d05b1c7 parent fdfd0d7
4 files changed, +11 −9
  • docs/superpowers/specs/2026-08-30-home-design.md +1 −0
  • internal/home/assetversion.go +3 −2
  • internal/home/noscript_test.go +3 −3
  • internal/home/static/passkey.js +4 −4

CI failed — run details

CI log
amadan: sync for CI failed: exit status 128: error: RPC failed; HTTP 429 curl 22 The requested URL returned error: 429
fatal: expected 'acknowledgments'
 (and could not reset the workspace: unlinkat /home/paulca/.local/state/amadan/runner/repos/oficina__home/cache/go-mod/filippo.io/age@v1.3.2/pq.go: permission denied)
diff --git a/docs/superpowers/specs/2026-08-30-home-design.md b/docs/superpowers/specs/2026-08-30-home-design.md
index 268b384..429edb4 100644
--- a/docs/superpowers/specs/2026-08-30-home-design.md
+++ b/docs/superpowers/specs/2026-08-30-home-design.md
@@ -1489,6 +1489,7 @@ The suite sharing doctrine (`oficina/suite`) binds this spec. Read against
| 12 | Membership is its own record, written only by an owner. This is ruling 23. |
| 13 | A compromised home is three compromises, conceded separately — see below. |
| 14 | Custody is bounded at acquisition and retired by the feed. |
+| 25 | **Supersedes 21 and 24: no script by default; script only to progressively enhance a page or provide interaction HTML and CSS cannot, and always prefer none.** Every screen works with scripts off; scripts are static same-origin files, never inline, and the no-`<script>` sweep names Home's script files (a closed list) rather than screens — §11 | A closed list of screens allowed script, which the busy rule on every form outgrew | Paul, 2026-09-29, verbatim: "the default [is] no script, but scripts can be added to progressively enhance and provide interaction, but always prefer no JavaScript" |
Calls 7 and 8 are added to §9. Call 7 is the activity pull, and it waits on
the identity layer (§13 step 10) because it carries a real assertion. Call 8
diff --git a/internal/home/assetversion.go b/internal/home/assetversion.go
index bcbc7cd..99feb6a 100644
--- a/internal/home/assetversion.go
+++ b/internal/home/assetversion.go
@@ -16,8 +16,9 @@ import (
// else, so a returning visitor fetches only the page, and a deploy still
// takes effect at once: new bytes, new version, new URL.
//
-// Scripts are left at their bare paths: the scriptless sweep's allowlist
-// names them there (passkey.go), and they are revalidated by ETag.
+// Scripts are linked the same way where a template can (busy.js); the
+// passkey and handoff scripts keep bare paths their own modules import,
+// revalidated by ETag. noscript_test.go's closed list names them all.
// contentVersion is the first 16 hex digits of the body's SHA-256.
func contentVersion(body []byte) string {
diff --git a/internal/home/noscript_test.go b/internal/home/noscript_test.go
index e820142..e6b3e80 100644
--- a/internal/home/noscript_test.go
+++ b/internal/home/noscript_test.go
@@ -148,7 +148,7 @@ func sampleData(name string) []any {
// mechanical (it once also named the only screens allowed any script; the
// rule is now no script by default and prefer none, so it names the
// files instead). Historically: every
-// screen works with scripts off, and ruling 21 carves out only the
+// screen works with scripts off, and ruling 21 once carved out only the
// passkey screens.
//
// Two stated limits, so nobody reads more into a green run than is
@@ -263,7 +263,7 @@ func TestServedCSPAllowsNoInlineScript(t *testing.T) {
// register endpoints). None was stopped by csrf; the Origin header
// below sees to that. So the sweep reached eighteen of
// twenty-three, and one of the five it missed renders the enrolment
- // screen, which is one of the two ruling 21 lets load script.
+ // screen, which is one of the pages that loads a script to enhance it.
//
// So: a fresh session cookie as well. With it, all twenty-three
// targets reach and run their handler — probe-verified rather than
@@ -324,7 +324,7 @@ func TestServedCSPAllowsNoInlineScript(t *testing.T) {
d := strings.TrimSpace(directive)
name, _, _ := strings.Cut(d, " ")
// Inline STYLES are allowed on purpose: ui's partials set
- // style attributes. Inline SCRIPT is what §11 and ruling 21
+ // style attributes. Inline SCRIPT is what §11 and the script rule
// are about.
if name == "style-src" {
continue
diff --git a/internal/home/static/passkey.js b/internal/home/static/passkey.js
index 6598320..74b3fc5 100644
--- a/internal/home/static/passkey.js
+++ b/internal/home/static/passkey.js
@@ -4,10 +4,10 @@
// the server says to go.
//
// It is a static, same-origin module, not an inline block: rastrillo's
-// default CSP forbids inline script, and §11's carve-out is for these
-// two static files and nothing else. Its path is stable and
-// unfingerprinted so the scriptless sweep can name it — a rule that
-// cannot name its subject is not a rule.
+// default CSP forbids inline script, and Home's scripts are a closed list
+// of static files (spec ruling superseding 21/24: no script by default,
+// enhance only, prefer none). Its path is stable so that list can name
+// it — a rule that cannot name its subject is not a rule.
import { register, authenticate, available } from "/static/webauthn.mjs";
async function post(path, body) {