service_aibroker.nu — hero_aibroker server + UI lifecycle module #90
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_skills#90
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Child of #75.
Objective
Add
tools/modules/services/service_aibroker.nuimplementing the standardinstall | start | stop | statuslifecycle for the hero_aibroker service (server + UI).Scope
ssh://git@forge.ourworld.tf/lhumina_code/hero_aibroker.git(NOTE:hero_zero/services/hero_aibroker.tomlerroneously listsgeomind_code/aibroker.git— the nu module must uselhumina_code/hero_aibroker, which is the canonical repo. Worth a follow-up TOML fix but out of scope here.)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).hero_aibroker_server,hero_aibroker_ui.lhumina_code/hero_zero/services/hero_aibroker.toml.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)$HERO_SOCKET_DIR/hero_aibroker/ui.sock.crates/hero_aibroker_lib/src/config/mod.rs):RUST_LOG=infoMODELS_CONFIG_PATH=$hero_home/var/hero_aibroker/modelsconfig.ymlOPENROUTER_API_KEYS=$OPENROUTER_API_KEY(read at action-build time from current env; empty if unset)GROQ_API_KEY=$GROQ_API_KEY(same)RUST_LOG=infoonly.$repo_path/modelsconfig.ymlinto$hero_home/var/hero_aibroker/modelsconfig.ymlonstart, preserving any existing operator-edited file (write only if missing or--resetwas requested).[package]).--rootflag optional; user-level default.Acceptance criteria
use services/mod.nu *makesservice_aibrokeravailable.service_aibroker install [--root] [--update]cloneslhumina_code/hero_aibroker, builds all 4 binaries, installs to~/hero/bin/(or/root/hero/bin/with--root).service_aibroker start [--reset] [--root] [--update]ensuresmodelsconfig.ymlexists, registers both actions + the service, starts, prints all three socket paths in the summary. Idempotent without--reset;--resetre-seeds the models config.service_aibroker status [--root]reports state.service_aibroker stop [--root]cleanly unregisters.OPENROUTER_API_KEYorGROQ_API_KEYis absent — the service will start but the affected providers will fail their requests.Template & references
service_db.nu(PR #88) — multi-socket serverkill_otherpattern (two UDS here instead of rpc + resp + TCP).service_books.nu(PR #81) — computed-env pattern (HERO_BOOKS_DATA / HERO_EMBEDDER_URL resolved at action-build time).tools/modules/services/lib.nu.Expected deviations
kill_other.socketcovers bothrpc.sockandrest.sock;port: [].MODELS_CONFIG_PATHviasvc_home, API keys read from the invoking env.modelsconfig.ymlfrom the cloned repo (or re-seeds with--reset).script: $bin(bare); serverscript: $bin(bare — no subcommand,main()callsrun_server()directly).Implementation Spec for Issue #90
Objective
Add a Nushell lifecycle module
service_aibroker.nuthat installs, registers, starts, stops, and queries status for thehero_aibrokerservice throughhero_proc, mirroring the shape ofservice_db.nu(multi-socket server with an explicitkill_othersocket list) and borrowing the action-build-time env computation + non-fatal preflight helper pattern fromservice_books.nu.hero_aibrokeris a two-binary registered service —hero_aibroker_server(OpenRPC onrpc.sock+ OpenAI-compatible REST onrest.sock) andhero_aibroker_ui(ui.sock) — with four binaries shipped in total (server, ui, CLIhero_aibroker, and the workspace companionhero_broker_server). The module additionally seeds a defaultmodelsconfig.ymlinto~/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
SVX_BINARIES):hero_aibroker,hero_aibroker_server,hero_aibroker_ui,hero_broker_server. Plaincargo build --releaseon the virtual workspace builds all four.SVX_ACTIONS):hero_aibroker_server,hero_aibroker_ui.hero_aibrokeris the CLI (unregistered).hero_broker_serveris an unrelated workspace companion — shipped but not registered.$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 torest.sock)kill_other:socket: [rpc.sock, rest.sock],port: []. UIkill_other.socket: [ui.sock].--rootflips 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 "")RUST_LOG = "info"only.script: $bin(bare binaries, no subcommands — matches TOMLexecminus the inline heredoc, which we replicate as a dedicated preflight helper instead).depends_on— TOML declares none, health is self-sufficient oncemodelsconfig.ymlis present.modelsconfig.ymlfrom the cloned repo's root-levelmodelsconfig.ymlinto$(svc_home $root)/var/hero_aibroker/modelsconfig.yml. Idempotent unless--reset. Must work under--root(sudomkdir -p+ sudocp).OPENROUTER_API_KEYorGROQ_API_KEYis empty in the invoking environment.--root/--reset/--updateflags — same semantics as every otherservice_*.nu.Files to Modify/Create
tools/modules/services/service_aibroker.nu— the new module.tools/modules/services/mod.nu— appendexport use service_aibroker.nu.Implementation Plan
Step 1: File header / module doc
Copy the
service_db.nuheader shape. Document:hero_aibrokerCLI andhero_broker_server(separate workspace-member service, not this module's concern).hero_proconly.modelsconfig.yml;.envis read by the server if present but is not written by this module.source = …bug:hero_zero/services/hero_aibroker.tomlmis-points atgeomind_code/aibroker.git; the nu module uses the authoritativelhumina_code/hero_aibrokerlocation and ignores the TOML field.Step 2: Imports + constants
No
SVX_*_TCP_PORTconstant — no TCP bind.Step 3: NEW helper
svx_seed_models_config [root: bool, reset: bool]Ensures
$(svc_home $root)/var/hero_aibroker/modelsconfig.ymlexists. Logic:dest_dir = $"(svc_home $root)/var/hero_aibroker",dest = $"($dest_dir)/modelsconfig.yml".forge_ensure_local $SVX_FORGE_LOC(already cloned by priorinstall— safe no-op lookup).src = $"($info.path)/modelsconfig.yml".srcmissing →error makewith actionable message.destexists and not$reset: print→ modelsconfig.yml already present at (...) — leaving operator edits intact, return.svc_need_sudo $root: eithersudo mkdir -p+sudo cp(checking.exit_code) or nativemkdir+^cp.✓ seeded modelsconfig.yml(orre-seededwhen$reset).Import
forge_ensure_localfrom../forge.nu(books already doesuse ../forge.nu [forge_ensure_local]).Step 4: NEW helper
svx_check_api_keys []Non-fatal warning, invoked from
startbetween preflight and register (same slot assvx_check_embedderin books). Independent of$root— the warning is about the invoking shell env because that is what gets captured into the action env.Explicitly does not error —
Config::load()inhero_aibroker_lib/src/config/mod.rshandles empty key lists gracefully.Step 5:
svx_server_action [root: bool]Mirror
svx_server_actioninservice_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"service_dbserver defaults.Step 6:
svx_ui_action [root: bool]Mirror
svx_ui_actioninservice_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"Step 7:
svx_service_config []Step 8:
svx_drop_registration [root: bool]Byte-for-byte clone of
service_db.nuversion.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:if $root { svc_require_sudo }svc_require_proc "service_aibroker" $rootif (not $reset) and (not $update) { … is_running … print and return }.install --root=$root --update=$updatesvc_need_sudo-awaretest -xonhero_aibroker_server).svx_seed_models_config $root $resetsvx_check_api_keys(warning only)svx_drop_registration $rootproc action set (svx_server_action $root) --root=$root | ignoreproc action set (svx_ui_action $root) --root=$root | ignoreproc service set (svx_service_config) --root=$root | ignoreproc service start $SVX_SERVICE_NAME --root=$root | ignoresleep 1sec, fetchis_running, print summary:rpc sock : .../rpc.sockrest sock : .../rest.sockui sock : .../ui.sock+ui url : http+unix://.../linemodels : $models_pathproc service status / proc logs tail hero_aibroker_server / proc logs tail hero_aibroker_uihints.Step 11:
stop [--root]Identical to
service_whiteboard.nu::stopwith the name swap.Step 12:
status [--root]Identical to
service_whiteboard.nu::status.Step 13:
mod.nuAppend one line
export use service_aibroker.nuafter the last existingexport useline.Smoke Test Plan (Hetzner,
--root)service_aibroker install --root && service_aibroker start --rootExpect: 4 binaries in
/root/hero/bin/,modelsconfig.ymlseeded at/root/hero/var/hero_aibroker/modelsconfig.yml, servicerunning, summary printed.curl --unix-socket rpc.sock http://localhost/openrpc.json→ OpenRPC doc.curl --unix-socket rest.sock http://localhost/v1/models→ JSON model list.curl --unix-socket rest.sock http://localhost/health→ 200.curl --unix-socket ui.sock http://localhost/→ HTML dashboard.--resetrestart: drop + re-register + restart, models config re-seeded, all three sockets back up.service 'hero_aibroker' not found.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)
installcompiles the workspace and places four binaries in the right bin dir.startseedsmodelsconfig.yml, registers both actions + the service, reachesrunningwith all three sockets bound.--resetdrops all registration, re-seeds the config, re-registers cleanly.openrpc.json,/v1/models,/health) return 200; UI returns HTML.stopcleanly unregisters;statusafter stop surfaces "not found".mod.nuloads without errors.--rootflips paths everywhere includingsvx_seed_models_config's sudo branches.Notes
sourcebug:hero_zero/services/hero_aibroker.tomlmis-setssource = "…geomind_code/aibroker.git". The module useslhumina_code/hero_aibroker. Nothing in the nu lifecycle path reads the TOMLsource, so the discrepancy does not block this PR; it is a separate hero_zero repo fix.sh -cheredoc prefix onexec. Doing the same in a nuscript:string forces operators to read a messy heredoc and re-stomps operator edits on every restart. Seeding from the repo'smodelsconfig.ymlonce (re-seeding on--reset) gives operators a single editable file with predictable semantics..envwriting:Config::load()auto-loads~/hero/var/hero_aibroker/.envviadotenvy::from_pathif 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 futureservice_secrets-style helper can manage.envseparately.hero_broker_servershipped but not registered: it's a peer workspace member (search/scraper binary) thatbuildenv.shincludes inBINARIESand the workspace build produces unconditionally. Dropping it would force a split-install; including it costs nothing and keepsbuildenv.sh+SVX_BINARIESin lockstep.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 summary
Changes
tools/modules/services/service_aibroker.nu— ~445 lines.tools/modules/services/mod.nu— appendedexport use service_aibroker.nu.End-to-end smoke test on Hetzner (
--root)Smoke ran with
OPENROUTER_API_KEYandGROQ_API_KEYboth 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.service_proc start --roothealthyservice_aibroker install --rootproduced 4 binaries (hero_aibroker,hero_aibroker_server,hero_aibroker_ui,hero_broker_server)service_aibroker start --reset --rootseededmodelsconfig.yml, printed the missing-key warning naming both absent vars, registered + started the servicerpc.sockpresentrest.sockpresentui.sockpresent/root/hero/var/hero_aibroker/modelsconfig.ymlseeded (14 919 bytes, 42 model entries, header preserved)curl --unix-socket rpc.sock /openrpc.json→ HTTP 200, OpenRPC 1.3.2, 33 methodscurl --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)curl --unix-socket rest.sock /health→ HTTP 200curl --unix-socket ui.sock /→ HTTP 200, 83 376-byte HTML dashboardservice_aibroker status --root→{name: hero_aibroker, state: running, restarts: 0, pid: 3661212, current_run_id: 16}start(no--reset) prints "already running" with hintcurrent_run_idstable at 16,restarts: 0, staterunning(3 samples)start --reset --rootwhile running — all three sockets reclaimed;rest.sock /healthreturns HTTP 200 after restartstop, editmodelsconfig.ymlwith a marker,start --root(no--reset) printsleaving operator edits intactand marker is preserved in the fileservice_aibroker stop --root→✓ hero_aibroker stopped and unregisteredstatusreturns expectedservice 'hero_aibroker' not foundhero_aibroker_server/hero_aibroker_uiprocesses, socket directory/root/hero/var/sockets/hero_aibroker/emptyNot tested in this run:
/v1/modelswith 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 theinstall --rootpath which hits the same cargo / forge helpers as every other merged service.Notes
modelsconfig.ymlpreservation across restart is explicit (operator-edited marker survivesstop+start) — this is the main behavioural promise of the newsvx_seed_models_confighelper.hero_broker_serverbuilds and ships underSVX_BINARIESas planned; not registered as an action, exactly as the spec called for.source =bug (geomind_code/aibroker.git) remains — out of scope per spec.Acceptance criteria (from #90)
installcompiles the workspace and places 4 binaries in the right bin dir.startseedsmodelsconfig.yml, registers both actions + the service, reachesrunningwith all three sockets bound.--resetdrops all registration, re-seeds config, re-registers cleanly.openrpc.json,/v1/models,/health) return 200; UI returns HTML.stopcleanly unregisters;statusafter stop surfaces "not found".use services/mod.nu *.--rootflips all paths includingsvx_seed_models_config's sudo branches.PR opened: #91