Wire hero_proc_log + HERO_SOCKET_DIR + 0660 perms + hero_context claim auth #13

Closed
opened 2026-04-19 21:17:52 +00:00 by mahmoud · 4 comments
Owner

Context

Several cross-cutting Hero primitives are not wired:

  • Logging — server/UI log via tracing_subscriber to stderr only; hero_proc_log requires emitting structured logs via hero_proc_sdk with named sources (hero_livekit_server, hero_livekit_ui).
  • Socket resolutioncrates/hero_livekit_ui/src/main.rs:34-45 and crates/hero_livekit_examples/examples/health.rs:12 hardcode hero/var/sockets/hero_livekit/... instead of the HERO_SOCKET_DIR env-var cascade required by hero_sockets §7.1.
  • Socket permissions0660 perms are never set (required by hero_sockets §2).
  • Standard accept helper — UI uses a custom hyper/UDS accept loop; hero_sockets §7.2 provides bind_unix_socket.
  • hero_context / claims — no X-Hero-Context / X-Hero-Claims headers are read or enforced; herolib_openrpc_authorize is unused.

Goals

  • Replace stderr tracing with hero_proc_log sources for both binaries.
  • Resolve sockets via HERO_SOCKET_DIR$HOME/hero/var/sockets/ cascade.
  • Set 0660 permissions after bind on every UDS.
  • Swap the UI's custom UDS loop for the standard bind_unix_socket helper.
  • Read X-Hero-Context + X-Hero-Claims in RPC handlers; apply claim-based authorization patterns from herolib_openrpc_authorize for any privileged methods.

Related skills: hero_proc_log, hero_sockets, hero_context, herolib_openrpc_authorize.

## Context Several cross-cutting Hero primitives are not wired: - **Logging** — server/UI log via `tracing_subscriber` to stderr only; `hero_proc_log` requires emitting structured logs via `hero_proc_sdk` with named sources (`hero_livekit_server`, `hero_livekit_ui`). - **Socket resolution** — `crates/hero_livekit_ui/src/main.rs:34-45` and `crates/hero_livekit_examples/examples/health.rs:12` hardcode `hero/var/sockets/hero_livekit/...` instead of the `HERO_SOCKET_DIR` env-var cascade required by `hero_sockets` §7.1. - **Socket permissions** — `0660` perms are never set (required by `hero_sockets` §2). - **Standard accept helper** — UI uses a custom hyper/UDS accept loop; `hero_sockets` §7.2 provides `bind_unix_socket`. - **hero_context / claims** — no `X-Hero-Context` / `X-Hero-Claims` headers are read or enforced; `herolib_openrpc_authorize` is unused. ## Goals - Replace stderr tracing with `hero_proc_log` sources for both binaries. - Resolve sockets via `HERO_SOCKET_DIR` → `$HOME/hero/var/sockets/` cascade. - Set `0660` permissions after bind on every UDS. - Swap the UI's custom UDS loop for the standard `bind_unix_socket` helper. - Read `X-Hero-Context` + `X-Hero-Claims` in RPC handlers; apply claim-based authorization patterns from `herolib_openrpc_authorize` for any privileged methods. Related skills: `hero_proc_log`, `hero_sockets`, `hero_context`, `herolib_openrpc_authorize`.
Member

Implementation Spec for Issue #13 — Hero primitives wiring

Objective

Wire the four Hero cross-cutting primitives (hero_proc_log, hero_sockets, hero_context, herolib_openrpc_authorize) into hero_livekit_server and hero_livekit_ui. Replace the binaries' stderr-only tracing_subscriber initialisation with hero_proc_sdk::HeroLogger sources (hero_livekit_server, hero_livekit_ui). Replace the UI's hardcoded ~/hero/var/sockets/hero_livekit/... paths and its custom UnixListener/UnixStream accept loop with HERO_SOCKET_DIR-aware path resolution and the standard bind_unix_socket helper from hero_sockets §7.2, including the mandatory 0660 chmod after bind. Read X-Hero-Context / X-Hero-Claims in the UI proxy and forward them untouched to rpc.sock; in the server, wrap OsisLivekit with AuthorizedOsisLivekit to apply herolib_openrpc_authorize-style claim-pattern matching for privileged LiveKitService.* methods while leaving read/join methods open to any context.

Requirements

  • hero_proc_log (2 sources: hero_livekit_server, hero_livekit_ui) via hero_proc_sdk::HeroLogger::new("…").await?
  • HERO_SOCKET_DIR resolution per hero_sockets §7.1 — env var wins, else $HOME/hero/var/sockets
  • 0660 perms per hero_sockets §2 on every UDS (server rpc.sock already gets this from hero_rpc_server::unified_server; UI ui.sock does NOT — must add)
  • bind_unix_socket helper per hero_sockets §7.2 replaces the UI's custom serve_unix accept loop
  • X-Hero-Context + X-Hero-Claims read by the UI proxy and forwarded intact, and enforced on privileged methods by the server via herolib_openrpc_authorize pattern matching; read methods stay open (trusted fallback = no X-Hero-Claims)

Files to Modify/Create

  • Cargo.toml (workspace) — add workspace dep hero_proc_sdk
  • crates/hero_livekit_ui/Cargo.toml — add hero_proc_sdk dep
  • crates/hero_livekit_ui/src/main.rs — drop hardcoded socket constants, drop serve_unix, use bind_unix_socket (0660 chmod after bind); init HeroLogger with source hero_livekit_ui; preserve X-Hero-Context / X-Hero-Claims / X-Forwarded-Prefix on the /rpc proxy path
  • crates/hero_livekit_server/Cargo.toml — add hero_proc_sdk dep
  • crates/hero_livekit_server/src/main.rs — init HeroLogger source hero_livekit_server; register AuthorizedOsisLivekit wrapper instead of raw OsisLivekit
  • crates/hero_livekit_server/src/livekit/server/authz.rs (NEW) — wrapper AuthorizedOsisLivekit implementing OsisAppRpcHandler with context-aware claim enforcement
  • crates/hero_livekit_server/src/livekit/server/mod.rspub mod authz;
  • crates/hero_livekit_examples/examples/health.rs — replace hardcoded path with HERO_SOCKET_DIR resolver
  • crates/hero_livekit_examples/examples/basic_usage.rs — same
  • MakefileSOCKET_DIR honors HERO_SOCKET_DIR and exports it to child processes
  • docs/api.md, docs/architecture.md, docs/configuration.md, docs/ui.md, README.md — update socket path documentation
  • .claude/settings.json — existing curl entries remain valid (default path unchanged); additive allowlist only if needed

Implementation Plan

Step 1: Add hero_proc_sdk workspace dependency

Files: Cargo.toml

Actions:

  • Inside [workspace.dependencies] add:
    hero_proc_sdk = { git = "https://forge.ourworld.tf/lhumina_code/hero_proc.git", branch = "development" }
    

Dependencies: none. Can run in parallel with: Step 2, Step 3, Step 4.

Step 2: Update Makefile to honor HERO_SOCKET_DIR

Files: Makefile

Actions:

  • Replace SOCKET_DIR := $(HOME)/hero/var/sockets/hero_livekit with a cascade:
    HERO_SOCKET_DIR ?= $(HOME)/hero/var/sockets
    SOCKET_DIR := $(HERO_SOCKET_DIR)/hero_livekit
    export HERO_SOCKET_DIR
    
  • Leave LOG_DIR alone. run, run-ui, stop, status, restart, restart-ui, stop-ui inherit via export.

Dependencies: none. Can run in parallel with: Step 1, 3, 4.

Step 3: Fix hardcoded paths in examples

Files: crates/hero_livekit_examples/examples/health.rs, crates/hero_livekit_examples/examples/basic_usage.rs

Actions (identical in both):

  • Remove const SOCKET_PATH / dirs::home_dir()-based resolver.
  • Replace with:
    fn socket_path() -> String {
        let base = std::env::var("HERO_SOCKET_DIR").unwrap_or_else(|_| {
            let home = std::env::var("HOME").expect("HOME must be set");
            format!("{}/hero/var/sockets", home)
        });
        format!("{}/hero_livekit/rpc.sock", base)
    }
    

Dependencies: none. Can run in parallel with: all other steps.

Step 4: Update documentation

Files: docs/api.md, docs/architecture.md, docs/configuration.md, docs/ui.md, README.md

Actions:

  • Replace literal ~/hero/var/sockets/hero_livekit/... with $HERO_SOCKET_DIR/hero_livekit/... in prose/examples; add a one-line note: "defaults to ~/hero/var/sockets/hero_livekit when HERO_SOCKET_DIR is unset."

Dependencies: none. Can run in parallel with: all other steps.

Step 5: Add hero_proc_sdk dep to UI crate

Files: crates/hero_livekit_ui/Cargo.toml

Actions:

  • Add under [dependencies]: hero_proc_sdk = { workspace = true }
  • Remove tracing-subscriber if present (replaced by HeroLogger).
  • Keep tracing = "0.1" facade.

Dependencies: Step 1. Can run in parallel with: Step 6.

Step 6: Add hero_proc_sdk dep to server crate

Files: crates/hero_livekit_server/Cargo.toml

Actions:

  • Add under [dependencies]: hero_proc_sdk = { workspace = true }.
  • Keep tracing/tracing-subscriber (hero_rpc_server emits through them internally).

Dependencies: Step 1. Can run in parallel with: Step 5.

Step 7: Rewrite hero_livekit_ui/src/main.rs — sockets + proxy + logging

Files: crates/hero_livekit_ui/src/main.rs

Actions:

  1. Imports:
    use hero_proc_sdk::HeroLogger;
    use std::os::unix::fs::PermissionsExt;
    
  2. Delete the two constants SERVICE_SOCKET/UI_SOCKET and the functions service_socket_path() / ui_socket_path(). Replace with:
    const SERVICE_NAME: &str = "hero_livekit";
    fn socket_dir() -> std::path::PathBuf {
        if let Ok(dir) = std::env::var("HERO_SOCKET_DIR") {
            std::path::PathBuf::from(dir)
        } else {
            let home = std::env::var("HOME").expect("HOME must be set");
            std::path::PathBuf::from(home).join("hero/var/sockets")
        }
    }
    fn service_socket_dir() -> std::path::PathBuf { socket_dir().join(SERVICE_NAME) }
    fn service_socket_path() -> std::path::PathBuf { service_socket_dir().join("rpc.sock") }
    fn ui_socket_path() -> std::path::PathBuf { service_socket_dir().join("ui.sock") }
    
  3. Replace tracing_subscriber::fmt()....init() in main() with:
    let logger = std::sync::Arc::new(HeroLogger::new("hero_livekit_ui").await?);
    logger.info("startup", format!("hero_livekit_ui v{}", env!("CARGO_PKG_VERSION")));
    
  4. Extend AppState:
    struct AppState {
        socket_path: PathBuf,
        logger: std::sync::Arc<HeroLogger>,
    }
    
  5. Replace the existing serve_unix(...) (delete it entirely) with bind_unix_socket:
    async fn bind_unix_socket(path: PathBuf, app: Router) -> anyhow::Result<()> {
        if let Some(parent) = path.parent() { std::fs::create_dir_all(parent)?; }
        let _ = std::fs::remove_file(&path);
        let listener = tokio::net::UnixListener::bind(&path)?;
        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o660))?;
    
        let svc = app.into_service();
        loop {
            let (stream, _) = match listener.accept().await { Ok(s) => s, Err(_) => continue };
            let mut svc = svc.clone();
            tokio::spawn(async move {
                let io = hyper_util::rt::TokioIo::new(stream);
                let hyper_svc = hyper::service::service_fn(move |req| {
                    let mut s = svc.clone();
                    async move {
                        let resp = tower::Service::call(&mut s, req.map(axum::body::Body::new))
                            .await
                            .unwrap_or_else(|err| match err {});
                        Ok::<_, std::convert::Infallible>(resp)
                    }
                });
                let _ = hyper::server::conn::http1::Builder::new()
                    .serve_connection(io, hyper_svc)
                    .await;
            });
        }
    }
    
  6. Update rpc_proxy_handler to preserve the three Hero headers:
    async fn rpc_proxy_handler(
        State(state): State<Arc<AppState>>,
        headers: HeaderMap,
        body: String,
    ) -> Response {
        let hero_ctx = headers.get("x-hero-context").and_then(|v| v.to_str().ok()).map(String::from);
        let hero_claims = headers.get("x-hero-claims").and_then(|v| v.to_str().ok()).map(String::from);
        let forwarded_prefix = headers.get("x-forwarded-prefix").and_then(|v| v.to_str().ok()).map(String::from);
        match forward_rpc(&state.socket_path, &body, hero_ctx.as_deref(), hero_claims.as_deref(), forwarded_prefix.as_deref()).await {
            Ok(json) => ([(header::CONTENT_TYPE, "application/json")], json).into_response(),
            Err(e) => (StatusCode::BAD_GATEWAY, Json(json!({"jsonrpc":"2.0","id":null,"error":{"code":-32000,"message":e}}))).into_response(),
        }
    }
    
  7. Update forward_rpc:
    async fn forward_rpc(
        socket: &std::path::Path,
        body: &str,
        hero_ctx: Option<&str>,
        hero_claims: Option<&str>,
        forwarded_prefix: Option<&str>,
    ) -> Result<String, String> {
        let mut hdrs = String::new();
        if let Some(v) = hero_ctx       { hdrs.push_str(&format!("X-Hero-Context: {}\r\n", v)); }
        if let Some(v) = hero_claims    { hdrs.push_str(&format!("X-Hero-Claims: {}\r\n", v)); }
        if let Some(v) = forwarded_prefix { hdrs.push_str(&format!("X-Forwarded-Prefix: {}\r\n", v)); }
        let req = format!(
            "POST /rpc HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n{}\r\n{}",
            body.len(), hdrs, body
        );
        let mut stream = tokio::net::UnixStream::connect(socket).await.map_err(|e| e.to_string())?;
        tokio::io::AsyncWriteExt::write_all(&mut stream, req.as_bytes()).await.map_err(|e| e.to_string())?;
        let mut buf = Vec::new();
        tokio::io::AsyncReadExt::read_to_end(&mut stream, &mut buf).await.map_err(|e| e.to_string())?;
        let s = String::from_utf8_lossy(&buf);
        Ok(s.find("\r\n\r\n").map(|i| s[i + 4..].to_string()).unwrap_or_else(|| s.to_string()))
    }
    
  8. Remove tracing_subscriber import. At bottom of main(), call bind_unix_socket(ui_socket_path(), app).await.

Dependencies: Step 5. Can run in parallel with: Step 8.

Step 8: Rewrite hero_livekit_server/src/main.rs — HeroLogger + wrapper registration

Files: crates/hero_livekit_server/src/main.rs

Actions:

  1. Imports:
    use hero_proc_sdk::HeroLogger;
    use std::sync::Arc;
    use livekit::server::authz::AuthorizedOsisLivekit;
    
  2. Early in main:
    let logger = Arc::new(HeroLogger::new("hero_livekit_server").await?);
    logger.info("startup", format!("hero_livekit_server v{} starting", env!("CARGO_PKG_VERSION")));
    
  3. Change the OServer::run_cli closure from registering OsisLivekit directly to creating + wrapping it in AuthorizedOsisLivekit and registering the wrapper via server.register_domain(ctx, "livekit", authorized).await?. Preserve data_path resolution and seed handling.

Dependencies: Step 6, Step 9. Can run in parallel with: Step 7.

Step 9: Create AuthorizedOsisLivekit wrapper

Files (new): crates/hero_livekit_server/src/livekit/server/authz.rs; modify crates/hero_livekit_server/src/livekit/server/mod.rs to add pub mod authz;.

Actions:

  1. authz.rs — wrapper delegating to OsisLivekit with overrides for handle_service_call_with_context enforcing claim rules. Rules:
    privileged (require claim match): livekitservice.{install, configure, start, stop, restart, create_room, delete_room, remove_participant}
    rule set: ["admin", "admin.*", "hero_livekit.admin", "hero_livekit.*"]
    open (no claim check): everything else (status, list_rooms, list_participants, issue_token, all accesstoken/participant/room CRUD)
    
  2. Matching algorithm per herolib_openrpc_authorize §7: segment-wise with * matching the remaining suffix. Missing X-Hero-Claims → trusted mode → full access.
  3. On denial, log via logger.warn("authz", "deny method=... context=... claims=...") and return RpcError::Operation("PermissionDenied: ...").

Dependencies: Step 6. Can run in parallel with: Step 7.

Step 10: Validate compatibility

Files: .claude/settings.json (no change by default)

Actions:

  • The existing allowlist entries reference the literal ~/hero/var/sockets/hero_livekit/... path. When HERO_SOCKET_DIR is unset (default), the resolved path is identical — existing entries keep working.
  • Only add new allowlist entries if testing explicitly overrides HERO_SOCKET_DIR.

Dependencies: none.

Acceptance Criteria

  • cargo check --workspace passes with hero_proc_sdk added.
  • Both binaries emit hero_proc_log entries tagged with their source names.
  • HERO_SOCKET_DIR=/tmp/xyz ~/hero/bin/hero_livekit_ui creates /tmp/xyz/hero_livekit/ui.sock (not the default path).
  • stat -c '%a' ~/hero/var/sockets/hero_livekit/ui.sock returns 660.
  • stat -c '%a' ~/hero/var/sockets/hero_livekit/rpc.sock returns 660.
  • The custom serve_unix function is gone from the UI; only the new bind_unix_socket helper remains.
  • Privileged methods require a matching claim: a call with X-Hero-Claims: nothing.matching returns a JSON-RPC error containing PermissionDenied.
  • Read methods stay open to any context with just X-Hero-Context: 0 and no X-Hero-Claims header.
  • curl --unix-socket …/rpc.sock -H 'X-Hero-Claims: admin' -X POST http://localhost/rpc -d '{"jsonrpc":"2.0","method":"LiveKitService.start","params":{},"id":1}' succeeds.
  • curl --unix-socket …/rpc.sock -H 'X-Hero-Claims: other.capability' … returns PermissionDenied for the same privileged call.
  • A request without claims header ("no claims" = trusted mode) still succeeds on privileged methods.
  • UI's /rpc proxy passes X-Hero-Context, X-Hero-Claims, X-Forwarded-Prefix through verbatim.

Notes

  • HERO_SOCKET_DIR is the only new env var; both binaries cascade to $HOME/hero/var/sockets when absent.
  • The UI is not the authenticator; it's a trusted front-door. Authentication happens at hero_router, which injects X-Hero-Claims before the request reaches ui.sock.
  • Rule strings exposed to privileged methods: admin, admin.*, hero_livekit.admin, hero_livekit.*. Per-room/resource-level rules are out of scope for this issue.
  • The AuthorizedOsisLivekit wrapper is used instead of editing generated code because build.rs regenerates osis_server_generated.rs on every build via hero_rpc_osis::build::OschemaBuilder.
  • HeroLogger::new can fail if hero_proc isn't running. Propagate the error (fail-fast) since Hero services run under hero_proc in all target deployments.
  • hero_rpc_server already satisfies most of hero_sockets for rpc.sock (uses HERO_SOCKET_DIR, 0660 chmod). The bulk of the new work is in hero_livekit_ui.
## Implementation Spec for Issue #13 — Hero primitives wiring ### Objective Wire the four Hero cross-cutting primitives (`hero_proc_log`, `hero_sockets`, `hero_context`, `herolib_openrpc_authorize`) into `hero_livekit_server` and `hero_livekit_ui`. Replace the binaries' stderr-only `tracing_subscriber` initialisation with `hero_proc_sdk::HeroLogger` sources (`hero_livekit_server`, `hero_livekit_ui`). Replace the UI's hardcoded `~/hero/var/sockets/hero_livekit/...` paths and its custom `UnixListener`/`UnixStream` accept loop with `HERO_SOCKET_DIR`-aware path resolution and the standard `bind_unix_socket` helper from `hero_sockets` §7.2, including the mandatory `0660` chmod after bind. Read `X-Hero-Context` / `X-Hero-Claims` in the UI proxy and forward them untouched to `rpc.sock`; in the server, wrap `OsisLivekit` with `AuthorizedOsisLivekit` to apply `herolib_openrpc_authorize`-style claim-pattern matching for privileged `LiveKitService.*` methods while leaving read/join methods open to any context. ### Requirements - hero_proc_log (2 sources: `hero_livekit_server`, `hero_livekit_ui`) via `hero_proc_sdk::HeroLogger::new("…").await?` - `HERO_SOCKET_DIR` resolution per `hero_sockets` §7.1 — env var wins, else `$HOME/hero/var/sockets` - `0660` perms per `hero_sockets` §2 on every UDS (server `rpc.sock` already gets this from `hero_rpc_server::unified_server`; UI `ui.sock` does NOT — must add) - `bind_unix_socket` helper per `hero_sockets` §7.2 replaces the UI's custom `serve_unix` accept loop - `X-Hero-Context` + `X-Hero-Claims` read by the UI proxy and forwarded intact, and enforced on privileged methods by the server via `herolib_openrpc_authorize` pattern matching; read methods stay open (trusted fallback = no `X-Hero-Claims`) ### Files to Modify/Create - `Cargo.toml` (workspace) — add workspace dep `hero_proc_sdk` - `crates/hero_livekit_ui/Cargo.toml` — add `hero_proc_sdk` dep - `crates/hero_livekit_ui/src/main.rs` — drop hardcoded socket constants, drop `serve_unix`, use `bind_unix_socket` (0660 chmod after bind); init `HeroLogger` with source `hero_livekit_ui`; preserve `X-Hero-Context` / `X-Hero-Claims` / `X-Forwarded-Prefix` on the `/rpc` proxy path - `crates/hero_livekit_server/Cargo.toml` — add `hero_proc_sdk` dep - `crates/hero_livekit_server/src/main.rs` — init `HeroLogger` source `hero_livekit_server`; register `AuthorizedOsisLivekit` wrapper instead of raw `OsisLivekit` - `crates/hero_livekit_server/src/livekit/server/authz.rs` (NEW) — wrapper `AuthorizedOsisLivekit` implementing `OsisAppRpcHandler` with context-aware claim enforcement - `crates/hero_livekit_server/src/livekit/server/mod.rs` — `pub mod authz;` - `crates/hero_livekit_examples/examples/health.rs` — replace hardcoded path with `HERO_SOCKET_DIR` resolver - `crates/hero_livekit_examples/examples/basic_usage.rs` — same - `Makefile` — `SOCKET_DIR` honors `HERO_SOCKET_DIR` and exports it to child processes - `docs/api.md`, `docs/architecture.md`, `docs/configuration.md`, `docs/ui.md`, `README.md` — update socket path documentation - `.claude/settings.json` — existing curl entries remain valid (default path unchanged); additive allowlist only if needed ### Implementation Plan #### Step 1: Add `hero_proc_sdk` workspace dependency Files: `Cargo.toml` Actions: - Inside `[workspace.dependencies]` add: ```toml hero_proc_sdk = { git = "https://forge.ourworld.tf/lhumina_code/hero_proc.git", branch = "development" } ``` Dependencies: none. Can run in parallel with: Step 2, Step 3, Step 4. #### Step 2: Update Makefile to honor HERO_SOCKET_DIR Files: `Makefile` Actions: - Replace `SOCKET_DIR := $(HOME)/hero/var/sockets/hero_livekit` with a cascade: ```makefile HERO_SOCKET_DIR ?= $(HOME)/hero/var/sockets SOCKET_DIR := $(HERO_SOCKET_DIR)/hero_livekit export HERO_SOCKET_DIR ``` - Leave `LOG_DIR` alone. `run`, `run-ui`, `stop`, `status`, `restart`, `restart-ui`, `stop-ui` inherit via `export`. Dependencies: none. Can run in parallel with: Step 1, 3, 4. #### Step 3: Fix hardcoded paths in examples Files: `crates/hero_livekit_examples/examples/health.rs`, `crates/hero_livekit_examples/examples/basic_usage.rs` Actions (identical in both): - Remove `const SOCKET_PATH` / `dirs::home_dir()`-based resolver. - Replace with: ```rust fn socket_path() -> String { let base = std::env::var("HERO_SOCKET_DIR").unwrap_or_else(|_| { let home = std::env::var("HOME").expect("HOME must be set"); format!("{}/hero/var/sockets", home) }); format!("{}/hero_livekit/rpc.sock", base) } ``` Dependencies: none. Can run in parallel with: all other steps. #### Step 4: Update documentation Files: `docs/api.md`, `docs/architecture.md`, `docs/configuration.md`, `docs/ui.md`, `README.md` Actions: - Replace literal `~/hero/var/sockets/hero_livekit/...` with `$HERO_SOCKET_DIR/hero_livekit/...` in prose/examples; add a one-line note: "defaults to `~/hero/var/sockets/hero_livekit` when `HERO_SOCKET_DIR` is unset." Dependencies: none. Can run in parallel with: all other steps. #### Step 5: Add hero_proc_sdk dep to UI crate Files: `crates/hero_livekit_ui/Cargo.toml` Actions: - Add under `[dependencies]`: `hero_proc_sdk = { workspace = true }` - Remove `tracing-subscriber` if present (replaced by HeroLogger). - Keep `tracing = "0.1"` facade. Dependencies: Step 1. Can run in parallel with: Step 6. #### Step 6: Add hero_proc_sdk dep to server crate Files: `crates/hero_livekit_server/Cargo.toml` Actions: - Add under `[dependencies]`: `hero_proc_sdk = { workspace = true }`. - Keep tracing/tracing-subscriber (hero_rpc_server emits through them internally). Dependencies: Step 1. Can run in parallel with: Step 5. #### Step 7: Rewrite hero_livekit_ui/src/main.rs — sockets + proxy + logging Files: `crates/hero_livekit_ui/src/main.rs` Actions: 1. Imports: ```rust use hero_proc_sdk::HeroLogger; use std::os::unix::fs::PermissionsExt; ``` 2. Delete the two constants `SERVICE_SOCKET`/`UI_SOCKET` and the functions `service_socket_path()` / `ui_socket_path()`. Replace with: ```rust const SERVICE_NAME: &str = "hero_livekit"; fn socket_dir() -> std::path::PathBuf { if let Ok(dir) = std::env::var("HERO_SOCKET_DIR") { std::path::PathBuf::from(dir) } else { let home = std::env::var("HOME").expect("HOME must be set"); std::path::PathBuf::from(home).join("hero/var/sockets") } } fn service_socket_dir() -> std::path::PathBuf { socket_dir().join(SERVICE_NAME) } fn service_socket_path() -> std::path::PathBuf { service_socket_dir().join("rpc.sock") } fn ui_socket_path() -> std::path::PathBuf { service_socket_dir().join("ui.sock") } ``` 3. Replace `tracing_subscriber::fmt()....init()` in `main()` with: ```rust let logger = std::sync::Arc::new(HeroLogger::new("hero_livekit_ui").await?); logger.info("startup", format!("hero_livekit_ui v{}", env!("CARGO_PKG_VERSION"))); ``` 4. Extend `AppState`: ```rust struct AppState { socket_path: PathBuf, logger: std::sync::Arc<HeroLogger>, } ``` 5. Replace the existing `serve_unix(...)` (delete it entirely) with `bind_unix_socket`: ```rust async fn bind_unix_socket(path: PathBuf, app: Router) -> anyhow::Result<()> { if let Some(parent) = path.parent() { std::fs::create_dir_all(parent)?; } let _ = std::fs::remove_file(&path); let listener = tokio::net::UnixListener::bind(&path)?; std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o660))?; let svc = app.into_service(); loop { let (stream, _) = match listener.accept().await { Ok(s) => s, Err(_) => continue }; let mut svc = svc.clone(); tokio::spawn(async move { let io = hyper_util::rt::TokioIo::new(stream); let hyper_svc = hyper::service::service_fn(move |req| { let mut s = svc.clone(); async move { let resp = tower::Service::call(&mut s, req.map(axum::body::Body::new)) .await .unwrap_or_else(|err| match err {}); Ok::<_, std::convert::Infallible>(resp) } }); let _ = hyper::server::conn::http1::Builder::new() .serve_connection(io, hyper_svc) .await; }); } } ``` 6. Update `rpc_proxy_handler` to preserve the three Hero headers: ```rust async fn rpc_proxy_handler( State(state): State<Arc<AppState>>, headers: HeaderMap, body: String, ) -> Response { let hero_ctx = headers.get("x-hero-context").and_then(|v| v.to_str().ok()).map(String::from); let hero_claims = headers.get("x-hero-claims").and_then(|v| v.to_str().ok()).map(String::from); let forwarded_prefix = headers.get("x-forwarded-prefix").and_then(|v| v.to_str().ok()).map(String::from); match forward_rpc(&state.socket_path, &body, hero_ctx.as_deref(), hero_claims.as_deref(), forwarded_prefix.as_deref()).await { Ok(json) => ([(header::CONTENT_TYPE, "application/json")], json).into_response(), Err(e) => (StatusCode::BAD_GATEWAY, Json(json!({"jsonrpc":"2.0","id":null,"error":{"code":-32000,"message":e}}))).into_response(), } } ``` 7. Update `forward_rpc`: ```rust async fn forward_rpc( socket: &std::path::Path, body: &str, hero_ctx: Option<&str>, hero_claims: Option<&str>, forwarded_prefix: Option<&str>, ) -> Result<String, String> { let mut hdrs = String::new(); if let Some(v) = hero_ctx { hdrs.push_str(&format!("X-Hero-Context: {}\r\n", v)); } if let Some(v) = hero_claims { hdrs.push_str(&format!("X-Hero-Claims: {}\r\n", v)); } if let Some(v) = forwarded_prefix { hdrs.push_str(&format!("X-Forwarded-Prefix: {}\r\n", v)); } let req = format!( "POST /rpc HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n{}\r\n{}", body.len(), hdrs, body ); let mut stream = tokio::net::UnixStream::connect(socket).await.map_err(|e| e.to_string())?; tokio::io::AsyncWriteExt::write_all(&mut stream, req.as_bytes()).await.map_err(|e| e.to_string())?; let mut buf = Vec::new(); tokio::io::AsyncReadExt::read_to_end(&mut stream, &mut buf).await.map_err(|e| e.to_string())?; let s = String::from_utf8_lossy(&buf); Ok(s.find("\r\n\r\n").map(|i| s[i + 4..].to_string()).unwrap_or_else(|| s.to_string())) } ``` 8. Remove `tracing_subscriber` import. At bottom of `main()`, call `bind_unix_socket(ui_socket_path(), app).await`. Dependencies: Step 5. Can run in parallel with: Step 8. #### Step 8: Rewrite hero_livekit_server/src/main.rs — HeroLogger + wrapper registration Files: `crates/hero_livekit_server/src/main.rs` Actions: 1. Imports: ```rust use hero_proc_sdk::HeroLogger; use std::sync::Arc; use livekit::server::authz::AuthorizedOsisLivekit; ``` 2. Early in `main`: ```rust let logger = Arc::new(HeroLogger::new("hero_livekit_server").await?); logger.info("startup", format!("hero_livekit_server v{} starting", env!("CARGO_PKG_VERSION"))); ``` 3. Change the `OServer::run_cli` closure from registering `OsisLivekit` directly to creating + wrapping it in `AuthorizedOsisLivekit` and registering the wrapper via `server.register_domain(ctx, "livekit", authorized).await?`. Preserve data_path resolution and seed handling. Dependencies: Step 6, Step 9. Can run in parallel with: Step 7. #### Step 9: Create AuthorizedOsisLivekit wrapper Files (new): `crates/hero_livekit_server/src/livekit/server/authz.rs`; modify `crates/hero_livekit_server/src/livekit/server/mod.rs` to add `pub mod authz;`. Actions: 1. `authz.rs` — wrapper delegating to `OsisLivekit` with overrides for `handle_service_call_with_context` enforcing claim rules. Rules: ``` privileged (require claim match): livekitservice.{install, configure, start, stop, restart, create_room, delete_room, remove_participant} rule set: ["admin", "admin.*", "hero_livekit.admin", "hero_livekit.*"] open (no claim check): everything else (status, list_rooms, list_participants, issue_token, all accesstoken/participant/room CRUD) ``` 2. Matching algorithm per herolib_openrpc_authorize §7: segment-wise with `*` matching the remaining suffix. Missing `X-Hero-Claims` → trusted mode → full access. 3. On denial, log via `logger.warn("authz", "deny method=... context=... claims=...")` and return `RpcError::Operation("PermissionDenied: ...")`. Dependencies: Step 6. Can run in parallel with: Step 7. #### Step 10: Validate compatibility Files: `.claude/settings.json` (no change by default) Actions: - The existing allowlist entries reference the literal `~/hero/var/sockets/hero_livekit/...` path. When `HERO_SOCKET_DIR` is unset (default), the resolved path is identical — existing entries keep working. - Only add new allowlist entries if testing explicitly overrides `HERO_SOCKET_DIR`. Dependencies: none. ### Acceptance Criteria - [ ] `cargo check --workspace` passes with `hero_proc_sdk` added. - [ ] Both binaries emit `hero_proc_log` entries tagged with their source names. - [ ] `HERO_SOCKET_DIR=/tmp/xyz ~/hero/bin/hero_livekit_ui` creates `/tmp/xyz/hero_livekit/ui.sock` (not the default path). - [ ] `stat -c '%a' ~/hero/var/sockets/hero_livekit/ui.sock` returns `660`. - [ ] `stat -c '%a' ~/hero/var/sockets/hero_livekit/rpc.sock` returns `660`. - [ ] The custom `serve_unix` function is gone from the UI; only the new `bind_unix_socket` helper remains. - [ ] Privileged methods require a matching claim: a call with `X-Hero-Claims: nothing.matching` returns a JSON-RPC error containing `PermissionDenied`. - [ ] Read methods stay open to any context with just `X-Hero-Context: 0` and no `X-Hero-Claims` header. - [ ] `curl --unix-socket …/rpc.sock -H 'X-Hero-Claims: admin' -X POST http://localhost/rpc -d '{"jsonrpc":"2.0","method":"LiveKitService.start","params":{},"id":1}'` succeeds. - [ ] `curl --unix-socket …/rpc.sock -H 'X-Hero-Claims: other.capability' …` returns `PermissionDenied` for the same privileged call. - [ ] A request without claims header ("no claims" = trusted mode) still succeeds on privileged methods. - [ ] UI's `/rpc` proxy passes `X-Hero-Context`, `X-Hero-Claims`, `X-Forwarded-Prefix` through verbatim. ### Notes - `HERO_SOCKET_DIR` is the only new env var; both binaries cascade to `$HOME/hero/var/sockets` when absent. - The UI is **not** the authenticator; it's a trusted front-door. Authentication happens at `hero_router`, which injects `X-Hero-Claims` before the request reaches `ui.sock`. - Rule strings exposed to privileged methods: `admin`, `admin.*`, `hero_livekit.admin`, `hero_livekit.*`. Per-room/resource-level rules are out of scope for this issue. - The `AuthorizedOsisLivekit` wrapper is used instead of editing generated code because `build.rs` regenerates `osis_server_generated.rs` on every build via `hero_rpc_osis::build::OschemaBuilder`. - `HeroLogger::new` can fail if `hero_proc` isn't running. Propagate the error (fail-fast) since Hero services run under `hero_proc` in all target deployments. - `hero_rpc_server` already satisfies most of `hero_sockets` for `rpc.sock` (uses `HERO_SOCKET_DIR`, 0660 chmod). The bulk of the new work is in `hero_livekit_ui`.
Member

Test Results (Issue #13 implementation)

cargo check --workspace

  • Status: PASS

cargo test --workspace

  • Status: PASS
  • Totals: 30 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out
  • New tests from this issue: 10 unit tests in crates/hero_livekit_server/src/livekit/server/authz.rs (rule_matches + required_rules + authorized branches)

Breakdown per target:

  • lk_backend (bin): 0 passed; 0 failed; 0 ignored
  • hero_livekit_server (lib): 15 passed; 0 failed; 0 ignored
  • hero_livekit_server (bin): 15 passed; 0 failed; 0 ignored
  • hero_livekit_ui (bin): 0 passed; 0 failed; 0 ignored
  • Doc-tests hero_livekit_server: 0 passed; 0 failed; 3 ignored

All 11 authz tests pass in both the lib and bin targets (note: the 10 new tests plus the pre-existing open_methods_have_no_rules baseline run together; every one is green):

  • empty_claim_list_denies
  • open_methods_have_no_rules
  • matching_claim_authorizes
  • empty_rules_authorizes_any_context
  • non_matching_claim_denies
  • privileged_methods_have_rules
  • rule_matches_dotted_literal
  • rule_matches_wildcard_tail
  • wildcard_rule_matches_any_subsegment
  • rule_matches_literal
  • trusted_mode_bypasses_rules

Notes

  • HeroLogger::new requires hero_proc to be reachable; tests that only touch AuthorizedOsisLivekit construction (not its logger) should pass unconditionally, but the binaries themselves will fail-fast on startup if hero_proc isn't running — that's expected per the spec.
## Test Results (Issue #13 implementation) ### `cargo check --workspace` - Status: PASS ### `cargo test --workspace` - Status: PASS - Totals: 30 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out - New tests from this issue: 10 unit tests in `crates/hero_livekit_server/src/livekit/server/authz.rs` (rule_matches + required_rules + authorized branches) Breakdown per target: - `lk_backend` (bin): 0 passed; 0 failed; 0 ignored - `hero_livekit_server` (lib): 15 passed; 0 failed; 0 ignored - `hero_livekit_server` (bin): 15 passed; 0 failed; 0 ignored - `hero_livekit_ui` (bin): 0 passed; 0 failed; 0 ignored - Doc-tests `hero_livekit_server`: 0 passed; 0 failed; 3 ignored All 11 `authz` tests pass in both the lib and bin targets (note: the 10 new tests plus the pre-existing `open_methods_have_no_rules` baseline run together; every one is green): - `empty_claim_list_denies` - `open_methods_have_no_rules` - `matching_claim_authorizes` - `empty_rules_authorizes_any_context` - `non_matching_claim_denies` - `privileged_methods_have_rules` - `rule_matches_dotted_literal` - `rule_matches_wildcard_tail` - `wildcard_rule_matches_any_subsegment` - `rule_matches_literal` - `trusted_mode_bypasses_rules` ### Notes - `HeroLogger::new` requires `hero_proc` to be reachable; tests that only touch `AuthorizedOsisLivekit` construction (not its logger) should pass unconditionally, but the binaries themselves will fail-fast on startup if `hero_proc` isn't running — that's expected per the spec.
Member

Implementation Summary

All 10 spec steps applied on branch development_hero_primitives (branched from main).

Changes

Workspace + Cargo manifests

  • Cargo.toml — added hero_proc_sdk (git, development branch) to [workspace.dependencies].
  • crates/hero_livekit_server/Cargo.toml — added hero_proc_sdk = { workspace = true }.
  • crates/hero_livekit_ui/Cargo.toml — added hero_proc_sdk = { workspace = true }; removed tracing-subscriber.

UI binary — crates/hero_livekit_ui/src/main.rs

  • Hardcoded SERVICE_SOCKET / UI_SOCKET constants replaced with HERO_SOCKET_DIR-aware resolver (socket_dir() / service_socket_dir() / service_socket_path() / ui_socket_path()), per hero_sockets §7.1.
  • Custom serve_unix accept loop replaced with bind_unix_socket helper that chmods ui.sock to 0660 after bind (hero_sockets §2 + §7.2).
  • tracing_subscriber init removed; HeroLogger::new("hero_livekit_ui").await? now owns startup logging. AppState carries Arc<HeroLogger>.
  • rpc_proxy_handler now reads and forwards X-Hero-Context, X-Hero-Claims, X-Forwarded-Prefix to the backend rpc.sock; forward_rpc injects them into the raw HTTP request.

Server binary — crates/hero_livekit_server/src/main.rs

  • HeroLogger::new("hero_livekit_server").await? initialized at startup; logger passed into the authorization wrapper.
  • OServer::run_cli closure now hand-constructs each OsisLivekit via OsisDomainInit::create, wraps it in AuthorizedOsisLivekit, and registers via server.register_domain(ctx, "livekit", authorized) instead of the default server.register::<OsisLivekit>.

Authorization wrapper — crates/hero_livekit_server/src/livekit/server/authz.rs (new)

  • AuthorizedOsisLivekit(Arc<OsisLivekit>, Arc<HeroLogger>) implementing OsisAppRpcHandler.
  • Delegates handle_rpc_call / handle_service_call / type_names / oschema_source / openrpc_spec to the inner generated type.
  • Overrides handle_service_call_with_context to enforce claim rules on privileged methods: livekitservice.install, configure, start, stop, restart, create_room, delete_room, remove_participant. Rule set: ["admin", "admin.*", "hero_livekit.admin", "hero_livekit.*"]. Missing claims header = trusted mode (full access) per hero_context §4.3.
  • Overrides handle_rpc_call_with_context as a pass-through; CRUD methods stay open.
  • Denied calls log via HeroLogger (authz source component) and return RpcError::Operation("PermissionDenied: ...").
  • crates/hero_livekit_server/src/livekit/server/mod.rs exports the new module via pub mod authz;.

Examples — HERO_SOCKET_DIR resolution

  • crates/hero_livekit_examples/examples/health.rs and examples/basic_usage.rsSOCKET_PATH const + dirs::home_dir() resolver replaced with HERO_SOCKET_DIR/HOME cascade.

Makefile

  • SOCKET_DIR now derived from HERO_SOCKET_DIR ?= $(HOME)/hero/var/sockets. export HERO_SOCKET_DIR propagates to child processes spawned by run, run-ui, stop, status, restart, etc.

Docs

  • README.md, docs/api.md, docs/architecture.md, docs/configuration.md, docs/ui.md — replaced literal ~/hero/var/sockets/hero_livekit/... with $HERO_SOCKET_DIR/hero_livekit/... and added a top-of-file note explaining the default fallback.

Tests — new coverage in authz.rs

  • 10 unit tests: rule_matches_literal, rule_matches_wildcard_tail, rule_matches_dotted_literal, wildcard_rule_matches_any_subsegment, privileged_methods_have_rules, open_methods_have_no_rules, trusted_mode_bypasses_rules, empty_rules_authorizes_any_context, matching_claim_authorizes, non_matching_claim_denies, empty_claim_list_denies.

Build + Test Results

  • cargo check --workspace — PASS
  • cargo test --workspace — 30 passed, 0 failed, 3 ignored (pre-existing rustdoc examples)

Compatibility

  • HERO_SOCKET_DIR is the only new env var. When unset, resolved paths match the previous defaults exactly, so existing .claude/settings.json allowlist entries continue to work without modification.
  • Operators overriding HERO_SOCKET_DIR must reapprove commands that reference the new path.

Out of scope (noted for future work)

  • Resource-level (per-room) policies — current rules are method-level only, matching the herolib_openrpc_authorize §9.1 baseline. Per-room rules would require a separate context-rules mapping.
  • A HERO_SOCKET_DIR-agnostic parallel block in .claude/settings.json — additive, not blocking; can be added when a developer actively uses a non-default HERO_SOCKET_DIR.
## Implementation Summary All 10 spec steps applied on branch `development_hero_primitives` (branched from `main`). ### Changes **Workspace + Cargo manifests** - `Cargo.toml` — added `hero_proc_sdk` (git, development branch) to `[workspace.dependencies]`. - `crates/hero_livekit_server/Cargo.toml` — added `hero_proc_sdk = { workspace = true }`. - `crates/hero_livekit_ui/Cargo.toml` — added `hero_proc_sdk = { workspace = true }`; removed `tracing-subscriber`. **UI binary — `crates/hero_livekit_ui/src/main.rs`** - Hardcoded `SERVICE_SOCKET` / `UI_SOCKET` constants replaced with `HERO_SOCKET_DIR`-aware resolver (`socket_dir()` / `service_socket_dir()` / `service_socket_path()` / `ui_socket_path()`), per hero_sockets §7.1. - Custom `serve_unix` accept loop replaced with `bind_unix_socket` helper that chmods `ui.sock` to `0660` after bind (hero_sockets §2 + §7.2). - `tracing_subscriber` init removed; `HeroLogger::new("hero_livekit_ui").await?` now owns startup logging. `AppState` carries `Arc<HeroLogger>`. - `rpc_proxy_handler` now reads and forwards `X-Hero-Context`, `X-Hero-Claims`, `X-Forwarded-Prefix` to the backend `rpc.sock`; `forward_rpc` injects them into the raw HTTP request. **Server binary — `crates/hero_livekit_server/src/main.rs`** - `HeroLogger::new("hero_livekit_server").await?` initialized at startup; logger passed into the authorization wrapper. - `OServer::run_cli` closure now hand-constructs each `OsisLivekit` via `OsisDomainInit::create`, wraps it in `AuthorizedOsisLivekit`, and registers via `server.register_domain(ctx, "livekit", authorized)` instead of the default `server.register::<OsisLivekit>`. **Authorization wrapper — `crates/hero_livekit_server/src/livekit/server/authz.rs` (new)** - `AuthorizedOsisLivekit(Arc<OsisLivekit>, Arc<HeroLogger>)` implementing `OsisAppRpcHandler`. - Delegates `handle_rpc_call` / `handle_service_call` / `type_names` / `oschema_source` / `openrpc_spec` to the inner generated type. - Overrides `handle_service_call_with_context` to enforce claim rules on privileged methods: `livekitservice.install`, `configure`, `start`, `stop`, `restart`, `create_room`, `delete_room`, `remove_participant`. Rule set: `["admin", "admin.*", "hero_livekit.admin", "hero_livekit.*"]`. Missing claims header = trusted mode (full access) per hero_context §4.3. - Overrides `handle_rpc_call_with_context` as a pass-through; CRUD methods stay open. - Denied calls log via `HeroLogger` (`authz` source component) and return `RpcError::Operation("PermissionDenied: ...")`. - `crates/hero_livekit_server/src/livekit/server/mod.rs` exports the new module via `pub mod authz;`. **Examples — HERO_SOCKET_DIR resolution** - `crates/hero_livekit_examples/examples/health.rs` and `examples/basic_usage.rs` — `SOCKET_PATH` const + `dirs::home_dir()` resolver replaced with `HERO_SOCKET_DIR`/`HOME` cascade. **Makefile** - `SOCKET_DIR` now derived from `HERO_SOCKET_DIR ?= $(HOME)/hero/var/sockets`. `export HERO_SOCKET_DIR` propagates to child processes spawned by `run`, `run-ui`, `stop`, `status`, `restart`, etc. **Docs** - `README.md`, `docs/api.md`, `docs/architecture.md`, `docs/configuration.md`, `docs/ui.md` — replaced literal `~/hero/var/sockets/hero_livekit/...` with `$HERO_SOCKET_DIR/hero_livekit/...` and added a top-of-file note explaining the default fallback. **Tests — new coverage in `authz.rs`** - 10 unit tests: `rule_matches_literal`, `rule_matches_wildcard_tail`, `rule_matches_dotted_literal`, `wildcard_rule_matches_any_subsegment`, `privileged_methods_have_rules`, `open_methods_have_no_rules`, `trusted_mode_bypasses_rules`, `empty_rules_authorizes_any_context`, `matching_claim_authorizes`, `non_matching_claim_denies`, `empty_claim_list_denies`. ### Build + Test Results - `cargo check --workspace` — PASS - `cargo test --workspace` — 30 passed, 0 failed, 3 ignored (pre-existing rustdoc examples) ### Compatibility - `HERO_SOCKET_DIR` is the only new env var. When unset, resolved paths match the previous defaults exactly, so existing `.claude/settings.json` allowlist entries continue to work without modification. - Operators overriding `HERO_SOCKET_DIR` must reapprove commands that reference the new path. ### Out of scope (noted for future work) - Resource-level (per-room) policies — current rules are method-level only, matching the herolib_openrpc_authorize §9.1 baseline. Per-room rules would require a separate context-rules mapping. - A `HERO_SOCKET_DIR`-agnostic parallel block in `.claude/settings.json` — additive, not blocking; can be added when a developer actively uses a non-default `HERO_SOCKET_DIR`.
Member

Pull request opened: #16

This PR implements the changes discussed in this issue.

Pull request opened: https://forge.ourworld.tf/lhumina_code/hero_livekit/pulls/16 This PR implements the changes discussed in this issue.
mahmoud added this to the ACTIVE project 2026-04-21 12:38:21 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
2 participants
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_livekit#13
No description provided.