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
34 changes: 26 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,25 @@ jobs:
run: npm run check-contrast

rust:
name: Rust
runs-on: macos-latest
timeout-minutes: 25
name: Rust (${{ matrix.label }})
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
# PR-17 — compile BOTH shipped host targets on every push/PR. Building
# only aarch64-apple-darwin left every #[cfg(target_os = "windows")] path
# (custom-chrome set_decorations, the sidecar PATH block, the
# windows-native keyring backend) uncompiled until release.yml's
# windows-latest job — which runs AFTER release-prep has already pushed
# the immutable vX.Y.Z tag, so a Windows-only break burned a version.
fail-fast: false
matrix:
include:
- label: macOS
os: macos-latest
llama-triple: aarch64-apple-darwin
- label: Windows
os: windows-latest
llama-triple: x86_64-pc-windows-msvc
steps:
- uses: actions/checkout@v6

Expand All @@ -74,12 +90,14 @@ jobs:
with:
workspaces: src-tauri -> target

# tauri.conf.json's bundle.externalBin entry requires the file to exist
# on disk during the cargo build script (V2-P1). The Rust job runs on
# macos-latest which is Apple Silicon, so the host triple matches the
# only one we need for cargo check.
# tauri.conf.json's bundle.externalBin entry requires the file to exist on
# disk during the cargo build script (V2-P1). Each runner is its own host
# triple (macos-latest = Apple Silicon, windows-latest = x86_64-msvc), so
# cargo check/test build natively for the shipped target. `shell: bash`
# runs the fetch script identically on the Windows runner.
- name: Fetch llama-server prebuild
run: bash scripts/fetch-llama-server.sh --triple aarch64-apple-darwin
shell: bash
run: bash scripts/fetch-llama-server.sh --triple ${{ matrix.llama-triple }}

- name: cargo fmt
working-directory: src-tauri
Expand Down
53 changes: 53 additions & 0 deletions .github/workflows/release-prep.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ jobs:
name: Quality gate
runs-on: ubuntu-latest
timeout-minutes: 15
outputs:
verified-sha: ${{ steps.ci-check.outputs.sha }}
steps:
- uses: actions/checkout@v6
with:
Expand Down Expand Up @@ -64,6 +66,17 @@ jobs:
- name: Contrast (WCAG AA) enforcement
run: npm run check-contrast

# PR-19 — the a11y gate (a CLAUDE.md quality gate that ci.yml runs) was
# missing here, so a release could be cut over an axe-core regression.
- name: Storybook build
run: npm run build-storybook

- name: Install Playwright (Chromium)
run: npx playwright install --with-deps chromium

- name: Accessibility audit (axe-core over every Storybook story)
run: npm run check-a11y

- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
Expand All @@ -72,6 +85,30 @@ jobs:
working-directory: src-tauri
run: cargo fmt --check

# PR-19 — compiling the Rust targets (both macOS + Windows legs) on this
# ubuntu gate is impractical, and shipping a release over a clippy or
# cargo-test failure the pre-commit hook can silently miss is the exact
# gap this closes. Require that CI (which DOES compile/clippy/test both
# host targets and runs check-a11y) has gone green on the precise main SHA
# being tagged, so the irreversible bump+tag+push can't outrun it.
- name: Require CI success on this main commit
id: ci-check
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
sha=$(git rev-parse HEAD)
echo "sha=$sha" >> "$GITHUB_OUTPUT"
echo "Requiring a successful CI run for main @ $sha"
run=$(gh api \
"repos/${{ github.repository }}/actions/workflows/ci.yml/runs?head_sha=$sha&per_page=1" \
--jq '.workflow_runs[0] | "\(.status):\(.conclusion)"' 2>/dev/null || echo "none")
echo "CI run for $sha: $run"
if [ "$run" != "completed:success" ]; then
echo "::error::CI has not passed on main @ $sha (got '$run'). Push the fix, wait for CI to go green, then re-run Release prep."
exit 1
fi

Comment thread
coderabbitai[bot] marked this conversation as resolved.
prep:
name: Bump, tag, trigger build
needs: gate
Expand All @@ -82,6 +119,22 @@ jobs:
with:
ref: main

# Pin the release to the EXACT SHA the gate verified. `ref: main` above
# can resolve to a newer commit if main advanced between the gate and prep
# checkouts, which would bump+tag+release code CI never approved (TOCTOU).
# Fail fast rather than release an unverified commit; the user re-runs
# Release prep, which re-gates the new HEAD.
- name: Verify checkout matches gate-verified SHA
run: |
set -euo pipefail
head=$(git rev-parse HEAD)
verified='${{ needs.gate.outputs.verified-sha }}'
echo "prep HEAD: $head; gate-verified: $verified"
if [ "$head" != "$verified" ]; then
echo "::error::main advanced since the gate verified $verified (now $head). Re-run Release prep."
exit 1
fi

- uses: actions/setup-node@v6
with:
node-version: '20'
Expand Down
8 changes: 6 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,9 @@ The `trystero` package uses Nostr by default — a network of public WebSocket r
- `room.onPeerJoin` / `onPeerLeave` for presence on the topic.

### Strategies
1. **Nostr** (default, `trystero` package) — public Nostr relay network, no auth required, small message footprint. This is the only strategy currently wired.
2. **BitTorrent trackers** (`@trystero-p2p/torrent`) and **MQTT** (`@trystero-p2p/mqtt`) — available in the library as fallbacks if Nostr relays misbehave, but **not yet wired**; strategy-racing is deferred.
1. **Nostr** (default, `trystero` package) — public Nostr relay network, no auth required, small message footprint. It carries all long-lived rooms (inbox, presence, session mesh).
2. **MQTT** (`@trystero-p2p/mqtt`) — wired since v1.2.2 for **pairing only**, raced alongside Nostr. `joinTopic` opens one room per strategy on the same topic + password over a single shared `@trystero-p2p/core`, so a peer keeps one stable `selfId`/peerId across transports; joins are deduped by peerId and both rooms are torn down on `leave` (see `src/lib/trystero/index.ts` `mergeRooms`). This survives a dark/blocked Nostr relay set **or** a clock-skewed peer (Nostr filters ephemeral announces on `since: now()`; MQTT has no time filter and shares no infrastructure). Only pairing races transports — long-lived rooms stay single-strategy (Nostr) to avoid duplicate peer connections.
3. **BitTorrent trackers** (`@trystero-p2p/torrent`) — available in the library as a further fallback but **not wired**.

We never ship Firebase or Supabase strategies; both require keys we'd own (= backend we operate).

Expand Down Expand Up @@ -700,6 +701,9 @@ The `Ctrl+]` AI dialog is a separate Tauri window with:
|-|-|-|
| Stranger spams my inbox | Drop messages from non-friends silently. Public Nostr relays absorb the load. | Low. |
| Stranger who learned my Ed25519 pubkey writes to my inbox topic | Inbox-topic password also derives from my pubkey, so they can write encrypted-at-the-trystero-layer junk; we still drop after decrypt + signature check. | Wasted bandwidth only — invite payloads are *additionally* NaCl-box-encrypted to my X25519 pubkey, so the stranger can't actually read or forge real invites. Acceptable for friend-only model; consider per-friend-pair shared-secret topics in V3 if abused. |
| Stranger replays a captured invite envelope to re-fire my invite toast/notification | The inbox receiver dedups on `(from_ed_pubkey, box nonce)` within the invite TTL, so a replayed envelope is dropped after the first delivery. | Low — one genuine invite still shows once; a stranger cannot manufacture new invites (they're signed + boxed). |
| Stranger camped on my inbox topic is seen by the sender as "delivered" (or silently drops my invite) | A peer joining the pubkey-derived inbox topic is treated as delivery, and the sender can't distinguish the real recipient from an eavesdropper on that shared topic. | **Accepted** under friends-only — the envelope is still NaCl-box-sealed to the recipient's X25519 key, so a stranger can't read it; the worst case is a suppressed offline-retry (the host re-clicks Invite). A signed application-level invite-ACK is a flagged future hardening. |
| Stranger who learned a friend's Ed25519 pubkey forges that friend's presence (online/offline) | Presence topic + password derive from the target's public pubkey and the heartbeat/goodbye payloads are unauthenticated (unlike invites, which are boxed + signed). | **Accepted** under friends-only — presence is soft UX state (a dot + invite affordance), not a data or session compromise; the worst case is a spurious "came online" notification or a griefed offline dot. Signing heartbeats would break cross-version presence (older peers send unsigned), so it's deferred rather than enforced. |
| Stranger eavesdrops on Nostr | All topic messages are password-encrypted by trystero. Pairing words / inbox derivation are not on-the-wire. | Negligible for friend-only model. |
| Friend disables their own AI / fakes score | Not defended. Social trust. | Accepted. |
| Friend impersonates another friend on Nostr | Ed25519 signatures on every event. Receivers verify against saved pubkey. | Very low. |
Expand Down
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,9 +309,11 @@ user-facing tour in `README.md`.
greys + amber with the warm-honey canvas that `DESIGN-SYSTEM` §1
now describes as "Calm Dark — Linear × Things 3, warm not
corporate".
- Per-arch macOS DMGs — the release workflow builds `aarch64` and
- Per-arch macOS DMGs — the release workflow built `aarch64` and
`x86_64` separately rather than producing a universal binary,
because the llama-server sidecar is per-arch.
because the llama-server sidecar is per-arch. (Superseded later in
v1.0.5 by the "Dropped the macOS Intel build target" note above:
the shipped matrix is now Apple Silicon `aarch64` only.)
- Audit pass (`audit/sev1-sev2-fixes` then `audit/sev3-sev4-fixes`)
closed Sev1 → Sev3 findings. See `ISSUES.md` for the full ledger;
I9 (Pomodoro broadcaster takeover) and I18 (sidecar model-path
Expand Down
Loading
Loading