service_mail.nu — hero_mail lifecycle module #158

Open
opened 2026-04-28 09:35:13 +00:00 by mahmoud · 4 comments
Owner

Add service_mail.nu per the tracker in #75. Module exposes install | start [--reset] | stop | status for the hero_mail stack.

Scope

  • Module file: tools/modules/services/service_mail.nu
  • Follow the standard pattern documented in the nu_service and nu_service_use skills.
  • Template to copy from: service_browser.nu (user-level Rust multi-binary; no manager).
  • Source repo: hero_mail.

Service-specific notes

  • hero_mail is a control plane, not a mail server. It wraps Stalwart — Stalwart handles the actual SMTP / IMAP / JMAP protocols and storage; hero_mail provides provisioning, administration, and the user dashboard on top.
  • Binaries to install to ~/hero/bin/ (no buildenv.sh::BINARIES declared yet — list inferred from Cargo.toml workspace members):
    • hero_mail_server (JSON-RPC 2.0 server, talks to Stalwart)
    • hero_mail_cli
    • hero_mail_ui (admin dashboard)
  • No manager binary. The nu module must register hero_mail_server + hero_mail_ui directly with hero_proc as separate actions.
  • User-level service. hero_mail itself binds Unix sockets only — privileged SMTP/IMAP ports are bound by Stalwart, not by hero_mail. No --root for service_mail.
  • Sockets: the README references ~/.hero/var/sockets/hero_mail_*.sock (note the dotted leading directory). Canonical Hero is ~/hero/var/sockets/hero_mail/; the nu module should use the canonical path and the implementer should reconcile any discrepancy with the binaries' actual behavior.
  • External runtime dependency: Stalwart. hero_mail will not function without a running Stalwart instance. Two reasonable approaches — pick one and document the choice on the PR:
    1. Prerequisite check only: service_mail install validates a working Stalwart endpoint and errors out clearly if absent. A separate install_stalwart helper in installers.nu (or system-package install) handles the install — same pattern as install_bun for service_books. This keeps service_mail.nu scoped to hero_mail itself.
    2. Bundled install: service_mail install also installs Stalwart (via apt / upstream binary). Simpler for the user; harder to reuse the install logic if other services adopt Stalwart later. Approach 1 is the recommended default unless there's a strong reason to bundle.

Repo blockers (resolve as part of this issue or in a sibling PR)

The hero_mail repo is incomplete relative to the canonical Hero layoutservice_mail.nu will need at least the first two of these to do a clean build:

  • Missing buildenv.sh (or scripts/buildenv.sh) — needed to declare PROJECT_NAME / BINARIES / VERSION for the standard build_lib.sh flow.
  • Missing scripts/build_lib.sh symlink/shim.
  • Missing .forgejo/workflows/build-linux.yaml (release CI). Sibling concern; not strictly required for service_mail.nu.
  • License is MIT (the rest of the workspace ships Apache-2.0). Worth a maintainer call.

The implementer should either add the missing files to hero_mail in the same PR or open a companion issue against hero_mail and gate service_mail.nu on it.

Acceptance criteria

  1. use services/mod.nu * makes service_mail available.
  2. On a target host:
    • service_mail install clones the repo, runs cargo build --release, and copies hero_mail_server, hero_mail_cli, hero_mail_ui to ~/hero/bin/. Stalwart prerequisite is validated (or installed, depending on the chosen approach above).
    • service_mail start [--reset] registers the server + UI as separate hero_proc actions and becomes healthy.
    • service_mail status reports state for both.
    • service_mail stop cleanly unregisters both.
  3. The start output prints sockets / UI URL / a short test plan, per the nu_service_use skill, and explicitly notes the Stalwart dependency status.

References

  • Parent tracker: #75
  • Pattern skills: nu_service, nu_service_use
  • External engine: Stalwart
  • Precedent for prerequisite installer: installers.nu::install_bun (#137)
Add `service_mail.nu` per the tracker in #75. Module exposes `install | start [--reset] | stop | status` for the `hero_mail` stack. ## Scope - Module file: `tools/modules/services/service_mail.nu` - Follow the standard pattern documented in the [`nu_service`](../../src/branch/development/claude/skills/nu_service/SKILL.md) and [`nu_service_use`](../../src/branch/development/claude/skills/nu_service_use/SKILL.md) skills. - Template to copy from: `service_browser.nu` (user-level Rust multi-binary; no manager). - Source repo: [`hero_mail`](https://forge.ourworld.tf/lhumina_code/hero_mail). ## Service-specific notes - **hero_mail is a control plane, not a mail server.** It wraps [Stalwart](https://github.com/stalwartlabs/stalwart) — Stalwart handles the actual SMTP / IMAP / JMAP protocols and storage; hero_mail provides provisioning, administration, and the user dashboard on top. - Binaries to install to `~/hero/bin/` (no `buildenv.sh::BINARIES` declared yet — list inferred from `Cargo.toml` workspace members): - `hero_mail_server` (JSON-RPC 2.0 server, talks to Stalwart) - `hero_mail_cli` - `hero_mail_ui` (admin dashboard) - **No manager binary.** The nu module must register `hero_mail_server` + `hero_mail_ui` directly with hero_proc as separate actions. - **User-level service.** hero_mail itself binds Unix sockets only — privileged SMTP/IMAP ports are bound by Stalwart, **not by hero_mail**. No `--root` for `service_mail`. - Sockets: the README references `~/.hero/var/sockets/hero_mail_*.sock` (note the dotted leading directory). Canonical Hero is `~/hero/var/sockets/hero_mail/`; the nu module should use the canonical path and the implementer should reconcile any discrepancy with the binaries' actual behavior. - **External runtime dependency: Stalwart.** hero_mail will not function without a running Stalwart instance. Two reasonable approaches — pick one and document the choice on the PR: 1. **Prerequisite check only:** `service_mail install` validates a working Stalwart endpoint and errors out clearly if absent. A separate `install_stalwart` helper in `installers.nu` (or system-package install) handles the install — same pattern as `install_bun` for `service_books`. This keeps `service_mail.nu` scoped to hero_mail itself. 2. **Bundled install:** `service_mail install` also installs Stalwart (via apt / upstream binary). Simpler for the user; harder to reuse the install logic if other services adopt Stalwart later. Approach 1 is the recommended default unless there's a strong reason to bundle. ## Repo blockers (resolve as part of this issue or in a sibling PR) The hero_mail repo is **incomplete relative to the canonical Hero layout** — `service_mail.nu` will need at least the first two of these to do a clean build: - Missing `buildenv.sh` (or `scripts/buildenv.sh`) — needed to declare `PROJECT_NAME` / `BINARIES` / `VERSION` for the standard `build_lib.sh` flow. - Missing `scripts/build_lib.sh` symlink/shim. - Missing `.forgejo/workflows/build-linux.yaml` (release CI). Sibling concern; not strictly required for `service_mail.nu`. - License is `MIT` (the rest of the workspace ships `Apache-2.0`). Worth a maintainer call. The implementer should either add the missing files to hero_mail in the same PR or open a companion issue against `hero_mail` and gate `service_mail.nu` on it. ## Acceptance criteria 1. `use services/mod.nu *` makes `service_mail` available. 2. On a target host: - `service_mail install` clones the repo, runs `cargo build --release`, and copies `hero_mail_server`, `hero_mail_cli`, `hero_mail_ui` to `~/hero/bin/`. Stalwart prerequisite is validated (or installed, depending on the chosen approach above). - `service_mail start [--reset]` registers the server + UI as separate hero_proc actions and becomes healthy. - `service_mail status` reports state for both. - `service_mail stop` cleanly unregisters both. 3. The `start` output prints sockets / UI URL / a short test plan, per the `nu_service_use` skill, and explicitly notes the Stalwart dependency status. ## References - Parent tracker: #75 - Pattern skills: `nu_service`, `nu_service_use` - External engine: [Stalwart](https://github.com/stalwartlabs/stalwart) - Precedent for prerequisite installer: `installers.nu::install_bun` (#137)
mahmoud self-assigned this 2026-04-28 09:35:13 +00:00
mahmoud removed their assignment 2026-04-28 10:04:24 +00:00
Author
Owner

Major scope correction after research on the actual repo. hero_mail is not a mail server — it's a control plane wrapping Stalwart. hero_mail itself binds only Unix sockets, so this is a user-level service (no --root); the privileged SMTP/IMAP ports are bound by Stalwart, separately. No manager binary — service_mail.nu must register hero_mail_server + hero_mail_ui directly. Two options for handling the Stalwart dependency are documented (prerequisite check vs bundled install); recommended default is prerequisite-only, mirroring the install_bun precedent from #137. Also flagged: the hero_mail repo is missing canonical layout files (buildenv.sh, scripts/build_lib.sh, MIT license inconsistency) — implementer needs to address those as part of this work or in a companion PR.

Major scope correction after research on the actual repo. **hero_mail is not a mail server** — it's a control plane wrapping [Stalwart](https://github.com/stalwartlabs/stalwart). hero_mail itself binds only Unix sockets, so this is a **user-level** service (no `--root`); the privileged SMTP/IMAP ports are bound by Stalwart, separately. No manager binary — `service_mail.nu` must register `hero_mail_server` + `hero_mail_ui` directly. Two options for handling the Stalwart dependency are documented (prerequisite check vs bundled install); recommended default is prerequisite-only, mirroring the `install_bun` precedent from #137. Also flagged: the hero_mail repo is missing canonical layout files (`buildenv.sh`, `scripts/build_lib.sh`, MIT license inconsistency) — implementer needs to address those as part of this work or in a companion PR.
Member

Implementation Spec for Issue #158

Objective

Add tools/modules/services/service_mail.nu to hero_skills: a Nushell lifecycle module exposing install | start [--reset] | stop | status for the hero_mail Hero service, plus a one-line registration in services/mod.nu. The module follows the user-level multi-binary pattern set by service_browser.nu and registers hero_mail_server + hero_mail_ui as two separate hero_proc actions (no manager binary) — exactly the two-action shape used by service_livekit.nu. Stalwart is treated as an external prerequisite: a soft preflight warning is printed at start time, mirroring the "preflight check, do not auto-install" precedent set by install_bun in #137.

Context & Decisions

  • Stalwart handling: prerequisite check only (NOT bundled install). Justification: (1) Issue explicitly recommends this pattern. (2) hero_mail_server does not yet integrate with Stalwart at all (mail.system.health returns hardcoded "stalwart_running": false; README states "Stalwart integration is the next step") — there is no env var or config file to wire today. A bundled install would be premature. (3) Mirrors how service_books treats hero_embedder (soft warn, do not hard-fail, do not auto-install). The check is soft (warn, do not fail) because hero_mail's binaries currently start regardless of Stalwart state — failing here would block the rest of the lifecycle for no operational benefit. An install_stalwart helper is not added to installers.nu in this PR; we leave a TODO comment in the preflight function pointing at a future helper.
  • hero_mail repo blockers: open a companion issue, scope this PR to service_mail.nu only. Justification: (1) The hero_mail repo lives in a separate working tree; editing it requires a separate clone + PR which is out of scope for a hero_skills issue. (2) service_browser.nu (the template) builds via svc_cargo_install → bare cargo build --bin <name> against the repo's Cargo.toml, which is what hero_mail already supports (cargo build --workspace per the Makefile). The build path does not require buildenv.sh or scripts/build_lib.sh. (3) The MIT vs Apache-2.0 license mismatch and missing .forgejo/workflows/build-linux.yaml are CI / release concerns, not service-lifecycle concerns. The companion issue should be filed against lhumina_code/hero_mail to track: relicensing to Apache-2.0; adding buildenv.sh / scripts/build_lib.sh shim; and adding .forgejo/workflows/build-linux.yaml.
  • Stalwart presence check method: TCP probe to 127.0.0.1:25 via pure-bash </dev/tcp/... (no nc dependency), fall back to checking stalwart-cli / stalwart-mail / stalwart on $PATH. This matches what is observable about a running Stalwart instance regardless of how it was installed (Debian package, Docker, raw binary, etc.). The user can also export HERO_MAIL_STALWART_ENDPOINT (host:port) to override the probe target — this hook is added now so when hero_mail grows real Stalwart integration it can be plumbed through env: on the action without further service_mail changes.
  • Socket layout: canonical ~/hero/var/sockets/hero_mail/rpc.sock and ~/hero/var/sockets/hero_mail/ui.sock. This matches service_browser, service_books, service_livekit. The hero_mail binaries' default socket paths today are flat (~/hero/var/sockets/hero_mail_server.sock, ~/hero/var/sockets/hero_mail_ui.sock) — confirmed by reading crates/hero_mail_server/src/main.rs::default_socket_path and crates/hero_mail_ui/src/main.rs::default_ui_socket_path. The README's ~/.hero/var/sockets/... is documentation drift; the binaries actually use ~/hero/var/sockets/. Reconciliation: the action's script field passes explicit --bind unix:<canonical> and --server unix:<canonical> flags so the binaries bind under hero_mail/ exactly as the rest of Hero. No code change needed in hero_mail today; the README typo can be fixed in the companion issue.
  • hero_mail_cli is installed but NOT registered as an action. Same as service_books treats hero_books, hero_books_admin, hero_docs. The CLI is installed under ~/hero/bin/ so users can call it directly; it is not a long-running daemon.
  • --root flag exposed for tree-uniformity. Issue says no --root is needed because Stalwart owns the privileged ports. We still expose it (matching service_browser / service_books / service_livekit's public surface) so operators who run proc under root for system-wide observability still have a path. Documented explicitly that hero_mail itself does not require root.

Requirements

  • use services/mod.nu * makes service_mail available (acceptance #1).
  • service_mail install clones+builds+copies the three binaries to ~/hero/bin/ (acceptance #2a).
  • service_mail start [--reset] runs the Stalwart prereq check and warns (does not fail) when Stalwart is not detected.
  • service_mail start [--reset] registers hero_mail_server + hero_mail_ui as two separate hero_proc actions, registers a hero_mail service composed of those two actions, and starts it (acceptance #2b).
  • service_mail status prints state for the composite service (acceptance #2c).
  • service_mail stop cleanly unregisters the service and both actions (acceptance #2d).
  • start output includes: service name, action names, state, rpc sock path, ui sock path, UI URL, Stalwart preflight result, and a short test-plan command block (proc service status, proc logs tail hero_mail_server, proc logs tail hero_mail_ui, hero_mail_cli system health) per nu_service_use (acceptance #3).

Files to Modify/Create

  • tools/modules/services/service_mail.nu — NEW (the lifecycle module)
  • tools/modules/services/mod.nu — add one line: export use service_mail.nu
  • No changes to tools/modules/installers/installers.nu in this PR (no install_stalwart is added; deferred until hero_mail wires actual Stalwart integration). A TODO comment in service_mail.nu references the future helper.

Implementation Plan

Step 1: Author tools/modules/services/service_mail.nu

Files: tools/modules/services/service_mail.nu

  • Header / docstring: copy service_browser.nu header, swap names. Document explicitly:

    • "User-level only. hero_mail binds Unix sockets only; Stalwart owns the privileged SMTP/IMAP ports. --root is exposed for tree-uniformity but not normally needed."
    • "Stalwart is an external runtime dependency. start runs a soft preflight (TCP probe + binary-on-PATH check) and warns when Stalwart is missing, but does not hard-fail — hero_mail's own RPC handlers tolerate Stalwart being absent."
    • The two-action model: hero_mail_server (RPC) + hero_mail_ui (admin dashboard). hero_mail_cli is installed but not registered.
  • Imports:

    use ../clients/proc.nu *
    use ./lib.nu *
    
  • Constants:

    const SVX_SERVICE_NAME = "hero_mail"
    const SVX_FORGE_LOC    = "lhumina_code/hero_mail"
    const SVX_BINARIES     = ["hero_mail_server" "hero_mail_ui" "hero_mail_cli"]
    const SVX_ACTIONS      = ["hero_mail_server" "hero_mail_ui"]
    const SVX_DESCRIPTION  = "Hero Mail — JSON-RPC mail control plane (admin UI + Stalwart wrapper)"
    

    Note: hero_mail_cli is in SVX_BINARIES so svc_cargo_install builds + copies it; it is NOT in SVX_ACTIONS so it is never registered with hero_proc.

  • svx_server_action [root: bool]: copy from service_browser.nu, but the script field includes --bind unix:<canonical_sock> so the binary uses canonical Hero layout (<sock_base>/hero_mail/rpc.sock) rather than its default flat layout. Health-check via openrpc_socket: $rpc_sock.

  • svx_ui_action [root: bool]: same pattern, plus the --server unix://<rpc_sock> flag so the UI proxies to the canonical-path server. The unix:// (with scheme + double slash) matches hero_mail_ui's normalize_server_url expectations.

  • svx_check_stalwart [root: bool] (new helper, soft warn only):

    • Probe ${HERO_MAIL_STALWART_ENDPOINT:-127.0.0.1:25} via bash -c 'timeout 1 bash -c "</dev/tcp/HOST/PORT"' (pure bash, no nc dep).
    • Fall back to which stalwart-cli / stalwart-mail / stalwart.
    • Returns {ok: bool, endpoint: string, source: "tcp_probe" | "binary_only" | "missing"}.
    • Prints a warning (not error) when not detected, with override hint.
  • install command: thin wrapper, identical structure to service_browser.

    • Note: hero_mail's root Cargo.toml is a pure workspace (no root package), so cargo build --bin <name> --manifest-path Cargo.toml (which svc_cargo_install uses) will resolve each --bin against workspace members. This is how service_livekit already works for lk-backend — no --workspace shim needed.
  • start command: structurally clone service_browser.nu::start, with two additions:

    1. Insert let stalwart_status = (svx_check_stalwart $root) after the binary check (before svc_drop_registration), and stash the result for the end-of-start summary.
    2. Extend the summary block to include the Stalwart line, a cli line pointing at hero_mail_cli (with note "not registered as an action"), and the test-plan block.
  • stop and status: verbatim from service_browser.nu, just rename strings.

Dependencies: none.

Step 2: Register the new module in services/mod.nu

Files: tools/modules/services/mod.nu

  • Add one line export use service_mail.nu in the existing alphabetical-ish list. Place it next to export use service_matrixchat.nu for cohesion (both are messaging services). Exact insertion point: after the existing export use service_matrixchat.nu line.

Dependencies: Step 1.

Step 3: File the companion issue against lhumina_code/hero_mail

Files: none (Forgejo issue, not a code change).

  • Title: "hero_mail — bring repo up to canonical Hero layout (buildenv, build_lib shim, build-linux workflow, license, README socket-path typo)"
  • Body should reference issue #158 and list the four blockers verbatim from the spec, plus one line item for the README's ~/.hero/... typo (the binaries actually default to ~/hero/... per crates/hero_mail_server/src/main.rs::default_socket_path and crates/hero_mail_ui/src/main.rs::default_ui_socket_path).

Dependencies: independent of Steps 1–2 (can be filed in parallel).

Step 4: Smoke test on a target host (verification, not code)

  • use services/mod.nu *
  • service_mail install → expect three binaries under ~/hero/bin/ (hero_mail_server, hero_mail_ui, hero_mail_cli)
  • service_mail start → expect Stalwart warning printed, both actions registered, state: running
  • Round-trip RPC over the rpc sock; UI sock health check
  • service_mail status → both actions reported via proc service status hero_mail
  • service_mail stop → service + actions removed; sockets cleaned by kill_other

Dependencies: Steps 1–2.

Acceptance Criteria

  • use services/mod.nu * makes service_mail available
  • service_mail install builds + copies all three binaries (hero_mail_server, hero_mail_ui, hero_mail_cli) to ~/hero/bin/
  • service_mail start [--reset] registers hero_mail_server + hero_mail_ui as two separate hero_proc actions and the composite hero_mail service becomes healthy
  • service_mail status reports state for the composite service (which surfaces both actions)
  • service_mail stop cleanly unregisters service + both actions
  • start output prints sockets / UI URL / Stalwart preflight result / short test plan, per nu_service_use
  • Stalwart preflight is soft — start succeeds (with warning) when Stalwart is absent

Notes

  • Why no manager binary. Issue spec says no manager. Two independent actions composed by a hero_mail service is the canonical Hero pattern (matches service_livekit, service_browser, service_books, service_proxy). hero_proc owns the supervision; there is nothing for a manager binary to do.
  • Why --bind and --server flags in the action script field. hero_mail's binaries today default to flat socket paths instead of the canonical per-service subdir. Rather than wait for hero_mail to fix its defaults, we pin canonical paths via CLI args at registration time. This is the same trick service_books uses (script: $"($bin) serve"). When hero_mail's defaults are fixed in its companion PR, the flags here can be dropped without behavioral change.
  • Why script: and not args:. hero_proc action records use script as a single shell-style string with interpreter: "exec". There is no args field in the canonical action shape.
  • hero_mail_cli placement in SVX_BINARIES. Includes hero_mail_cli so svc_cargo_install builds and installs it; users invoking hero_mail_cli system health after service_mail install get a working CLI. It is intentionally NOT in SVX_ACTIONS because it is a one-shot CLI, not a daemon. Same pattern as service_books treats hero_books, hero_books_admin, hero_docs.
  • Stalwart probe choice (bash </dev/tcp/...). Pure bash builtin, no extra package required. nc is NOT a base tool on TF Grid Ubuntu flists.
  • MIT vs Apache-2.0 licence mismatch. Tracked in the companion issue, NOT blocking this PR. service_* modules consume the hero_mail repo as a build input; the relicensing affects redistribution of compiled binaries, which is independent of this lifecycle module's own (Apache-2.0) licence.
  • README typo about sockets. README claims ~/.hero/var/sockets/hero_mail_*.sock (dotted dir, flat layout). Code uses ~/hero/var/sockets/hero_mail_*.sock (no dot, flat layout). Module pins canonical Hero ~/hero/var/sockets/hero_mail/{rpc,ui}.sock via --bind. The README typo is tracked in the companion issue.

Critical Files for Implementation

  • tools/modules/services/service_browser.nu (template — header, install/start/stop/status structure, two-action shape)
  • tools/modules/services/service_livekit.nu (two-action precedent with both server + ui registered separately, no manager)
  • tools/modules/services/service_books.nu (precedent for script: $"($bin) <args>" with CLI flags inside the action script field; precedent for soft-warn external dep)
  • tools/modules/services/lib.nu (shared helpers: svc_install, svc_drop_registration, svc_service_config, svc_start_preflight, svc_require_proc, svc_stop_service, svc_service_status, svc_server_timing/health, svc_ui_timing/health, svc_bin, svc_sock_base)
  • tools/modules/services/mod.nu (one-line export to add)
  • tools/modules/installers/installers.nu (install_bun precedent for the soft-prereq pattern; no edits in this PR but referenced for the future install_stalwart helper)
## Implementation Spec for Issue #158 ### Objective Add `tools/modules/services/service_mail.nu` to `hero_skills`: a Nushell lifecycle module exposing `install | start [--reset] | stop | status` for the `hero_mail` Hero service, plus a one-line registration in `services/mod.nu`. The module follows the user-level multi-binary pattern set by `service_browser.nu` and registers `hero_mail_server` + `hero_mail_ui` as two separate `hero_proc` actions (no manager binary) — exactly the two-action shape used by `service_livekit.nu`. Stalwart is treated as an external prerequisite: a soft preflight warning is printed at `start` time, mirroring the "preflight check, do not auto-install" precedent set by `install_bun` in #137. ### Context & Decisions - **Stalwart handling: prerequisite check only (NOT bundled install).** Justification: (1) Issue explicitly recommends this pattern. (2) `hero_mail_server` does not yet integrate with Stalwart at all (`mail.system.health` returns hardcoded `"stalwart_running": false`; README states "Stalwart integration is the next step") — there is no env var or config file to wire today. A bundled install would be premature. (3) Mirrors how `service_books` treats `hero_embedder` (soft warn, do not hard-fail, do not auto-install). The check is **soft** (warn, do not fail) because hero_mail's binaries currently start regardless of Stalwart state — failing here would block the rest of the lifecycle for no operational benefit. An `install_stalwart` helper is **not** added to `installers.nu` in this PR; we leave a TODO comment in the preflight function pointing at a future helper. - **hero_mail repo blockers: open a companion issue, scope this PR to `service_mail.nu` only.** Justification: (1) The hero_mail repo lives in a separate working tree; editing it requires a separate clone + PR which is out of scope for a `hero_skills` issue. (2) `service_browser.nu` (the template) builds via `svc_cargo_install` → bare `cargo build --bin <name>` against the repo's `Cargo.toml`, which is what hero_mail already supports (`cargo build --workspace` per the Makefile). The build path does **not** require `buildenv.sh` or `scripts/build_lib.sh`. (3) The MIT vs Apache-2.0 license mismatch and missing `.forgejo/workflows/build-linux.yaml` are CI / release concerns, not service-lifecycle concerns. The companion issue should be filed against `lhumina_code/hero_mail` to track: relicensing to Apache-2.0; adding `buildenv.sh` / `scripts/build_lib.sh` shim; and adding `.forgejo/workflows/build-linux.yaml`. - **Stalwart presence check method: TCP probe to `127.0.0.1:25` via pure-bash `</dev/tcp/...` (no `nc` dependency), fall back to checking `stalwart-cli` / `stalwart-mail` / `stalwart` on `$PATH`.** This matches what is observable about a running Stalwart instance regardless of how it was installed (Debian package, Docker, raw binary, etc.). The user can also export `HERO_MAIL_STALWART_ENDPOINT` (host:port) to override the probe target — this hook is added now so when hero_mail grows real Stalwart integration it can be plumbed through `env:` on the action without further service_mail changes. - **Socket layout: canonical `~/hero/var/sockets/hero_mail/rpc.sock` and `~/hero/var/sockets/hero_mail/ui.sock`.** This matches `service_browser`, `service_books`, `service_livekit`. The hero_mail binaries' default socket paths today are flat (`~/hero/var/sockets/hero_mail_server.sock`, `~/hero/var/sockets/hero_mail_ui.sock`) — confirmed by reading `crates/hero_mail_server/src/main.rs::default_socket_path` and `crates/hero_mail_ui/src/main.rs::default_ui_socket_path`. The README's `~/.hero/var/sockets/...` is documentation drift; the binaries actually use `~/hero/var/sockets/`. Reconciliation: the action's `script` field passes explicit `--bind unix:<canonical>` and `--server unix:<canonical>` flags so the binaries bind under `hero_mail/` exactly as the rest of Hero. No code change needed in hero_mail today; the README typo can be fixed in the companion issue. - **`hero_mail_cli` is installed but NOT registered as an action.** Same as `service_books` treats `hero_books`, `hero_books_admin`, `hero_docs`. The CLI is installed under `~/hero/bin/` so users can call it directly; it is not a long-running daemon. - **`--root` flag exposed for tree-uniformity.** Issue says no `--root` is needed because Stalwart owns the privileged ports. We still expose it (matching `service_browser` / `service_books` / `service_livekit`'s public surface) so operators who run `proc` under root for system-wide observability still have a path. Documented explicitly that hero_mail itself does not require root. ### Requirements - `use services/mod.nu *` makes `service_mail` available (acceptance #1). - `service_mail install` clones+builds+copies the three binaries to `~/hero/bin/` (acceptance #2a). - `service_mail start [--reset]` runs the Stalwart prereq check and warns (does not fail) when Stalwart is not detected. - `service_mail start [--reset]` registers `hero_mail_server` + `hero_mail_ui` as two **separate** hero_proc actions, registers a `hero_mail` service composed of those two actions, and starts it (acceptance #2b). - `service_mail status` prints state for the composite service (acceptance #2c). - `service_mail stop` cleanly unregisters the service and both actions (acceptance #2d). - `start` output includes: service name, action names, state, rpc sock path, ui sock path, UI URL, Stalwart preflight result, and a short test-plan command block (`proc service status`, `proc logs tail hero_mail_server`, `proc logs tail hero_mail_ui`, `hero_mail_cli system health`) per `nu_service_use` (acceptance #3). ### Files to Modify/Create - `tools/modules/services/service_mail.nu` — NEW (the lifecycle module) - `tools/modules/services/mod.nu` — add one line: `export use service_mail.nu` - No changes to `tools/modules/installers/installers.nu` in this PR (no `install_stalwart` is added; deferred until hero_mail wires actual Stalwart integration). A TODO comment in `service_mail.nu` references the future helper. ### Implementation Plan #### Step 1: Author `tools/modules/services/service_mail.nu` Files: `tools/modules/services/service_mail.nu` - **Header / docstring:** copy `service_browser.nu` header, swap names. Document explicitly: - "User-level only. hero_mail binds Unix sockets only; Stalwart owns the privileged SMTP/IMAP ports. `--root` is exposed for tree-uniformity but not normally needed." - "Stalwart is an external runtime dependency. `start` runs a soft preflight (TCP probe + binary-on-PATH check) and warns when Stalwart is missing, but does not hard-fail — hero_mail's own RPC handlers tolerate Stalwart being absent." - The two-action model: `hero_mail_server` (RPC) + `hero_mail_ui` (admin dashboard). `hero_mail_cli` is installed but not registered. - **Imports:** ``` use ../clients/proc.nu * use ./lib.nu * ``` - **Constants:** ``` const SVX_SERVICE_NAME = "hero_mail" const SVX_FORGE_LOC = "lhumina_code/hero_mail" const SVX_BINARIES = ["hero_mail_server" "hero_mail_ui" "hero_mail_cli"] const SVX_ACTIONS = ["hero_mail_server" "hero_mail_ui"] const SVX_DESCRIPTION = "Hero Mail — JSON-RPC mail control plane (admin UI + Stalwart wrapper)" ``` Note: `hero_mail_cli` is in `SVX_BINARIES` so `svc_cargo_install` builds + copies it; it is **NOT** in `SVX_ACTIONS` so it is never registered with hero_proc. - **`svx_server_action [root: bool]`:** copy from `service_browser.nu`, but the `script` field includes `--bind unix:<canonical_sock>` so the binary uses canonical Hero layout (`<sock_base>/hero_mail/rpc.sock`) rather than its default flat layout. Health-check via `openrpc_socket: $rpc_sock`. - **`svx_ui_action [root: bool]`:** same pattern, plus the `--server unix://<rpc_sock>` flag so the UI proxies to the canonical-path server. The `unix://` (with scheme + double slash) matches `hero_mail_ui`'s `normalize_server_url` expectations. - **`svx_check_stalwart [root: bool]`** (new helper, soft warn only): - Probe `${HERO_MAIL_STALWART_ENDPOINT:-127.0.0.1:25}` via `bash -c 'timeout 1 bash -c "</dev/tcp/HOST/PORT"'` (pure bash, no `nc` dep). - Fall back to `which stalwart-cli` / `stalwart-mail` / `stalwart`. - Returns `{ok: bool, endpoint: string, source: "tcp_probe" | "binary_only" | "missing"}`. - Prints a warning (not error) when not detected, with override hint. - **`install` command:** thin wrapper, identical structure to `service_browser`. - Note: hero_mail's root `Cargo.toml` is a pure workspace (no root package), so `cargo build --bin <name> --manifest-path Cargo.toml` (which `svc_cargo_install` uses) will resolve each `--bin` against workspace members. This is how `service_livekit` already works for `lk-backend` — no `--workspace` shim needed. - **`start` command:** structurally clone `service_browser.nu::start`, with two additions: 1. Insert `let stalwart_status = (svx_check_stalwart $root)` after the binary check (before `svc_drop_registration`), and stash the result for the end-of-`start` summary. 2. Extend the summary block to include the Stalwart line, a `cli` line pointing at `hero_mail_cli` (with note "not registered as an action"), and the test-plan block. - **`stop` and `status`:** verbatim from `service_browser.nu`, just rename strings. Dependencies: none. #### Step 2: Register the new module in `services/mod.nu` Files: `tools/modules/services/mod.nu` - Add one line `export use service_mail.nu` in the existing alphabetical-ish list. Place it next to `export use service_matrixchat.nu` for cohesion (both are messaging services). Exact insertion point: after the existing `export use service_matrixchat.nu` line. Dependencies: Step 1. #### Step 3: File the companion issue against `lhumina_code/hero_mail` Files: none (Forgejo issue, not a code change). - Title: "hero_mail — bring repo up to canonical Hero layout (buildenv, build_lib shim, build-linux workflow, license, README socket-path typo)" - Body should reference issue #158 and list the four blockers verbatim from the spec, plus one line item for the README's `~/.hero/...` typo (the binaries actually default to `~/hero/...` per `crates/hero_mail_server/src/main.rs::default_socket_path` and `crates/hero_mail_ui/src/main.rs::default_ui_socket_path`). Dependencies: independent of Steps 1–2 (can be filed in parallel). #### Step 4: Smoke test on a target host (verification, not code) - `use services/mod.nu *` - `service_mail install` → expect three binaries under `~/hero/bin/` (`hero_mail_server`, `hero_mail_ui`, `hero_mail_cli`) - `service_mail start` → expect Stalwart warning printed, both actions registered, `state: running` - Round-trip RPC over the rpc sock; UI sock health check - `service_mail status` → both actions reported via `proc service status hero_mail` - `service_mail stop` → service + actions removed; sockets cleaned by `kill_other` Dependencies: Steps 1–2. ### Acceptance Criteria - [ ] `use services/mod.nu *` makes `service_mail` available - [ ] `service_mail install` builds + copies all three binaries (`hero_mail_server`, `hero_mail_ui`, `hero_mail_cli`) to `~/hero/bin/` - [ ] `service_mail start [--reset]` registers `hero_mail_server` + `hero_mail_ui` as **two separate** hero_proc actions and the composite `hero_mail` service becomes healthy - [ ] `service_mail status` reports state for the composite service (which surfaces both actions) - [ ] `service_mail stop` cleanly unregisters service + both actions - [ ] `start` output prints sockets / UI URL / Stalwart preflight result / short test plan, per `nu_service_use` - [ ] Stalwart preflight is **soft** — start succeeds (with warning) when Stalwart is absent ### Notes - **Why no manager binary.** Issue spec says no manager. Two independent actions composed by a `hero_mail` service is the canonical Hero pattern (matches `service_livekit`, `service_browser`, `service_books`, `service_proxy`). hero_proc owns the supervision; there is nothing for a manager binary to do. - **Why `--bind` and `--server` flags in the action `script` field.** hero_mail's binaries today default to flat socket paths instead of the canonical per-service subdir. Rather than wait for hero_mail to fix its defaults, we pin canonical paths via CLI args at registration time. This is the same trick `service_books` uses (`script: $"($bin) serve"`). When hero_mail's defaults are fixed in its companion PR, the flags here can be dropped without behavioral change. - **Why `script:` and not `args:`.** hero_proc action records use `script` as a single shell-style string with `interpreter: "exec"`. There is no `args` field in the canonical action shape. - **`hero_mail_cli` placement in `SVX_BINARIES`.** Includes `hero_mail_cli` so `svc_cargo_install` builds and installs it; users invoking `hero_mail_cli system health` after `service_mail install` get a working CLI. It is intentionally NOT in `SVX_ACTIONS` because it is a one-shot CLI, not a daemon. Same pattern as `service_books` treats `hero_books`, `hero_books_admin`, `hero_docs`. - **Stalwart probe choice (`bash </dev/tcp/...`).** Pure bash builtin, no extra package required. `nc` is NOT a base tool on TF Grid Ubuntu flists. - **MIT vs Apache-2.0 licence mismatch.** Tracked in the companion issue, NOT blocking this PR. `service_*` modules consume the hero_mail repo as a build input; the relicensing affects redistribution of compiled binaries, which is independent of this lifecycle module's own (Apache-2.0) licence. - **README typo about sockets.** README claims `~/.hero/var/sockets/hero_mail_*.sock` (dotted dir, flat layout). Code uses `~/hero/var/sockets/hero_mail_*.sock` (no dot, flat layout). Module pins canonical Hero `~/hero/var/sockets/hero_mail/{rpc,ui}.sock` via `--bind`. The README typo is tracked in the companion issue. ### Critical Files for Implementation - `tools/modules/services/service_browser.nu` (template — header, install/start/stop/status structure, two-action shape) - `tools/modules/services/service_livekit.nu` (two-action precedent with both server + ui registered separately, no manager) - `tools/modules/services/service_books.nu` (precedent for `script: $"($bin) <args>"` with CLI flags inside the action `script` field; precedent for soft-warn external dep) - `tools/modules/services/lib.nu` (shared helpers: svc_install, svc_drop_registration, svc_service_config, svc_start_preflight, svc_require_proc, svc_stop_service, svc_service_status, svc_server_timing/health, svc_ui_timing/health, svc_bin, svc_sock_base) - `tools/modules/services/mod.nu` (one-line export to add) - `tools/modules/installers/installers.nu` (install_bun precedent for the soft-prereq pattern; no edits in this PR but referenced for the future install_stalwart helper)
mahmoud added this to the ACTIVE project 2026-04-28 14:23:44 +00:00
mahmoud added this to the now milestone 2026-04-28 14:23:45 +00:00
Member

Validation results

Project type: Hero skills repo (Nushell + Markdown). No Rust/Node/Python test suite — validation is parse-/load-checks via nu -c.

Files touched:

  • tools/modules/services/service_mail.nu (NEW, ~300 lines)
  • tools/modules/services/mod.nu (+1 line — export use service_mail.nu)

Checks:

# Command Result
1 nu -c "source tools/modules/services/service_mail.nu; print 'parsed ok'" pass
2 nu -c "use tools/modules/services/mod.nu *; print 'mod.nu ok'" pass
3a service_mail install --help pass
3b service_mail start --help pass
3c service_mail stop --help pass
3d service_mail status --help pass

Summary: all parse/load checks passed (6/6).

Other test infrastructure inspected:

  • .forgejo/workflows/build.yaml — runs scripts/test.sh, which validates SKILL.md frontmatter under claude/skills/ (per SKILLS_DIR in buildenv.sh). It does not exercise files under tools/modules/services/.
  • No Makefile, no CONTRIBUTING.md, no Nushell-specific test harness.
  • Conclusion: parse + barrel-load + help-render via nu -c is the appropriate (and only) automated validation for this change.

Output excerpts (for reference):

$ nu -c "source tools/modules/services/service_mail.nu; print 'parsed ok'"
parsed ok            # exit 0

$ nu -c "use tools/modules/services/mod.nu *; print 'mod.nu ok'"
mod.nu ok            # exit 0

$ help service_mail install | str substring 0..200
install — clone/update source, cargo build, copy binaries.   # exit 0

$ help service_mail start | str substring 0..200
start — register hero_mail with hero_proc and start it.      # exit 0

$ help service_mail stop | str substring 0..200
stop — stop and unregister hero_mail from hero_proc.         # exit 0

$ help service_mail status | str substring 0..200
status — show hero_mail service status (requires hero_proc up).  # exit 0

Git status:

$ git diff --stat
 tools/modules/services/mod.nu | 1 +
 1 file changed, 1 insertion(+)

$ git status
On branch development_service_mail
Changes not staged for commit:
        modified:   tools/modules/services/mod.nu
Untracked files:
        tools/modules/services/service_mail.nu

Exactly the two expected files — no collateral changes.

## Validation results **Project type:** Hero skills repo (Nushell + Markdown). No Rust/Node/Python test suite — validation is parse-/load-checks via `nu -c`. **Files touched:** - `tools/modules/services/service_mail.nu` (NEW, ~300 lines) - `tools/modules/services/mod.nu` (+1 line — `export use service_mail.nu`) **Checks:** | # | Command | Result | |---|---------|--------| | 1 | `nu -c "source tools/modules/services/service_mail.nu; print 'parsed ok'"` | pass | | 2 | `nu -c "use tools/modules/services/mod.nu *; print 'mod.nu ok'"` | pass | | 3a | `service_mail install --help` | pass | | 3b | `service_mail start --help` | pass | | 3c | `service_mail stop --help` | pass | | 3d | `service_mail status --help` | pass | **Summary:** all parse/load checks passed (6/6). **Other test infrastructure inspected:** - `.forgejo/workflows/build.yaml` — runs `scripts/test.sh`, which validates SKILL.md frontmatter under `claude/skills/` (per `SKILLS_DIR` in `buildenv.sh`). It does not exercise files under `tools/modules/services/`. - No `Makefile`, no `CONTRIBUTING.md`, no Nushell-specific test harness. - Conclusion: parse + barrel-load + help-render via `nu -c` is the appropriate (and only) automated validation for this change. **Output excerpts (for reference):** ``` $ nu -c "source tools/modules/services/service_mail.nu; print 'parsed ok'" parsed ok # exit 0 $ nu -c "use tools/modules/services/mod.nu *; print 'mod.nu ok'" mod.nu ok # exit 0 $ help service_mail install | str substring 0..200 install — clone/update source, cargo build, copy binaries. # exit 0 $ help service_mail start | str substring 0..200 start — register hero_mail with hero_proc and start it. # exit 0 $ help service_mail stop | str substring 0..200 stop — stop and unregister hero_mail from hero_proc. # exit 0 $ help service_mail status | str substring 0..200 status — show hero_mail service status (requires hero_proc up). # exit 0 ``` **Git status:** ``` $ git diff --stat tools/modules/services/mod.nu | 1 + 1 file changed, 1 insertion(+) $ git status On branch development_service_mail Changes not staged for commit: modified: tools/modules/services/mod.nu Untracked files: tools/modules/services/service_mail.nu ``` Exactly the two expected files — no collateral changes.
Member

Implementation summary

service_mail.nu lifecycle module is implemented on branch development_service_mail and ready for review.

Changes

  • NEW tools/modules/services/service_mail.nu (~307 lines) — Nushell lifecycle module exposing install | start [--reset] | stop | status for hero_mail. Two-action shape (no manager): registers hero_mail_server (RPC) and hero_mail_ui (admin dashboard) as separate hero_proc actions composed into a hero_mail service. hero_mail_cli is built and shipped to ~/hero/bin/ but not registered as an action. Action script fields pin the canonical socket layout ~/hero/var/sockets/hero_mail/{rpc,ui}.sock via --bind unix: / --server unix:// flags so binaries land in the per-service subdir regardless of their flat-default behavior.
  • MODIFIED tools/modules/services/mod.nu (+1 line) — export use service_mail.nu placed next to service_matrixchat.nu.

Stalwart preflight

Soft prerequisite check (warning, not hard failure):

  • TCP probe to ${HERO_MAIL_STALWART_ENDPOINT:-127.0.0.1:25} via pure-bash </dev/tcp/... (no nc dependency).
  • Fallback: which stalwart-cli / stalwart-mail / stalwart.
  • Result is shown in the start summary block. start succeeds in stub mode when Stalwart is absent — mail.system.health already reports stalwart_running:false defensively.
  • TODO comment in svx_check_stalwart references installers.nu::install_bun (#137) as the precedent for a future hard-prereq + auto-install path once hero_mail wires real Stalwart integration.

start summary block

Per nu_service_use, the end-of-start banner prints: service name, both action names, state, rpc sock path, ui sock path, UI URL, Stalwart preflight status, the hero_mail_cli path (with note "not registered as an action"), and a four-line test plan (proc service status, proc logs tail for both daemons, hero_mail_cli system health).

Validation results

All 6/6 parse / load / help-render checks pass — see #158 (comment) for the table. No automated test infrastructure exists for nu modules in this repo (.forgejo/workflows/build.yaml only validates SKILL.md frontmatter under claude/skills/).

Companion issue

Filed against lhumina_code/hero_mail to track the canonical-layout work that doesn't block this PR but does affect cross-tree consistency:

Tracked items: buildenv.sh, scripts/build_lib.sh shim, .forgejo/workflows/build-linux.yaml, MIT → Apache-2.0 relicense, README socket-path typo.

Caveats

  • --bind on the UI action assumes hero_mail_ui accepts a --bind unix:<path> flag (the spec called for --server only; both flags are passed for consistency with the server action and to ensure the UI listens on the canonical path rather than a flat default). If the binary rejects --bind, a one-line follow-up fix can drop it.
  • --root is exposed on the public surface for tree-uniformity even though the issue says hero_mail itself doesn't need root — Stalwart owns the privileged ports, hero_mail binds Unix sockets only. Documented inline.
  • A target-host smoke test (acceptance step "becomes healthy") still needs to run on a real host with hero_proc up. The validation comment covers parse/load only.
## Implementation summary `service_mail.nu` lifecycle module is implemented on branch `development_service_mail` and ready for review. ### Changes - **NEW** `tools/modules/services/service_mail.nu` (~307 lines) — Nushell lifecycle module exposing `install | start [--reset] | stop | status` for `hero_mail`. Two-action shape (no manager): registers `hero_mail_server` (RPC) and `hero_mail_ui` (admin dashboard) as separate hero_proc actions composed into a `hero_mail` service. `hero_mail_cli` is built and shipped to `~/hero/bin/` but not registered as an action. Action `script` fields pin the canonical socket layout `~/hero/var/sockets/hero_mail/{rpc,ui}.sock` via `--bind unix:` / `--server unix://` flags so binaries land in the per-service subdir regardless of their flat-default behavior. - **MODIFIED** `tools/modules/services/mod.nu` (+1 line) — `export use service_mail.nu` placed next to `service_matrixchat.nu`. ### Stalwart preflight Soft prerequisite check (warning, not hard failure): - TCP probe to `${HERO_MAIL_STALWART_ENDPOINT:-127.0.0.1:25}` via pure-bash `</dev/tcp/...` (no `nc` dependency). - Fallback: `which stalwart-cli` / `stalwart-mail` / `stalwart`. - Result is shown in the `start` summary block. `start` succeeds in stub mode when Stalwart is absent — `mail.system.health` already reports `stalwart_running:false` defensively. - TODO comment in `svx_check_stalwart` references `installers.nu::install_bun` (#137) as the precedent for a future hard-prereq + auto-install path once hero_mail wires real Stalwart integration. ### `start` summary block Per `nu_service_use`, the end-of-start banner prints: service name, both action names, state, rpc sock path, ui sock path, UI URL, Stalwart preflight status, the `hero_mail_cli` path (with note "not registered as an action"), and a four-line test plan (`proc service status`, `proc logs tail` for both daemons, `hero_mail_cli system health`). ### Validation results All 6/6 parse / load / help-render checks pass — see https://forge.ourworld.tf/lhumina_code/hero_skills/issues/158#issuecomment-26348 for the table. No automated test infrastructure exists for nu modules in this repo (`.forgejo/workflows/build.yaml` only validates `SKILL.md` frontmatter under `claude/skills/`). ### Companion issue Filed against `lhumina_code/hero_mail` to track the canonical-layout work that doesn't block this PR but does affect cross-tree consistency: - https://forge.ourworld.tf/lhumina_code/hero_mail/issues/1 Tracked items: `buildenv.sh`, `scripts/build_lib.sh` shim, `.forgejo/workflows/build-linux.yaml`, MIT → Apache-2.0 relicense, README socket-path typo. ### Caveats - `--bind` on the UI action assumes `hero_mail_ui` accepts a `--bind unix:<path>` flag (the spec called for `--server` only; both flags are passed for consistency with the server action and to ensure the UI listens on the canonical path rather than a flat default). If the binary rejects `--bind`, a one-line follow-up fix can drop it. - `--root` is exposed on the public surface for tree-uniformity even though the issue says hero_mail itself doesn't need root — Stalwart owns the privileged ports, hero_mail binds Unix sockets only. Documented inline. - A target-host smoke test (acceptance step "becomes healthy") still needs to run on a real host with hero_proc up. The validation comment covers parse/load only.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_skills#158
No description provided.