Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ A mouse-friendly multitasking terminal built with pnpm, react, typescript, vite,

```
pnpm install # install deps
pnpm build # build lib, vscode extension, Pocket, and website
pnpm build # build lib, vscode extension, Pocket, website, and Hosted
```

**Inside Dormouse, run `innerdogfood`** — `dor tool innerdogfood`.
Expand All @@ -31,7 +31,7 @@ The Tool shows the harness in its own pane and prints the command to drive it
- **`vscode-ext/`** — VS Code extension wrapping the lib in a webview (esbuild; node-pty via forked child process; direct-path WebRTC via node-datachannel, every platform's addon in one VSIX)
- **`website/`** — Marketing site (Vite) bundling part of the lib as an interactive demo on `FakePtyAdapter`
- **`relay/`** — Selfhost coordinating Relay for remote control (Hono): accounts + passkey auth in local JSON files (no database), WebSocket routing between Clients and Burrows, serves the built Pocket app
- **`hosted/`** — Hosted's three Hono Workers: account and Better Auth (`hosted.dormouse.sh`), one-time rendezvous (`relay.`), voice (`voice.`); Postgres.
- **`hosted/`** — Hosted's three Hono Workers: account and Better Auth (`hosted.dormouse.sh`), account-scoped Relay/Pocket and one-time rendezvous (`relay.`), voice (`voice.`); Postgres.
- **`dor/`** — The `dor` CLI (stricli) staged onto the `PATH` of every Dormouse-launched terminal; talks to its host over a private control socket
- **`remote-lib-common/`** — Security primitives + remote wire contract shared by `relay`, the Burrow module in `lib`, and the Pocket app (bare ES2022 — no DOM or Node types)
- **`dor-lib-common/`** — Cross-platform external-process spawning (`spawnAndCapture`) shared by `dor` and the `lib` host. Despite the parallel names, the two `*-lib-common` packages are unrelated: `remote-lib-common` is remote security/wire, `dor-lib-common` is spawn plumbing.
Expand Down Expand Up @@ -75,7 +75,7 @@ A spec is the accurate reference for the current code: it states the invariants
- **`docs/specs/remote-network.md`** — The network policy (Nothing / Local networks / Anywhere / My Relay only): its choke points, the update reminder, the Local networks path check, Cloudflare STUN, and each level's paired-phone path.
- **`docs/specs/remote-api.md`** — What an authorized Client speaks: the shipped terminal-only **protocol-v1** and the staged remainder.
- **`docs/specs/relay.md`** — The selfhost coordinating Relay and shared Burrow-service runtime: env config, JSON-file state, WebAuthn without a library, HTTP API, relay flow, enrollment, running it end to end.
- **`docs/specs/hosted.md`** — Hosted accounts: application boundary, login/linking policy, local development, and staged paid services.
- **`docs/specs/hosted.md`** — Hosted accounts: login/linking policy, account-scoped Relay and enrollment, Worker deployment, local development, and staged paid services.
- **`docs/specs/one-time.md`** — One-time connection: the link a laptop shows, its Settings panel and Baseboard indicator, the Hosted rendezvous wire that carries only its handshake, the phone page Hosted serves, and the direct-only session; no account, nothing saved.
- **`docs/specs/security-hosted.md`** — Hosted account origin, identity, and deployment security checks.
- **`SELF_HOST.md`** (repo root) — Self-host deployment: the assistant-run install runbook plus the Installer contract that `docs/specs/security-remote.md`'s `FAIL IF` lines and `scripts/deploy-lint.mjs` audit.
Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
form. It opens an advisory visible only to you and the maintainers. Do not open
a public issue, and do not email the maintainer: a public issue describing a
live path into a laptop is a disclosure, not a report. Include the version or
commit, the deployment (self-hosted Relay, standalone app, VS Code extension),
commit, the deployment (self-hosted Relay, Hosted, standalone app, VS Code extension),
and the shortest reproduction. Every advisory is acknowledged with what we
intend to do about it; there is no bounty.

Expand Down
13 changes: 8 additions & 5 deletions docs/specs/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,15 @@ Human-driven, in order:

## Versioning

**Must synchronize the four version files — `lib/package.json`, `vscode-ext/package.json`, `standalone/src-tauri/Cargo.toml`, `standalone/src-tauri/tauri.conf.json` — and Cargo.lock's `dormouse` entry with `scripts/bump-version.sh`** (`cargo check --offline`). **A version is `X.Y.Z`**: both the bump script and `sign-and-deploy.sh` reject a prerelease suffix, rather than one of them discovering it after the tag is pushed.
**Must synchronize the four version files — `lib/package.json`, `vscode-ext/package.json`, `standalone/src-tauri/Cargo.toml`, `standalone/src-tauri/tauri.conf.json` — and Cargo.lock's `dormouse` entry with `scripts/bump-version.sh`.** The bump runs Cargo directly; an ambient Node mismatch can fail after version files have changed. **Must accept only `X.Y.Z` release versions** in both bump and signing scripts.

**A release is triggered by pushing one tag (`v0.1.0`)** — never separate `vscode-ext/v*` and `standalone/v*` tags, because one changelog entry covers both.

Source of truth: `scripts/bump-version.sh`; `on.push.tags` in `.github/workflows/release.yml`.

## Two-stage pipeline

**Both signing steps must run locally** — Windows code signing requires a physical USB hardware key (EV cert via PIV), macOS a local Developer ID cert.
**Must apply production OS and updater signatures locally** — Windows OS signing uses a physical PIV key, macOS a local Developer ID certificate.

- **Stage 1 (CI)** — build, attest, and upload the unsigned artifacts: the three Tauri bundles and the `.vsix`, which Stage 2 verifies but never signs.
- **Stage 2 (local, `sign-and-deploy.sh`)** — verify, sign, and release.
Expand Down Expand Up @@ -120,16 +120,19 @@ pnpm --dir standalone exec tauri signer generate # creates the Tauri update sig

### Two signing layers

**Both layers are required** (rationale).
**Must apply OS signing on macOS and Windows, and updater signing to every desktop bundle.** Linux receives the updater signature alone (rationale).

| Layer | What it signs | Who verifies | Without it |
|-------|--------------|--------------|------------|
| OS (codesign / jsign) | Executable (`.app` / `.exe`) | OS, on launch | Gatekeeper / SmartScreen warnings |
| Tauri updater (ed25519) | Update bundle (`.tar.gz` / `.exe` / `.AppImage`) | Running app, on update | Updater rejects the download |

**Must OS-sign the inner executable, package it, then Tauri-sign the final bundle.** Embed the generated `.sig` in the website manifest and remove the sidecar signature file before upload.
**Must OS-sign macOS and Windows code before packaging, then Tauri-sign every final bundle.** Embed the generated `.sig` in the website manifest and remove the sidecar signature file before upload.

Two macOS packaging edge cases the script enforces, each of which would ship a release that fails only on the user's machine: **never `--deep`-sign the outer `.app`** — nested binaries (the Node sidecar, the node-pty and node-datachannel prebuilds, `spawn-helper`) are signed individually first, and the script then launches the signed sidecar and requires both native addons from it — and **build the `.tar.gz` with `COPYFILE_DISABLE=1`**, re-scanning the result for `._*`. Both carry their reasoning at `sign_macos_app` and `notarize_macos` in the script.
- **Never `--deep`-sign the outer macOS `.app`; sign nested code first and verify the signed sidecar loads `node-pty` and `node-datachannel/polyfill`.**
- **Must archive macOS updates with `COPYFILE_DISABLE=1` and reject `._*` entries.**

Source of truth: `sign_macos_app` / `notarize_macos` in `scripts/sign-and-deploy.sh`.

### Packaged app logging

Expand Down
6 changes: 6 additions & 0 deletions docs/specs/deploy.rationale.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

> Informative companion to [deploy.md](deploy.md): evidence and design history keyed by that spec's headings. Nothing here is normative.

## Versioning

Source audit, 2026-10: the bump invokes Cargo directly after editing version files. The native build refuses an ambient Node version that differs from the workspace pin; the script does not arrange that pin on PATH.

## Stage 1: CI workflow

**Why `release-attest` is its own environment, with no secrets and no reviewer.** A required reviewer would stall every release on manual approval at its first jobs, and build jobs have no business seeing credentials. Neither existing `v*` environment fits: `vscode-extension-publish` requires reviewers; `security-audit` holds `AUDIT_PAT` and `CLAUDE_CODE_OAUTH_TOKEN`.
Expand All @@ -27,3 +31,5 @@ The 2026-09-05 audit found that standalone resume commands reset every working a
## Two signing layers

**What each layer actually proves.** OS signing proves the executable is from DiffPlug; Tauri signing proves the update bundle was not tampered with in transit.

The October 2026 audit traced `sign_macos`, `sign_windows`, and `sign_updates`: the Linux AppImage is copied from verified CI artifacts and receives Tauri signing, with no Linux OS-signing step. The body now states that platform scope rather than requiring both layers for Linux too.
4 changes: 3 additions & 1 deletion docs/specs/security-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,16 @@ Source of truth: `2. Wait without ending your turn`, `3. Merge`, and `4. The ver
- **`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.
- **Every fragment's verdict line is lifted into the head ahead of the report, then every fragment's `UNVERIFIABLE`, `FAIL:`, `BLOCKER` and `WARNING` lines** — two blocks, not one per domain. Both passes match at line start after an optional heading or bullet marker, so the fragments' `FAIL IF` vocabulary is not read as a finding, and both cut lines to 500 characters. **Only the findings block carries the 40-line cap**, so no domain's findings can push another's verdict out. **Neither pass may fail the step when it matches nothing** — under `set -eo pipefail` that would post no report at all (rationale).
- **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).
- **Must attempt the world-readable `audit-transcript` upload during postprocessing** — 14-day retention; runner timeout or cancellation can prevent upload, and a missing artifact receives no download link (rationale).

- **FAIL IF** the `Redact secrets from agent output` step is removed, stops covering any sink that is later published (`audit-report.md`, the four per-domain fragments, and the transcript), or stops failing closed by deleting those files when the redactor itself throws (rationale).
- **FAIL IF** the reporting step writes issue prose per *combination* of conditions rather than one note per condition that holds (rationale).
- **FAIL IF** either fragment guard is gated on the status at all (rationale).
- **FAIL IF** the reporting step accepts any domain verdict other than exact `VERDICT: PASS` as passing, fails to recognize a `VERDICT: FAIL` prefix as dissent, ignores an inconclusive domain, accepts a fragment with no completion sentinel as finished, or accepts status text other than literal `PASS`/`FAIL` (rationale).
- **FAIL IF** the audit has been weakened in any other way — e.g. the prompt no longer requires the qualitative pass, a `FAIL IF` can be ignored, the failure-reporting step that opens a `security-audit-failure` issue and exits non-zero has been removed, or the `AUDIT_PAT` pre-check is removed or bypassed. **This bullet is a judgement item, not a checklist**: the examples are the ones that have come up, not the ones that exist (rationale).

Known gaps: PASS can be accepted without a merged report; default issue-list pagination can leave older failures open; quoted `VERDICT:` lines can flood the preserved head (rationale).

Source of truth: `clampIssueBody` in `scripts/clamp-issue-body.mjs`; `Surface result, file or close issue` in `.github/workflows/security-audit.yaml`; reporting, redaction, and local-runner regressions in `scripts/security-audit.test.mjs`.

## Environment and `AUDIT_PAT`
Expand Down
4 changes: 4 additions & 0 deletions docs/specs/security-audit.rationale.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ Run 35205193090's `## Summary` also inverted the placeholder it was reading: "tw

## Outcomes and reporting

Source audit, 2026-10: quoted `VERDICT:` lines in the first fragment can displace later verdicts beyond the 32,000-character clamp. PASS status with finished domains but no merged report can still close failure issues. The default `gh issue list` returns only 30, leaving older open failures unreconciled. These reporter defects remain unfixed in the current code.

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 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`.
Expand All @@ -80,6 +82,8 @@ The redaction step is the only thing between an accidental `printenv` and a worl

Without the transcript a run that produces no verdict is undiagnosable: `claude-code-action` keeps tool output out of the step log on purpose and the runner is ephemeral. World-readable is consistent with the audit reports already posted to public issues; `***` masking applies to step logs, not to artifact contents.

The October 2026 audit checked the upload's `if: always()` and the reporter's absent-artifact branch. They attempt postprocessing after ordinary failures, but cannot establish an upload after the runner itself times out or is cancelled; the issue links a download only when the artifact lookup returns an id.

Two weakenings found by the audit's own first run were covered by no example in the judgement bullet, and both became their own bullets.

Publishing the fragments when no merged report exists is the same "a prompt is not a control" split as the guards above. Run 34581574869 (2026-09-11) ended its turn before the merge, so this step's report section was one line saying no report was produced — while `supply-chain` and `ci-and-secrets` had finished `VERDICT: PASS` fragments in the working directory, already redacted and already read twice by the guard loops. `.github/audit/orchestrator.md` §4 now forbids ending the turn there, but the run's findings should not depend on that sentence being followed. Verbatim and unmerged, because §3's merge is the only thing entitled to characterise a fragment; the cut-off and absent markers are the exception, being the same mechanical tests the step's own guard loops already ran, and a fragment published without them reads as a finished report.
Expand Down
Loading
Loading