Procházet zdrojové kódy

Tighten visual companion hardening spec

Drew Ritter před 3 měsíci
rodič
revize
92a0a7acc0

+ 89 - 18
docs/superpowers/specs/2026-06-11-visual-companion-final-hardening-fixup-design.md

@@ -41,6 +41,21 @@ The final review pass found five remaining issues:
 - Do not add backward compatibility for stale stop-server PID files unless Drew
 - Do not add backward compatibility for stale stop-server PID files unless Drew
   explicitly approves that tradeoff.
   explicitly approves that tradeoff.
 
 
+## Inherited Security Invariants
+
+This fixup preserves the auth hardening already designed and implemented:
+
+- `.last-token` and `state/server-info` remain sensitive owner-only state.
+- Fallback tokens may appear in startup JSON and `state/server-info`, but must
+  not be written to `.last-token`.
+- Cookies remain port-named, `HttpOnly`, `SameSite=Strict`, and scoped to `/`.
+- WebSocket upgrades still require a valid key or cookie.
+- WebSocket `Origin` checks remain enforced when the browser supplies an
+  `Origin` header.
+- Direct no-`Origin` clients remain allowed only when they carry the session key.
+- Generated same-origin screen JavaScript and future same-origin vendored
+  libraries are trusted. Sandboxing malicious screen HTML remains deferred.
+
 ## Design
 ## Design
 
 
 ### 1. Rebase Onto Current `dev`
 ### 1. Rebase Onto Current `dev`
@@ -51,8 +66,12 @@ work. Resolve the `evals` submodule conflict by taking `dev`.
 After the rebase:
 After the rebase:
 
 
 - `evals` must not appear in the PR diff.
 - `evals` must not appear in the PR diff.
-- PR #1720 can still mention eval evidence that was run elsewhere.
+- PR #1720 can still mention eval evidence that was run elsewhere, but it must
+  include exact external evidence: eval repo commit, scenario path, command,
+  result artifact path or id, and RED/GREEN outcome.
 - The PR body must not imply the evals submodule bump is part of this PR.
 - The PR body must not imply the evals submodule bump is part of this PR.
+- Any earlier PR-body text or comment implying the submodule bump is included
+  must be superseded by the final PR-body evidence.
 
 
 ### 2. Root Screen Containment
 ### 2. Root Screen Containment
 
 
@@ -67,9 +86,11 @@ one when the platform reports link counts.
 Expected behavior:
 Expected behavior:
 
 
 - A symlink under `content/` pointing outside `content/` is ignored.
 - A symlink under `content/` pointing outside `content/` is ignored.
-- A hardlink under `content/` to a file outside `content/` is ignored when the
-  platform exposes enough metadata to detect it.
+- A hardlink under `content/` to `state/server-info` is ignored when
+  `fs.linkSync` succeeds and `lstat.nlink > 1`.
 - If no safe screen file remains, the waiting page is served.
 - If no safe screen file remains, the waiting page is served.
+- Existing `/files/*` containment behavior remains unchanged: empty names,
+  dotfiles, symlinks, hardlinks, and directories still return 404.
 
 
 ### 3. Fallback Token Isolation
 ### 3. Fallback Token Isolation
 
 
@@ -78,7 +99,9 @@ Port fallback must not reuse a token loaded from persisted `.last-token`.
 Token source should be explicit in code:
 Token source should be explicit in code:
 
 
 - `BRAINSTORM_TOKEN` from the environment is an intentional operator/test
 - `BRAINSTORM_TOKEN` from the environment is an intentional operator/test
-  override and remains unchanged on fallback.
+  override. If the preferred port is occupied while an explicit environment
+  token is set, the server must fail closed instead of falling back, because the
+  occupied server may be using the same explicit token.
 - `.last-token` is persisted state for same-port reconnect convenience. If the
 - `.last-token` is persisted state for same-port reconnect convenience. If the
   server falls back because the preferred port is occupied, discard that loaded
   server falls back because the preferred port is occupied, discard that loaded
   token and generate a fresh unpersisted token for the fallback process.
   token and generate a fresh unpersisted token for the fallback process.
@@ -94,16 +117,23 @@ The fallback server must continue to avoid overwriting `.last-port` and
 Node as an inert command-line argument, for example:
 Node as an inert command-line argument, for example:
 
 
 ```text
 ```text
---brainstorm-server-id=<opaque-id>
+node server.cjs --brainstorm-server-id=<id>
 ```
 ```
 
 
 The id is not an auth credential. It is only process-ownership evidence for the
 The id is not an auth credential. It is only process-ownership evidence for the
 local lifecycle scripts. `server.cjs` can ignore the argument.
 local lifecycle scripts. `server.cjs` can ignore the argument.
 
 
+The id must use a shell/MSYS-safe alphabet, such as
+`^[A-Za-z0-9_-]{32,64}$`. Store it in `state/server-instance-id` with
+owner-only permissions.
+
 `stop-server.sh` should read the expected id from state and only signal the PID
 `stop-server.sh` should read the expected id from state and only signal the PID
-when the target process command line contains the exact id argument. Existing
-port-to-PID checks may remain as additional evidence, but they should not be the
-only path that permits killing an ambiguous process.
+when the target process argv contains the exact argument
+`--brainstorm-server-id=<id>` as a full argv token, not as a loose substring.
+Prefer `/proc/<pid>/cmdline` when available, then fall back to wide `ps` output.
+A matching instance id is sufficient proof even when `server-info` is missing
+or `lsof` is unavailable. Existing port-to-PID checks may remain as additional
+evidence.
 
 
 Fail closed when ownership cannot be proven:
 Fail closed when ownership cannot be proven:
 
 
@@ -116,6 +146,18 @@ Fail closed when ownership cannot be proven:
 This intentionally prefers leaving a stale process running over killing an
 This intentionally prefers leaving a stale process running over killing an
 unrelated process.
 unrelated process.
 
 
+Operator-visible outcomes should be explicit:
+
+- missing PID file returns `not_running`
+- missing or malformed server id returns `stale_pid`
+- unavailable command line returns `stale_pid`
+- wrong or absent argv id returns `stale_pid`
+- successful stop returns `stopped`
+
+On `stale_pid` and `stopped` outcomes, remove `server.pid` and
+`server-instance-id` so future stop attempts do not keep targeting the same
+ambiguous process. Do not remove persistent session content.
+
 ### 5. Test Hardening
 ### 5. Test Hardening
 
 
 The test pass should be deterministic across macOS and the Windows Git Bash host
 The test pass should be deterministic across macOS and the Windows Git Bash host
@@ -131,6 +173,20 @@ Required changes:
   assertion when the host cannot create usable test symlinks.
   assertion when the host cannot create usable test symlinks.
 - Tests that create impostor processes must assert that the impostor survives
 - Tests that create impostor processes must assert that the impostor survives
   when lifecycle metadata is missing or insufficient.
   when lifecycle metadata is missing or insufficient.
+- Windows/MSYS start-server tests must assert that Windows-like detection still
+  clears `BRAINSTORM_OWNER_PID`, still auto-foregrounds when appropriate, and
+  still passes the instance-id argv exactly.
+
+### 6. Docs And PR Consistency
+
+Before Jesse reviews, reconcile reviewer-visible docs and PR metadata:
+
+- Update the issue catalog so dispositions match what this PR actually ships.
+- Keep auto-open docs consistent with the implemented `--open` behavior.
+- Keep the documented default idle timeout at 4 hours everywhere.
+- Review the PR body against the template after the rebase.
+- Record macOS, Windows, browser/manual, and external eval evidence in the PR
+  body with concrete commands and results.
 
 
 ## Testing Strategy
 ## Testing Strategy
 
 
@@ -144,20 +200,31 @@ Use TDD for each behavior change:
 
 
 Required focused regressions:
 Required focused regressions:
 
 
-- root screen symlink escape is not served
-- root screen hardlink escape is not served when the platform supports the check
-- fallback after occupied preferred port uses a token different from the
-  persisted preferred-port token
-- fallback token does not authenticate to the original preferred-port server
-- `stop-server.sh` does not kill an impostor when `server-info` or the new
-  server id proof is missing
-- fixed-port tests fail clearly if fallback occurs unexpectedly
-- shell cleanup traps reap all background children on failure
+| Behavior | Test File | Focused Command | Expected RED | Expected GREEN |
+| --- | --- | --- | --- | --- |
+| Root route ignores symlink escape | `tests/brainstorm-server/server.test.js` | `node tests/brainstorm-server/server.test.js` | authenticated `GET /` serves linked outside content | response serves waiting page or safe screen |
+| Root route ignores supported hardlink escape | `tests/brainstorm-server/server.test.js` | `node tests/brainstorm-server/server.test.js` | authenticated `GET /` serves hardlinked `server-info` | hardlink candidate is ignored when `nlink > 1` |
+| `/files/*` containment stays unchanged | `tests/brainstorm-server/server.test.js` | `node tests/brainstorm-server/server.test.js` | existing containment test regresses | empty, dotfile, directory, symlink, hardlink cases remain 404 |
+| Persisted-token fallback rotates token | `tests/brainstorm-server/lifecycle.test.js` | `node tests/brainstorm-server/lifecycle.test.js` | fallback URL key equals persisted preferred-port key | fallback URL key differs and is not written to `.last-token` |
+| Explicit-token fallback fails closed | `tests/brainstorm-server/lifecycle.test.js` | `node tests/brainstorm-server/lifecycle.test.js` | server falls back while `BRAINSTORM_TOKEN` is set | process exits non-zero and does not start fallback |
+| Fallback key cannot authenticate to original server | `tests/brainstorm-server/lifecycle.test.js` | `node tests/brainstorm-server/lifecycle.test.js` | fallback key receives 200 from original port | original port rejects fallback key |
+| Correct instance id permits stop | `tests/brainstorm-server/stop-server.test.sh` | `bash tests/brainstorm-server/stop-server.test.sh` | real start-server-launched server survives | stop returns `stopped` and process exits |
+| Wrong, missing, malformed, or stale id is safe | `tests/brainstorm-server/stop-server.test.sh` | `bash tests/brainstorm-server/stop-server.test.sh` | impostor is signaled | stop returns `stale_pid` and impostor survives |
+| Fixed-port suites cannot pass through fallback | `tests/brainstorm-server/server.test.js`, `tests/brainstorm-server/auth.test.js` | respective `node` commands | test silently talks to fallback port | test fails clearly or uses reported port intentionally |
+| Shell cleanup traps run on failures | `tests/brainstorm-server/stop-server.test.sh` | `bash tests/brainstorm-server/stop-server.test.sh` | failure leaves child processes | trap reaps background children |
+| Windows/MSYS start behavior keeps lifecycle invariants | `tests/brainstorm-server/start-server.test.sh`, `tests/brainstorm-server/windows-lifecycle.test.sh` | `bash` test commands on macOS and `ballmer` | owner PID or argv handling regresses | owner PID is cleared, foreground detection holds, id argv is present |
+
+Each RED/GREEN cycle should leave a short evidence note for the PR body: focused
+command, failing assertion before the fix, passing assertion after the fix, and
+whether the evidence was gathered on macOS or Windows.
 
 
 ## Verification
 ## Verification
 
 
 Before calling the fixup complete, run:
 Before calling the fixup complete, run:
 
 
+- `git fetch origin dev && git rebase origin/dev`
+- `git diff --quiet origin/dev...HEAD -- evals`
+- `gh pr view 1720 --json mergeStateStatus,statusCheckRollup,headRefOid`
 - `cd tests/brainstorm-server && npm test`
 - `cd tests/brainstorm-server && npm test`
 - relevant focused test commands used during TDD
 - relevant focused test commands used during TDD
 - `git diff --check`
 - `git diff --check`
@@ -174,10 +241,14 @@ Manual/browser testing comes only after the automated pass is green.
 - `evals` is absent from the PR diff.
 - `evals` is absent from the PR diff.
 - Root screen serving cannot read outside `content/` through symlink or
 - Root screen serving cannot read outside `content/` through symlink or
   supported hardlink escapes.
   supported hardlink escapes.
-- A fallback server does not share a persisted token with the occupied
+- `/files/*` containment protections remain unchanged.
+- No fallback server runs with a token that may be shared with the occupied
   preferred-port server.
   preferred-port server.
 - `stop-server.sh` does not signal unrelated processes when ownership proof is
 - `stop-server.sh` does not signal unrelated processes when ownership proof is
   missing or ambiguous.
   missing or ambiguous.
+- `stop-server.sh` can still stop a legitimate server with a matching instance
+  id when `server-info` or `lsof` is unavailable.
+- Focused RED/GREEN evidence is recorded for each regression.
 - macOS and Windows validation evidence is recorded in the PR body.
 - macOS and Windows validation evidence is recorded in the PR body.
 - The PR body accurately describes what is in the branch and what evidence was
 - The PR body accurately describes what is in the branch and what evidence was
   gathered externally.
   gathered externally.