Convention question: how should hero_* services consume proxy-injected identity headers? + what is hero_osis's identity domain for? #191

Open
opened 2026-04-26 18:41:07 +00:00 by sameh-farouk · 1 comment
Member

What's the question

hero_proxy is the edge — terminates TLS, runs auth (bearer / OAuth / secp256k1 signature / IP-auto-login), and injects three identity headers on every forwarded request:

Five backends I audited handle these five different ways. There's no stack convention, no portable client across services, and one big load-bearing question about hero_osis's identity domain that needs an answer before any convention can land.

State today (5 backends, 5 different answers)

Service reads X-Hero-User? X-Hero-Context? X-Hero-Claims? local users table? external_id col? dev bypass?
hero_collab required, fail-closed logged only logged only yes — auto-provisioned via user.me yes --auth-mode=dev
hero_voice no logged only logged only no n/a none
hero_slides no no no no n/a none
hero_whiteboard no no no yes — but no external_id, decoupled from proxy no none
hero_agent no — uses local JWT yes (forwarded to MCP downstreams) yes (forwarded to MCP downstreams) no n/a anonymous fallback

Implications:

  • No portable client. A client speaking proxy's X-Hero-User has full identity in collab, no reachable identity in whiteboard (no external_id to map onto), is effectively anonymous in voice/slides, and is the wrong shape for agent (which expects a JWT).
  • X-Hero-Claims is wire-defined but operationally inert. Collab logs it; agent forwards it to MCP downstreams; nobody makes a permission decision on it. The proxy spends BFS work computing it on every request.
  • X-Hero-Context is sourced and injected but no service scopes data by it. The denormalized proxy.users.context source decision (5f7bb04) settled the proxy side; the downstream side is unanswered.

Concrete reference impl (one possible answer for the collab side)

Recently merged in hero_collab as one option for "how a downstream service consumes proxy-injected identity":

  • hero_collab PR #21 feat(auth): bootstrap collab admin from proxy.is_admin + dev-mode honesty — on first user.me, queries hero_proxy_sdk::users_list (3s timeout) and grants collab admin if proxy reports is_admin=1. Falls back to "first-user-on-empty-DB grants admin" when proxy is unreachable (mirrors hero_osis's first user_create gets Owner role). Plus a 4-line shim that drops upstream X-Hero-User when --auth-mode=dev (collab declares itself the source of truth for identity in dev mode).

This is collab's local choice, not a stack convention. Nothing about #21 obliges voice / slides / whiteboard / agent to do anything similar.

The load-bearing question — what is hero_osis's identity domain for?

hero_osis's identity schema defines User, Group, Session, Device, Profile, Contact, SshKey, plus a full ed25519 challenge-response AuthService. None of it is in the request path of any web-facing app:

  • hero_proxy does its own auth and stores its own users table; after 5f7bb04 it explicitly does NOT live-sync identity from osis (the context_sync.rs module that briefly existed was reverted within hours of being added).
  • hero_collab reads X-Hero-User from proxy; never queries osis for identity.
  • hero_agent imports hero_osis_sdk and connects at startup, but the connection is not in the auth path.
  • voice / slides / whiteboard don't link to osis at all.

Meanwhile osis's communication domain is being actively developed (e.g. list_messages added e0e02dd on Apr 19). So the codebase is alive — identity just isn't in the operational flow.

Three plausible explanations the team needs to pick one of:

  1. Different audience. osis identity is for native / mobile / key-based clients using ed25519, not browser clients behind hero_proxy. Both auth systems coexist by design. If so: document it — "for browser clients, the user store is proxy.users; for key-based clients, it's osis.User."
  2. Future state. osis is intended to become the source of truth, proxy.users is transitional. 5f7bb04's revert was tactical (that specific sync was wrong), not strategic (osis irrelevant). If so: path and timeline?
  3. Operationally orphaned. osis identity is alive in the data model but dormant in the request path. If so: stop calling it the identity tier.

Without a chosen answer, every new app maintainer faces the same fork ("wire to proxy or osis?") with no canonical answer; the default ends up being "wire to neither" (voice, slides) or "wire to proxy and ignore the rest" (collab).

Questions

  1. What is hero_osis's identity domain for? (one of the three above, or a fourth I'm missing)
  2. Should there be a stack convention for X-Hero-User handling? Reference impl in collab PR #21 — do other services adopt this pattern, or is per-service freedom intentional?
  3. X-Hero-Claims: enforce it or drop it? Currently nobody decides on it; the proxy spends compute producing it on every request.

Question 1 precedes the others — the answer changes whether external_id should map to proxy.users.id or osis.User.sid, and whether display_name should be sourced from proxy or osis. If there's appetite for a one-page "identity contract for hero apps" that codifies the answers, happy to draft it.

## What's the question [`hero_proxy`](https://forge.ourworld.tf/lhumina_code/hero_proxy) is the edge — terminates TLS, runs auth (bearer / OAuth / secp256k1 signature / IP-auto-login), and injects three identity headers on every forwarded request: - `X-Hero-User: <username>` — the authenticated principal - `X-Hero-Context: <integer>` — context id, sourced from `proxy.users.context` since [commit `5f7bb04` (proxy #23, "source X-Hero-Context from authenticated user, not route")](https://forge.ourworld.tf/lhumina_code/hero_proxy/commit/5f7bb04) - `X-Hero-Claims: <comma-separated>` — flattened RBAC claims via group → role → claim BFS **Five backends I audited handle these five different ways. There's no stack convention, no portable client across services, and one big load-bearing question about hero_osis's identity domain that needs an answer before any convention can land.** ## State today (5 backends, 5 different answers) | Service | reads `X-Hero-User`? | `X-Hero-Context`? | `X-Hero-Claims`? | local users table? | `external_id` col? | dev bypass? | |---|---|---|---|---|---|---| | [hero_collab](https://forge.ourworld.tf/lhumina_code/hero_collab) | required, fail-closed | logged only | logged only | yes — auto-provisioned via `user.me` | yes | `--auth-mode=dev` | | [hero_voice](https://forge.ourworld.tf/lhumina_code/hero_voice) | no | logged only | logged only | no | n/a | none | | [hero_slides](https://forge.ourworld.tf/lhumina_code/hero_slides) | no | no | no | no | n/a | none | | [hero_whiteboard](https://forge.ourworld.tf/lhumina_code/hero_whiteboard) | no | no | no | yes — but **no `external_id`**, decoupled from proxy | no | none | | [hero_agent](https://forge.ourworld.tf/lhumina_code/hero_agent) | no — uses local JWT | yes (forwarded to MCP downstreams) | yes (forwarded to MCP downstreams) | no | n/a | anonymous fallback | Implications: - **No portable client.** A client speaking proxy's `X-Hero-User` has full identity in collab, no reachable identity in whiteboard (no `external_id` to map onto), is effectively anonymous in voice/slides, and is the wrong shape for agent (which expects a JWT). - **`X-Hero-Claims` is wire-defined but operationally inert.** Collab logs it; agent forwards it to MCP downstreams; nobody makes a permission decision on it. The proxy spends BFS work computing it on every request. - **`X-Hero-Context` is sourced and injected** but no service scopes data by it. The denormalized `proxy.users.context` source decision (5f7bb04) settled the proxy side; the downstream side is unanswered. ## Concrete reference impl (one possible answer for the collab side) Recently merged in hero_collab as one option for "how a downstream service consumes proxy-injected identity": - [hero_collab PR #21 `feat(auth): bootstrap collab admin from proxy.is_admin + dev-mode honesty`](https://forge.ourworld.tf/lhumina_code/hero_collab/pulls/21) — on first `user.me`, queries `hero_proxy_sdk::users_list` (3s timeout) and grants collab `admin` if proxy reports `is_admin=1`. Falls back to "first-user-on-empty-DB grants admin" when proxy is unreachable (mirrors hero_osis's `first user_create gets Owner role`). Plus a 4-line shim that drops upstream `X-Hero-User` when `--auth-mode=dev` (collab declares itself the source of truth for identity in dev mode). This is **collab's local choice**, not a stack convention. Nothing about #21 obliges voice / slides / whiteboard / agent to do anything similar. ## The load-bearing question — what is hero_osis's identity domain for? [`hero_osis`'s `identity` schema](https://forge.ourworld.tf/lhumina_code/hero_osis/src/branch/development/crates/hero_osis/schemas/identity) defines `User`, `Group`, `Session`, `Device`, `Profile`, `Contact`, `SshKey`, plus a full ed25519 challenge-response `AuthService`. **None of it is in the request path of any web-facing app**: - hero_proxy does its own auth and stores its own `users` table; after [`5f7bb04`](https://forge.ourworld.tf/lhumina_code/hero_proxy/commit/5f7bb04) it explicitly does NOT live-sync identity from osis (the `context_sync.rs` module that briefly existed was reverted within hours of being added). - hero_collab reads `X-Hero-User` from proxy; never queries osis for identity. - hero_agent imports `hero_osis_sdk` and connects at startup, but the connection is not in the auth path. - voice / slides / whiteboard don't link to osis at all. Meanwhile osis's [`communication` domain](https://forge.ourworld.tf/lhumina_code/hero_osis/src/branch/development/crates/hero_osis/schemas/communication) is being actively developed (e.g. `list_messages` added [`e0e02dd`](https://forge.ourworld.tf/lhumina_code/hero_osis/commit/e0e02dd) on Apr 19). So the codebase is alive — identity just isn't in the operational flow. Three plausible explanations the team needs to pick one of: 1. **Different audience.** osis identity is for native / mobile / key-based clients using ed25519, not browser clients behind hero_proxy. Both auth systems coexist by design. *If so: document it — "for browser clients, the user store is `proxy.users`; for key-based clients, it's `osis.User`."* 2. **Future state.** osis is intended to become the source of truth, `proxy.users` is transitional. `5f7bb04`'s revert was tactical (that specific sync was wrong), not strategic (osis irrelevant). *If so: path and timeline?* 3. **Operationally orphaned.** osis identity is alive in the data model but dormant in the request path. *If so: stop calling it the identity tier.* Without a chosen answer, every new app maintainer faces the same fork ("wire to proxy or osis?") with no canonical answer; the default ends up being "wire to neither" (voice, slides) or "wire to proxy and ignore the rest" (collab). ## Questions 1. **What is `hero_osis`'s identity domain for?** (one of the three above, or a fourth I'm missing) 2. **Should there be a stack convention for `X-Hero-User` handling?** Reference impl in [collab PR #21](https://forge.ourworld.tf/lhumina_code/hero_collab/pulls/21) — do other services adopt this pattern, or is per-service freedom intentional? 3. **`X-Hero-Claims`: enforce it or drop it?** Currently nobody decides on it; the proxy spends compute producing it on every request. Question 1 precedes the others — the answer changes whether `external_id` should map to `proxy.users.id` or `osis.User.sid`, and whether `display_name` should be sourced from proxy or osis. If there's appetite for a one-page "identity contract for hero apps" that codifies the answers, happy to draft it.
Owner

I agree it is very important to settle this.

@despiegk @timur have been working on this refactoring I think they should share their POV.

Thanks @sameh-farouk

I agree it is very important to settle this. @despiegk @timur have been working on this refactoring I think they should share their POV. Thanks @sameh-farouk
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/home#191
No description provided.