Skip to content

feat(ndo): NDO membership — join, list, and is-member on the per-NDO cell - #129

Merged
Soushi888 merged 1 commit into
devfrom
feat/ndo-membership
Aug 16, 2026
Merged

feat(ndo): NDO membership — join, list, and is-member on the per-NDO cell#129
Soushi888 merged 1 commit into
devfrom
feat/ndo-membership

Conversation

@Soushi888

@Soushi888 Soushi888 commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

What this does

Replaces the NDO membership stub (NdoNotImplementedError) with a real DHT-backed surface in zome_resource, so an agent can declare participation in an NDO and enumerate who else is taking part.

Stacked on #128 (fix/120-clone-cell-dna-properties). Review/merge that first; this PR targets it as base.

The design decision worth reading

Membership is not an access grant. Under the per-NDO-cell model (ADR-010 model A) an agent must already hold the cloned NDO cell to read the NDO at all, so cell possession is what confers access. NdoMembership only makes participation listable. Nothing may treat its absence as a read denial, and the module docs, integrity comments, and tests all say so explicitly so a future reader does not turn it into a permission check.

Backend — zome_resource

New integrity entry:

pub struct NdoMembership {
    pub ndo_identity_hash: ActionHash,
    pub role: Option<String>,
}

The joining agent and join time are not in the entry — they come from the action header, matching the GroupMembership convention in the group DNA.

Function Behaviour
join_ndo(JoinNdoInput) Idempotent: a second join returns AlreadyMember. Verifies the identity exists in this cell. Creates NdoToMembers + MemberToNdos.
get_ndo_members(ActionHash) Vec<AgentPubKey>, read from each link's author
is_ndo_member((AgentPubKey, ActionHash)) "is this agent taking part", never "may this agent read"

Why members come from link authors, not a per-link get: a per-link get can fail when a peer's membership record has not yet gossiped to this shard, silently dropping that member so only the local agent appears. That exact bug was found and fixed in zome_group (#126); the comment in get_ndo_members says not to reintroduce it, and the two-agent Sweettest is the assertion that would catch a regression.

Validation split follows the group DNA convention: integrity checks only what is local (role non-empty, <= 100 chars); identity existence is checked in the coordinator so membership creation never depends on gossip timing.

UI

  • joinNdo / getNdoMembers resolve the NDO clone cell from its anchor, then call the zome. Unlike reads, membership has no legacy shared-cell fallback: an unresolved identity is a hard NdoNotFoundError rather than a silent degrade.
  • joinNdo guards on is_ndo_member so re-joining is a benign no-op (mirrors the group's self-healing membership, REQ-UI-GRP-04).
  • Member names resolve via zome_person on the provisioned cell, with a truncated-pubkey fallback for agents who have no Person entry yet (normal — REQ-UI-ID-03 defers Person creation).
  • NdoNotImplementedError deleted; the amber "not yet implemented" banners become real error states.

Tests

  • Sweettest dnas/nondominium/tests/src/nondominium/ndo_membership/ — round trip (creating an NDO does not implicitly join it), double-join rejection with no duplicate, unknown-identity rejection, and two-agent convergence where each agent must see both members.
  • Playwright multi-agent.spec.ts — both agents join and see each other in the member list, polling through gossip.

Docs

resource_zome.md, specifications.md, ui_architecture.md move from "planned / stub" to implemented, with the not-an-access-grant rule and the link-author rule written down. .rules + DOCUMENTATION_INDEX.md record that await_consistency_20_s takes the array by value ([&a, &b], not &[&a, &b]) — the bound is IntoIterator<Item = &SweetCell>, so a slice reference yields &&SweetCell and fails to compile.

Closes #133

@Soushi888

Copy link
Copy Markdown
Collaborator Author

Status: where this stands, and what is still missing

Verified

Sweettest — 4/4 pass, run in nix develop against the .happ built from this branch:

test ndo_membership::ndo_membership_join_round_trip ... ok
test ndo_membership::ndo_membership_join_unknown_identity_rejected ... ok
test ndo_membership::ndo_membership_double_join_is_rejected ... ok
test ndo_membership::ndo_membership_two_agents_converge ... ok
test result: ok. 4 passed; 0 failed; finished in 216.32s

cargo check --package nondominium_sweettest --tests is clean. The two-agent convergence test is the one that matters most: it is the assertion that fails if get_ndo_members ever goes back to a per-link get instead of reading the link author.

Not verified

  • The Playwright test in this PR has not been run. multi-agent.spec.ts gained a "both agents join and see each other" case; it needs the browser harness against live conductors and has not been executed here. Treat it as written-but-unproven.
  • The clone-cell path is not covered by Sweettest. setup_two_agents() gives the provisioned nondominium cell, and zome_resource lives in both that DNA and the ndo DNA, so the zome logic is proven but the cell resolution (anchor lookup, callNdoZome on the right clone) is only exercised by the unrun e2e test.

Missing / deliberately out of scope

  1. No leave_ndo. There is no coordinator function to withdraw participation, and the integrity zome's DeleteEntry branch falls through to Valid for NdoMembership. Worse, since get_ndo_members reads link authors, deleting the membership entry would not remove the agent from the list — the NdoToMembers link would survive. Leaving needs a delete_link path, not just an entry delete. Flagging so nobody assumes delete works.
  2. role is plumbed but unused. The zome accepts and validates Option<String>; the UI always sends null and NdoView hardcodes role: 'Member' for display. No role picker, no community-defined taxonomy yet.
  3. No Person entry created on join. REQ-UI-ID-03 names joining an NDO as a Level 3 DHT-active trigger that should create the Person entry. It does not. Consequence: most members render as truncated pubkeys rather than names. This is the single change that would most improve how the member list reads.
  4. Pull-only, no push. TODO(signals) in join_ndo: no remote_signal to other members, so member lists refresh on poll/focus/reload. Same pending upgrade as zome_group.
  5. MemberToNdos is write-only. The reverse index exists purely as the duplicate-join guard. No get_my_joined_ndos coordinator function, so an agent cannot yet query "which NDOs am I participating in" even though the data is there.
  6. Participation is ungated. Anyone holding the cell can join; there is no request/approve flow, no governance rule, no AccountableAgent check. The button was renamed from "Request to join" to "Join this NDO" so the UI stops implying an approval step that does not exist. If gated participation is wanted, that belongs in zome_gouvernance, not here.
  7. No Layer 2 wiring. Joining generates no EconomicEvent, no Commitment, no PPR. Membership is a bare Layer 0 fact today.

Merge order

Stacked on #128 (fix/120-clone-cell-dna-propertiesdev). This PR bases on that branch and should not merge before it. Once #128 lands, either retarget this to dev or merge it through.

The one thing a reviewer should push back on if they disagree

The claim that membership is not an access grant. Everything here follows from it: no read gate, no approval flow, is_ndo_member documented as "is this agent taking part" rather than "may this agent read". It rests on ADR-010 model A, where holding the cloned cell is what confers access. If that premise is wrong, the design is wrong, not just incomplete.

@Soushi888

Copy link
Copy Markdown
Collaborator Author

Review verdict: APPROVE (after rebase onto dev, post-#128)

Reviewed the marginal diff (16 files on top of the #128 tip).

Backend — clean. NdoMembership follows the group DNA conventions end to end: author and timestamp from the action header (not the entry), integrity validates only what is local (role non-empty, max 100 chars), identity existence checked in the coordinator so membership creation never depends on gossip timing. The membership-is-not-an-access-grant invariant is stated in the module docs, the integrity comments, and the tests, so a future reader cannot miss it.

get_ndo_members reads member identity from each link's author, not from a per-link get — the gossip-timing drop that was found and fixed in zome_group (#126) cannot recur here, and the two-agent Sweettest convergence test is the assertion that would catch it.

Tests — round-trip, double-join rejection (no duplicate), unknown-identity rejection, two-agent convergence with consistency waits, plus a browser e2e (both agents join and see each other).

UIjoinNdo/getNdoMembers resolve the clone cell from the anchor with no legacy shared-cell fallback; unresolved identity is a hard NdoNotFoundError, the correct call for a write path.

Suggestion (non-blocking): is_ndo_member fetches the full member list to answer a boolean. The MemberToNdos reverse link on the queried agent would answer in one local round-trip, same shape as the duplicate-join guard in join_ndo. Fine at pilot scale; worth switching before an NDO has hundreds of members.

Merge mechanics: this branch is 1 ahead / 3 behind the #128 tip (the #128 self-review round added commits), hence the DIRTY state. After #128 merges: retarget base to dev, rebase the single commit (watch ndo.service.ts and NdoView.svelte, both touched by #128's review round), then this PR gets its first CI run ever — full matrix from #137.

…cell

Replaces the UI stub with a real DHT-backed membership surface in
zome_resource, so agents can declare participation in an NDO and
enumerate who else is taking part.

Membership is deliberately not an access grant. Under the per-NDO-cell
model (ADR-010 model A) an agent must already hold the cloned NDO cell
to read the NDO at all, so cell possession is what confers access.
NdoMembership only makes participation listable; nothing may treat its
absence as a read denial.

Backend (zome_resource):
- NdoMembership integrity entry { ndo_identity_hash, role }. Author and
  join time come from the action header, matching GroupMembership.
- join_ndo: idempotent (AlreadyMember), verifies the identity exists in
  this cell, creates NdoToMembers + MemberToNdos links.
- get_ndo_members: reads each link's author, never a per-link get. A
  per-link get silently drops peers whose record has not gossiped to
  this shard — the bug already fixed once in zome_group.
- is_ndo_member: participation check only, never a read gate.
- Validation: role, when present, non-empty and <= 100 chars. Identity
  existence stays in the coordinator so creation does not depend on
  gossip timing.

UI (ndo.service.ts, NdoView, MemberList):
- joinNdo / getNdoMembers resolve the NDO clone cell from its anchor
  and call the zome; an unresolved identity is a hard NdoNotFoundError
  rather than a silent degrade.
- joinNdo guards on is_ndo_member so re-joining is a benign no-op.
- Member names resolve via zome_person with a truncated-pubkey fallback
  for agents with no Person entry yet (REQ-UI-ID-03).
- NdoNotImplementedError removed.

Tests:
- Sweettest ndo_membership: round trip, double-join rejection, unknown
  identity rejection, two-agent convergence.
- Playwright multi-agent: both agents join and see each other.

Docs: resource_zome.md, specifications.md, ui_architecture.md updated
from "planned" to implemented; .rules and DOCUMENTATION_INDEX.md record
that await_consistency_20_s takes the array by value.
@Soushi888
Soushi888 force-pushed the feat/ndo-membership branch from 350960f to 71cf7f0 Compare August 16, 2026 00:37
@Soushi888
Soushi888 changed the base branch from fix/120-clone-cell-dna-properties to dev August 16, 2026 00:56
@Soushi888 Soushi888 closed this Aug 16, 2026
@Soushi888 Soushi888 reopened this Aug 16, 2026
@Soushi888
Soushi888 merged commit c0a1089 into dev Aug 16, 2026
7 checks passed
Soushi888 added a commit that referenced this pull request Aug 25, 2026
Resolves both conflicts by keeping #129's membership additions and the
round 1 cell-routing fix together, since neither replaces the other:

- NdoView.svelte: the `membersStubMessage` placeholder is gone (membership
  is real now, so `membersError` carries the failure), and `ndoCellId`
  stays, because every Layer 1 and Layer 2 tab still needs the NDO's own
  clone cell.
- ndo.service.ts: `resolveCellIdForNdo` keeps its interface entry and its
  implementation alongside the real `joinNdo` / `getNdoMembers`, which now
  call `join_ndo`, `is_ndo_member`, and `get_ndo_members` on the ndo cell.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(ndo): NDO membership surface (join, list members, is-member)

1 participant