Conversation
…ionale Fixes inaccuracies the spec audit found in the security group, and moves evidence out of the specs that had no headroom left for the corrections. Corrections - security-local.md's persisted-state FAIL IF named "three callers" of `write_file_atomically`; there are five. It now says every caller and tells the auditor to enumerate from the file. - security-audit.md's Domains FAIL IF quantified `.github/audit/` and `.vscode/` as outside *every* consumer of the workflow-audit diff window, which reads as "absent from all" and so never fires on the failure it exists to catch — widening one consumer without the others. Inverted to *any*. The rule is latent today (all four consumers derive from one WINDOW array), but it was an audited rule that checked nothing. - security-remote.md and SELF_HOST.md claimed deploy-lint is the only automated signal about the Windows installer; ps1-cmdlet-lint reads the same file. - security.md said `recovery.json` is written at the umask. It is written 0600 under a 0700 directory; the real residual is Windows, where the mode is a no-op and only standalone applies a DACL. - security.md's Persisted state and Desktop Releases rows said `audit`; both are pinned on every `pnpm test` (`cargo test`, sign-and-deploy.test.mjs). security-ci.md now names the same test. - security-local.md's Settings-Sync FAIL IF said the rule was kept by a call not existing; a test scans for it. - security-ci.md's `pull_request_target` rule now quantifies over an `on:` trigger, so the comment in hosted-preview.yml is not a false FAIL. - security-audit.md now names `BASH_DEFAULT_TIMEOUT_MS`, states the monotone-upward issue-title rule, says `claude_args` rather than `--agents`, and drops the false "disjoint scopes" claim. - SECURITY.md named five audited checklists and omitted security-hosted.md; it now points at security.md's Domains table. Same fix on the website page. - hosted.md: root `pnpm test` runs the deploy suites, and archive hashes are `verifyPackages`, not `preflight`. security-hosted.md gains its `Pinned by`. - SELF_HOST.md: `setup-password.json` added to the state tree, the stale dev-relay port claim corrected, and the Windows kill block no longer selects by image name, which its own trap forbids. Prompts - supply-chain.md now carries the `GH_TOKEN=$AUDIT_PAT` instruction its secret-scanning and Dependabot checks need; without it both read as absent. - application-security.md gains the Hosted paths security-hosted.md requires and Hosted bullets for its qualitative pass. Lint - A root-level spec now pairs with a root-level rationale, so check 15 covers SELF_HOST.md (6,036 words, previously exempt) and check 11 sees its markers. - New check 17: a `**FAIL IF**` rule leads a line only in a security spec, so an audited rule cannot be written where no domain claims it. - Both carry self-test cases. Condensation - SELF_HOST.rationale.md is new; the Invariants, mechanism-map and traps narratives move into it or point at the code comment that already holds them, and the Installer contract points at security-remote.md rather than reproducing it. - security-remote.md, security-supply-chain.md, security.md, security-local.md and security-audit.md drop restatements their rationale files or the audit prompts already carry. Budgets ratcheted down for SELF_HOST.md (6200→6050), security-remote.md (5850→5750) and security-supply-chain.md (1250→1150). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…o values Two things SELF_HOST.md's "Definition of done" states for all three platforms were checked on fewer than three, so `manage verify` reported healthy on an install that was not. - **A dangling `previous` pointer.** Only Linux failed it. macOS and Windows compared `previous` to `current` and stopped, so a pointer at a pruned release passed green and `manage rollback` then failed on a release that was not on disk. Both now fail it, as Linux does. - **A credential in the service definition.** Nothing inspected the plist, the unit, the scheduled-task XML, or the `run-relay` wrapper for one: a plist carrying `DORMOUSE_SETUP_PASSWORD` passed verify. All three now search the definition and the wrapper for every credential name the installer knows, exempting the enrollment offer's `_FILE` path, which they legitimately carry. Both are deploy-lint rules with per-platform patterns, so deleting the control from any one installer fails the build, and the self-test's mutation proves each is load-bearing (112 -> 118 checks). Also: - `manage verify`'s "Serve does not proxy" failure prints the `manage serve` command that re-applies the mapping, on all three. - The audit workflow parses the status file into `FILE_STATUS` and assigns `STATUS` from it once, deleting `STATUS_FILE_VERDICT` — a derived variable with one reader that existed only because `STATUS` was mutated in place. - The Hosted account screen's recent-login window was a `600_000` literal beside the packed adapter's `freshAge: 10 * 60`; a pgstencil bump would have desynced the UI gate from the server gate, visible as a "connect" button that always 302s to `/login?error=`. One `LOGIN_FRESH_AGE_MS` in `hosted/server/policy.ts` now, pinned against the adapter's own built options by `hosted/server/tests/policy.test.ts`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Deploying mouseterm with
|
| Latest commit: |
eb1604a
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://5983d880.mouseterm.pages.dev |
| Branch Preview URL: | https://specs-security-audit.mouseterm.pages.dev |
dormouse-bot
left a comment
There was a problem hiding this comment.
Feedback on work in progress, not a merge verdict — mark it ready when you want the full review.
Two issues in the new manage verify credential check, and one note on the Hosted constant, inline. The spec compression itself reads correctly where I sampled it against the rationale files and the code.
Standalone Smoketest is red on this head; the run is still in progress, so its log isn't readable yet.
| # rest"). A name, not a value: the installer supplies none of these, so one | ||
| # appearing means a hand-edit or a regression put a credential where any | ||
| # process that can read the definition can read it. | ||
| if (("$taskXml" + "`n" + $wrapperText) -match 'DORMOUSE_SETUP_PASSWORD|DORMOUSE_VAPID_PRIVATE_KEY|DORMOUSE_ENROLL_TOKEN(?!_FILE)') { |
There was a problem hiding this comment.
The check reports a pass for a definition it never read. $taskXml comes back $null whenever Export-ScheduledTask fails — the three cases the comment at its assignment already names — and "$null" is the empty string, so the -match cannot fire and this prints Pass "the service definition names no credential". $wrapperText has the same shape: it stays '' when bin\run-relay.ps1 is missing. The file's other $taskXml consumer guards for exactly this, with if (-not $taskXml) { Fail "the task definition could not be exported -- it was not searched for the source checkout" }.
The two shells land in the same place by a different route: grep -q exits 2 when a named file is absent, which is neither 0 nor a match, so a missing $PLIST, $UNIT_FILE or $ROOT/bin/run-relay takes the else and green-ticks the check. (grep -qE PAT present.txt missing.txt exits 2 when present.txt doesn't match.) SELF_HOST.md -> "Definition of done" states the opposite two bullets above this one: "a definition it cannot read at all fails rather than passes".
Failing first on unreadable inputs restores that — -not $taskXml -or -not $wrapperText here, and an [ -r "$PLIST" ] && [ -r "$ROOT/bin/run-relay" ] arm on unix.
| # rest"). A name, not a value: the installer supplies none of these, so one | ||
| # appearing means a hand-edit or a regression put a credential where any | ||
| # process that can read the definition can read it. | ||
| if grep -qE 'DORMOUSE_SETUP_PASSWORD|DORMOUSE_VAPID_PRIVATE_KEY|DORMOUSE_ENROLL_TOKEN[^_]' \ |
There was a problem hiding this comment.
DORMOUSE_ENROLL_TOKEN[^_] requires a character after the name on the same line, so a bare export DORMOUSE_ENROLL_TOKEN at end of line — inheriting the value from the environment, which is a shape a wrapper edit can plausibly take — doesn't match: printf 'export DORMOUSE_ENROLL_TOKEN\n' | grep -qE 'DORMOUSE_ENROLL_TOKEN[^_]' exits 1. The Windows (?!_FILE) lookahead does catch it, so that hand-edit fails verify on Windows and passes on macOS and Linux — the divergence SELF_HOST.md -> "Installer contract (maintainers)" calls a finding ("Those lines bind all three installers — a control present in one and absent from another is a finding").
An end-anchored alternative closes it in ERE on both shells:
'DORMOUSE_SETUP_PASSWORD|DORMOUSE_VAPID_PRIVATE_KEY|DORMOUSE_ENROLL_TOKEN[^_]|DORMOUSE_ENROLL_TOKEN$'
Not posted as a one-click suggestion because scripts/deploy-lint.mjs's two unix patterns pin this string literally — they need the same edit in the same commit, or pnpm lint:deploy goes red. Same line in deploy/local/install-macos.sh.
| type Provider, | ||
| type Session, | ||
| } from "./api"; | ||
| import { LOGIN_FRESH_AGE_MS } from "../server/policy"; |
There was a problem hiding this comment.
Worth weighing: this pulls hosted/server/policy.ts into the frontend import graph, and with it providerBindings — the function that reads the OAuth client secrets out of env. Rollup should drop it, but docs/specs/security-hosted.md -> "Origin boundary" has an auditor "inspect the frontend import graph", and the comment above the providerIds re-export in policy.ts says the allowlist lives in ./providers.js so the frontend can import it "without this module's dependencies" — which this import walks back.
A module holding only LOGIN_FRESH_AGE_MS, imported by both policy.ts and App.tsx, keeps the single source of truth (and the new freshAge test) without widening that graph.
Spec audit, group 7 (the
security*.mdspecs,SECURITY.md,SELF_HOST.md, the.github/audit/prompts). Stacked on #714 (the spec-audit PRs merge in order #708 → #715); independent of the alert series. NoFAIL IFcondition was deleted; several were corrected.Call-outs
security-audit.md's DomainsFAIL IFsaid.github/audit//.vscode/must be outside every consumer of the workflow-audit window, which could never fire on the failure its own rationale names (one consumer widened without the others). Now any.manage verifygaps: a danglingpreviouspointer failed only on Linux (macOS/Windows green-ticked a pruned rollback target), and no platform inspected the service definition or wrapper for a credential. Both are now checks in all three installers withdeploy-lintrules and self-test mutations (112 → 118 checks). The Windows branch is lint-read only — no PowerShell on this machine.supply-chainaudit prompt never used$AUDIT_PAT, so its admin-scopedFAIL IFwould readnulland record a false FAIL; fixed. Theapplication-securityprompt now carries the Hosted guidance its 11 claimed rules need.Also:
recovery.jsonis 0600/0700, not "written at the umask" (Windows is the real residual); the ten-minute Bash cap is now tied toBASH_DEFAULT_TIMEOUT_MS;pull_request_targetscoped to anon:trigger; the monotone-upward issue-title rule stated; two unpinned claims now name their tests;SECURITY.mdpoints at the Domains table instead of listing five of six audited specs; spec-lint gains check 17 (FAIL IFonly insecurity*.md) and check 15 now covers root specs, both self-tested;SELF_HOST.mdgets its rationale file and loses ≈100 words of restatedsecurity-remote.mdrules; the Hosted freshness window is one constant pinned against the server gate.Budgets: SELF_HOST 6200→6100, security-remote 5850→5750, security-supply-chain 1250→1150.
Deferred (owner decisions or larger refactors): one release-pointer mechanism, one sentinel predicate for the audit fragments, one report merge,
manage statusvsverify, the fixedPORTkey, the generate-deps override tables.🤖 Generated with Claude Code