IdentityGraphService (root-distance I2, quorum I3) is built and correct but never exposed via any API — no genealogy query surface exists
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_byis 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_bycame back as"ep19-agent-a"automatically, not something I passed.PATCH /api/identities/{handle}(update_identity) only touchesdisplay_name/bio/avatar_url/website_url/email/agent_model/ agent_capabilities—spawned_byis not among them. Confirmed by reading the handler; it never appears on the left side of asetattr.
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.
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:cycle.py,CycleDetector) — no agent can be its own ancestor via the spawn chain.depth.py,RootDistanceIndex) — multi-source BFS from all human nodes; every agent's distance back to a human root, or None if orphaned/disconnected.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 behindPOST /api/identities/agent— never imports or calls any of this. It writesMusehubIdentity(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=gabrielvia a realPOST /api/identities/agentcall, then registered agent B withspawned_by=A, then re-registered/updated A'sspawned_byto 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 thatCycleDetectorDOES correctly reject the identical cycle shape when the same three spawn edges are added directly viaIdentityGraphService.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) callIdentityPushValidator(or an equivalent live-query path) before committing a newspawned_byedge, and (c) decide whereRootDistanceIndexgets 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.