service_office.nu — hero_office server + UI lifecycle module #97

Closed
opened 2026-04-21 06:12:27 +00:00 by mahmoud · 3 comments
Owner

Child of #75.

Objective

Add tools/modules/services/service_office.nu implementing the standard install | start | stop | status lifecycle for the hero_office service (OnlyOffice connector — server + UI). The repo already ships an in-tree scripts/nu_service.nu with most of the shape; this issue lifts that into the central services/ tree (so use services/mod.nu * surfaces service_office) with three small additions that match the newer modules (svc_bins_ok short-circuit, hero_foundry soft-dep warning, richer summary block).

Scope

  • Repo: ssh://git@forge.ourworld.tf/lhumina_code/hero_office.git
  • Binaries (per buildenv.sh): hero_office_server, hero_office_ui (no separate CLI — the __HERO_BIN__/hero_office reference in the hero_zero TOML is a bug).
  • Runtime actions: hero_office_server, hero_office_ui.
  • TOML: lhumina_code/hero_zero/services/hero_office.toml. KNOWN BUG — points [server] exec at a non-existent hero_office binary and has no [ui] block. The module ignores the TOML and uses buildenv.sh / the in-repo nu_service.nu as ground truth. Fix of the TOML is out of scope.
  • Sockets:
    • $HERO_SOCKET_DIR/hero_office/rpc.sock — OpenRPC JSON-RPC (health-checked)
    • $HERO_SOCKET_DIR/hero_office/ui.sock — admin + OnlyOffice wrapper pages
  • Env (pass-through only when set in the invoking env, matches in-repo module):
    • server: RUST_LOG=info,hero_office_server=debug, plus optional ONLYOFFICE_JWT_SECRET, OO_SERVER_URL, CONNECTOR_EXTERNAL_URL, DEFAULT_CONTEXT, HERO_SOCKET_DIR
    • ui: RUST_LOG=info,hero_office_ui=debug, plus optional OO_SERVER_URL, CONNECTOR_EXTERNAL_URL, DEFAULT_CONTEXT, HERO_SOCKET_DIR
  • Dependencies: none in TOML. Server uses hero_foundry/rpc.sock at request time for file ops; absence is a soft warning (service starts + health OK, only file operations fail).
  • Workspace: virtual ([workspace] only), 4 members, 2 with [[bin]]. Plain svc_cargo_install suffices.
  • --root flag optional; user-level default.

Acceptance criteria

  • use services/mod.nu * makes service_office available.
  • service_office install [--root] [--update] [--reset] clones lhumina_code/hero_office, skips rebuild when both binaries already exist (via svc_bins_ok) unless --reset or --update.
  • service_office start [--reset] [--root] [--update] registers both actions + the service, seeds optional OnlyOffice env when set in the invoking shell, warns (non-fatally) when hero_foundry socket is absent, prints both socket paths in the summary. Idempotent without --reset.
  • service_office status [--root] reports state.
  • service_office stop [--root] cleanly unregisters.
  • Smoke-tested on Hetzner: install → start --reset → status shows running with 0 restarts → probe rpc.sock + ui.sock → stop.

Template & references

  • Template: service_whiteboard.nu for the two-binary baseline; service_db.nu for the new --reset + svc_bins_ok short-circuit pattern on install; service_books.nu for the svx_check_foundry soft-dep warning.
  • Source-of-truth: lhumina_code/hero_office/scripts/nu_service.nu — already captures env pass-through and kill_other/health-check shapes; our module aligns with it.
  • Shared helpers: tools/modules/services/lib.nu.
Child of #75. ## Objective Add `tools/modules/services/service_office.nu` implementing the standard `install | start | stop | status` lifecycle for the **hero_office** service (OnlyOffice connector — server + UI). The repo already ships an in-tree `scripts/nu_service.nu` with most of the shape; this issue lifts that into the central `services/` tree (so `use services/mod.nu *` surfaces `service_office`) with three small additions that match the newer modules (`svc_bins_ok` short-circuit, hero_foundry soft-dep warning, richer summary block). ## Scope - **Repo**: `ssh://git@forge.ourworld.tf/lhumina_code/hero_office.git` - **Binaries** (per `buildenv.sh`): `hero_office_server`, `hero_office_ui` (no separate CLI — the `__HERO_BIN__/hero_office` reference in the hero_zero TOML is a bug). - **Runtime actions**: `hero_office_server`, `hero_office_ui`. - **TOML**: `lhumina_code/hero_zero/services/hero_office.toml`. KNOWN BUG — points `[server] exec` at a non-existent `hero_office` binary and has no `[ui]` block. The module ignores the TOML and uses `buildenv.sh` / the in-repo `nu_service.nu` as ground truth. Fix of the TOML is out of scope. - **Sockets**: - `$HERO_SOCKET_DIR/hero_office/rpc.sock` — OpenRPC JSON-RPC (health-checked) - `$HERO_SOCKET_DIR/hero_office/ui.sock` — admin + OnlyOffice wrapper pages - **Env** (pass-through only when set in the invoking env, matches in-repo module): - server: `RUST_LOG=info,hero_office_server=debug`, plus optional `ONLYOFFICE_JWT_SECRET`, `OO_SERVER_URL`, `CONNECTOR_EXTERNAL_URL`, `DEFAULT_CONTEXT`, `HERO_SOCKET_DIR` - ui: `RUST_LOG=info,hero_office_ui=debug`, plus optional `OO_SERVER_URL`, `CONNECTOR_EXTERNAL_URL`, `DEFAULT_CONTEXT`, `HERO_SOCKET_DIR` - **Dependencies**: none in TOML. Server uses `hero_foundry/rpc.sock` at request time for file ops; absence is a soft warning (service starts + health OK, only file operations fail). - **Workspace**: virtual (`[workspace]` only), 4 members, 2 with `[[bin]]`. Plain `svc_cargo_install` suffices. - `--root` flag optional; user-level default. ## Acceptance criteria - [ ] `use services/mod.nu *` makes `service_office` available. - [ ] `service_office install [--root] [--update] [--reset]` clones `lhumina_code/hero_office`, skips rebuild when both binaries already exist (via `svc_bins_ok`) unless `--reset` or `--update`. - [ ] `service_office start [--reset] [--root] [--update]` registers both actions + the service, seeds optional OnlyOffice env when set in the invoking shell, warns (non-fatally) when `hero_foundry` socket is absent, prints both socket paths in the summary. Idempotent without `--reset`. - [ ] `service_office status [--root]` reports state. - [ ] `service_office stop [--root]` cleanly unregisters. - [ ] Smoke-tested on Hetzner: install → start --reset → status shows running with 0 restarts → probe rpc.sock + ui.sock → stop. ## Template & references - Template: `service_whiteboard.nu` for the two-binary baseline; `service_db.nu` for the new `--reset` + `svc_bins_ok` short-circuit pattern on `install`; `service_books.nu` for the `svx_check_foundry` soft-dep warning. - Source-of-truth: `lhumina_code/hero_office/scripts/nu_service.nu` — already captures env pass-through and kill_other/health-check shapes; our module aligns with it. - Shared helpers: `tools/modules/services/lib.nu`.
Author
Owner

Implementation Spec for Issue #97

Objective

Add a Nushell lifecycle module that manages the hero_office service (OnlyOffice connector) through hero_proc, exactly the way service_db.nu / service_whiteboard.nu / service_books.nu / service_aibroker.nu manage theirs. The module supervises two binaries — hero_office_server and hero_office_ui — both of which bind Unix domain sockets only (no TCP ports). The hero_office repo already ships an in-tree scripts/nu_service.nu that is functionally equivalent; this module reproduces that behaviour inside the central hero_skills services index with four deviations listed in §4.

Requirements

  • Two-binary service: hero_office_server + hero_office_ui, both registered under a single service hero_office.
  • Sockets (UDS only, no TCP): $HERO_SOCKET_DIR/hero_office/rpc.sock (server) and $HERO_SOCKET_DIR/hero_office/ui.sock (UI).
  • Build: plain cargo build --release at the virtual workspace root. buildenv.sh lists exactly BINARIES="hero_office_server hero_office_ui". No CLI binary ships — SVX_BINARIES == SVX_ACTIONS.
  • Action script is the bare binary path for both (no subcommand — neither main.rs has a clap parser).
  • Env pass-through — forward the following vars only when they are set in the invoking environment (resolved at action-build time so --root flips paths correctly):
    • Server: ONLYOFFICE_JWT_SECRET, OO_SERVER_URL, CONNECTOR_EXTERNAL_URL, DEFAULT_CONTEXT, HERO_SOCKET_DIR.
    • UI: OO_SERVER_URL, CONNECTOR_EXTERNAL_URL, DEFAULT_CONTEXT, HERO_SOCKET_DIR.
    • RUST_LOG base: info,hero_office_server=debug (server) and info,hero_office_ui=debug (UI). Always set.
  • Retry / health / kill_other / stop-signal values are taken verbatim from the in-repo scripts/nu_service.nu — server max_attempts=5 / UI=3, 2s delay, backoff, 60s cap, 30s start+stability; server stop_timeout_ms=10000 / UI=5000; health interval 2s server / 3s UI, timeout 5s, retries 3, start_period 3s server / 5s UI.
  • Soft dependency on hero_foundry: start emits a warning when $HERO_SOCKET_DIR/hero_foundry/rpc.sock is absent but does NOT fail (server starts and health-checks clean without it; only document-read/write RPCs fail until foundry comes up; server retries on next RPC so no hero_office restart is needed).
  • Hard dependency on hero_proc as always — svc_require_proc path.
  • install carries the new canonical shape: --root(-r), --update(-u), --reset; short-circuit via svc_bins_ok when neither --reset nor --update and all binaries already in place; otherwise delegate to svc_cargo_install.
  • start idempotent; stop tolerates dead hero_proc.
  • Rich summary block at the tail of start: service / actions / state / rpc sock / ui sock / ui url http+unix://…/ / commands hints. Ship-house style matching service_db.nu / service_aibroker.nu.

Files to Modify / Create

  • tools/modules/services/service_office.nu — new module.
  • tools/modules/services/mod.nu — append export use service_office.nu.

Nothing else is touched. In particular, hero_zero/services/hero_office.toml is NOT modified — see §7.

Implementation Plan

Step 1: Header comment

Follow service_db.nu:1-50 shape. Sections: purpose + two-binary breakdown, socket layout block, hero_proc hard-dep paragraph, hero_foundry soft-dep paragraph, env pass-through rationale (conditional inclusion, not empty-string), TOML-bug callout, standard Import as a module… usage block, --root(-r) paragraph, ./lib.nu closer. Do NOT carry over the "user's context comes from main.rs" paragraph from the in-repo module — that belongs to server internals.

Step 2: Imports

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

No forge.nu import (no in-source file lookup).

Step 3: Constants

const SVX_SERVICE_NAME = "hero_office"
const SVX_FORGE_LOC    = "lhumina_code/hero_office"
const SVX_BINARIES     = ["hero_office_server" "hero_office_ui"]
const SVX_ACTIONS      = ["hero_office_server" "hero_office_ui"]

Step 4: NEW helper svx_check_foundry [root: bool] (Deviation #3)

Structurally identical to service_books.nu::svx_check_embedder (lines 185-202). Check $(svc_sock_base $root)/hero_foundry/rpc.sock. When missing, emit the six-line warning:

  • ⚠ hero_foundry socket not found at ($foundry_sock)
  • hero_office_server will start in degraded mode — document read/write calls will fail.
  • Bring up hero_foundry when ready:
  • service_foundry install($flag)
  • service_foundry start($flag)
  • hero_office does not require a restart after hero_foundry comes up —
  • the server retries on the next RPC call.

Non-fatal: returns normally in both branches.

Step 5: svx_server_action [root: bool]

Follow service_aibroker.nu::svx_server_action shape for the conditional-env pattern. KEY DIFFERENCE FROM AIBROKER: OnlyOffice's config.rs treats unset env vars as "use default"; passing empty strings breaks the defaults. So instead of aibroker's "always pass, possibly empty" pattern, use upsert inside if is-not-empty guards:

mut env_rec = {RUST_LOG: "info,hero_office_server=debug"}
if ($onlyoffice_jwt | is-not-empty) { $env_rec = ($env_rec | upsert ONLYOFFICE_JWT_SECRET $onlyoffice_jwt) }
if ($oo_server_url  | is-not-empty) { $env_rec = ($env_rec | upsert OO_SERVER_URL        $oo_server_url) }
if ($connector_ext  | is-not-empty) { $env_rec = ($env_rec | upsert CONNECTOR_EXTERNAL_URL $connector_ext) }
if ($default_ctx    | is-not-empty) { $env_rec = ($env_rec | upsert DEFAULT_CONTEXT      $default_ctx) }
if ($hero_sock_dir  | is-not-empty) { $env_rec = ($env_rec | upsert HERO_SOCKET_DIR      $hero_sock_dir) }

Rest of the action record: script: $bin, retry/stop/kill_other/health values verbatim from the in-repo module (captured in the ground-truth block).

Step 6: svx_ui_action [root: bool]

Same conditional-env pattern as step 5. RUST_LOG base: info,hero_office_ui=debug; four optional vars (no ONLYOFFICE_JWT_SECRET). Retry max_attempts: 3; stop_timeout_ms: 5000; kill_other.socket is ui.sock; health interval 3000 / start_period 5000.

Step 7: svx_service_config []

context_name: "core"
service: {
    name: $SVX_SERVICE_NAME
    actions: $SVX_ACTIONS
    class: "system"
    critical: false
    description: "Hero Office — OnlyOffice document editor connector backed by hero_fossil"
    status: "start"
}

Step 8: svx_drop_registration [root: bool]

Identical to service_db.nu:171-177.

Step 9: install [--root(-r), --update(-u), --reset] (Deviations #1 + #2)

Copy service_db.nu:186-198 verbatim with constants swapped:

if $root { svc_require_sudo }
if $update { svc_update $SVX_FORGE_LOC }
if (not $reset) and (not $update) and (svc_bins_ok $SVX_BINARIES $root) {
    print $"→ ($SVX_SERVICE_NAME) binaries already in place — skipping build"
    return
}
svc_cargo_install $SVX_FORGE_LOC $SVX_BINARIES $root

Step 10: start [--reset, --root(-r), --update(-u)] (Deviation #4 placement)

Follow service_books.nu::start structure:

  1. if $root { svc_require_sudo }
  2. svc_require_proc "service_office" $root
  3. Early-exit idempotency check
  4. install --root=$root --update=$update --reset=$reset + post-install binary presence verification
  5. NEW svx_check_foundry $root (non-fatal)
  6. svx_drop_registration $root
  7. proc action set (svx_server_action $root) --root=$root | ignore
  8. proc action set (svx_ui_action $root) --root=$root | ignore
  9. proc service set (svx_service_config) --root=$root | ignore
  10. proc service start $SVX_SERVICE_NAME --root=$root | ignore
  11. sleep 1sec + rich summary block (service/actions/state/rpc sock/ui sock/ui url/commands), modelled on service_db.nu:269-293.

Step 11: stop [--root(-r)]

Copy service_db.nu::stop verbatim with name swap. Keep the svc_proc_healthy short-circuit.

Step 12: status [--root(-r)]

Copy service_db.nu::status.

Step 13: mod.nu

Add export use service_office.nu in line with the other service_* entries.

The four deviations from the in-repo scripts/nu_service.nu

  1. install grows a --reset flag (in-repo has only --root / --update).
  2. install gains the svc_bins_ok short-circuit.
  3. start gains svx_check_foundry preflight warning.
  4. start grows the ship-house summary block.

Smoke Test Plan (Hetzner, --root)

  1. Binaries short-circuit + reset rebuild — first install runs cargo; second install without flags prints → hero_office binaries already in place — skipping build; install --reset re-runs cargo.
  2. Degraded mode (hero_foundry absent) — svx_check_foundry emits the six-line warning, service still reaches running; curl --unix-socket rpc.sock /health → 200; curl --unix-socket rpc.sock /openrpc.json → OpenRPC doc; curl --unix-socket ui.sock / → 3xx to /admin/; curl -L --unix-socket ui.sock /admin/ → 200 HTML.
  3. Idempotent start — second start without --reset returns hero_office is already running.
  4. --reset reclaim path — while running, start --reset; all health probes pass again; no socket-bind errors.
  5. Full mode (with hero_foundry up) — no warning; same summary otherwise.
  6. Stop + post-stop status — stop clean; status surfaces hero_proc "not found"; /health → connection refused.
  7. Stop with hero_proc down — warn-and-return branch, exit 0.

Acceptance Criteria (from #97)

  • service_office.nu exports install, start, stop, status.
  • install accepts --root, --update, --reset; short-circuits via svc_bins_ok.
  • start is idempotent, calls svx_check_foundry between install-verify and drop-registration, prints the rich summary.
  • Both action records carry the correct RUST_LOG base + conditionally-included env vars.
  • mod.nu exports the new module.
  • Smoke tests 1–7 pass on Hetzner with --root.
  • hero_zero/services/hero_office.toml is NOT modified.
  • No new dependencies in lib.nu.
  • Output glyphs/prefixes match sibling modules (, , , ===).

Notes

  • Why we duplicate scripts/nu_service.nu: the in-repo module stays for anyone working inside hero_office/ directly; the central tools/modules/services/ copy is what mod.nu exports. Same decision taken for every other service_*.nu. The two must not drift beyond the four declared deviations without updating both in lockstep.
  • Why we ignore the hero_zero TOML: it has two known bugs — exec = "__HERO_BIN__/hero_office" references a nonexistent binary, and there is no [ui] block. hero_zero consumes that TOML at a different provisioning layer; the nushell lifecycle path bypasses it and writes actions/services directly via proc action set / proc service set. Fixing the TOML is a separate hero_zero cleanup, out of scope here — mirrors service_aibroker.nu's treatment of its mis-set source field.
  • Why env pass-through is conditional (not empty-string): OnlyOffice's config.rs treats unset env vars as "use default"; passing empty strings would break defaults (e.g. OO_SERVER_URL="" would 4xx every browser request). service_aibroker.nu can pass empty strings because hero_aibroker_lib::Config::load tolerates empty lists; no such tolerance here.
  • Why hero_foundry is a soft dep: server opens the foundry socket lazily at first document request. /health and /openrpc.json don't touch it. Hard-failing would block a legitimate "bring up office first, foundry later" scenario and force a restart cascade that the server itself does not need. The warning is operator awareness, not gating.
  • No CLI binary means SVX_BINARIES == SVX_ACTIONS. Keep them as separate constants for shape consistency with siblings; five bytes for readability.

Critical Files for Implementation

  • tools/modules/services/service_office.nu (new)
  • tools/modules/services/mod.nu (one-line append)
  • tools/modules/services/service_db.nu (install shape + summary template)
  • tools/modules/services/service_aibroker.nu (conditional-env reference)
  • tools/modules/services/service_books.nu (soft-dep warning shape)
  • tools/modules/services/lib.nu (svc_bins_ok etc.)
## Implementation Spec for Issue #97 ### Objective Add a Nushell lifecycle module that manages the `hero_office` service (OnlyOffice connector) through `hero_proc`, exactly the way `service_db.nu` / `service_whiteboard.nu` / `service_books.nu` / `service_aibroker.nu` manage theirs. The module supervises two binaries — `hero_office_server` and `hero_office_ui` — both of which bind Unix domain sockets only (no TCP ports). The `hero_office` repo already ships an in-tree `scripts/nu_service.nu` that is functionally equivalent; this module reproduces that behaviour inside the central `hero_skills` services index with four deviations listed in §4. ### Requirements - Two-binary service: `hero_office_server` + `hero_office_ui`, both registered under a single service `hero_office`. - Sockets (UDS only, no TCP): `$HERO_SOCKET_DIR/hero_office/rpc.sock` (server) and `$HERO_SOCKET_DIR/hero_office/ui.sock` (UI). - Build: plain `cargo build --release` at the virtual workspace root. `buildenv.sh` lists exactly `BINARIES="hero_office_server hero_office_ui"`. No CLI binary ships — `SVX_BINARIES == SVX_ACTIONS`. - Action `script` is the bare binary path for both (no subcommand — neither `main.rs` has a clap parser). - Env pass-through — forward the following vars **only when they are set** in the invoking environment (resolved at action-build time so `--root` flips paths correctly): - Server: `ONLYOFFICE_JWT_SECRET`, `OO_SERVER_URL`, `CONNECTOR_EXTERNAL_URL`, `DEFAULT_CONTEXT`, `HERO_SOCKET_DIR`. - UI: `OO_SERVER_URL`, `CONNECTOR_EXTERNAL_URL`, `DEFAULT_CONTEXT`, `HERO_SOCKET_DIR`. - `RUST_LOG` base: `info,hero_office_server=debug` (server) and `info,hero_office_ui=debug` (UI). Always set. - Retry / health / kill_other / stop-signal values are taken verbatim from the in-repo `scripts/nu_service.nu` — server max_attempts=5 / UI=3, 2s delay, backoff, 60s cap, 30s start+stability; server stop_timeout_ms=10000 / UI=5000; health interval 2s server / 3s UI, timeout 5s, retries 3, start_period 3s server / 5s UI. - Soft dependency on `hero_foundry`: `start` emits a warning when `$HERO_SOCKET_DIR/hero_foundry/rpc.sock` is absent but does NOT fail (server starts and health-checks clean without it; only document-read/write RPCs fail until foundry comes up; server retries on next RPC so no hero_office restart is needed). - Hard dependency on `hero_proc` as always — `svc_require_proc` path. - `install` carries the new canonical shape: `--root(-r)`, `--update(-u)`, `--reset`; short-circuit via `svc_bins_ok` when neither `--reset` nor `--update` and all binaries already in place; otherwise delegate to `svc_cargo_install`. - `start` idempotent; `stop` tolerates dead `hero_proc`. - Rich summary block at the tail of `start`: service / actions / state / `rpc sock` / `ui sock` / `ui url http+unix://…/` / commands hints. Ship-house style matching `service_db.nu` / `service_aibroker.nu`. ### Files to Modify / Create - `tools/modules/services/service_office.nu` — new module. - `tools/modules/services/mod.nu` — append `export use service_office.nu`. Nothing else is touched. In particular, `hero_zero/services/hero_office.toml` is NOT modified — see §7. ### Implementation Plan #### Step 1: Header comment Follow `service_db.nu:1-50` shape. Sections: purpose + two-binary breakdown, socket layout block, `hero_proc` hard-dep paragraph, `hero_foundry` soft-dep paragraph, env pass-through rationale (conditional inclusion, not empty-string), TOML-bug callout, standard `Import as a module…` usage block, `--root(-r)` paragraph, `./lib.nu` closer. Do NOT carry over the "user's context comes from main.rs" paragraph from the in-repo module — that belongs to server internals. #### Step 2: Imports ``` use ../clients/proc.nu * use ./lib.nu * ``` No `forge.nu` import (no in-source file lookup). #### Step 3: Constants ``` const SVX_SERVICE_NAME = "hero_office" const SVX_FORGE_LOC = "lhumina_code/hero_office" const SVX_BINARIES = ["hero_office_server" "hero_office_ui"] const SVX_ACTIONS = ["hero_office_server" "hero_office_ui"] ``` #### Step 4: NEW helper `svx_check_foundry [root: bool]` (Deviation #3) Structurally identical to `service_books.nu::svx_check_embedder` (lines 185-202). Check `$(svc_sock_base $root)/hero_foundry/rpc.sock`. When missing, emit the six-line warning: - `⚠ hero_foundry socket not found at ($foundry_sock)` - ` hero_office_server will start in degraded mode — document read/write calls will fail.` - ` Bring up hero_foundry when ready:` - ` service_foundry install($flag)` - ` service_foundry start($flag)` - ` hero_office does not require a restart after hero_foundry comes up —` - ` the server retries on the next RPC call.` Non-fatal: returns normally in both branches. #### Step 5: `svx_server_action [root: bool]` Follow `service_aibroker.nu::svx_server_action` shape for the conditional-env pattern. KEY DIFFERENCE FROM AIBROKER: OnlyOffice's `config.rs` treats unset env vars as "use default"; passing empty strings breaks the defaults. So instead of aibroker's "always pass, possibly empty" pattern, use `upsert` inside `if is-not-empty` guards: ``` mut env_rec = {RUST_LOG: "info,hero_office_server=debug"} if ($onlyoffice_jwt | is-not-empty) { $env_rec = ($env_rec | upsert ONLYOFFICE_JWT_SECRET $onlyoffice_jwt) } if ($oo_server_url | is-not-empty) { $env_rec = ($env_rec | upsert OO_SERVER_URL $oo_server_url) } if ($connector_ext | is-not-empty) { $env_rec = ($env_rec | upsert CONNECTOR_EXTERNAL_URL $connector_ext) } if ($default_ctx | is-not-empty) { $env_rec = ($env_rec | upsert DEFAULT_CONTEXT $default_ctx) } if ($hero_sock_dir | is-not-empty) { $env_rec = ($env_rec | upsert HERO_SOCKET_DIR $hero_sock_dir) } ``` Rest of the action record: `script: $bin`, retry/stop/kill_other/health values verbatim from the in-repo module (captured in the ground-truth block). #### Step 6: `svx_ui_action [root: bool]` Same conditional-env pattern as step 5. `RUST_LOG` base: `info,hero_office_ui=debug`; four optional vars (no `ONLYOFFICE_JWT_SECRET`). Retry `max_attempts: 3`; `stop_timeout_ms: 5000`; kill_other.socket is `ui.sock`; health interval 3000 / start_period 5000. #### Step 7: `svx_service_config []` ``` context_name: "core" service: { name: $SVX_SERVICE_NAME actions: $SVX_ACTIONS class: "system" critical: false description: "Hero Office — OnlyOffice document editor connector backed by hero_fossil" status: "start" } ``` #### Step 8: `svx_drop_registration [root: bool]` Identical to `service_db.nu:171-177`. #### Step 9: `install [--root(-r), --update(-u), --reset]` (Deviations #1 + #2) Copy `service_db.nu:186-198` verbatim with constants swapped: ``` if $root { svc_require_sudo } if $update { svc_update $SVX_FORGE_LOC } if (not $reset) and (not $update) and (svc_bins_ok $SVX_BINARIES $root) { print $"→ ($SVX_SERVICE_NAME) binaries already in place — skipping build" return } svc_cargo_install $SVX_FORGE_LOC $SVX_BINARIES $root ``` #### Step 10: `start [--reset, --root(-r), --update(-u)]` (Deviation #4 placement) Follow `service_books.nu::start` structure: 1. `if $root { svc_require_sudo }` 2. `svc_require_proc "service_office" $root` 3. Early-exit idempotency check 4. `install --root=$root --update=$update --reset=$reset` + post-install binary presence verification 5. **NEW** `svx_check_foundry $root` (non-fatal) 6. `svx_drop_registration $root` 7. `proc action set (svx_server_action $root) --root=$root | ignore` 8. `proc action set (svx_ui_action $root) --root=$root | ignore` 9. `proc service set (svx_service_config) --root=$root | ignore` 10. `proc service start $SVX_SERVICE_NAME --root=$root | ignore` 11. `sleep 1sec` + rich summary block (service/actions/state/rpc sock/ui sock/ui url/commands), modelled on `service_db.nu:269-293`. #### Step 11: `stop [--root(-r)]` Copy `service_db.nu::stop` verbatim with name swap. Keep the `svc_proc_healthy` short-circuit. #### Step 12: `status [--root(-r)]` Copy `service_db.nu::status`. #### Step 13: `mod.nu` Add `export use service_office.nu` in line with the other `service_*` entries. ### The four deviations from the in-repo `scripts/nu_service.nu` 1. `install` grows a `--reset` flag (in-repo has only `--root` / `--update`). 2. `install` gains the `svc_bins_ok` short-circuit. 3. `start` gains `svx_check_foundry` preflight warning. 4. `start` grows the ship-house summary block. ### Smoke Test Plan (Hetzner, `--root`) 1. **Binaries short-circuit + reset rebuild** — first `install` runs cargo; second `install` without flags prints `→ hero_office binaries already in place — skipping build`; `install --reset` re-runs cargo. 2. **Degraded mode** (hero_foundry absent) — `svx_check_foundry` emits the six-line warning, service still reaches running; `curl --unix-socket rpc.sock /health` → 200; `curl --unix-socket rpc.sock /openrpc.json` → OpenRPC doc; `curl --unix-socket ui.sock /` → 3xx to `/admin/`; `curl -L --unix-socket ui.sock /admin/` → 200 HTML. 3. **Idempotent start** — second `start` without `--reset` returns `hero_office is already running`. 4. **`--reset` reclaim path** — while running, `start --reset`; all health probes pass again; no socket-bind errors. 5. **Full mode** (with hero_foundry up) — no warning; same summary otherwise. 6. **Stop + post-stop status** — stop clean; status surfaces hero_proc "not found"; `/health` → connection refused. 7. **Stop with hero_proc down** — warn-and-return branch, exit 0. ### Acceptance Criteria (from #97) - [ ] `service_office.nu` exports `install`, `start`, `stop`, `status`. - [ ] `install` accepts `--root`, `--update`, `--reset`; short-circuits via `svc_bins_ok`. - [ ] `start` is idempotent, calls `svx_check_foundry` between install-verify and drop-registration, prints the rich summary. - [ ] Both action records carry the correct `RUST_LOG` base + conditionally-included env vars. - [ ] `mod.nu` exports the new module. - [ ] Smoke tests 1–7 pass on Hetzner with `--root`. - [ ] `hero_zero/services/hero_office.toml` is NOT modified. - [ ] No new dependencies in `lib.nu`. - [ ] Output glyphs/prefixes match sibling modules (`→`, `✓`, `⚠`, `===`). ### Notes - **Why we duplicate `scripts/nu_service.nu`**: the in-repo module stays for anyone working inside `hero_office/` directly; the central `tools/modules/services/` copy is what `mod.nu` exports. Same decision taken for every other `service_*.nu`. The two must not drift beyond the four declared deviations without updating both in lockstep. - **Why we ignore the hero_zero TOML**: it has two known bugs — `exec = "__HERO_BIN__/hero_office"` references a nonexistent binary, and there is no `[ui]` block. `hero_zero` consumes that TOML at a different provisioning layer; the nushell lifecycle path bypasses it and writes actions/services directly via `proc action set` / `proc service set`. Fixing the TOML is a separate hero_zero cleanup, out of scope here — mirrors `service_aibroker.nu`'s treatment of its mis-set `source` field. - **Why env pass-through is conditional (not empty-string)**: OnlyOffice's `config.rs` treats unset env vars as "use default"; passing empty strings would break defaults (e.g. `OO_SERVER_URL=""` would 4xx every browser request). `service_aibroker.nu` can pass empty strings because `hero_aibroker_lib::Config::load` tolerates empty lists; no such tolerance here. - **Why `hero_foundry` is a soft dep**: server opens the foundry socket lazily at first document request. `/health` and `/openrpc.json` don't touch it. Hard-failing would block a legitimate "bring up office first, foundry later" scenario and force a restart cascade that the server itself does not need. The warning is operator awareness, not gating. - **No CLI binary** means `SVX_BINARIES == SVX_ACTIONS`. Keep them as separate constants for shape consistency with siblings; five bytes for readability. ### Critical Files for Implementation - `tools/modules/services/service_office.nu` (new) - `tools/modules/services/mod.nu` (one-line append) - `tools/modules/services/service_db.nu` (install shape + summary template) - `tools/modules/services/service_aibroker.nu` (conditional-env reference) - `tools/modules/services/service_books.nu` (soft-dep warning shape) - `tools/modules/services/lib.nu` (`svc_bins_ok` etc.)
mahmoud self-assigned this 2026-04-21 06:20:28 +00:00
mahmoud added this to the ACTIVE project 2026-04-21 06:20:29 +00:00
mahmoud added this to the now milestone 2026-04-21 06:20:31 +00:00
Author
Owner

Implementation summary

Changes

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

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

Smoke ran with hero_foundry absent — the degraded-mode scenario, which is the more demanding branch because it exercises the non-fatal preflight AND the lazy-open server path.

# Assertion Result
1a service_office status --root with hero_proc down → actionable error pointing to service_proc start --root PASS
1b service_office stop --root with hero_proc down → benign warning, no error PASS
2a service_proc start --root healthy PASS
2b service_office install --root (first) produced 2 binaries in /root/hero/bin/ via cargo build PASS
2c Second install with no flags → → hero_office binaries already in place — skipping build (svc_bins_ok short-circuit) PASS
2d service_office start --reset --root with foundry absent → 6-line soft-dep warning fires, service still reaches running PASS
2e rpc.sock present PASS
2f ui.sock present PASS
2g curl --unix-socket rpc.sock /health → HTTP 200, {"status":"healthy","version":"0.1.0"} PASS
2h curl --unix-socket rpc.sock /openrpc.json → HTTP 200, OpenRPC 1.3.2, 6 methods PASS
2i curl --unix-socket ui.sock / → HTTP 307 redirect to /admin/ PASS
2j curl --unix-socket ui.sock /admin/ → HTTP 200, Hero Office Admin HTML PASS
2k status{name: hero_office, state: running, restarts: 0, pid: 1061844, current_run_id: 20} PASS
2l Idempotent start (no --reset) prints "already running" with hint PASS
2m 15 s observation — current_run_id stable at 20, restarts: 0, state running (3 samples) PASS
2n start --reset --root while running — both sockets reclaimed; rest.sock /health returns HTTP 200 after restart PASS
2o service_office stop --root✓ hero_office stopped and unregistered PASS
2p Post-stop status returns expected service 'hero_office' not found PASS
2q Post-stop: no hero_office_server/hero_office_ui processes; socket directory empty PASS

Notes

  • Soft-dep warning path fully validated: foundry absent → 6-line warning fires → service still registers + reaches running + both health checks pass (rpc.sock /health HTTP 200, ui.sock /admin/ HTTP 200). This is exactly the branch the in-repo scripts/nu_service.nu did not cover.
  • svc_bins_ok short-circuit validated: second install returns in <1 s with the canonical one-line skip message; start --reset still triggers a rebuild pass via the inner install --reset=$reset call.
  • Env pass-through rationale confirmed in practice: the test env has none of ONLYOFFICE_JWT_SECRET / OO_SERVER_URL / CONNECTOR_EXTERNAL_URL set, so the action env contains only RUST_LOG — server logs show it resolving defaults cleanly, no OO-related JS errors in the /admin/ page.
  • No leftover state after stop: empty socket dir, no orphaned processes — unlike hero_whiteboard, hero_office binaries do clean up their sockets on SIGTERM.
  • TOML bug (hero_zero/services/hero_office.toml referencing a non-existent hero_office CLI, no [ui] block) remains — out of scope per spec, same treatment as aibroker's mis-set source field.

Acceptance criteria (from #97)

  • service_office.nu exports install, start, stop, status.
  • install accepts --root, --update, --reset; short-circuits via svc_bins_ok.
  • start is idempotent, calls svx_check_foundry between install-verify and drop-registration, prints the rich summary.
  • Both action records carry correct RUST_LOG base + conditionally-included env vars.
  • mod.nu exports the new module.
  • All smoke-test scenarios pass on Hetzner with --root.
  • hero_zero/services/hero_office.toml is NOT modified.
  • No new dependencies in lib.nu.
  • Output glyphs/prefixes match sibling modules.
## Implementation summary ### Changes - Added `tools/modules/services/service_office.nu` — ~400 lines. - Updated `tools/modules/services/mod.nu` — appended `export use service_office.nu`. ### End-to-end smoke test on Hetzner (`--root`) Smoke ran with `hero_foundry` **absent** — the degraded-mode scenario, which is the more demanding branch because it exercises the non-fatal preflight AND the lazy-open server path. | # | Assertion | Result | |---|---|---| | 1a | `service_office status --root` with hero_proc down → actionable error pointing to `service_proc start --root` | PASS | | 1b | `service_office stop --root` with hero_proc down → benign warning, no error | PASS | | 2a | `service_proc start --root` healthy | PASS | | 2b | `service_office install --root` (first) produced 2 binaries in `/root/hero/bin/` via cargo build | PASS | | 2c | Second `install` with no flags → `→ hero_office binaries already in place — skipping build` (`svc_bins_ok` short-circuit) | PASS | | 2d | `service_office start --reset --root` with foundry absent → 6-line soft-dep warning fires, service still reaches running | PASS | | 2e | `rpc.sock` present | PASS | | 2f | `ui.sock` present | PASS | | 2g | `curl --unix-socket rpc.sock /health` → HTTP 200, `{"status":"healthy","version":"0.1.0"}` | PASS | | 2h | `curl --unix-socket rpc.sock /openrpc.json` → HTTP 200, OpenRPC 1.3.2, 6 methods | PASS | | 2i | `curl --unix-socket ui.sock /` → HTTP 307 redirect to `/admin/` | PASS | | 2j | `curl --unix-socket ui.sock /admin/` → HTTP 200, Hero Office Admin HTML | PASS | | 2k | `status` → `{name: hero_office, state: running, restarts: 0, pid: 1061844, current_run_id: 20}` | PASS | | 2l | Idempotent `start` (no `--reset`) prints "already running" with hint | PASS | | 2m | 15 s observation — `current_run_id` stable at 20, `restarts: 0`, state `running` (3 samples) | PASS | | 2n | `start --reset --root` while running — both sockets reclaimed; `rest.sock /health` returns HTTP 200 after restart | PASS | | 2o | `service_office stop --root` → `✓ hero_office stopped and unregistered` | PASS | | 2p | Post-stop `status` returns expected `service 'hero_office' not found` | PASS | | 2q | Post-stop: no `hero_office_server`/`hero_office_ui` processes; socket directory empty | PASS | ### Notes - **Soft-dep warning path fully validated**: foundry absent → 6-line warning fires → service still registers + reaches `running` + both health checks pass (rpc.sock `/health` HTTP 200, ui.sock `/admin/` HTTP 200). This is exactly the branch the in-repo `scripts/nu_service.nu` did not cover. - **`svc_bins_ok` short-circuit validated**: second `install` returns in <1 s with the canonical one-line skip message; `start --reset` still triggers a rebuild pass via the inner `install --reset=$reset` call. - **Env pass-through rationale confirmed in practice**: the test env has none of `ONLYOFFICE_JWT_SECRET` / `OO_SERVER_URL` / `CONNECTOR_EXTERNAL_URL` set, so the action env contains only `RUST_LOG` — server logs show it resolving defaults cleanly, no OO-related JS errors in the /admin/ page. - **No leftover state** after stop: empty socket dir, no orphaned processes — unlike `hero_whiteboard`, `hero_office` binaries do clean up their sockets on SIGTERM. - **TOML bug** (`hero_zero/services/hero_office.toml` referencing a non-existent `hero_office` CLI, no `[ui]` block) remains — out of scope per spec, same treatment as aibroker's mis-set `source` field. ### Acceptance criteria (from #97) - [x] `service_office.nu` exports `install`, `start`, `stop`, `status`. - [x] `install` accepts `--root`, `--update`, `--reset`; short-circuits via `svc_bins_ok`. - [x] `start` is idempotent, calls `svx_check_foundry` between install-verify and drop-registration, prints the rich summary. - [x] Both action records carry correct `RUST_LOG` base + conditionally-included env vars. - [x] `mod.nu` exports the new module. - [x] All smoke-test scenarios pass on Hetzner with `--root`. - [x] `hero_zero/services/hero_office.toml` is NOT modified. - [x] No new dependencies in `lib.nu`. - [x] Output glyphs/prefixes match sibling modules.
Author
Owner

PR opened: #98

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