Imported from humaipsy/humaipsy (
AGENTS.md). Install upstream withnpx skills add humaipsy/humaipsy. Copyright stays with the author.
AGENTS.md
Guide for AI coding agents (and humans) working in this repository. This file is the canonical reference; the project keeps no second agent guide.
What this is
HumaiPsy is a cross-platform (Windows + Linux + macOS) desktop application for running
Human-AI Interaction / HCI experiments. It's a three-crate Cargo workspace:
humaipsy-core is a UI-free library (agent prompting, the Ollama client, experiment
recording, the chat SQLite store, the master-password vault); apps/humaipsy is the
eframe/egui desktop binary built on top of it; humaipsy-web is a small local
read-only browser dashboard, spawned by apps/humaipsy in-process, that serves plain
HTML/JS/CSS plus a JSON API reading the same chat Store. The app talks to a local
or remote Ollama server for model inference, and records
experiment trials to CSV plus a mirrored JSONL event ledger plus a local SQLite chat
log. There is no separate backend service and no build step beyond cargo.
Licensed under AGPL-3.0-only (see LICENSE); do not add a dependency whose license is incompatible with the AGPL (e.g. code under a "no derivatives" or similarly restrictive license) without checking first.
For a narrative walkthrough of how these modules fit together — process model, the experiment engine's trial/condition logic, storage formats, security model, known limitations, and the roadmap — see ARCHITECTURE.md. For what's actually protected (and what isn't) about experiment data — network boundaries, the vault, file permissions, what's plaintext at rest — see SECURITY.md. Any change that touches the vault, air-gap policy, the web dashboard's gating, or file permissions should keep that document accurate; it's read by people deciding whether this tool is safe to use with real participant data.
Repository layout
Cargo.toml # workspace root, members = ["apps/humaipsy", "crates/humaipsy-core", "crates/humaipsy-web"]
crates/humaipsy-core/ # library crate — zero egui/eframe dependency
Cargo.toml
src/
lib.rs # module tree / public API
agent.rs # `HumaiPsyAgent`: prompt construction + calls into
# provider::Provider::chat (not ollama:: directly)
chat_types.rs # ChatMessage/Room/MessageSource/AssistantGeneration/
# MessageCorrelation/UiInbox — shared by store.rs and
# apps/humaipsy's ChatExample (which does the egui rendering)
experiment/ # pure trial-sequencing/counterbalancing engine —
# parameterized, unit-tested, no process-global state.
# apps/humaipsy/src/ui/overview_chat/panel.rs delegates its
# condition/trial math here instead of hardcoding it.
provider/ # backend-agnostic chat-completion trait +
# token budgeting. ollama_provider.rs is the only
# impl today — see "Adding a new provider" below.
ollama/ # Ollama HTTP client, chat engine, model discovery —
# unchanged by the provider abstraction; still the
# only thing that speaks HTTP to a model backend
recording/ # experiment_csv (CSV writer + JSONL ledger mirror),
# ledger (JSONL event log + summary/manifest hashing —
# wired into experiment_csv, not standalone), timing, metrics
store.rs / store_schema.rs # SQLite chat store (rusqlite, "chat.sqlite")
sync_ext.rs # MutexExt/RwLockExt poison-recovering lock
# accessors — used by all three crates, see Locking below
timer.rs # trial countdown timer used by the experiment UI
vault.rs # master-password gate (Argon2id) — no
# egui/UI dependency (rendering lives in
# apps/humaipsy/src/ui/vault_panel.rs; keep it that way)
crates/humaipsy-web/ # library crate — local read-only browser dashboard
Cargo.toml # depends on humaipsy-core only, not apps/humaipsy
web/ # plain HTML/JS/CSS, no build step — embedded via include_str!
# (routes.rs), not served from a filesystem path at runtime
src/
lib.rs # `pub async fn serve(WebServerConfig)`, spawned by
# apps/humaipsy onto its existing Tokio runtime
status.rs # WebStatusSnapshot + the Arc<Mutex<..>> apps/humaipsy writes into
routes.rs # GET /, /api/status, /api/messages (reads humaipsy_core::store::Store)
apps/humaipsy/ # the UI binary — depends on humaipsy-core and humaipsy-web as path dependencies
Cargo.toml # package manifest (version is the release version, e.g. 0.1.3)
.cargo/config.toml # target-dir = "target" so `cargo` works from this dir without -p
build.rs # Windows-only: generates the .ico and embeds it as a resource
src/
main.rs # entry point: Tokio runtime + eframe::run_native
lib.rs # module tree / re-exports (pub use ... as the crate's public API)
app_model.rs # `HumaiPsy` struct: top-level in-memory app state, incl.
# web_status/web_dashboard_addr and spawn_web_dashboard()
app_state.rs # `AppState`: shared, lockable runtime config (metrics)
window_icon.rs # platform-split: Win32 SendMessage icons vs egui IconData
ui/ # egui panels: settings, design, overview chat, vault unlock UI
mod.rs # HumaiPsyUiState also holds the persisted egui_tiles::Tree
# for the Overview tab's resizable sidebar/chat split
overview_chat/ # experiment engine UI + chat rendering (largest module,
# panel.rs is ~2k lines) — see "Known state-management debt" below
vault_panel.rs # renders MasterVault; the only file that may import egui
# *and* humaipsy_core::vault together
bin/
gen_master_hash.rs # standalone binary: interactively writes
# vault/.master_hash; depends only on humaipsy_core::vault
assets/ # window/topbar icon PNGs + bundled NotoEmoji-Regular.ttf (source of
# truth; the Windows .ico is generated from icon_window_bar.png)
vault/.master_hash # gitignored — local Argon2id hash file, created on first run
metrics/ # gitignored except structure — CSV/ledger exports + chat.sqlite land here
scripts/run_app.sh / .bat # `cargo clean && cargo run` — quick dev loop
.github/workflows/ci.yml # runs on every PR: check/test/clippy --workspace on Linux + Windows + macOS;
# also `workflow_call`, so a release reuses it as its gate
.github/workflows/release.yml # tag-triggered: one three-OS matrix build, then SHA256SUMS + publish
.github/workflows/pages.yml # publishes docs/ to GitHub Pages on a push to main that touches docs/
release.yml is a single matrix workflow rather than one file per platform: a v* tag
push runs the same ci.yml gate a pull request runs, creates a draft release, builds
the three targets (x86_64-pc-windows-msvc, x86_64-unknown-linux-gnu,
aarch64-apple-darwin), attaches one archive per target, publishes SHA256SUMS.txt
computed from what was actually uploaded, and only then flips the draft public — a
half-finished release can never be installed. Each archive is
humaipsy-<version>-<os>-<arch>.<ext> (<version> is the tag with its leading v
stripped, e.g. v0.1.6 → humaipsy-0.1.6-windows-x86_64.zip). The macOS build runs on
GitHub's macos-latest runner, which is Apple Silicon (arm64) — there is currently no
Intel/x86_64 macOS build; add a matrix row (or a lipo-merged universal binary) if
that's ever needed. The Linux row pins ubuntu-22.04 deliberately: the glibc an archive
links against is the oldest machine that can run it.
There is no separate staging step and no dist/ folder to assemble. Everything the app
draws is compiled in — apps/humaipsy/assets/*.png and the Noto emoji font via
include_bytes!, humaipsy-web's HTML/CSS/JS via include_str! — so a plain
cargo build --release produces the same binary the release archives carry. The only
thing a user adds beside it is their own vault/.master_hash.
humaipsy-core / apps/humaipsy / humaipsy-web: where new code goes
- If it doesn't need
egui/eframe, it belongs inhumaipsy-core. Business logic (prompt construction, trial sequencing, recording, the chat store, the vault) lives there so it can be unit tested directly and sohumaipsy-web(and a future CLI) can depend on it without pulling in a GUI toolkit.crates/humaipsy-core/src/experiment/mod.rsis the reference example: pure functions taking their inputs as parameters, noOnceLock/global state, straightforward to test. - If it renders (
egui::Ui,egui::Context, ...), it belongs inapps/humaipsy.apps/humaipsy/src/ui/overview_chat/chat.rs'sChatExampleis the reference example of the seam: the pure data types it operates on (ChatMessage,Room, ...) live inhumaipsy_core::chat_types, butChatExampleitself and itssidebar_uirendering method stay in the UI crate. - If it's an HTTP route or anything the browser dashboard serves, it belongs in
humaipsy-web.humaipsy-webdepends only onhumaipsy-core, never onapps/humaipsy— don't add a dependency in that direction, it would invert the whole point of the split (a UI-free engine that a server can depend on). New read endpoints go incrates/humaipsy-web/src/routes.rsand should read fromhumaipsy-corestate (theStore, or a snapshot type likeWebStatusSnapshot) rather than trying to reach intoapps/humaipsy's private UI-module statics — see "Known state-management debt" below for why that's not accessible today anyway.apps/humaipsy/src/app_model.rs'sspawn_web_dashboard/update_web_status(called fromui/overview_chat/panel.rsat the trial-lifecycle points) is the pattern for feeding new status fields through: add toWebStatusSnapshot, populate it inupdate_web_status, done — no changes needed on thehumaipsy-webside for a field that's just carried through. - If it's a new model backend (OpenAI, Anthropic, ...), it's a new
impl Providerincrates/humaipsy-core/src/provider/, modeled onollama_provider.rs. Do not add a second field toHumaiPsyAgentor anif self.backend == ...branch anywhere — the entire reasonProviderexists is soagent.rsand its callers never need to know which backend is active. A new provider should call whatever HTTP/SDK client it needs itself (mirroring howOllamaProvidercalls intoollama::, not by extending theollamamodule to handle a second service) and honorChatRequest.max_output_tokensthe wayOllamaProvidermaps it onto Ollama'snum_predict. As of this writing exactly one provider exists (Ollama) — this guidance is for when that changes, not a hint that more should be added speculatively now. - State-management pattern:
apps/humaipsy/src/ui/overview_chat/panel.rsandchat.rshold experiment/chat UI state (current trial, chat rooms, participant ID, modal open/closed flags) onOverviewChatUiState, a field ofHumaiPsyUiStateowned byHumaiPsyApp, threaded through render calls explicitly — this used to be process-globalOnceLock<Mutex<T>>statics (predating thehumaipsy-coresplit); don't reintroduce that pattern. New per-app UI state should be a field onHumaiPsy(app_model.rs) or a small nested struct held onHumaiPsyApp(seeHumaiPsyUiState/OllamaUiState/OverviewChatUiStateinui/mod.rs/overview_chat/panel.rsfor the pattern to follow), threaded through render calls explicitly. The oneOnceLock-based cache that is the right pattern is a genuine cross-cutting singleton keyed by something other than "the app started":ollama::engine::InferenceEngine::from_host's per-host connection-pool cache, andpanel.rs'sshared_storechat-database cache modeled after it — both areOnceLock<Mutex<HashMap<Key, Arc<T>>>>caches with a real cache-key, not a disguised global variable for state that has exactly one owner.
Cross-platform rules (read this before touching platform-sensitive code)
This app must build and behave the same on Windows, Linux, and macOS. Concretely:
- Never assume
HOMEis set. Windows setsUSERPROFILE, notHOME. Any code that needs the user's home directory must check both (seecopy_experiment_csv_to_desktopinapps/humaipsy/src/ui/overview_chat/panel.rsfor the pattern:std::env::var("HOME").or_else(|_| std::env::var("USERPROFILE"))). - Don't add dependencies that pull in vendored OpenSSL. A leftover
openssl = { features = ["vendored"] }direct dependency used to make the app fail to build on plain Windows dev machines (openssl-systries to compile OpenSSL from source via Perl/NASM, which most Windows boxes don't have). It has been removed;reqwest(inhumaipsy-core) usesfeatures = ["rustls-tls"]andrusqliteusesfeatures = ["bundled"]specifically so no system OpenSSL/pkg-config is required on any platform. If a new dependency wants to pull inopenssl/native-tls, prefer a rustls-based alternative. - Fonts must be bundled, not loaded from OS-specific filesystem paths. The chat UI
needs Unicode emoji glyph coverage (Ollama responses can contain emoji), which egui's
stock fonts don't provide.
apps/humaipsy/src/ui/mod.rs(prepare_shell) bundlesassets/NotoEmoji-Regular.ttfviainclude_bytes!and registers it as a fallback in both font families — this works identically on Windows, Linux, and macOS with no runtime dependency on fonts being installed. An earlier version of this code probed hardcoded/usr/share/fonts/...paths, which found nothing on Windows (no such paths) and was unreliable on Linux (path varies by distro) — don't reintroduce that pattern. Also note: egui/epaint's text rasterizer only supports single-color glyph outlines, not COLR/CBDT color tables, so this renders emoji as tinted monochrome pictographs, not full-color — that's a renderer limitation, not a bug in the font setup. Seeui::tests::bundled_emoji_font_covers_common_emoji_after_prepare_shellfor the regression test (usesFonts::has_glyphsto prove real glyph resolution). - Platform-specific code lives behind
#[cfg(windows)]/#[cfg(not(windows))], never behind atarget_os = "linux"check unless macOS is explicitly meant to be excluded too. Seewindow_icon.rsandCargo.toml's[target.'cfg(windows)'...]sections for the existing pattern — mirror it rather than inventing a new one. scripts/run_app.shandscripts/run_app.batmust stay in parity. Each is a thincargo clean && cargo runinsideapps/humaipsy/; if you change one, change the other. Neither release binary is code-signed — the Windows build will trigger a SmartScreen "unknown publisher" warning; that's a known, accepted state, not a bug to silently work around. Don't add signing scripts/CI steps unless explicitly asked.- Line endings are enforced via
.gitattributes:*.shis forced to LF,*.bat/*.ps1to CRLF, regardless of a contributor's localcore.autocrlf. If you add new shell or batch scripts, make sure.gitattributescovers them — a.shfile checked in with CRLF will fail to run on Linux/CI (bad interpreter). - Paths should be built with
PathBuf/Path::join, never string concatenation with a literal/or\.
Configuration / environment variables
Every setting is HUMAIPSY_<NAME> as of 0.1.6 (it was HUMAI_<NAME> before the rename).
humaipsy_core::env_compat::var is the only thing that knows both spellings: it reads
HUMAIPSY_<NAME> and falls back to HUMAI_<NAME>. Call sites pass the bare suffix
(env_compat::var("METRICS_DIR")), never a full variable name.
That fallback is deliberate, not leftover scaffolding: an unread HUMAI_METRICS_DIR
does not fail, it silently moves a researcher's CSV output into the default directory,
and an unread HUMAI_MASTER_HASH silently changes which vault the app unlocks. Do not
"finish" the rename by dropping the legacy branch without deciding what happens to
machines already configured under the old name.
Two identifiers are deliberately not renamed, because they are records rather than labels:
vault/.master_hash— the filename holds an Argon2id hash, not a name. Renaming the path would make every existing install prompt to create a new master password.- the
overview_chat.humai_agent.*inference-trace sources inapps/humaipsy/src/ui/overview_chat/panel.rs— thesourcestring is persisted into the CSV/ledger, so changing it would make the same event read differently in old and new runs.
Locking
Never write .lock().unwrap() / .read().unwrap() / .write().unwrap() — that
propagates a single panic while a lock was held into a permanent crash on every
subsequent call to that lock (some of these locks are touched every UI frame). Use
humaipsy_core::sync_ext's MutexExt/RwLockExt instead:
mutex.lock_recover(), rwlock.read_recover()/.write_recover(). Every Mutex/RwLock
in the codebase goes through these; a new one that doesn't is a regression.
Build / run / test
All commands are run from apps/humaipsy/ (or via the scripts/run_app.sh/.bat wrapper,
which cds into it for you). Cargo resolves the
humaipsy-core path dependency automatically, so these commands build/test both crates'
code as needed even though they're invoked from the binary crate's directory; to
target humaipsy-core directly (e.g. running only its tests), pass -p humaipsy-core or
run the same commands from crates/humaipsy-core/ instead. default-members = ["apps/humaipsy"] in the workspace root Cargo.toml means a bare cargo check/test
run from the repository root only covers apps/humaipsy — use --workspace from the
root if you want both crates.
# Fast compile check
cargo check --all-targets
# Run the app (debug build; vault lock is skipped by default in debug — see vault.rs)
cargo run
# Run tests
cargo test
# Lint — CI-equivalent gate, must pass clean
cargo clippy --all-targets -- -D warnings
# Regenerate vault/.master_hash from the command line (prompts for a password)
cargo run --bin gen_master_hash
# Release build
cargo build --release
rust-toolchain.toml pins channel = "stable" and components = ["clippy", "rustfmt"],
so a fresh rustup show in this repo provisions everything the workflow above needs.
Edition is 2024 (rustc >= 1.85), which enables let-chains (if let Some(x) = y && cond)
— this is the idiomatic style already used throughout the codebase for what would
otherwise be nested if let { if ... }. Because channel = "stable" floats forward,
a new stable Rust release can occasionally graduate a new clippy/rustc lint that
trips -D warnings on previously-fine code with zero changes on our side (this
happened with float_literal_f32_fallback — untyped float literals passed to a
generic f32-bound parameter). If CI goes red on main with no relevant diff,
check whether the lint is new before assuming a regression.
[profile.release] (LTO, codegen-units = 1, strip = true) lives in the root
Cargo.toml, not either member's manifest — Cargo only reads profile settings from the
workspace root and silently ignores them in member manifests, so don't move it. Panic
strategy is deliberately left at the default (unwind), not abort: the app spawns
Tokio tasks (rt.spawn) for Ollama requests, and Tokio isolates a panicking task via
unwind + catch_unwind so the rest of the app keeps running; panic = "abort" would
turn any single bad task panic into a full process crash.
.github/workflows/ci.yml runs cargo check/test/clippy -D warnings --workspace
on Linux, Windows, and macOS for every PR — this is the gate that catches breakage
before a merge, and therefore before a release tag is ever cut. Keep it green. It
does not also run on every push to main: merges land through squash-merged PRs
that already ran these checks against the same tree, so re-running on main would
just be duplicate work, not extra coverage. --workspace is load-bearing here — a
plain cargo check/test/clippy invoked from inside a single member's directory
(apps/humaipsy/ or crates/humaipsy-core/) only covers that one package and silently
skips the other member's tests and lints.
Building a release locally
cargo build --release # from apps/humaipsy/, or `cargo build --release -p humaipsy`
That binary is complete: the icons, the emoji font and the dashboard are compiled in, so
it runs from any directory without a sibling folder. Copy vault/.master_hash beside it
only if you want the build to start already unlocked — otherwise the first launch prompts
to create a master password, which is the state to hand to someone else.
Publishing is CI's job (release.yml). There is deliberately no distribute_app script:
one staging path produces the artifacts, not two that can drift apart.
The vault / master password gate
- Release builds always enforce the Argon2id master-password lock. Debug builds skip it
by default (set
HUMAIPSY_ENABLE_PASSWORD_LOCK=1to test lock behavior in a debug build, orHUMAIPSY_SKIP_VAULT=1to force-skip even if the lock env var is set). - The hash lives at
vault/.master_hash(relative to CWD) or can be supplied viaHUMAIPSY_MASTER_HASH. This file is gitignored — never commit a real hash. - KDF tuning (
HUMAIPSY_ARGON2_M_KIB,HUMAIPSY_ARGON2_T,HUMAIPSY_ARGON2_P) is incrates/humaipsy-core/src/vault.rs; see the module doc comment at the top of that file for the full env-var list. vault.rsmust not depend onegui/eframe— it lives inhumaipsy-corespecifically so it can't. It's crypto and state (MasterVault, KDF params); rendering lives inapps/humaipsy/src/ui/vault_panel.rs, which talks to it only throughpubaccessors (is_skip_vault,needs_password_setup,status,master_input_mut,confirm_input_mut,try_unlock,try_setup_password). If you need the UI to observe new state, add an accessor rather than making the fields more visible thanpub(crate)withinvault.rsor pullingeguiintohumaipsy-core.
Ollama integration
OLLAMA_HOST(defaulthttp://127.0.0.1:11434) andOLLAMA_MODELselect the inference target.OLLAMA_HOSTcan be changed at runtime from the Settings panel, which sets the process env var so downstream code picks it up immediately.OLLAMA_MODELis read from the environment exactly once, inHumaiPsy::new(), to seed the initial model selection at startup — after that, the selected model is plain typed state (HumaiPsy::selected_ollama_model) passed explicitly into every inference call; nothing readsOLLAMA_MODELfrom the environment again at runtime. Don't reintroduce a runtimestd::env::set_var("OLLAMA_MODEL", ..)call as a way to propagate a UI change — that was removed specifically because it made the selected model exist in two unsynchronized places at once.- If a caller doesn't have a model configured yet,
humaipsy_core::ollamafalls back to theDEFAULT_OLLAMA_MODELconstant (ollama/mod.rs) — a plain string, not an env-var lookup. HUMAIPSY_AIR_GAP=1disables all outbound HTTP;HUMAIPSY_ALLOW_LOCAL_OLLAMA=0additionally blocks the loopback Ollama call. This is enforced at request time inollama/mod.rs(guard_ollama_request), not just hidden in the UI — don't bypass it when adding new HTTP call sites. Note this is opt-in, not the default: with air-gap off (the default), there's no restriction on what hostOLLAMA_HOSTpoints to, so a remote host receives full experiment transcripts. That's intentional (remote-Ollama is a supported use case), just be aware of it.- Both
reqwest::Clientconstruction sites (ollama/client.rs,ollama/engine.rs) setconnect_timeoutso an unreachable host fails fast.ollama/client.rs(model listing) also sets a short overalltimeoutsince that request should always be quick.ollama/engine.rs(chat inference) deliberately does not set a tight overall timeout — a legitimate generation can stream for minutes — but caps it generously (10 minutes) as a backstop against a connection that goes silent forever after accepting. Don't tighten the inference client's timeout without checking it won't cut off real long-running generations.
Experiment recording
- Per-trial rows are appended to
metrics/<ExpName>.csv(configurable viaHUMAIPSY_METRICS_DIR); the header/schema is incrates/humaipsy-core/src/recording/experiment_csv.rs. Adding a new CSV column is a breaking format change for anyone with existing exported data — treat it as such (bump a version marker, don't just insert a column). - The CSV export filename is derived from
design.name/design.csv_name, both free-text UI fields —csv_path_for_designruns them throughsanitize_file_stem(strips/,\,:, control chars, and leading.) before ever joining them into a path. That's the only place this sanitization happens; if you add another code path that turns a design field into a filesystem path, sanitize there too rather than assuming the UI already validated it. - Every CSV row is also mirrored into a JSONL event ledger at
metrics/runs/<experiment_id>/<run_id>/events.jsonl(recording::ledger::EventLedger, opened lazily byexperiment_csv::ensure_ledgerthe first time a row is emitted for a given session, finalized —system.run_stopped+summary.jsonwith SHA-256 hashes and event counts — inend_experiment()). This used to be fully-implemented, fully-tested, and completely unused; it's wired in now, so a new CSV event type should get a corresponding ledger event automatically (both go through the sharedemit()/ledger_append()path) — don't add a new call site that writes a CSV row without going throughemit(). - The Overview chat's message history is a separate SQLite database at
<metrics output dir>/chat.sqlite(crates/humaipsy-core/src/store.rs+store_schema.rs), independent of the CSV/ledger experiment log, and its path now flows through the sameMetricsConfigoutput directory as the CSV instead of a separately-hardcoded path.Store::opensetsbusy_timeout+ WAL mode, restricts the file to0600on Unix, andStore::conn()recovers from a poisoned mutex instead of panicking — useself.conn()for any new method, neverself.conn.lock().unwrap()directly. Multi-statement writes (seeappend_message) must go throughconn.transaction(), not sequentialexecute()calls, so a crash mid-write can't leave a message row without its generation row or a staleconversations.updated_at. TheStoreitself is opened at most once per path and cached (apps/humaipsy/src/ui/overview_chat/panel.rs'sshared_store, anArc<Store>keyed byPathBuf) — don't callStore::opendirectly from UI render code; go throughshared_store/chat_db_pathinstead, or you'll reintroduce the every-frame-reopen bug that pattern fixed. - Schema changes are "ensure column exists" (
ensure_conversations_columnsinstore_schema.rs), not real stepped migrations — fine for adding a nullable/defaulted column, not sufficient for a column type change or a data backfill. If the schema needs that kind of change, branch onPRAGMA user_versionproperly rather than extending the idempotent-column-add pattern past what it's good for. vault/.master_hashis also restricted to0600on Unix (vault.rs::restrict_to_owner) — it's an Argon2id password hash, readable-by-anyone-on-the-box was the previous state.
Local web dashboard
crates/humaipsy-webis spawned automatically byHumaiPsy::new()(apps/humaipsy/src/app_model.rs::spawn_web_dashboard) onto the Tokio runtimemain.rsalready keeps alive — there is no separate server process to start or stop, and no way to run it without also running the desktop app (by design; seecrates/humaipsy-web/src/lib.rs's doc comment if you want to change that).HUMAIPSY_WEB_BIND(default127.0.0.1:8787) sets the bind address; must be ahost:portsocket address (SocketAddr), not a hostname.HUMAIPSY_WEB_DISABLED=1turns the server off entirely. Both are read once, atHumaiPsy::new()— changing them requires a restart, same asHUMAIPSY_AIR_GAP/HUMAIPSY_METRICS_DIR.- The default bind is loopback-only and that default matters. Experiment
transcripts are potentially sensitive human-subjects data; don't change the
default to
0.0.0.0or otherwise widen exposure without it being an explicit, deliberate ask — this mirrors the same cautionHUMAIPSY_AIR_GAPalready applies to outbound Ollama traffic, just for the inbound direction. - The dashboard is read-only —
crates/humaipsy-web/src/routes.rshas no route that mutates anything. Keep it that way unless de-globalizingpanel.rs's experiment state (see "Known state-management debt" above) has actually happened first; a write endpoint today would have nothing safe to write to. crates/humaipsy-web/web/*.html/.js/.cssare embedded into the binary viainclude_str!inroutes.rs, not served from a filesystem path — editing them requires a rebuild to take effect (no live-reload), and nothing is staged beside the binary for the dashboard to work.- The dashboard is gated by the master-password vault, mirrored via a shared
flag.
HumaiPsyApp::update(ui/mod.rs) syncshumaipsy_core::vault::MasterVault::is_unlocked()intoHumaiPsy::vault_unlocked(humaipsy_web::SharedVaultLock, anArc<AtomicBool>) every frame;routes::get_status/get_messagescheck it first and withhold content whenfalse. If you add a new route that reads any experiment data, check this flag the same way — don't assume the loopback bind alone is enough, since "locked" is a real state a user actively puts the app into and expects respected. See SECURITY.md for the full reasoning.
Release process
Releases are tagged directly on main, not from a dedicated release branch — that's
Git Flow-style overhead this project doesn't need (it earns its keep only once you must
maintain multiple release lines in parallel, e.g. patching an old version while main
has diverged; not the case here). The flow: bump version in apps/humaipsy/Cargo.toml
(and let Cargo.lock update) on its own short-lived branch, PR it into main like any
other change, then tag vX.Y.Z at that merge commit on main and push the tag — that
fires release.yml, which gates on ci.yml, opens a draft release, builds
humaipsy-<version>-{windows-x86_64.zip,linux-x86_64.tar.gz,macos-aarch64.tar.gz},
attaches SHA256SUMS.txt, and publishes.
Before tagging, the three crate versions are kept equal (apps/humaipsy,
crates/humaipsy-core, crates/humaipsy-web): they are path dependencies and are never
published separately, so a split version number only misleads. The window title reads
its version from CARGO_PKG_VERSION, so it cannot drift from the manifest again.
Things to leave alone unless explicitly asked
- Do not merge branches or push to
mainwithout being asked. - Do not push a
v*tag or rungh release create/gh release uploadyourself — that cuts a real, public GitHub Release and is the user's call, not something to do as a side effect of an unrelated change. - Do not run a blanket
cargo fmtacross the whole tree. The codebase predates a consistent formatting pass; a full reformat would bury real changes in noise. Match the surrounding file's existing style when editing, and only fix formatting in lines you're already touching. apps/humaipsy/metrics/*.csv,metrics/runs/,metrics/chat.sqlite, andvault/.master_hashare gitignored local/experiment artifacts — never add them to git.- Don't add a new process-global
OnceLock<Mutex<T>>singleton for state that has a natural owner (HumaiPsy,HumaiPsyApp, or a small nested state struct). See "humaipsy-corevsapps/humaipsy: where new code goes" above —panel.rs/chat.rsused to do this; it's fixed now (seeOverviewChatUiState), don't reintroduce the pattern.
