Date: 2026-06-19
Implemented:
- Cargo and pnpm workspaces
- Rust-owned versioned collector protocol
- generated TypeScript protocol types
- synthetic browser fixture generator
- local Unix-socket daemon
- SQLCipher-backed aggregate storage
- typed local check-in storage
- OS-keyring database-key provider
- Chrome native-message framing and forwarding
- Manifest V3 extension build
- extension popup with native-host, enabled-signal, queue, and delivery status
- privacy-preserving 15-minute tab-rhythm reducer
- CI checks for Rust, TypeScript, generated files, and fixtures
- deterministic local analytics prototype
- typed daemon read RPC for daily rhythm insights
- per-dimension baseline readiness and explicit abstention reasons
- typed daemon read RPC for a bounded daily timeline with explicit gaps
- reusable Rust daemon client with socket round-trip tests
mindcanaryctllocal control CLI for daemon health, settings, signal enable/disable, and record summaries- desktop dashboard view models for insight cards, timeline charts, exact daily rows, gaps, and empty states
- OS aggregate signal IDs, consent settings, daily storage projection, timeline lane, export file, and deterministic insight read-model support
- Linux GNOME/X11 OS active/idle duration adapter using GNOME Mutter idle time,
with consent-gated 15-minute
os.active_secondsandos.idle_secondsaggregates - consent-gated lock/unlock and suspend/resume count adapters using GNOME ScreenSaver and logind event streams
- two-step canonical local-record clearing with expiring confirmation tokens
- default-off, timestamped per-signal browser collection settings
- historical consent enforcement that blocks paused-period retry backfill
- two-step deletion of one aggregate signal's stored metric history without removing unrelated signals or check-ins
- fail-closed extension filtering using read-only daemon settings
- Tauri 2 and React desktop shell with a narrow typed daemon command bridge
- rendered optional check-in, daily history, insight, platform capability, collection, and record-clearing flows
- desktop getting-started checklist that distinguishes daemon readiness, browser signal consent, and first local record delivery
- first-run desktop onboarding that presents the local-first boundary and lets users start with check-ins or optional sources before showing the full app
- skippable onboarding choices for Chrome context and computer active/idle context, using starter sets rather than fine-grained signal configuration
- tabbed desktop sections for Today, History, Sources, and Data so setup and destructive controls do not crowd the daily check-in surface
- non-overlapping local dashboard refresh, with a manual control and one-minute polling only while the desktop window is visible
- confirmed local export to a human-readable report plus daily aggregate CSVs
- user-owned day and time-window annotations in encrypted storage, history, and export, deliberately excluded from automated analysis
- immediate latest-record context and explicit baseline-building progress
- user-initiated versioned SQLCipher backup, one-time recovery secret, verification, and empty-profile restoration
- offline local profile destruction for the encrypted database and keyring key
- user-level Linux systemd service installation and removal for
mindcanaryd - user-level Linux Chrome/Chromium native-host manifest installation and removal
- deterministic Chrome development identity plus shared development/release identity configuration for extension and native-host builds
- Firefox development build and native-host identity/manifest support, using a Firefox-compatible background script while preserving the same protocol
- desktop Chrome connector status and packaged native-host manifest repair flow that does not accept pasted extension IDs or arbitrary runtime paths
- count-only tab-retention carryover observer that survives extension restarts better than a single midnight bucket
- surfaced extension retry-queue overflow through a local dropped-batch count in the popup status
- descriptive insight hardening for pooled per-signal baselines, two-day sustained-change descriptions, isolated-spike abstention, exact prior-date evidence, and a visible "waiting for sustained change" readiness state
- descriptive insight abstention when a comparable baseline is too variable
- versioned local-v1 alpha insight thresholds with a zero-false-nudge synthetic fixture budget
- separately consented continuous-scrolling duration on the fixed
x.comandtwitter.comallowlist, with optional host permission and aggregate-only timeline/export output
Not yet implemented:
- signed ordinary-user Linux release artifacts
- unlisted Chrome Web Store beta/release identity and store installation flow
- disabled or removed extension detection in desktop connection status
- real-user/design-participant threshold validation and longer-horizon false-nudge evaluation
- week-long collector soak and overhead testing
- complete uninstall cleanup of extension queue, application configuration, and user-created exports
- Firefox temporary-install/native-messaging runtime validation
- foreground category and application-switch adapters [post-v1]
- broader website interaction adapters [post-v1 and user-research gated]
- local AI adapter
- synchronization
The daemon now writes aggregate batches and typed check-ins to a SQLCipher database. The production bootstrap path stores the database key through the operating-system keyring and fails closed if an existing encrypted database no longer has its key.
Pinned:
- Rust 1.86
- Node.js 22 or later
- pnpm 10.8.1
The editable logo mark is apps/desktop/public/mindcanary-mark.svg. The
desktop launcher icon source is apps/desktop/src-tauri/icons/app-icon.svg;
apps/desktop/src-tauri/icons/icon.png and the browser-extension PNG icons are
generated from that SVG. Keep the bird/wave mark in sync across desktop,
extension, and release docs.
Install JavaScript dependencies:
pnpm install --frozen-lockfileRun the complete check:
pnpm checkIndividual checks:
cargo fmt --all --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace --all-features
cargo test -p mindcanary-storage --test os_keyring -- --ignored --nocapture
pnpm protocol:check
pnpm fixtures:check
pnpm format:check
pnpm lint
pnpm test
pnpm buildcargo run -p mindcanarydThe default socket is:
$XDG_RUNTIME_DIR/mindcanary/mindcanaryd.sock
The default database is:
$XDG_DATA_HOME/mindcanary/mindcanary.db
If XDG_DATA_HOME is unavailable, the daemon follows the usual user data
fallback under $HOME/.local/share/mindcanary.
If XDG_RUNTIME_DIR is unavailable, the daemon uses the operating-system
temporary directory and creates its own mode-0700 subdirectory. The socket is
mode 0600.
For the short command-only runbook, see
development-profiles.md.
The default XDG profile is live dogfood data once a developer is using MindCanary personally. Do not use the default profile for clean-state onboarding tests, destructive checks, or migration experiments.
Create a human-readable local export before sensitive work:
pnpm backup:localBy default this writes a timestamped export under
$HOME/Documents/mindcanary-backups/. Set MINDCANARY_BACKUP_DIR to choose a
different parent directory. The export contains readable daily aggregates and
check-ins, so keep it private.
Run clean-state development with an isolated profile:
scripts/with-dev-profile.sh onboarding -- cargo run -p mindcanarydIn another terminal, use the same profile name for the desktop shell:
scripts/with-dev-profile.sh onboarding -- pnpm --filter @mindcanary/desktop tauri devThe wrapper sets isolated XDG_RUNTIME_DIR, XDG_DATA_HOME, and
XDG_CONFIG_HOME directories under target/dev-profiles/<profile>/. Commands
that use the default socket will then talk to that profile:
scripts/with-dev-profile.sh onboarding -- \
cargo run -q -p mindcanary-client --bin mindcanaryctl -- summaryThis profile isolation is for daemon, desktop, check-in, and onboarding work. The existing Chrome extension and native-host registration should be treated as the dogfood connector path unless Chrome is also launched with the same isolated config environment and the native-host manifest is reinstalled there.
Override only the daemon socket location for narrower development:
cargo run -p mindcanaryd -- --socket /tmp/mindcanary-dev/mindcanaryd.sockInstall the user-level Linux systemd service after building the daemon:
cargo build -p mindcanaryd
cargo run -p mindcanaryd -- \
--install-user-service \
--daemon-path "$PWD/target/debug/mindcanaryd"The installer writes mindcanaryd.service under
$XDG_CONFIG_HOME/systemd/user when set and otherwise under
$HOME/.config/systemd/user. It does not enable or start the service unless
you pass --enable-now:
cargo run -p mindcanaryd -- \
--install-user-service \
--daemon-path "$PWD/target/debug/mindcanaryd" \
--enable-nowRemove the installed user service:
cargo run -p mindcanaryd -- --uninstall-user-servicePass --disable-now to stop and disable it with systemctl --user before
removing the unit file.
Destroy the local encrypted database profile after stopping the daemon:
systemctl --user stop mindcanaryd.service
cargo run -p mindcanaryd -- \
--destroy-local-profile \
--confirm-destroy-local-profileThis removes the default encrypted database, SQLite sidecar files, and the
OS-keyring database key. Pass --database <path> to target a development
database. It does not remove Chrome extension storage, native-host manifests,
application configuration, or exports saved elsewhere.
For a complete app-owned local removal path, use the wrapper script:
pnpm uninstall:local -- --confirm-local-removalThe wrapper removes the user service, Chrome and Chromium native-host manifests, the default encrypted database profile, SQLite sidecars, the OS-keyring database key, the package setup marker, and the runtime socket directory. It still does not remove Chrome-owned extension storage or user-created exports/backups. Preview the commands without removing anything:
pnpm uninstall:local -- --dry-runThe desktop Local Data panel exposes the same app-owned removal boundary behind an exact confirmation phrase. Exercise the destructive path only in a disposable test user or VM because the OS-keyring database key is shared by the default local profile.
For a repeatable destructive audit without touching the dogfood profile, run the private-session audit:
pnpm removal:auditThe audit creates a private HOME, XDG config/data/runtime directories, DBus
session, and GNOME keyring daemon under target/removal-audit/, seeds a tiny
profile, runs the real complete local-removal command, and checks that the
app-owned files and private keyring entry are gone.
Rust is the source of truth for the wire protocol.
Regenerate TypeScript:
pnpm protocol:generateRegenerate synthetic data:
pnpm fixtures:generateCI fails if either committed artifact differs from its generator.
pnpm --filter @mindcanary/extension buildThe unpacked extension is written to:
apps/extension/dist
Development builds have a deterministic Chrome identity:
agokdhalkipifklmbipkgmfakdcaekbj
The development public key and expected ID live together in
config/chrome-extension-identities.json. The build verifies that the key
derives that ID before writing dist/manifest.json. Do not add a private key
to the repository.
Release builds use a separate explicit identity:
pnpm --filter @mindcanary/extension build:releaseThat command intentionally fails until the unlisted Chrome Web Store listing
provides a release ID in config/chrome-extension-identities.json. Release
builds never reuse the development manifest key.
The extension requests only:
alarmsnativeMessagingstorage
The idle and scripting permissions are optional. The only optional host
permissions are https://x.com/* and https://twitter.com/*, used by the
separately enabled continuous-scrolling adapter. There is no history permission
or incognito access. The adapter is not part of the starter set and does not
read routes or page content.
Build the Firefox development package separately:
pnpm --filter @mindcanary/extension build:firefoxIt is written to apps/extension/dist-firefox with development add-on ID
development@mindcanary.local. Firefox Manifest V3 uses a background script
rather than Chrome's service-worker manifest entry. Release builds fail closed
until a distinct Firefox release ID is configured in
config/firefox-extension-identities.json.
The native host name is currently:
app.mindcanary.collector
To try the current collector locally:
-
Start the daemon in one terminal:
cargo run -p mindcanaryd
-
Build the extension:
pnpm --filter @mindcanary/extension build
-
Open
chrome://extensions, enable Developer mode, choose "Load unpacked", and selectapps/extension/dist. -
Confirm that Chrome shows extension ID
agokdhalkipifklmbipkgmfakdcaekbj. The popup also checks its runtime ID against the selected build identity and shows an explicit mismatch instead of offering a setup command for the wrong extension. -
Install the native-host manifest using the command shown in the popup, or run the equivalent manually:
cargo build -p mindcanary-native-host cargo run -p mindcanary-native-host -- \ --install-manifest \ --browser chrome \ --channel development \ --host-path "$PWD/target/debug/mindcanary-native-host" -
Reload the unpacked extension in
chrome://extensions. -
Open the MindCanary extension popup and press "Refresh status". The popup should tell you whether the native host is connected, show the extension ID and native-host name, report whether any signals are enabled, and show whether aggregate batches are queued. It also reports whether Chrome's optional idle permission is granted. Once collection starts, it shows coarse progress toward the current fixed 15-minute bucket boundary. Refreshing also closes and delivers any bucket whose boundary has already passed.
-
Enable the default browser aggregate set. In the desktop app, open "Browser signals" and choose "Enable starter set". The same development action is available through the local control CLI:
cargo run -p mindcanary-client --bin mindcanaryctl -- \ enable-browser-defaults cargo run -p mindcanary-client --bin mindcanaryctl -- settings
Until a signal is enabled, the extension intentionally reports "Collection is paused" and does not persist browser aggregates.
The default browser set includes tab switching, maximum and average open tabs, browser-window maximum, active/idle duration, and tabs retained across a local-day boundary. If the popup shows that idle permission is needed, click "Allow idle permission" in the popup and approve Chrome's permission prompt.
The extension popup is operational status only. It does not show URLs, titles, page text, search terms, or browsing history.
Build the Firefox package and native host:
pnpm --filter @mindcanary/extension build:firefox
cargo build -p mindcanary-native-host
cargo run -p mindcanary-native-host -- \
--install-manifest \
--browser firefox \
--channel development \
--host-path "$PWD/target/debug/mindcanary-native-host"Open about:debugging#/runtime/this-firefox, choose "Load Temporary Add-on",
and select apps/extension/dist-firefox/manifest.json. Temporary installation
and the native-message round trip require a running Firefox session and remain
a manual validation. Use only one browser connector at a time for now: browser
aggregate records do not yet carry connector provenance, so overlapping Chrome
and Firefox collection would double-count shared time.
Useful local control commands while the daemon is running:
cargo run -p mindcanary-client --bin mindcanaryctl -- health
cargo run -p mindcanary-client --bin mindcanaryctl -- source-status
cargo run -p mindcanary-client --bin mindcanaryctl -- signals
cargo run -p mindcanary-client --bin mindcanaryctl -- settings
cargo run -p mindcanary-client --bin mindcanaryctl -- summary
cargo run -p mindcanary-client --bin mindcanaryctl -- \
export --directory "$HOME/Documents/mindcanary-export"
cargo run -p mindcanary-client --bin mindcanaryctl -- enable-browser-defaults
cargo run -p mindcanary-client --bin mindcanaryctl -- \
enable browser.open_tab_count_mean
cargo run -p mindcanary-client --bin mindcanaryctl -- disable-browser-defaultsCapture a stable Chrome connection smoke snapshot during manual testing:
pnpm smoke:chrome
pnpm smoke:chrome restart-daemon
pnpm smoke:chrome after-manual-stepThe report records daemon health, source status, local summary counts, the
development native-host manifest status, and the user-level daemon service
state under target/stable-chrome-smoke/. Use it before and after manual
extension reload, disable, remove, or re-enable checks.
Install the user-level Linux Chrome manifest after building the native host:
cargo build -p mindcanary-native-host
cargo run -p mindcanary-native-host -- \
--install-manifest \
--browser chrome \
--channel development \
--host-path "$PWD/target/debug/mindcanary-native-host"Use --browser chromium for Chromium. The installer writes
app.mindcanary.collector.json under the browser's NativeMessagingHosts
directory, using XDG_CONFIG_HOME when set and otherwise $HOME/.config.
The manifest allowlists only the extension ID configured for the selected
channel. The installer refuses --channel release until a distinct Web Store
ID is configured. It does not accept arbitrary extension IDs. The daemon still
has to be running separately.
Use --browser firefox for Firefox. On Linux, that manifest is written under
$HOME/.mozilla/native-messaging-hosts and uses Firefox's configured add-on ID
rather than a Chrome extension origin.
Remove the user-level browser manifest:
cargo run -p mindcanary-native-host -- \
--uninstall-manifest \
--browser chromeUse --browser chromium or --browser firefox to remove the corresponding
registration. This removes only the native-host manifest file, not the browser
extension or its local storage.
The React frontend can be checked independently:
pnpm --filter @mindcanary/desktop test
pnpm --filter @mindcanary/desktop buildAfter installing the Tauri prerequisites below, check and run the native shell:
pnpm --filter @mindcanary/desktop tauri:check
pnpm --filter @mindcanary/desktop tauri devBuild the first ordinary-user Linux package:
pnpm package:linuxThe packaging command builds release versions of mindcanaryd and
mindcanary-native-host, includes them under /usr/lib/mindcanary, and
produces a Debian package under the Tauri bundle output directory. When the
packaged desktop cannot reach the daemon, it installs and starts the user-level
service and then retries. Login startup remains a user-controlled app setting.
Development builds do not contain those packaged helpers and continue to use the
manually started development daemon.
The webview has a narrow command surface for daemon health, source status, insights, daily history, collection settings, typed check-in submission, local record counts, per-signal record deletion, and the two-step clear flow. It cannot submit arbitrary protocol requests and has no SQL, shell, filesystem, or public-network capability.
source-status reports only source type, operational health, and the most
recent local receipt timestamp. Browser and OS aggregate collectors become
stale after 45 minutes without a stored batch. Check-ins remain ready because
they have no required cadence.
The daily history read model returns the most recent 30 calendar days by default, bounded to the first and last recorded local dates. Days without a browser aggregate, OS aggregate, or check-in are returned explicitly rather than represented as zero.
On Linux GNOME/X11, the daemon can collect OS active and idle duration if the two OS activity signals are enabled. The desktop app exposes them in "Computer activity signals". The same development action is available through the local control CLI:
cargo run -p mindcanary-client --bin mindcanaryctl -- enable os.active_seconds
cargo run -p mindcanary-client --bin mindcanaryctl -- enable os.idle_seconds
cargo run -p mindcanary-client --bin mindcanaryctl -- settingsThe adapter samples GNOME Mutter's idle monitor and stores only aligned 15-minute active/idle totals after daemon consent checks. It does not collect window titles, document names, foreground applications, lock events, suspend events, or resume events.
The baseline prototype compares the latest local day with all prior recorded days containing that dimension. It does not infer workdays or weekends. It requires three prior values before change language can appear. The insight response also reports one deterministic readiness state per dimension:
- change described
- within the current baseline threshold
- no latest value
- insufficient prior comparable days
- prior comparable values centered on zero
The desktop keeps these reasons behind "How each dimension was handled." A fresh profile therefore explains that it is building a baseline instead of showing an unexplained empty insight panel. Emitted descriptions also include the number and exact dates of prior baseline days in their evidence. Cumulative signals are coverage-normalized for descriptions: tab switching is measured per recorded hour, and browser/computer active time is measured as a share of recorded periods. History continues to show raw daily totals.
rusqlite is pinned to 0.39.x because the newer libsqlite3-sys 0.38.x
build script requires a macro unavailable on the pinned Rust 1.86 toolchain.
The chosen release still provides bundled-sqlcipher-vendored-openssl, so the
project does not depend on a system SQLCipher installation.
The regular workspace test suite uses temporary fixed keys. The OS-keyring test is ignored by default because it creates and deletes a real desktop keyring entry:
cargo test -p mindcanary-storage --test os_keyring -- --ignored --nocaptureThe wrong-key test may cause SQLCipher to write an HMAC failure line to stderr. That is expected and confirms that a mismatched key cannot read page 1.
The first check-in record supports bounded numeric fields and enum context tags:
- sleep minutes
- perceived sleep need
- mood
- energy
- irritability
- concentration
- impulsivity
- medication taken
- substance use
- context tags such as deadline, travel, illness, news cycle, or exercise
There is deliberately no free-text note, diagnosis, clinical phase, URL, title, or generic metadata map in the wire protocol.
The current Pop!_OS 22.04 development machine has the native Tauri development libraries installed and has completed a development startup smoke test. A new Ubuntu or Pop!_OS development machine should install:
sudo apt-get install -y \
libgtk-3-dev \
libwebkit2gtk-4.1-dev \
libxdo-dev \
libssl-dev \
libayatana-appindicator3-dev \
librsvg2-devOfficial reference: https://v2.tauri.app/start/prerequisites/
Do not replace the SQLCipher store with plaintext SQLite merely to make a UI demo run.
Collector records are closed typed objects. Protocol tests reject unknown
fields and unknown signal IDs, including attempted url fields.
Before adding a signal:
- add a typed Rust
SignalId - define its validation rule
- regenerate TypeScript
- add synthetic data
- prove the source adapter discards transient context
- update consent and retention documentation
Never add a generic metadata map to the collector protocol.