- Rust 91%
- HTML 4.2%
- TypeScript 2.8%
- Shell 1.4%
- JavaScript 0.5%
- Other 0.1%
|
Some checks failed
lab publish / publish (push) Failing after 30s
Remove explicit version constraints from hero_proc_sdk, hero_sdk_derive, hero_rpc_openrpc, herolib_core, and hero_proxy_sdk dependencies where git branch is set to development. This allows Cargo to use the version from the git repository head instead of enforcing a specific pinned version. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> |
||
|---|---|---|
| .cargo | ||
| .forgejo/workflows | ||
| crates | ||
| docs | ||
| runs | ||
| scripts | ||
| tests | ||
| .gitignore | ||
| buildenv.sh | ||
| Cargo.toml | ||
| LICENSE | ||
| 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 Alignment meta-issue (open): https://forge.ourworld.tf/lhumina_code/hero_assistance/issues/15
What it is, plainly
Three roles, three experiences
Customer (end user with a problem)
Opens the Support UI in a browser. Enters the product's mycelium address and their email. Gets a magic link in their inbox, clicks it, and their device is bound to their account. From then on:
- Files tickets with title + body + pasted screenshots straight from the clipboard.
- Sees the support team's responses arrive live over SSE (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 and sees both inboxes consolidated.
Support agent (product team member triaging tickets)
Same UI, 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)
Mounts the customer-facing hero_assistance_ui daemon behind their reverse
proxy or in an iframe inside their Hero admin dashboard. The
web_embed
postMessage protocols (hero:theme + hero:route) sync theme and breadcrumbs
into the host's outer chrome. The customer sees a Support tab inside the host
app, scoped to that host's project, branded as part of the host.
Architecture summary
Topology: many-to-many. A customer's browser 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).
The UI is server-rendered (Axum + Askama + Unpoly + EventSource SSE) per
D-26 (retire-Dioxus-deviation; the in-repo decisions/ directory was
scrubbed in s2-021, so D-NN records now live in the workspace's shared
decisions/ under hero_work, or as Forge-issue ADR comments). The customer
SSE channel inherits per-recipient privacy from the same per-user
broadcast::Sender the WebSocket uses, per D-27 (per-ticket SSE wire
shape and D-20 inheritance; same survival path as D-26).
What's invisible to users
- 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. - Many-to-many: a client subscribes to N projects, each project hosts N users. Cross-project isolation enforced (per D-04).
- Auth = email + magic-link + device-binding. Three-layer identity (project / device / user); the customer never has to think about mycelium addresses.
How the UI is embedded
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. 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, UI still fully functional |
| Only iframe (no postMessage) | UI renders with default theme, works internally |
The handshake is implemented in crates/hero_assistance_ui/templates/base.html
as vanilla-JS receivers per the web_embed skill. X-Forwarded-Prefix
base-path rewrite supports any reverse-proxy mount point.
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 UI. |
| hero_proc | Lifecycle supervisor. Registers _server + _ui + _admin via lab service hero_assistance --install. |
hero_proc ADMIN_SECRETS |
Backs the operator IP whitelist that gates _admin's TCP arm 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. |
| hero_admin_lib | Provides the operator-admin shell (Bootstrap + Unpoly + <hero-*> web components) used by _admin. |
web_embed postMessage protocols |
The wire contract that lets any web host drive theme + breadcrumbs + back/forward; implemented by _ui and _admin base.html templates. |
| 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. |
What's in the workspace
crates/
├── hero_assistance/ CLI / lifecycle owner. Started via
│ `lab service hero_assistance --install --start`.
├── 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; server-rendered
│ Askama + Unpoly + per-ticket SSE live tail
│ (D-27). Hosts the /rpc byte-passthrough
│ proxy (D-04) and the /tickets/{id}/_messages
│ SSE channel.
├── hero_assistance_admin/ Operator admin Axum binary; binds admin.sock
│ per §3.2 + an optional TCP arm gated by
│ an ADMIN_SECRETS IP whitelist; server-rendered
│ Askama + hero_admin_lib middleware/assets +
│ Bootstrap + Unpoly + `<hero-*>` web
│ components (8 tabs: Tickets / Users /
│ Projects / Comments + Logs / Stats /
│ Admin / Docs). Hosts the byte-passthrough
│ /openrpc.json + /rpc proxies (D-04, D-21).
├── hero_assistance_sdk/ Auto-generated Rust client via
│ openrpc_client! + MultiProjectClient
│ aggregator (D-11) + RpcTransport trait
│ for transport-polymorphism (D-16)
└── hero_assistance_examples/ Runnable examples
Locked architecture decisions: D-01 through D-27 (minus D-14, held-bias
and never filed). D-26 retires D-09 + D-17 + D-22 + D-25 atomically (the
Dioxus-era SPA and config-persistence-layer decisions); D-27 locks the
per-ticket SSE wire shape and how it inherits the D-20 per-recipient privacy
invariant. The in-repo decisions/D-*.md directory was scrubbed in s2-021
(Phase Z AI-pipeline cleanup); the canonical record now lives in the
workspace's shared decisions/ directory (under the hero_work checkout that
drives this repo) or as Forge-issue ADR comments. A backup of the D-01..D-27
content from before the scrub is preserved at
/tmp/scrub-backup/hero_assistance-prefilter.git (local-only mirror) +
archive/hero_assistance-pre-scrub-2026-05-23/ (local-only copy) for
reconstruction if needed.
Intentional gaps: L-01, L-02, L-05, L-07 remain open; L-03, L-04, L-06, L-08,
L-09 closed. The in-repo limitations/L-*.md directory was also scrubbed in
s2-021; limitation framing for the open set is preserved inline in
docs/dev/e2e_checklist.md Section K rows K-16, K-17, K-19 (L-02, L-05,
L-07 respectively; L-01 is the cross-language test-substrate parity
question carried in this repo's earlier audit logs). The full executable
spec lives in docs/dev/e2e_checklist.md; see
docs/vision/HERO_ASSISTANCE.md for the
stakeholder-facing vision.
Build + test + run
Native Cargo workspace; no WASM target after D-26.
cargo build --release --workspace
cargo test --release --workspace --no-fail-fast
cargo fmt --check
cargo clippy --release --workspace --all-targets -- -D warnings
Service lifecycle goes through lab service:
lab service hero_assistance --install # build all 4 binaries + register with hero_proc
lab service hero_assistance --start # start _server + _ui + _admin under hero_proc
lab service hero_assistance --stop
lab service hero_assistance --status
lab infocheck # canonical-shape gate (expect 4 ok / 0 findings)
CI auto-publish via .forgejo/workflows/lab-publish.yaml on every push to
development: builds linux-musl-x86_64, refreshes releases/tag/latest.
A deploy host re-pulls with lab build hero_assistance --download --install.
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 open limitations (L-01, L-02, L-05, 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.
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. |
Testing
Test posture: cargo test --release --workspace --no-fail-fast reports
254 pass / 1 fail / 14 ignored. The single failure
(phase24b_ui_add_access_fails_when_hero_proc_unreachable) is a documented
pre-existing host-env flake that fires when hero_proc_server is reachable
on the host running the test; it does not block CI on clean runners.
Workspace pre-merge gate:
cargo fmt --check
cargo clippy --release --workspace --all-targets -- -D warnings
cargo build --workspace --release
cargo test --release --workspace --no-fail-fast
lab infocheck reports 4 crate(s) clean, 0 crate(s) with issues, 0 finding(s) total.
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.