service_aibroker.nu — hero_aibroker server + UI lifecycle module #90

Closed
opened 2026-04-19 21:16:29 +00:00 by mahmoud · 3 comments
Owner

Child of #75.

Objective

Add tools/modules/services/service_aibroker.nu implementing the standard install | start | stop | status lifecycle for the hero_aibroker service (server + UI).

Scope

  • Repo: ssh://git@forge.ourworld.tf/lhumina_code/hero_aibroker.git (NOTE: hero_zero/services/hero_aibroker.toml erroneously lists geomind_code/aibroker.git — the nu module must use lhumina_code/hero_aibroker, which is the canonical repo. Worth a follow-up TOML fix but out of scope here.)
  • Binaries (per buildenv.sh): hero_aibroker, hero_aibroker_server, hero_aibroker_ui, hero_broker_server (the latter is a separate crate for search/scraping tools; ship but do NOT register).
  • Runtime actions: hero_aibroker_server, hero_aibroker_ui.
  • TOML: lhumina_code/hero_zero/services/hero_aibroker.toml.
  • Server binds THREE artifacts (from hero_aibroker_server/src/main.rs):
    • $HERO_SOCKET_DIR/hero_aibroker/rpc.sock — OpenRPC JSON-RPC (health-checked by hero_proc)
    • $HERO_SOCKET_DIR/hero_aibroker/rest.sock — REST HTTP (OpenAI-compatible; UI proxies here)
    • no TCP ports
  • UI socket: $HERO_SOCKET_DIR/hero_aibroker/ui.sock.
  • Env needed on server (from TOML + crates/hero_aibroker_lib/src/config/mod.rs):
    • RUST_LOG=info
    • MODELS_CONFIG_PATH=$hero_home/var/hero_aibroker/modelsconfig.yml
    • OPENROUTER_API_KEYS=$OPENROUTER_API_KEY (read at action-build time from current env; empty if unset)
    • GROQ_API_KEY=$GROQ_API_KEY (same)
  • Env needed on UI: RUST_LOG=info only.
  • modelsconfig.yml provisioning: the TOML inlines a here-doc that writes a default config before exec. The nu module will simplify this to a preflight step — copy $repo_path/modelsconfig.yml into $hero_home/var/hero_aibroker/modelsconfig.yml on start, preserving any existing operator-edited file (write only if missing or --reset was requested).
  • Workspace: virtual (no root [package]).
  • --root flag optional; user-level default.

Acceptance criteria

  • use services/mod.nu * makes service_aibroker available.
  • service_aibroker install [--root] [--update] clones lhumina_code/hero_aibroker, builds all 4 binaries, installs to ~/hero/bin/ (or /root/hero/bin/ with --root).
  • service_aibroker start [--reset] [--root] [--update] ensures modelsconfig.yml exists, registers both actions + the service, starts, prints all three socket paths in the summary. Idempotent without --reset; --reset re-seeds the models config.
  • service_aibroker status [--root] reports state.
  • service_aibroker stop [--root] cleanly unregisters.
  • Warn (not fail) at start when OPENROUTER_API_KEY or GROQ_API_KEY is absent — the service will start but the affected providers will fail their requests.
  • Smoke-tested on Hetzner: install → start --reset → status shows running with 0 restarts → probe rpc.sock + rest.sock + ui.sock → stop.

Template & references

  • Template: service_db.nu (PR #88) — multi-socket server kill_other pattern (two UDS here instead of rpc + resp + TCP).
  • Reference: service_books.nu (PR #81) — computed-env pattern (HERO_BOOKS_DATA / HERO_EMBEDDER_URL resolved at action-build time).
  • Shared helpers: tools/modules/services/lib.nu.

Expected deviations

  1. Server kill_other.socket covers both rpc.sock and rest.sock; port: [].
  2. Server env computed at action-build time: MODELS_CONFIG_PATH via svc_home, API keys read from the invoking env.
  3. Preflight helper seeds modelsconfig.yml from the cloned repo (or re-seeds with --reset).
  4. UI script: $bin (bare); server script: $bin (bare — no subcommand, main() calls run_server() directly).
Child of #75. ## Objective Add `tools/modules/services/service_aibroker.nu` implementing the standard `install | start | stop | status` lifecycle for the **hero_aibroker** service (server + UI). ## Scope - **Repo**: `ssh://git@forge.ourworld.tf/lhumina_code/hero_aibroker.git` (NOTE: `hero_zero/services/hero_aibroker.toml` erroneously lists `geomind_code/aibroker.git` — the nu module must use `lhumina_code/hero_aibroker`, which is the canonical repo. Worth a follow-up TOML fix but out of scope here.) - **Binaries** (per `buildenv.sh`): `hero_aibroker`, `hero_aibroker_server`, `hero_aibroker_ui`, `hero_broker_server` (the latter is a separate crate for search/scraping tools; ship but do NOT register). - **Runtime actions**: `hero_aibroker_server`, `hero_aibroker_ui`. - **TOML**: `lhumina_code/hero_zero/services/hero_aibroker.toml`. - **Server binds THREE artifacts** (from `hero_aibroker_server/src/main.rs`): - `$HERO_SOCKET_DIR/hero_aibroker/rpc.sock` — OpenRPC JSON-RPC (health-checked by hero_proc) - `$HERO_SOCKET_DIR/hero_aibroker/rest.sock` — REST HTTP (OpenAI-compatible; UI proxies here) - no TCP ports - **UI socket**: `$HERO_SOCKET_DIR/hero_aibroker/ui.sock`. - **Env needed on server** (from TOML + `crates/hero_aibroker_lib/src/config/mod.rs`): - `RUST_LOG=info` - `MODELS_CONFIG_PATH=$hero_home/var/hero_aibroker/modelsconfig.yml` - `OPENROUTER_API_KEYS=$OPENROUTER_API_KEY` (read at action-build time from current env; empty if unset) - `GROQ_API_KEY=$GROQ_API_KEY` (same) - **Env needed on UI**: `RUST_LOG=info` only. - **modelsconfig.yml provisioning**: the TOML inlines a here-doc that writes a default config before exec. The nu module will simplify this to a preflight step — copy `$repo_path/modelsconfig.yml` into `$hero_home/var/hero_aibroker/modelsconfig.yml` on `start`, preserving any existing operator-edited file (write only if missing or `--reset` was requested). - **Workspace**: virtual (no root `[package]`). - `--root` flag optional; user-level default. ## Acceptance criteria - [ ] `use services/mod.nu *` makes `service_aibroker` available. - [ ] `service_aibroker install [--root] [--update]` clones `lhumina_code/hero_aibroker`, builds all 4 binaries, installs to `~/hero/bin/` (or `/root/hero/bin/` with `--root`). - [ ] `service_aibroker start [--reset] [--root] [--update]` ensures `modelsconfig.yml` exists, registers both actions + the service, starts, prints all three socket paths in the summary. Idempotent without `--reset`; `--reset` re-seeds the models config. - [ ] `service_aibroker status [--root]` reports state. - [ ] `service_aibroker stop [--root]` cleanly unregisters. - [ ] Warn (not fail) at start when `OPENROUTER_API_KEY` or `GROQ_API_KEY` is absent — the service will start but the affected providers will fail their requests. - [ ] Smoke-tested on Hetzner: install → start --reset → status shows running with 0 restarts → probe rpc.sock + rest.sock + ui.sock → stop. ## Template & references - Template: `service_db.nu` (PR #88) — multi-socket server `kill_other` pattern (two UDS here instead of rpc + resp + TCP). - Reference: `service_books.nu` (PR #81) — computed-env pattern (HERO_BOOKS_DATA / HERO_EMBEDDER_URL resolved at action-build time). - Shared helpers: `tools/modules/services/lib.nu`. ## Expected deviations 1. Server `kill_other.socket` covers both `rpc.sock` and `rest.sock`; `port: []`. 2. Server env computed at action-build time: `MODELS_CONFIG_PATH` via `svc_home`, API keys read from the invoking env. 3. Preflight helper seeds `modelsconfig.yml` from the cloned repo (or re-seeds with `--reset`). 4. UI `script: $bin` (bare); server `script: $bin` (bare — no subcommand, `main()` calls `run_server()` directly).
mahmoud self-assigned this 2026-04-19 21:16:36 +00:00
mahmoud added this to the ACTIVE project 2026-04-19 21:16:38 +00:00
mahmoud added this to the now milestone 2026-04-19 21:16:40 +00:00
Author
Owner

Implementation Spec for Issue #90

Objective

Add a Nushell lifecycle module service_aibroker.nu that installs, registers, starts, stops, and queries status for the hero_aibroker service through hero_proc, mirroring the shape of service_db.nu (multi-socket server with an explicit kill_other socket list) and borrowing the action-build-time env computation + non-fatal preflight helper pattern from service_books.nu. hero_aibroker is a two-binary registered service — hero_aibroker_server (OpenRPC on rpc.sock + OpenAI-compatible REST on rest.sock) and hero_aibroker_ui (ui.sock) — with four binaries shipped in total (server, ui, CLI hero_aibroker, and the workspace companion hero_broker_server). The module additionally seeds a default modelsconfig.yml into ~/hero/var/hero_aibroker/ on first start and surfaces a non-fatal warning when LLM provider API keys are absent from the invoking environment.

Requirements

  • Binaries shipped (SVX_BINARIES): hero_aibroker, hero_aibroker_server, hero_aibroker_ui, hero_broker_server. Plain cargo build --release on the virtual workspace builds all four.
  • Actions registered (SVX_ACTIONS): hero_aibroker_server, hero_aibroker_ui. hero_aibroker is the CLI (unregistered). hero_broker_server is an unrelated workspace companion — shipped but not registered.
  • Sockets (UDS only, no TCP):
    • $HERO_SOCKET_DIR/hero_aibroker/rpc.sock — OpenRPC management
    • $HERO_SOCKET_DIR/hero_aibroker/rest.sock — OpenAI-compatible REST
    • $HERO_SOCKET_DIR/hero_aibroker/ui.sock — admin dashboard (UI proxies REST calls to rest.sock)
  • Server kill_other: socket: [rpc.sock, rest.sock], port: []. UI kill_other.socket: [ui.sock].
  • Server env computed at action-build time (books pattern, so --root flips paths):
    • RUST_LOG = "info"
    • MODELS_CONFIG_PATH = $"(svc_home $root)/var/hero_aibroker/modelsconfig.yml"
    • OPENROUTER_API_KEYS = ($env.OPENROUTER_API_KEY? | default "")
    • GROQ_API_KEY = ($env.GROQ_API_KEY? | default "")
  • UI env: RUST_LOG = "info" only.
  • Action scripts: both script: $bin (bare binaries, no subcommands — matches TOML exec minus the inline heredoc, which we replicate as a dedicated preflight helper instead).
  • No depends_on — TOML declares none, health is self-sufficient once modelsconfig.yml is present.
  • Preflight: seed modelsconfig.yml from the cloned repo's root-level modelsconfig.yml into $(svc_home $root)/var/hero_aibroker/modelsconfig.yml. Idempotent unless --reset. Must work under --root (sudo mkdir -p + sudo cp).
  • Preflight: API-key warning (non-fatal) when OPENROUTER_API_KEY or GROQ_API_KEY is empty in the invoking environment.
  • --root / --reset / --update flags — same semantics as every other service_*.nu.

Files to Modify/Create

  • Create tools/modules/services/service_aibroker.nu — the new module.
  • Modify tools/modules/services/mod.nu — append export use service_aibroker.nu.

Implementation Plan

Step 1: File header / module doc

Copy the service_db.nu header shape. Document:

  • Two-binary Hero service (server + UI), three UDS binds, no TCP.
  • Two shipped-but-unregistered binaries: hero_aibroker CLI and hero_broker_server (separate workspace-member service, not this module's concern).
  • Dependency line: hero_proc only.
  • Preflight: module seeds modelsconfig.yml; .env is read by the server if present but is not written by this module.
  • Call out the TOML source = … bug: hero_zero/services/hero_aibroker.toml mis-points at geomind_code/aibroker.git; the nu module uses the authoritative lhumina_code/hero_aibroker location and ignores the TOML field.

Step 2: Imports + constants

use ../clients/proc.nu *
use ./lib.nu *

const SVX_SERVICE_NAME = "hero_aibroker"
const SVX_FORGE_LOC    = "lhumina_code/hero_aibroker"
const SVX_BINARIES     = ["hero_aibroker" "hero_aibroker_server" "hero_aibroker_ui" "hero_broker_server"]
const SVX_ACTIONS      = ["hero_aibroker_server" "hero_aibroker_ui"]

No SVX_*_TCP_PORT constant — no TCP bind.

Step 3: NEW helper svx_seed_models_config [root: bool, reset: bool]

Ensures $(svc_home $root)/var/hero_aibroker/modelsconfig.yml exists. Logic:

  1. dest_dir = $"(svc_home $root)/var/hero_aibroker", dest = $"($dest_dir)/modelsconfig.yml".
  2. Resolve source via forge_ensure_local $SVX_FORGE_LOC (already cloned by prior install — safe no-op lookup). src = $"($info.path)/modelsconfig.yml".
  3. If src missing → error make with actionable message.
  4. If dest exists and not $reset: print → modelsconfig.yml already present at (...) — leaving operator edits intact, return.
  5. Otherwise, dispatch on svc_need_sudo $root: either sudo mkdir -p + sudo cp (checking .exit_code) or native mkdir + ^cp.
  6. Print ✓ seeded modelsconfig.yml (or re-seeded when $reset).

Import forge_ensure_local from ../forge.nu (books already does use ../forge.nu [forge_ensure_local]).

Step 4: NEW helper svx_check_api_keys []

Non-fatal warning, invoked from start between preflight and register (same slot as svx_check_embedder in books). Independent of $root — the warning is about the invoking shell env because that is what gets captured into the action env.

def svx_check_api_keys [] {
    let openrouter = ($env | get -o OPENROUTER_API_KEY | default "")
    let groq       = ($env | get -o GROQ_API_KEY | default "")
    mut missing = []
    if ($openrouter | is-empty) { $missing = ($missing | append "OPENROUTER_API_KEY") }
    if ($groq       | is-empty) { $missing = ($missing | append "GROQ_API_KEY") }
    if ($missing | is-empty) { return }
    print $"⚠ missing LLM provider key\(s\): ($missing | str join ', ')"
    print "  hero_aibroker will start and pass health checks, but calls to the"
    print "  affected provider\(s\) will fail at request time until the key is"
    print "  exported and the service is restarted with --reset."
    print "  To fix:"
    print "    export OPENROUTER_API_KEY=…       # and/or GROQ_API_KEY=…"
    print "    service_aibroker start --reset"
}

Explicitly does not error — Config::load() in hero_aibroker_lib/src/config/mod.rs handles empty key lists gracefully.

Step 5: svx_server_action [root: bool]

Mirror svx_server_action in service_db.nu, with these deviations:

  • let models_path = $"(svc_home $root)/var/hero_aibroker/modelsconfig.yml"
  • let openrouter = ($env | get -o OPENROUTER_API_KEY | default "")
  • let groq = ($env | get -o GROQ_API_KEY | default "")
  • name: "hero_aibroker_server"
  • script: $bin (bare binary, no subcommand)
  • env: { RUST_LOG: "info", MODELS_CONFIG_PATH: $models_path, OPENROUTER_API_KEYS: $openrouter, GROQ_API_KEY: $groq }
  • kill_other.port: []
  • kill_other.socket: [ $"($sock_base)/hero_aibroker/rpc.sock", $"($sock_base)/hero_aibroker/rest.sock" ]
  • health_checks[0].openrpc_socket: $"($sock_base)/hero_aibroker/rpc.sock"
  • Retry / stop_signal / timeouts identical to service_db server defaults.

Step 6: svx_ui_action [root: bool]

Mirror svx_ui_action in service_whiteboard.nu (closest shape: bare binary, single socket):

  • script: $bin (NOT $"($bin) serve" — hero_aibroker_ui has no subcommand)
  • env: {RUST_LOG: "info"}
  • kill_other.socket: [$"($sock_base)/hero_aibroker/ui.sock"]
  • health_checks[0].openrpc_socket: $"($sock_base)/hero_aibroker/ui.sock"
  • Retry / timeouts identical to whiteboard UI defaults.

Step 7: svx_service_config []

{
    context_name: "core"
    service: {
        name: $SVX_SERVICE_NAME
        actions: $SVX_ACTIONS
        class: "system"
        critical: false
        description: "Hero AIBroker — multi-provider LLM broker with OpenRPC + OpenAI-compatible REST and admin UI"
        status: "start"
    }
}

Step 8: svx_drop_registration [root: bool]

Byte-for-byte clone of service_db.nu version.

Step 9: install [--root, --update]

Byte-for-byte clone of service_whiteboard.nu::install. The virtual workspace builds all four bins in one pass; svc_cargo_install's missing-binary preflight catches any build gap before copy.

Step 10: start [--reset, --root, --update]

Same skeleton as service_books.nu::start, with the new preflights slotted between the binary existence check and the registration drop:

  1. if $root { svc_require_sudo }
  2. svc_require_proc "service_aibroker" $root
  3. Early-exit idempotency: if (not $reset) and (not $update) { … is_running … print and return }.
  4. install --root=$root --update=$update
  5. Binary existence check (svc_need_sudo-aware test -x on hero_aibroker_server).
  6. NEW svx_seed_models_config $root $reset
  7. NEW svx_check_api_keys (warning only)
  8. svx_drop_registration $root
  9. proc action set (svx_server_action $root) --root=$root | ignore
  10. proc action set (svx_ui_action $root) --root=$root | ignore
  11. proc service set (svx_service_config) --root=$root | ignore
  12. proc service start $SVX_SERVICE_NAME --root=$root | ignore
  13. sleep 1sec, fetch is_running, print summary:
    • service / actions / state
    • rpc sock : .../rpc.sock
    • rest sock : .../rest.sock
    • ui sock : .../ui.sock + ui url : http+unix://.../ line
    • models : $models_path
    • proc service status / proc logs tail hero_aibroker_server / proc logs tail hero_aibroker_ui hints.

Step 11: stop [--root]

Identical to service_whiteboard.nu::stop with the name swap.

Step 12: status [--root]

Identical to service_whiteboard.nu::status.

Step 13: mod.nu

Append one line export use service_aibroker.nu after the last existing export use line.

Smoke Test Plan (Hetzner, --root)

  1. Install + first start:
    service_aibroker install --root && service_aibroker start --root
    Expect: 4 binaries in /root/hero/bin/, modelsconfig.yml seeded at /root/hero/var/hero_aibroker/modelsconfig.yml, service running, summary printed.
  2. OpenRPC probe: curl --unix-socket rpc.sock http://localhost/openrpc.json → OpenRPC doc.
  3. REST /v1/models: curl --unix-socket rest.sock http://localhost/v1/models → JSON model list.
  4. REST /health: curl --unix-socket rest.sock http://localhost/health → 200.
  5. UI: curl --unix-socket ui.sock http://localhost/ → HTML dashboard.
  6. Idempotent start: no-op with hint.
  7. 15s observation: no retries, state stable.
  8. --reset restart: drop + re-register + restart, models config re-seeded, all three sockets back up.
  9. Stop: clean unregister, sockets removed.
  10. Post-stop status: service 'hero_aibroker' not found.
  11. Missing-key warning path: unset OPENROUTER_API_KEY GROQ_API_KEY; service_aibroker start --reset --root → warning prints both keys, service still starts and both probes return 200.

Acceptance Criteria (from #90)

  • install compiles the workspace and places four binaries in the right bin dir.
  • start seeds modelsconfig.yml, registers both actions + the service, reaches running with all three sockets bound.
  • --reset drops all registration, re-seeds the config, re-registers cleanly.
  • Idempotent start on a running service is a no-op with actionable hint.
  • Three UDS probes (openrpc.json, /v1/models, /health) return 200; UI returns HTML.
  • Missing keys → warning, service still starts and passes health.
  • stop cleanly unregisters; status after stop surfaces "not found".
  • Re-import via mod.nu loads without errors.
  • --root flips paths everywhere including svx_seed_models_config's sudo branches.

Notes

  • TOML source bug: hero_zero/services/hero_aibroker.toml mis-sets source = "…geomind_code/aibroker.git". The module uses lhumina_code/hero_aibroker. Nothing in the nu lifecycle path reads the TOML source, so the discrepancy does not block this PR; it is a separate hero_zero repo fix.
  • modelsconfig.yml provisioning rationale: the upstream TOML bakes the default config into an inline sh -c heredoc prefix on exec. Doing the same in a nu script: string forces operators to read a messy heredoc and re-stomps operator edits on every restart. Seeding from the repo's modelsconfig.yml once (re-seeding on --reset) gives operators a single editable file with predictable semantics.
  • No .env writing: Config::load() auto-loads ~/hero/var/hero_aibroker/.env via dotenvy::from_path if present. The module deliberately does NOT write this file — API keys must come from the operator's shell via action env at register-time. A future service_secrets-style helper can manage .env separately.
  • hero_broker_server shipped but not registered: it's a peer workspace member (search/scraper binary) that buildenv.sh includes in BINARIES and the workspace build produces unconditionally. Dropping it would force a split-install; including it costs nothing and keeps buildenv.sh + SVX_BINARIES in lockstep.
  • Missing keys: warning, not error: Config::load() tolerates empty key lists. A hard-fail would block valid scenarios (operator only uses OpenRouter but not Groq, or injects keys via a downstream context switch). The warning tells the operator which provider will fail at request time.

Critical Files for Implementation

  • tools/modules/services/service_db.nu (multi-socket server template)
  • tools/modules/services/service_books.nu (computed env + preflight helper pattern)
  • tools/modules/services/service_whiteboard.nu (overall file shape)
  • tools/modules/services/lib.nu (helpers)
  • tools/modules/services/mod.nu (one-line append)
## Implementation Spec for Issue #90 ### Objective Add a Nushell lifecycle module `service_aibroker.nu` that installs, registers, starts, stops, and queries status for the `hero_aibroker` service through `hero_proc`, mirroring the shape of `service_db.nu` (multi-socket server with an explicit `kill_other` socket list) and borrowing the action-build-time env computation + non-fatal preflight helper pattern from `service_books.nu`. `hero_aibroker` is a two-binary registered service — `hero_aibroker_server` (OpenRPC on `rpc.sock` + OpenAI-compatible REST on `rest.sock`) and `hero_aibroker_ui` (`ui.sock`) — with four binaries shipped in total (server, ui, CLI `hero_aibroker`, and the workspace companion `hero_broker_server`). The module additionally seeds a default `modelsconfig.yml` into `~/hero/var/hero_aibroker/` on first start and surfaces a non-fatal warning when LLM provider API keys are absent from the invoking environment. ### Requirements - **Binaries shipped** (`SVX_BINARIES`): `hero_aibroker`, `hero_aibroker_server`, `hero_aibroker_ui`, `hero_broker_server`. Plain `cargo build --release` on the virtual workspace builds all four. - **Actions registered** (`SVX_ACTIONS`): `hero_aibroker_server`, `hero_aibroker_ui`. `hero_aibroker` is the CLI (unregistered). `hero_broker_server` is an unrelated workspace companion — shipped but not registered. - **Sockets (UDS only, no TCP)**: - `$HERO_SOCKET_DIR/hero_aibroker/rpc.sock` — OpenRPC management - `$HERO_SOCKET_DIR/hero_aibroker/rest.sock` — OpenAI-compatible REST - `$HERO_SOCKET_DIR/hero_aibroker/ui.sock` — admin dashboard (UI proxies REST calls to `rest.sock`) - **Server `kill_other`**: `socket: [rpc.sock, rest.sock]`, `port: []`. UI `kill_other.socket: [ui.sock]`. - **Server env computed at action-build time** (books pattern, so `--root` flips paths): - `RUST_LOG = "info"` - `MODELS_CONFIG_PATH = $"(svc_home $root)/var/hero_aibroker/modelsconfig.yml"` - `OPENROUTER_API_KEYS = ($env.OPENROUTER_API_KEY? | default "")` - `GROQ_API_KEY = ($env.GROQ_API_KEY? | default "")` - **UI env**: `RUST_LOG = "info"` only. - **Action scripts**: both `script: $bin` (bare binaries, no subcommands — matches TOML `exec` minus the inline heredoc, which we replicate as a dedicated preflight helper instead). - **No `depends_on`** — TOML declares none, health is self-sufficient once `modelsconfig.yml` is present. - **Preflight: seed `modelsconfig.yml`** from the cloned repo's root-level `modelsconfig.yml` into `$(svc_home $root)/var/hero_aibroker/modelsconfig.yml`. Idempotent unless `--reset`. Must work under `--root` (sudo `mkdir -p` + sudo `cp`). - **Preflight: API-key warning** (non-fatal) when `OPENROUTER_API_KEY` or `GROQ_API_KEY` is empty in the invoking environment. - **`--root` / `--reset` / `--update` flags** — same semantics as every other `service_*.nu`. ### Files to Modify/Create - **Create** `tools/modules/services/service_aibroker.nu` — the new module. - **Modify** `tools/modules/services/mod.nu` — append `export use service_aibroker.nu`. ### Implementation Plan #### Step 1: File header / module doc Copy the `service_db.nu` header shape. Document: - Two-binary Hero service (server + UI), three UDS binds, no TCP. - Two shipped-but-unregistered binaries: `hero_aibroker` CLI and `hero_broker_server` (separate workspace-member service, not this module's concern). - Dependency line: `hero_proc` only. - Preflight: module seeds `modelsconfig.yml`; `.env` is read by the server if present but is **not** written by this module. - Call out the TOML `source = …` bug: `hero_zero/services/hero_aibroker.toml` mis-points at `geomind_code/aibroker.git`; the nu module uses the authoritative `lhumina_code/hero_aibroker` location and ignores the TOML field. #### Step 2: Imports + constants ``` use ../clients/proc.nu * use ./lib.nu * const SVX_SERVICE_NAME = "hero_aibroker" const SVX_FORGE_LOC = "lhumina_code/hero_aibroker" const SVX_BINARIES = ["hero_aibroker" "hero_aibroker_server" "hero_aibroker_ui" "hero_broker_server"] const SVX_ACTIONS = ["hero_aibroker_server" "hero_aibroker_ui"] ``` No `SVX_*_TCP_PORT` constant — no TCP bind. #### Step 3: NEW helper `svx_seed_models_config [root: bool, reset: bool]` Ensures `$(svc_home $root)/var/hero_aibroker/modelsconfig.yml` exists. Logic: 1. `dest_dir = $"(svc_home $root)/var/hero_aibroker"`, `dest = $"($dest_dir)/modelsconfig.yml"`. 2. Resolve source via `forge_ensure_local $SVX_FORGE_LOC` (already cloned by prior `install` — safe no-op lookup). `src = $"($info.path)/modelsconfig.yml"`. 3. If `src` missing → `error make` with actionable message. 4. If `dest` exists and not `$reset`: print `→ modelsconfig.yml already present at (...) — leaving operator edits intact`, return. 5. Otherwise, dispatch on `svc_need_sudo $root`: either `sudo mkdir -p` + `sudo cp` (checking `.exit_code`) or native `mkdir` + `^cp`. 6. Print `✓ seeded modelsconfig.yml` (or `re-seeded` when `$reset`). Import `forge_ensure_local` from `../forge.nu` (books already does `use ../forge.nu [forge_ensure_local]`). #### Step 4: NEW helper `svx_check_api_keys []` Non-fatal warning, invoked from `start` between preflight and register (same slot as `svx_check_embedder` in books). Independent of `$root` — the warning is about the invoking shell env because that is what gets captured into the action env. ``` def svx_check_api_keys [] { let openrouter = ($env | get -o OPENROUTER_API_KEY | default "") let groq = ($env | get -o GROQ_API_KEY | default "") mut missing = [] if ($openrouter | is-empty) { $missing = ($missing | append "OPENROUTER_API_KEY") } if ($groq | is-empty) { $missing = ($missing | append "GROQ_API_KEY") } if ($missing | is-empty) { return } print $"⚠ missing LLM provider key\(s\): ($missing | str join ', ')" print " hero_aibroker will start and pass health checks, but calls to the" print " affected provider\(s\) will fail at request time until the key is" print " exported and the service is restarted with --reset." print " To fix:" print " export OPENROUTER_API_KEY=… # and/or GROQ_API_KEY=…" print " service_aibroker start --reset" } ``` Explicitly does **not** error — `Config::load()` in `hero_aibroker_lib/src/config/mod.rs` handles empty key lists gracefully. #### Step 5: `svx_server_action [root: bool]` Mirror `svx_server_action` in `service_db.nu`, with these deviations: - `let models_path = $"(svc_home $root)/var/hero_aibroker/modelsconfig.yml"` - `let openrouter = ($env | get -o OPENROUTER_API_KEY | default "")` - `let groq = ($env | get -o GROQ_API_KEY | default "")` - `name: "hero_aibroker_server"` - `script: $bin` (bare binary, no subcommand) - `env: { RUST_LOG: "info", MODELS_CONFIG_PATH: $models_path, OPENROUTER_API_KEYS: $openrouter, GROQ_API_KEY: $groq }` - `kill_other.port: []` - `kill_other.socket: [ $"($sock_base)/hero_aibroker/rpc.sock", $"($sock_base)/hero_aibroker/rest.sock" ]` - `health_checks[0].openrpc_socket: $"($sock_base)/hero_aibroker/rpc.sock"` - Retry / stop_signal / timeouts identical to `service_db` server defaults. #### Step 6: `svx_ui_action [root: bool]` Mirror `svx_ui_action` in `service_whiteboard.nu` (closest shape: bare binary, single socket): - `script: $bin` (NOT `$"($bin) serve"` — hero_aibroker_ui has no subcommand) - `env: {RUST_LOG: "info"}` - `kill_other.socket: [$"($sock_base)/hero_aibroker/ui.sock"]` - `health_checks[0].openrpc_socket: $"($sock_base)/hero_aibroker/ui.sock"` - Retry / timeouts identical to whiteboard UI defaults. #### Step 7: `svx_service_config []` ``` { context_name: "core" service: { name: $SVX_SERVICE_NAME actions: $SVX_ACTIONS class: "system" critical: false description: "Hero AIBroker — multi-provider LLM broker with OpenRPC + OpenAI-compatible REST and admin UI" status: "start" } } ``` #### Step 8: `svx_drop_registration [root: bool]` Byte-for-byte clone of `service_db.nu` version. #### Step 9: `install [--root, --update]` Byte-for-byte clone of `service_whiteboard.nu::install`. The virtual workspace builds all four bins in one pass; `svc_cargo_install`'s missing-binary preflight catches any build gap before copy. #### Step 10: `start [--reset, --root, --update]` Same skeleton as `service_books.nu::start`, with the new preflights slotted between the binary existence check and the registration drop: 1. `if $root { svc_require_sudo }` 2. `svc_require_proc "service_aibroker" $root` 3. Early-exit idempotency: `if (not $reset) and (not $update) { … is_running … print and return }`. 4. `install --root=$root --update=$update` 5. Binary existence check (`svc_need_sudo`-aware `test -x` on `hero_aibroker_server`). 6. **NEW** `svx_seed_models_config $root $reset` 7. **NEW** `svx_check_api_keys` (warning only) 8. `svx_drop_registration $root` 9. `proc action set (svx_server_action $root) --root=$root | ignore` 10. `proc action set (svx_ui_action $root) --root=$root | ignore` 11. `proc service set (svx_service_config) --root=$root | ignore` 12. `proc service start $SVX_SERVICE_NAME --root=$root | ignore` 13. `sleep 1sec`, fetch `is_running`, print summary: - service / actions / state - `rpc sock : .../rpc.sock` - `rest sock : .../rest.sock` - `ui sock : .../ui.sock` + `ui url : http+unix://.../` line - `models : $models_path` - `proc service status / proc logs tail hero_aibroker_server / proc logs tail hero_aibroker_ui` hints. #### Step 11: `stop [--root]` Identical to `service_whiteboard.nu::stop` with the name swap. #### Step 12: `status [--root]` Identical to `service_whiteboard.nu::status`. #### Step 13: `mod.nu` Append one line `export use service_aibroker.nu` after the last existing `export use` line. ### Smoke Test Plan (Hetzner, `--root`) 1. **Install + first start**: `service_aibroker install --root && service_aibroker start --root` Expect: 4 binaries in `/root/hero/bin/`, `modelsconfig.yml` seeded at `/root/hero/var/hero_aibroker/modelsconfig.yml`, service `running`, summary printed. 2. **OpenRPC probe**: `curl --unix-socket rpc.sock http://localhost/openrpc.json` → OpenRPC doc. 3. **REST /v1/models**: `curl --unix-socket rest.sock http://localhost/v1/models` → JSON model list. 4. **REST /health**: `curl --unix-socket rest.sock http://localhost/health` → 200. 5. **UI**: `curl --unix-socket ui.sock http://localhost/` → HTML dashboard. 6. **Idempotent start**: no-op with hint. 7. **15s observation**: no retries, state stable. 8. **`--reset` restart**: drop + re-register + restart, models config re-seeded, all three sockets back up. 9. **Stop**: clean unregister, sockets removed. 10. **Post-stop status**: `service 'hero_aibroker' not found`. 11. **Missing-key warning path**: `unset OPENROUTER_API_KEY GROQ_API_KEY; service_aibroker start --reset --root` → warning prints both keys, service still starts and both probes return 200. ### Acceptance Criteria (from #90) - [ ] `install` compiles the workspace and places four binaries in the right bin dir. - [ ] `start` seeds `modelsconfig.yml`, registers both actions + the service, reaches `running` with all three sockets bound. - [ ] `--reset` drops all registration, re-seeds the config, re-registers cleanly. - [ ] Idempotent start on a running service is a no-op with actionable hint. - [ ] Three UDS probes (`openrpc.json`, `/v1/models`, `/health`) return 200; UI returns HTML. - [ ] Missing keys → warning, service still starts and passes health. - [ ] `stop` cleanly unregisters; `status` after stop surfaces "not found". - [ ] Re-import via `mod.nu` loads without errors. - [ ] `--root` flips paths everywhere including `svx_seed_models_config`'s sudo branches. ### Notes - **TOML `source` bug**: `hero_zero/services/hero_aibroker.toml` mis-sets `source = "…geomind_code/aibroker.git"`. The module uses `lhumina_code/hero_aibroker`. Nothing in the nu lifecycle path reads the TOML `source`, so the discrepancy does not block this PR; it is a separate hero_zero repo fix. - **modelsconfig.yml provisioning rationale**: the upstream TOML bakes the default config into an inline `sh -c` heredoc prefix on `exec`. Doing the same in a nu `script:` string forces operators to read a messy heredoc and re-stomps operator edits on every restart. Seeding from the repo's `modelsconfig.yml` once (re-seeding on `--reset`) gives operators a single editable file with predictable semantics. - **No `.env` writing**: `Config::load()` auto-loads `~/hero/var/hero_aibroker/.env` via `dotenvy::from_path` if present. The module deliberately does NOT write this file — API keys must come from the operator's shell via action env at register-time. A future `service_secrets`-style helper can manage `.env` separately. - **`hero_broker_server` shipped but not registered**: it's a peer workspace member (search/scraper binary) that `buildenv.sh` includes in `BINARIES` and the workspace build produces unconditionally. Dropping it would force a split-install; including it costs nothing and keeps `buildenv.sh` + `SVX_BINARIES` in lockstep. - **Missing keys: warning, not error**: `Config::load()` tolerates empty key lists. A hard-fail would block valid scenarios (operator only uses OpenRouter but not Groq, or injects keys via a downstream context switch). The warning tells the operator which provider will fail at request time. ### Critical Files for Implementation - `tools/modules/services/service_db.nu` (multi-socket server template) - `tools/modules/services/service_books.nu` (computed env + preflight helper pattern) - `tools/modules/services/service_whiteboard.nu` (overall file shape) - `tools/modules/services/lib.nu` (helpers) - `tools/modules/services/mod.nu` (one-line append)
Author
Owner

Implementation summary

Changes

  • Added tools/modules/services/service_aibroker.nu — ~445 lines.
  • Updated tools/modules/services/mod.nu — appended export use service_aibroker.nu.

End-to-end smoke test on Hetzner (--root)

Smoke ran with OPENROUTER_API_KEY and GROQ_API_KEY both unset — the warning branch. This is the more demanding scenario for this module because it exercises the non-fatal preflight and the no-provider server path.

# Assertion Result
2a service_proc start --root healthy PASS
2b service_aibroker install --root produced 4 binaries (hero_aibroker, hero_aibroker_server, hero_aibroker_ui, hero_broker_server) PASS
2c service_aibroker start --reset --root seeded modelsconfig.yml, printed the missing-key warning naming both absent vars, registered + started the service PASS
2d rpc.sock present PASS
2e rest.sock present PASS
2f ui.sock present PASS
2g /root/hero/var/hero_aibroker/modelsconfig.yml seeded (14 919 bytes, 42 model entries, header preserved) PASS
2h curl --unix-socket rpc.sock /openrpc.json → HTTP 200, OpenRPC 1.3.2, 33 methods PASS
2i curl --unix-socket rest.sock /v1/models → HTTP 200, {"data": []} (empty because no providers initialised — consistent with no-key branch; the server parses the 42-model config but filters to models with an available backend) PASS
2j curl --unix-socket rest.sock /health → HTTP 200 PASS
2k curl --unix-socket ui.sock / → HTTP 200, 83 376-byte HTML dashboard PASS
2l service_aibroker status --root{name: hero_aibroker, state: running, restarts: 0, pid: 3661212, current_run_id: 16} PASS
2m Idempotent start (no --reset) prints "already running" with hint PASS
2n 15 s observation — current_run_id stable at 16, restarts: 0, state running (3 samples) PASS
2o start --reset --root while running — all three sockets reclaimed; rest.sock /health returns HTTP 200 after restart PASS
2o.2 stop, edit modelsconfig.yml with a marker, start --root (no --reset) prints leaving operator edits intact and marker is preserved in the file PASS
2p service_aibroker stop --root✓ hero_aibroker stopped and unregistered PASS
2q Post-stop status returns expected service 'hero_aibroker' not found PASS
2r Post-stop: no hero_aibroker_server/hero_aibroker_ui processes, socket directory /root/hero/var/sockets/hero_aibroker/ empty PASS

Not tested in this run:

  • /v1/models with keys set. Empty-when-no-keys is the server's intended filter behaviour (providers list is empty → only models with an available backend surface), not a module bug. Re-testing with keys set would exercise the provider's request path, not the lifecycle module.
  • --update. Covered indirectly by the install --root path which hits the same cargo / forge helpers as every other merged service.

Notes

  • Missing-key warning path validates cleanly: both keys flagged by name, fix instructions printed, service still registers and passes health checks without intervention.
  • modelsconfig.yml preservation across restart is explicit (operator-edited marker survives stop + start) — this is the main behavioural promise of the new svx_seed_models_config helper.
  • hero_broker_server builds and ships under SVX_BINARIES as planned; not registered as an action, exactly as the spec called for.
  • TOML source = bug (geomind_code/aibroker.git) remains — out of scope per spec.

Acceptance criteria (from #90)

  • install compiles the workspace and places 4 binaries in the right bin dir.
  • start seeds modelsconfig.yml, registers both actions + the service, reaches running with all three sockets bound.
  • --reset drops all registration, re-seeds config, re-registers cleanly.
  • Idempotent start on a running service is a no-op with actionable hint.
  • Three UDS probes (openrpc.json, /v1/models, /health) return 200; UI returns HTML.
  • Missing keys → non-fatal warning, service still starts and passes health.
  • stop cleanly unregisters; status after stop surfaces "not found".
  • Module loadable via use services/mod.nu *.
  • --root flips all paths including svx_seed_models_config's sudo branches.
## Implementation summary ### Changes - Added `tools/modules/services/service_aibroker.nu` — ~445 lines. - Updated `tools/modules/services/mod.nu` — appended `export use service_aibroker.nu`. ### End-to-end smoke test on Hetzner (`--root`) Smoke ran with `OPENROUTER_API_KEY` and `GROQ_API_KEY` **both unset** — the warning branch. This is the more demanding scenario for this module because it exercises the non-fatal preflight *and* the no-provider server path. | # | Assertion | Result | |---|---|---| | 2a | `service_proc start --root` healthy | PASS | | 2b | `service_aibroker install --root` produced 4 binaries (`hero_aibroker`, `hero_aibroker_server`, `hero_aibroker_ui`, `hero_broker_server`) | PASS | | 2c | `service_aibroker start --reset --root` seeded `modelsconfig.yml`, printed the missing-key warning naming both absent vars, registered + started the service | PASS | | 2d | `rpc.sock` present | PASS | | 2e | `rest.sock` present | PASS | | 2f | `ui.sock` present | PASS | | 2g | `/root/hero/var/hero_aibroker/modelsconfig.yml` seeded (14 919 bytes, 42 model entries, header preserved) | PASS | | 2h | `curl --unix-socket rpc.sock /openrpc.json` → HTTP 200, OpenRPC 1.3.2, 33 methods | PASS | | 2i | `curl --unix-socket rest.sock /v1/models` → HTTP 200, `{"data": []}` (empty because no providers initialised — consistent with no-key branch; the server parses the 42-model config but filters to models with an available backend) | PASS | | 2j | `curl --unix-socket rest.sock /health` → HTTP 200 | PASS | | 2k | `curl --unix-socket ui.sock /` → HTTP 200, 83 376-byte HTML dashboard | PASS | | 2l | `service_aibroker status --root` → `{name: hero_aibroker, state: running, restarts: 0, pid: 3661212, current_run_id: 16}` | PASS | | 2m | Idempotent `start` (no `--reset`) prints "already running" with hint | PASS | | 2n | 15 s observation — `current_run_id` stable at 16, `restarts: 0`, state `running` (3 samples) | PASS | | 2o | `start --reset --root` while running — all three sockets reclaimed; `rest.sock /health` returns HTTP 200 after restart | PASS | | 2o.2 | `stop`, edit `modelsconfig.yml` with a marker, `start --root` (no `--reset`) prints `leaving operator edits intact` and marker is preserved in the file | PASS | | 2p | `service_aibroker stop --root` → `✓ hero_aibroker stopped and unregistered` | PASS | | 2q | Post-stop `status` returns expected `service 'hero_aibroker' not found` | PASS | | 2r | Post-stop: no `hero_aibroker_server`/`hero_aibroker_ui` processes, socket directory `/root/hero/var/sockets/hero_aibroker/` empty | PASS | Not tested in this run: - `/v1/models` with keys set. Empty-when-no-keys is the server's intended filter behaviour (providers list is empty → only models with an available backend surface), not a module bug. Re-testing with keys set would exercise the provider's request path, not the lifecycle module. - `--update`. Covered indirectly by the `install --root` path which hits the same cargo / forge helpers as every other merged service. ### Notes - Missing-key warning path validates cleanly: both keys flagged by name, fix instructions printed, service still registers and passes health checks without intervention. - `modelsconfig.yml` preservation across restart is explicit (operator-edited marker survives `stop` + `start`) — this is the main behavioural promise of the new `svx_seed_models_config` helper. - `hero_broker_server` builds and ships under `SVX_BINARIES` as planned; not registered as an action, exactly as the spec called for. - TOML `source =` bug (`geomind_code/aibroker.git`) remains — out of scope per spec. ### Acceptance criteria (from #90) - [x] `install` compiles the workspace and places 4 binaries in the right bin dir. - [x] `start` seeds `modelsconfig.yml`, registers both actions + the service, reaches `running` with all three sockets bound. - [x] `--reset` drops all registration, re-seeds config, re-registers cleanly. - [x] Idempotent start on a running service is a no-op with actionable hint. - [x] Three UDS probes (`openrpc.json`, `/v1/models`, `/health`) return 200; UI returns HTML. - [x] Missing keys → non-fatal warning, service still starts and passes health. - [x] `stop` cleanly unregisters; `status` after stop surfaces "not found". - [x] Module loadable via `use services/mod.nu *`. - [x] `--root` flips all paths including `svx_seed_models_config`'s sudo branches.
Author
Owner

PR opened: #91

PR opened: https://forge.ourworld.tf/lhumina_code/hero_skills/pulls/91
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#90
No description provided.