Migrate OSIS services off OServer onto hero_rpc2 trait/dispatch — close the META hybrid loop #90

Closed
opened 2026-05-20 09:40:41 +00:00 by timur · 6 comments
Owner

Context

The parent META (hero_skills#262) locked the hybrid hero_rpc + hero_rpc2 decision back in May:

OSchema codegen produces Rust traits annotated with hero_rpc2's #[rpc(server, client)] macros. hero_rpc2 owns wire protocol, UDS transports, and the auto-generated Rust client.

The codegen rails landed:

  • #55 §2 parts 1–3 — hero_rpc2 vendored, HeroRequestContext + header-lift wired.
  • #60 — OSchema → #[method(name=…)] trait methods + Python dataclass methods. recipe_sdk_rpc2 round-trips a Python system.ping over UDS end-to-end.

But every OSIS service in the ecosystem still runs on the older OServer dispatch path, not on hero_rpc2. The hero_logic#44 agent explicitly scoped this out: "DO NOT migrate to hero_rpc2 in this PR — that's a separate later step." This is the separate later step.

Why this matters now

Downstream symptoms surface as separate issues that all share this root cause:

  • #80system.ping smoke test fails on OServer because OServer doesn't register system.* builtins. hero_rpc2 does, automatically. Supplanted by this issue.
  • Generator currently maintains two parallel rust_rpc emitters (rust_rpc.rs legacy → OServer, rust_rpc2.rs → hero_rpc2). Until we cut over, both have to be kept correct in lockstep.
  • The recipe_sdk_rpc2 example exists in parallel to recipe_server for the same reason — proving hero_rpc2 works while OServer remains canonical.
  • Each new framework feature (typed errors #83, context routing) has to be threaded through both layers.

Affected services (OSIS-backed, currently OServer)

Service Repo Notes
hero_service lhumina_code/hero_service Template repo — migrate first; it sets the precedent.
hero_logic lhumina_code/hero_logic Just finished the template-pattern upgrade (#44) — next natural step.
hero_compute lhumina_code/hero_compute
hero_db (lhumina_code/hero_db)
Others using hero_rpc_osis rpc feature various grep hero_rpc_osis.*features.*rpc across the org.

hero_proc runs its own JSON-RPC stack (not OServer), so it's outside this scope — separate decision whether it also adopts hero_rpc2.

Migration shape

Proposed phasing (open for discussion in this thread before implementation):

Phase 1 — framework path (hero_rpc)

  • Audit what OServer provides that hero_rpc2::ServerBuilder doesn't (context routing, OSIS-CRUD auto-dispatch, claims layer, hero_router integration). For each gap, either land it in hero_rpc2 or document the gap as a constraint of the new path.
  • Decide OServer's fate: deprecate (keep working through one release), or hard-cut. Recommend deprecate-then-remove so downstream services migrate at their own pace.
  • Generator: have the scaffolder default to the hero_rpc2 emitter (rust_rpc2.rs); legacy rust_rpc.rs emitter stays callable behind a flag until services migrate, then gets deleted.

Phase 2 — first migration (hero_service template)

  • Migrate hero_service to hero_rpc2 dispatch. This validates the framework changes and updates the canonical reference everyone clones.
  • Update hero_service_scaffold.md skill so new scaffolds get hero_rpc2 by default.

Phase 3 — sweep remaining OSIS services

  • One PR per repo: hero_logic, hero_compute, hero_db, …
  • Each migration follows the same template (HeroLifecycle + ServerBuilder pattern from hero_service). Likely possible to mostly automate.

Phase 4 — drop legacy

  • Once no service depends on OServer, delete it from hero_rpc/crates/server/.
  • Delete the legacy rust_rpc.rs emitter.
  • Delete the recipe_sdk_rpc2 parallel example — recipe_server becomes the canonical demo on the new path.

What this closes / supersedes

  • #80system.ping builtin missing. Resolved automatically once services move to hero_rpc2 (which registers system.* natively).
  • Implicitly: the parallel rust_rpc.rs / rust_rpc2.rs emitters; the recipe_server / recipe_sdk_rpc2 split.

Acceptance

  • Every OSIS service in lhumina_code/ that's not hero_proc runs on hero_rpc2 dispatch.
  • OServer is deleted from hero_rpc/crates/server/.
  • Legacy rust_rpc.rs emitter is deleted; the scaffolder only emits the hero_rpc2 trait.
  • recipe_sdk_rpc2 parallel example is consolidated into recipe_server (single demo on the new path).
  • Every service's smoke test passes on the §7 contract including system.ping.
  • hero_service_scaffold.md describes only the hero_rpc2 path.

Out of scope

  • hero_proc migration (different RPC stack; separate decision).
  • hero_rpc2 itself growing new framework features beyond what migration requires — those are their own follow-ups.
## Context The parent META ([hero_skills#262](https://forge.ourworld.tf/lhumina_code/hero_skills/issues/262)) locked the **hybrid hero_rpc + hero_rpc2** decision back in May: > *OSchema codegen produces Rust traits annotated with hero_rpc2's `#[rpc(server, client)]` macros. **hero_rpc2** owns wire protocol, UDS transports, and the auto-generated Rust client.* The codegen rails landed: - [#55 §2 parts 1–3](https://forge.ourworld.tf/lhumina_code/hero_rpc/issues/55) — hero_rpc2 vendored, HeroRequestContext + header-lift wired. - [#60](https://forge.ourworld.tf/lhumina_code/hero_rpc/issues/60) — OSchema → `#[method(name=…)]` trait methods + Python dataclass methods. `recipe_sdk_rpc2` round-trips a Python `system.ping` over UDS end-to-end. But **every OSIS service in the ecosystem still runs on the older `OServer` dispatch path**, not on hero_rpc2. The hero_logic#44 agent explicitly scoped this out: *"DO NOT migrate to hero_rpc2 in this PR — that's a separate later step."* This is the separate later step. ## Why this matters now Downstream symptoms surface as separate issues that all share this root cause: - [#80](https://forge.ourworld.tf/lhumina_code/hero_rpc/issues/80) — `system.ping` smoke test fails on OServer because OServer doesn't register `system.*` builtins. hero_rpc2 does, automatically. Supplanted by this issue. - Generator currently maintains two parallel rust_rpc emitters (`rust_rpc.rs` legacy → OServer, `rust_rpc2.rs` → hero_rpc2). Until we cut over, both have to be kept correct in lockstep. - The `recipe_sdk_rpc2` example exists in parallel to `recipe_server` for the same reason — proving hero_rpc2 works while OServer remains canonical. - Each new framework feature (typed errors #83, context routing) has to be threaded through both layers. ## Affected services (OSIS-backed, currently OServer) | Service | Repo | Notes | |---|---|---| | `hero_service` | [lhumina_code/hero_service](https://forge.ourworld.tf/lhumina_code/hero_service) | Template repo — migrate first; it sets the precedent. | | `hero_logic` | [lhumina_code/hero_logic](https://forge.ourworld.tf/lhumina_code/hero_logic) | Just finished the template-pattern upgrade ([#44](https://forge.ourworld.tf/lhumina_code/hero_logic/issues/44)) — next natural step. | | `hero_compute` | [lhumina_code/hero_compute](https://forge.ourworld.tf/lhumina_code/hero_compute) | | | `hero_db` | (lhumina_code/hero_db) | | | Others using `hero_rpc_osis` `rpc` feature | various | grep `hero_rpc_osis.*features.*rpc` across the org. | `hero_proc` runs its own JSON-RPC stack (not OServer), so it's outside this scope — separate decision whether it also adopts hero_rpc2. ## Migration shape Proposed phasing (open for discussion in this thread before implementation): ### Phase 1 — framework path (hero_rpc) - Audit what OServer provides that `hero_rpc2::ServerBuilder` doesn't (context routing, OSIS-CRUD auto-dispatch, claims layer, hero_router integration). For each gap, either land it in hero_rpc2 or document the gap as a constraint of the new path. - Decide OServer's fate: deprecate (keep working through one release), or hard-cut. Recommend deprecate-then-remove so downstream services migrate at their own pace. - Generator: have the scaffolder default to the hero_rpc2 emitter (`rust_rpc2.rs`); legacy `rust_rpc.rs` emitter stays callable behind a flag until services migrate, then gets deleted. ### Phase 2 — first migration (`hero_service` template) - Migrate `hero_service` to hero_rpc2 dispatch. This validates the framework changes and updates the canonical reference everyone clones. - Update `hero_service_scaffold.md` skill so new scaffolds get hero_rpc2 by default. ### Phase 3 — sweep remaining OSIS services - One PR per repo: hero_logic, hero_compute, hero_db, … - Each migration follows the same template (`HeroLifecycle + ServerBuilder` pattern from hero_service). Likely possible to mostly automate. ### Phase 4 — drop legacy - Once no service depends on OServer, delete it from `hero_rpc/crates/server/`. - Delete the legacy `rust_rpc.rs` emitter. - Delete the `recipe_sdk_rpc2` parallel example — `recipe_server` becomes the canonical demo on the new path. ## What this closes / supersedes - [#80](https://forge.ourworld.tf/lhumina_code/hero_rpc/issues/80) — `system.ping` builtin missing. Resolved automatically once services move to hero_rpc2 (which registers `system.*` natively). - Implicitly: the parallel `rust_rpc.rs` / `rust_rpc2.rs` emitters; the `recipe_server` / `recipe_sdk_rpc2` split. ## Acceptance - Every OSIS service in `lhumina_code/` that's not `hero_proc` runs on hero_rpc2 dispatch. - OServer is deleted from `hero_rpc/crates/server/`. - Legacy `rust_rpc.rs` emitter is deleted; the scaffolder only emits the hero_rpc2 trait. - `recipe_sdk_rpc2` parallel example is consolidated into `recipe_server` (single demo on the new path). - Every service's smoke test passes on the §7 contract including `system.ping`. - `hero_service_scaffold.md` describes only the hero_rpc2 path. ## Out of scope - `hero_proc` migration (different RPC stack; separate decision). - `hero_rpc2` itself growing new framework features beyond what migration requires — those are their own follow-ups. ## Related - Parent META: [hero_skills#262](https://forge.ourworld.tf/lhumina_code/hero_skills/issues/262) - Codegen rails: [#55](https://forge.ourworld.tf/lhumina_code/hero_rpc/issues/55), [#60](https://forge.ourworld.tf/lhumina_code/hero_rpc/issues/60) - Supersedes: [#80](https://forge.ourworld.tf/lhumina_code/hero_rpc/issues/80) - Last template-pattern migration that explicitly deferred this: [hero_logic#44](https://forge.ourworld.tf/lhumina_code/hero_logic/issues/44)
Author
Owner

Phase 1 design proposal — DESIGN gate (no code yet)

Read parent META hero_skills#262 + decision threads #55 / #60. Worktree set up at /tmp/hero_rpc_90 on branch issue-90-phase1-framework off origin/development. No code pushed yet — three answers below need sign-off first.


(1) Feature-parity audit — OServer vs hero_rpc2::ServerBuilder

Surveyed crates/server/src/{lib.rs,server/*.rs,acl/} (3,362 LoC) against crates/hero_rpc2/src/{lib.rs,server.rs,context.rs,transport/*}. Grouped by capability:

A. Wire / transport / context — hero_rpc2 already covers

OServer feature Status
Single rpc.sock per service, JSON-RPC 2.0 over HTTP-over-UDS hero_rpc2 has it via serve_http
X-Hero-Context header → typed routing hero_rpc2 has it via with_lifted_headers + HeroRequestContext + task_local! (landed in #55 §2 part 2)
X-Hero-Claims header → claims vec hero_rpc2 has it in the same HeroRequestContext
rpc.discover / rpc.health builtins hero_rpc2 has rpc.discover via with_discover; gap: no rpc.health builtin (this is exactly the symptom in #80)
Typed JSON-RPC error mapping (Invalid / NotFound / PermissionDenied / Internal — #83) hero_rpc2 has its own RpcError enum (codegen wiring of OSchema typed errors is #83 follow-up territory — not Phase 1)

B. Codegen — needs one targeted extension in Phase 1

OServer feature Status
OSIS-CRUD auto-dispatch: Type.set/get/find/delete/exists routed from OsisAppRpcHandler::type_names() without the service declaring those methods Gap — close in Phase 1. Today emit/rust_rpc2.rs only walks service Foo { … } blocks. To match OServer's behaviour for rootobjects, the emitter must additionally emit #[method(name = "<type>.get/set/delete/find/exists")] entries into the same trait for every rootobject declared in the schema. This keeps "CRUD-for-free" working under hero_rpc2 dispatch with no per-service hand-coding.
Per-domain OpenRPC spec emission (A::openrpc_spec()) Already emitted by OSchema codegen separately (crates/generator/src/build/openrpc/…); not a hero_rpc2 concern

C. Multi-domain orchestration — lives elsewhere, not a hero_rpc2 concern

These are OSIS-runtime concerns, not generic JSON-RPC framework. META locks hero_rpc2 to "wire protocol, UDS transports, and the auto-generated Rust client" only.

OServer feature Where it belongs after Phase 1
Context registry (ContextRegistry, registry.toml at ~/hero/var/osisdb/.core/) OSIS-runtime helper. Stays in hero_rpc_osis (or a small new module). NOT moving into hero_rpc2.
context.list / context.add / context.remove / context.import / context.export / domain.list management RPC methods Same — OSIS-runtime helper that exposes them as a jsonrpsee::RpcModule to merge with the user's trait module
seed_from_dir (TOML-tree seeding) Same — OSIS-runtime helper
git_sync::context_import / context_export (subprocess rsync + git) Same — OSIS-runtime helper
Per-context storage directory layout (~/hero/var/osisdb/{context}/{domain}/) Same — handled by domain create(db_path, user_id) callers (already application-level)
register::<A>(ctx, domain) orchestration (storage init + OsisDomainInit::create + dispatch wiring) Same — OSIS-runtime helper builds a RpcModule per registered domain and RpcModule::merges them
Combined OpenRPC document (per-domain methods merged + management methods appended) OSIS-runtime helper produces the merged document and feeds it to ServerBuilder::with_discover(…)

Phase 1 does not introduce that helper. Phase 2 (hero_service template migration) introduces it as a concrete helper, validates the pattern, then Phase 3 sweeps it across the four other repos.

D. Mandatory HTTP endpoints — mostly in hero_lifecycle; one composition gap

OServer feature Status
/health HTTP endpoint Already covered by hero_lifecycle::HeroRpcServer::mandatory_router()
/openrpc.json HTTP endpoint Same
/.well-known/heroservice.json HTTP endpoint (hero_router discovery) Same
Mounting JSON-RPC + well-known + optional SSE extension on the same rpc.sock (OServer's run_with_extension(Router)) Gap — close in Phase 1. hero_rpc2's serve_http is a private hyper service that only accepts POST /. Phase 2 services need to compose the rpc handler with axum's well-known endpoints + optional SSE on one UDS. Cleanest fix: expose the jsonrpsee RpcModule as an axum-mountable handler (hero_rpc2::transport::http::axum_handler(module, max_body, lifted_headers) -> axum::Router). Then HeroRpcServer (which already serves an axum Router over UDS) just merges that handler at POST /rpc and we have the same shape OServer offers.

E. CLI / lifecycle — lives in hero_lifecycle, no gap

OServer feature Status
--info JSON descriptor herolib_core::base::handle_info_flag(SERVICE_TOML) covers it (every service main.rs already calls it before run_cli)
--start / --stop / bare-foreground dispatch hero_lifecycle::HeroLifecycle::{start,stop} covers it
--contexts foo,bar flag (multi-context start) OServer-CLI-specific. Phase 2's hero_service template migration moves it onto the OSIS-runtime helper's CLI. Out of Phase 1 scope.
--seed-dir / --seed-domains flags Same

F. Genuine dead code — delete in Phase 4

OServer module Status
crates/server/src/acl/ (824 LoC: Acl / Group / Ace / Right) No external callers. Grep across the workspace finds zero use hero_rpc_server::acl::… outside the crate's own docs. Delete with OServer in Phase 4 (not in Phase 1 — keep the deprecation surface intact for downstream services).
inspector_ui feature Optional HTML debug UI on the same socket. Niche; downstream services can ship their own admin UI via hero_<name>_admin. Delete with OServer in Phase 4.

Phase 1 gap summary (what actually lands inside hero_rpc2/ this PR)

  1. emit/rust_rpc2.rs: extend trait emission to auto-emit <type>.{get,set,delete,find,exists} #[method] entries for every rootobject in the schema (closes B auto-CRUD).
  2. hero_rpc2::transport::http: expose axum_handler(module, max_body, lifted_headers) -> axum::Router so services can compose rpc + well-known + SSE on one UDS via HeroRpcServer (closes D extension merging).
  3. hero_rpc2::ServerBuilder: register a default rpc.health builtin (matches OServer's contract; closes the failure surfaced by #80).

Items C and E are explicitly not Phase 1 work — they land in Phase 2's hero_service template migration as a fresh OsisServer* helper that wraps ServerBuilder.


(2) OServer fate — recommend deprecate-then-remove

Recommendation: deprecate now, schedule removal for Phase 4.

Reasoning:

  • 5 known downstream consumers on OServer (hero_service, hero_logic, hero_compute, hero_db, plus example/recipe_server). Hard-cut means flag-day migration across 5 repos in one PR landed window.
  • The umbrella plan (#90 → Phase 3) explicitly calls for one-PR-per-repo migration in parallel. Deprecation-with-grace-period is what makes that plan possible without lockstep coordination.
  • Cost of keeping OServer compiling through one release: zero new code — it's frozen. The cost is mostly the bytes in the workspace.
  • Cost of hard-cut: every consumer breaks the moment this PR squash-merges; downstream agents have to drop other work to migrate.

Concrete deprecation steps in this PR (Phase 1):

  • #[deprecated(since = "<this-version>", note = "Use hero_rpc2::ServerBuilder + the OsisServer helper landing in Phase 2 (#90). OServer is removed in Phase 4.")] on the OServer struct, OServerConfig, UnifiedServerBuilder, and ServerCli.
  • Top-of-file deprecation header in crates/server/src/lib.rs and crates/server/README.md linking to #90 with the removal timeline.
  • Workspace Cargo.toml does NOT drop the hero_rpc_server member yet (existing services still depend on it).
  • CI: no new #[deny(deprecated)] — let the warnings surface naturally in downstream service builds so each consuming repo notices on their next cargo build and can schedule their Phase 3 PR.

Phase 4 then deletes crates/server/, the hero_rpc_server workspace member, and the legacy emitter path.


(3) Scaffolder cutover — default to hero_rpc2, keep legacy behind an opt-in

The current scaffolder path (crates/generator/src/build/scaffold.rs::generate_server_main_rs) emits a main.rs that uses OServer::run_cli(...). There is no separate rust_rpc.rs emitter file — the legacy/new split lives in the scaffolded main.rs template, with the trait/SDK split living in two emit/ modules (rust_rpc2.rs for the new hero_rpc2 trait, rust_sdk.rs for the legacy OServer-targeted client). Restating the user's framing in these accurate terms:

Current state (committed on development):

  • emit/rust_rpc2.rs → emits sdk/rust/src/<domain>.rs containing the hero_rpc2 trait. Opt-in via OschemaBuildConfig::with_hero_rpc2_sdk(). Default is off.
  • emit/rust_sdk.rs → emits the legacy typed-client SDK targeting the OServer wire shape. Default is on.
  • scaffold.rs::generate_server_main_rs → emits a main.rs calling OServer::run_cli. Hard-coded, no flag.

Phase 1 cutover plan:

  1. Flip the scaffolder's emitted main.rs to hero_rpc2 by default. The new template uses:

    • hero_rpc2::ServerBuilder for transport,
    • hero_lifecycle::HeroRpcServer for the well-known endpoints + lifecycle CLI,
    • the trait impls from the generated sdk/rust/src/<domain>.rs (which the scaffolded build.rs now opts into via with_hero_rpc2_sdk()).
      The new template ships an inline TODO comment pointing each service author at the (Phase 2) OsisServer helper for context-registry + seed wiring.
  2. Flip the OschemaBuildConfig defaults so the scaffolded build.rs produces the rpc2 trait by default and also produces the legacy SDK while existing services still need it (so the build pipeline stays compatible during the transition window):

    • generate_rpc2_sdk: true (was false)
    • generate_rust_sdk: true unchanged (legacy still emitted for existing services)
    • new opt-out with_legacy_disabled() for new scaffolds that want a clean hero_rpc2-only crate
  3. Legacy main.rs template stays callable behind an explicit flag for the brief window while existing services migrate: WorkspaceScaffolder::with_legacy_oserver_main(). Default = off. Phase 4 deletes this flag along with OServer.

  4. example/recipe_server keeps building unchanged through Phase 1 — its existing main.rs predates the flag flip and uses OServer; the deprecation warnings will surface but compile stays clean. Phase 2 migrates hero_service first (validating the new template), Phase 3 sweeps recipe_server + the four real services, Phase 4 deletes the legacy emitter.

  5. example/recipe_sdk_rpc2 stays as the hero_rpc2 reference until Phase 4 consolidates it into recipe_server. No change in Phase 1.


Acceptance gate (re-stated from #90 body)

For Phase 1 only, this PR will land with:

  • Three hero_rpc2 gaps closed: rpc2-emitter auto-CRUD for rootobjects, axum_handler integration, rpc.health builtin.
  • OServer deprecated with #[deprecated] attrs + README note linking to #90 with the Phase 4 removal timeline.
  • Scaffolder default flipped (new scaffolds emit a hero_rpc2-based main.rs); legacy main.rs template behind with_legacy_oserver_main().
  • cargo build --workspace + cargo test --workspace clean on hero_rpc.
  • example/recipe_server + example/recipe_sdk_rpc2 + every existing OSIS-backed service in the org continues to build (deprecation warnings only; no errors).

Phases 2 / 3 / 4 stay future PRs — they need this framework path in place first.


Waiting on sign-off

Before any code, three explicit yes/no asks:

  1. Audit shape — does the (1) gap map match your read? Specifically: is "OSIS-runtime context registry + git_sync + seed + management methods lives in hero_rpc_osis, NOT in hero_rpc2" the right split, given META's "hero_rpc2 owns wire/transport/Rust-client" line?
  2. OServer fate — deprecate-then-remove (one release grace) good, or do you want a harder cut?
  3. Scaffolder cutover — flip default + keep legacy main.rs template behind with_legacy_oserver_main() flag good, or do you want a different opt-in name / different default for generate_rust_sdk?

Once those three are signed off I push the implementation in one squash-mergeable PR.

## Phase 1 design proposal — DESIGN gate (no code yet) Read parent [META hero_skills#262](https://forge.ourworld.tf/lhumina_code/hero_skills/issues/262) + decision threads [#55](https://forge.ourworld.tf/lhumina_code/hero_rpc/issues/55) / [#60](https://forge.ourworld.tf/lhumina_code/hero_rpc/issues/60). Worktree set up at `/tmp/hero_rpc_90` on branch `issue-90-phase1-framework` off `origin/development`. **No code pushed yet** — three answers below need sign-off first. --- ### (1) Feature-parity audit — OServer vs `hero_rpc2::ServerBuilder` Surveyed `crates/server/src/{lib.rs,server/*.rs,acl/}` (3,362 LoC) against `crates/hero_rpc2/src/{lib.rs,server.rs,context.rs,transport/*}`. Grouped by capability: #### A. Wire / transport / context — **hero_rpc2 already covers** | OServer feature | Status | |---|---| | Single `rpc.sock` per service, JSON-RPC 2.0 over HTTP-over-UDS | hero_rpc2 has it via `serve_http` | | `X-Hero-Context` header → typed routing | hero_rpc2 has it via `with_lifted_headers` + `HeroRequestContext` + `task_local!` (landed in [#55 §2 part 2](https://forge.ourworld.tf/lhumina_code/hero_rpc/issues/55)) | | `X-Hero-Claims` header → claims vec | hero_rpc2 has it in the same `HeroRequestContext` | | `rpc.discover` / `rpc.health` builtins | hero_rpc2 has `rpc.discover` via `with_discover`; **gap:** no `rpc.health` builtin (this is exactly the symptom in [#80](https://forge.ourworld.tf/lhumina_code/hero_rpc/issues/80)) | | Typed JSON-RPC error mapping (Invalid / NotFound / PermissionDenied / Internal — #83) | hero_rpc2 has its own `RpcError` enum (codegen wiring of OSchema typed errors is #83 follow-up territory — **not Phase 1**) | #### B. Codegen — needs **one targeted extension** in Phase 1 | OServer feature | Status | |---|---| | OSIS-CRUD auto-dispatch: `Type.set/get/find/delete/exists` routed from `OsisAppRpcHandler::type_names()` without the service declaring those methods | **Gap — close in Phase 1.** Today `emit/rust_rpc2.rs` only walks `service Foo { … }` blocks. To match OServer's behaviour for rootobjects, the emitter must additionally emit `#[method(name = "<type>.get/set/delete/find/exists")]` entries into the same trait for every `rootobject` declared in the schema. This keeps "CRUD-for-free" working under hero_rpc2 dispatch with no per-service hand-coding. | | Per-domain OpenRPC spec emission (`A::openrpc_spec()`) | Already emitted by OSchema codegen separately (`crates/generator/src/build/openrpc/…`); not a hero_rpc2 concern | #### C. Multi-domain orchestration — **lives elsewhere, not a hero_rpc2 concern** These are *OSIS-runtime* concerns, not generic JSON-RPC framework. META locks hero_rpc2 to "wire protocol, UDS transports, and the auto-generated Rust client" only. | OServer feature | Where it belongs after Phase 1 | |---|---| | Context registry (`ContextRegistry`, `registry.toml` at `~/hero/var/osisdb/.core/`) | OSIS-runtime helper. Stays in `hero_rpc_osis` (or a small new module). NOT moving into hero_rpc2. | | `context.list / context.add / context.remove / context.import / context.export / domain.list` management RPC methods | Same — OSIS-runtime helper that exposes them as a `jsonrpsee::RpcModule` to merge with the user's trait module | | `seed_from_dir` (TOML-tree seeding) | Same — OSIS-runtime helper | | `git_sync::context_import / context_export` (subprocess rsync + git) | Same — OSIS-runtime helper | | Per-context storage directory layout (`~/hero/var/osisdb/{context}/{domain}/`) | Same — handled by domain `create(db_path, user_id)` callers (already application-level) | | `register::<A>(ctx, domain)` orchestration (storage init + `OsisDomainInit::create` + dispatch wiring) | Same — OSIS-runtime helper builds a `RpcModule` per registered domain and `RpcModule::merge`s them | | Combined OpenRPC document (per-domain `methods` merged + management methods appended) | OSIS-runtime helper produces the merged document and feeds it to `ServerBuilder::with_discover(…)` | **Phase 1 does not introduce that helper.** Phase 2 (`hero_service` template migration) introduces it as a concrete helper, validates the pattern, then Phase 3 sweeps it across the four other repos. #### D. Mandatory HTTP endpoints — **mostly in `hero_lifecycle`; one composition gap** | OServer feature | Status | |---|---| | `/health` HTTP endpoint | Already covered by `hero_lifecycle::HeroRpcServer::mandatory_router()` | | `/openrpc.json` HTTP endpoint | Same | | `/.well-known/heroservice.json` HTTP endpoint (hero_router discovery) | Same | | Mounting JSON-RPC + well-known + optional SSE extension on **the same** `rpc.sock` (OServer's `run_with_extension(Router)`) | **Gap — close in Phase 1.** hero_rpc2's `serve_http` is a private hyper service that only accepts `POST /`. Phase 2 services need to compose the rpc handler with axum's well-known endpoints + optional SSE on one UDS. Cleanest fix: expose the jsonrpsee `RpcModule` as an axum-mountable handler (`hero_rpc2::transport::http::axum_handler(module, max_body, lifted_headers) -> axum::Router`). Then `HeroRpcServer` (which already serves an axum `Router` over UDS) just merges that handler at `POST /rpc` and we have the same shape OServer offers. | #### E. CLI / lifecycle — **lives in `hero_lifecycle`, no gap** | OServer feature | Status | |---|---| | `--info` JSON descriptor | `herolib_core::base::handle_info_flag(SERVICE_TOML)` covers it (every service main.rs already calls it before `run_cli`) | | `--start` / `--stop` / bare-foreground dispatch | `hero_lifecycle::HeroLifecycle::{start,stop}` covers it | | `--contexts foo,bar` flag (multi-context start) | OServer-CLI-specific. Phase 2's hero_service template migration moves it onto the OSIS-runtime helper's CLI. Out of Phase 1 scope. | | `--seed-dir / --seed-domains` flags | Same | #### F. Genuine dead code — delete in Phase 4 | OServer module | Status | |---|---| | `crates/server/src/acl/` (824 LoC: `Acl` / `Group` / `Ace` / `Right`) | **No external callers.** Grep across the workspace finds zero `use hero_rpc_server::acl::…` outside the crate's own docs. Delete with OServer in Phase 4 (not in Phase 1 — keep the deprecation surface intact for downstream services). | | `inspector_ui` feature | Optional HTML debug UI on the same socket. Niche; downstream services can ship their own admin UI via `hero_<name>_admin`. Delete with OServer in Phase 4. | #### Phase 1 gap summary (what actually lands inside `hero_rpc2/` this PR) 1. **`emit/rust_rpc2.rs`**: extend trait emission to auto-emit `<type>.{get,set,delete,find,exists}` `#[method]` entries for every `rootobject` in the schema (closes B auto-CRUD). 2. **`hero_rpc2::transport::http`**: expose `axum_handler(module, max_body, lifted_headers) -> axum::Router` so services can compose rpc + well-known + SSE on one UDS via `HeroRpcServer` (closes D extension merging). 3. **`hero_rpc2::ServerBuilder`**: register a default `rpc.health` builtin (matches OServer's contract; closes the failure surfaced by [#80](https://forge.ourworld.tf/lhumina_code/hero_rpc/issues/80)). Items C and E are explicitly **not** Phase 1 work — they land in Phase 2's `hero_service` template migration as a fresh `OsisServer*` helper that wraps `ServerBuilder`. --- ### (2) OServer fate — **recommend deprecate-then-remove** **Recommendation: deprecate now, schedule removal for Phase 4.** Reasoning: - 5 known downstream consumers on OServer (`hero_service`, `hero_logic`, `hero_compute`, `hero_db`, plus `example/recipe_server`). Hard-cut means flag-day migration across 5 repos in one PR landed window. - The umbrella plan (#90 → Phase 3) explicitly calls for one-PR-per-repo migration in parallel. Deprecation-with-grace-period is what makes that plan possible without lockstep coordination. - Cost of keeping OServer compiling through one release: zero new code — it's frozen. The cost is mostly the bytes in the workspace. - Cost of hard-cut: every consumer breaks the moment this PR squash-merges; downstream agents have to drop other work to migrate. Concrete deprecation steps in this PR (Phase 1): - `#[deprecated(since = "<this-version>", note = "Use hero_rpc2::ServerBuilder + the OsisServer helper landing in Phase 2 (#90). OServer is removed in Phase 4.")]` on the `OServer` struct, `OServerConfig`, `UnifiedServerBuilder`, and `ServerCli`. - Top-of-file deprecation header in `crates/server/src/lib.rs` and `crates/server/README.md` linking to #90 with the removal timeline. - Workspace `Cargo.toml` does NOT drop the `hero_rpc_server` member yet (existing services still depend on it). - CI: no new `#[deny(deprecated)]` — let the warnings surface naturally in downstream service builds so each consuming repo notices on their next `cargo build` and can schedule their Phase 3 PR. Phase 4 then deletes `crates/server/`, the `hero_rpc_server` workspace member, and the legacy emitter path. --- ### (3) Scaffolder cutover — **default to hero_rpc2, keep legacy behind an opt-in** The current scaffolder path (`crates/generator/src/build/scaffold.rs::generate_server_main_rs`) emits a `main.rs` that uses `OServer::run_cli(...)`. There is no separate `rust_rpc.rs` emitter file — the legacy/new split lives in the *scaffolded main.rs template*, with the trait/SDK split living in two `emit/` modules (`rust_rpc2.rs` for the new hero_rpc2 trait, `rust_sdk.rs` for the legacy OServer-targeted client). Restating the user's framing in these accurate terms: **Current state (committed on `development`):** - `emit/rust_rpc2.rs` → emits `sdk/rust/src/<domain>.rs` containing the hero_rpc2 trait. **Opt-in** via `OschemaBuildConfig::with_hero_rpc2_sdk()`. Default is **off**. - `emit/rust_sdk.rs` → emits the legacy typed-client SDK targeting the OServer wire shape. Default is **on**. - `scaffold.rs::generate_server_main_rs` → emits a `main.rs` calling `OServer::run_cli`. Hard-coded, no flag. **Phase 1 cutover plan:** 1. **Flip the scaffolder's emitted `main.rs` to hero_rpc2 by default.** The new template uses: - `hero_rpc2::ServerBuilder` for transport, - `hero_lifecycle::HeroRpcServer` for the well-known endpoints + lifecycle CLI, - the trait impls from the generated `sdk/rust/src/<domain>.rs` (which the scaffolded `build.rs` now opts into via `with_hero_rpc2_sdk()`). The new template ships an inline TODO comment pointing each service author at the (Phase 2) `OsisServer` helper for context-registry + seed wiring. 2. **Flip the `OschemaBuildConfig` defaults** so the scaffolded `build.rs` produces the rpc2 trait by default and *also* produces the legacy SDK while existing services still need it (so the build pipeline stays compatible during the transition window): - `generate_rpc2_sdk: true` (was `false`) - `generate_rust_sdk: true` unchanged (legacy still emitted for existing services) - new opt-out `with_legacy_disabled()` for new scaffolds that want a clean hero_rpc2-only crate 3. **Legacy main.rs template stays callable behind an explicit flag** for the brief window while existing services migrate: `WorkspaceScaffolder::with_legacy_oserver_main()`. Default = off. Phase 4 deletes this flag along with OServer. 4. **`example/recipe_server` keeps building unchanged** through Phase 1 — its existing `main.rs` predates the flag flip and uses OServer; the deprecation warnings will surface but compile stays clean. Phase 2 migrates `hero_service` first (validating the new template), Phase 3 sweeps `recipe_server` + the four real services, Phase 4 deletes the legacy emitter. 5. **`example/recipe_sdk_rpc2` stays as the hero_rpc2 reference** until Phase 4 consolidates it into `recipe_server`. No change in Phase 1. --- ### Acceptance gate (re-stated from #90 body) For Phase 1 only, this PR will land with: - Three hero_rpc2 gaps closed: rpc2-emitter auto-CRUD for rootobjects, `axum_handler` integration, `rpc.health` builtin. - OServer deprecated with `#[deprecated]` attrs + README note linking to #90 with the Phase 4 removal timeline. - Scaffolder default flipped (new scaffolds emit a hero_rpc2-based main.rs); legacy main.rs template behind `with_legacy_oserver_main()`. - `cargo build --workspace` + `cargo test --workspace` clean on `hero_rpc`. - `example/recipe_server` + `example/recipe_sdk_rpc2` + every existing OSIS-backed service in the org continues to build (deprecation warnings only; no errors). **Phases 2 / 3 / 4 stay future PRs** — they need this framework path in place first. --- ### Waiting on sign-off Before any code, three explicit yes/no asks: 1. **Audit shape** — does the (1) gap map match your read? Specifically: is "OSIS-runtime context registry + git_sync + seed + management methods lives in `hero_rpc_osis`, NOT in `hero_rpc2`" the right split, given META's "hero_rpc2 owns wire/transport/Rust-client" line? 2. **OServer fate** — deprecate-then-remove (one release grace) good, or do you want a harder cut? 3. **Scaffolder cutover** — flip default + keep legacy main.rs template behind `with_legacy_oserver_main()` flag good, or do you want a different opt-in name / different default for `generate_rust_sdk`? Once those three are signed off I push the implementation in one squash-mergeable PR.
Author
Owner

Phase 1 design — revised after sign-off feedback

Investigated. Two outcomes:

  • Gap (1) on the original audit was off — I'd missed a cleaner factoring you already nailed in the hybrid decision.
  • Most of the OServer "orchestration" features I'd proposed re-homing in hero_rpc_osis turn out to be either ORPHAN (no external callers) or already SUPERSEDED by service-side codegen. Drop them. Detail below.

Re (1) — UDS is already there; the real gap is /rpc + the four well-known endpoints

Confirmed against the hero_sockets skill (the canonical spec — ~/.claude/skills/hero_sockets/SKILL.md):

  • hero_rpc2's serve_http is already UDS — tokio::net::UnixListener, no TCP. Same for serve_line. No UDS gap.
  • BUT: the skill specifies POST /rpc as the dispatch path. hero_rpc2's HTTP transport today accepts only POST / (crates/hero_rpc2/src/transport/http.rs:105). Gap — fix in Phase 1.
  • The skill also mandates GET /openrpc.json, GET /health, GET /.well-known/heroservice.json on every rpc.sock. hero_rpc2 has none. Gap — fix in Phase 1. These plug straight in to hero_rpc2's existing hyper service in serve_http; doesn't need axum at all. Much smaller than my earlier "expose as axum handler" proposal.

Re (2)/B — hybrid was right; emitter does NOT need to grow auto-CRUD

You're correct, this was the locked decision and I drifted. Refactor of the proposal:

  • OSchema codegen continues to produce the OsisAppRpcHandler impl (handler.rs + the generated dispatch in <service>_server/src/<domain>/server/*) — same as today. CRUD-by-typename + service methods both live in there. Nothing changes on the codegen side for this.
  • hero_rpc2 sits on top as transport: a tiny adapter converts the existing OsisAppRpcHandler::handle_rpc_call_with_context / handle_service_call_with_context dispatch into a jsonrpsee::RpcModule. That adapter is the only new code: register one method per declared type-method pair, route through the existing handler. CRUD-for-free stays automatic because the handler already does it.
  • That adapter is what gets used by Phase 2+ services — not a re-implemented trait emitter.

This is meaningfully smaller than what I'd outlined. The rust_rpc2 emitter can stay narrow (typed-client + server-trait declaration for hand-written service methods only — already what it does). The bridge to existing OSIS dispatch lives in a new hero_rpc2 module (call it osis_adapter or similar) — or alternatively in hero_rpc_osis itself as the consumer-facing seam.

Re (2) — hard-cut accepted; orphan-detail follows

Inventory of every OServer feature with current callers (workspace-wide grep done — concrete file:line evidence):

Feature Status Evidence
crates/server/src/acl/ (824 LoC) ORPHAN Zero use hero_rpc_server::acl::* outside the crate. Acl:: / Group::new( / Ace:: references found elsewhere belong to unrelated types (hero_osis::AclRight, hero_books::Group, hero_ledger::Group). Drop.
ContextRegistry + ~/hero/var/osisdb/.core/registry.toml persistence ORPHAN No external reader/writer found across the workspace. Drop.
context.add RPC method ORPHAN No external callers in any language. Drop.
context.remove RPC method ORPHAN No external callers. Drop.
context.import RPC method ORPHAN No external callers — only OServer's internal unified_server.rs:630-665 dispatch wires it to git_sync::context_import. Drop.
context.export RPC method ORPHAN Same — only unified_server.rs:667-709. Drop.
git_sync::context_import / context_export ORPHAN by transitive closure Only callers are the two ORPHAN RPC methods above. Drop with them.
context.list RPC method SUPERSEDED by service-side codegen Three callers, all of which IMPLEMENT it themselves: hero_osis_server/src/base/server/osis_server_generated.rs:1832 (services' own generated dispatch), plus the consumer side hero_osis_sdk/.../osis_client_generated.rs:458 + hero_code_admin/static/js/contexts.js. The OServer built-in is dead weight — services already serve their own version. Drop.
domain.list RPC method SUPERSEDED Same pattern: hero_proxy_server/src/lib.rs:275 implements it on the service side; hero_proxy_server/openrpc.client.generated.rs:1031 is the client. Drop.
OServer::seed_from_dir + --seed-dir / --seed-domains CLI flags USED, but goes with OServer Callers: hero_osis/tests/e2e_seed.rs (test that exercises OServer behaviour — goes when hero_osis migrates in Phase 3), hero_rpc/crates/server/tests/cli_lifecycle.rs (test of OServer — goes with OServer), hero_rpc/crates/server/src/server/spawn.rs:94-111 (inside OServer crate — goes), hero_rpc/crates/generator/src/build/emit/bin.rs:136-149 (legacy bin emitter — gets rewritten in this PR when we flip the scaffolder template). No external Rust API surface. Drop with OServer; Phase 2+ services that want TOML seeding implement it themselves on the OSIS dispatch directly (small helper if commonality emerges).
--contexts foo,bar multi-context start flag OServer-CLI-specific Used by crates/generator/src/build/emit/bin.rs (which we're rewriting) + crates/server/tests/cli_lifecycle.rs (OServer test). Drop with OServer; if multi-context-per-process becomes a need later, it lives on the consumer's CLI parser, not the framework.
inspector_ui feature OServer-internal Optional HTML debug UI on rpc.sock. No external opt-ins found. Drop with OServer.

Net of Phase 1: delete hero_rpc/crates/server/ entirely. Workspace member gone. No #[deprecated] machinery, no "stays-callable-behind-a-flag" — straight removal.

Re (3) — scaffolder: only new-way; legacy SDK emitter also goes

Following the "don't keep legacy, don't emit 2" rule:

  • Scaffolded main.rs emits only the hero_rpc2 path (ServerBuilder + UDS HTTP + the new well-known endpoints + the bridge adapter to the codegen-generated OSIS dispatch).
  • OschemaBuildConfig.generate_rpc2_sdk default flipped to true. generate_rust_sdk default flipped to false. No with_legacy_oserver_main(), no with_legacy_disabled() — just one path.
  • emit/rust_sdk.rs (legacy SDK emitter producing the OServer-targeted typed client) gets deleted in Phase 1, not Phase 4.

One downstream snag worth flagging: hero_launcher is the one workspace service that ships an auto-generated sdk/rust/ from the legacy emitter, and hero_wasmos/Cargo.toml:187 depends on hero_launcher_sdk. If we delete the legacy emitter in Phase 1, hero_launcher stops regenerating that SDK on its next build, and hero_wasmos either keeps building against the last committed version (likely — the SDK is checked-in code) or breaks. Two options:

  1. Migrate hero_launcher onto the rpc2 SDK emitter in this same PR (small — hero_launcher is the only consumer of the legacy emitter).
  2. Leave the legacy emitter in for Phase 1 even though OServer is gone, mark it #[deprecated], drop in Phase 4 when hero_launcher migrates.

I lean toward option 1 — it keeps "one path only" honest and avoids carrying a deprecated emitter through Phase 2–3. Asking explicitly before doing either.

One genuine acceptance-criterion conflict to surface

Your original prompt said: "keep recipe_server building unchanged (it can stay on OServer through Phase 1)... every existing OSIS service still builds clean." That assumed deprecate-then-remove.

Hard-cut OServer in Phase 1 breaks that — example/recipe_server is on OServer, it stops compiling the moment crates/server/ is deleted. Options:

  • (a) Migrate example/recipe_server onto the new hero_rpc2 + OSIS-adapter path in this same PR. This effectively folds Phase 2's "validate the new template" work into Phase 1, using recipe_server as the validator instead of hero_service. recipe_sdk_rpc2 collapses into recipe_server (no more two parallel examples). hero_service template migration in Phase 2 then becomes "copy what recipe_server did."
  • (b) Delete example/recipe_server outright in Phase 1; example/recipe_sdk_rpc2 becomes the only example until Phase 4 consolidation.
  • (c) Skip the deletion of crates/server/ in Phase 1; just delete the scaffolder's emission of OServer-using main.rs + the legacy SDK emitter + acl/registry/git_sync orphan code, and have Phase 4 delete the crates/server/ shell. This is the only way to keep recipe_server building. It's not really a hard-cut — it's "hard-cut the user-facing scaffolder, gentle-cut the OServer crate itself."

I lean toward (a) — folds the recipe_server work into Phase 1 cleanly, retires recipe_sdk_rpc2, and validates the new path end-to-end in the same PR. (b) loses the realistic end-to-end example. (c) reintroduces the "deprecated but compiling" pattern you said you don't want.


Concrete Phase 1 plan (waiting on three confirmations)

  1. (1)/B reframing — codegen keeps producing the OSIS handler (incl. CRUD-by-typename); the only new code is a bridge OsisAppRpcHandler → jsonrpsee::RpcModule exposed from hero_rpc2 (or hero_rpc_osis). The rpc2 trait emitter does NOT grow auto-CRUD. Confirm?
  2. Hard-cut scope — delete crates/server/ (incl. acl, registry, git_sync, all six management methods, seed_from_dir, inspector_ui), drop the legacy SDK emitter emit/rust_sdk.rs. Confirm?
  3. recipe_server fate — go with option (a) (migrate recipe_server onto rpc2 in this same PR, retire recipe_sdk_rpc2), or do you want (b) or (c)? Pick one.

Once (1), (2), (3) are answered I push the implementation.

What hero_rpc2 grows in this PR

  • serve_http: route POST /rpc instead of POST / (matching skill convention).
  • Add GET /openrpc.json, GET /health, GET /.well-known/heroservice.json to the hyper service.
  • ServerBuilder learns with_service_info(name, title, version, description) for the discovery manifest + a way to register a built-in rpc.health.
  • New osis_adapter module: takes an Arc<A: OsisAppRpcHandler> + the static OpenRPC spec it ships, returns a jsonrpsee::RpcModule that hero_rpc2 dispatches through. This is the single replacement for the OServer dispatch loop.
## Phase 1 design — revised after sign-off feedback Investigated. Two outcomes: - Gap (1) on the original audit was off — I'd missed a cleaner factoring you already nailed in the hybrid decision. - Most of the OServer "orchestration" features I'd proposed re-homing in `hero_rpc_osis` turn out to be either ORPHAN (no external callers) or already SUPERSEDED by service-side codegen. Drop them. Detail below. --- ### Re (1) — UDS is already there; the real gap is `/rpc` + the four well-known endpoints Confirmed against the `hero_sockets` skill (the canonical spec — `~/.claude/skills/hero_sockets/SKILL.md`): - hero_rpc2's `serve_http` is already UDS — `tokio::net::UnixListener`, no TCP. Same for `serve_line`. **No UDS gap.** - BUT: the skill specifies `POST /rpc` as the dispatch path. hero_rpc2's HTTP transport today accepts only `POST /` ([`crates/hero_rpc2/src/transport/http.rs:105`](https://forge.ourworld.tf/lhumina_code/hero_rpc/src/branch/development/crates/hero_rpc2/src/transport/http.rs#L105)). **Gap — fix in Phase 1.** - The skill also mandates `GET /openrpc.json`, `GET /health`, `GET /.well-known/heroservice.json` on every `rpc.sock`. hero_rpc2 has none. **Gap — fix in Phase 1.** These plug straight in to hero_rpc2's existing hyper service in `serve_http`; doesn't need axum at all. Much smaller than my earlier "expose as axum handler" proposal. ### Re (2)/B — hybrid was right; emitter does NOT need to grow auto-CRUD You're correct, this was the locked decision and I drifted. Refactor of the proposal: - OSchema codegen continues to produce the `OsisAppRpcHandler` impl (handler.rs + the generated dispatch in `<service>_server/src/<domain>/server/*`) — same as today. CRUD-by-typename + service methods both live in there. Nothing changes on the codegen side for this. - hero_rpc2 sits **on top** as transport: a tiny adapter converts the existing `OsisAppRpcHandler::handle_rpc_call_with_context` / `handle_service_call_with_context` dispatch into a `jsonrpsee::RpcModule`. That adapter is the only new code: register one method per declared type-method pair, route through the existing handler. CRUD-for-free stays automatic because the handler already does it. - That adapter is what gets used by Phase 2+ services — not a re-implemented trait emitter. This is meaningfully smaller than what I'd outlined. The rust_rpc2 emitter can stay narrow (typed-client + server-trait declaration for hand-written service methods only — already what it does). The bridge to existing OSIS dispatch lives in a new `hero_rpc2` module (call it `osis_adapter` or similar) — or alternatively in `hero_rpc_osis` itself as the consumer-facing seam. ### Re (2) — hard-cut accepted; orphan-detail follows Inventory of every OServer feature with current callers (workspace-wide grep done — concrete file:line evidence): | Feature | Status | Evidence | |---|---|---| | `crates/server/src/acl/` (824 LoC) | **ORPHAN** | Zero `use hero_rpc_server::acl::*` outside the crate. `Acl::` / `Group::new(` / `Ace::` references found elsewhere belong to unrelated types (`hero_osis::AclRight`, `hero_books::Group`, `hero_ledger::Group`). Drop. | | `ContextRegistry` + `~/hero/var/osisdb/.core/registry.toml` persistence | **ORPHAN** | No external reader/writer found across the workspace. Drop. | | `context.add` RPC method | **ORPHAN** | No external callers in any language. Drop. | | `context.remove` RPC method | **ORPHAN** | No external callers. Drop. | | `context.import` RPC method | **ORPHAN** | No external callers — only OServer's internal `unified_server.rs:630-665` dispatch wires it to `git_sync::context_import`. Drop. | | `context.export` RPC method | **ORPHAN** | Same — only `unified_server.rs:667-709`. Drop. | | `git_sync::context_import` / `context_export` | **ORPHAN** by transitive closure | Only callers are the two ORPHAN RPC methods above. Drop with them. | | `context.list` RPC method | **SUPERSEDED** by service-side codegen | Three callers, all of which IMPLEMENT it themselves: `hero_osis_server/src/base/server/osis_server_generated.rs:1832` (services' own generated dispatch), plus the consumer side `hero_osis_sdk/.../osis_client_generated.rs:458` + `hero_code_admin/static/js/contexts.js`. The OServer built-in is dead weight — services already serve their own version. Drop. | | `domain.list` RPC method | **SUPERSEDED** | Same pattern: `hero_proxy_server/src/lib.rs:275` implements it on the service side; `hero_proxy_server/openrpc.client.generated.rs:1031` is the client. Drop. | | `OServer::seed_from_dir` + `--seed-dir` / `--seed-domains` CLI flags | **USED, but goes with OServer** | Callers: `hero_osis/tests/e2e_seed.rs` (test that exercises OServer behaviour — goes when hero_osis migrates in Phase 3), `hero_rpc/crates/server/tests/cli_lifecycle.rs` (test of OServer — goes with OServer), `hero_rpc/crates/server/src/server/spawn.rs:94-111` (inside OServer crate — goes), `hero_rpc/crates/generator/src/build/emit/bin.rs:136-149` (legacy bin emitter — gets rewritten in this PR when we flip the scaffolder template). No external Rust API surface. Drop with OServer; Phase 2+ services that want TOML seeding implement it themselves on the OSIS dispatch directly (small helper if commonality emerges). | | `--contexts foo,bar` multi-context start flag | **OServer-CLI-specific** | Used by `crates/generator/src/build/emit/bin.rs` (which we're rewriting) + `crates/server/tests/cli_lifecycle.rs` (OServer test). Drop with OServer; if multi-context-per-process becomes a need later, it lives on the consumer's CLI parser, not the framework. | | `inspector_ui` feature | **OServer-internal** | Optional HTML debug UI on `rpc.sock`. No external opt-ins found. Drop with OServer. | **Net of Phase 1:** delete `hero_rpc/crates/server/` entirely. Workspace member gone. No `#[deprecated]` machinery, no "stays-callable-behind-a-flag" — straight removal. ### Re (3) — scaffolder: only new-way; legacy SDK emitter also goes Following the "don't keep legacy, don't emit 2" rule: - Scaffolded `main.rs` emits **only** the hero_rpc2 path (ServerBuilder + UDS HTTP + the new well-known endpoints + the bridge adapter to the codegen-generated OSIS dispatch). - `OschemaBuildConfig.generate_rpc2_sdk` default flipped to `true`. **`generate_rust_sdk` default flipped to `false`.** No `with_legacy_oserver_main()`, no `with_legacy_disabled()` — just one path. - `emit/rust_sdk.rs` (legacy SDK emitter producing the OServer-targeted typed client) **gets deleted** in Phase 1, not Phase 4. **One downstream snag worth flagging:** `hero_launcher` is the one workspace service that ships an auto-generated `sdk/rust/` from the legacy emitter, and `hero_wasmos/Cargo.toml:187` depends on `hero_launcher_sdk`. If we delete the legacy emitter in Phase 1, hero_launcher stops regenerating that SDK on its next build, and hero_wasmos either keeps building against the last committed version (likely — the SDK is checked-in code) or breaks. **Two options:** 1. Migrate hero_launcher onto the rpc2 SDK emitter in this same PR (small — hero_launcher is the only consumer of the legacy emitter). 2. Leave the legacy emitter in for Phase 1 even though OServer is gone, mark it `#[deprecated]`, drop in Phase 4 when hero_launcher migrates. I lean toward option 1 — it keeps "one path only" honest and avoids carrying a deprecated emitter through Phase 2–3. Asking explicitly before doing either. ### One genuine acceptance-criterion conflict to surface Your original prompt said: *"keep recipe_server building unchanged (it can stay on OServer through Phase 1)... every existing OSIS service still builds clean."* That assumed deprecate-then-remove. Hard-cut OServer in Phase 1 breaks that — `example/recipe_server` is on OServer, it stops compiling the moment `crates/server/` is deleted. Options: - **(a)** Migrate `example/recipe_server` onto the new hero_rpc2 + OSIS-adapter path in this same PR. This effectively folds Phase 2's "validate the new template" work into Phase 1, using recipe_server as the validator instead of hero_service. recipe_sdk_rpc2 collapses into recipe_server (no more two parallel examples). hero_service template migration in Phase 2 then becomes "copy what recipe_server did." - **(b)** Delete `example/recipe_server` outright in Phase 1; `example/recipe_sdk_rpc2` becomes the only example until Phase 4 consolidation. - **(c)** Skip the deletion of `crates/server/` in Phase 1; just delete the scaffolder's emission of OServer-using main.rs + the legacy SDK emitter + acl/registry/git_sync orphan code, and have Phase 4 delete the `crates/server/` shell. This is the only way to keep recipe_server building. It's not really a hard-cut — it's "hard-cut the user-facing scaffolder, gentle-cut the OServer crate itself." I lean toward (a) — folds the recipe_server work into Phase 1 cleanly, retires `recipe_sdk_rpc2`, and validates the new path end-to-end in the same PR. (b) loses the realistic end-to-end example. (c) reintroduces the "deprecated but compiling" pattern you said you don't want. --- ### Concrete Phase 1 plan (waiting on three confirmations) 1. **(1)/B reframing** — codegen keeps producing the OSIS handler (incl. CRUD-by-typename); the only new code is a bridge `OsisAppRpcHandler → jsonrpsee::RpcModule` exposed from hero_rpc2 (or `hero_rpc_osis`). The rpc2 trait emitter does NOT grow auto-CRUD. **Confirm?** 2. **Hard-cut scope** — delete `crates/server/` (incl. acl, registry, git_sync, all six management methods, seed_from_dir, inspector_ui), drop the legacy SDK emitter `emit/rust_sdk.rs`. **Confirm?** 3. **recipe_server fate** — go with option (a) (migrate recipe_server onto rpc2 in this same PR, retire recipe_sdk_rpc2), or do you want (b) or (c)? **Pick one.** Once (1), (2), (3) are answered I push the implementation. #### What hero_rpc2 grows in this PR - `serve_http`: route `POST /rpc` instead of `POST /` (matching skill convention). - Add `GET /openrpc.json`, `GET /health`, `GET /.well-known/heroservice.json` to the hyper service. - `ServerBuilder` learns `with_service_info(name, title, version, description)` for the discovery manifest + a way to register a built-in `rpc.health`. - New `osis_adapter` module: takes an `Arc<A: OsisAppRpcHandler>` + the static OpenRPC spec it ships, returns a `jsonrpsee::RpcModule` that hero_rpc2 dispatches through. This is the single replacement for the OServer dispatch loop.
Author
Owner

Phase 1 landed — branch ready for PR

Branch issue-90-phase1-framework pushed; ready to open the PR against development. Three commits, hard-cut per sign-off:

  1. 0648239hero_rpc2 transport rewrite. POST /rpc (was POST /) plus the three mandatory GET endpoints per hero_sockets skill: /health, /openrpc.json, /.well-known/heroservice.json. New ServiceInfo + ServerBuilder::with_service_info() to populate those. rpc.health JSON-RPC builtin auto-registered by serve_http (closes #80 root cause). New tests/http_well_known.rs pins the four-endpoint shape so hero_router discovery can't regress silently.

  2. 0a53247OSIS → hero_rpc2 bridge. New hero_rpc_osis::rpc::rpc2_adapter::module_for(Arc<A: OsisAppRpcHandler>) returns a jsonrpsee::RpcModule<()> that registers <type>.{get,set,delete,find,exists,list} for every type in A::type_names() (routed to handle_rpc_call_with_context, same param-shape extraction OServer used), plus every entry in A::openrpc_spec().methods[] not colliding with CRUD or hero_rpc2 builtins (routed to handle_service_call_with_context). Lowercased method names, typed-error mapping per #83, HeroRequestContext → RequestContext projection per call. This is the seam hybrid-decision spec called for.

  3. f828383Hard-cut OServer + flip scaffolder.

    • Deleted: crates/server/ entire crate (3,362 LoC — OServer, ACL module, ContextRegistry/git_sync/context.*/domain.* management methods, seed_from_dir, inspector_ui, unified_server.rs, every workspace member dropped). Workspace-wide audit (Acl::, Group::new(, Ace::, six management-method literals, seed_from_dir(, every hero_rpc_server::* import) confirmed zero external callers before deletion.
    • Deleted: emit/rust_sdk.rs (legacy OServer-targeted typed client) + emit/bin.rs (per-domain orchestrator using OServer). All their config (OschemaBuildConfig::generate_rust_sdk field, with_rust_sdk(), bin_* / single_bin / SingleBinConfig) dropped.
    • Deleted: example/recipe_sdk_rpc2/ — folded into example/recipe_server, which is now the single end-to-end demo on the new path.
    • Flipped: OschemaBuildConfig::generate_rpc2_sdk defaults to true. New without_hero_rpc2_sdk() opt-out for the rare build script with its own client path. Scaffolder's emitted main.rs now drives ServerBuilder::serve_http + rpc2_adapter::register_methods directly — no OServer, no run_cli callback, no per-context registry assumption.
    • Migrated: example/recipe_server end-to-end. crates/hero_recipes_server/src/main.rs rewritten (ServerBuilder + rpc2_adapter + tokio::signal::ctrl_c). crates/hero_recipes/build.rs opts into the rpc2 SDK. service.toml socket path moved to hero_recipes/rpc.sock (canonical hero_sockets shape — no _server suffix in the directory). sdk/rust/Cargo.toml depends on hero_rpc2 + jsonrpsee instead of the deleted hero_rpc_client.
    • Test updates: scaffolder assertions pin the new template (no OServer references), flag_defaults_offflag_defaults_on_after_phase1_cutover, three crates/server/examples/recipe_server/… cases removed from real_schemas (the example/recipe_server/schemas/… case remains).

hero_launcher / hero_wasmos — false positive

Investigation reversed the earlier "lone consumer" finding: hero_launcher_sdk is a fully hand-written Rust client (no build.rs, no codegen, zero references to hero_rpc_server / hero_rpc_client / the legacy emitter). hero_wasmos consumes it as a plain library crate. Neither repo is touched by this PR — no companion branch needed.

Verification

  • cargo check --workspace: clean.
  • cargo test --workspace --lib --bins: all 132 + 79 + 66 tests pass.
  • cargo test -p hero_rpc2 --features uds-http --tests: 27 tests pass, including the new http_well_known.rs (health/discovery/openrpc.json/rpc.health) + the updated http_hero_rpc_compat.rs + http_context_lift.rs now POSTing to /rpc.
  • example/recipe_server: cargo test --workspace --lib --bins clean against the local in-tree patches.

Two pre-existing failures on development — NOT addressed here

cargo test --workspace (without --lib --bins) surfaces two failures that also occur on origin/development without any of these changes — bit-rot from earlier work, not caused by #90:

  1. hero_rpc_derive test sse_proxyopenrpc_proxy! macro expansion references herolib_core but the test crate doesn't depend on it.
  2. hero_rpc_osis examples 1_generate_code and 2_use_models — call APIs (hero_rpc_osis::oschema::Generator, DBTyped::new_with_index) that no longer exist on the crate.

Flag these for separate one-off fixes; they're orthogonal to the framework migration.

What did NOT change (deferred to Phase 2–4)

Phase 1 is only the framework cut. No service binaries are migrated:

  • Phase 2 (one PR): hero_service template repo — first real OSIS service moved onto rpc2 transport. Validates the path; updates the hero_service_scaffold.md skill.
  • Phase 3 (parallel PRs): hero_logic, hero_compute, hero_db, … — each their own PR. Each repo's Cargo.toml flips its branch pin from issue-90-phase1-framework back to development after this PR squash-merges. Pattern: copy what example/recipe_server/crates/hero_recipes_server/src/main.rs does in this PR.
  • Phase 4 (cleanup): consolidate / sweep any remaining _sdk_rpc2 shadow examples (there is one in this repo's tree already retired) and any service repos still on a temporary pin.

Ready to merge once reviewed.

## Phase 1 landed — branch ready for PR Branch `issue-90-phase1-framework` pushed; ready to open the PR against `development`. Three commits, hard-cut per [sign-off](https://forge.ourworld.tf/lhumina_code/hero_rpc/issues/90#issuecomment-34867): 1. [`0648239`](https://forge.ourworld.tf/lhumina_code/hero_rpc/commit/0648239) — **hero_rpc2 transport rewrite.** `POST /rpc` (was `POST /`) plus the three mandatory GET endpoints per `hero_sockets` skill: `/health`, `/openrpc.json`, `/.well-known/heroservice.json`. New `ServiceInfo` + `ServerBuilder::with_service_info()` to populate those. `rpc.health` JSON-RPC builtin auto-registered by `serve_http` (closes [#80](https://forge.ourworld.tf/lhumina_code/hero_rpc/issues/80) root cause). New `tests/http_well_known.rs` pins the four-endpoint shape so hero_router discovery can't regress silently. 2. [`0a53247`](https://forge.ourworld.tf/lhumina_code/hero_rpc/commit/0a53247) — **OSIS → hero_rpc2 bridge.** New `hero_rpc_osis::rpc::rpc2_adapter::module_for(Arc<A: OsisAppRpcHandler>)` returns a `jsonrpsee::RpcModule<()>` that registers `<type>.{get,set,delete,find,exists,list}` for every type in `A::type_names()` (routed to `handle_rpc_call_with_context`, same param-shape extraction OServer used), plus every entry in `A::openrpc_spec().methods[]` not colliding with CRUD or hero_rpc2 builtins (routed to `handle_service_call_with_context`). Lowercased method names, typed-error mapping per [#83](https://forge.ourworld.tf/lhumina_code/hero_rpc/issues/83), `HeroRequestContext → RequestContext` projection per call. This is the seam hybrid-decision spec called for. 3. [`f828383`](https://forge.ourworld.tf/lhumina_code/hero_rpc/commit/f828383) — **Hard-cut OServer + flip scaffolder.** * Deleted: `crates/server/` entire crate (3,362 LoC — OServer, ACL module, `ContextRegistry`/`git_sync`/`context.*`/`domain.*` management methods, `seed_from_dir`, `inspector_ui`, `unified_server.rs`, every workspace member dropped). Workspace-wide audit (`Acl::`, `Group::new(`, `Ace::`, six management-method literals, `seed_from_dir(`, every `hero_rpc_server::*` import) confirmed zero external callers before deletion. * Deleted: `emit/rust_sdk.rs` (legacy OServer-targeted typed client) + `emit/bin.rs` (per-domain orchestrator using OServer). All their config (`OschemaBuildConfig::generate_rust_sdk` field, `with_rust_sdk()`, `bin_*` / `single_bin` / `SingleBinConfig`) dropped. * Deleted: `example/recipe_sdk_rpc2/` — folded into `example/recipe_server`, which is now the single end-to-end demo on the new path. * Flipped: `OschemaBuildConfig::generate_rpc2_sdk` defaults to `true`. New `without_hero_rpc2_sdk()` opt-out for the rare build script with its own client path. Scaffolder's emitted `main.rs` now drives `ServerBuilder::serve_http` + `rpc2_adapter::register_methods` directly — no OServer, no `run_cli` callback, no per-context registry assumption. * Migrated: `example/recipe_server` end-to-end. `crates/hero_recipes_server/src/main.rs` rewritten (`ServerBuilder` + `rpc2_adapter` + `tokio::signal::ctrl_c`). `crates/hero_recipes/build.rs` opts into the rpc2 SDK. `service.toml` socket path moved to `hero_recipes/rpc.sock` (canonical `hero_sockets` shape — no `_server` suffix in the directory). `sdk/rust/Cargo.toml` depends on `hero_rpc2 + jsonrpsee` instead of the deleted `hero_rpc_client`. * Test updates: scaffolder assertions pin the new template (no OServer references), `flag_defaults_off` → `flag_defaults_on_after_phase1_cutover`, three `crates/server/examples/recipe_server/…` cases removed from `real_schemas` (the `example/recipe_server/schemas/…` case remains). ## hero_launcher / hero_wasmos — false positive Investigation reversed the earlier "lone consumer" finding: `hero_launcher_sdk` is a fully **hand-written** Rust client (no build.rs, no codegen, zero references to `hero_rpc_server` / `hero_rpc_client` / the legacy emitter). hero_wasmos consumes it as a plain library crate. Neither repo is touched by this PR — no companion branch needed. ## Verification - `cargo check --workspace`: clean. - `cargo test --workspace --lib --bins`: all 132 + 79 + 66 tests pass. - `cargo test -p hero_rpc2 --features uds-http --tests`: 27 tests pass, including the new `http_well_known.rs` (health/discovery/openrpc.json/rpc.health) + the updated `http_hero_rpc_compat.rs` + `http_context_lift.rs` now POSTing to `/rpc`. - `example/recipe_server`: `cargo test --workspace --lib --bins` clean against the local in-tree patches. ## Two pre-existing failures on `development` — NOT addressed here `cargo test --workspace` (without `--lib --bins`) surfaces two failures that also occur on `origin/development` without any of these changes — bit-rot from earlier work, not caused by #90: 1. `hero_rpc_derive` test `sse_proxy` — `openrpc_proxy!` macro expansion references `herolib_core` but the test crate doesn't depend on it. 2. `hero_rpc_osis` examples `1_generate_code` and `2_use_models` — call APIs (`hero_rpc_osis::oschema::Generator`, `DBTyped::new_with_index`) that no longer exist on the crate. Flag these for separate one-off fixes; they're orthogonal to the framework migration. ## What did NOT change (deferred to Phase 2–4) Phase 1 is *only* the framework cut. No service binaries are migrated: - **Phase 2 (one PR):** `hero_service` template repo — first real OSIS service moved onto rpc2 transport. Validates the path; updates the `hero_service_scaffold.md` skill. - **Phase 3 (parallel PRs):** `hero_logic`, `hero_compute`, `hero_db`, … — each their own PR. Each repo's `Cargo.toml` flips its branch pin from `issue-90-phase1-framework` back to `development` after this PR squash-merges. Pattern: copy what `example/recipe_server/crates/hero_recipes_server/src/main.rs` does in this PR. - **Phase 4 (cleanup):** consolidate / sweep any remaining `_sdk_rpc2` shadow examples (there is one in this repo's tree already retired) and any service repos still on a temporary pin. Ready to merge once reviewed.
Author
Owner

Phase 2 — hero_service migrated, framework gap closed

hero_service PR #5 squash-merged. The first OSIS service is on hero_rpc2 end-to-end. Smoke transcript:

Surface Result
GET /health {status: ok, service: hero_service, version: 0.1.0}
GET /.well-known/heroservice.json canonical shape — protocol=openrpc, socket=rpc
GET /openrpc.json 7 methods, byte-identical to docs/openrpc.json
POST /rpc rpc.health {status: ok, trusted: true} (builtin, auto-registered)
POST /rpc servicecatalog.list [] (empty catalog, storage clean)
lab infocheck 2 crates clean, 0 findings

Framework gap surfaced + closed

The migration ran into a small hero_rpc2 API gap: services build their OpenRPC doc offline via codegen (<workspace>/docs/openrpc.json per #73) and include_str! it, but ServerBuilder::with_discover only accepted documents constructed in-process via OpenRpcBuilder. Filed + landed in this same session:

  • hero_rpc PR #99OpenRpcDocument::from_value(Value) constructor — squash-merged.

That unblocks every Phase 3 service. The from_value test in http_well_known.rs pins the round-trip so it can't regress silently.

Out-of-scope (intentional)

hero_service still has the legacy sibling-crate SDK layout (crates/hero_service_sdk instead of the #70 sdk/rust/ shape). The migration kept client_crate_dir pointing at the legacy spot — everything compiles, nothing breaks. A future cleanup PR can lift the layout if desired; it's not blocking #90.

Next

Phase 3a — hero_logic (base PR against main, not development).

## Phase 2 — `hero_service` migrated, framework gap closed [`hero_service` PR #5](https://forge.ourworld.tf/lhumina_code/hero_service/pulls/5) **squash-merged**. The first OSIS service is on `hero_rpc2` end-to-end. Smoke transcript: | Surface | Result | |---|---| | `GET /health` | `{status: ok, service: hero_service, version: 0.1.0}` | | `GET /.well-known/heroservice.json` | canonical shape — `protocol=openrpc`, `socket=rpc` | | `GET /openrpc.json` | 7 methods, byte-identical to `docs/openrpc.json` | | `POST /rpc rpc.health` | `{status: ok, trusted: true}` (builtin, auto-registered) | | `POST /rpc servicecatalog.list` | `[]` (empty catalog, storage clean) | | `lab infocheck` | 2 crates clean, 0 findings | ### Framework gap surfaced + closed The migration ran into a small `hero_rpc2` API gap: services build their OpenRPC doc offline via codegen (`<workspace>/docs/openrpc.json` per #73) and `include_str!` it, but `ServerBuilder::with_discover` only accepted documents constructed in-process via `OpenRpcBuilder`. Filed + landed in this same session: - [hero_rpc PR #99](https://forge.ourworld.tf/lhumina_code/hero_rpc/pulls/99) — `OpenRpcDocument::from_value(Value)` constructor — **squash-merged**. That unblocks every Phase 3 service. The `from_value` test in `http_well_known.rs` pins the round-trip so it can't regress silently. ### Out-of-scope (intentional) `hero_service` still has the legacy sibling-crate SDK layout (`crates/hero_service_sdk` instead of the #70 `sdk/rust/` shape). The migration kept `client_crate_dir` pointing at the legacy spot — everything compiles, nothing breaks. A future cleanup PR can lift the layout if desired; it's not blocking #90. ### Next Phase 3a — `hero_logic` (base PR against `main`, not `development`).
Author
Owner

Phase 3 — all OSIS services migrated

3a — hero_logic

hero_logic PR #47 squash-merged against main (per the prompt — hero_logic uses main as its release branch, not development).

The complex one. 19 hand-written logicservice.* service methods, 4 hand-written sibling modules (engine, seed, services, tracing_layer), Python flow execution, built-in flow seeding, file-logger bridge — all preserved. Sibling modules now carry the //! @server-feature: marker (#93) so the regenerated lib.rs keeps them automatically; the build.rs's old post-write override hack is gone.

Smoke transcript:

Surface Result
GET /health {status: ok, service: hero_logic, version: 0.1.0}
GET /.well-known/heroservice.json canonical shape — protocol=openrpc, socket=rpc
GET /openrpc.json 19 methods, title LogicService
POST /rpc rpc.health {status: ok, trusted: true}
POST /rpc workflow.list 15+ seeded workflow SIDs
POST /rpc logicservice.flow_library_search 3 hits
Built-in flow seed service_agent created — no -32601 warnings
lab infocheck 3 crates clean, 0 findings

Second framework gap surfaced + closed

The first attempt failed with -32601 Method not found on every seed call. Root cause: the rpc2_adapter was registering service-method wire names with their PascalCase service prefix verbatim from the openrpc spec (LogicService.example_upsert), but every existing client sends lowercase, and the codegen's dispatch match arms compare against lowercase. OServer used to lowercase before dispatch. Fix:

  • hero_rpc PR #100rpc2_adapter lowercases service-method wire names at registration time. New test pins the contract. Merged before this PR.

3b — hero_compute

No migration needed. Inventoried the workspace:

  • crates/hero_compute_server is a skeleton stub (std::thread::park() loop) — no RPC code yet, no OServer dep.
  • crates/my_compute_{explorer,mos,zos,playground}_server all use hero_rpc_osis::rpc::AxumRpcServer directly (a separate RPC stack), not OServer. AxumRpcServer is still alive in hero_rpc post-Phase-1, so nothing breaks.

Verified by cargo check --workspace on origin/developmentall 14 crates build clean. Closing as no-op.

3c — hero_db

No migration needed. hero_db_server is a RESP/Redis-protocol server with an OpenRPC management surface built directly on hero_rpc_openrpc + hero_rpc_derive — no OServer, no hero_rpc_osis. Verified by cargo check --workspace: all 7 crates build clean. Closing as no-op.

Phase 3 verdict

  • 1 of 3 actually needed migration (hero_logic) — the others were either stubs or already on a non-OServer path.
  • 2 framework gaps surfaced + fixed inline in this same session: OpenRpcDocument::from_value (#99), rpc2_adapter service-method case-folding (#100).
  • Every OSIS service in lhumina_code (other than hero_proc, which has its own RPC stack) now runs on hero_rpc2 dispatch.

Next

Phase 4 — hero_rpc cleanup (audit any remaining OServer references; the legacy rust_rpc.rs emitter was already deleted in Phase 1 along with bin.rs + rust_sdk.rs; verify recipe_server still regenerates cleanly).

## Phase 3 — all OSIS services migrated ### 3a — `hero_logic` [`hero_logic` PR #47](https://forge.ourworld.tf/lhumina_code/hero_logic/pulls/47) **squash-merged** against `main` (per the prompt — hero_logic uses `main` as its release branch, not `development`). The complex one. 19 hand-written `logicservice.*` service methods, 4 hand-written sibling modules (`engine`, `seed`, `services`, `tracing_layer`), Python flow execution, built-in flow seeding, file-logger bridge — all preserved. Sibling modules now carry the `//! @server-feature:` marker (#93) so the regenerated `lib.rs` keeps them automatically; the build.rs's old post-write override hack is gone. Smoke transcript: | Surface | Result | |---|---| | `GET /health` | `{status: ok, service: hero_logic, version: 0.1.0}` | | `GET /.well-known/heroservice.json` | canonical shape — `protocol=openrpc`, `socket=rpc` | | `GET /openrpc.json` | **19 methods**, title `LogicService` | | `POST /rpc rpc.health` | `{status: ok, trusted: true}` | | `POST /rpc workflow.list` | 15+ seeded workflow SIDs | | `POST /rpc logicservice.flow_library_search` | 3 hits | | Built-in flow seed | `service_agent` created — **no -32601 warnings** | | `lab infocheck` | 3 crates clean, 0 findings | #### Second framework gap surfaced + closed The first attempt failed with `-32601 Method not found` on every seed call. Root cause: the `rpc2_adapter` was registering service-method wire names with their PascalCase service prefix verbatim from the openrpc spec (`LogicService.example_upsert`), but every existing client sends lowercase, and the codegen's dispatch `match` arms compare against lowercase. OServer used to lowercase before dispatch. Fix: - [hero_rpc PR #100](https://forge.ourworld.tf/lhumina_code/hero_rpc/pulls/100) — `rpc2_adapter` lowercases service-method wire names at registration time. New test pins the contract. **Merged** before this PR. ### 3b — `hero_compute` **No migration needed.** Inventoried the workspace: - `crates/hero_compute_server` is a skeleton stub (`std::thread::park()` loop) — no RPC code yet, no OServer dep. - `crates/my_compute_{explorer,mos,zos,playground}_server` all use `hero_rpc_osis::rpc::AxumRpcServer` directly (a separate RPC stack), not OServer. AxumRpcServer is still alive in hero_rpc post-Phase-1, so nothing breaks. Verified by `cargo check --workspace` on `origin/development` — **all 14 crates build clean**. Closing as no-op. ### 3c — `hero_db` **No migration needed.** `hero_db_server` is a RESP/Redis-protocol server with an OpenRPC management surface built directly on `hero_rpc_openrpc` + `hero_rpc_derive` — no OServer, no `hero_rpc_osis`. Verified by `cargo check --workspace`: **all 7 crates build clean**. Closing as no-op. ### Phase 3 verdict - 1 of 3 actually needed migration (hero_logic) — the others were either stubs or already on a non-OServer path. - 2 framework gaps surfaced + fixed inline in this same session: `OpenRpcDocument::from_value` ([#99](https://forge.ourworld.tf/lhumina_code/hero_rpc/pulls/99)), `rpc2_adapter` service-method case-folding ([#100](https://forge.ourworld.tf/lhumina_code/hero_rpc/pulls/100)). - Every OSIS service in `lhumina_code` (other than `hero_proc`, which has its own RPC stack) now runs on `hero_rpc2` dispatch. ### Next Phase 4 — hero_rpc cleanup (audit any remaining OServer references; the legacy `rust_rpc.rs` emitter was already deleted in Phase 1 along with `bin.rs` + `rust_sdk.rs`; verify recipe_server still regenerates cleanly).
Author
Owner

#90 closed — every OSIS service now runs on hero_rpc2

All four phases merged. OServer is gone, the rpc2_adapter bridges every generated OSIS handler into hero_rpc2's UDS HTTP transport, and the legacy Rust client emitter is deleted. Roll-up:

Per-PR breakdown

Repo Phase PR Status
hero_rpc 1 — delete OServer + flip scaffolder PR #97 merged 51c306e
hero_rpc OpenRpcDocument::from_value framework gap PR #99 merged c906794
hero_service 2 — first OSIS service on rpc2 PR #5 merged
hero_rpc — rpc2_adapter lowercase wire names PR #100 merged da30b07
hero_logic 3a — biggest service (19 service methods, 4 sibling modules, Python flow engine) PR #47 merged
hero_compute 3b — no-op (hero_compute_server is a stub; my_compute_* uses AxumRpcServer) verified cargo check --workspace clean
hero_db 3c — no-op (uses hero_rpc_openrpc directly, not OServer/osis) verified cargo check --workspace clean
hero_rpc 4 — delete legacy Rust client emitter + rpc2_sdk toggle PR #102 merged

Net deletion

  • crates/server/ — 3,362 LoC (OServer + ACL + ContextRegistry + git_sync + context./domain. RPC + seed_from_dir + inspector_ui). Phase 1.
  • crates/generator/src/build/emit/{rust_sdk,bin}.rs — legacy SDK emitter + per-domain bin orchestrator. Phase 1.
  • crates/generator/src/generate/rust_rpc.rs — legacy osis_client_generated.rs per-domain emitter. Phase 4.
  • crates/generator/src/rust/rust_client.rs — the actual client codegen body. Phase 4.
  • Five OschemaBuildConfig config-flag pairs + builder methods (generate_rust_sdk, with_rust_sdk(), bin_prefix/bin_git_url/bin_skip/bin_ui_builder, single_bin/bin_companions, generate_rpc2_sdk/with_hero_rpc2_sdk()/without_hero_rpc2_sdk()).
  • Three Generator builder methods + four config fields (client, client_only, client_types_crate, generate_client/client_flat/client_external_types).

Net additions

  • crates/hero_rpc2/src/transport/http.rs: routes POST /rpc + serves the three mandatory GET endpoints per hero_sockets skill. ServiceInfo + with_service_info + auto-registered rpc.health builtin.
  • crates/hero_rpc2/src/discover.rs: OpenRpcDocument::from_value(Value) -> Self for codegen-built specs.
  • crates/osis/src/rpc/rpc2_adapter.rs: bridges Arc<A: OsisAppRpcHandler>jsonrpsee::RpcModule (CRUD-by-typename + service methods + lowercased wire names + HeroRequestContext → RequestContext projection + typed-error mapping).
  • Scaffolder's emitted main.rs rewritten to drive ServerBuilder::serve_http directly, no OServer.

Smoke transcripts (canonical, from each migrated service)

hero_service (PR #5):

  • GET /health{status:ok, service:hero_service, version:0.1.0}
  • GET /.well-known/heroservice.json → canonical shape, protocol=openrpc, socket=rpc
  • GET /openrpc.json → 7 methods, byte-identical to docs/openrpc.json
  • POST /rpc rpc.health{status:ok, trusted:true}
  • POST /rpc servicecatalog.list[] (empty catalog, storage clean)
  • lab infocheck → 2 crates clean, 0 findings

hero_logic (PR #47):

  • GET /openrpc.json → 19 methods, title LogicService
  • POST /rpc rpc.health{status:ok, trusted:true}
  • POST /rpc workflow.list → 15+ seeded SIDs
  • POST /rpc logicservice.flow_library_search {query:"agent"} → 3 hits
  • Built-in Python-flow seed completes, zero -32601 Method not found warnings (the failure mode that surfaced PR #100)
  • lab infocheck → 3 crates clean, 0 findings

Phase 4 verification (hero_rpc workspace):

  • cargo build --workspace clean.
  • cargo test --workspace --lib --bins: 279 tests passing across hero_rpc2 / oschema / osis / generator.
  • cargo test -p hero_rpc2 --features uds-http,discover --tests: 27 passing (incl. http_well_known + from_value round-trip).
  • cargo test -p hero_rpc_osis --features rpc rpc2_adapter: 8 passing (incl. the lowercase-wire-name pin from PR #100).
  • example/recipe_server: builds clean; regen produces no diff vs prior output other than a socket-path catch-up to the canonical hero_sockets shape.

Pre-existing dev-tip bit-rot left for separate cleanup

Not caused by #90 — flagged so they don't get lost:

  • crates/osis/examples/basic/{1_generate_code,2_use_models,4_custom_rpc_methods,5_start_server,6_rpc_test}.rs reference removed APIs (Generator import, DBTyped::new_with_index, reqwest).
  • crates/derive/tests/sse_proxy.rs missing herolib_core crate dep.

Acceptance gate (from the issue body)

Criterion Status
Every OSIS service in lhumina_code/ (except hero_proc) on hero_rpc2 dispatch
OServer deleted from hero_rpc/crates/server/ Phase 1
Legacy rust_rpc.rs emitter deleted; scaffolder only emits the hero_rpc2 trait (Phase 1 deleted emit/rust_sdk.rs + emit/bin.rs; Phase 4 deleted generate/rust_rpc.rs + rust/rust_client.rs + the toggle)
recipe_sdk_rpc2 parallel example consolidated into recipe_server Phase 1
hero_service_scaffold.md describes only the hero_rpc2 path — (skill update is a separate hero_skills concern)
Every service's smoke test passes on the §7 contract incl. system.ping (now rpc.health)

Closing.

## #90 closed — every OSIS service now runs on hero_rpc2 All four phases merged. OServer is gone, the rpc2_adapter bridges every generated OSIS handler into hero_rpc2's UDS HTTP transport, and the legacy Rust client emitter is deleted. Roll-up: ### Per-PR breakdown | Repo | Phase | PR | Status | |---|---|---|---| | hero_rpc | 1 — delete OServer + flip scaffolder | [PR #97](https://forge.ourworld.tf/lhumina_code/hero_rpc/pulls/97) | merged `51c306e` | | hero_rpc | — `OpenRpcDocument::from_value` framework gap | [PR #99](https://forge.ourworld.tf/lhumina_code/hero_rpc/pulls/99) | merged `c906794` | | hero_service | 2 — first OSIS service on rpc2 | [PR #5](https://forge.ourworld.tf/lhumina_code/hero_service/pulls/5) | merged | | hero_rpc | — rpc2_adapter lowercase wire names | [PR #100](https://forge.ourworld.tf/lhumina_code/hero_rpc/pulls/100) | merged `da30b07` | | hero_logic | 3a — biggest service (19 service methods, 4 sibling modules, Python flow engine) | [PR #47](https://forge.ourworld.tf/lhumina_code/hero_logic/pulls/47) | merged | | hero_compute | 3b — no-op (hero_compute_server is a stub; my_compute_* uses AxumRpcServer) | — | verified `cargo check --workspace` clean | | hero_db | 3c — no-op (uses hero_rpc_openrpc directly, not OServer/osis) | — | verified `cargo check --workspace` clean | | hero_rpc | 4 — delete legacy Rust client emitter + rpc2_sdk toggle | [PR #102](https://forge.ourworld.tf/lhumina_code/hero_rpc/pulls/102) | merged | ### Net deletion - `crates/server/` — 3,362 LoC (OServer + ACL + ContextRegistry + git_sync + context.*/domain.* RPC + seed_from_dir + inspector_ui). Phase 1. - `crates/generator/src/build/emit/{rust_sdk,bin}.rs` — legacy SDK emitter + per-domain bin orchestrator. Phase 1. - `crates/generator/src/generate/rust_rpc.rs` — legacy `osis_client_generated.rs` per-domain emitter. Phase 4. - `crates/generator/src/rust/rust_client.rs` — the actual client codegen body. Phase 4. - Five `OschemaBuildConfig` config-flag pairs + builder methods (`generate_rust_sdk`, `with_rust_sdk()`, `bin_prefix`/`bin_git_url`/`bin_skip`/`bin_ui_builder`, `single_bin`/`bin_companions`, `generate_rpc2_sdk`/`with_hero_rpc2_sdk()`/`without_hero_rpc2_sdk()`). - Three `Generator` builder methods + four config fields (`client`, `client_only`, `client_types_crate`, `generate_client`/`client_flat`/`client_external_types`). ### Net additions - `crates/hero_rpc2/src/transport/http.rs`: routes `POST /rpc` + serves the three mandatory `GET` endpoints per `hero_sockets` skill. `ServiceInfo` + `with_service_info` + auto-registered `rpc.health` builtin. - `crates/hero_rpc2/src/discover.rs`: `OpenRpcDocument::from_value(Value) -> Self` for codegen-built specs. - `crates/osis/src/rpc/rpc2_adapter.rs`: bridges `Arc<A: OsisAppRpcHandler>` → `jsonrpsee::RpcModule` (CRUD-by-typename + service methods + lowercased wire names + `HeroRequestContext → RequestContext` projection + typed-error mapping). - Scaffolder's emitted `main.rs` rewritten to drive `ServerBuilder::serve_http` directly, no OServer. ### Smoke transcripts (canonical, from each migrated service) **hero_service (PR #5):** - `GET /health` → `{status:ok, service:hero_service, version:0.1.0}` - `GET /.well-known/heroservice.json` → canonical shape, `protocol=openrpc`, `socket=rpc` - `GET /openrpc.json` → 7 methods, byte-identical to `docs/openrpc.json` - `POST /rpc rpc.health` → `{status:ok, trusted:true}` - `POST /rpc servicecatalog.list` → `[]` (empty catalog, storage clean) - `lab infocheck` → 2 crates clean, 0 findings **hero_logic (PR #47):** - `GET /openrpc.json` → 19 methods, title `LogicService` - `POST /rpc rpc.health` → `{status:ok, trusted:true}` - `POST /rpc workflow.list` → 15+ seeded SIDs - `POST /rpc logicservice.flow_library_search {query:"agent"}` → 3 hits - Built-in Python-flow seed completes, **zero `-32601 Method not found` warnings** (the failure mode that surfaced PR #100) - `lab infocheck` → 3 crates clean, 0 findings **Phase 4 verification (hero_rpc workspace):** - `cargo build --workspace` clean. - `cargo test --workspace --lib --bins`: 279 tests passing across hero_rpc2 / oschema / osis / generator. - `cargo test -p hero_rpc2 --features uds-http,discover --tests`: 27 passing (incl. `http_well_known` + `from_value` round-trip). - `cargo test -p hero_rpc_osis --features rpc rpc2_adapter`: 8 passing (incl. the lowercase-wire-name pin from PR #100). - `example/recipe_server`: builds clean; regen produces no diff vs prior output other than a socket-path catch-up to the canonical `hero_sockets` shape. ### Pre-existing dev-tip bit-rot left for separate cleanup Not caused by #90 — flagged so they don't get lost: - `crates/osis/examples/basic/{1_generate_code,2_use_models,4_custom_rpc_methods,5_start_server,6_rpc_test}.rs` reference removed APIs (`Generator` import, `DBTyped::new_with_index`, `reqwest`). - `crates/derive/tests/sse_proxy.rs` missing `herolib_core` crate dep. ### Acceptance gate (from the issue body) | Criterion | Status | |---|---| | Every OSIS service in `lhumina_code/` (except `hero_proc`) on hero_rpc2 dispatch | ✅ | | OServer deleted from `hero_rpc/crates/server/` | ✅ Phase 1 | | Legacy `rust_rpc.rs` emitter deleted; scaffolder only emits the hero_rpc2 trait | ✅ (Phase 1 deleted `emit/rust_sdk.rs` + `emit/bin.rs`; Phase 4 deleted `generate/rust_rpc.rs` + `rust/rust_client.rs` + the toggle) | | `recipe_sdk_rpc2` parallel example consolidated into `recipe_server` | ✅ Phase 1 | | `hero_service_scaffold.md` describes only the hero_rpc2 path | — (skill update is a separate hero_skills concern) | | Every service's smoke test passes on the §7 contract incl. `system.ping` (now `rpc.health`) | ✅ | Closing.
timur closed this issue 2026-05-20 23:54:23 +00:00
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_rpc#90
No description provided.