# 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_01` — `GET /api/orgs/{org}` returns 200 with full org metadata for existing org; 404 for unknown handle - [ ] `OG_02` — `GET /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_03` — `POST /api/orgs` accepts MSign; rejects unsigned request with 401 - [ ] `OG_04` — `POST /api/orgs/{org}/members/{handle}` accepts MSign; 401 if unsigned - [ ] `OG_05` — `GET /api/orgs/{org}/members` accepts MSign; 401 if unsigned - [ ] `OG_06` — `DELETE /api/orgs/{org}/members/{handle}` accepts MSign; 401 if unsigned - [ ] `OG_07` — `create_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 ```bash 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`:** ```json { "identity_id": "sha256:...", "handle": "acme", "identity_type": "org", "display_name": "Acme Inc", "quorum": 2, "created_at": "" } ``` **`org member list`:** ```json { "org": "acme", "members": [ { "from_handle": "gabriel", "to_handle": "acme", "edge_type": "member_of", "weight": "admin", "authorized_by": [] } ] } ``` #### Deliverables - [ ] `OG_08` — `muse hub org create` calls `POST /api/orgs`, returns JSON with `identity_id`, `handle`, `identity_type`, `display_name`, `quorum`, `created_at` - [ ] `OG_09` — `muse hub org create` with duplicate handle exits 1 with clear error - [ ] `OG_10` — `muse hub org read acme` calls `GET /api/orgs/acme`, returns full org JSON - [ ] `OG_11` — `muse hub org read` on unknown handle exits 1 with "not found" message - [ ] `OG_12` — `muse hub org member add acme gabriel` calls `POST /api/orgs/acme/members/gabriel`, returns JSON confirmation - [ ] `OG_13` — `muse hub org member list acme` calls `GET /api/orgs/acme/members`, returns members array - [ ] `OG_14` — `muse 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 ```bash # 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_18` — `muse 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_19` — `hub 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-org` → `muse hub org create` (already documented as wrong pattern; make it correct) - [ ] `OG_21` — `muse 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)