gabriel / musehub public
Open #98
filed by gabriel human · 3 days ago

Orgs: first-class collective identities — CLI + auth wiring

0 Anchors
Blast radius
Churn 30d
0 Proposals

Orgs — First-Class Collective Identities

Background

Humans, agents, and orgs are the three entity types in the Muse identity model. They sit at entity_type' = 0', 1', 2' in the HD derivation path m/purpose'/domain'/entity_type'/entity_id'/role'/index'. The docs define orgs as:

A collective identity. Membership and governance live above the key layer; the tree records only that this principal is a collective.

Orgs have no signing key of their own. Their authority is the quorum of their members — distributed threshold authorisation rather than a single key that can be stolen. The quorum field sets the threshold; members carry fractional weight values so voting power can be non-uniform.

The server-side foundation already exists: MusehubIdentity with identity_type="org" and org_quorum, a full musehub_orgs service (create_org, add_org_member, remove_org_member, list_org_members), the /api/orgs route tree, and the full musehub/graph/ stack (IdentityPushValidator, QuorumEngine, DAG invariants I1–I3). What's missing is the read endpoint, MSign auth on the existing write routes, and the entire CLI layer that ties everything together.

Goal

muse hub org create, muse hub org read, and muse hub org member {add,remove,list} all work end-to-end on localhost and staging, are covered by integration tests, and the identity DAG is correctly populated (identity repo with pubkey=null, type="org", quorum=N, plus RelationshipRecord files for each member_of edge).

Identity model recap

HD path

m / 1075233755' / 1660078172' / 2' / entity_id' / role' / index'
                              ^^^
                         entity_type = org

Orgs have no signing key at any HD path — pubkey is null in their IdentityRecord. Their authority comes from quorum of members, not from a private key. The identity_id is blob_id(b"org\x00{handle}\x00{created_at_iso}").

Identity repo layout (org)

identities/{handle}.json    ← IdentityRecord  {handle, type="org", pubkey=null,
                                                quorum=N, registered_at, metadata}
relationships/{member}/member_of/{org}.json
                            ← RelationshipRecord  {from_handle=member,
                                                   to_handle=org,
                                                   edge_type="member_of",
                                                   weight="admin"|"write"|"read",
                                                   authorized_by=[...]}

Quorum bootstrap sequence (quorum=2 example)

Step 1: gabriel joins (0 prior members)
  → bootstrap: gabriel self-signs              ✓  1 sig (self)

Step 2: alice joins (1 prior member: gabriel)
  → min(quorum=2, prior=1) = 1                 ✓  1 sig from {gabriel}

Step 3: bob joins (2 prior members: gabriel, alice)
  → min(quorum=2, prior=2) = 2                 ✓  2 sigs from existing members

Step 4: carol tries to join (3 prior members)
  → min(quorum=2, prior=3) = 2                 ✓  any 2 of 3 existing members

DAG invariants enforced at push time

ID Name Enforcement Effect
I1 Acyclicity hard error Push rejected
I2 Root distance warning Push accepted, orphan flagged
I3 Authorization hard error Push rejected, missing sig listed

What already exists

Component File Status
DB model: identity_type="org", org_quorum musehub/db/musehub_identity_models.py:58,99
create_org service musehub/services/musehub_orgs.py
add_org_member service musehub/services/musehub_orgs.py
remove_org_member service musehub/services/musehub_orgs.py
list_org_members service musehub/services/musehub_orgs.py
POST /api/orgs musehub/api/routes/api/orgs.py ✅ (auth gap — see Phase 1)
POST /api/orgs/{org}/members/{handle} musehub/api/routes/api/orgs.py ✅ (auth gap)
GET /api/orgs/{org}/members musehub/api/routes/api/orgs.py ✅ (auth gap)
DELETE /api/orgs/{org}/members/{handle} musehub/api/routes/api/orgs.py ✅ (auth gap)
GET /api/orgs/{org} ❌ missing
IdentityPushValidator, QuorumEngine, DAG musehub/graph/
IdentityRecord / RelationshipRecord in identity plugin muse/plugins/identity/records.py
Profile page renders orgs musehub/api/routes/musehub/ui_user_profile.py
muse hub org CLI ❌ missing entirely

Phases


Phase 1 — Server: MSign auth + GET /api/orgs/{org}

The existing org routes use require_valid_token (session/JWT auth). The CLI uses MSign (require_signed_request). Without this fix, every CLI org command returns 401. This phase makes the routes accept MSign and adds the missing read endpoint.

Deliverables
  • OG_01GET /api/orgs/{org} returns 200 with full org metadata for existing org; 404 for unknown handle
  • OG_02GET /api/orgs/{org} returns 403 for non-public org when caller is not a member (if org is private — skip if orgs are always public)
  • OG_03POST /api/orgs accepts MSign; rejects unsigned request with 401
  • OG_04POST /api/orgs/{org}/members/{handle} accepts MSign; 401 if unsigned
  • OG_05GET /api/orgs/{org}/members accepts MSign; 401 if unsigned
  • OG_06DELETE /api/orgs/{org}/members/{handle} accepts MSign; 401 if unsigned
  • OG_07create_org service: add get_org(session, handle) function that returns MusehubIdentity | None; used by the new read endpoint

Files touched:

  • musehub/services/musehub_orgs.py — add get_org
  • musehub/api/routes/api/orgs.py — swap auth dependency, add GET route

Phase 2 — CLI: muse hub org subcommand tree

New file muse/cli/commands/hub/orgs.py. All commands use MSign (same pattern as hub repos.py, hub issues.py). Register in muse/cli/commands/hub/__init__.py.

Command surface
muse hub org create --handle acme --display-name "Acme Inc" [--quorum 2] [--json]
muse hub org read   acme [--json]
muse hub org member add    acme gabriel [--weight admin|write|read] [--json]
muse hub org member remove acme gabriel [--json]
muse hub org member list   acme [--json]
JSON output shapes

org create / org read:

{
  "identity_id":    "sha256:...",
  "handle":         "acme",
  "identity_type":  "org",
  "display_name":   "Acme Inc",
  "quorum":         2,
  "created_at":     "<iso8601>"
}

org member list:

{
  "org":     "acme",
  "members": [
    {
      "from_handle": "gabriel",
      "to_handle":   "acme",
      "edge_type":   "member_of",
      "weight":      "admin",
      "authorized_by": []
    }
  ]
}
Deliverables
  • OG_08muse hub org create calls POST /api/orgs, returns JSON with identity_id, handle, identity_type, display_name, quorum, created_at
  • OG_09muse hub org create with duplicate handle exits 1 with clear error
  • OG_10muse hub org read acme calls GET /api/orgs/acme, returns full org JSON
  • OG_11muse hub org read on unknown handle exits 1 with "not found" message
  • OG_12muse hub org member add acme gabriel calls POST /api/orgs/acme/members/gabriel, returns JSON confirmation
  • OG_13muse hub org member list acme calls GET /api/orgs/acme/members, returns members array
  • OG_14muse hub org member remove acme gabriel calls DELETE /api/orgs/acme/members/gabriel, exits 0 on success
  • OG_15 — all five commands registered under hub org in hub/__init__.py; muse hub org --help and muse hub org member --help work

Files touched:

  • muse/cli/commands/hub/orgs.py (new)
  • muse/cli/commands/hub/__init__.py — import + orgs.register(subs)

Phase 3 — End-to-end verification on localhost and staging

Manual smoke test plus a lightweight integration test that calls the live server.

Localhost checklist
# Create org
muse hub org create --handle acme-test --display-name "Acme Test" --quorum 1 \
  --hub https://localhost:1337 --json
# → {"identity_id": "sha256:...", "handle": "acme-test", ...}

# Read it back
muse hub org read acme-test --hub https://localhost:1337 --json
# → same fields

# Add gabriel as member
muse hub org member add acme-test gabriel --weight admin \
  --hub https://localhost:1337 --json

# List members
muse hub org member list acme-test --hub https://localhost:1337 --json
# → {"org": "acme-test", "members": [{from_handle: "gabriel", ...}]}

# Verify identity repo exists with correct domain_id
muse hub repo read acme-test/identity --hub https://localhost:1337 --json
# → domain_id: "identity", visibility: "private"

# Remove member
muse hub org member remove acme-test gabriel --hub https://localhost:1337
Deliverables
  • OG_16 — full localhost smoke test passes (create → read → member add → member list → member remove)
  • OG_17 — identity repo for org exists with domain_id="identity", visibility="private", HEAD commit message "identity: register org acme-test"
  • OG_18muse hub repo list --hub https://staging.musehub.ai --json shows acme-test/identity after deploy; same smoke test passes on staging

Phase 4 — Agent guide + docs

Update ~/ecosystem/muse/docs/agent-guide.md with the muse hub org command surface so agents can discover and use orgs without reading source.

Deliverables
  • OG_19hub org commands added to the reflex replacement table and command reference section in agent-guide.md
  • OG_20 — gitism glossary entry updated: muse hub create-orgmuse hub org create (already documented as wrong pattern; make it correct)
  • OG_21muse agent-config sync run on muse repo after agent-guide.md update; no drift reported

Acceptance criteria

  1. muse hub org create --handle acme ... succeeds on both localhost and staging, creating an identity row with identity_type="org" and a private identity repo with domain_id="identity" and the correct IdentityRecord.
  2. muse hub org member add commits a RelationshipRecord file at relationships/{member}/member_of/{org}.json in the org's identity repo.
  3. muse hub org member list reads those files from HEAD and returns the correct membership array.
  4. All six server routes accept MSign-signed requests from the CLI.
  5. GET /api/orgs/{org} returns the org's identity metadata (404 if not found).
  6. Tests OG_01–OG_07 (server) green before any CLI work begins.
  7. Tests OG_08–OG_15 (CLI) green.
  8. End-to-end smoke on localhost and staging passes (OG_16–OG_18).

Out of scope

  • Web UI for org management (profile page already renders orgs; no new UI needed)
  • Quorum-enforced member admission via the CLI (Phase 1–2 use the service directly; multi-sig member admission is a future layer on top)
  • Org-owned billing, MPay addresses, or on-chain ERC8004 registration
  • Sub-org nesting (the QuorumEngine supports it; the CLI does not need to expose it yet)
  • muse auth keygen --entity-type org (orgs have no signing key by design)
Activity
gabriel opened this issue 3 days ago
No activity yet. Use the CLI to comment.