From f1888e97c94c32930f3a9343e8c728c3eeb168ab Mon Sep 17 00:00:00 2001 From: dormouse-bot Date: Tue, 22 Sep 2026 10:14:25 +0000 Subject: [PATCH 1/7] Keep provisioning obligations out of the FAIL IF list so the audit can decide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit security-hosted.md carried two conditions no run can determine from the repository: the Cloudflare script-injection exclusion, a zone setting, and the closing activation sentence, which states its own answer. Two consecutive nightly runs read them opposite ways — 2026-09-21 resolved both to PASS, 2026-09-22 left both UNVERIFIABLE and returned INCONCLUSIVE on a 375-PASS, 0-FAIL pass, holding the release gate shut (#747). Both now sit in the activation paragraph as obligations rather than inside a FAIL IF. security-audit.md states the rule for future authors, and the shared preamble tells a domain what verdict an external obligation gets, so neither becomes an undetermined check again. Refs #747 --- .github/audit/_preamble.md | 6 ++++++ docs/specs/security-audit.md | 1 + docs/specs/security-audit.rationale.md | 2 ++ docs/specs/security-hosted.md | 4 ++-- 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/audit/_preamble.md b/.github/audit/_preamble.md index 5652a6eea..b817946fa 100644 --- a/.github/audit/_preamble.md +++ b/.github/audit/_preamble.md @@ -20,6 +20,12 @@ for a check you could not determine — a transient network error, or an area yo ran out of room to reach — and say which it was. It is never a substitute for a check you could have run. +A condition on state outside the repository — a provisioning step, an external +zone setting — is not a check you could not determine; it is not a check at +all. Verdict the `FAIL IF`'s condition on the repository's own state, and +record the external obligation as INFO. `UNVERIFIABLE` there would make every +later run inconclusive too, because nothing a later run can read settles it. + Where `docs/specs/security.md` says a risk is accepted ("What is not defended") or a gap is known ("Known gaps"), do not re-report it as a finding — report only if the situation has changed or is worse than described. diff --git a/docs/specs/security-audit.md b/docs/specs/security-audit.md index e29b4bf92..a6f2b6c0a 100644 --- a/docs/specs/security-audit.md +++ b/docs/specs/security-audit.md @@ -78,6 +78,7 @@ Source of truth: `2. Wait without ending your turn`, `3. Merge`, and `4. The ver - **Partial has three shapes**, each named in the INCONCLUSIVE issue: `UNVERIFIABLE` for a check reached but not determined; `_Incomplete …_` above a fragment cut off mid-report; `_No report …_` for a domain that never wrote one. The merged `## Summary` may likewise read `INCONCLUSIVE`, and **gives no coverage count for a cut-off domain** (rationale). - **With no `audit-report.md` the reporting step publishes each fragment verbatim under its own heading**, unmerged (rationale). - **Must return `VERDICT: INCONCLUSIVE` from a domain with any undetermined check unless it found a failure.** Only all-determined passing checks permit `VERDICT: PASS`; a domain's inconclusive verdict prevents a merged pass. +- **Never write a `FAIL IF` condition on state outside the repository**: state the in-repo half, the obligation beside it (rationale). - **`STATUS` is assigned in exactly two places**: where the status file is parsed, and in the single escalation block, **which orders `FAIL` > `MISSING` > `PASS`** — a dissent can raise `MISSING` to `FAIL` and never the reverse, and a `FAIL` alongside missing or unreadable fragments still reports them. - **The report is truncated to 32,000 characters before posting**, head kept, by `scripts/clamp-issue-body.mjs` (self-tested by `scripts/clamp-issue-body-selftest.mjs`). The call is non-fatal; the `audit-transcript` artifact holds the report in full; `.github/workflows/workflow-audit.yaml` truncates its commit list the same way (rationale). - **Every run uploads the `audit-transcript` artifact, which is world-readable and not secret-masked** — 14-day retention, deep-linked from failure issues (rationale). diff --git a/docs/specs/security-audit.rationale.md b/docs/specs/security-audit.rationale.md index bc0fae7be..92fddd839 100644 --- a/docs/specs/security-audit.rationale.md +++ b/docs/specs/security-audit.rationale.md @@ -60,6 +60,8 @@ Run 35205193090's `## Summary` also inverted the placeholder it was reading: "tw Collapsing the inconclusive case into `FAIL`, as the step originally did, filed an identical issue for "the repo is insecure" and "the auditor stopped early". +A `FAIL IF` condition on state outside the repository makes the verdict a coin flip, because no run can ever determine it. `security-hosted.md` carried two: the Cloudflare script-injection exclusion, which is a zone setting, and a closing activation sentence that stated its own answer. Run 35586089654 (2026-09-21) reached both as `UNVERIFIABLE` in its sub-auditors and its domain lead resolved both to PASS, on the ground that the audited condition was the in-repo half; run 35709640946 (2026-09-22) left both `UNVERIFIABLE`, so a pass with 375 PASS and 0 FAIL returned INCONCLUSIVE and held the release gate shut (issue #747). Nothing in the tree had changed between them. + GitHub rejects an over-long issue body outright; that rejection lands on a `set -e` step *after* the verdict is decided, and the finding then reaches no issue and no comment — only a red run and an artifact that expires. Truncation keeps the head because that is where the verdict and the links are, and the clamp call is non-fatal so a failure of the helper cannot reopen the window it closes. Issue prose per combination of conditions cannot be kept correct by fixing combinations. Four consecutive review rounds found the same defect in different clothes — an arm whose text was true only of the states that could reach it, made false by the next gate that widened. A note claiming nothing about the other conditions cannot be invalidated by a new one. diff --git a/docs/specs/security-hosted.md b/docs/specs/security-hosted.md index 375d7edc2..79ed106ad 100644 --- a/docs/specs/security-hosted.md +++ b/docs/specs/security-hosted.md @@ -9,7 +9,7 @@ - **FAIL IF** Hosted accepts a request URL outside configured `APP_ORIGIN`, grants marketing-origin credentialed CORS, or permits a state-changing auth request without exact Origin and CSRF checks; inspect `hosted/server/worker-app.ts` and the packed adapter. - **FAIL IF** authentication cookies have a Domain attribute, lack `__Host-`, Secure, HttpOnly, or Path=/ in HTTPS, or session tokens appear in browser JSON or persistent browser storage; inspect the adapter and `hosted/src/api.ts`. - **FAIL IF** the production HTML permits third-party scripts, framing, or inline script execution, any response bypasses `secureHeaders` including a misconfigured deployment's error, or anything but a content-hashed `/assets/` file is cacheable, the SPA fallback's shell included; inspect `secureHeaders` in `hosted/server/headers.ts`, binding resolution in `hosted/server/worker-app.ts`, and asset routing in `hosted/wrangler.jsonc`. -- **FAIL IF** marketing scripts, analytics, provider avatars, or remote fonts enter the Hosted frontend; inspect the frontend import graph and deployed response when available. Cloudflare script injection must be excluded for the Hosted hostname at provisioning. +- **FAIL IF** marketing scripts, analytics, provider avatars, or remote fonts enter the Hosted frontend; inspect the frontend import graph and deployed response when available. Pinned by `hosted/server/tests/workers.test.ts`. @@ -31,7 +31,7 @@ Pinned by `hosted/server/tests/workers.test.ts` and `hosted/server/tests/policy. Pinned by `hosted/server/tests/artifacts.test.ts`, `hosted/server/tests/workers.test.ts`, `hosted/server/tests/policy.test.ts`. -Production activation must verify uncached Hyperdrive, separate credentials, and excluded marketing injection (`hosted/README.md`); checked-in placeholders prove none of them. +Activation must verify uncached Hyperdrive, separate credentials, and the Hosted hostname excluded from Cloudflare script injection (`hosted/README.md`); a checked-in placeholder proves none of them, so none is a `FAIL IF` above. ## Future From b6e248ea930665ce0a42c78034d31c6390ee4a9a Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 22 Sep 2026 21:34:32 -0700 Subject: [PATCH 2/7] Stage external obligations under Future, matching #757 #757 already moved the two Hosted provisioning sentences under security-hosted.md's ## Future and kept the in-repo preflight gate as a FAIL IF, so the merge takes main's security-hosted.md. The general rule and the preamble instruction still apply to every other spec; they now name ## Future as where an external obligation goes, the rationale records how #747 was resolved, and the budget is ratcheted for the rule. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/audit/_preamble.md | 3 ++- docs/specs/security-audit.md | 2 +- docs/specs/security-audit.rationale.md | 2 +- scripts/spec-word-budgets.json | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/audit/_preamble.md b/.github/audit/_preamble.md index b817946fa..62b2e4324 100644 --- a/.github/audit/_preamble.md +++ b/.github/audit/_preamble.md @@ -23,7 +23,8 @@ check you could have run. A condition on state outside the repository — a provisioning step, an external zone setting — is not a check you could not determine; it is not a check at all. Verdict the `FAIL IF`'s condition on the repository's own state, and -record the external obligation as INFO. `UNVERIFIABLE` there would make every +record the external obligation as INFO — as for anything a spec stages under +`## Future`. `UNVERIFIABLE` there would make every later run inconclusive too, because nothing a later run can read settles it. Where `docs/specs/security.md` says a risk is accepted ("What is not defended") diff --git a/docs/specs/security-audit.md b/docs/specs/security-audit.md index 1e2a67bd2..f952e046a 100644 --- a/docs/specs/security-audit.md +++ b/docs/specs/security-audit.md @@ -79,7 +79,7 @@ Source of truth: `2. Wait without ending your turn`, `3. Merge`, and `4. The ver - **Partial has three shapes**, each named in the INCONCLUSIVE issue: `UNVERIFIABLE` for a check reached but not determined; `_Incomplete …_` above a fragment cut off mid-report; `_No report …_` for a domain that never wrote one. The merged `## Summary` may likewise read `INCONCLUSIVE`, and **gives no coverage count for a cut-off domain** (rationale). - **With no `audit-report.md` the reporting step publishes each fragment verbatim under its own heading**, unmerged (rationale). - **Must return `VERDICT: INCONCLUSIVE` from a domain with any undetermined check unless it found a failure.** Only all-determined passing checks permit `VERDICT: PASS`; a domain's inconclusive verdict prevents a merged pass. -- **Never write a `FAIL IF` condition on state outside the repository**: state the in-repo half, the obligation beside it (rationale). +- **Never write a `FAIL IF` condition on state outside the repository**: audit the in-repo half and stage the rest under `## Future` (rationale). - **`STATUS` is assigned in exactly two places**: where the status file is parsed, and in the single escalation block, **which orders `FAIL` > `MISSING` > `PASS`** — a dissent can raise `MISSING` to `FAIL` and never the reverse, and a `FAIL` alongside missing or unreadable fragments still reports them. - **The report is truncated to 32,000 characters before posting**, head kept, by `scripts/clamp-issue-body.mjs` (self-tested by `scripts/clamp-issue-body-selftest.mjs`). The call is non-fatal; the `audit-transcript` artifact holds the report in full; `.github/workflows/workflow-audit.yaml` truncates its commit list the same way (rationale). - **Every run uploads the `audit-transcript` artifact, which is world-readable and not secret-masked** — 14-day retention, deep-linked from failure issues (rationale). diff --git a/docs/specs/security-audit.rationale.md b/docs/specs/security-audit.rationale.md index 8cf605cde..ee60b22d7 100644 --- a/docs/specs/security-audit.rationale.md +++ b/docs/specs/security-audit.rationale.md @@ -62,7 +62,7 @@ Run 35205193090's `## Summary` also inverted the placeholder it was reading: "tw Collapsing the inconclusive case into `FAIL`, as the step originally did, filed an identical issue for "the repo is insecure" and "the auditor stopped early". -A `FAIL IF` condition on state outside the repository makes the verdict a coin flip, because no run can ever determine it. `security-hosted.md` carried two: the Cloudflare script-injection exclusion, which is a zone setting, and a closing activation sentence that stated its own answer. Run 35586089654 (2026-09-21) reached both as `UNVERIFIABLE` in its sub-auditors and its domain lead resolved both to PASS, on the ground that the audited condition was the in-repo half; run 35709640946 (2026-09-22) left both `UNVERIFIABLE`, so a pass with 375 PASS and 0 FAIL returned INCONCLUSIVE and held the release gate shut (issue #747). Nothing in the tree had changed between them. +A `FAIL IF` condition on state outside the repository makes the verdict a coin flip, because no run can ever determine it. `security-hosted.md` carried two: the Cloudflare script-injection exclusion, which is a zone setting, and a closing activation sentence that stated its own answer. Run 35586089654 (2026-09-21) reached both as `UNVERIFIABLE` in its sub-auditors and its domain lead resolved both to PASS, on the ground that the audited condition was the in-repo half; run 35709640946 (2026-09-22) left both `UNVERIFIABLE`, so a pass with 375 PASS and 0 FAIL returned INCONCLUSIVE and held the release gate shut (issue #747). Nothing in the tree had changed between them. #757 staged both under `security-hosted.md`'s `## Future` and kept the in-repo half — the deploy's `preflight` gate — as a `FAIL IF`. GitHub rejects an over-long issue body outright; that rejection lands on a `set -e` step *after* the verdict is decided, and the finding then reaches no issue and no comment — only a red run and an artifact that expires. Truncation keeps the head because that is where the verdict and the links are, and the clamp call is non-fatal so a failure of the helper cannot reopen the window it closes. diff --git a/scripts/spec-word-budgets.json b/scripts/spec-word-budgets.json index 555b27d60..7c2d069c1 100644 --- a/scripts/spec-word-budgets.json +++ b/scripts/spec-word-budgets.json @@ -18,7 +18,7 @@ "docs/specs/relay.md": 10150, "docs/specs/remote-api.md": 4700, "docs/specs/remote-security-model.md": 4750, - "docs/specs/security-audit.md": 1950, + "docs/specs/security-audit.md": 2000, "docs/specs/security-ci.md": 2700, "docs/specs/security-hosted.md": 600, "docs/specs/security-local.md": 3150, From 2ab4ad9f2e3dc053c64f69363b717944fb0188d7 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 22 Sep 2026 21:41:54 -0700 Subject: [PATCH 3/7] Key the rule on what no audit run can read, and keep Future for unbuilt work Applies dormouse-bot's three threads together. The rule and preamble now say "no audit run can read" instead of "outside the repository", so the GitHub state AUDIT_PAT reaches stays a check. ## Future holds an obligation only while its subject is unbuilt; a standing one is stated beside its rule. The preamble carries the promotion clause from hosted.md. security-ci.md's Hosted credential rule is split the same way: GitHub secret placement stays audited, provider token scope becomes a Must. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/audit/_preamble.md | 16 ++++++++++------ docs/specs/security-audit.md | 2 +- docs/specs/security-audit.rationale.md | 2 +- docs/specs/security-ci.md | 4 +++- scripts/spec-word-budgets.json | 2 +- 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/.github/audit/_preamble.md b/.github/audit/_preamble.md index 62b2e4324..49bfa0257 100644 --- a/.github/audit/_preamble.md +++ b/.github/audit/_preamble.md @@ -20,12 +20,16 @@ for a check you could not determine — a transient network error, or an area yo ran out of room to reach — and say which it was. It is never a substitute for a check you could have run. -A condition on state outside the repository — a provisioning step, an external -zone setting — is not a check you could not determine; it is not a check at -all. Verdict the `FAIL IF`'s condition on the repository's own state, and -record the external obligation as INFO — as for anything a spec stages under -`## Future`. `UNVERIFIABLE` there would make every -later run inconclusive too, because nothing a later run can read settles it. +A condition no audit run can read — a provisioning step, a setting in an +external service's console — is not a check you could not determine; it is not +a check at all. Verdict the `FAIL IF`'s readable condition, and record the +external obligation as INFO. `UNVERIFIABLE` there would make every later run +inconclusive too, because nothing a later run can read settles it. GitHub state +`AUDIT_PAT` reaches — rulesets, environments, secret placement, workflow +permissions — is readable: it stays a check, and `UNVERIFIABLE` stays right for +a call that fails. An obligation a spec stages under `## Future` is not a check +either; once it is promoted above the fold, audit it as a `FAIL IF` like any +other. Where `docs/specs/security.md` says a risk is accepted ("What is not defended") or a gap is known ("Known gaps"), do not re-report it as a finding — report diff --git a/docs/specs/security-audit.md b/docs/specs/security-audit.md index f952e046a..6ca52be14 100644 --- a/docs/specs/security-audit.md +++ b/docs/specs/security-audit.md @@ -79,7 +79,7 @@ Source of truth: `2. Wait without ending your turn`, `3. Merge`, and `4. The ver - **Partial has three shapes**, each named in the INCONCLUSIVE issue: `UNVERIFIABLE` for a check reached but not determined; `_Incomplete …_` above a fragment cut off mid-report; `_No report …_` for a domain that never wrote one. The merged `## Summary` may likewise read `INCONCLUSIVE`, and **gives no coverage count for a cut-off domain** (rationale). - **With no `audit-report.md` the reporting step publishes each fragment verbatim under its own heading**, unmerged (rationale). - **Must return `VERDICT: INCONCLUSIVE` from a domain with any undetermined check unless it found a failure.** Only all-determined passing checks permit `VERDICT: PASS`; a domain's inconclusive verdict prevents a merged pass. -- **Never write a `FAIL IF` condition on state outside the repository**: audit the in-repo half and stage the rest under `## Future` (rationale). +- **Never write a `FAIL IF` condition no audit run can read**: audit the readable half; stage the rest under `## Future` only while it is unbuilt, and otherwise state it beside the rule. `AUDIT_PAT`-readable GitHub state stays audited (rationale). - **`STATUS` is assigned in exactly two places**: where the status file is parsed, and in the single escalation block, **which orders `FAIL` > `MISSING` > `PASS`** — a dissent can raise `MISSING` to `FAIL` and never the reverse, and a `FAIL` alongside missing or unreadable fragments still reports them. - **The report is truncated to 32,000 characters before posting**, head kept, by `scripts/clamp-issue-body.mjs` (self-tested by `scripts/clamp-issue-body-selftest.mjs`). The call is non-fatal; the `audit-transcript` artifact holds the report in full; `.github/workflows/workflow-audit.yaml` truncates its commit list the same way (rationale). - **Every run uploads the `audit-transcript` artifact, which is world-readable and not secret-masked** — 14-day retention, deep-linked from failure issues (rationale). diff --git a/docs/specs/security-audit.rationale.md b/docs/specs/security-audit.rationale.md index ee60b22d7..9ccbb0515 100644 --- a/docs/specs/security-audit.rationale.md +++ b/docs/specs/security-audit.rationale.md @@ -62,7 +62,7 @@ Run 35205193090's `## Summary` also inverted the placeholder it was reading: "tw Collapsing the inconclusive case into `FAIL`, as the step originally did, filed an identical issue for "the repo is insecure" and "the auditor stopped early". -A `FAIL IF` condition on state outside the repository makes the verdict a coin flip, because no run can ever determine it. `security-hosted.md` carried two: the Cloudflare script-injection exclusion, which is a zone setting, and a closing activation sentence that stated its own answer. Run 35586089654 (2026-09-21) reached both as `UNVERIFIABLE` in its sub-auditors and its domain lead resolved both to PASS, on the ground that the audited condition was the in-repo half; run 35709640946 (2026-09-22) left both `UNVERIFIABLE`, so a pass with 375 PASS and 0 FAIL returned INCONCLUSIVE and held the release gate shut (issue #747). Nothing in the tree had changed between them. #757 staged both under `security-hosted.md`'s `## Future` and kept the in-repo half — the deploy's `preflight` gate — as a `FAIL IF`. +A `FAIL IF` condition no audit run can read makes the verdict a coin flip, because no run can ever determine it. `AUDIT_PAT`-readable GitHub state is not in that class: a failed call there is a real `UNVERIFIABLE`. `## Future` holds such an obligation only while its subject is unbuilt, since a staged item must eventually be promoted; a standing obligation on existing infrastructure is present-tense fact and stays beside its rule. `security-hosted.md` carried two: the Cloudflare script-injection exclusion, which is a zone setting, and a closing activation sentence that stated its own answer. Run 35586089654 (2026-09-21) reached both as `UNVERIFIABLE` in its sub-auditors and its domain lead resolved both to PASS, on the ground that the audited condition was the in-repo half; run 35709640946 (2026-09-22) left both `UNVERIFIABLE`, so a pass with 375 PASS and 0 FAIL returned INCONCLUSIVE and held the release gate shut (issue #747). Nothing in the tree had changed between them. #757 staged both under `security-hosted.md`'s `## Future` and kept the in-repo half — the deploy's `preflight` gate — as a `FAIL IF`. GitHub rejects an over-long issue body outright; that rejection lands on a `set -e` step *after* the verdict is decided, and the finding then reaches no issue and no comment — only a red run and an artifact that expires. Truncation keeps the head because that is where the verdict and the links are, and the clamp call is non-fatal so a failure of the helper cannot reopen the window it closes. diff --git a/docs/specs/security-ci.md b/docs/specs/security-ci.md index 6744139c9..5a7a323cf 100644 --- a/docs/specs/security-ci.md +++ b/docs/specs/security-ci.md @@ -87,7 +87,9 @@ Source of truth: `packageRules` in `.github/renovate.json`; `WINDOW` and `is_ten **Must keep Hosted credentials in dedicated environments.** `hosted-production` and `hosted-release-tag` admit only `main`; `hosted-preview` admits only `main` and `refs/pull/*/merge`. All require Ned or Edgar's review with administrator bypass disabled; self-review is allowed. Preview approval authorizes the PR code to receive test-resource credentials only. - **FAIL IF** a Hosted environment lacks those branch restrictions, required reviewers, or disabled administrator bypass; inspect all three environments and their deployment policies. -- **FAIL IF** Hosted credentials appear at repository/org scope, production credentials appear in `hosted-preview`, or preview credentials can reach production/TTR/marketing resources. Inspect GitHub secret placement and Cloudflare/Neon token scope; names alone do not isolate resources. +- **FAIL IF** Hosted credentials appear at repository/org scope, or production credentials appear in `hosted-preview`; inspect GitHub secret placement. + +**Must scope `hosted-preview`'s Cloudflare and Neon tokens away from production, TTR, and marketing resources** when issuing them; names do not isolate resources, and no audit run can read a token's scope. - **FAIL IF** `HOSTED_TAG_TOKEN` appears outside `hosted-release-tag`, or that environment is used by a job other than `tag` in `.github/workflows/hosted-production.yml`. Its admin identity's repository-scoped Contents-write PAT can write code and bypass tag protection; it must never enter a deployment job or PR execution. - **FAIL IF** a Hosted preview deploy accepts a fork or a failing verification, preview cleanup checks out a PR ref rather than `main`, or a Hosted production tag can run before live verification succeeds; inspect the workflow dependency/condition graph. diff --git a/scripts/spec-word-budgets.json b/scripts/spec-word-budgets.json index 7c2d069c1..9a385e2fe 100644 --- a/scripts/spec-word-budgets.json +++ b/scripts/spec-word-budgets.json @@ -19,7 +19,7 @@ "docs/specs/remote-api.md": 4700, "docs/specs/remote-security-model.md": 4750, "docs/specs/security-audit.md": 2000, - "docs/specs/security-ci.md": 2700, + "docs/specs/security-ci.md": 2750, "docs/specs/security-hosted.md": 600, "docs/specs/security-local.md": 3150, "docs/specs/security-remote.md": 5750, From dd8d9c322a52bf1e92dcf906403936d7b1d8c39f Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 22 Sep 2026 21:50:48 -0700 Subject: [PATCH 4/7] Name all three dispositions of an unreadable obligation in the preamble Takes dormouse-bot's preamble wording, drops hosted.md's now-duplicate promotion clause, and moves security-ci.md's token-scope Must below the rule list so the section keeps one shape. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/audit/_preamble.md | 17 +++++++++-------- .github/audit/hosted.md | 3 +-- docs/specs/security-ci.md | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/audit/_preamble.md b/.github/audit/_preamble.md index 49bfa0257..e36452dd8 100644 --- a/.github/audit/_preamble.md +++ b/.github/audit/_preamble.md @@ -22,14 +22,15 @@ check you could have run. A condition no audit run can read — a provisioning step, a setting in an external service's console — is not a check you could not determine; it is not -a check at all. Verdict the `FAIL IF`'s readable condition, and record the -external obligation as INFO. `UNVERIFIABLE` there would make every later run -inconclusive too, because nothing a later run can read settles it. GitHub state -`AUDIT_PAT` reaches — rulesets, environments, secret placement, workflow -permissions — is readable: it stays a check, and `UNVERIFIABLE` stays right for -a call that fails. An obligation a spec stages under `## Future` is not a check -either; once it is promoted above the fold, audit it as a `FAIL IF` like any -other. +a check at all. Where one is written into a `FAIL IF`, verdict that rule's +readable condition; where a spec states the obligation beside its rule or +stages it under `## Future`, there is no rule to verdict. Either way, record +the obligation as INFO. `UNVERIFIABLE` would make every later run inconclusive +too, because nothing a later run can read settles it. GitHub state `AUDIT_PAT` +reaches — rulesets, environments, secret placement, workflow permissions — is +readable: it stays a check, and `UNVERIFIABLE` stays right for a call that +fails. Once a staged obligation is promoted above the fold, audit it as a +`FAIL IF` like any other. Where `docs/specs/security.md` says a risk is accepted ("What is not defended") or a gap is known ("Known gaps"), do not re-report it as a finding — report diff --git a/.github/audit/hosted.md b/.github/audit/hosted.md index 99931b808..219711275 100644 --- a/.github/audit/hosted.md +++ b/.github/audit/hosted.md @@ -38,8 +38,7 @@ code from pending production configuration; do not treat local provider simulations as live OAuth acceptance, and treat a checked-in placeholder as no evidence about an external control. Production activation is staged under the spec's `## Future`: while it sits below the fold there is no check here, so -report its state as INFO under `### Qualitative findings`. Once it is promoted -above the fold, audit it as a `FAIL IF` like any other. +report its state as INFO under `### Qualitative findings`. ## Qualitative pass diff --git a/docs/specs/security-ci.md b/docs/specs/security-ci.md index 5a7a323cf..47e43ac0c 100644 --- a/docs/specs/security-ci.md +++ b/docs/specs/security-ci.md @@ -88,11 +88,11 @@ Source of truth: `packageRules` in `.github/renovate.json`; `WINDOW` and `is_ten - **FAIL IF** a Hosted environment lacks those branch restrictions, required reviewers, or disabled administrator bypass; inspect all three environments and their deployment policies. - **FAIL IF** Hosted credentials appear at repository/org scope, or production credentials appear in `hosted-preview`; inspect GitHub secret placement. - -**Must scope `hosted-preview`'s Cloudflare and Neon tokens away from production, TTR, and marketing resources** when issuing them; names do not isolate resources, and no audit run can read a token's scope. - **FAIL IF** `HOSTED_TAG_TOKEN` appears outside `hosted-release-tag`, or that environment is used by a job other than `tag` in `.github/workflows/hosted-production.yml`. Its admin identity's repository-scoped Contents-write PAT can write code and bypass tag protection; it must never enter a deployment job or PR execution. - **FAIL IF** a Hosted preview deploy accepts a fork or a failing verification, preview cleanup checks out a PR ref rather than `main`, or a Hosted production tag can run before live verification succeeds; inspect the workflow dependency/condition graph. +**Must scope `hosted-preview`'s Cloudflare and Neon tokens away from production, TTR, and marketing resources** when issuing them; names do not isolate resources, and no audit run can read a token's scope. + Source of truth: `hosted/scripts/setup-github.mjs`; `.github/workflows/hosted-preview.yml`; `.github/workflows/hosted-production.yml`. ## VS Code Extension Releases From 52eabbd37138482d66a19a7b1b0ab5759457e384 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 22 Sep 2026 21:57:56 -0700 Subject: [PATCH 5/7] Restate the unreadable-condition rule as a list of dispositions The paragraph drew a new finding on four consecutive pushes. As a short list it states each disposition once, keeps UNVERIFIABLE scoped to the unreadable case, and defers failed calls on AUDIT_PAT state to the paragraph above instead of restating it. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/audit/_preamble.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/.github/audit/_preamble.md b/.github/audit/_preamble.md index e36452dd8..eef0837f2 100644 --- a/.github/audit/_preamble.md +++ b/.github/audit/_preamble.md @@ -21,16 +21,18 @@ ran out of room to reach — and say which it was. It is never a substitute for check you could have run. A condition no audit run can read — a provisioning step, a setting in an -external service's console — is not a check you could not determine; it is not -a check at all. Where one is written into a `FAIL IF`, verdict that rule's -readable condition; where a spec states the obligation beside its rule or -stages it under `## Future`, there is no rule to verdict. Either way, record -the obligation as INFO. `UNVERIFIABLE` would make every later run inconclusive -too, because nothing a later run can read settles it. GitHub state `AUDIT_PAT` -reaches — rulesets, environments, secret placement, workflow permissions — is -readable: it stays a check, and `UNVERIFIABLE` stays right for a call that -fails. Once a staged obligation is promoted above the fold, audit it as a -`FAIL IF` like any other. +external service's console — is not a check. Record it as INFO, never as +`UNVERIFIABLE`: nothing a later run can read would settle it, so every later +run would be inconclusive too. + +- Written into a `FAIL IF`: verdict only that rule's readable condition. +- Stated beside a rule, or staged under `## Future`: there is no rule to + verdict. +- Promoted above the fold: it is a `FAIL IF` like any other. + +GitHub state `AUDIT_PAT` reaches — rulesets, environments, secret placement, +workflow permissions — is readable, so it is always a check, and the paragraph +above governs a call that fails. Where `docs/specs/security.md` says a risk is accepted ("What is not defended") or a gap is known ("Known gaps"), do not re-report it as a finding — report From 08839cd6281f0d4febee0be4402cb4316865d11d Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 22 Sep 2026 22:03:41 -0700 Subject: [PATCH 6/7] Audit a promoted obligation as written, and name UNVERIFIABLE for failed calls Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/audit/_preamble.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/audit/_preamble.md b/.github/audit/_preamble.md index eef0837f2..fd6c6a582 100644 --- a/.github/audit/_preamble.md +++ b/.github/audit/_preamble.md @@ -28,11 +28,12 @@ run would be inconclusive too. - Written into a `FAIL IF`: verdict only that rule's readable condition. - Stated beside a rule, or staged under `## Future`: there is no rule to verdict. -- Promoted above the fold: it is a `FAIL IF` like any other. +- Promoted above the fold: audit it as the promoted rule is written — a + readable condition as a `FAIL IF`, an unreadable one by the bullets above. GitHub state `AUDIT_PAT` reaches — rulesets, environments, secret placement, -workflow permissions — is readable, so it is always a check, and the paragraph -above governs a call that fails. +workflow permissions — is readable, so it is always a check, and +`UNVERIFIABLE` stays right for a call that fails. Where `docs/specs/security.md` says a risk is accepted ("What is not defended") or a gap is known ("Known gaps"), do not re-report it as a finding — report From 15c11da23064f215e8abc9922388e489bd7978ea Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 22 Sep 2026 22:39:49 -0700 Subject: [PATCH 7/7] Drop the TTR references and the provider-token isolation rule TTR is an unrelated project; nothing in Dormouse should name it. The token-scope Must and the marketing-separated deployment identity were obligations no audit can read and no one asked for; GitHub-side secret placement stays audited. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/specs/security-ci.md | 2 -- hosted/README.md | 23 ++++++++--------------- scripts/spec-word-budgets.json | 2 +- 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/docs/specs/security-ci.md b/docs/specs/security-ci.md index 47e43ac0c..88f31a79c 100644 --- a/docs/specs/security-ci.md +++ b/docs/specs/security-ci.md @@ -91,8 +91,6 @@ Source of truth: `packageRules` in `.github/renovate.json`; `WINDOW` and `is_ten - **FAIL IF** `HOSTED_TAG_TOKEN` appears outside `hosted-release-tag`, or that environment is used by a job other than `tag` in `.github/workflows/hosted-production.yml`. Its admin identity's repository-scoped Contents-write PAT can write code and bypass tag protection; it must never enter a deployment job or PR execution. - **FAIL IF** a Hosted preview deploy accepts a fork or a failing verification, preview cleanup checks out a PR ref rather than `main`, or a Hosted production tag can run before live verification succeeds; inspect the workflow dependency/condition graph. -**Must scope `hosted-preview`'s Cloudflare and Neon tokens away from production, TTR, and marketing resources** when issuing them; names do not isolate resources, and no audit run can read a token's scope. - Source of truth: `hosted/scripts/setup-github.mjs`; `.github/workflows/hosted-preview.yml`; `.github/workflows/hosted-production.yml`. ## VS Code Extension Releases diff --git a/hosted/README.md b/hosted/README.md index e54c40e4c..03730e3ec 100644 --- a/hosted/README.md +++ b/hosted/README.md @@ -72,11 +72,8 @@ revision before a production release. | Recovery | Neon backups/PITR enabled, encrypted pre-migration dumps retained as GitHub artifacts for 30 days, age identity also retained independently in a password manager | Cloudflare Workers Scripts and Hyperdrive permissions are account-scoped, so -previews need their own test account. Production deployment isolation likewise -requires a boundary marketing's existing credentials cannot reach; coordinate -the hostname/zone placement before choosing an account. Do not reuse TTR's Neon -project, mail token, or OAuth registrations. `docs/specs/security-ci.md` -> -"Hosted Deployments" owns the credential isolation the audit checks. +previews need their own test account. `docs/specs/security-ci.md` -> "Hosted +Deployments" owns the credential placement the audit checks. ## GitHub setup @@ -142,11 +139,11 @@ back from GitHub; retain independent copies in your password manager. ## Provision the production boundary Use dedicated Dormouse resources in the existing Cloudflare, Neon, and Postmark -accounts. Do not reuse TTR's database, mail server/token, or OAuth registrations. +accounts. -1. Create a dedicated Dormouse production Postgres database (Neon is the TTR - precedent) on PostgreSQL 17; the backup/restore tooling pins PostgreSQL - 17.11. Keep TTR, development, and previews separate. Enable backups and a +1. Create a dedicated Dormouse production Postgres database on Neon, on + PostgreSQL 17; the backup/restore tooling pins PostgreSQL + 17.11. Keep development and previews separate. Enable backups and a suitable PITR window, and verify a restore into a separate database before accepting real accounts. 2. Create a Cloudflare Hyperdrive configuration for that database with **query @@ -166,10 +163,7 @@ accounts. Do not reuse TTR's database, mail server/token, or OAuth registrations `signin@hosted.dormouse.sh` (or update `EMAIL_FROM`). Configure SPF/DKIM and DMARC. Register the sender with Apple Private Email Relay for relay-address delivery. -5. Use a deployment identity separate from marketing, with access limited to - the Hosted deployment resources. If a Cloudflare account token cannot express - that isolation, use a separate account/deployment boundary. -6. Configure `hosted.dormouse.sh` as the Worker's custom domain. Exclude this +5. Configure `hosted.dormouse.sh` as the Worker's custom domain. Exclude this hostname from Cloudflare Web Analytics, Zaraz, and other script injection or rewriting rules. Disable account API caching. Keep `workers_dev` and public preview URLs disabled. @@ -182,8 +176,7 @@ keychain. Authenticate in your own terminal; account/provider sign-in is operato ## Separate OAuth registrations -Create Dormouse registrations; do not reuse TTR credentials or replace TTR's -callbacks. Register these exact URLs with no trailing slash: +Create Dormouse registrations. Register these exact URLs with no trailing slash: | Provider | Registration | Callback | | --- | --- | --- | diff --git a/scripts/spec-word-budgets.json b/scripts/spec-word-budgets.json index 9a385e2fe..7c2d069c1 100644 --- a/scripts/spec-word-budgets.json +++ b/scripts/spec-word-budgets.json @@ -19,7 +19,7 @@ "docs/specs/remote-api.md": 4700, "docs/specs/remote-security-model.md": 4750, "docs/specs/security-audit.md": 2000, - "docs/specs/security-ci.md": 2750, + "docs/specs/security-ci.md": 2700, "docs/specs/security-hosted.md": 600, "docs/specs/security-local.md": 3150, "docs/specs/security-remote.md": 5750,