service_claude.nu — hero_claude ACP server + UI lifecycle module #99
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#99
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_claude.nuimplementing the standardinstall | start | stop | statuslifecycle for the hero_claude service — the ACP (Agent Control Protocol) server that wraps theclaude-agent-sdkPython library, plus a Bootstrap admin UI.Scope
ssh://git@forge.ourworld.tf/lhumina_code/hero_claude.githero_claude_sdk,hero_claude(CLI),hero_claude_ui(admin),hero_claude_examples.crates/hero_claude_server/is not a Rust crate — it holds the Python ACP server (server.py) and the OpenRPC spec. The workspace[workspace] members = […]list excludes it.uv(pyproject.toml,uv.lock).install.shandenv.shin the repo drive the venv.hero_claude,hero_claude_ui. Built viacargo build --release.hero_claude_server— a bash shim thatexecsuv run --project <repo_path> python <repo_path>/crates/hero_claude_server/server.py. Generated ininstallby writing a heredoc to~/hero/bin/hero_claude_serverand chmod +x (matching the repo'sMakefile install:target).hero_claude_server(the wrapper),hero_claude_ui.hero_claudesupports--start/--stopself-registration (per thehero_proc_service_selfstartskill). This module builds the action specs in nu (matching every otherservice_*.nu) and does NOT delegate tohero_claude --start. The two paths produce the same hero_proc registration; operators should use one or the other, not both.build_service_definitionincrates/hero_claude/src/main.rs):~/hero/var/sockets/hero_claude_server.sock(Python ACP server, OpenRPC JSON-RPC)~/hero/var/sockets/hero_claude_ui.sock(Rust admin UI)RUST_LOG=info, plusANTHROPIC_API_KEYforwarded only when set in the invoking env (per main.rs; absent → Claude Max OAuth is used)RUST_LOG=infoonlyuv syncin the repo);uvitself is a hard dep on the host (install.sh auto-installs it if missing, matching ourinstallbehaviour).HERO_CLAUDE_PORT=3780appears in the hero_zero TOML but neither the Python server.py nor the Rust UI binds TCP (both are UDS-only). The TOML entry is unused; this module ignores it.hero_zero/services/hero_claude.tomlregisters only[server], points itsexecat__HERO_BIN__/hero_claude(the CLI, which would no-op without--start), and has no[ui]block. The module ignores the TOML and writes actions/service directly — same handling asservice_aibroker.nu/service_office.nu.--rootflag optional; user-level default.Acceptance criteria
use services/mod.nu *makesservice_claudeavailable.service_claude install [--root] [--update] [--reset]clones, runsuv syncin the repo, builds all Rust binaries viacargo build --release, copieshero_claude+hero_claude_uiinto~/hero/bin/, and writes thehero_claude_servershell wrapper. Skips rebuild viasvc_bins_ok-style short-circuit when both Rust binaries AND the wrapper are present (absent--reset/--update).service_claude start [--reset] [--root] [--update]registers both actions + the service, warns (non-fatally) whenANTHROPIC_API_KEYis unset, prints both socket paths in the summary. Idempotent.service_claude status [--root]reports state.service_claude stop [--root]cleanly unregisters.uvis on PATH at install time, fail-fast with an actionable error message referencinginstall.shwhen not.Template & references
service_office.nu(PR #98) — new canonical shape withsvc_bins_okshort-circuit + conditional env pass-through.service_aibroker.nu— for writing a generated artifact (modelsconfig.yml) into the service's var dir under--rootsudo. Here it is the shell wrapper into/root/hero/bin/hero_claude_server.lhumina_code/hero_claude/crates/hero_claude/src/main.rs::build_service_definition— source of truth for health check policy, stop timeouts, retry policy.hero_proc_service_selfstartskill — confirms the pattern where the CLI owns --start/--stop; operators must use our nu module ORhero_claude --start, never both at once.Expected deviations vs. the
service_office.nubaselinesvc_cargo_installbuilds 2 Rust bins; a new helpersvx_install_server_wrapperwrites the bash heredoc shim.uvpreflight — fail-fast ifuvis missing; hint to run the repo'sinstall.shor installuvvia astral's installer.$HERO_SOCKET_DIR/hero_claude_server.sockand$HERO_SOCKET_DIR/hero_claude_ui.sock, not per-service subdirs.upsert-guarded likeservice_office.nu(absent means "use Claude Max OAuth", not "use empty string").hero_claudeCLI as the user-facing entry point (hero_claude ping,hero_claude task submit …).Implementation Spec for Issue #99
Objective
Add a Nushell lifecycle module that registers, supervises, and tears down the
hero_claudeACP (Agent Control Protocol) service underhero_proc. Unlike every existingservice_*.nu,hero_claudeis a hybrid stack: a Python ACP server (crates/hero_claude_server/server.py) driven viauv run, and a Rust admin UI (hero_claude_ui). The module must (a) build Rust binaries viasvc_cargo_install, (b) resolve Python runtime deps viauv sync, and (c) materialise a bash wrapperhero_claude_serverin<hero_home>/binthatexecs intouv run python server.py. It registers two actions (hero_claude_server,hero_claude_ui) against two flat-layout Unix sockets (<sock_base>/hero_claude_server.sock,<sock_base>/hero_claude_ui.sock), mirroringhero_claude --startexactly so either path produces the same registration.Requirements
lhumina_code/hero_claude, default branchdevelopment. Virtual workspace with Rust membershero_claude_sdk,hero_claude,hero_claude_ui,hero_claude_examples.hero_claude_serveris NOT a workspace member — it's a Python source dir.SVX_BINARIES = ["hero_claude", "hero_claude_ui"](two Rust artifacts).hero_claudeis shipped but NOT an action (user-facing CLI).<bin_dir>/hero_claude_servergenerated at install time,0755: Written viasudo teewhensvc_need_sudo $root, else nativesave --force --raw.SVX_ACTIONS = ["hero_claude_server", "hero_claude_ui"].hero_claude::build_service_definition):<sock_base>/hero_claude_server.sock— Python ACP server (/health,/.well-known/heroservice.json,/openrpc.json,POST /rpc)<sock_base>/hero_claude_ui.sock— Rust UIRUST_LOG: "info"always;ANTHROPIC_API_KEYonly when set (upsertinsideis-not-emptyguard — unset must be OMITTED, not empty string).RUST_LOG: "info"only.crates/hero_claude/src/main.rs::build_service_definition):max_attempts=5, delay_ms=2000, backoff=true, max_delay_ms=60000, start_timeout_ms=30000, stop_timeout_ms=10000; healthinterval=2000, timeout=5000, retries=3, start_period=5000max_attempts=3, delay_ms=2000, backoff=true, max_delay_ms=30000(note: 30000 not 60000),stop_timeout_ms=5000; healthinterval=3000, timeout=5000, retries=3, start_period=5000uvon PATH, Python ≥3.13. Hard-fail preflight on missinguv; surfaceuv syncerror verbatim for Python-version mismatch.ANTHROPIC_API_KEY— preflight warning (non-fatal); service still starts and passes health (claude-agent-sdkOAuth fallback at request time).context_name: "core",class: "system",critical: false,status: "start",description: "ACP — Agent Control Protocol (Python server + Rust UI)".[server] exec = "__HERO_BIN__/hero_claude"points to the CLI, no[ui]block,HERO_CLAUDE_PORT=3780unused.Files to Modify / Create
tools/modules/services/service_claude.nu.tools/modules/services/mod.nu— appendexport use service_claude.nu.Implementation Plan
Step 1: File header
Document: hybrid stack (bash wrapper + Rust UI + Python backend), flat socket names and why, soft dep on
ANTHROPIC_API_KEYwith OAuth fallback, hard deps onuv+ Python 3.13, hero_zero TOML bug triplet, selfstart coexistence caveat (bothhero_claude --startand this module register the same shape; pick one, never both concurrently; this module is preferred because it bundlesuv sync+ wrapper-install).Step 2: Imports
Step 3: Constants
Step 4: NEW helpers (Deviation block)
svx_require_uv []— hard-fail early ifuvnot on PATH. Message: install hint viacurl -LsSf https://astral.sh/uv/install.sh | sh, then reopen shell.svx_uv_sync [repo_path: string]—cd $repo_path+^uv sync --quiet; check$env.LAST_EXIT_CODE; surface stderr on error. Always runs in invoking user's shell (not sudo —.venvis inside the repo anduv run --projectis idempotent).svx_install_server_wrapper [repo_path: string, root: bool]— writes the bash wrapper body with$repo_pathinterpolated. Dest:(svc_bin $SVX_WRAPPER_NAME $root). Sudo branch:echo $body | sudo tee $dst+sudo chmod +x, check.exit_codeon each. Native branch:$body | save --force --raw $dst+^chmod +x. Post-write verify withsvc_need_sudo-awaretest -x.svx_wrapper_ok [root: bool]— bool test for wrapper presence + executable.svx_all_artifacts_ok [root: bool]—(svc_bins_ok $SVX_BINARIES $root) and (svx_wrapper_ok $root). Used byinstall's short-circuit.svx_check_api_key []— non-fatal warning whenANTHROPIC_API_KEYis unset (tone matchessvx_check_api_keysin aibroker):Step 5:
svx_server_action [root: bool]script: (svc_bin $SVX_WRAPPER_NAME $root)(the wrapper, NOThero_claude).upsert-gated ANTHROPIC_API_KEY on top ofRUST_LOG: "info".kill_other.socket: [$"($sock_base)/hero_claude_server.sock"](flat).health_checks[0].openrpc_socket = .../hero_claude_server.sock; retry/policy values as in §Requirements.Step 6:
svx_ui_action [root: bool]script: (svc_bin "hero_claude_ui" $root).env: {RUST_LOG: "info"}.retry_policy.max_delay_ms: 30000(intentional — matches main.rs; differs from office/aibroker 60000).kill_other.socket: [$"($sock_base)/hero_claude_ui.sock"](flat).Step 7:
svx_service_config []— standard shape,description: "ACP — Agent Control Protocol (Python server + Rust UI)".Step 8:
svx_drop_registration [root: bool]— standard shape.Step 9:
install [--root(-r), --update(-u), --reset](Deviation #1 + #2)Order:
if $root { svc_require_sudo }svx_require_uv— hard-fail early before cargo runs.if $update { svc_update $SVX_FORGE_LOC }.if (not $reset) and (not $update) and (svx_all_artifacts_ok $root) { print "→ hero_claude binaries + wrapper already in place — skipping build"; return }.let info = forge_ensure_local $SVX_FORGE_LOC— need$info.path.svx_uv_sync $info.path— before cargo build so Python-version mismatch fails cheap.svc_cargo_install $SVX_FORGE_LOC $SVX_BINARIES $root— builds + installs 2 Rust binaries.svx_install_server_wrapper $info.path $root— write wrapper; must happen AFTER cargo install (bin dir may be created by that step when--root).Step 10:
start [--reset, --root(-r), --update(-u)](Deviation #4 placement)if $root { svc_require_sudo }svc_require_proc "service_claude" $rootinstall --root=$root --update=$update --reset=$resetsvc_bin $SVX_WRAPPER_NAME $root) is executable (there is no Rust server binary).svx_check_api_key(non-fatal warning)svx_drop_registration $rootsleep 1sec+ summary block:Step 11:
stop [--root(-r)], Step 12:status [--root(-r)], Step 13:mod.nuAll standard — see templates.
The six deviations from the
service_office.nubaselinesvx_require_uv,svx_uv_sync,svx_install_server_wrapperadded toinstall.svx_all_artifacts_okincludes the wrapper test.hero_claude/subdir in the sockets dir, to match the CLI's self-registration.ANTHROPIC_API_KEY—upsertinsideis-not-emptyguard +svx_check_api_keysoft warning.hero_claudeis inSVX_BINARIESbut NOTSVX_ACTIONS.Smoke Test Plan (Hetzner,
--root)hero_claude,hero_claude_ui,hero_claude_serverwrapper) in/root/hero/bin/. Wrapper body byte-checked.uv syncor cargo invocation./root/hero/var/sockets/hero_claude_{server,ui}.sock.curl --unix-socket hero_claude_server.sock /health→ 200 + JSON ok.start --resetreclaim — both sockets' inodes change, health still passes.unset ANTHROPIC_API_KEY && service_claude start --reset --root→ warning fires, service still reaches running, both probes 200.Acceptance Criteria (from #99)
installproduces exactly 3 executables in the target bin dir (2 Rust + 1 wrapper).$repo_pathcorrectly interpolated.installshort-circuits viasvx_all_artifacts_okwhen no flags passed.start --resetreachesrunningwithin per-action timeouts; both sockets bound.hero_claude/subdir).curlprobes (server /health, /openrpc.json; ui /health, /) all succeed.--resetreclaim all work.stopclean; post-stop status returns not found.svx_require_uvhard-fails before cargo runs whenuvis absent.mod.nuexposes the module.Notes
[server] exec = "__HERO_BIN__/hero_claude"points to the CLI (would no-op without--start); no[ui]block;HERO_CLAUDE_PORT=3780declared but never bound. Module ignores the TOML entirely — nu lifecycle bypasses hero_zero (same as aibroker/office). Fixing the TOML is separate hero_zero cleanup.hero_claude --startregisters identical shape to this module (we mirroredbuild_service_definitionfor every numeric field). Pick one path; running both simultaneously causes duplicate-action errors or races. Module is preferred path because it bundlesuv sync+ wrapper-install; the selfstart path assumes those artifacts already exist.forge_ensure_local. Differs per user/host (e.g.~/code/...vs./root/hero/code0/...), so cannot be committed or shipped as a static asset.hero_claude_serveris not a Rust crate: top-levelCargo.tomllists onlyhero_claude_sdk, hero_claude, hero_claude_ui, hero_claude_examples;crates/hero_claude_server/holdsserver.py+openrpc.json. The canonicalMakefile install:target is mirrored exactly:uv sync→cargo build --release→ copy 2 bins → heredoc wrapper +chmod +x.hero_claude::build_service_definitionwrites<sock_base>/hero_claude_server.sockand<sock_base>/hero_claude_ui.sockdirectly (no subdir). Changing to subdir layout would break the "either path works" invariant withhero_claude --start.Critical Files for Implementation
tools/modules/services/service_claude.nu(new)tools/modules/services/service_office.nu(shape template — conditional env + preflight-warning patterns)tools/modules/services/service_aibroker.nu(sudo-aware artifact-write pattern for the wrapper + API-key warning helper)tools/modules/services/lib.nu(helpers)tools/modules/services/mod.nu(one-line append)Deferring — architectural concern with the spec as drafted
Flagging this before implementation because the shape above has a drift problem that wasn't obvious until I drafted the six deviations.
The concern
The spec mirrors
hero_claude::build_service_definition(Rust, incrates/hero_claude/src/main.rs) for every numeric field — retry policy, health-check cadence, stop timeouts, socket names, env vars. That's the "both paths produce the same registration" invariant.But in practice the module and the Rust selfstart code would silently diverge any time upstream tunes a parameter. The Rust side has compile-time guarantees from the
hero_proc_sdktypes; the nu side is a hand-transcribed copy that reviewers have to cross-check visually. Every merge tohero_claudemain.rs becomes a silent contract drift against everyhero_skillscheckout.Three of the six deviations (hybrid install, flat sockets, selfstart coexistence caveat) are intrinsic to the repo — they don't go away either way. The other three (duplicate action spec, svc_bins_ok extension, rich nu summary) only exist because we're re-implementing in nu what the CLI already does in Rust.
Cleaner alternative (to consider when we pick this back up)
Split responsibility along the seam that already exists in the repo:
service_claude install [--root] [--update] [--reset]— keep it. This is genuinely our job:uvpreflight,uv sync,cargo build --release, copy binaries, write the bash wrapper. The repo's Makefile encodes this sequence but doesn't give us--root/--reset/ svc_bins_ok short-circuit /forge mergeupdate.service_claude start [--root]— thin wrapper: ensure binaries present, warn on missingANTHROPIC_API_KEY, then^hero_claude --start(as root via sudo when--root). The CLI owns the registration contract.service_claude stop [--root]—^hero_claude --stop.service_claude status [--root]—proc service status hero_claudeas every other service module does (no delegation needed).Trade-offs:
--resetsemantics for registration re-seed,svx_drop_registration, the ship-house summary block (the CLI prints its ownhero_claude started successfullyline instead).Status
Leaving the sub-issue open but deferring implementation. Recommend revisiting after the remaining Tier 1 services land — those are simpler shapes and don't have a canonical Rust-side registration to compete with.