service_claude.nu — hero_claude ACP server + UI lifecycle module #99

Open
opened 2026-04-21 06:43:19 +00:00 by mahmoud · 2 comments
Owner

Child of #75.

Objective

Add tools/modules/services/service_claude.nu implementing the standard install | start | stop | status lifecycle for the hero_claude service — the ACP (Agent Control Protocol) server that wraps the claude-agent-sdk Python library, plus a Bootstrap admin UI.

Scope

  • Repo: ssh://git@forge.ourworld.tf/lhumina_code/hero_claude.git
  • Hybrid Rust + Python service — this is the first one in the services/ tree.
    • Rust workspace members: hero_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.
    • Python deps managed by uv (pyproject.toml, uv.lock). install.sh and env.sh in the repo drive the venv.
  • Binaries installed (SVX_BINARIES = 2): hero_claude, hero_claude_ui. Built via cargo build --release.
  • Shell wrapper installed: hero_claude_server — a bash shim that execs uv run --project <repo_path> python <repo_path>/crates/hero_claude_server/server.py. Generated in install by writing a heredoc to ~/hero/bin/hero_claude_server and chmod +x (matching the repo's Makefile install: target).
  • Runtime actions (SVX_ACTIONS = 2): hero_claude_server (the wrapper), hero_claude_ui.
  • CLI hero_claude supports --start / --stop self-registration (per the hero_proc_service_selfstart skill). This module builds the action specs in nu (matching every other service_*.nu) and does NOT delegate to hero_claude --start. The two paths produce the same hero_proc registration; operators should use one or the other, not both.
  • Sockets (FLAT paths — NOT per-service subdirs, deviation from the recent convention; matches the CLI's build_service_definition in crates/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)
  • Env:
    • server: RUST_LOG=info, plus ANTHROPIC_API_KEY forwarded only when set in the invoking env (per main.rs; absent → Claude Max OAuth is used)
    • ui: RUST_LOG=info only
  • Dependencies: none declared in TOML. Python venv is a hard dep resolved at install time (via uv sync in the repo); uv itself is a hard dep on the host (install.sh auto-installs it if missing, matching our install behaviour).
  • TCP port: HERO_CLAUDE_PORT=3780 appears 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.
  • TOML bug: hero_zero/services/hero_claude.toml registers only [server], points its exec at __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 as service_aibroker.nu / service_office.nu.
  • --root flag optional; user-level default.

Acceptance criteria

  • use services/mod.nu * makes service_claude available.
  • service_claude install [--root] [--update] [--reset] clones, runs uv sync in the repo, builds all Rust binaries via cargo build --release, copies hero_claude + hero_claude_ui into ~/hero/bin/, and writes the hero_claude_server shell wrapper. Skips rebuild via svc_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) when ANTHROPIC_API_KEY is unset, prints both socket paths in the summary. Idempotent.
  • service_claude status [--root] reports state.
  • service_claude stop [--root] cleanly unregisters.
  • Preflight check: verify uv is on PATH at install time, fail-fast with an actionable error message referencing install.sh when not.
  • Smoke-tested on Hetzner: install → start --reset → status shows running with 0 restarts → probe rpc.sock /health + ui.sock / → stop.

Template & references

  • Template: service_office.nu (PR #98) — new canonical shape with svc_bins_ok short-circuit + conditional env pass-through.
  • Reference: service_aibroker.nu — for writing a generated artifact (modelsconfig.yml) into the service's var dir under --root sudo. Here it is the shell wrapper into /root/hero/bin/hero_claude_server.
  • Reference: lhumina_code/hero_claude/crates/hero_claude/src/main.rs::build_service_definition — source of truth for health check policy, stop timeouts, retry policy.
  • Reference: hero_proc_service_selfstart skill — confirms the pattern where the CLI owns --start/--stop; operators must use our nu module OR hero_claude --start, never both at once.

Expected deviations vs. the service_office.nu baseline

  1. Hybrid installsvc_cargo_install builds 2 Rust bins; a new helper svx_install_server_wrapper writes the bash heredoc shim.
  2. uv preflight — fail-fast if uv is missing; hint to run the repo's install.sh or install uv via astral's installer.
  3. Flat socket paths — constants point to $HERO_SOCKET_DIR/hero_claude_server.sock and $HERO_SOCKET_DIR/hero_claude_ui.sock, not per-service subdirs.
  4. Conditional ANTHROPIC_API_KEY env pass-throughupsert-guarded like service_office.nu (absent means "use Claude Max OAuth", not "use empty string").
  5. Summary block mentions hero_claude CLI as the user-facing entry point (hero_claude ping, hero_claude task submit …).
Child of #75. ## Objective Add `tools/modules/services/service_claude.nu` implementing the standard `install | start | stop | status` lifecycle for the **hero_claude** service — the ACP (Agent Control Protocol) server that wraps the `claude-agent-sdk` Python library, plus a Bootstrap admin UI. ## Scope - **Repo**: `ssh://git@forge.ourworld.tf/lhumina_code/hero_claude.git` - **Hybrid Rust + Python service** — this is the first one in the services/ tree. - Rust workspace members: `hero_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. - Python deps managed by `uv` (`pyproject.toml`, `uv.lock`). `install.sh` and `env.sh` in the repo drive the venv. - **Binaries installed** (SVX_BINARIES = 2): `hero_claude`, `hero_claude_ui`. Built via `cargo build --release`. - **Shell wrapper installed**: `hero_claude_server` — a bash shim that `exec`s `uv run --project <repo_path> python <repo_path>/crates/hero_claude_server/server.py`. Generated in `install` by writing a heredoc to `~/hero/bin/hero_claude_server` and chmod +x (matching the repo's `Makefile install:` target). - **Runtime actions** (SVX_ACTIONS = 2): `hero_claude_server` (the wrapper), `hero_claude_ui`. - **CLI `hero_claude`** supports `--start` / `--stop` self-registration (per the `hero_proc_service_selfstart` skill). This module builds the action specs in nu (matching every other `service_*.nu`) and does NOT delegate to `hero_claude --start`. The two paths produce the same hero_proc registration; operators should use one or the other, not both. - **Sockets** (FLAT paths — NOT per-service subdirs, deviation from the recent convention; matches the CLI's `build_service_definition` in `crates/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) - **Env**: - server: `RUST_LOG=info`, plus `ANTHROPIC_API_KEY` forwarded **only when set** in the invoking env (per main.rs; absent → Claude Max OAuth is used) - ui: `RUST_LOG=info` only - **Dependencies**: none declared in TOML. Python venv is a hard dep resolved at install time (via `uv sync` in the repo); `uv` itself is a hard dep on the host (install.sh auto-installs it if missing, matching our `install` behaviour). - **TCP port**: `HERO_CLAUDE_PORT=3780` appears 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. - **TOML bug**: `hero_zero/services/hero_claude.toml` registers only `[server]`, points its `exec` at `__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 as `service_aibroker.nu` / `service_office.nu`. - `--root` flag optional; user-level default. ## Acceptance criteria - [ ] `use services/mod.nu *` makes `service_claude` available. - [ ] `service_claude install [--root] [--update] [--reset]` clones, runs `uv sync` in the repo, builds all Rust binaries via `cargo build --release`, copies `hero_claude` + `hero_claude_ui` into `~/hero/bin/`, and writes the `hero_claude_server` shell wrapper. Skips rebuild via `svc_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) when `ANTHROPIC_API_KEY` is unset, prints both socket paths in the summary. Idempotent. - [ ] `service_claude status [--root]` reports state. - [ ] `service_claude stop [--root]` cleanly unregisters. - [ ] Preflight check: verify `uv` is on PATH at install time, fail-fast with an actionable error message referencing `install.sh` when not. - [ ] Smoke-tested on Hetzner: install → start --reset → status shows running with 0 restarts → probe rpc.sock /health + ui.sock / → stop. ## Template & references - Template: `service_office.nu` (PR #98) — new canonical shape with `svc_bins_ok` short-circuit + conditional env pass-through. - Reference: `service_aibroker.nu` — for writing a generated artifact (modelsconfig.yml) into the service's var dir under `--root` sudo. Here it is the shell wrapper into `/root/hero/bin/hero_claude_server`. - Reference: `lhumina_code/hero_claude/crates/hero_claude/src/main.rs::build_service_definition` — source of truth for health check policy, stop timeouts, retry policy. - Reference: `hero_proc_service_selfstart` skill — confirms the pattern where the CLI owns --start/--stop; operators must use our nu module OR `hero_claude --start`, never both at once. ## Expected deviations vs. the `service_office.nu` baseline 1. **Hybrid install** — `svc_cargo_install` builds 2 Rust bins; a new helper `svx_install_server_wrapper` writes the bash heredoc shim. 2. **`uv` preflight** — fail-fast if `uv` is missing; hint to run the repo's `install.sh` or install `uv` via astral's installer. 3. **Flat socket paths** — constants point to `$HERO_SOCKET_DIR/hero_claude_server.sock` and `$HERO_SOCKET_DIR/hero_claude_ui.sock`, not per-service subdirs. 4. **Conditional ANTHROPIC_API_KEY env pass-through** — `upsert`-guarded like `service_office.nu` (absent means "use Claude Max OAuth", not "use empty string"). 5. **Summary block** mentions `hero_claude` CLI as the user-facing entry point (`hero_claude ping`, `hero_claude task submit …`).
mahmoud self-assigned this 2026-04-21 06:43:29 +00:00
mahmoud added this to the ACTIVE project 2026-04-21 06:43:31 +00:00
mahmoud added this to the now milestone 2026-04-21 06:43:34 +00:00
Author
Owner

Implementation Spec for Issue #99

Objective

Add a Nushell lifecycle module that registers, supervises, and tears down the hero_claude ACP (Agent Control Protocol) service under hero_proc. Unlike every existing service_*.nu, hero_claude is a hybrid stack: a Python ACP server (crates/hero_claude_server/server.py) driven via uv run, and a Rust admin UI (hero_claude_ui). The module must (a) build Rust binaries via svc_cargo_install, (b) resolve Python runtime deps via uv sync, and (c) materialise a bash wrapper hero_claude_server in <hero_home>/bin that execs into uv 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), mirroring hero_claude --start exactly so either path produces the same registration.

Requirements

  • Forge location: lhumina_code/hero_claude, default branch development. Virtual workspace with Rust members hero_claude_sdk, hero_claude, hero_claude_ui, hero_claude_examples. hero_claude_server is NOT a workspace member — it's a Python source dir.
  • Binaries: SVX_BINARIES = ["hero_claude", "hero_claude_ui"] (two Rust artifacts). hero_claude is shipped but NOT an action (user-facing CLI).
  • Shell wrapper: <bin_dir>/hero_claude_server generated at install time, 0755:
    #!/bin/bash
    exec uv run --project <repo_path> python <repo_path>/crates/hero_claude_server/server.py "$@"
    
    Written via sudo tee when svc_need_sudo $root, else native save --force --raw.
  • Actions: SVX_ACTIONS = ["hero_claude_server", "hero_claude_ui"].
  • Sockets (flat — no per-service subdir, mirrors 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 UI
  • Env:
    • server: RUST_LOG: "info" always; ANTHROPIC_API_KEY only when set (upsert inside is-not-empty guard — unset must be OMITTED, not empty string).
    • ui: RUST_LOG: "info" only.
  • Retry/health (authoritative — from crates/hero_claude/src/main.rs::build_service_definition):
    • server: max_attempts=5, delay_ms=2000, backoff=true, max_delay_ms=60000, start_timeout_ms=30000, stop_timeout_ms=10000; health interval=2000, timeout=5000, retries=3, start_period=5000
    • ui: max_attempts=3, delay_ms=2000, backoff=true, max_delay_ms=30000 (note: 30000 not 60000), stop_timeout_ms=5000; health interval=3000, timeout=5000, retries=3, start_period=5000
  • Host deps (hard): uv on PATH, Python ≥3.13. Hard-fail preflight on missing uv; surface uv sync error verbatim for Python-version mismatch.
  • Soft dep: ANTHROPIC_API_KEY — preflight warning (non-fatal); service still starts and passes health (claude-agent-sdk OAuth fallback at request time).
  • Service config: context_name: "core", class: "system", critical: false, status: "start", description: "ACP — Agent Control Protocol (Python server + Rust UI)".
  • TOML bug callout (header-only; module does not read hero_zero TOML): [server] exec = "__HERO_BIN__/hero_claude" points to the CLI, no [ui] block, HERO_CLAUDE_PORT=3780 unused.

Files to Modify / Create

  • Create tools/modules/services/service_claude.nu.
  • Modify tools/modules/services/mod.nu — append export 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_KEY with OAuth fallback, hard deps on uv + Python 3.13, hero_zero TOML bug triplet, selfstart coexistence caveat (both hero_claude --start and this module register the same shape; pick one, never both concurrently; this module is preferred because it bundles uv sync + wrapper-install).

Step 2: Imports

use ../clients/proc.nu *
use ../forge.nu [forge_ensure_local]
use ./lib.nu *

Step 3: Constants

const SVX_SERVICE_NAME = "hero_claude"
const SVX_FORGE_LOC    = "lhumina_code/hero_claude"
const SVX_BINARIES     = ["hero_claude" "hero_claude_ui"]
const SVX_ACTIONS      = ["hero_claude_server" "hero_claude_ui"]
const SVX_WRAPPER_NAME = "hero_claude_server"

Step 4: NEW helpers (Deviation block)

svx_require_uv [] — hard-fail early if uv not on PATH. Message: install hint via curl -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 — .venv is inside the repo and uv run --project is idempotent).

svx_install_server_wrapper [repo_path: string, root: bool] — writes the bash wrapper body with $repo_path interpolated. Dest: (svc_bin $SVX_WRAPPER_NAME $root). Sudo branch: echo $body | sudo tee $dst + sudo chmod +x, check .exit_code on each. Native branch: $body | save --force --raw $dst + ^chmod +x. Post-write verify with svc_need_sudo-aware test -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 by install's short-circuit.

svx_check_api_key [] — non-fatal warning when ANTHROPIC_API_KEY is unset (tone matches svx_check_api_keys in aibroker):

⚠ ANTHROPIC_API_KEY is not set
  hero_claude_server will start and pass health checks. Requests will
  fall back to Claude Max OAuth via claude-agent-sdk at call time; if
  that OAuth session is also missing, /rpc calls will fail until a key
  is exported:
    export ANTHROPIC_API_KEY=sk-…
    service_claude start --reset

Step 5: svx_server_action [root: bool]

  • script: (svc_bin $SVX_WRAPPER_NAME $root) (the wrapper, NOT hero_claude).
  • Env: upsert-gated ANTHROPIC_API_KEY on top of RUST_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).
  • Health values per §Requirements.

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:

  1. if $root { svc_require_sudo }
  2. svx_require_uv — hard-fail early before cargo runs.
  3. if $update { svc_update $SVX_FORGE_LOC }.
  4. Short-circuit: if (not $reset) and (not $update) and (svx_all_artifacts_ok $root) { print "→ hero_claude binaries + wrapper already in place — skipping build"; return }.
  5. let info = forge_ensure_local $SVX_FORGE_LOC — need $info.path.
  6. svx_uv_sync $info.path — before cargo build so Python-version mismatch fails cheap.
  7. svc_cargo_install $SVX_FORGE_LOC $SVX_BINARIES $root — builds + installs 2 Rust binaries.
  8. 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)

  1. if $root { svc_require_sudo }
  2. svc_require_proc "service_claude" $root
  3. Early-exit is-running check
  4. install --root=$root --update=$update --reset=$reset
  5. Post-install bin check: verify the WRAPPER (svc_bin $SVX_WRAPPER_NAME $root) is executable (there is no Rust server binary).
  6. NEW svx_check_api_key (non-fatal warning)
  7. svx_drop_registration $root
  8. server/ui action set, service set, start
  9. sleep 1sec + summary block:
=== hero_claude registered & started ===
  service  : hero_claude
  actions  : hero_claude_server, hero_claude_ui
  state    : running | NOT running
  rpc  sock: <sock_base>/hero_claude_server.sock
  ui   sock: <sock_base>/hero_claude_ui.sock
  ui   url : http+unix://<ui_sock>/
             served by hero_claude_ui; reach the UI via hero_router
  commands :
    proc service status hero_claude[ --root]
    proc logs tail hero_claude_server[ --root]
    proc logs tail hero_claude_ui[ --root]

Step 11: stop [--root(-r)], Step 12: status [--root(-r)], Step 13: mod.nu

All standard — see templates.

The six deviations from the service_office.nu baseline

  1. Hybrid installsvx_require_uv, svx_uv_sync, svx_install_server_wrapper added to install.
  2. Extended short-circuitsvx_all_artifacts_ok includes the wrapper test.
  3. Flat socket paths — no hero_claude/ subdir in the sockets dir, to match the CLI's self-registration.
  4. Conditional ANTHROPIC_API_KEYupsert inside is-not-empty guard + svx_check_api_key soft warning.
  5. No CLI actionhero_claude is in SVX_BINARIES but NOT SVX_ACTIONS.
  6. Selfstart coexistence caveat explicitly documented in the header.

Smoke Test Plan (Hetzner, --root)

  1. Install cold — expect 3 executables (hero_claude, hero_claude_ui, hero_claude_server wrapper) in /root/hero/bin/. Wrapper body byte-checked.
  2. Install warm — short-circuit prints the "already in place" banner; no uv sync or cargo invocation.
  3. Start --reset — both sockets appear flat at /root/hero/var/sockets/hero_claude_{server,ui}.sock.
  4. Server /healthcurl --unix-socket hero_claude_server.sock /health → 200 + JSON ok.
  5. Server /openrpc.json — 200 + OpenRPC 1.x JSON.
  6. UI /health — 200.
  7. UI / — 200 + HTML.
  8. Idempotent start — "already running" banner.
  9. 15s stability — no restarts.
  10. start --reset reclaim — both sockets' inodes change, health still passes.
  11. Clean stop — sockets removed.
  12. Post-stop status — not-found.
  13. Missing-key warningunset ANTHROPIC_API_KEY && service_claude start --reset --root → warning fires, service still reaches running, both probes 200.

Acceptance Criteria (from #99)

  • install produces exactly 3 executables in the target bin dir (2 Rust + 1 wrapper).
  • Wrapper is byte-identical to the template with $repo_path correctly interpolated.
  • install short-circuits via svx_all_artifacts_ok when no flags passed.
  • start --reset reaches running within per-action timeouts; both sockets bound.
  • Sockets live at the flat paths (no hero_claude/ subdir).
  • curl probes (server /health, /openrpc.json; ui /health, /) all succeed.
  • Idempotent start, 15s stability, --reset reclaim all work.
  • stop clean; post-stop status returns not found.
  • Missing-key warning fires and service still healthy.
  • svx_require_uv hard-fails before cargo runs when uv is absent.
  • mod.nu exposes the module.

Notes

  • hero_zero TOML bug: [server] exec = "__HERO_BIN__/hero_claude" points to the CLI (would no-op without --start); no [ui] block; HERO_CLAUDE_PORT=3780 declared 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.
  • Selfstart coexistence: hero_claude --start registers identical shape to this module (we mirrored build_service_definition for every numeric field). Pick one path; running both simultaneously causes duplicate-action errors or races. Module is preferred path because it bundles uv sync + wrapper-install; the selfstart path assumes those artifacts already exist.
  • Wrapper install-time-generated: bakes in absolute repo path from 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_server is not a Rust crate: top-level Cargo.toml lists only hero_claude_sdk, hero_claude, hero_claude_ui, hero_claude_examples; crates/hero_claude_server/ holds server.py + openrpc.json. The canonical Makefile install: target is mirrored exactly: uv synccargo build --release → copy 2 bins → heredoc wrapper + chmod +x.
  • Flat socket paths preserved: hero_claude::build_service_definition writes <sock_base>/hero_claude_server.sock and <sock_base>/hero_claude_ui.sock directly (no subdir). Changing to subdir layout would break the "either path works" invariant with hero_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)
## Implementation Spec for Issue #99 ### Objective Add a Nushell lifecycle module that registers, supervises, and tears down the `hero_claude` ACP (Agent Control Protocol) service under `hero_proc`. Unlike every existing `service_*.nu`, `hero_claude` is a hybrid stack: a Python ACP server (`crates/hero_claude_server/server.py`) driven via `uv run`, and a Rust admin UI (`hero_claude_ui`). The module must (a) build Rust binaries via `svc_cargo_install`, (b) resolve Python runtime deps via `uv sync`, and (c) materialise a bash wrapper `hero_claude_server` in `<hero_home>/bin` that `exec`s into `uv 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`), mirroring `hero_claude --start` exactly so either path produces the same registration. ### Requirements - **Forge location**: `lhumina_code/hero_claude`, default branch `development`. Virtual workspace with Rust members `hero_claude_sdk`, `hero_claude`, `hero_claude_ui`, `hero_claude_examples`. `hero_claude_server` is NOT a workspace member — it's a Python source dir. - **Binaries**: `SVX_BINARIES = ["hero_claude", "hero_claude_ui"]` (two Rust artifacts). `hero_claude` is shipped but NOT an action (user-facing CLI). - **Shell wrapper**: `<bin_dir>/hero_claude_server` generated at install time, `0755`: ``` #!/bin/bash exec uv run --project <repo_path> python <repo_path>/crates/hero_claude_server/server.py "$@" ``` Written via `sudo tee` when `svc_need_sudo $root`, else native `save --force --raw`. - **Actions**: `SVX_ACTIONS = ["hero_claude_server", "hero_claude_ui"]`. - **Sockets (flat — no per-service subdir, mirrors `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 UI - **Env**: - server: `RUST_LOG: "info"` always; `ANTHROPIC_API_KEY` only when set (`upsert` inside `is-not-empty` guard — unset must be OMITTED, not empty string). - ui: `RUST_LOG: "info"` only. - **Retry/health** (authoritative — from `crates/hero_claude/src/main.rs::build_service_definition`): - server: `max_attempts=5, delay_ms=2000, backoff=true, max_delay_ms=60000, start_timeout_ms=30000, stop_timeout_ms=10000`; health `interval=2000, timeout=5000, retries=3, start_period=5000` - ui: `max_attempts=3, delay_ms=2000, backoff=true, max_delay_ms=30000` (note: 30000 not 60000), `stop_timeout_ms=5000`; health `interval=3000, timeout=5000, retries=3, start_period=5000` - **Host deps (hard)**: `uv` on PATH, Python ≥3.13. Hard-fail preflight on missing `uv`; surface `uv sync` error verbatim for Python-version mismatch. - **Soft dep**: `ANTHROPIC_API_KEY` — preflight warning (non-fatal); service still starts and passes health (`claude-agent-sdk` OAuth fallback at request time). - **Service config**: `context_name: "core"`, `class: "system"`, `critical: false`, `status: "start"`, `description: "ACP — Agent Control Protocol (Python server + Rust UI)"`. - **TOML bug callout** (header-only; module does not read hero_zero TOML): `[server] exec = "__HERO_BIN__/hero_claude"` points to the CLI, no `[ui]` block, `HERO_CLAUDE_PORT=3780` unused. ### Files to Modify / Create - **Create** `tools/modules/services/service_claude.nu`. - **Modify** `tools/modules/services/mod.nu` — append `export 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_KEY` with OAuth fallback, hard deps on `uv` + Python 3.13, hero_zero TOML bug triplet, **selfstart coexistence caveat** (both `hero_claude --start` and this module register the same shape; pick one, never both concurrently; this module is preferred because it bundles `uv sync` + wrapper-install). #### Step 2: Imports ``` use ../clients/proc.nu * use ../forge.nu [forge_ensure_local] use ./lib.nu * ``` #### Step 3: Constants ``` const SVX_SERVICE_NAME = "hero_claude" const SVX_FORGE_LOC = "lhumina_code/hero_claude" const SVX_BINARIES = ["hero_claude" "hero_claude_ui"] const SVX_ACTIONS = ["hero_claude_server" "hero_claude_ui"] const SVX_WRAPPER_NAME = "hero_claude_server" ``` #### Step 4: NEW helpers (Deviation block) **`svx_require_uv []`** — hard-fail early if `uv` not on PATH. Message: install hint via `curl -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 — `.venv` is inside the repo and `uv run --project` is idempotent). **`svx_install_server_wrapper [repo_path: string, root: bool]`** — writes the bash wrapper body with `$repo_path` interpolated. Dest: `(svc_bin $SVX_WRAPPER_NAME $root)`. Sudo branch: `echo $body | sudo tee $dst` + `sudo chmod +x`, check `.exit_code` on each. Native branch: `$body | save --force --raw $dst` + `^chmod +x`. Post-write verify with `svc_need_sudo`-aware `test -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 by `install`'s short-circuit. **`svx_check_api_key []`** — non-fatal warning when `ANTHROPIC_API_KEY` is unset (tone matches `svx_check_api_keys` in aibroker): ``` ⚠ ANTHROPIC_API_KEY is not set hero_claude_server will start and pass health checks. Requests will fall back to Claude Max OAuth via claude-agent-sdk at call time; if that OAuth session is also missing, /rpc calls will fail until a key is exported: export ANTHROPIC_API_KEY=sk-… service_claude start --reset ``` #### Step 5: `svx_server_action [root: bool]` - `script: (svc_bin $SVX_WRAPPER_NAME $root)` (the wrapper, NOT `hero_claude`). - Env: `upsert`-gated ANTHROPIC_API_KEY on top of `RUST_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). - Health values per §Requirements. #### 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: 1. `if $root { svc_require_sudo }` 2. `svx_require_uv` — hard-fail early before cargo runs. 3. `if $update { svc_update $SVX_FORGE_LOC }`. 4. **Short-circuit**: `if (not $reset) and (not $update) and (svx_all_artifacts_ok $root) { print "→ hero_claude binaries + wrapper already in place — skipping build"; return }`. 5. `let info = forge_ensure_local $SVX_FORGE_LOC` — need `$info.path`. 6. `svx_uv_sync $info.path` — before cargo build so Python-version mismatch fails cheap. 7. `svc_cargo_install $SVX_FORGE_LOC $SVX_BINARIES $root` — builds + installs 2 Rust binaries. 8. `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) 1. `if $root { svc_require_sudo }` 2. `svc_require_proc "service_claude" $root` 3. Early-exit is-running check 4. `install --root=$root --update=$update --reset=$reset` 5. **Post-install bin check**: verify the WRAPPER (`svc_bin $SVX_WRAPPER_NAME $root`) is executable (there is no Rust server binary). 6. **NEW** `svx_check_api_key` (non-fatal warning) 7. `svx_drop_registration $root` 8. server/ui action set, service set, start 9. `sleep 1sec` + summary block: ``` === hero_claude registered & started === service : hero_claude actions : hero_claude_server, hero_claude_ui state : running | NOT running rpc sock: <sock_base>/hero_claude_server.sock ui sock: <sock_base>/hero_claude_ui.sock ui url : http+unix://<ui_sock>/ served by hero_claude_ui; reach the UI via hero_router commands : proc service status hero_claude[ --root] proc logs tail hero_claude_server[ --root] proc logs tail hero_claude_ui[ --root] ``` #### Step 11: `stop [--root(-r)]`, Step 12: `status [--root(-r)]`, Step 13: `mod.nu` All standard — see templates. ### The six deviations from the `service_office.nu` baseline 1. **Hybrid install** — `svx_require_uv`, `svx_uv_sync`, `svx_install_server_wrapper` added to `install`. 2. **Extended short-circuit** — `svx_all_artifacts_ok` includes the wrapper test. 3. **Flat socket paths** — no `hero_claude/` subdir in the sockets dir, to match the CLI's self-registration. 4. **Conditional `ANTHROPIC_API_KEY`** — `upsert` inside `is-not-empty` guard + `svx_check_api_key` soft warning. 5. **No CLI action** — `hero_claude` is in `SVX_BINARIES` but NOT `SVX_ACTIONS`. 6. **Selfstart coexistence caveat** explicitly documented in the header. ### Smoke Test Plan (Hetzner, `--root`) 1. **Install cold** — expect 3 executables (`hero_claude`, `hero_claude_ui`, `hero_claude_server` wrapper) in `/root/hero/bin/`. Wrapper body byte-checked. 2. **Install warm** — short-circuit prints the "already in place" banner; no `uv sync` or cargo invocation. 3. **Start --reset** — both sockets appear flat at `/root/hero/var/sockets/hero_claude_{server,ui}.sock`. 4. **Server /health** — `curl --unix-socket hero_claude_server.sock /health` → 200 + JSON ok. 5. **Server /openrpc.json** — 200 + OpenRPC 1.x JSON. 6. **UI /health** — 200. 7. **UI /** — 200 + HTML. 8. **Idempotent start** — "already running" banner. 9. **15s stability** — no restarts. 10. **`start --reset` reclaim** — both sockets' inodes change, health still passes. 11. **Clean stop** — sockets removed. 12. **Post-stop status** — not-found. 13. **Missing-key warning** — `unset ANTHROPIC_API_KEY && service_claude start --reset --root` → warning fires, service still reaches running, both probes 200. ### Acceptance Criteria (from #99) - [ ] `install` produces exactly 3 executables in the target bin dir (2 Rust + 1 wrapper). - [ ] Wrapper is byte-identical to the template with `$repo_path` correctly interpolated. - [ ] `install` short-circuits via `svx_all_artifacts_ok` when no flags passed. - [ ] `start --reset` reaches `running` within per-action timeouts; both sockets bound. - [ ] Sockets live at the flat paths (no `hero_claude/` subdir). - [ ] `curl` probes (server /health, /openrpc.json; ui /health, /) all succeed. - [ ] Idempotent start, 15s stability, `--reset` reclaim all work. - [ ] `stop` clean; post-stop status returns not found. - [ ] Missing-key warning fires and service still healthy. - [ ] `svx_require_uv` hard-fails before cargo runs when `uv` is absent. - [ ] `mod.nu` exposes the module. ### Notes - **hero_zero TOML bug**: `[server] exec = "__HERO_BIN__/hero_claude"` points to the CLI (would no-op without `--start`); no `[ui]` block; `HERO_CLAUDE_PORT=3780` declared 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. - **Selfstart coexistence**: `hero_claude --start` registers identical shape to this module (we mirrored `build_service_definition` for every numeric field). Pick one path; running both simultaneously causes duplicate-action errors or races. Module is preferred path because it bundles `uv sync` + wrapper-install; the selfstart path assumes those artifacts already exist. - **Wrapper install-time-generated**: bakes in absolute repo path from `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_server` is not a Rust crate**: top-level `Cargo.toml` lists only `hero_claude_sdk, hero_claude, hero_claude_ui, hero_claude_examples`; `crates/hero_claude_server/` holds `server.py` + `openrpc.json`. The canonical `Makefile install:` target is mirrored exactly: `uv sync` → `cargo build --release` → copy 2 bins → heredoc wrapper + `chmod +x`. - **Flat socket paths preserved**: `hero_claude::build_service_definition` writes `<sock_base>/hero_claude_server.sock` and `<sock_base>/hero_claude_ui.sock` directly (no subdir). Changing to subdir layout would break the "either path works" invariant with `hero_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)
Author
Owner

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, in crates/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_sdk types; the nu side is a hand-transcribed copy that reviewers have to cross-check visually. Every merge to hero_claude main.rs becomes a silent contract drift against every hero_skills checkout.

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:

  1. service_claude install [--root] [--update] [--reset] — keep it. This is genuinely our job: uv preflight, 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 merge update.
  2. service_claude start [--root] — thin wrapper: ensure binaries present, warn on missing ANTHROPIC_API_KEY, then ^hero_claude --start (as root via sudo when --root). The CLI owns the registration contract.
  3. service_claude stop [--root]^hero_claude --stop.
  4. service_claude status [--root]proc service status hero_claude as every other service module does (no delegation needed).

Trade-offs:

  • Loses: --reset semantics for registration re-seed, svx_drop_registration, the ship-house summary block (the CLI prints its own hero_claude started successfully line instead).
  • Gains: ~150 lines instead of ~450; drift-proof by construction; whoever owns the Rust spec owns the only copy of the spec.

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.

## 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, in `crates/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_sdk` types; the nu side is a hand-transcribed copy that reviewers have to cross-check visually. Every merge to `hero_claude` main.rs becomes a silent contract drift against every `hero_skills` checkout. 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: 1. **`service_claude install [--root] [--update] [--reset]`** — keep it. This is genuinely our job: `uv` preflight, `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 merge` update. 2. **`service_claude start [--root]`** — thin wrapper: ensure binaries present, warn on missing `ANTHROPIC_API_KEY`, then `^hero_claude --start` (as root via sudo when `--root`). The CLI owns the registration contract. 3. **`service_claude stop [--root]`** — `^hero_claude --stop`. 4. **`service_claude status [--root]`** — `proc service status hero_claude` as every other service module does (no delegation needed). Trade-offs: - Loses: `--reset` semantics for registration re-seed, `svx_drop_registration`, the ship-house summary block (the CLI prints its own `hero_claude started successfully` line instead). - Gains: ~150 lines instead of ~450; drift-proof by construction; whoever owns the Rust spec owns the only copy of the spec. ### 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.
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#99
No description provided.