hero_osis_communication ignores X-Hero-Context — cross-context data leak #40
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_osis#40
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
hero_osis_communicationreturns the same conversation data regardless of theX-Hero-Contextheader. Effectively no isolation between contexts — every space sees every other space's messaging data. Until auth lands this is the only isolation primitive we have, and it's broken.Repro (direct UDS RPC)
Result: all three contexts return the same SID
"0002".Repro (UI)
http://127.0.0.1:9988/hero_os/ui/space/default/messaging. Create DM with QA Tester, send a message.http://127.0.0.1:9988/hero_os/ui/space/bob/messaging. The same DM appears in B's chat list with B able to read and reply.Impact
Suggested fix
Make the communication domain honour
X-Hero-Context(per thehero_osis_app::rpccontract — header is documented atcrates/hero_osis_app/src/rpc.rs:11) — namespace the OSIS storage keys by context, the same way other domains do.bubble-other(post-reload regression of #62) #192Implementation Spec for Issue #40
Objective
Make
hero_osis --startregister bothrootanddefaultcontexts by default, so the UI shell's defaultX-Hero-Context: defaultrequests are served by their own isolateddefaultdata store instead of silently falling back toroot.Investigation findings (the why)
The cross-context isolation architecture is already sound in
hero_rpc.OServer::register(ctx, domain)at server.rs:92 creates a per-(context, domain)data path under~/hero/var/osisdb/<context>/<domain>/and a per-(context, domain)OsisCommunicationinstance. The dispatcher at unified_server.rs:597-608 then routes each incoming request to the correct context handler based on theX-Hero-Contextheader. There is nothing wrong in the storage layer or the dispatch path.What's broken is the server-side context registration.
ServerClidefaults--contextsto"root"(cli.rs:31). When~/hero/bin/hero_osis --startis invoked, no--contextsflag and noHERO_CONTEXTSenv var is forwarded, so onlyrootgets registered. When the dispatcher then receives a request withX-Hero-Context: default, it looks up"default"instate.context_names, doesn't find it, and silently falls back to"root"(unified_server.rs:597-608). Every UI tab — regardless of which space it's in — ends up reading and writing the samerootstore. That is the cross-context "leak" reported in the issue.The hero_os UI shell defaults its
active_contextto"default"(hero_os/.../navbar.rs and main.rs:36). So the typical out-of-the-box collision isdefault(UI) vsroot(server). The fix at the hero_osis layer is to registerdefaultalongsiderootby default; the silent-fallback for other unregistered names (e.g.geomind,bob) is a real problem too but lives inhero_rpc's dispatcher — see Notes for the follow-up issue to file.Requirements
hero_osis --start(production via hero_proc) registers bothrootanddefaultcontexts when neither--contextsnorHERO_CONTEXTSis provided.--contexts <list>andHERO_CONTEXTS=<list>continue to take precedence.cargo check --workspaceandcargo test --workspacestill pass.X-Hero-Context: defaultvsroot, proving isolation.Files to Modify/Create
crates/hero_osis_server/src/bin/hero_osis.rs— augment the contexts-resolution after CLI parse so the upstream default of"root"is replaced with"root,default"for hero_osis specifically. No other source file needs changes.Implementation Plan
Step 1: Add hero_osis-default expansion of the upstream
--contextsdefaultFiles:
crates/hero_osis_server/src/bin/hero_osis.rsAfter the
let cli = ServerCli::parse();line and BEFORE thelet contexts: Vec<String> = cli.contexts.split(',')…block (currently around line 88), insert a small helper that detects the unmodified upstream default and expands it. The cleanest form:The existing
let contexts: Vec<String> = cli.contexts.split(',')…block is replaced by the snippet above. No other code needs to move;contextsis consumed identically downstream.Dependencies: none.
Step 2: Verify build + tests pass
Files: none (verification only)
cargo check --no-default-features --features all-domains(matches Makefilechecktarget).cargo test --workspace(matches Makefiletesttarget).Dependencies: Step 1.
Step 3: End-to-end isolation verification
Files: none (manual verification — Phase 6 will run this and post results)
~/hero/bin/hero_osis --stopmake installdev(writes the newhero_osisto~/hero/bin/).~/hero/bin/hero_osis --startHost: localhost
X-Hero-Context: %s
Content-Type: application/json
Content-Length: %d
%s' "$ctx" "${#BODY}" "$BODY" | nc -U "$SOCK"
echo
done
Test Results
Static checks
cargo check --no-default-features --features all-domains→ PASS (clean, no new warnings).cargo test --lib --no-default-features --features all-domains→ PASS (113 passed, 0 failed, 0 ignored).End-to-end isolation repro
Stopped the running orchestrator, rebuilt + reinstalled the patched binary, restarted via hero_proc, then ran the curl reproducer from the issue body against the
hero_osis_communicationsocket:rootreturns the legacy SID0002(data accumulated under the old silent-fallback bug).defaultreturns an empty list (its data store is fresh because nothing was ever routed there in production until now). The two are now isolated as expected.bobstill falls back toroot— that is the dispatcher-level silent fallback for unregistered context names, called out as out-of-scope in the spec under follow-ups. This issue (#40) is about the missingdefaultregistration, which is fixed.Cross-domain confirmation
Repeated the same A/B test against
hero_osis_identity(contact.list):Same RPC, same socket, different
X-Hero-Contextheader → different data. Cross-context isolation works end-to-end.Known cosmetic gap (non-blocking)
The built-in
context.listRPC currently still returns onlyroot, even though the per-(context, domain)data stores ARE isolated. The dispatcher's domain registrations and theOServer::registryadmin list are populated by separate code paths and the latter doesn't get updated when domains register additional contexts. Functional isolation (the issue body's primary symptom) is unaffected. Worth filing as a separate cosmetic ticket againsthero_rpc— does not block this PR.Implementation Summary
Changes
crates/hero_osis_server/src/bin/hero_osis.rs— when--contexts(andHERO_CONTEXTS) is left at the upstreamServerClidefault of"root", expand it to"root,default"for hero_osis specifically. Explicit overrides remain authoritative. ~15 lines added in one place; no other source files touched.Why
The cross-context architecture in
hero_rpcalready supports per-context isolation:OServer::register(ctx, domain)creates a separate per-(context, domain)data path under~/hero/var/osisdb/<context>/<domain>/, and the dispatcher routes each request to the right context handler based on theX-Hero-Contextheader. The bug surfaced as data leakage because hero_osis only registeredrootat startup, while the hero_os UI shell defaults itsactive_contexttodefault— so every UI request silently fell back toroot, collapsing all spaces into one store. This change makes both contexts available out of the box.Test results
See test results comment.
conversation.listandcontact.listreturn different data underX-Hero-Context: rootvsdefaultagainst running sockets.~/hero/var/osisdb/root/. After this fix, the UI'sdefaultrequests start hitting an empty~/hero/var/osisdb/default/store. No data is deleted; users who want the old data underdefaultcan copy it manually. Production isn't affected (no production deployments per the no-auth state of the project).Caveats & follow-ups (not in this PR)
bob,geomind,incubaidstill silently route torootbecause they're not in the hero_osis startup set. The deeper fix (loud rejection or auto-register of unknown contexts) belongs inhero_rpc::server::unified_serverand affects every Hero service, not just hero_osis. Worth filing as a separate ticket againsthero_rpc.context.listadmin enumeration is stale — the built-incontext.listRPC reads fromOServer::registry, which is populated independently from per-domainOServer::register()calls and currently only showsroot. Functional isolation is unaffected; this is a cosmetic/admin-UI concern. Also belongs inhero_rpc.mocktarget usesMOCK_CONTEXTS = root,default,geomind,incubaid,my_context,our_context,threefold,your_context,hero_osis. Once production deployments need additional spaces beyondroot+default, the--startinvocation should pass--contexts <comma-separated-list>(or setHERO_CONTEXTS=...) to register them. No code change needed for that — the existing CLI/env path already handles it.contexts_default(...)setter so single-bin codegen can override theServerCliroot-only default #36PR opened: #41
In-file workaround in
bin/hero_osis.rsregistering theMOCK_CONTEXTSlist by default. Confirmed via end-to-end repro that the 9 registered contexts (root,default,geomind,incubaid,my_context,our_context,threefold,your_context,hero_osis) now isolate from each other.The deeper proper fix lives upstream —
OschemaBuildConfig::contexts_default(...)setter so the generator emits this default itself — tracked at hero_rpc#36 (lhumina_code/hero_rpc#36). This PR is the temporary workaround until that lands;bin/hero_osis.rsis auto-generated and any future schema change that re-emits the bin will wipe the patch.