service_foundry.nu — hero_foundry server + UI lifecycle module #94

Closed
opened 2026-04-20 10:43:08 +00:00 by mahmoud · 3 comments
Owner

Child of #75.

Objective

Add tools/modules/services/service_foundry.nu implementing install | start | stop | status for the hero_foundry service (version control + repo browser).

Scope — with a known ambiguity to resolve in spec

There are two separate repos both relevant to "foundry":

  1. lhumina_code/hero_foundrybuildenv.sh says BINARIES="hero_foundry hero_foundry_server hero_foundry_ui". The hero_foundry.toml in hero_zero declares BOTH [server] (binary hero_foundry_server) and [ui] (binary hero_foundry_ui start) pointing at this repo. This is the canonical "foundry" stack.

  2. lhumina_code/hero_foundry_ui — separate repo with BINARIES="hero_foundry hero_foundry_ui_server hero_foundry_ui". Has its own hero_foundry_ui.toml in hero_zero that defines a standalone service with exec = sh -c 'cd /root/hero/share/hero_foundry_ui && exec __HERO_BIN__/hero_foundry_ui'.

Both buildenv.sh files set PROJECT_NAME="hero_foundry" — they collide on binary name hero_foundry_ui.

Decision to lock in the spec (Plan agent should probe both repos' main.rs and pick):

  • Option A: single service_foundry.nu covering hero_foundry only (server + UI both from that repo, matching hero_foundry.toml). Skip hero_foundry_ui.toml / repo entirely; someone else handles that service later.
  • Option B: service_foundry covers hero_foundry repo AND a second module service_foundry_ui.nu (separate PR, separate sub-issue) covers the standalone repo.
  • Option C: Some other resolution after inspecting both repos.

My recommendation: Option A for this PR — tightest scope, matches the hero_foundry.toml which is the more complete service definition. The hero_foundry_ui repo and its TOML look like an alternate/legacy config; needs an owner decision before we invest.

Scope (assuming Option A)

  • Repo: ssh://git@forge.ourworld.tf/lhumina_code/hero_foundry.git
  • Binaries (per hero_foundry/buildenv.sh): hero_foundry, hero_foundry_server, hero_foundry_ui
  • Runtime actions: hero_foundry_server, hero_foundry_ui
  • TOML reference: hero_zero/services/hero_foundry.toml
  • Server exec with inline flags: hero_foundry_server --allow-dir __HERO_VAR__/hero_foundry/repos --webdav-storage __HERO_VAR__/hero_foundry/webdav
  • UI exec with subcommand: hero_foundry_ui start
  • Shared env (from TOML top-level [env], applies to both):
    • RUST_LOG=info
    • HERO_FOUNDRY_REPOS=__HERO_VAR__/hero_foundry/repos
    • HERO_FOUNDRY_BASE_PATH=/hero_foundry/ui
  • Sockets: expected $HERO_SOCKET_DIR/hero_foundry/{rpc,ui}.sock (spec should confirm via main.rs).
  • Dependencies: none declared in TOML.
  • Workspace: virtual (confirm in spec).

Acceptance criteria

  • use services/mod.nu * makes service_foundry available.
  • service_foundry install [--root] [--update] [--reset] clones + builds all 3 binaries.
  • service_foundry start [--reset] [--root] [--update] registers both actions + service; passes the --allow-dir / --webdav-storage flags to the server action; invokes hero_foundry_ui start for the UI action.
  • Server action env sets RUST_LOG, HERO_FOUNDRY_REPOS, HERO_FOUNDRY_BASE_PATH with __HERO_VAR__ resolved at register time via svc_home $root.
  • service_foundry status [--root] reports state.
  • service_foundry stop [--root] cleanly unregisters.
  • Module entry added to packages.nu services_extra.
  • Smoke-tested on Hetzner: install → start --reset → running → stop.

Template

  • Base: service_voice.nu (most recent merged, uses the latest --reset/--update patterns and svc_bins_ok short-circuit).
  • Reference for the serve/start subcommand pattern: service_books.nu (uses script: $"($bin) serve").
  • Reference for __HERO_VAR__ / env-var resolution at register time: service_books.nu's HERO_BOOKS_DATA + HERO_EMBEDDER_URL pattern.

Questions the spec must answer

  1. Option A/B/C decision (see above).
  2. UI binary subcommand: TOML says hero_foundry_ui start — confirm via main.rs that it requires start (not serve).
  3. Server inline flags: pass --allow-dir and --webdav-storage in script: with resolved paths, or set them via env?
  4. Socket paths: confirm via crates/*/src/main.rs.
Child of #75. ## Objective Add `tools/modules/services/service_foundry.nu` implementing `install | start | stop | status` for the **hero_foundry** service (version control + repo browser). ## Scope — with a known ambiguity to resolve in spec There are **two separate repos** both relevant to "foundry": 1. **`lhumina_code/hero_foundry`** — `buildenv.sh` says `BINARIES="hero_foundry hero_foundry_server hero_foundry_ui"`. The `hero_foundry.toml` in hero_zero declares BOTH `[server]` (binary `hero_foundry_server`) and `[ui]` (binary `hero_foundry_ui start`) pointing at this repo. This is the canonical "foundry" stack. 2. **`lhumina_code/hero_foundry_ui`** — separate repo with `BINARIES="hero_foundry hero_foundry_ui_server hero_foundry_ui"`. Has its own `hero_foundry_ui.toml` in hero_zero that defines a standalone service with `exec = sh -c 'cd /root/hero/share/hero_foundry_ui && exec __HERO_BIN__/hero_foundry_ui'`. Both buildenv.sh files set `PROJECT_NAME="hero_foundry"` — they collide on binary name `hero_foundry_ui`. **Decision to lock in the spec** (Plan agent should probe both repos' `main.rs` and pick): - Option A: single `service_foundry.nu` covering hero_foundry only (server + UI both from that repo, matching `hero_foundry.toml`). Skip hero_foundry_ui.toml / repo entirely; someone else handles that service later. - Option B: service_foundry covers hero_foundry repo AND a second module `service_foundry_ui.nu` (separate PR, separate sub-issue) covers the standalone repo. - Option C: Some other resolution after inspecting both repos. My recommendation: **Option A** for this PR — tightest scope, matches the `hero_foundry.toml` which is the more complete service definition. The `hero_foundry_ui` repo and its TOML look like an alternate/legacy config; needs an owner decision before we invest. ## Scope (assuming Option A) - **Repo**: `ssh://git@forge.ourworld.tf/lhumina_code/hero_foundry.git` - **Binaries** (per `hero_foundry/buildenv.sh`): `hero_foundry`, `hero_foundry_server`, `hero_foundry_ui` - **Runtime actions**: `hero_foundry_server`, `hero_foundry_ui` - **TOML reference**: `hero_zero/services/hero_foundry.toml` - **Server exec** with inline flags: `hero_foundry_server --allow-dir __HERO_VAR__/hero_foundry/repos --webdav-storage __HERO_VAR__/hero_foundry/webdav` - **UI exec** with subcommand: `hero_foundry_ui start` - **Shared env** (from TOML top-level `[env]`, applies to both): - `RUST_LOG=info` - `HERO_FOUNDRY_REPOS=__HERO_VAR__/hero_foundry/repos` - `HERO_FOUNDRY_BASE_PATH=/hero_foundry/ui` - **Sockets**: expected `$HERO_SOCKET_DIR/hero_foundry/{rpc,ui}.sock` (spec should confirm via main.rs). - **Dependencies**: none declared in TOML. - **Workspace**: virtual (confirm in spec). ## Acceptance criteria - [ ] `use services/mod.nu *` makes `service_foundry` available. - [ ] `service_foundry install [--root] [--update] [--reset]` clones + builds all 3 binaries. - [ ] `service_foundry start [--reset] [--root] [--update]` registers both actions + service; passes the `--allow-dir` / `--webdav-storage` flags to the server action; invokes `hero_foundry_ui start` for the UI action. - [ ] Server action env sets `RUST_LOG`, `HERO_FOUNDRY_REPOS`, `HERO_FOUNDRY_BASE_PATH` with `__HERO_VAR__` resolved at register time via `svc_home $root`. - [ ] `service_foundry status [--root]` reports state. - [ ] `service_foundry stop [--root]` cleanly unregisters. - [ ] Module entry added to `packages.nu` `services_extra`. - [ ] Smoke-tested on Hetzner: install → start --reset → running → stop. ## Template - Base: `service_voice.nu` (most recent merged, uses the latest `--reset`/`--update` patterns and `svc_bins_ok` short-circuit). - Reference for the `serve`/`start` subcommand pattern: `service_books.nu` (uses `script: $"($bin) serve"`). - Reference for `__HERO_VAR__` / env-var resolution at register time: `service_books.nu`'s `HERO_BOOKS_DATA` + `HERO_EMBEDDER_URL` pattern. ## Questions the spec must answer 1. Option A/B/C decision (see above). 2. UI binary subcommand: TOML says `hero_foundry_ui start` — confirm via `main.rs` that it requires `start` (not `serve`). 3. Server inline flags: pass `--allow-dir` and `--webdav-storage` in `script:` with resolved paths, or set them via env? 4. Socket paths: confirm via `crates/*/src/main.rs`.
mahmoud self-assigned this 2026-04-20 10:45:16 +00:00
mahmoud added this to the ACTIVE project 2026-04-20 10:45:18 +00:00
mahmoud added this to the now milestone 2026-04-20 10:45:20 +00:00
Author
Owner

Implementation Spec for Issue #94

Objective

Add tools/modules/services/service_foundry.nu — an install | start | stop | status lifecycle module for the hero_foundry service (server + UI) that registers both actions with hero_proc and mirrors the authoritative Rust build_service_definition() at hero_foundry/crates/hero_foundry/src/main.rs:102-181.

Requirements

  • Covers the lhumina_code/hero_foundry repo only. Option A confirmed — the separate lhumina_code/hero_foundry_ui repo is a distinct Actix-based service with its own store/issues/wiki, out of scope for this sub-issue (future sub-issue after owner decision).
  • Binaries built: hero_foundry, hero_foundry_server, hero_foundry_ui (per hero_foundry/buildenv.sh:6).
  • Runtime actions: hero_foundry_server, hero_foundry_ui. hero_foundry CLI is installed but NOT registered.
  • Server action script includes --allow-dir and --webdav-storage flags with paths resolved at register time (see Notes — env-var fallback not viable because the server's clap parser doesn't declare env attributes for those flags).
  • UI action script invokes hero_foundry_ui start literally as declared in hero_zero/services/hero_foundry.toml:12. UI binary has no clap parser so start is a harmless no-op token, but kept for TOML parity.
  • --root, --update, --reset flags on install and start, matching the service_voice.nu contract.

Files to Modify/Create

  • tools/modules/services/service_foundry.nu (new, ~210 lines, modeled on service_voice.nu with service_biz.nu-style inline-script flags).
  • tools/modules/services/mod.nu (+1 line: export use service_foundry.nu appended after service_voice.nu).
  • tools/modules/services/packages.nu (+1 use service_foundry.nu at top, +1 entry in services_extra after the service_voice entry).

Implementation Plan

Step 1: Copy baseline from service_voice.nu

Files: tools/modules/services/service_foundry.nu

  • Clone service_voice.nu:1-203. Rename every hero_voicehero_foundry, every service_voiceservice_foundry.
  • Set constants:
    • SVX_SERVICE_NAME = "hero_foundry"
    • SVX_FORGE_LOC = "lhumina_code/hero_foundry"
    • SVX_BINARIES = ["hero_foundry" "hero_foundry_server" "hero_foundry_ui"]
    • SVX_ACTIONS = ["hero_foundry_server" "hero_foundry_ui"]
  • Source confirmations: binary list from hero_foundry/buildenv.sh:6; action names from hero_foundry/crates/hero_foundry/src/main.rs:105 (server) and main.rs:142 (ui).
    Dependencies: none.

Step 2: Write svx_server_action with inline flags + env

Files: tools/modules/services/service_foundry.nu
Model on service_voice.nu:11-50 + service_biz.nu:81-126 inline-script pattern. Resolve paths at register time via svc_home $root:

let bin         = (svc_bin "hero_foundry_server" $root)
let sock_base   = (svc_sock_base $root)
let data_repos  = $"(svc_home $root)/var/hero_foundry/repos"
let data_webdav = $"(svc_home $root)/var/hero_foundry/webdav"
# script: $"($bin) --allow-dir ($data_repos) --webdav-storage ($data_webdav)"
  • name: "hero_foundry_server" (confirmed main.rs:105).
  • interpreter: "exec" — required when script contains spaces/flags.
  • env: {RUST_LOG: "info", HERO_FOUNDRY_REPOS: $data_repos, HERO_FOUNDRY_BASE_PATH: "/hero_foundry/ui"}.
    • HERO_FOUNDRY_BASE_PATH is consumed at hero_foundry_server/src/main.rs:155 (baked into ServerConfig.base_path).
    • HERO_FOUNDRY_REPOS is cosmetic/TOML-parity (only read by hero_foundry_examples/examples/seed_data.rs:18).
  • retry_policy: max_attempts: 5, delay_ms: 2000, backoff: true, max_delay_ms: 60000, start_timeout_ms: 30000, stability_period_ms: 30000 (Rust source main.rs:110-116 + nu-convention completion).
  • stop_signal: "SIGTERM", stop_timeout_ms: 10000, timeout_ms: 0, tty: false (main.rs:107-108).
  • kill_other.socket: [$"($sock_base)/hero_foundry/rpc.sock"] (confirmed hero_foundry_server/src/main.rs:199).
  • health_checks[0]: action: "hero_foundry_server", openrpc_socket: $"($sock_base)/hero_foundry/rpc.sock", policy interval_ms: 2000, timeout_ms: 5000, retries: 3, start_period_ms: 3000 (main.rs:133-138).
    Dependencies: Step 1.

Step 3: Write svx_ui_action with start subcommand

Files: tools/modules/services/service_foundry.nu
Model on service_voice.nu:52-91 + service_books.nu:118-124 subcommand shape:

let bin = (svc_bin "hero_foundry_ui" $root)
# script: $"($bin) start"
  • name: "hero_foundry_ui", interpreter: "exec", env: {RUST_LOG: "info"}.
  • UI reads no Hero-specific env vars (grep confirms zero matches for HERO_FOUNDRY* in hero_foundry/crates/hero_foundry_ui/). Base-path prefixing uses the X-Forwarded-Prefix HTTP middleware (hero_foundry_ui/src/main.rs:58-71), not env.
  • retry_policy.max_attempts: 3 (main.rs:148) + nu-convention completions.
  • stop_timeout_ms: 5000.
  • kill_other.socket: [$"($sock_base)/hero_foundry/ui.sock"] (confirmed hero_foundry_ui/src/main.rs:128).
  • health_checks[0]: interval_ms: 3000, timeout_ms: 5000, retries: 3, start_period_ms: 5000 (main.rs:168-172).
    Dependencies: Step 1.

Step 4: svx_service_config

Files: tools/modules/services/service_foundry.nu
Identical to service_voice.nu:93-105 with:

  • description: "Hero Foundry — version control and code repository system" (from hero_zero/services/hero_foundry.toml:3).
    Dependencies: Step 1.

Step 5: svx_drop_registration

Files: tools/modules/services/service_foundry.nu
Identical to service_voice.nu:107-113.
Dependencies: Step 1.

Step 6: install

Files: tools/modules/services/service_foundry.nu
Identical to service_voice.nu:115-127. No workspace pre-build needed — hero_foundry/Cargo.toml is a virtual workspace, svc_cargo_install handles all three binaries directly (unlike hero_books which has a hybrid workspace).
Dependencies: Step 1.

Step 7: start

Files: tools/modules/services/service_foundry.nu
Model on service_voice.nu:129-180. Order: svc_require_sudo (if --root) → svc_require_proc "service_foundry" → early-exit if already running (unless --reset/--update) → install → binary existence check on hero_foundry_serversvx_drop_registration → register server action → register UI action → register service → start → settle → summary.

Summary block prints rpc/ui sockets + the data directory paths (repos, webdav) so operators see where repos are expected to land:

print $"  repos dir : ($data_repos)"
print $"  webdav    : ($data_webdav)"
print $"  base path : /hero_foundry/ui"

No preflight for external sockets — TOML declares no dependencies.
Dependencies: Steps 2-6.

Step 8: stop and status

Files: tools/modules/services/service_foundry.nu
Identical to service_voice.nu:182-202.
Dependencies: Step 5.

Step 9: Module header docstring

Files: tools/modules/services/service_foundry.nu
30-40 line preamble in the style of service_biz.nu:1-60. Must explicitly document:

  • The two-repo collision (hero_foundry vs hero_foundry_ui); this module covers only hero_foundry. Future sub-issue for the separate Actix-based repo.
  • Authoritative source: hero_foundry/crates/hero_foundry/src/main.rs:102-181.
  • Server script passes --allow-dir + --webdav-storage inline because the clap parser does not declare [env: ...] fallbacks.
  • UI start token is decorative (no clap parser in the UI binary); kept for TOML parity.
  • HERO_FOUNDRY_REPOS is cosmetic — only read by hero_foundry_examples; kept for operator expectations.
  • HERO_FOUNDRY_BASE_PATH is server-side (not UI) — the server uses it to render prefixed links.
    Dependencies: Steps 1-8.

Step 10: Wire into mod.nu

Files: tools/modules/services/mod.nu
Append export use service_foundry.nu at the end (after service_voice.nu). Preserves newest-at-end convention.
Dependencies: Step 1.

Step 11: Wire into packages.nu

Files: tools/modules/services/packages.nu

  • Add use service_foundry.nu in the use block at the top, after the existing use service_voice.nu.
  • Append to services_extra after the service_voice entry:
    {name: "service_foundry",    run: {|| if $reset { service_foundry install --reset } else if $update { service_foundry install --update } else { service_foundry install }}}
    

Dependencies: Step 1.

Step 12: Syntax check

  • nu -c "source tools/modules/services/service_foundry.nu; print parse-ok".
  • nu -c "source tools/modules/services/packages.nu; print parse-ok".
  • nu -c "use tools/modules/services/mod.nu *; scope commands | where name =~ '^service_foundry '" — expect 4 subcommands.
    Dependencies: Steps 1-11.

Step 13: Smoke test on Hetzner

Run under flock to serialize with the other agent. hero_proc must be up first.

  1. service_foundry install --root — clone, build 3 binaries, copy to /root/hero/bin/.
  2. service_foundry install --root again — expect svc_bins_ok short-circuit.
  3. service_foundry start --reset --root — both actions registered, service running.
  4. ls /root/hero/var/sockets/hero_foundry/rpc.sock + ui.sock present.
  5. curl --unix-socket rpc.sock http://localhost/ — HTTP response.
  6. curl --unix-socket ui.sock http://localhost/ — HTTP response.
  7. service_foundry status --root — record shows running, 0 restarts.
  8. proc action get hero_foundry_server --root — verify env has RUST_LOG, HERO_FOUNDRY_REPOS, HERO_FOUNDRY_BASE_PATH.
  9. proc action get hero_foundry_ui --root — verify env has only RUST_LOG.
  10. ls /root/hero/var/hero_foundry/repos/ and webdav/ auto-created by server.
  11. service_foundry start --root — idempotent "already running".
  12. Observe 15s — no new restarts.
  13. service_foundry stop --root — clean unregister.
  14. Post-stop service_foundry status --rootservice 'hero_foundry' not found.
    Dependencies: Steps 1-12.

Acceptance Criteria

  • use services/mod.nu * makes service_foundry available with 4 subcommands.
  • service_foundry install [--root] [--update] [--reset] clones + builds 3 binaries via svc_cargo_install; --reset forces rebuild, else svc_bins_ok short-circuits.
  • service_foundry start [--reset] [--root] [--update] registers both actions + service; server script: contains --allow-dir <svc_home>/var/hero_foundry/repos --webdav-storage <svc_home>/var/hero_foundry/webdav; UI script: contains <bin>/hero_foundry_ui start.
  • Server action env: RUST_LOG=info, HERO_FOUNDRY_REPOS=<svc_home>/var/hero_foundry/repos, HERO_FOUNDRY_BASE_PATH=/hero_foundry/ui.
  • UI action env: RUST_LOG=info only.
  • kill_other.socket on server = <sock_base>/hero_foundry/rpc.sock; on UI = <sock_base>/hero_foundry/ui.sock.
  • service_foundry status [--root] delegates to proc service status hero_foundry.
  • service_foundry stop [--root] cleanly unregisters.
  • lib.nu not modified.
  • mod.nu has export use service_foundry.nu appended.
  • packages.nu has use service_foundry.nu in imports AND an entry in services_extra with the --reset/--update passthrough pattern.
  • Smoke test passes all 14 scenarios above on the Hetzner box.

Notes

Option A/B/C — A confirmed. Read both main.rs:

  • hero_foundry/crates/hero_foundry_ui/src/main.rs (649 lines): Axum, rust_embed static assets, askama templates, FoundryClient SDK proxying to server over rpc.sock. Pure read-only repo browser.
  • hero_foundry_ui/crates/hero_foundry_ui/src/main.rs (2111 lines): Actix, actix_session, opens .forge/.heroforge files directly, own FoundryStore with issues/projects/wiki/PRs, reads REPO_PATH env, org-based routing /{org}/{repo}/....
    These are distinct services with non-overlapping feature sets. Option A covers hero_foundry only; lhumina_code/hero_foundry_ui needs its own sub-issue after an owner decision.

UI subcommand verification. hero_foundry/crates/hero_foundry_ui/src/main.rs:86-89:

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    run_server().await
}

No clap, no argv parsing — main ignores all arguments. start is decorative; kept for TOML parity.

Flags vs env on server action. Server clap at hero_foundry/crates/hero_foundry_server/src/main.rs:46-74 exposes --allow-dir, --webdav-storage, --repo, --read-write, --max-cache, --bind, --default-repo. None declare [env: ...] fallbacks. --allow-dir and --webdav-storage MUST be CLI args — inline script: is the only option (same pattern as service_biz.nu).

Env var placement.

  • HERO_FOUNDRY_BASE_PATH → server. Read at hero_foundry_server/src/main.rs:155, used by ServerConfig.base_path to prefix links in rendered responses.
  • HERO_FOUNDRY_REPOS → server (cosmetic). Grep across hero_foundry/crates/ finds only one read, in hero_foundry_examples/examples/seed_data.rs:18. Keeping it on the server action matches the TOML [env] block and operator expectations.
  • RUST_LOG → both.
  • UI reads no Hero-specific env vars. Base-path prefixing uses X-Forwarded-Prefix HTTP header.

Socket paths verified.

  • Server rpc: $HERO_SOCKET_DIR/hero_foundry/rpc.sock (from socket_path("rpc") at hero_foundry_server/src/main.rs:199).
  • UI: $HERO_SOCKET_DIR/hero_foundry/ui.sock (from socket_path("ui") at hero_foundry_ui/src/main.rs:128).
  • hero_foundry/crates/hero_foundry_ui_server/src/main.rs:35 binds a different rpc path — this binary is an SDK shim, NOT in SVX_BINARIES, NOT registered.

Workspace layout. hero_foundry/Cargo.toml is a virtual workspace. All three binaries reachable through svc_cargo_install directly — no hybrid pre-build like service_books.nu needs.

Data directories. Server auto-creates --webdav-storage path via std::fs::create_dir_all at hero_foundry_server/src/main.rs:121. The --allow-dir path for repos must exist or be creatable; server errors out if not.

Critical Files for Implementation

  • /Users/mahmoud/code/forge.ourworld.tf/lhumina_code/hero_skills/tools/modules/services/service_voice.nu
  • /Users/mahmoud/code/forge.ourworld.tf/lhumina_code/hero_skills/tools/modules/services/service_biz.nu
  • /Users/mahmoud/code/forge.ourworld.tf/lhumina_code/hero_skills/tools/modules/services/service_books.nu
  • /Users/mahmoud/code/forge.ourworld.tf/lhumina_code/hero_skills/tools/modules/services/packages.nu
  • /Users/mahmoud/code/forge.ourworld.tf/lhumina_code/hero_foundry/crates/hero_foundry/src/main.rs
## Implementation Spec for Issue #94 ### Objective Add `tools/modules/services/service_foundry.nu` — an `install | start | stop | status` lifecycle module for the `hero_foundry` service (server + UI) that registers both actions with `hero_proc` and mirrors the authoritative Rust `build_service_definition()` at `hero_foundry/crates/hero_foundry/src/main.rs:102-181`. ### Requirements - Covers the `lhumina_code/hero_foundry` repo only. **Option A confirmed** — the separate `lhumina_code/hero_foundry_ui` repo is a distinct Actix-based service with its own store/issues/wiki, out of scope for this sub-issue (future sub-issue after owner decision). - Binaries built: `hero_foundry`, `hero_foundry_server`, `hero_foundry_ui` (per `hero_foundry/buildenv.sh:6`). - Runtime actions: `hero_foundry_server`, `hero_foundry_ui`. `hero_foundry` CLI is installed but NOT registered. - Server action script includes `--allow-dir` and `--webdav-storage` flags with paths resolved at register time (see Notes — env-var fallback not viable because the server's clap parser doesn't declare env attributes for those flags). - UI action script invokes `hero_foundry_ui start` literally as declared in `hero_zero/services/hero_foundry.toml:12`. UI binary has no clap parser so `start` is a harmless no-op token, but kept for TOML parity. - `--root`, `--update`, `--reset` flags on `install` and `start`, matching the `service_voice.nu` contract. ### Files to Modify/Create - `tools/modules/services/service_foundry.nu` (new, ~210 lines, modeled on `service_voice.nu` with `service_biz.nu`-style inline-script flags). - `tools/modules/services/mod.nu` (+1 line: `export use service_foundry.nu` appended after `service_voice.nu`). - `tools/modules/services/packages.nu` (+1 `use service_foundry.nu` at top, +1 entry in `services_extra` after the `service_voice` entry). ### Implementation Plan #### Step 1: Copy baseline from `service_voice.nu` Files: `tools/modules/services/service_foundry.nu` - Clone `service_voice.nu:1-203`. Rename every `hero_voice` → `hero_foundry`, every `service_voice` → `service_foundry`. - Set constants: - `SVX_SERVICE_NAME = "hero_foundry"` - `SVX_FORGE_LOC = "lhumina_code/hero_foundry"` - `SVX_BINARIES = ["hero_foundry" "hero_foundry_server" "hero_foundry_ui"]` - `SVX_ACTIONS = ["hero_foundry_server" "hero_foundry_ui"]` - Source confirmations: binary list from `hero_foundry/buildenv.sh:6`; action names from `hero_foundry/crates/hero_foundry/src/main.rs:105` (server) and `main.rs:142` (ui). Dependencies: none. #### Step 2: Write `svx_server_action` with inline flags + env Files: `tools/modules/services/service_foundry.nu` Model on `service_voice.nu:11-50` + `service_biz.nu:81-126` inline-script pattern. Resolve paths at register time via `svc_home $root`: ``` let bin = (svc_bin "hero_foundry_server" $root) let sock_base = (svc_sock_base $root) let data_repos = $"(svc_home $root)/var/hero_foundry/repos" let data_webdav = $"(svc_home $root)/var/hero_foundry/webdav" # script: $"($bin) --allow-dir ($data_repos) --webdav-storage ($data_webdav)" ``` - `name: "hero_foundry_server"` (confirmed `main.rs:105`). - `interpreter: "exec"` — required when `script` contains spaces/flags. - `env: {RUST_LOG: "info", HERO_FOUNDRY_REPOS: $data_repos, HERO_FOUNDRY_BASE_PATH: "/hero_foundry/ui"}`. - `HERO_FOUNDRY_BASE_PATH` is consumed at `hero_foundry_server/src/main.rs:155` (baked into `ServerConfig.base_path`). - `HERO_FOUNDRY_REPOS` is cosmetic/TOML-parity (only read by `hero_foundry_examples/examples/seed_data.rs:18`). - `retry_policy`: `max_attempts: 5, delay_ms: 2000, backoff: true, max_delay_ms: 60000, start_timeout_ms: 30000, stability_period_ms: 30000` (Rust source `main.rs:110-116` + nu-convention completion). - `stop_signal: "SIGTERM", stop_timeout_ms: 10000, timeout_ms: 0, tty: false` (`main.rs:107-108`). - `kill_other.socket: [$"($sock_base)/hero_foundry/rpc.sock"]` (confirmed `hero_foundry_server/src/main.rs:199`). - `health_checks[0]`: `action: "hero_foundry_server"`, `openrpc_socket: $"($sock_base)/hero_foundry/rpc.sock"`, policy `interval_ms: 2000, timeout_ms: 5000, retries: 3, start_period_ms: 3000` (`main.rs:133-138`). Dependencies: Step 1. #### Step 3: Write `svx_ui_action` with `start` subcommand Files: `tools/modules/services/service_foundry.nu` Model on `service_voice.nu:52-91` + `service_books.nu:118-124` subcommand shape: ``` let bin = (svc_bin "hero_foundry_ui" $root) # script: $"($bin) start" ``` - `name: "hero_foundry_ui"`, `interpreter: "exec"`, `env: {RUST_LOG: "info"}`. - UI reads no Hero-specific env vars (grep confirms zero matches for `HERO_FOUNDRY*` in `hero_foundry/crates/hero_foundry_ui/`). Base-path prefixing uses the `X-Forwarded-Prefix` HTTP middleware (`hero_foundry_ui/src/main.rs:58-71`), not env. - `retry_policy.max_attempts: 3` (`main.rs:148`) + nu-convention completions. - `stop_timeout_ms: 5000`. - `kill_other.socket: [$"($sock_base)/hero_foundry/ui.sock"]` (confirmed `hero_foundry_ui/src/main.rs:128`). - `health_checks[0]`: `interval_ms: 3000, timeout_ms: 5000, retries: 3, start_period_ms: 5000` (`main.rs:168-172`). Dependencies: Step 1. #### Step 4: `svx_service_config` Files: `tools/modules/services/service_foundry.nu` Identical to `service_voice.nu:93-105` with: - `description: "Hero Foundry — version control and code repository system"` (from `hero_zero/services/hero_foundry.toml:3`). Dependencies: Step 1. #### Step 5: `svx_drop_registration` Files: `tools/modules/services/service_foundry.nu` Identical to `service_voice.nu:107-113`. Dependencies: Step 1. #### Step 6: `install` Files: `tools/modules/services/service_foundry.nu` Identical to `service_voice.nu:115-127`. No workspace pre-build needed — `hero_foundry/Cargo.toml` is a virtual workspace, `svc_cargo_install` handles all three binaries directly (unlike `hero_books` which has a hybrid workspace). Dependencies: Step 1. #### Step 7: `start` Files: `tools/modules/services/service_foundry.nu` Model on `service_voice.nu:129-180`. Order: `svc_require_sudo` (if `--root`) → `svc_require_proc "service_foundry"` → early-exit if already running (unless `--reset`/`--update`) → `install` → binary existence check on `hero_foundry_server` → `svx_drop_registration` → register server action → register UI action → register service → start → settle → summary. Summary block prints rpc/ui sockets + the data directory paths (`repos`, `webdav`) so operators see where repos are expected to land: ``` print $" repos dir : ($data_repos)" print $" webdav : ($data_webdav)" print $" base path : /hero_foundry/ui" ``` No preflight for external sockets — TOML declares no dependencies. Dependencies: Steps 2-6. #### Step 8: `stop` and `status` Files: `tools/modules/services/service_foundry.nu` Identical to `service_voice.nu:182-202`. Dependencies: Step 5. #### Step 9: Module header docstring Files: `tools/modules/services/service_foundry.nu` 30-40 line preamble in the style of `service_biz.nu:1-60`. Must explicitly document: - The two-repo collision (`hero_foundry` vs `hero_foundry_ui`); this module covers only `hero_foundry`. Future sub-issue for the separate Actix-based repo. - Authoritative source: `hero_foundry/crates/hero_foundry/src/main.rs:102-181`. - Server script passes `--allow-dir` + `--webdav-storage` inline because the clap parser does not declare `[env: ...]` fallbacks. - UI `start` token is decorative (no clap parser in the UI binary); kept for TOML parity. - `HERO_FOUNDRY_REPOS` is cosmetic — only read by `hero_foundry_examples`; kept for operator expectations. - `HERO_FOUNDRY_BASE_PATH` is server-side (not UI) — the server uses it to render prefixed links. Dependencies: Steps 1-8. #### Step 10: Wire into `mod.nu` Files: `tools/modules/services/mod.nu` Append `export use service_foundry.nu` at the end (after `service_voice.nu`). Preserves newest-at-end convention. Dependencies: Step 1. #### Step 11: Wire into `packages.nu` Files: `tools/modules/services/packages.nu` - Add `use service_foundry.nu` in the `use` block at the top, after the existing `use service_voice.nu`. - Append to `services_extra` after the `service_voice` entry: ``` {name: "service_foundry", run: {|| if $reset { service_foundry install --reset } else if $update { service_foundry install --update } else { service_foundry install }}} ``` Dependencies: Step 1. #### Step 12: Syntax check - `nu -c "source tools/modules/services/service_foundry.nu; print parse-ok"`. - `nu -c "source tools/modules/services/packages.nu; print parse-ok"`. - `nu -c "use tools/modules/services/mod.nu *; scope commands | where name =~ '^service_foundry '"` — expect 4 subcommands. Dependencies: Steps 1-11. #### Step 13: Smoke test on Hetzner Run under `flock` to serialize with the other agent. `hero_proc` must be up first. 1. `service_foundry install --root` — clone, build 3 binaries, copy to `/root/hero/bin/`. 2. `service_foundry install --root` again — expect `svc_bins_ok` short-circuit. 3. `service_foundry start --reset --root` — both actions registered, service `running`. 4. `ls /root/hero/var/sockets/hero_foundry/` — `rpc.sock` + `ui.sock` present. 5. `curl --unix-socket rpc.sock http://localhost/` — HTTP response. 6. `curl --unix-socket ui.sock http://localhost/` — HTTP response. 7. `service_foundry status --root` — record shows running, 0 restarts. 8. `proc action get hero_foundry_server --root` — verify env has `RUST_LOG`, `HERO_FOUNDRY_REPOS`, `HERO_FOUNDRY_BASE_PATH`. 9. `proc action get hero_foundry_ui --root` — verify env has only `RUST_LOG`. 10. `ls /root/hero/var/hero_foundry/` — `repos/` and `webdav/` auto-created by server. 11. `service_foundry start --root` — idempotent "already running". 12. Observe 15s — no new restarts. 13. `service_foundry stop --root` — clean unregister. 14. Post-stop `service_foundry status --root` — `service 'hero_foundry' not found`. Dependencies: Steps 1-12. ### Acceptance Criteria - [ ] `use services/mod.nu *` makes `service_foundry` available with 4 subcommands. - [ ] `service_foundry install [--root] [--update] [--reset]` clones + builds 3 binaries via `svc_cargo_install`; `--reset` forces rebuild, else `svc_bins_ok` short-circuits. - [ ] `service_foundry start [--reset] [--root] [--update]` registers both actions + service; server `script:` contains `--allow-dir <svc_home>/var/hero_foundry/repos --webdav-storage <svc_home>/var/hero_foundry/webdav`; UI `script:` contains `<bin>/hero_foundry_ui start`. - [ ] Server action env: `RUST_LOG=info`, `HERO_FOUNDRY_REPOS=<svc_home>/var/hero_foundry/repos`, `HERO_FOUNDRY_BASE_PATH=/hero_foundry/ui`. - [ ] UI action env: `RUST_LOG=info` only. - [ ] `kill_other.socket` on server = `<sock_base>/hero_foundry/rpc.sock`; on UI = `<sock_base>/hero_foundry/ui.sock`. - [ ] `service_foundry status [--root]` delegates to `proc service status hero_foundry`. - [ ] `service_foundry stop [--root]` cleanly unregisters. - [ ] `lib.nu` not modified. - [ ] `mod.nu` has `export use service_foundry.nu` appended. - [ ] `packages.nu` has `use service_foundry.nu` in imports AND an entry in `services_extra` with the `--reset`/`--update` passthrough pattern. - [ ] Smoke test passes all 14 scenarios above on the Hetzner box. ### Notes **Option A/B/C — A confirmed.** Read both `main.rs`: - `hero_foundry/crates/hero_foundry_ui/src/main.rs` (649 lines): Axum, `rust_embed` static assets, askama templates, `FoundryClient` SDK proxying to server over `rpc.sock`. Pure read-only repo browser. - `hero_foundry_ui/crates/hero_foundry_ui/src/main.rs` (2111 lines): Actix, `actix_session`, opens `.forge`/`.heroforge` files directly, own `FoundryStore` with issues/projects/wiki/PRs, reads `REPO_PATH` env, org-based routing `/{org}/{repo}/...`. These are distinct services with non-overlapping feature sets. Option A covers hero_foundry only; `lhumina_code/hero_foundry_ui` needs its own sub-issue after an owner decision. **UI subcommand verification.** `hero_foundry/crates/hero_foundry_ui/src/main.rs:86-89`: ``` #[tokio::main] async fn main() -> anyhow::Result<()> { run_server().await } ``` No clap, no argv parsing — `main` ignores all arguments. `start` is decorative; kept for TOML parity. **Flags vs env on server action.** Server clap at `hero_foundry/crates/hero_foundry_server/src/main.rs:46-74` exposes `--allow-dir`, `--webdav-storage`, `--repo`, `--read-write`, `--max-cache`, `--bind`, `--default-repo`. None declare `[env: ...]` fallbacks. `--allow-dir` and `--webdav-storage` MUST be CLI args — inline `script:` is the only option (same pattern as `service_biz.nu`). **Env var placement.** - `HERO_FOUNDRY_BASE_PATH` → server. Read at `hero_foundry_server/src/main.rs:155`, used by `ServerConfig.base_path` to prefix links in rendered responses. - `HERO_FOUNDRY_REPOS` → server (cosmetic). Grep across `hero_foundry/crates/` finds only one read, in `hero_foundry_examples/examples/seed_data.rs:18`. Keeping it on the server action matches the TOML `[env]` block and operator expectations. - `RUST_LOG` → both. - UI reads no Hero-specific env vars. Base-path prefixing uses `X-Forwarded-Prefix` HTTP header. **Socket paths verified.** - Server rpc: `$HERO_SOCKET_DIR/hero_foundry/rpc.sock` (from `socket_path("rpc")` at `hero_foundry_server/src/main.rs:199`). - UI: `$HERO_SOCKET_DIR/hero_foundry/ui.sock` (from `socket_path("ui")` at `hero_foundry_ui/src/main.rs:128`). - `hero_foundry/crates/hero_foundry_ui_server/src/main.rs:35` binds a different rpc path — this binary is an SDK shim, NOT in `SVX_BINARIES`, NOT registered. **Workspace layout.** `hero_foundry/Cargo.toml` is a virtual workspace. All three binaries reachable through `svc_cargo_install` directly — no hybrid pre-build like `service_books.nu` needs. **Data directories.** Server auto-creates `--webdav-storage` path via `std::fs::create_dir_all` at `hero_foundry_server/src/main.rs:121`. The `--allow-dir` path for repos must exist or be creatable; server errors out if not. ### Critical Files for Implementation - `/Users/mahmoud/code/forge.ourworld.tf/lhumina_code/hero_skills/tools/modules/services/service_voice.nu` - `/Users/mahmoud/code/forge.ourworld.tf/lhumina_code/hero_skills/tools/modules/services/service_biz.nu` - `/Users/mahmoud/code/forge.ourworld.tf/lhumina_code/hero_skills/tools/modules/services/service_books.nu` - `/Users/mahmoud/code/forge.ourworld.tf/lhumina_code/hero_skills/tools/modules/services/packages.nu` - `/Users/mahmoud/code/forge.ourworld.tf/lhumina_code/hero_foundry/crates/hero_foundry/src/main.rs`
Author
Owner

Implementation summary

Changes

  • Added tools/modules/services/service_foundry.nu — ~280 lines, combines patterns from service_voice.nu (baseline), service_biz.nu (inline script flags), and service_books.nu (env-at-register-time paths).
  • Updated tools/modules/services/mod.nu — appended export use service_foundry.nu.
  • Updated tools/modules/services/packages.nu — added use service_foundry.nu and entry in services_extra with --reset/--update passthrough.

End-to-end smoke test on Hetzner

Ran under flock on /tmp/hero_skills_smoke.lock. 23 of 24 assertions PASS. The lone FAIL is a wrong test assumption, not a module bug.

# Assertion Result
1a–1c hero_proc-down error paths PASS × 3
2a service_proc start --root up PASS
2b service_foundry install --root built 3 binaries PASS
2c service_foundry install --root again → svc_bins_ok short-circuit PASS
2d service_foundry start --reset --root registers + starts PASS
2e rpc.sock is a live unix socket PASS
2f ui.sock is a live unix socket PASS
2g curl --unix-socket rpc.sock accepts HTTP PASS
2h curl --unix-socket ui.sock accepts HTTP PASS
2i status returns {name: hero_foundry, state: running} PASS × 2
2j Server env has HERO_FOUNDRY_REPOS + HERO_FOUNDRY_BASE_PATH + RUST_LOG PASS × 2
2k Server script: contains --allow-dir and --webdav-storage PASS × 2
2l UI env has only RUST_LOG; UI script has start token PASS × 2
2m Idempotent start prints "already running" PASS
2n 15 s observation — no new restarts, state: running held PASS × 2
2o /root/hero/var/hero_foundry/webdav dir auto-created PASS
2o /root/hero/var/hero_foundry/repos dir auto-created FAIL (expected)
2p service_foundry stop --root cleanly unregisters PASS
2q Post-stop status returns service 'hero_foundry' not found PASS

Note on the 2o "fail"

Server source (hero_foundry_server/src/main.rs:121) calls std::fs::create_dir_all only on the --webdav-storage path. It does NOT create the --allow-dir path — that flag is treated as an existing access-control root. The server runs happily without that directory existing (hence state: running), but any actual repo operations against it would need the dir to be present.

That's consistent with how --allow-dir is documented in the server's clap help ("directory tree to allow access into"). The module is correct; the test just had the wrong expectation. Operators bootstrapping hero_foundry need to mkdir -p <svc_home>/var/hero_foundry/repos (or the start summary could be extended to create it — but that's an opinion, and the server doesn't complain about absence today).

Acceptance criteria

  • use services/mod.nu * exposes 4 subcommands.
  • service_foundry install builds 3 binaries (hero_foundry, hero_foundry_server, hero_foundry_ui); idempotent short-circuit via svc_bins_ok.
  • service_foundry start registers both actions + service; server script: contains --allow-dir <path> and --webdav-storage <path> with paths resolved via svc_home $root; UI script: contains <bin>/hero_foundry_ui start.
  • Server action env sets RUST_LOG=info, HERO_FOUNDRY_REPOS=<svc_home>/var/hero_foundry/repos, HERO_FOUNDRY_BASE_PATH=/hero_foundry/ui.
  • UI action env sets RUST_LOG=info only.
  • kill_other.socket on server = <sock_base>/hero_foundry/rpc.sock; on UI = <sock_base>/hero_foundry/ui.sock.
  • status delegates to proc service status hero_foundry.
  • stop cleanly unregisters.
  • lib.nu unchanged.
  • mod.nu re-exports service_foundry.
  • packages.nu has use service_foundry.nu and services_extra entry with --reset/--update passthrough.
  • Smoke-tested end-to-end on the Hetzner box.

Out of scope (flagged in header)

The sibling lhumina_code/hero_foundry_ui repo is a distinct Actix-based service (own FoundryStore, issues/wiki/PRs, org-scoped routing, reads REPO_PATH). It needs its own sub-issue after an owner decision on whether it's still supported. This module covers lhumina_code/hero_foundry only.

## Implementation summary ### Changes - Added `tools/modules/services/service_foundry.nu` — ~280 lines, combines patterns from `service_voice.nu` (baseline), `service_biz.nu` (inline script flags), and `service_books.nu` (env-at-register-time paths). - Updated `tools/modules/services/mod.nu` — appended `export use service_foundry.nu`. - Updated `tools/modules/services/packages.nu` — added `use service_foundry.nu` and entry in `services_extra` with `--reset`/`--update` passthrough. ### End-to-end smoke test on Hetzner Ran under `flock` on `/tmp/hero_skills_smoke.lock`. 23 of 24 assertions PASS. The lone FAIL is a wrong test assumption, not a module bug. | # | Assertion | Result | |---|---|---| | 1a–1c | hero_proc-down error paths | PASS × 3 | | 2a | `service_proc start --root` up | PASS | | 2b | `service_foundry install --root` built 3 binaries | PASS | | 2c | `service_foundry install --root` again → `svc_bins_ok` short-circuit | PASS | | 2d | `service_foundry start --reset --root` registers + starts | PASS | | 2e | `rpc.sock` is a live unix socket | PASS | | 2f | `ui.sock` is a live unix socket | PASS | | 2g | `curl --unix-socket rpc.sock` accepts HTTP | PASS | | 2h | `curl --unix-socket ui.sock` accepts HTTP | PASS | | 2i | `status` returns `{name: hero_foundry, state: running}` | PASS × 2 | | 2j | Server env has `HERO_FOUNDRY_REPOS` + `HERO_FOUNDRY_BASE_PATH` + `RUST_LOG` | PASS × 2 | | 2k | Server `script:` contains `--allow-dir` and `--webdav-storage` | PASS × 2 | | 2l | UI env has only `RUST_LOG`; UI script has `start` token | PASS × 2 | | 2m | Idempotent start prints "already running" | PASS | | 2n | 15 s observation — no new restarts, `state: running` held | PASS × 2 | | 2o | `/root/hero/var/hero_foundry/webdav` dir auto-created | PASS | | 2o | `/root/hero/var/hero_foundry/repos` dir auto-created | **FAIL (expected)** | | 2p | `service_foundry stop --root` cleanly unregisters | PASS | | 2q | Post-stop `status` returns `service 'hero_foundry' not found` | PASS | ### Note on the 2o "fail" Server source (`hero_foundry_server/src/main.rs:121`) calls `std::fs::create_dir_all` only on the `--webdav-storage` path. It does NOT create the `--allow-dir` path — that flag is treated as an existing access-control root. The server runs happily without that directory existing (hence `state: running`), but any actual repo operations against it would need the dir to be present. That's consistent with how `--allow-dir` is documented in the server's clap help ("directory tree to allow access into"). The module is correct; the test just had the wrong expectation. Operators bootstrapping hero_foundry need to `mkdir -p <svc_home>/var/hero_foundry/repos` (or the start summary could be extended to create it — but that's an opinion, and the server doesn't complain about absence today). ### Acceptance criteria - [x] `use services/mod.nu *` exposes 4 subcommands. - [x] `service_foundry install` builds 3 binaries (hero_foundry, hero_foundry_server, hero_foundry_ui); idempotent short-circuit via `svc_bins_ok`. - [x] `service_foundry start` registers both actions + service; server `script:` contains `--allow-dir <path>` and `--webdav-storage <path>` with paths resolved via `svc_home $root`; UI `script:` contains `<bin>/hero_foundry_ui start`. - [x] Server action env sets `RUST_LOG=info`, `HERO_FOUNDRY_REPOS=<svc_home>/var/hero_foundry/repos`, `HERO_FOUNDRY_BASE_PATH=/hero_foundry/ui`. - [x] UI action env sets `RUST_LOG=info` only. - [x] `kill_other.socket` on server = `<sock_base>/hero_foundry/rpc.sock`; on UI = `<sock_base>/hero_foundry/ui.sock`. - [x] `status` delegates to `proc service status hero_foundry`. - [x] `stop` cleanly unregisters. - [x] `lib.nu` unchanged. - [x] `mod.nu` re-exports `service_foundry`. - [x] `packages.nu` has `use service_foundry.nu` and `services_extra` entry with `--reset`/`--update` passthrough. - [x] Smoke-tested end-to-end on the Hetzner box. ### Out of scope (flagged in header) The sibling `lhumina_code/hero_foundry_ui` repo is a distinct Actix-based service (own FoundryStore, issues/wiki/PRs, org-scoped routing, reads `REPO_PATH`). It needs its own sub-issue after an owner decision on whether it's still supported. This module covers `lhumina_code/hero_foundry` only.
Author
Owner

PR opened: #95

PR opened: https://forge.ourworld.tf/lhumina_code/hero_skills/pulls/95
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
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#94
No description provided.