gabriel / musehub public
Open #211 Bug
filed by gabriel human · 33 days ago

IdentityGraphService (root-distance I2, quorum I3) is built and correct but never exposed via any API — no genealogy query surface exists

0 Anchors
— Blast radius
— Churn 30d
0 Proposals
No description provided.
◎ Activity2
●
gabriel opened this issue 33 days ago
○
gabriel 33 days ago

Confirmed while building Episode 19 ("Identity Profiles") demo material. Same shape as staging#204 (CRDT join() unreachable from muse merge) — a real, well-designed, heavily-tested subsystem that's never wired into the live application.

The engine (real, correct, tested): musehub/graph/ implements three formal invariants over an in-memory identity DAG:

  • I1 (cycle.py, CycleDetector) — no agent can be its own ancestor via the spawn chain.
  • I2 (depth.py, RootDistanceIndex) — multi-source BFS from all human nodes; every agent's distance back to a human root, or None if orphaned/disconnected.
  • I3 (quorum.py, QuorumEngine) — org membership quorum soundness.

Unified in IdentityGraphService/IdentityPushValidator (service.py, push_validator.py). Backed by 66 references across 5 dedicated test files (test_graph_cycle.py, test_graph_depth.py, test_graph_quorum.py, test_graph_service.py, test_identity_push_validator.py, test_quorum_enforcement.py).

The gap: grep -rln "IdentityPushValidator\|IdentityGraphService" . --include="*.py" | grep -v tests | grep -v musehub/graph/ returns NOTHING. musehub/services/musehub_auth.py::register_agent_identity() — the real implementation behind POST /api/identities/agent — never imports or calls any of this. It writes MusehubIdentity(spawned_by=...) as a plain string column and commits, no cycle check, no root-distance computation, no DAG built from the persisted data at all.

Confirmed reproducible (see Episode 19's demo script for the exact sequence): registered agent A with spawned_by=gabriel via a real POST /api/identities/agent call, then registered agent B with spawned_by=A, then re-registered/updated A's spawned_by to point at B — completing a genuine 2-hop cycle (A -> B -> A, disconnected from any human root) via three ordinary, successful API calls. Nothing rejected it. Confirmed separately that CycleDetector DOES correctly reject the identical cycle shape when the same three spawn edges are added directly via IdentityGraphService.add_spawn() in isolation — so the engine itself is sound; only the wiring is missing.

Suggested starting point: register_agent_identity() needs to (a) load the current persisted spawn graph, (b) call IdentityPushValidator (or an equivalent live-query path) before committing a new spawned_by edge, and (c) decide where RootDistanceIndex gets exposed to callers (there's currently no REST endpoint for genealogy queries at all — that would need to be added alongside the enforcement fix).

Not itself a security bypass in the sense of forging someone else's identity — this is about spawn-graph soundness (can a cycle exist, can an agent be genuinely orphaned from any accountable human) rather than authentication. Deferred per usual until after the season wraps.

○
gabriel 33 days ago

CORRECTION to my own repro steps above, found by actually testing live (rather than reasoning it through) — important enough to flag clearly rather than leave the earlier claim standing uncorrected.

What I got wrong: I claimed a 3-call cycle (A spawned_by B, B spawned_by A) was directly achievable. It is not, and the original wording overstated the risk. Verified by reading register_agent_identity() and update_identity() (PATCH) closely, then actually exercising both live:

  • spawned_by is set ONLY to the authenticated caller's own handle — never a value the request body can specify. Confirmed: registered a real agent A (spawned_by=gabriel, my own operator handle), then authenticated AS agent A's own Ed25519 key to register agent B — spawned_by came back as "ep19-agent-a" automatically, not something I passed.
  • PATCH /api/identities/{handle} (update_identity) only touches display_name/bio/avatar_url/website_url/email/agent_model/ agent_capabilities — spawned_by is not among them. Confirmed by reading the handler; it never appears on the left side of a setattr.

Because the spawner must already exist and be authenticated before a child can be created, and spawned_by can never be changed afterward, every edge in the real persisted spawn graph necessarily points from a newer node to an already-existing older one. That structurally prevents classic cycles (I1) by construction — no CycleDetector call needed for that specific property, regardless of whether the engine is wired in. I was wrong to call this "confirmed reproducible" without testing it first; correcting the record here rather than leaving it.

What's still a real, live gap (this part holds up): there is no production code path, anywhere, that computes root-distance (I2) or exposes it. I built a real 2-hop chain (gabriel → ep19-agent-a → ep19-agent-b, all via genuine API calls) and there is no endpoint to ask "what is ep19-agent-b's distance to a human root" or "list its human ancestors" — despite RootDistanceIndex existing, being fully correct, and answering that exact question instantly when called directly on the same three nodes reproduced in-memory. Same for I3 (QuorumEngine) — no live org-quorum query surface exists either.

Also still real and unaffected by this correction: if a human operator is later revoked (DELETE /api/identities/{handle}), agents they spawned keep a spawned_by string pointing at a now-nonexistent handle — a genuinely orphaned chain with no reachable human root — and nothing detects or reports that today. (Noted as a plausible consequence of the current design here, not independently verified live in this pass — flagging for whoever picks this up to confirm.)

Revised framing: this is "a well-built root-distance/quorum query engine exists, is correct, and is never exposed or consulted" — not "cycles are currently possible." Still worth fixing (add a genealogy query endpoint backed by this engine, and decide whether revocation should cascade or flag orphaned agents), just not the risk I originally described. Apologies for the imprecision in the first pass.