- Rust 69.8%
- JavaScript 15.9%
- Shell 6.7%
- CSS 4.7%
- TypeScript 1.5%
- Other 1.2%
Per the s47 two-commit pattern: lift commit lands, then a follow-up commit backfills the SHA into prompt.md §1, sessions/49.yml, and the L-09 closure record. |
||
|---|---|---|
| .cargo | ||
| .forgejo/workflows | ||
| crates | ||
| decisions | ||
| docs | ||
| limitations | ||
| runs | ||
| scripts | ||
| sessions | ||
| tests | ||
| .gitignore | ||
| buildenv.sh | ||
| Cargo.toml | ||
| CLAUDE.md | ||
| LICENSE | ||
| Makefile | ||
| pipeline-config.yaml | ||
| prompt.md | ||
| README.md | ||
hero_assistance
A peer-to-peer support / ticketing system for Hero-stack products. No central server, no SaaS, no firewall holes — just product teams running their own per-product ticket server, and customers connecting directly over an encrypted overlay network.
Master tracking issue: https://forge.ourworld.tf/lhumina_code/hero_assistance/issues/1
What it is, plainly
Three roles, three experiences
Customer (end user with a problem)
Installs the Support app once. Enters the product's mycelium address + their email. Gets a magic link in their inbox; clicks it; their device is bound to their account. From then on:
- Files tickets — title + body + pasted screenshots straight from clipboard.
- Sees the support team's responses arrive live (no polling, no refresh).
- Sees "the support agent is looking at your ticket right now" presence.
- If they use multiple Hero products, adds both projects in the UI; sees both inboxes consolidated.
Support agent (product team member triaging tickets)
Same app, different config. Subscribes to all the projects their team owns at once. Sees:
- A consolidated cross-project ticket inbox.
- Per-ticket detail view with threaded comments.
- Real-time presence ("the customer is reading my reply right now").
- Image-rich tickets render inline.
Product team integrator (developer embedding hero_assistance into their own product)
Drops _ui_wasm::App as a Bootstrap-styled component into their existing
Dioxus app. A customer logged into Product X's app sees a "Support" tab;
clicking it opens a fully-functional ticketing UI inline, branded as part
of Product X, scoped to Product X's project. Same component graph the
standalone app uses — one codebase, two shells, embed-ready.
Two ways to ship the UI
- Standalone desktop app —
hero_assistance_app. A native window. Bundles its own mycelium daemon. For support agents juggling customers, and for customers using multiple products. - Embedded Support tab —
_ui_wasm::Appdropped into any Hero (Dioxus) host app. The primary deployment per D-09.
What's invisible to users (works under the hood)
- Mycelium IPv6 for all transport — encrypted by default, no TLS, no certificates, no firewall punch-through.
- One server per product (
hero_assistance_server); SQLite per server; the support team runs theirs, the customer connects to it. - Many-to-many: a client subscribes to N projects; each project hosts N users. Cross-project isolation enforced (per D-04, D-11).
- Auth = email + magic-link + device-binding. Three-layer identity (project / device / user) — the customer never has to think about mycelium addresses.
Roadmap
| Tag | What ships | Status |
|---|---|---|
| v0.1.0 | Backend-complete release. Server, SDK, transport, fanout, presence, image attachments, full-text search, cross-host RPC over mycelium. Dual-pane _app shell with live event stream (left pane) but placeholder right pane. Customer-facing UI is not in this release — drive it via SDK or curl. |
✅ Released |
| v0.2.0 | Phases 15 + 16 + 17 — end-to-end customer-facing UI. Transport-polymorphic SDK (D-16); magic-link enrollment; project-config dialog with live restart-on-mutation (D-17); embed-ready component contract; ticket UI (list / detail / threaded comments / image-paste / drag-drop); standalone _app desktop binary with N-tabbed multi-project mount; Bootstrap loading via wry::register_uri_scheme_protocol (assist:// scheme); dioxus-bootstrap-css typed components everywhere; visual polish + Toast + Placeholder skeletons + breadcrumbs. Closes acceptance criteria 2–8 from the tracking issue. |
🚧 Candidate (s31; tag pending Rule-6 walkthrough sign-off) |
| v0.2.2 | Phase 21 — L-04 closure (additive users.display_name column + partial UNIQUE INDEX on email != '' + pre-flight duplicate-email detection in db.rs::migrate()); L-03 closure (3 inherited collab-snapshot test failures formally #[ignore]d with read-side root-cause analysis at each test site). Patch tag; CI auto-publishes via .forgejo/workflows/build-linux.yaml. |
✅ Released (s33) |
| v0.3.0 | Phases 18 + 18c — spec-as-checklist (D-18) + executable test substrate (D-19). Vision doc, 315-row e2e_checklist, architecture + testing docs as first-class artefacts. 7-layer pyramid (L1 unit / L2 smoke / L3 API integration / L4 E2E shell / L5 Playwright regression / L6 Playwright adversarial / L7 visual). 4-port browser-deployable scheme (scripts/phase13_demo.sh --browser) + Playwright suite. |
✅ Released (s29) |
| v0.3.1 | Phase 22 — L-06 closure (events-over-TCP transport). New /events GET route on _server's existing TCP listener with per-recipient server-side filter as the load-bearing privacy invariant (D-20). SDK adds RpcEndpoint::{Uds, Http} + EventsTransport::{Uds, Http} + MultiProjectClient::register_endpoints (existing UDS-only register preserved as a wrapper). envelope_frames generified over AsyncRead so UDS and TCP transports share a single decoder. Engineering-complete pre-customer. |
✅ Released (s34) |
| Post-v1 | Web gateway for mycelium-blocked networks (L-02), full D-10 storage rename, MCP tool surface (compile 91 OpenRPC methods to MCP), mobile, AI-anything (all explicitly deferred per the tracking issue). | Backlog |
Architecture summary
Topology: many-to-many. A customer's client subscribes to multiple project
servers (one server per supported product / customer engagement). A server
hosts multiple users (customers + support agents). All inter-host transport
runs on mycelium IPv6 — addresses are device-authenticated by the kernel
(D-04, D-05), users authenticate over mycelium with email + magic-link
(D-06), and every server binds its TCP listener to a 400::/7 overlay
address (D-08).
How the UI is built and embedded
Two ways to embed the customer SPA
Hero_assistance is built to be embedded — that's the load-bearing requirement of D-09. Two paths exist; the right one depends on the host's stack.
Path 1 — Native Dioxus composition (Dioxus 0.7 hosts only)
A host whose UI is built in Dioxus 0.7 + dioxus-bootstrap-css can
depend on hero_assistance_ui_wasm_components and drop <App />
directly into its own RSX tree. No iframe. No postMessage. Theme +
state share the host's signal tree natively. The standalone _app
desktop binary uses exactly this path internally — proves the
contract; crates/embed_smoke/ is a deliberate stand-in for any
third-party Dioxus host and exercises the same component graph
against a synthetic RPC backend.
Path 2 — iframe + web_embed postMessage protocols (any web host)
Anything that can render <iframe src="…"> can embed hero_assistance:
hero_router, an Askama-rendered Hero admin dashboard, a non-Hero web
app, a hand-rolled HTML page. Phase 26 (D-25) implemented this in s46:
hero:theme handshake on mount, HeroRouteOutbound envelope on every
navigation, X-Forwarded-Prefix base-path rewrite for any reverse-proxy
mount point. Integration quality scales with how much of the web_embed
skill the host implements, and degrades gracefully:
| Host implements | Result |
|---|---|
Both hero:theme + hero:route |
Full integration: theme syncs, host's outer chrome shows breadcrumbs, deep-link URLs work |
Only hero:theme |
Theme syncs; no host chrome integration; SPA still fully functional |
| Only iframe (no postMessage) | SPA renders with default theme; works internally |
So non-Dioxus hosts are first-class embed targets, not second-class. The "Dioxus" qualifier in D-09 refers to the richer (Path 1) embed path, not the only one.
Where hero_assistance fits in the Hero ecosystem
| Hero piece | Role w.r.t. hero_assistance |
|---|---|
| hero_router | Single TCP entry point. Routes the web / app / admin socket types per hero_sockets to the right binary; can iframe-embed the customer SPA via Path 2. |
| hero_archipelagos | Hosts the customer SPA as an island per the archipelagos skill; D-25 §"Out of scope" tracks the host-side PR. |
| hero_proc | Lifecycle supervisor. scripts/service_assistance.nu (per nu_service) registers _server + _ui + _admin actions; binary discovery via SVX_BINARIES (Phase 24, issue #9). |
hero_proc ADMIN_SECRETS |
Backs the operator IP whitelist that gates _admin's TCP arm (Phase 24 part B; per hero_ui_whitelists). |
| mycelium | All inter-host server transport. _server binds its TCP listener to a 400::/7 overlay address (D-08); device addresses are kernel-authenticated. |
web_embed postMessage protocols |
The wire contract that lets any web host drive theme + breadcrumbs + back/forward — implemented by _ui_wasm in s46. |
| hero_proxy / claims | Out-of-scope for v1 — hero_assistance is an isolated peer-to-peer service that does not consume the central proxy's claim grammar. May change post-v1 if a customer needs it. |
Why we deviated from the canonical Hero dashboard recipe
The hero_ui_dashboard and hero_ui_dashboard_admin skills describe the
canonical Hero pattern: Askama HTML compiled at build time + Unpoly +
rust-embed Bootstrap + server-side aggregation. Most Hero services follow
it (hero_proc, hero_books). It's the right pattern when no forcing
function pulls toward a richer UI stack.
Hero_assistance has one. D-09 §Argument requires the customer SPA to be embeddable as a "Support" tab inside any Hero (Dioxus) host app via direct component composition (Path 1), with one component graph shared between the standalone desktop and any embedder. That requirement is incompatible with Askama-rendered HTML — you cannot compose Askama into a Dioxus tree.
So the customer SPA is Dioxus 0.7 WASM + dioxus-bootstrap-css. Once
that was true, the operator admin dashboard had a choice:
- Build admin in Askama (canonical) — maintain two completely different UI stacks side-by-side, duplicate theme/status/route plumbing.
- Build admin in Dioxus too — share
_ui_wasm_componentsbetween customer and admin, single style language, single theme hook.
D-22 picked option 2. The polish gate that landed with it preserves
wire-compatibility with what hero_ui_dashboard expects, even
though the rendering technology differs:
/openrpc.jsonis byte-passthrough proxied (s40 byte-equality test)./rpcis byte-passthrough except for the localui.*namespace (Phase 24c property tests; Phase 24 part Bui.*dispatch)._adminbindsadmin.sockperhero_sockets§3.2 (D-24);_uibindsapp.sockper §3.5.- Operator IP whitelist via
ADMIN_SECRETSperhero_ui_whitelists(Phase 24 part B). - ConnectionStatus widget per
hero_ui_connection_status. - All required tabs (Logs / Stats / Admin / Docs + Tickets / Users /
Projects / Comments) per
hero_ui_dashboard(Phase 25).
A Hero operator looking at the admin dashboard sees the same shape, the
same tabs, the same socket layout, the same secret store. The renderer
is different; the contract is identical. The deviation costs us no
server-side initial render and the literal grep-based compliance
checks in the skill won't apply verbatim — see D-22 for the equivalent
invariants enforced by drift-guard tests.
What's in the workspace
crates/
├── hero_assistance_server/ JSON-RPC over rpc.sock + events.sock + a
│ TCP listener on a 400::/7 mycelium overlay
│ address; SQLite-backed
├── hero_assistance_ui/ Customer-facing Axum binary; binds app.sock
│ per hero_sockets §3.5; serves the WASM
│ SPA dist + owns the /rpc proxy
├── hero_assistance_ui_wasm/ Dioxus 0.7 WASM customer SPA (Bootstrap
│ 5.3.3 via dioxus-bootstrap-css); D-09
├── hero_assistance_ui_wasm_components/ Shared Dioxus component library consumed
│ by both customer SPA and admin SPA;
│ host-side unit-testable; the surface
│ Path 1 embedders depend on
├── hero_assistance_admin/ Operator admin Axum binary; binds
│ admin.sock per §3.2 + an optional TCP
│ arm gated by an ADMIN_SECRETS IP
│ whitelist; serves the admin SPA dist
│ + ui.* local RPC namespace + the
│ byte-passthrough /openrpc.json proxy;
│ D-22 / D-24
├── hero_assistance_admin_ui_wasm/ Dioxus 0.7 WASM operator admin SPA
│ (Logs / Stats / Admin / Docs + Tickets
│ / Users / Projects / Comments)
├── hero_assistance_app/ Standalone Dioxus desktop binary that
│ aggregates N project subscriptions and
│ embeds _ui_wasm components natively
│ (Path 1; D-13)
├── hero_assistance_sdk/ Auto-generated Rust client via
│ openrpc_client! + MultiProjectClient
│ aggregator (D-11) + RpcTransport trait
│ for transport-polymorphism (D-16)
├── hero_assistance/ CLI (start / stop / status under hero_proc)
└── hero_assistance_examples/ Runnable examples
Plus a throwaway crates/embed_smoke/ (Dioxus desktop binary that mounts
_ui_wasm::App against a synthetic MockRpc + NativeEventsFeed to
verify the Path 1 embed contract holds for arbitrary non-_app Dioxus
host apps; not in workspace.default-members).
Locked architecture decisions live as one file per decision in
decisions/D-*.md — 25 currently (D-01 through D-13 + D-15 through
D-25; D-14 was held-bias and never filed). Intentional gaps live in
limitations/L-*.md (L-01, L-02, L-05, L-07, L-08 currently open;
L-09 added s47 tracks the upstream Hero-stack 0.5.0 → 0.6.0 dep-graph
blocker; L-03, L-04, L-06 closed). The full executable spec lives in
docs/dev/e2e_checklist.md (315 rows, A through M; see
docs/vision/HERO_ASSISTANCE.md for
the stakeholder-facing vision).
Build + test + run
Native build + tests honour [workspace.default-members] so the
WASM-only _ui_wasm crate is excluded from the host build:
cargo build --release # native crates
cargo test --no-fail-fast # full native test suite
WASM dist is built separately via dx:
make dist # produces _ui_wasm dist
# or:
dx build --release --features web -p hero_assistance_ui_wasm
Two-project demo (mirrors the Phase 13 walkthrough — two independent
servers mycelium + hero with seeded data, plus the desktop aggregator):
bash scripts/phase13_demo.sh # spawns 2 servers, seeds, opens _app
bash scripts/phase13_demo.sh --no-app # leaves servers up for curl driving
Both servers' RPC and event sockets land under /tmp/phase13_mycelium
and /tmp/phase13_hero; runs/phase13_demo_state.md describes the
seeded dataset and walkthrough recipes.
Visual regression (for _ui_wasm migration QA):
make visual-diff # 3 routes × 3 viewports; <1% perceptual diff per D-09
Deployment posture
Mycelium binding
hero_assistance_server accepts --mycelium-bind=<spec>:
| Spec | Behavior |
|---|---|
auto:<port> |
Broad-binds [::]:<port> with IPV6_V6ONLY=1. The dispatcher activates enforce_overlay_peer and drops any peer_addr outside 400::/7 to anonymous (D-08; kernel source-address validation prevents non-mycelium local processes from forging an overlay source). |
[<ipv6>]:<port> |
Literal bind to a specific overlay address (use this when you've reserved the host bits and want strict locality). The 400::/7 filter is intentionally bypassed in literal mode — the operator's bind choice IS the constraint. |
Set HERO_ASSISTANCE_MYCELIUM_BIND in the environment instead if you'd
rather not pass it on the CLI; the flag wins when both are set.
Auth modes
| Mode | Behavior |
|---|---|
proxy (production) |
Server reads identity from peer_addr (mycelium-authenticated) plus a users row located via the magic-link flow (auth.request_magic_link → auth.consume_magic_link). Per-RPC caller_id is gated against the binding. |
dev |
Permission checks ALL pass; user existence checks where present (presence.set_status_text, comment.create per Phase 14) still fire. Loud startup banner. Never use in production. |
PRODUCTION =
--auth-mode=proxyALWAYS. Dev mode bypasses every permission check and was designed for hermetic tests + local walkthroughs. A server accidentally launched in dev mode against real customer data will accept any caller as authorised. The runbook (docs/runbook.md) covers thehero_procaction that pins the flag for production deploys.
Operator runbook
Operator docs live in docs/runbook.md:
- Live cross-host smoke procedure (L-07:
sudo setcap cap_net_admin+ep $(which mycelium); cargo test -- --ignored phase12b). - Auth-mode pinning under hero_proc.
- The seven open limitations (L-01 through L-07) and their post-v1 plans.
D-10 conventions (RPC param-name shape)
The wire surface uses two layered vocabularies:
- Stable user-facing nouns —
ticket,comment,presence. Theticket.*andcomment.*methods are the canonical surface; new D-10 methods (e.g.presence.set_viewing_ticket) use these nouns for both the method name AND the params they accept. - Inherited storage names —
channel,message. The collab snapshot's tables, handler module symbols, and CSS classes still carry these names (the post-v1 cleanup that dropsaskama_templateson_uiwill rename them).
ticket.* / comment.* methods are dispatcher-level aliases of the
underlying channel.* / message.* handlers (D-10 §"additive aliasing
only"). Most aliased methods accept the storage-side param name on the
wire (channel_id, message_id) — only the new D-12-era methods use
the user-facing names directly. Keep this in mind when reading the
OpenRPC spec at crates/hero_assistance_server/openrpc.json:
| Method | Wire param | Why |
|---|---|---|
ticket.get / ticket.update / ticket.archive / ticket.delete |
id |
Bare alias; underlying channel.* shape preserved. |
ticket.member.list / ticket.member.add / ticket.member.remove |
channel_id |
Same reason. |
comment.create / comment.update / comment.delete |
channel_id (the parent ticket) |
message.send shape preserved. |
presence.set_viewing_ticket |
ticket_id |
New D-12 method, not an alias. User-facing name throughout. |
This will all be unified in the post-v1 cleanup that drops the inherited collab vocabulary entirely.
Wire + UI contract changes since v0.1.0
Phase 14 (v0.1.0 backend-complete) absorbed the Phase 13b walkthrough findings:
- Slug rule on
namedropped —ticket.create/ticket.updatenow accept any UTF-8 ≤80 chars (uppercase, spaces, punctuation). Tickets are user-facing titles, not slugs. - NotFound mapping —
comment.create,ticket.{get,update,archive,delete},workspace.update, andmessage.deletereturn-32003 NotFoundfor missing primary-key lookups (was-32603 Internal errorwith trace_id). status_textonpresence.update— broadcast envelope carries the user's current status text (subscribers no longer round-trippresence.listper change).- Fanout for ticket-mutating ops —
ticket.archiveemitschannel.archived,ticket.deleteemitschannel.removed. users.idexistence check oncomment.create— symmetric withpresence.set_status_text's gate.
See runs/phase13_findings.md for the walkthrough record.
Phase 17 (v0.2.0 SPA fixes) closed five SPA-side findings from the s23 walkthrough:
- WebSocket reactive-dep fix —
use_events_websocketwasm32 arm now usesuse_resourceinstead ofuse_futureso Dioxus tracks the identity-signal dep. Pre-fix: WS never opened post-enrollment. - Identity localStorage persistence —
_ui_wasm/src/identity_storage.rshydrates on mount, writes on change. F5 / bookmark / paste-URL no longer force re-enrollment. - SPA-router 404 fallback —
_uirouter serves<dist>/index.htmlfor client-router-only paths (/enroll,/tickets/:id, …) so the SPA boots and dispatches client-side. ticket.createcreated_byfallback — resolves fromcaller_idwhen omitted (mirrorscomment.createprecedent).- Honest enrollment copy — "we'll issue a one-time sign-in token; the token is logged to your operator's server log" replaces misleading SMTP-implication wording.
Phase 16 (v0.2.0 UI polish) shipped Bootstrap proper:
assist://custom URL scheme viawry::register_uri_scheme_protocol— bypasses every asset-pipeline failure mode the inline-<style>/ CDN approaches hit.dioxus-bootstrap-csstyped components across every UI surface (Modal, Nav, Badge, Collapse, Placeholder, Breadcrumb).- Toast at composer + enrollment for typed RPC errors.
- Placeholder skeletons during ticket-list + comment-list fetch.
- Window icon bundled (64×64 RGBA via
tao::WindowBuilder::with_window_icon).
Phase 16b-release-fix (s31) closed two desktop bugs surfaced at the Rule-6 walkthrough:
- B5 — empty-launch blank-screen fix: head-inject Bootstrap via
document::Linkand inlineapp.cssinto wry'swith_custom_head(body-level<style>/<link>weren't applied at first paint when no subsequent DOM mutation happened). - B2 — N=1→0 subscription removal deadlock real fix:
ProjectTabdriver task observes atokio::sync::watchshutdown signal viatokio::select!, fired byuse_dropon unmount. Drops the s30-era disable-on-N=1 UI guard.
Persona rename (s31) — the demo orchestrator's two seeded
projects went from acme/globex to mycelium / hero to match
the memory/project_hero_assistance_demo_personas.md convention
the Playwright fixtures already documented.
Testing
7-layer pyramid (D-19; full file map at docs/dev/testing.md). Three load-bearing failures are inherited from upstream (L-03; collab's rate-limit / federation / cleanup tests), unchanged since Phase 1.
| Layer | Where | Count (s31) | Run with |
|---|---|---|---|
| L1 unit | crates/*/src/** #[cfg(test)] |
205 native + 32 wasm32 | cargo test --release --lib |
| L2 smoke | tests/smoke.sh |
16 | bash tests/smoke.sh (against running --browser demo) |
| L3 API integration | crates/hero_assistance_server/tests/integration.rs |
73 | cargo test -p hero_assistance_server --release --test integration |
| L4 E2E flow shell | tests/e2e_journey.sh |
18 | bash tests/e2e_journey.sh (against running --browser demo) |
| L5 Playwright regression | tests/playwright/regression/ |
11 | cd tests && npm run test:regression |
| L6 Playwright adversarial | tests/playwright/adversarial/ |
7 | cd tests && npm run test:adversarial |
| L7 visual regression | tests/baselines/ + tests/perceptual_diff.mjs + hero_browser MCP |
20 baselines (9 askama auto + 6 SPA auto + 5 manual desktop) | make visual-diff (askama side); manual capture for desktop |
| Cross-host smoke (L-07) | phase12b_cross_host_rpc_smoke_attests_via_device_bindings |
1 (#[ignore] by default) |
sudo setcap cap_net_admin+ep $(which mycelium); cargo test -- --ignored phase12b |
Run the 4-port browser scheme (8083 + 8084 → Mycelium project, 8085 + 8086 → Hero project) before L2/L4/L5/L6:
bash scripts/phase13_demo.sh --browser # 2 servers + 2 _ui --dist + 4 socat bridges
Test posture s34: 222 native + 32 wasm32 + 78 integration + 16 L2 + 18 L4 + 11 L5 + 7 L6; 0 failed (3 L-03 inherited tests now formally #[ignore]d s33 with reasoned annotations; 13 ignored total including phase22 cross-host smoke gated by L-07 / CAP_NET_ADMIN); 1 phase10 transient flake (passes in isolation).
Branching + commits
Hero conventions: development is the default branch; feature work goes
on development_<name> (underscores, not slashes); merges back via
squash-merge. Conventional-commit messages with an absolute issue URL.
No AI co-author trailers.
License
See Cargo.toml.